DBZoo - Talent without discipline is like an Octopus on rollerskates.

xAP Plugboard (script engine)

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:

  • Send a twitter message when the power being monitored with currentcost goes above 2kW
  • If the temperature drops below 10C turn on a heater turning it off again when it reaches 12C
  • When a currentcost/temperature event is seen log it to pachube
  • If Input 1 goes high turn on relay 1
  • If RF 1 is turned on schedule a Google calendar event to turn it off in 1 hour
  • Log a calendar event whenever Input 2 changes state
  • If no change is detected on Input 3 during the last 2 hours, send an SMS message
  • If a change is detected on Input 4 AND a named calendar event is still active, send an email
  • If 'uptime' is > a given number of hours, perform a scheduled reboot

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.

  • User tweets
  • This message is picked up by the xap-twitter components and broadcast as an xAP alias command
  • The script engine interprets this alias and re-transmits a RELAY on command
  • Relay on is seen by the adapter and is processed as a control for the HAH hardware.

Examples of daemons that currently conform to this model are:

  • xap-twitter
    • PRODUCER: monitor a twitter feed and produces a CMD when an action is found
    • CONSUMER: for sending a tweet.
  • xap-googlecal
    • PRODUCER: monitors google calendar and pushes whatever command has been registered.
    • CONSUMER: used as a service to create calendar events.
  • xap-sms
    • PRODUCER: emits an xAP message when an SMS is received
    • CONSUMER: accept an xAP message to send an SMS.
  • xap-currentcost
    • PRODUCER: sends a messages every minute to report on temperature or power levels.
  • xap-livebox:
    • PRODUCER: generates xAP events on INPUT, 1WIRE or I2C changes
    • CONSUMER: accept messages for the RF, RELAYS and I2C sub systems

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:

  • xap-currentcost - xmit an xAPBSC.event telling of a power or temperature level.
  • xap-plugboard - scripted to watch for any message where the source is CURRENTCOST parse and transform the event to a command and send out a PACHUBE logging message.
  • xap-pachube - accepts logging messages and pushes to the pachube web service.

Design considerations

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

  • event driven
  • time driven

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

Script design

The LUA scripts will consist of the following functions that will be called by the C wrapper

  • init - Executed on script loading.
  • onmessage - Executed for each candidate message.
  • ontimer - When a registered timer has expired.

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

Implementation

A total of 10 C functions and 1 global table is exposed for LUA script usage

  • register_source
  • register_target
  • register_class
  • xapmsg_getvalue
  • xap_send
  • xapcache_find
  • xapcache_getvalue
  • xapcache_gettime
  • start_timer
  • stop_timer

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

Filtering

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

Message Parsing

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.source
  • xAPMsg.target
  • xAPMsg.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'

Cache access

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

Sending messages

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

Timers

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

Alias Interpreter

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

Simple Reboot Service

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.

livebox/hah_plugboard.txt · Last modified: 2010/01/30 08:59 by minerva9