livebox:hah_plugboard

Only if you are on a build < 280, otherwise use the V2 script engine

xAP Plugboard (V1 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 a 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:
    • CONSUMER: accepts data logging messages.
      For simplistic messages, it can can also directly watch for events, manipulate the message, and send these to pachube without the plugboards involvement. This is configurable through the web interface.

If Pachube is used in the CONSUMER 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.

The scripting language that is embedded into the HAH 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 is 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 tight we will only keep the last message for a source/class type.

Each script executes in its own interpreter therefore variable and functions cannot be shared from one script to another, also a runtime/compilation failure of 1 script will not affect others.

The Lua scripts consist of a single function that will be called by the C wrapper which will register with the xAP processing engine messages which are of interest to the script. When a match is found the Lua function registered will be invoked.

function init()
  register_source('dbzoo.livebox.CurrentCost:>', docurrentcost)
  register_class('xAPBSC.event', doevent)
end
 
function docurrentcost()
end
 
function doevent()
end

timer - The timer functions allow 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()
  -- Watch for a directed message to the number 1 relay.
  register_target('dbzoo.livebox.Controller:relay.1', "oncmd")
end
 
function oncmd()
   -- As we are concentrating on the timer logic, the "on command" and "off command"
   -- logic are just here as placeholders (this isn't real lua logic).
   if "on command" then start_timer("t1", 3600, "ontimer" )
   if "off command" then stop_timer("t1")
end
 
function ontimer()
  -- At this point the 1hr timer has expired so we need to do something
  -- pseudo code: Send an OFF command back to relay 1.
  send "off command to relay 1"
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
  • register_source(filter, function)
  • register_target(filter, function)
  • register_class(filter, function)

The register set of functions act as filters only handing a 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 a 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.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.2
}
output.state
{
state=on
}

The following LUA table variables will be set

  xAPMsg.source = 'dbzoo.livebox.Controller:relay.2'
  xAPMsg.class = 'xAPBSC.info'

Example function usage:

-- Report when any relay changes state
 
function init()
  -- Watching the source means that both xAPBSC.info and xAPBSC.event
  -- class message will be processed by the 'dosource' function.
  -- This pattern will match any of the 4 relays in the system.
  register_source('dbzoo.livebox.Controller:relay.*', "dosource")
end
 
function dosource()
  -- Ignore the xAPBSC.info class messages.
  if xAPMsg.class == "xAPBSC.event" then
 
        -- Get the relays STATE either on or off
        state = xapmsg_getvalue("output.state","state")
 
        -- This LUA pattern will parse the source address ie. 'dbzoo.livebox.Controller:relay.2'
        -- From this string extract the digit 2. 
        relay = string.gsub(xAPMsg.source,".*(%d)","%1")
 
        -- Print out this relays current state
        print(string.format('Relay %s is %s', relay, state))
  end
end

When ran this script will print 'Relay 2 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 its xapmsg_getvalue counterpart, except on a cache entry.

As of build 250 plugboard will cache up to 100 unique elements. By Unique we mean a combination of source, class and optionally target. If the cache fills up the last entry cached, that is element 99, will be dumped and its contents will be updated.

From the body of the xap-message up to 20 elements, for each cache key, will be stored.

Consider the following inbound message.

xAP-header
{
v=12
hop=1
uid=FF00DB01
class=xAPBSC.info
source=dbzoo.livebox.Controller:relay.2
}
output.state
{
state=on
}

Into the cache we would store the following.

cache = { 1: { key: { source: 'dbzoo.livebox.Controller:relay.2', class: 'xAPBSC.info'},
               entry: { 'output.state' : [{ 'state', 'on'}]
                      }
                time: 1277747704
             }
          2: ...
         }

This whole message would be considered 1 entry, so up to 100 unique message will be stored before truncation occurs. There is no way to flush the cache apart from restarting the xap-plugboard. This strategy made be revised so that a LIFO/LRU etc strategy is used. As memory is not a wastefully resource on the Livebox an excessively large cache is not possible.

If this xAP message KEY is seen again, the existing cache entry will be updated.

Our cache entry can be found like this:

   entry = xapcache_find ( "dbzoo.livebox.Controller:relay.2", "", "xAPBSC.info")

Now we can use this entry to extract data from the value section.

   mystate = xapcache_getvalue ( entry, 'output.state', 'state' )
   -- mystate = ON

There is one other piece of information in the cache; the TIME it was added. For date/time format strings for os.date see http://www.lua.org/pil/22.1.html

  epoch = xapcache_gettime( entry )
  addtime = os.date('%c', epoch)
  -- addtime = 'Mon Jun 28 18:55:04 2010'

Code example:

-- 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")
   -- an xAPBSC.event is only generated once a relay changes state, if it's never changed state
   -- then the cache will not have an entry for it so we fallback to looking for a xAPBSC.info
   -- cache entry, which is emitted every 2 minutes by xap-livebox.
   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)
 
        -- Get and print the current state of relay 1.
        state = xapmsg_getvalue("output.state","state")
        print("Relay 1 state: " .. state)
 
        -- From the cache show the state of relays 2 to 4
        for i=2,4 do
           -- Get the cache ENTRY for each relay.
           local cache = recent_cache("dbzoo.livebox.Controller:relay." .. i)
           -- When was this cache entry last updated?
           local cachetime = xapcache_gettime(cache)
           local secsold = -1
           if cachetime > 0 then
               secsold = os.difftime(os.time(), cachetime)
           end
           -- From the cache entry get the output state of this relay.
           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. Here we expect some sort of state change on input 1 every 60 seconds, if this does not occur then an action will be taken. In this example we print some debug text however most likely it would fire off another xAP message to make something happen, or to alert us of this inactivity.

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; it's 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()
  -- After 60 seconds the 't1' timer will expire and call the 'doclock' function.
  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"))
  -- Reset the timer so it calls 'doclock' again in 60 seconds.
  start_timer("t1",60,"doclock")
end

Alias Interpreter

The Twitter and Google Calendar xap-processes both transmit an xAP 'alias' message that represents a command that needs to be performed. A sample command from xap-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.

Getting started with Plugboard Scripting

Work in Progress

The Plugboard is arguably one of the coolest features of the HAH. It is capable of adding custom logic based not only around xAP messages that are used by the HAH itself, but can work with any other xAP messages on the bus.

It is a little bit more complex than other facilities on the HAH but it's well worth the time to learn how to use it properly.

The first thing to do is to read a little bit about the scripting language itself - Lua.

Then, read some of the example scripts. Start with a simple one and build up slowly.

Next, if you haven't already done so, install the Windows xFx viewer tool. This fine program lets you monitor xAP messages. It's handy to see what is going on. Leave the viewer running as you experiment with your scripts.

From your PC, you can SSH or TELNET into the HAH and look at the sample scripts in the /etc/plugboard directory. Use the vi editor to modify these (or if you don't know vi, use notepad in Windows and ftp your files up to the HAH). For a windows SSH client I use putty

$ telnet 192.168.11.6


Welcome to LIVEBOX unchained


hahbox login: root
Password:

Recall that the default root passwod is admin. If you find that the Telnet doesn't work, try SSH, do check the 'Admin/Services' tab on the HAH Web UI to make sure that Telnet/SSH is enabled.

# cd /etc/plugboard
# ls
catchrly.lux        relay_auto_off.lux
expire_timer.lux    tryprint.lua
#

We can see some sample files. The ones named .lux are just placeholders and won't be processed by the plugboard. The one ending in .lua will.

The plugboard is implemented by a process on the HAH named 'xap-plugboard'. If you issue a 'ps' command from the command prompt, you should see it running. It's important that only one instance of this process is running, so don't start off another without first killing any existing one(s).

e.g.

# ps
  PID USER       VSZ STAT COMMAND
    1 root      2260 S    init
    2 root         0 SW   [keventd]
    3 root         0 SWN  [ksoftirqd_CPU0]
    4 root         0 SW   [kswapd]
    5 root         0 SW   [bdflush]
    6 root         0 SW   [kupdated]
    7 root         0 SW   [mtdblockd]
    8 root         0 SW   [khubd]
   29 root         0 SWN  [jffs2_gcd_mtd2]
   94 root      2252 S    udhcpc -b -i br0
  110 root      2244 S    telnetd -p 23
  120 root      1248 S    pure-ftpd (SERVER)
  127 root      2244 S    inetd
  133 root      1036 S    /usr/bin/xap-hub br0
  135 root      1076 S    /usr/bin/xap-livebox -s /dev/ttyS0 -i br0
  140 root      3280 S    /usr/bin/xap-pachube -i br0
  142 root      2864 S    /usr/bin/kloned
  143 root      2864 S    /usr/bin/kloned
  144 root      5792 S    /usr/bin/xap-googlecal -i br0
  147 root      2640 S    /usr/bin/xap-currentcost -s /dev/ttyUSB0 -i br0
  149 root      4836 S    /usr/bin/xap-twitter -i br0
  155 root      2864 S    /usr/bin/kloned
  162 root      2272 S    -ash
  166 root      3528 S    xap-plugboard
  167 root      2248 R    ps
#

or, if the plugboard was started automatically at HAH bootup time, you will see this

166 root      3528 S    /usr/bin/xap-plugboard -i br0

From this, we can see that the xap-plugboard is running as process 166 (note that the process id number on your HAH will probably be different). To kill this we can use …

# pgrep xap-plugboard
166
# kill -9 166

Or much simpler, which saves us looking up its PID etc..

# killall xap-plugboard

Normally, the xap-plugboard process starts up when the HAH boots (well, if your xap-livebox.ini file is configured to allow this). However, when you are debugging scripts, it can be handy to start this process manually, in debug mode. You can do this from the command line with …

# xap-plugboard -d 9

xAP Plugboard - for xAP v1.2
Copyright (C) DBzoo 2009

br0: address 192.168.11.6
br0: netmask 255.255.255.0
Autoconfig: xAP broadcasts on 192.168.11.255:3639
xAP uid=FF00D800, source=dbzoo.livebox.Plugboard
Debug level 9
Broadcast socket port 3639 in use
Assuming a hub is active
Socket port 3639 in use
Socket port 3640 in use
Socket port 3641 in use
Socket port 3642 in use
Socket port 3643 in use
Socket port 3644 in use
Discovered port 3645
loadScripts(/etc/plugboard)
loadScript(tryprint.lua)
register_source('dbzoo.livebox.Controller:relay.1','debug_print')
Ready
XAPLIB Msg: Name=<xap-hbeat:v>, Value=<12>
XAPLIB Msg: Name=<xap-hbeat:hop>, Value=<1>
XAPLIB Msg: Name=<xap-hbeat:uid>, Value=<FF00D800>
XAPLIB Msg: Name=<xap-hbeat:class>, Value=<xap-hbeat.alive>
XAPLIB Msg: Name=<xap-hbeat:source>, Value=<dbzoo.livebox.Plugboard>
XAPLIB Msg: Name=<xap-hbeat:interval>, Value=<60>
XAPLIB Msg: Name=<xap-hbeat:port>, Value=<3645>
XAPLIB Msg: Name=<xAP-header:v>, Value=<12>
XAPLIB Msg: Name=<xAP-header:hop>, Value=<1>
XAPLIB Msg: Name=<xAP-header:uid>, Value=<FF00DB0B>
XAPLIB Msg: Name=<xAP-header:class>, Value=<xAPBSC.event>
XAPLIB Msg: Name=<xAP-header:source>, Value=<dbzoo.livebox.Controller:1wire.1>
XAPLIB Msg: Name=<input.state:state>, Value=<on>
XAPLIB Msg: Name=<input.state:text>, Value=<19.0>
XAPLIB Msg: Name=<input.state:displaytext>, Value=<Outside 19.0>
findMessage src-dbzoo.livebox.Controller:1wire.1 tgt- cls-xAPBSC.event
Adding Cache entry 0
Dump cache entry
        source: dbzoo.livebox.Controller:1wire.1
        target:
        class: xAPBSC.event
        input.state:state=on
        input.state:text=19.0
        input.state:displaytext=Outside 19.0
check dbzoo.livebox.Controller:relay.1 -> dbzoo.livebox.Controller:1wire.1

Looking at what is going on here …

# xap-plugboard -d 9

Starts the plugboard in debug mode. The '9' signifies that we want maximum (verbose) debug output.

Discovered port 3645

Tells us that the plugboard is talking to the xap-hub process on port 3645

loadScripts(/etc/plugboard)

Tells us that the directory where the Plugboard is looking to find scripts is /etc/plugboard

loadScript(tryprint.lua)

Tells us that the script 'tryprint.lua' is being processed. At plugboard startup, each file in the script directory with a .lua extension is processed. The 'Init()' function in each file is called to setup the Lua event handlers.

register_source('dbzoo.livebox.Controller:relay.1','debug_print')

This shows the code that is inside the Init() function. In this case, we are looking to catch xAP messages which come from 'dbzoo.livebox.Controller:relay.1'. When such a message is found, the Lua function 'debug_print' will be invoked.

Now, let's look at the Lua script that is on our HAH.

-- Simple Lua script to show debug print working
--
-- init() is called automatically as the plugboard process starts up
-- this is the place to register events of interest to us
 
function init()
  register_source("dbzoo.livebox.Controller:relay.1", "debug_print")
end
 
-- This is the function that will be called whenever a message from the 
-- the registered source is spotted by the plugboard
 
function debug_print()
   print("** Relay one trigger **")
end

It is pretty simple. All it does is print a message whenever an xAP message relating to relay one is found. When you are entering your own scripts, remember that variables and function names in Lua are case sensitive.

The debug printout from the running xap-plugboard will quickly flash past and you might miss the printed output. So, kill off the current plugboard by pressing <ctrl-c>, and then restart the plugboard with a reduced debug level.

e.g.

# xap-plugboard -d 5

You should see the plugboard fire up & register your callback function.

Wait for a minute or two. You should see the message Relay one trigger appear. You might wonder why we are getting this message - when we haven't triggered relay one. Go use the xFx viewer tool to inspect messages for relay 1. The HAH generates a '.info' message every minute and this causes our code to be triggered.

Suppose that we only want actual 'events' relating to relay 1. Kill the xap-plugboard with <ctrl-c> and go edit the .lua script to add an if statement around the print statement.

tryprint.lua

-- Simple Lua script to show debug print working
--
-- init() is called automatically as the plugboard process starts up
-- this is the place to register events of interest to us
 
function init()
  register_source("dbzoo.livebox.Controller:relay.1", "debug_print")
end
 
-- This is the function that will be called whenever a message from the 
-- the registered source is spotted by the plugboard
 
function debug_print()
   if xAPMsg.class == "xAPBSC.event" then
      print("** Relay one trigger **")
   end
end

Start the xap-plugboard again with debug level 5.

Note that this time, you don't see the 'Relay one trigger' debug message. Next, fire up a browser and point it at the HAH web UI. Use the web interface to switch on Relay 1. You should now see the debug message printed on your telnet session.

You have just created your first 'intelligent' script!

This worked because before the plugboard called your script, it analysed the xAP message and setup a few Lua variables for you. One of these handy variables is 'xAPMsg.class'. This holds the content of the 'class=' tag in the xAP message. Use xFx viewer to inspect one of the regular .info messages and compare it with one of the .event messages. Our script tested the value of this variable and made the debug printing conditional.

So, suppose that we want to count how many times relay one has been turned on or off over a period of time and display this onto the HAH LCD (or the virtual LCD, or the LCD on the HAH web UI if you don't have the add-on hardware for the HAH). One way to do this is to introduce a global variable into our script and send it in a xAP message to the LCD.

tryprint.lua

-- Simple Lua script to show debug print working
--
-- init() is called automatically as the plugboard process starts up
-- this is the place to register events of interest to us
 
function init()
  register_source("dbzoo.livebox.Controller:relay.1", "debug_print")
  rlycount = 0
end
 
-- This function updates the text on the HAH LCD (or the virtual LCD)
function update_LCD(theCount)
   xap_send(string.format("xap-header\
    {\
     target=dbzoo.livebox.Controller:lcd\
     class=xAPBSC.cmd\
    }\
    output.state.1\
    {\
      id=*\
      text=%d\
    }\
    ", theCount))
 
end
 
-- This is the function that will be called whenever a message from the 
-- the registered source is spotted by the plugboard
 
function debug_print()
   if xAPMsg.class == "xAPBSC.event" then
      print("** Relay one trigger **")
      rlycount = rlycount + 1
      update_LCD(rlycount)
   end
end

So now, we've introduced a few new concepts.

Firstly, the global variable 'rlycount'. This variable keeps its value between invocations of the script. In Lua, a variable is created just by using it.

Secondly, we demonstrate the use of the 'xap_send' function. This causes the string parameter to be transmitted onto the bus as an xAP message. Compare the text that we've sent with the xAP message as shown by xFx. You will notice that the plugboard has wrapped the string that we supplied with extra 'standard message formatting' to make a valid xAP message. Note that Lua uses the '/' character to continue a string on the next line of code.

Thirdly, we've used the Lua string.format function. This took two parameters. 1. The string to be formatted. Note the %d token in this string. This represents a decimal number. 2. The variable to be used as the format replacement for the %d token. You can find out more about string formatting from the Lua http://lua-users.org/wiki/StringLibraryTutorial.

  • livebox/hah_plugboard.txt
  • Last modified: 2012/03/18 18:48
  • by brett