Smart Transducer: getting started
Overview
Smart-Transducer is a platform for building Smart Home solutions the easy way. It uses very simple interfaces, with a push
model, to acquire sensor information and also to change the state of actuators. This recipe will analyze those interfaces and how to use them.
Ingredients
In order to follow this recipe, you will need to satisfy the following requirements:
- The
smart-transducer
package, available at Pike’s repository. - Python skills (search in python.org for tutorials if needed).
- Basic understanding of the ZeroC Ice midleware.
- Debian/Ubuntu Linux and a terminal.
If you already added the Pike repository to your system, then just install smart-transducer
:
$ sudo apt install smart-transducer
ST
description
The ST
module provides a set of interfaces, written in ZeroC Slice IDL, specifically designed to be simple, and aimed to build sensors and actuators for a smart environment.
The definition of the module is as follows:
module st {
// basic types
interface IBool { void set(bool v, string sourceAddr); };
interface IByte { void set(byte v, string sourceAddr); };
interface IFloat { void set(float v, string sourceAddr); };
interface IString { void set(string v, string sourceAddr); };
// observable pattern
interface Linkable { void linkto(string observerAddr); };
};
Note:
It has an interface for every basic type (bool
, byte
, float
, string
…), with a single method, set
, which accepts two arguments:
value
, of a specific type (determined by the interface).sourceAddr
, a string with the source address of the caller.
These interfaces could be implemented by actuators and used by sensors. For instance, a door controller would implement the IBool
interface to lock/unlock the door. Or a temperature sensor could use the IFloat
interface to publish its temperature to some subscriber.
The last interface, Linkable
is used to register an observer to some observable object. For instance, the temperature sensor could implement this interface, so when a service wants to receive the temperature, it could call to linkto()
with a proxy (or a valid IDM address) to a servant that implements the same interface that the temperature sensor uses (in the former example, IFloat
). The sensor usually will store that proxy, and whenever it wants to publish the temperature, it calls the set()
method on it.
Let’s use them on a very simple example.
A (not-so) minimal service
Imagine that you want to create a clock service. It has a method that allows the user to change the current time, and it also publishes the updated time every five seconds.
This clock service will implement the interface IString
to set the current hour. Also, it implements the Linkable
interface, so other services could attach their observers. And it will use the same interface IString
to publish the updated time.
So, first, let’s create a custom interface that inherits IString
and Linkable
. This way, we may have only one servant. The slice would be:
#include <st.ice>
module recipe {
interface Clock extends st::IString, st::Linkable {};
};
Now, we can implement the servant. The method IString.set()
will be used to set the current time, so the given value must have some format. Let’s assume that it will be in the form “hh:mm:ss”, where hh is the hour in 24 hours format, mm is the minute and ss is the second. The code may be:
def set(self, value, source=None, current=None):
self.seconds = sum(
x * int(t) for x, t in zip([3600, 60, 1], value.split(":"))
)
if source is not None:
print("time set to '{}' by {}".format(value, source))
On the other hand, the method Linkable.linkto()
will provide the (stringfied) proxy to some object that will implement the IString
interface, so we store it for later use:
def linkto(self, observer, current):
ic = current.adapter.getCommunicator()
prx = ic.stringToProxy(observer)
self.observer = st.IStringPrx.uncheckedCast(prx)
print("new observer set: '{}'".format(str(prx)))
The last part is to create the server that will hold the adapter, instantiate this servant, and register it on the adapter. It will also run the event loop: once every five seconds, it will call a method on the servant to publish the time to its observer (if any). The whole clock-server.py
code is:
#!/usr/bin/python3
import sys
import Ice
import time
from datetime import datetime, timedelta
Ice.loadSlice("clock.ice -I. --all")
import st
import recipe
class ClockI(recipe.Clock):
def __init__(self):
self.observer = None
self.set(time.strftime("%H:%M:%S"))
def set(self, value, source=None, current=None):
self.seconds = sum(
x * int(t) for x, t in zip([3600, 60, 1], value.split(":"))
)
if source is not None:
print("time set to '{}' by {}".format(value, source))
def linkto(self, observer, current):
ic = current.adapter.getCommunicator()
prx = ic.stringToProxy(observer)
self.observer = st.IStringPrx.uncheckedCast(prx)
print("new observer set: '{}'".format(str(prx)))
def publish_time(self, elapsed):
if self.observer is None:
return
self.seconds += elapsed
dt = datetime(1, 1, 1) + timedelta(seconds=self.seconds)
now = dt.strftime("%H:%M:%S")
self.observer.set(now, "clock")
print("publish time: '{}'".format(now))
class ClockServer(Ice.Application):
def run(self, args):
ic = self.communicator()
adapter = ic.createObjectAdapterWithEndpoints(
"Adapter", "tcp -h 127.0.0.1 -p 1234"
)
adapter.activate()
servant = ClockI()
proxy = adapter.add(servant, ic.stringToIdentity("Clock"))
print("proxy ready: '{}'".format(proxy))
last = time.time()
while True:
try:
time.sleep(5)
elapsed = int(time.time() - last)
last = time.time()
servant.publish_time(elapsed)
except KeyboardInterrupt:
break
if __name__ == "__main__":
ClockServer(1).main(sys.argv)
Run it and you should see something like this:
$ ./clock-server.py proxy ready: 'Clock -t -e 1.1:tcp -h 127.0.0.1 -p 1234 -t 60000'
A minimal client
You can also write a custom client, which in this case is very straightforward. Just create the proxy, and call it using the given arguments. It will be used to change the clock time, so you must call it using the clock’s proxy and the desired time, in the correct format.
The whole client code may be:
#!/usr/bin/python3
import sys
import Ice
Ice.loadSlice("clock.ice -I. --all")
import recipe
class ClockClient(Ice.Application):
def run(self, args):
if len(args) < 3:
print("Usage: {} <proxy> <hh:mm:ss>".format(args[0]))
return
ic = self.communicator()
clock = ic.stringToProxy(args[1])
clock = recipe.ClockPrx.uncheckedCast(clock)
clock.set(args[2], "time-master")
if __name__ == "__main__":
ClockClient().main(sys.argv)
If you run it, you should see a message on the server telling you that it changed its time:
client $ ./clock-client.py 'Clock -t:tcp -h 127.0.0.1 -p 1234' "23:59:46" server output: [...] time set to '23:59:46' by time-master
A minimal observer
In order to receive time publications from the server, we would need an observer. It is just a common servant of the proper type (in this case IString
) that we will register on the server using the Linkable
interface. We don’t need to do any special thing in the servant, so:
class ClockObserverI(st.IString):
def set(self, value, source, current):
print("time event: '{}', by {}".format(value, source))
As always, we need to register this servant on an adapter. We then use the resulting proxy as the argument for the linkto
method. The whole observer would be:
#!/usr/bin/python3
import sys
import Ice
import time
import sched
from datetime import datetime, timedelta
Ice.loadSlice("clock.ice -I. --all")
import st
import recipe
class ClockObserverI(st.IString):
def set(self, value, source, current):
print("time event: '{}', by {}".format(value, source))
class ClockObserver(Ice.Application):
def run(self, args):
if len(args) < 2:
print("Usage: {} <clock-server>".format(args[0]))
return
ic = self.communicator()
adapter = ic.createObjectAdapterWithEndpoints(
"Adapter", "tcp -h 127.0.0.1 -p 1235"
)
adapter.activate()
servant = ClockObserverI()
proxy = adapter.add(servant, ic.stringToIdentity("Clock"))
print("Proxy ready: '{}'".format(proxy))
server = ic.stringToProxy(args[1])
server = st.LinkablePrx.uncheckedCast(server)
server.linkto(str(proxy))
print("Subscribed to clock, waiting events...")
self.shutdownOnInterrupt()
ic.waitForShutdown()
if __name__ == "__main__":
ClockObserver().main(sys.argv)
Run the observer and it should start receiving new events:
$ ./clock-observer.py 'Clock -t:tcp -h 127.0.0.1 -p 1234' Proxy ready: 'Clock -t -e 1.1:tcp -h 127.0.0.1 -p 1235 -t 60000' Subscribed to clock, waiting events... time event: '23:59:51', by clock time event: '23:59:56', by clock [...] server output: [...] new observer set: 'Clock -t -e 1.1:tcp -h 127.0.0.1 -p 1235 -t 60000' publish time: '23:59:51' publish time: '23:59:56' [...]
Using the st-client
tool
To invoke any st
interface, you can use the st-client
tool, provided in the smart-transducer
package. So, to change the clock time, just call it with the proxy and the desired value (run st-client -h
for more options):
$ st-client -t string -p "Clock -t:tcp -h 127.0.0.1 -p 1234" "12:23:34"