-
Notifications
You must be signed in to change notification settings - Fork 2
Module Development
- Driver Discovery
- Device Modules
- Service Modules
- Logic Modules
- Utilities and Helpers
- Response Tokenisation
- Logging and Security
- Devices connecting to engine - best practice for hosting services in drivers
- run
rails g module Module/Scope/AndName
- This creates a file:
app/modules/module/scope/and_name.rb
Running the commands above will generate:
module Module; end
module Module::Scope; end
class Module::Scope::AndName
include ::Orchestrator::Constants # On, Off and other useful constants
include ::Orchestrator::Transcoder # binary, hex and string helper methods
def on_load
# module has been started
on_update
end
def on_unload
# module has been stopped
end
def on_update
# Called when class updated at runtime
# or configuration relating to this module has changed (settings etc)
end
end
Drivers are ruby classes that define device protocols. They are used to communicate with and track state of a device or service and make the functions available through a common interface.
A device, in Engine, is an instance of a driver, paired with an IP address or URI. Engine manages the connection and performs the IO.
Drivers can request data to be sent to the endpoint it is communicating with. Metadata is provided with each request that describes the expected behaviour of the device being communicated with. This metadata indicates whether a request should block until a response is received, timeouts, how many times it can be retried before giving up, how many responses can be ignored from the endpoint etc
Engine queues requests so that you can write code that is easy to reason with
For example, controlling a projector from a logic module:
projector = system[:Projector]
if projector.power? do
projector.power On if projector[:power] == Off
projector.switch_to :hdmi
projector.volume 60
end
All public functions will be exposed to both external UI and logic modules except for callbacks like on_load
or received
.
ACA Engine deals with buffering data, sends and receives, and retrying commands that may have failed.
Commands are placed in a priority queue, that buffers what is sent to the device, and each command can define buffering behaviour - delaying sends before the next send or after the last receive for example.
The queue also supports named commands. This prevents sending the same command multiple times - for example consider changing input on a projector, if you requested, in quick succession:
power On
switch_to :hdmi
switch_to :vga
switch_to :display_port
Then you don’t actually want the projector to run through all those commands - you really only want to:
power On
input :display_port
Named commands will overwrite any command in the queue with the same name with the latest command requested. They are also the only commands that are queued when a device is considered offline or disconnected - so when the device comes online it will be moved into the desired state.
By default, one command is sent at a time and the queue is paused until the device responds with the appropriate data. When the device does respond it is up to the received function to decide if
- the command was a success / failure
- if further retries are required
- if retries should be aborted
- if that particular response should be ignored
There are adjustable failsafe timeouts and retry limits that can be defined for each command.
Every driver has access to a high precision scheduler with a range of options.
NOTE::
- Schedules created in a driver are automatically cleaned up when the module is stopped.
- Drivers are pegged to a thread so there is no concurrent or parallel access - you don’t need to worry about locking and synchronization.
You can access the scheduler via the schedule
function - this returns a scheduler object with the following options:
Method | Arguments | Description |
---|---|---|
in | integer or string, block or proc |
schedule.in "2s" { some_task } NOTE: Integer values are in milliseconds |
every | integer or string, block or proc | schedule.every 300 { some_task } |
at | string or date time object | schedule.at "2009-06-15T13:45:30" { some_task } |
cron | string | schedule.cron "0 1 * * *" { some_task } |
Status variables expose data for external consumption. They represent the public state of a module and can be accessed by other modules, user interfaces and the REST API.
It is simple to set a status variable:
self[:status_variable] = 'value'
Status variables can be any object, however they are converted to JSON when transmitted to user interfaces, so hashes, arrays, strings and numbers are the preferred datatypes. It is worth noting that status variables are public (to authenticated users) so it is recommended no sensitive data is stored there.
Before a status variable is changed it is compared with the previous value. If the value is different, when compared with ==
, then an update is triggered and listeners are informed.
If an existing value is modified, such as adding a new entry to an array or hash, this change will not be detected automatically (and you may want to perform multiple operations before transmitting this anyway). Once the update is complete, there are two ways to trigger a change:
- Force an update using
signal_status(:status)
- Duplicate the object and re-apply it
self[:status] = self[:status].dup
(not recommended)