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.
Design considerations
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.
Script design
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
Filtering
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.
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.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'
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 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
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. 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
Introduction
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.
Setting up
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
What does all that mean, then
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.
Sending xAP commands from a script
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.