Whilst developing the hah project I noticed there was an architectural component that would simplify the interactions between xAP compliant devices and daemons. I call this the plug-board.
Consider the following use cases:
It's possible to cascade rules in the plug-board. It's also possible to code circular dependency which the script-er must be careful to avoid.
In this model we want to decouple the producers from the consumers and use a scripting intermediary, the plug-board, to perform an action. With N producers and N consumers we have N*N ways of plugging these together making a very flexible and expandable system. Consumers can also be thought of as SERVICE providers as they accept an xAP message and perform something useful.
Example of how the script engine is used to interpret tweet command to turn on a relay.
Examples of daemons that currently conform to this model are:
xap-pachube does not currently fit this PRODUCER/CONSUMER model it watches for events and manipulates the message and sends these to pachube, once this rule engine is in place it will need to be altered to conform. It should provide a SERVICE (consumer) such that things can be logged.
In this model the following would occur:
The scripting language I'm proposing to embed is LUA, although I'm not familiar with the language it seems to have the most documentation, support and its compiled size makes it ideal for use in the embedded world.
The scripting language will be invoked from inside a C wrapper that provides all the heavy lifting to and from the network of the xAP messages along with its parsing. This means the scripts will only need to deal with the logic to determine if a new message is to be sent or not so they should be pretty simplistic.
From the use cases there are two modes in which the scripts need to operate
Scripts will want to maintain state information across activations.
A cache of historical event data for all messages that have been seen on the bus will be available to the scripts. This historical data will be augmented with the date/time that it was received/sent. As memory on our HAH is tightly bounded we will only keep the last message for a source/class type. A limited number of messages will be cached once full the last recorded will be dropped (LIFO - last in first out).
The LUA scripts will consist of the following functions that will be called by the C wrapper
init - Should a script register a regular expression to represent what source patterns it's interested in and let the C wrapper perform the filtering, or should each script be responsible for performing a getSource(), getClass() etc.. and figuring out if it wants to continue?
function init() register_source('dbzoo.livebox.CurrentCost:>') register_class('xAPBSC.event') end
or something more like
function onmessage() if not xap_compare(getSource(), 'dbzoo.livebox.CurrentCost:>') return if not xap_compare(getClass(), 'xAPBSC.event') return end
The merits of the register approach are a much cleaner script design, but perhaps at the expense of some control. Perhaps both can be supported, by not registering any interest we fallback naturally to the 2nd approach anyway.
Pushing this register idea further we can supply a dispatch function to handle the message type and simplify the scripting logic.
function init() register_source('dbzoo.livebox.CurrentCost:>', docurrentcost) register_class('xAPBSC.event', doevent) end function docurrentcost() end function doevent() end
Now the onmessage function proposed earlier can be decomposed to
register_source('*',onmessage)
timer - The timer event allows the script to be called after a preset amount of time has elapsed. This is useful for use cases such as “auto shutoff after 1hr of the on command and no off received in that time.”
function init() register_target('dbzoo.livebox.Controller:relay.1', "oncmd") end function oncmd() if "on command" then start_timer("t1", 3600, "ontimer" ) if "off command" then stop_timer("t1") end function ontimer() send "off command" end
or “No activity for 2 hours do something”,
function init() register_source("dbzoo.livebox.Controller:input.1", onevent) start_timer("t1",2*60*60, "ontimer") end function onevent() -- Reset on any event from this source. start_timer("t1", 2*60*60, "ontimer" ) end function ontimer() send "tweet No activity for 2hrs?" end
A total of 10 C functions and 1 global table is exposed for LUA script usage
The /etc/xap-livebox.ini file controls configurable parameters for this daemon. The only options are whether this daemon starts automatically and the script directory location.
[plugboard] enable=1 scriptdir=/etc/plugboard
register_source(filter, function)register_target(filter, function)register_class(filter, function)The register set of functions act as filters only handing message to the registered callbacks when something is of interest. This saves the scripts from having to perform these tests. Currently the callbacks are registered as string type rather than functions, support for both may occur in future release this is only done for coding simplicity.
function init() register_source('dbzoo.livebox.Controller:relay.1', 'dosource') register_target('dbzoo.livebox.Controller:relay.1', 'dotarget') register_class('xAPBSC.cmd', 'doclass') end
The target will be present whenever a command is directed at an xAP device, this allows a script to intercept an action. The source and class attributes are present in all messages.
The only 3 items that are of general interest in the xap-header are the source, target and class. These variables always contains the values of the current message being processed.
xAPMsg.sourcexAPMsg.targetxAPMsg.class
To extract data from a xAP message body the function xapmsg_getvalue(section, name) is provided. This function has 2 string parameters. It will search the current message buffer and extract the value based upon these parameters.
Consider the following inbound message
xAP-header
{
v=12
hop=1
uid=FF00DB01
class=xAPBSC.info
source=dbzoo.livebox.Controller:relay.1
}
output.state
{
state=on
}
Example function usage:
-- Report when any relay changes state function init() register_source('dbzoo.livebox.Controller:relay.*', "dosource") end function dosource() if xAPMsg.class == "xAPBSC.event" then state = xapmsg_getvalue("output.state","state") relay = string.gsub(xAPMsg.source,".*(%d)","%1") print(string.format('Relay %s is %s', relay, state)) end end
When ran this script will print 'Relay 1 is on'
A Cache of previous xAP messages is made available to the scripts. These functions are used to locate a previous message that has been transmitted.
entry = xapcache_find ( source, target, class )xapcache_getvalue ( entry, section, name )xapcache_gettime( entry ) - time cache event was loaded (seconds past epoch)
The SOURCE and TARGET are optional parameters and may be left as the empty string (””), generally you supply one or the other. The xapcache_getvalue works identically to it xapmsg_getvalue counterpart except on a cache entry.
-- Examining and using cache entries -- Demonstration of variable persistence across invocations function init() register_source('dbzoo.livebox.Controller:relay.1', "dosource") state="?" end function recent_cache(source) local cache = xapcache_find(source,"","xAPBSC.event") -- otherwise fallback to an .info cache entry. if cache == nil then cache = xapcache_find(source,"","xAPBSC.info") end return cache end function dosource() local i if xAPMsg.class == "xAPBSC.event" then print("Previous state: " .. state) state = xapmsg_getvalue("output.state","state") print("Relay 1 state: " .. state) for i=2,4 do local cache = recent_cache("dbzoo.livebox.Controller:relay." .. i) local cachetime = xapcache_gettime(cache) local secsold = -1 if cachetime > 0 then secsold = os.difftime(os.time(), cachetime) end local cachestate = xapcache_getvalue(cache,"output.state","state") print("Relay " ..i.. " state: " ..cachestate.." age: "..secsold.." secs") end end end
Sample output:
Previous state: on Relay 1 state: off Relay 2 state: off age: 23 secs Relay 3 state: off age: 18 secs Relay 4 state: off age: 4 secs
The function xap_send is used to send xAP command messages. The xAP messages that you submit to xap_send are in short form. The xap-header need only specify a target= and class= the remainder of the values required to make the header valid will be automatically populated. If you supply any other xap-header attribute such as source= it will be overridden.
-- Keep relay 2 in sync with relay 1's state. function init() register_source("dbzoo.livebox.Controller:relay.1", "dorelay") end function cmd(relay) local state = xapmsg_getvalue("output.state","state") xap_send(string.format("xap-header\ {\ target=dbzoo.livebox.Controller:relay.%s\ class=xAPBSC.cmd\ }\ output.state.1\ {\ id=*\ state=%s\ }", relay, state)) end function dorelay() if xAPMsg.class == "xAPBSC.event" then cmd(2) end end
There are two functions that deal with timers
start_timer ( name, seconds, function )stop_timer ( name )
Timers are named so that many timers can co-exist in the same script. If the start_timer function is called while the timer is still active, that is it still has time remaining before firing, it will simply reset.
Simplistic example to demonstrate start_timer
function init() start_timer("t1", 10, "dotimer") end function dotimer() print "Timer expired" end
This example demonstrates how their usage can be used to perform an action automatically after a period of time has elapsed.
-- Auto turn relay 1 off after 10 seconds of the ON event function init() register_source("dbzoo.livebox.Controller:relay.1", "relay_auto_off") end function auto_off() xap_send("xap-header\ {\ target=dbzoo.livebox.Controller:relay.1\ class=xAPBSC.cmd\ }\ output.state.1\ {\ id=*\ state=off\ }\ ") end function relay_auto_off() if xAPMsg.class == "xAPBSC.event" then if xapmsg_getvalue("output.state","state") == "on" then start_timer("t1",10,"auto_off") else -- This will also fire on the XAP AUTO-OFF cmd but -- that's ok it will have no effect. stop_timer("t1") end end end
We use the fact that when start_timer is called again the timer count will be reset to create an inactivity alerting mechanism.
function init() start_timer("t1",60,"ontimer") register_source("dbzoo.livebox.Controller:input.1","oninput") end function ontimer() print "No activity on input 1 for 60 seconds" end function oninput() if xAPMsg.class == "xAPBSC.event" then start_timer("t1",60,"ontimer") end end
This example does not respond to xAP messages at all its purely an periodic message service. We can build a clock service by resetting a timer every 1 minute and updating the LCD with a date/time string as per http://www.lua.org/pil/22.1.html
-- Update the LCD every minute with the current time function init() start_timer("t1",60,"doclock") end function doclock() xap_send(string.format("xap-header\ {\ target=dbzoo.livebox.Controller:lcd\ class=xAPBSC.cmd\ }\ output.state.1\ {\ id=*\ text=%s\ }\ "),os.date("%d %b %H:%M")) start_timer("t1",60,"doclock") end
Twitter and google calendar both transmit an xAP alias message that represents a command that needs to be performed. A sample command from twitter:
xap-header
{
v=12
hop=1
uid=FF00D900
class=alias
source=dbzoo.livebox.Twitter
}
command
{
text=relay 1 on
}
The plugboards job is to find and interpret these messages into xAP actions. Here we have coded a simple interpreter that understands commands such as “relay 1 on” and “rf 2 off”. Having a scriptable alias interpreter allows you to express exactly the action that should occur and provides the ultimate flexibility for coding your own.
function init() register_class("alias","doalias") end function cmd(key,subkey,state) xap_send(string.format("xap-header\ {\ target=dbzoo.livebox.Controller:%s.%s\ class=xAPBSC.cmd\ }\ output.state.1\ {\ id=*\ state=%s\ }", key, subkey, state)) end function doalias() alias = xapmsg_getvalue("command","text") k,r,s = string.gfind(alias,"(%a+) (%d) (%a+)")() if (k == "rf" or k == "relay") and r > "0" and r < "5" and (s == "on" or s == "off") then cmd(k, r, s) end end
Without regular expression support in LUA parsing is rather cumbersome using the built in “pattern” matching logic. As these will be used extensively for parsing alias commands I'm going to link in the REX library to the binary.
So now the function can be expressed, more accurately, in a smaller amount of code
function doalias() alias = xapmsg_getvalue("command","text") k,r,s = rex.match(alias,"(relay|rf) ([1-4]) (on|off)") if k then cmd(k, r, s) end end
Using the REX library we can push the interpreter much further. This design uses a table named pat whose key is either a compiled regular expression or a simple string. The assigned function will accept a table of matched regexp pairs, it's then the function responsibility to correctly unpack that number of matching regexp matched expressions for expansion into an xAP message.
pat={ [rex.new("(relay|rf) ([1-4]) (on|off)")]=function(f) cmd(f) end, [rex.new("tweet (.*)")]=function(f) tweet(f) end, ["reboot"]=function() os.execute("/sbin/reboot") end } function init() register_class("alias","doalias") end function cmd(t) key,subkey,state = unpack(t) xap_send(string.format("xap-header\ {\ target=dbzoo.livebox.Controller:%s.%s\ class=xAPBSC.cmd\ }\ output.state.1\ {\ id=*\ state=%s\ }", key, subkey, state)) end function tweet(t) msg = unpack(t) xap_send(string.format("xap-header\ {\ target=dbzoo.livebox.Twitter\ class=xAPBSC.cmd\ }\ output.state.1\ {\ id=*\ text=%s\ }", msg)) end function doalias() local alias = xapmsg_getvalue("command","text") for r,f in pairs(pat) do if type(r) == "string" then if r == alias then f() end else p={r:match(alias)} if #p > 0 then f(p) end end end end
To get you thinking about how your own service can be built here is an example of building a reboot xAP service.
We register the service as the endpoint dbzoo.livebox.Reboot such that any message directed to this endpoint will trigger the reboot command.
function init() register_target("dbzoo.livebox.Reboot", "reboot") end function reboot() os.execute("/sbin/reboot") end
Now, all we need is a sample message to verify that it works. The class and the source must be present but our simple service does not really care what they contain, nor does it care about the xAP body payload.
xAP-header
{
v=12
hop=1
uid=FF00DB0A
class=anything
target=dbzoo.livebox.Reboot
source=acme.reboot.generator
}
If you want to reboot your HAH every night at say 1am in the morning then you could use the GOOGLE CALENDAR component to schedule up a recurring event that sends this xAP payload at the appropriate time.