TheThingSystem / steward

The Thing System is a set of software components and network protocols that aims to fix the Internet of Things. Our steward software is written in node.js making it both portable and easily extensible. It can run on your laptop, or fit onto a small single board computer like the Raspberry Pi.
http://thethingsystem.com
Other
346 stars 81 forks source link

How should the 'dev' structure be structured? #130

Closed RussNelson closed 10 years ago

RussNelson commented 10 years ago

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?

mrose17 commented 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 }); 
});
  1. 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!

RussNelson commented 10 years ago

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?

mrose17 commented 10 years ago

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...

RussNelson commented 10 years ago

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().

RussNelson commented 10 years ago

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.

mrose17 commented 10 years ago

yes, although i'm thinking is is more for embedded apprentices than actual devices...

mrose17 commented 10 years ago
    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.

RussNelson commented 10 years ago

Gotcha, thanks.