-
Notifications
You must be signed in to change notification settings - Fork 2
Device
Device provides a DSL for describing devices of all kinds. Anything that can be controlled by a computer--whether by IR, serial, ethernet, or laser pulse--can be described by this DSL. Furthermore, in order to be part of the cmdr system, a device must be implemented as a Device. New devices are created by either subclassing Device directly or by subclassing one of its child classes, like RS232Device, Projector, or VideoSwitcher. If there exists a class like Projector which describes the category of devices that your device is pat of you should subclass it instead of Device directly. Doing so will give your projector the same basic interface as every other, making it easy to exchange for another. With Device, all of the details of communicating with the outside world and updating the database are taken care of for you, letting you focus on just implementing the device's features.
##Device Variables
The basis for this DSL is the concept of state variables, defined by the
::state_var method. State vars are–as
their name suggests–variables that track the state of something about
the device. For example, a projector device might have have a state var
“power,” which is true if the projector is on and false if the projector
is off. There are two kinds of state vars: those that are immutable
(e.g., the model number of the projector) and those that are mutable
(e.g., the aforementioned power state var). Mutability is specified when
the state var is created by the :editable parameter, which defaults to
true. Note that even an immutable state var can be changed
programatically by calling its device.varname=
method, but controls
for it will not be created in the web interface. For mutable vars you
are responsible for creating a method called set_varname, which takes
in one argument of the type specified and which should affect the device
in such a way as to transition it to the state described by the
argument. Alternatively, by passing a Proc to the :action parameter,
::state_var will create the set
method for you. State vars can be created by placing a line like this in
your class body:
state_var power, :type => :boolean
The type field is mandatory, but there are various other optional parameters which are described in the {Device::state_var} method definition.
However, not every action on a device fits into the state var paradigm, though you should try to use it if possible. For example, a camera may have a “zoom in” and “zoom out” feature, but no way to set the zoom level directly. In these cases, a command can be used instead. Calling a command sends a message to the device class but does not include any state information. To declare a command, the syntax below should be used:
command zoom_in
In addition, you should create a method with the same name as the command (zoom_in in this case) which does the actual work of sending the command to the device. Command also has many options, which can be found in the {Device::command} method definition.
Rounding out the trio of variable types, we have virtual_var. Virtual var is in some ways the opposite of command: instead of providing only control, it provides only information. More importantly, it is not set directly, but is computed from one or more other variables. The purpose of this is primarily to provide useful information for the web interface to display. For example, a projector may report the number of hours a lamp has been in use as well as the percentage of the lamp’s life that is gone. However, the more useful metric for somebody evaluating when the lamp needs to be replaced is the number of hours that are left before the lamp dies. We can use simple algebra and a virtual var to compute this information:
virtual_var :lamp_remaining, :type => :string, :depends_on => [:lamp_hours, :percent_lamp_used], :transformation => proc {
"#{((lamp_hours/percent_lamp_used - lamp_hours)/(60*60.0)).round(1)} hours"
}
Virtual vars are updated whenever the variables they depend on (which can be either state vars or other virtual vars) are updated.
##Configuration Configuration is defined by a configure block, like this, from RS232Device:
configure do
port :type => :port
baud :type => :integer, :default => 9600
data_bits 8
stop_bits 1
parity 0
message_end "rn"
end
Here we can see two kind of configuration statements: system defined and user defined. The first two, port and baud, are user defined, while the rest are system defined. User defined config vars are intended to be set by the user in the web interface. The type parameter is a hint to the web interface about what kind of control to show and what kind of validation to do on the input. For example, setting a type of :port will display a drop-down of the serial ports defined for the system. A type of :integer will display a text box whose input is restricted to numbers. Other possibilities are :password, :string, :decimal, :boolean and :percentage. If you supply a type that is not defined in the system, a simple text box will be used. An optional :default parameter can be used to set the initial value.
System defined config vars, on the other hand, are not modifiable by the user. They are specified in the device definition (as can be seen here) and cannot change. You can add whatever configuration variables you need, though they should be named using lowercase letters connected_by_underscores. Configuration information is accessible through the {Device#configuration} method, which returns a hash mapping between a symbol of the name to the value. More information in the {Device::configure} method definition.
##Controlling Devices
Devices are controlled externally through the use of a message queue which speaks the AMQP protocol. At the moment we use RabbitMQ as the message broker, but technically everything should work with another broker and RabbitMQ should not be assumed. AMQP is a very complicated protocol which can support several different messaging strategies. The only one used here is the simplest: direct messaging, wherein there is one sender and one recipient. AMQP messages travel over “queues,” which are named structures that carry messages in one direction. Each device has a queue, named cmdr:dqueue:{name} (replace {name} with the actual name of the device), which it watches for messages. Messages are JSON documents with at least three pieces of information: a unique ID (GUIDs are recommended to ensure uniqueness), a response queue which the sender is watching, and a type. The device will carry out the instructions in the message and send the response as a json message to the queue specified.
###Messages There are three kinds of messages one can send to a device:
####state_get To get information about the current state of a variable, send a state_get message, which looks like the following:
!!!json
{
id: "DD2297B4-6982-4804-976A-AEA868564DF3",
queue: "cmdr:http"
type: "state_get",
var: "input"
}
A state_get message returns a response like the following:
!!!json
{
id: "DD2297B4-6982-4804-976A-AEA868564DF3",
result: 5
}
####state_set To set a ::state_var, send a state_set message
!!!json
{
id: "D62F993B-E036-417C-948B-FEA389480984",
queue: "cmdr:websocket"
type: "state_set",
var: "input",
value: 4
}
The response will look like this:
!!!json
{
"id": "FF00F317-108C-41BD-90CB-388F4419B9A1",
"result": true
}
####command To send a command to a device, use this:
!!!json
{
id: "FF00F317-108C-41BD-90CB-388F4419B9A1",
queue: "cmdr:http"
type: "command",
method: "power",
args: [true]
}
Which will produce a response like:
!!!json
{
"id": "FF00F317-108C-41BD-90CB-388F4419B9A1",
"result": "power=true"
}
Any of these calls can also produce an error. Error responses look like this:
!!!json
{
"id": "FF00F317-108C-41BD-90CB-388F4419B9A1",
"error": "Failed to turn projector on"
}
This and the rest of the documentation here should cover everything you need to know to write Device subclasses. More information can also be found in the tests, which define exactly what the device class must do and not do.