Writing Drivers - advancedcontrol/engine GitHub Wiki


Generating Modules / Drivers

  1. run rails g module Module/Scope/AndName
  2. 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
    def on_unload
        # module has been stopped
    def on_update
        # Called when class updated at runtime
        # or configuration relating to this module has changed (settings etc)

How Drivers Work

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

All public functions will be exposed to both external UI and logic modules except for callbacks like on_load or received.

Data Flow

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:

  1. power On
  2. switch_to :hdmi
  3. switch_to :vga
  4. switch_to :display_port

Then you don’t actually want the projector to run through all those commands - you really only want to:

  1. power On
  2. 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.

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 should be aborted or if that particular response should be ignored. There are adjustable failsafe timeouts and retry limits that can be defined for each command.

Scheduled Tasks

Every driver has access to a high precision scheduler with a range of options.


  • 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

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.

Status change detection

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:

  1. Duplicate the object and re-apply it self[:status] = self[:status].dup
  2. Force an update using signal_status(:status)

Further Reading

Edge Cases