To add and run a plugin, place the plugin's entire directory in /src/server/plugins
, and (re)start the server.
RotorHazard makes use of externally loaded plugins to extend its functionality and behavior. Plugins are distributed as a directory (folder) containing an __init__.py
file and potentially other files. Plugins are loaded during server startup. A line is added to the log for each plugin as it is found and imported; refer to the log to ensure plugins are being loaded as expected.
If you have issues with a plugin, contact its developer to ensure compatibility with the version of RotorHazard you are running.
At minimum, a plugin must contain an initialize()
function within its __init__.py
file. A plugin may assign functions to standard events or register handlers to various hooks within the system to run its code.
RotorHazard calls a plugin's initialize()
function early during server startup. This function should not be used to add behaviors directly, but to register handlers where behavior will be called. As the only argument to initialize
, RotorHazard provides the timer's API for working with data and frontend UI; see RHAPI.
For example, a plugin might register events to be run at startup like this:
from eventmanager import Evt
def initialize(rhapi):
rhapi.events.on(Evt.STARTUP, my_startup_function)
RHAPI
provides a wide range of properties and methods across RotorHazard's internal systems. Using RHAPI, one can manipulate nearly every facet of a race event and RotorHazard's behavior.
Events are triggered by the timer as important actions take place—for example, when a frequency is set, a pilot is added, or a race begins. When an event is triggered, all registered handlers are run. Events may pass arguments containing useful data such as the node number, pilot callsign, or race object.
Interfacing with Events is provided via RHAPI.
Points Methods are functions that assign point values to pilot results when a race is completed. If a user assigns a points method to a race format, points will be displayed on the race and heat summary leaderboards. Points may also be used and displayed by Class Ranking Methods.
Points Methods must be registered to be available in the UI. Access to registration is provided though the register_fn
argument of the Evt.POINTS_INITIALIZE
event. Pass a RacePointsMethod
object to this method to register it.
For example, a points method might be registered with the following functions:
from eventmanager import Evt
from Results import RacePointsMethod
def my_points_method_fn(rhapi, leaderboard, args):
...
def register_handlers(args):
args['register_fn'](
RacePointsMethod("My Points Method", my_points_method_fn)
)
def initialize(rhapi):
rhapi.events.on(Evt.POINTS_INITIALIZE, register_handlers)
Provides metadata and function linkage for points methods.
label
(string): user-facing text that appears in the RotorHazard frontend interfaceassign_fn
(function): function to run when points are calculated for a racedefault_args
optional (dict): arguments passed to theassign_fn
when run, unless overridden by local argumentssettings
optional (list[UIField]): A list of paramters to provide to the user; see UI Fieldsname
optional (string): internal identifier (auto-generated fromlabel
if not provided)
The assignFn
receives as arguments:
rhapi
(RHAPI): the RHAPI classleaderboard
(dict): full race leaderboardargs
(dict): collated default and locally-provided arguments
assign_fn
must return a modified leaderboard dict where the "primary leaderboard" includes a points
key with appropriate values assigned. The "primary leaderboard" is a dict at the root level of the full leaderboard, and will be identified in the meta
dict with the primary_leaderboard
key.
Class Ranking Methods are functions that output custom leaderboards to class results after races are completed. If a user assigns a ranking method to a class, the cooresponding leaderboard will be displayed as a "Class Ranking" panel.
Class Ranking Methods must be registered to be available in the UI. Access to registration is provided though the register_fn
argument of the Evt.CLASS_RANK_INITIALIZE
event. Pass a RaceClassRankMethod
object to this method to register it.
For example, a class rank might be registered with the following functions:
from eventmanager import Evt
from Results import RaceClassRankMethod
def my_class_rank_fn(rhapi, race_class, args):
...
def register_handlers(args):
args['register_fn'](
RaceClassRankMethod("My Class Ranking", my_class_rank_fn)
)
def initialize(rhapi):
rhapi.events.on(Evt.CLASS_RANK_INITIALIZE, register_handlers)
Provides metadata and function linkage for points methods.
label
(string): user-facing text that appears in the RotorHazard frontend interfacerank_fn
(function): function to run when class leaderboards are calculateddefault_args
optional (dict): arguments passed to therank_fn
when run, unless overridden by local argumentssettings
optional (list[UIField]): A list of paramters to provide to the user; see UI Fieldsname
optional (string): internal identifier (auto-generated fromlabel
if not provided)
The rank_fn
receives as arguments:
rhapi
(RHAPI): the RHAPI classrace_class
(dict): currentRaceClass
objectargs
(dict): collated default and locally-provided arguments
rank_fn
must return a tuple of (leaderboard
, meta
).
leaderboard
is a table—functionally, a list of dicts—that should be ordered by ranking. The "row" dicts must contain:
position
(string): The rank for this row; not required to be unique or numericpilot_id
(int): ID value for the pilot in this rowcallsign
(string): callsing for the pilot in this row
The row dicts may additionally contain any other keys the author deems necessary, but all rows in the table must maintain the same structure.
meta
is a dict with the following format:
method_label
optional (string): User-facing rank title (for Results page)rank_fields
(list): A list of dicts with the following format:name
(string): the key of the field inleaderboard
with data to displaylabel
(string): user-facing text used as column header in the ranking table
When displayed on the front-end, only position
, callsign
and fields listed in rank_fields
will be displayed in the ranking table.
Heat Generators are functions that return a list of heats which are fed into a race class. When a user runs a generator, they choose a source for seeding and feed the results into an existing class or create a new class.
HeatGenerators must be registered to be available in the UI. Access to registration is provided though the register_fn
argument of the Evt.HEAT_GENERATOR_INITIALIZE
event. Pass a HeatGenerator
object to this method to register it.
For example, a heat generator might be registered with the following functions:
from eventmanager import Evt
from HeatGenerator import HeatGenerator, HeatPlan, HeatPlanSlot, SeedMethod
def my_heat_generator_fn(rhapi, args):
...
def register_handlers(args):
args['register_fn'](
HeatGenerator("My Generator", my_heat_generator_fn)
)
def initialize(rhapi):
rhapi.events.on(Evt.HEAT_GENERATOR_INITIALIZE, register_handlers)
label
(string): user-facing text that appears in the RotorHazard frontend interfacegenerator_fn
(function): function to run when generator is invokeddefault_args
optional (dict): arguments passed to thegenerator_fn
when run, unless overridden by local argumentssettings
optional (list[UIField]): A list of paramters to provide to the user; see UI Fieldsname
optional (string): internal identifier (auto-generated fromlabel
if not provided)
The generator_fn
receives as arguments:
rhapi
(RHAPI): the RHAPI classargs
(dict): collated default and locally-provided arguments
args
will include, at minimum:
input_class
(int): id of race class intended to be used for seedingoutput_class
(int or None): id of race class where generated heats will be outputavailable_seats
(int): number of seats currently available to assign pilots into (seats with an active frequency assignment)
Your generator_fn
must return a list of HeatPlan
s (or None
).
A HeatPlan
object uses the following format:
name
(string): Name to be applied to this heatslots
(list[HeatPlanSlot]): A list ofHeatPlanSlot
s
A HeatPlanSlot
object uses the following format:
- method
(SeedMethod): Method used for seeding
- seed_rank
(int): Rank to seed from
- seed_index
optional (int): Index of heat within the plan list to seed from, when method
is HEAT_INDEX
or CLASS_INDEX
Heat slots can be seeded either directly from
SeedMethod
(imported from HeatGenerator
)
INPUT
: The slot is seeded from theseed_rank
position in the input class rankingHEAT_INDEX
: The slot is seeded from theseed_rank
position in the heat specified byseed_index
CLASS_INDEX
: The slot is seeded from theseed_rank
position in the class specified byseed_index
The following heat plan is a double-advance ladder. Pilots ranked 3rd through 6th from the input class are seeded into the first heat. Then, the 1st and 2nd place from that heat advance to the second heat where they join the 1st and 2nd place from the input class.
[
HeatPlan(
"B Main",
[
HeatPlanSlot(SeedMethod.INPUT, 3),
HeatPlanSlot(SeedMethod.INPUT, 4),
HeatPlanSlot(SeedMethod.INPUT, 5),
HeatPlanSlot(SeedMethod.INPUT, 6)
]
),
HeatPlan(
"A Main",
[
HeatPlanSlot(SeedMethod.HEAT_INDEX, 1, 0),
HeatPlanSlot(SeedMethod.HEAT_INDEX, 2, 0),
HeatPlanSlot(SeedMethod.INPUT, 1),
HeatPlanSlot(SeedMethod.INPUT, 2)
]
)
]
Actions are behaviors assigned to events by users from the server's UI. Action effects are assigned to and triggered by the event a user has configured within an action. All parameters of the selected event in the action become available to the effect.
Effects must be registered to be available in the UI. Access to registration is provided though the register_fn
argument of the Evt.ACTIONS_INITIALIZE
event. Pass an ActionEffect
object to this method to register it.
For example, an effect might be registered with the following functions:
from eventmanager import Evt
from EventActions import ActionEffect
def my_actions_fn(action, args):
...
def register_handlers(args):
args['register_fn'](
ActionEffect("My Action", my_actions_fn)
)
def initialize(rhapi):
rhapi.events.on(Evt.ACTIONS_INITIALIZE, register_handlers)
Provides metadata and function linkage for action effects.
label
(string): user-facing text that appears in the RotorHazard frontend interfaceeffect_fn
(function): function to run when this effect is triggeredfields
optional (list[UIField]): A list of paramters to provide to the user; see UI Fieldsname
optional (string): internal identifier (auto-generated fromlabel
if not provided)
Example:
ActionEffect(
'udpmessage',
'UDP Message',
UDP_message_effect,
[
UIField('text', "UDP message", UIFieldType.TEXT),
UIField('ipaddress', "UDP IP Address", UIFieldType.TEXT),
UIField('udpport', "UDP Port", UIFieldType.TEXT),
]
)
LED Effects are colors and patterns that may be displayed by an LED strip (or panel) attached to the server. Effects are assigned by users in the server's UI, and triggered by the server when appropriate. Most effects are triggered by standard events, but some (such as the "idle" states) are unique to the LED system and not broadcast elsewhere. All parameters of the event become available to the LED Effect.
Effects must be registered to be available in the UI. Access to registration is provided though the register_fn
argument of the Evt.LED_INITIALIZE
event. Pass an LEDEffect
object to this method to register it.
For example, an LED effect might be registered with the following functions:
from eventmanager import Evt
from led_event_manager import LEDEffect, effect_delay
def my_led_effect(args):
...
def register_handlers(args):
args['register_fn'](
LEDEffect("Image: RotorHazard", my_led_effect, {}),
)
def initialize(rhapi):
rhapi.events.on(Evt.LED_INITIALIZE, register_handlers)
Effects run as IDLE do not clear the display when they complete.
LED effects which contain animation require execution delays. Effects MUST use the provided effect_delay
for this purpose (see below).
Caution
Using time.sleep
, gevent.sleep
, or other methods for execution delays in LED effects will prevent proper effect termination and cause visual issues on the LED display or other erratic behavior. See effect_delay()
Provides metadata and function linkage for LED effects.
Often, color
will be passed through as an argument, which is an RGB hexadecimal code that can be used to modify the effect's output as appropriate. For example, during the RACE_LAP_RECORDED
event, color is often determined by the pilot that completed the lap.
label
(string): user-facing text that appears in the RotorHazard frontend interfacehandler_fn
(function): function to run when this effect is triggeredvalid_events
(list): controls whether events can be assigned to various eventsdefault_args
optional (dict): provides default arguments for the handler. These arguments will be overwritten if theEvent
provides arguments with the same keys.name
optional (string): internal identifier (auto-generated fromlabel
if not provided)
By default, an LED effect will be available to all events that can produce LED output except LEDEvent.IDLE_DONE, LEDEvent.IDLE_RACING, and LEDEvent.IDLE_READY. This can be modified with valid_events
. It should contain a dict with the following optional keys. Each value should be a list of event identifiers.
exclude
(list): this effect will never be available for events specified here. As a special case,Evt.ALL
will remove this effect from all events except those specifically included.include
(list): this effect will always be available for events specified here unless specifically excluded.recommended
(list): effects in this list will receive priority ordering and visibility in the effect selection UI, at the top of the list, with an asterisk.Evt.ALL
may be used here.
Normally when an LED effect's handler function completes, the display system will look for a time
argument and wait this many seconds before switching to an appropriate idle state. You can prevent switching to idle with the preventIdle
argument, but usually it is more appropriate to set a reasonable time
.
A list of standard and LED-specific events that will accept and trigger effects can be found in src/server/led_event_manager.py
.
Example:
LEDEffect(
"Image: RotorHazard",
show_bitmap,
{
'recommended': [Evt.STARTUP]
},
{
'bitmaps':
[
{
"image": "static/image/LEDpanel-16x16-RotorHazard.png",
"delay": 0
}
],
'time': 60
}
)
Delay execution of LED effect code, similar to time.sleep()
. Works asynchronously so main processes continue and provides for clean effect termination when new LED effects are run.
ms
(int|float): number of milliseconds to delayargs
(dict): args passed to the LEDEffect
Exporters provide formatting of event data so it may be saved or sent elsewhere. A user may select and run an exporter from the UI, and will be provided with its contents in a file. Plugins may also trigger exports for their own purposes.
Exporters must be registered before use. Access to registration is provided though the register_fn
argument of the Evt.DATA_EXPORT_INITIALIZE
event. Pass a DataExporter
object to this method to register it.
For example, an exporter might be registered with the following functions:
from eventmanager import Evt
from data_export import DataExporter
def my_formatter_fn(data):
...
def my_assembler_fn(rhapi):
...
def register_handlers(args):
args['register_fn'](
DataExporter(
"My Exporter",
my_formatter_function,
my_assembler_function
)
)
def initialize(rhapi):
rhapi.events.on(Evt.DATA_EXPORT_INITIALIZE, register_handlers)
Provides metadata and function linkage for exporters.
Exporters are run in two stages. First, the assembler pulls the data needed, then passes it to the formatter. In this way, a variety of assemblers can share a formatter, such as assembling pilot data, heat data, or race data and then passing it to be formatted as CSV or JSON.
label
(string): user-facing text that appears in the RotorHazard frontend interfaceformatter_fn
(function): function to run for formatting stageassembler_fn
(function): function to run for assembly stagename
optional (string): internal identifier (auto-generated fromlabel
if not provided)
The assembler_fn
receives rhapi
as an argument so that it may access and prepare timer data as needed.
The formatter_fn
receives the output of the assembler_fn
.
Importers accept data and process it so that it can be added to the RH database. A user may select and run an importer from the UI, with a file selector as input. Plugins may also trigger imports for their own purposes.
Importers must be registered before use. Access to registration is provided though the register_fn
argument of the Evt.DATA_IMPORT_INITIALIZE
event. Pass a DataImporter
object to this method to register it.
For example, an importer might be registered with the following functions:
from eventmanager import Evt
from data_import import DataImporter
def my_import_fn(rhapi, data, args):
...
def register_handlers(args):
args['register_fn'](
DataImporter(
"My Importer",
my_import_fn,
),
)
def initialize(rhapi):
rhapi.events.on(Evt.DATA_IMPORT_INITIALIZE, register_handlers)
Provides metadata and function linkage for importers.
When an importer is run, the run_import
method is called, which collates default and locally-provided arguments, then calls the import_fn
.
label
(string): user-facing text that appears in the RotorHazard frontend interfaceimport_fn
(function): function to run for formatting stagedefault_args
optional (dict): arguments passed to theimport_fn
when run, unless overridden by local argumentssettings
optional (list[UIField]): A list of paramters to provide to the user; see UI Fieldsname
optional (string): internal identifier (auto-generated fromlabel
if not provided)
The import_fn
receives as arguments:
rhapi
(RHAPI): the RHAPI classdata
(any): data to import, provided by the userargs
(dict): collated default and locally-provided arguments
An RHUI.UIField
object defines a frontend user interface for collecting data. It is defined in the following format:
name
(string): internal identifier for this parameterlabel
(string): text that appears in the RotorHazard frontend interfacefield_type
(UIFieldType): One ofUIFieldType.TEXT
,UIFieldType.BASIC_INT
,UIFieldType.SELECT
, orUIFieldType.CHECKBOX
value
optional (any): Default value for fielddesc
optional (string): additional user-facing text that appears in the RotorHazard frontend interface describing notes or special instructions for useprivate
optional (boolean): Prevent automatically generated UI
If field_type
is TEXT
placeholder
optional (string): Text displayed when no value is present
If field_type
is BASIC_INT
placeholder
optional (string): Text displayed when no value is present
If field_type
is CHECKBOX
value
is boolean and no longer optional
If field_type
is SELECT
options
(list[UIFieldSelectOption]): a list ofUIFieldSelectOption
objects with the following properties:value
(string): internal identifier used when this option is selectedlabel
(string): user-facing text that appears in the RotorHazard frontend interface
value
is no longer optional and must match thevalue
of an item inoptions
.
Import UI Fields objects from RHUI.
from RHUI import UIField, UIFieldType, UIFieldSelectOption
Plugin authors are strongly encouraged to declare metadata. In your plugin folder, create the JSON-formatted file manifest.json
with the following keys. Keys may be omitted or null
.
name
: The name of your pluginauthor
: The plugin author's nameauthor_uri
: valid HTTP link to the author's websitedescription
: short description of the plugin's functioninfo_uri
: valid HTTP link to a website about the pluginlicense
: name of the plugin's licenselicense_uri
: valid HTTP link to the plugin's license informationversion
: a version identifier for the plugin's own coderequired_rhapi_version
: the minimum RHAPI version required to run the plugin, such as "1.1"update_uri
: (not yet implemented)text_domain
: (not yet implemented)