Arco Recipes


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:

If you already added the Pike repository to your system, then just install smart-transducer:

console
$ 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:

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:

snippet.py
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:

snippet.py
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:

console
$ ./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:

console
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:

snippet.py
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:

console
$ ./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):

console
$ st-client -t string -p "Clock -t:tcp -h 127.0.0.1 -p 1234" "12:23:34"