Closed RussNelson closed 10 years ago
hi. i suspect we're looking at this from the wrong level of abstraction. here is what i would do:
% cd steward
% git pull
% cp drivers/lighting-bulb-template.js to steward/devices/devices-lighting/lighting-lifx.js
% cd steward
then edit package.json to include this dependency
, "lifx" : "0.1.3"
then grab the module
% npm -l install
and now start editing
devices/devices-lighting-lifx.js
(you may have already done all this, i just want a "level set")
ok, so what do we do in devices-lighting-lifx.js?
you might as well begin by doing a global replace
s/TBD/LIFX/g
and then changing
var LIFX = require('LIFX')
to
var lifx = require('lifxjs')
then at the bottom of the cline and look at this function
exports.start = function() { … };
this function is invoked when the module is loaded, it does two things: first, it registers the device prototype, and second it starts discovery of bulbs.
all lighting drivers are required to support an rgb model and then are free to support any other models they prefer. it looks like the LIFX folks like HSL, so you want these properties:
, properties : { name : true
, status : [ 'waiting', 'on', 'off' ]
, color : { model: [ { rgb : { r: 'u8', g: 'u8', b: 'u8' } }
, { hue : { hue: 'degrees', saturation: 'percentage' } }
]
}
, brightness : 'percentage'
}
next, let's look at this line which is where discovery is going to take place:
new LIFX().on('discover', function(bulb) { … }
it wants to call the lifx module to ask it to inform it whenever a new bulb is found. when that happens the info structure is filled out and the device is "discovered" by the steward. the key thing to note in this callback is that you're passing a "bulb" object that will subsequently be used by the rest of the file to do things. perhaps the single most important thing is to make sure that the info.device.unit.udn is unique and stable for this bulb. in other words, no other bulb should be able to generate that UDN, and if you restart the steward, the same UDN must be generated for that bulb.
so, just eyeballing the README.js file for the lifts repo, i'd say that you are looking at something that starts like this:
var lx = lifx.init().on('bulb', function(bulb) {
// when creating info, be sure to create both info.lx and info.bulb, we'll need them both!
serialNo = bulb.lifxAddress.toString('hex');
…udn = 'uuid:2f402f80-da50-11e1-9b23-' + serialNo;
}).on('bulbstate', function(bulbstate) {
var bulb, udn;
udn = 'uuid:2f402f80-da50-11e1-9b23-' + bulbstate.bulb.lifxAddress.toString('hex');
if (!devices.devices[udn]) return;
bulb = devices.devices[udn].device;
bulb.update(bulb, bulbstate.state);
}).on('bulbonoff, function(bulbonoff) {
var bulb, udn;
udn = 'uuid:2f402f80-da50-11e1-9b23-' + bulbstate.bulb.lifxAddress.toString('hex');
if (!devices.devices[udn]) return;
bulb = devices.devices[udn].device;
bulb.update(bulb, { power: bulbonoff.on ? 65535 : 0 });
});
now scroll up to the top of the file where it says
var TBD = exports.Device = function(deviceID, deviceUID, info) {
you will want to capture both info.bulb and info.lx as self.bulb an self.lx, and since bulbs don't emit events, but the lifx object does, you will want to get rid of these lines about
// TBD: invoked by the lower-level bulb driver whenever the bulb changes state. You probably
// have to set the name of the event to whatever the bulb driver emits when its state changes.
self.bulb.on('stateChange', function(state) { self.update(self, state); });
self.update(self, self.bulb.state);
self.changed();
instead, we already did the callbacks for this in exports.start() - so what we want to do is to look at
LIFX.prototype.update
and have that update self.status and self.info based on gets passed to it.
next, for the
LIFX.prototype.perform
validate_perform
functions, you will want to handle converting rgb to hsl. look at
lighting-yoctopuce-powercolor.js
to see how that is done.
finally, look for all calls of the form
self.bulb.XYZ(...)
in
LIFX.prototype.perform
since self.bulb isn't an object, you'll be changing those to self.lifx.XYZ(…)
this may seem a bit daunting, but it's pretty straight-forward. after you've done two of these, you can bang out subsequent drivers very quickly
good luck!
Looks like a foo.on() call returns foo, hence the chained on calls. Could just as reasonably be
var lx = lifx.init() lx.on(...); lx.on(...); lx.on(...);
correct?
correct. the node.js community appears mixed in the usage. i believe the old school likes the chaining, and i confess i do as well...
It is daunting. I'm definitely hurting from not having a "big picture" view of how the driver should work. It seems like the driver contains several components. It has start() code which starts the module up by setting a global variable which is a hash containing two parts: $info (what does the dollar sign do??) which has a description of the properties that the device has (how do I know which ones can be be changed?), and $validate, which seems to be a subroutine which checks the parameters that are supplied on a perform for validity (is this where you reject the setting of parameters that can't be changed?) It also registers callback with the device driver, or if none such exists, it starts a timer-driven subroutine that polls.
When a device is discovered, a hash is filled out with information about the new device, and later passed to devices.discover(). That seems to create a device entry, and then (sometimes? always?) calls the module's exports.Device() function with the same info it just got passed. This seems like a needless complexity -- why not just do those same functions as the device is discovered?
So, every device in TSS has to have an exports.start(), an exports.Device(), but I'm not sure when exports.Device() gets called, or why it even needs to exist, since it looks like it just gets called by devices.discover().
These two lines: broker.subscribe('actors', function(request, taskID, actor, perform, parameter) { if (actor !== ('device/' + self.deviceID)) return; imply that it's called on every perform request. I guess the idea is that every device can spy on what every other device is being asked to perform.
yes, although i'm thinking is is more for embedded apprentices than actual devices...
It is daunting. I'm definitely hurting from not having a "big picture" view of how the driver
should work. It seems like the driver contains several components. It has start() code which
starts the module up by setting a global variable which is a hash containing two parts: $info
(what does the dollar sign do??) which has a description of the properties that the device has
(how do I know which ones can be be changed?), and $validate, which seems to be a subroutine
which checks the parameters that are supplied on a perform for validity (is this where you
reject the setting of parameters that can't be changed?) It also registers callback with the
device driver, or if none such exists, it starts a timer-driven subroutine that polls.
there is no signifiance to the '$' other than i'm tried to avoid naming collisions when the next generation of curators takes over. at the present time, we don't bother specifying which variables are read-only/read-write/write-only. if it appears in the property list, it's readable, if you care to manage in the perform routines, then it's writable, and there is no such things write-only
the layout of a driver is simple:
the thing to keep in mind is that there is a very large range of device-specific models and that the steward has to accomodate them all. that is why you may see things that look a little redundant at times, simply because the plumbing between the device-specific stuff and the device-generic stuff demands it.
When a device is discovered, a hash is filled out with information about the new device, and
later passed to devices.discover(). That seems to create a device entry, and then (sometimes?
always?) calls the module's exports.Device() function with the same info it just got passed.
This seems like a needless complexity -- why not just do those same functions as the device is
discovered?
Because device.discover does things like determining whether the device has been seen before by the steward, so it can use the same deviceID as before. Otherwise, it has to create a database entry. Either way it has to update things in that entry.
So, every device in TSS has to have an exports.start(), an exports.Device(), but I'm not sure
when exports.Device() gets called, or why it even needs to exist, since it looks like it just
gets called by devices.discover().
some devices inherit properties from other devices, hence the need to export the javascript construtcor.
Gotcha, thanks.
Is there a standard for how internal device state should be stored? I notice that the dev structure has a discovery hash. Should that be only the things reported in the initial discovery? Should I store the current state in a separate subhash in the dev?