A complex example - norwegianblues/norwegianblues GitHub Wiki

Introduction

This example describes a residential building with a set of networked sensors and actuators. These nodes are connected to a residential gateway, providing uniform access to all sensors and actuators in the entire building. Moreover, to facilitate access from the outside (i.e. non-simulated) network, the residential gateway is controlled by a non-simulated socket interface. The following figure illustrates this using an example of three temperature sensors (Celcius), three door indicators (open/closed), and the control of three light sources (on/off).

FIXME: Image

The python code for the simulated nodes and the configuration file used in this tutorial can be found in Platform/pool/nodes and Platform/pool/configs, respectively (path names are relative to the Parrot installation directory).

Configuration

The configuration house.json below corresponds largely to the description above. The first node is the gateway. It is followed by three network-controlled light switches, sw0-sw2, three temperature sensors, temp0-temp2, and three door sensors, d0-d2. Finally, the residential LAN urn:backplane:subnet:lan is described.

{
    "version":1,
    "description":
    "house: demonstrate a house with a set of sensors and actuators.",
    "nodes":{
        "urn:hodcp:node:gw":{
            "config":{
        "port":2000
        },
            "interfaces": {
                "eth0": {
                    "ip": "192.168.0.1",
                    "network": "urn:backplane:subnet:lan"
                }
            },
            "weblink_update": "True",
            "class": "house_gateway"
        },
    "urn:hodcp:node:house_gateway_proxy":{
            "config":{
        "server":"192.168.0.1",
        "port":2000,
        "proxy_port":2000, 
        "outbound":false
        },
            "interfaces": {
                "eth0": {
                    "ip": "192.168.0.221",
                    "network": "urn:backplane:subnet:lan"
                }
            },
            "class": "tcp_proxy"
        },
        "urn:hodcp:node:sw0":{
            "config":{},
            "interfaces": {
                "eth0": {
                    "ip": "192.168.0.10",
                    "network": "urn:backplane:subnet:lan"
                }
            },
            "weblink_update": "True",
            "class": "light_switch"
        },
        "urn:hodcp:node:sw1":{
            "config":{},
            "interfaces": {
                "eth0": {
                    "ip": "192.168.0.11",
                    "network": "urn:backplane:subnet:lan"
                }
            },
            "weblink_update": "True",
            "class": "light_switch"
        },
        "urn:hodcp:node:sw2":{
            "config":{},
            "interfaces": {
                "eth0": {
                    "ip": "192.168.0.12",
                    "network": "urn:backplane:subnet:lan"
                }
            },
            "weblink_update": "True",
            "class": "light_switch"
        },
        "urn:hodcp:node:temp0":{
            "config":{},
            "interfaces": {
                "eth0": {
                    "ip": "192.168.0.20",
                    "network": "urn:backplane:subnet:lan"
                }
            },
            "weblink_update": "True",
            "class": "temp_sensor"
        },
        "urn:hodcp:node:temp1":{
            "config":{},
            "interfaces": {
                "eth0": {
                    "ip": "192.168.0.21",
                    "network": "urn:backplane:subnet:lan"
                }
            },
            "weblink_update": "True",
            "class": "temp_sensor"
        },
        "urn:hodcp:node:temp2":{
            "config":{},
            "interfaces": {
                "eth0": {
                    "ip": "192.168.0.22",
                    "network": "urn:backplane:subnet:lan"
                }
            },
            "weblink_update": "True",
            "class": "temp_sensor"
        },
        "urn:hodcp:node:d0":{
            "config":{},
            "interfaces": {
                "eth0": {
                    "ip": "192.168.0.30",
                    "network": "urn:backplane:subnet:lan"
                }
            },
            "weblink_update": "True",
            "class": "door_sensor"
        },
        "urn:hodcp:node:d1":{
            "config":{},
            "interfaces": {
                "eth0": {
                    "ip": "192.168.0.31",
                    "network": "urn:backplane:subnet:lan"
                }
            },
            "weblink_update": "True",
            "class": "door_sensor"
        },
        "urn:hodcp:node:d2":{
            "config":{},
            "interfaces": {
                "eth0": {
                    "ip": "192.168.0.32",
                    "network": "urn:backplane:subnet:lan"
                }
            },
            "weblink_update": "True",
            "class": "door_sensor"
        }
    },
    "networks": {
        "urn:backplane:subnet:lan": {
            "Delay": "2ms",
            "IPv4Base": "192.168.0.0",
            "type": "CSMA",
            "IPv4Mask": "255.255.255.0",
            "DataRate": "5Mbps"
        }
    }
}

Sensor and actuator nodes

The example references a set of simulated nodes. In Parrot, the simulated nodes are implmented in Python. The sensor/actuator simulations are rather simple. Consider, for example, the light switch:

from hodcp import Node

class light_switch(Node):
    """Simple light switch."""

    # methods get/set for reading/writing capabilities + thread for socket
    # access over port 1234
    from accessors import configure, get, set, activate, deactivate, access_socket_main
    
    def __init__(self, urn, conn):
        Node.__init__(self, urn, conn)
        self.state['capabilities'].update({ 
        	'on': { 'type': 'boolean', 'access':'rw'}, 
        	'manufacturer': { 'type': 'string'}
        	})
        self.state.update({'on': True, 'manufacturer': 'Bulbs, inc.'})

This simple example defines a boolean property named on, and a manufacturer description named manufacturer. The capabilities map makes the node self-describing: another node can learn all about the on and manufacturer attributes by inspecting capabilities. Note the use of update() above; since we inherit common traits from the Node superclass we must extend rather than set the capabilities/state.

A set of methods are mixed in from the module accessors. These methods provide functionality for reading and writing the attributes (on and manufacturers in this case) using a simple protocol over the local network. The protocol is TCP-based and not entirely dissimilar from REST, though far simpler.

The sensors temp_sensor and door_sensors are implemented similarly to light_switch.

Gateway

The central piece of the puzzle is the residential gateway, simulated by the file house_gateway.py. This is the largest Python file in this example, and is responsible for:

(1) Receiving commands over a TCP socket. These are commands that would be issued by a client (external to this example) to monitor and control the residential sensors and actuators. Examples of such commands, and the gateway's responses, are

$ read light/2/on
ok True
$ write light/2/on False
ok
$ read temp_sensor/2/temp
ok 39.3

In each of these examples, the gateway responds to the command with the string ok; for read commands, the requested value is included in the response.

(2) Mapping the requested entity to one of the LAN nodes. In this example, the mapping is simple and largely hard-coded, as indicated by the AddressMap table, the path_to_address() method and the list command (further described below).

In many cases, you would like this mapping to be constructed dynamically from the connected nodes' announced capabilities. Parrot is well-suited for experimenting in this area -- we leave this as an instructive exercise for the reader.

(3) Relaying requests to the correct node. The protocol used by the individual nodes is similar to the gateway's external protocol; however, the entity names lack a path component.

Without further ado, then, we now present house_gateway.py:

import threading
import parrot
import time
import ParrotSocketServer

from hodcp import Node

# Avoid complaints about address in use (set REUSEADDR flag)
ParrotSocketServer.TCPServer.allow_reuse_address = True

class house_gateway(Node, threading.Thread):
    """Gateway, dispatching read/write requests to individual nodes.
    Assumes light switches have addresses 192.168.0.(N+10), where N >= 0.
    Assumes temperature sensors have addresses 192.168.0.(M+20), where M >= 0.
    Assumes door sensors have addresses 192.168.0.(S+30), where S >= 0.
        
    Accesses take the form
        read light/2/on
        write temp_sensor/1/temp 39.3
        read door/0/open
    etc.
        """

    AddressMap = { 'light': 10, 'temp_sensor': 20, 'door': 30 }

    NODE_BASE_ADDRESS = "192.168.0.%d"
    NODE_PORT         = 1234
    
    
    from accessors import get, set, configure as default_configure

    def __init__(self, urn, conn):
        Node.__init__(self, urn, conn)
        threading.Thread.__init__(self)

    def configure(self, params):
        self.default_configure(params)
        config = params.get('config', {})
        self.SERVER_ADDRESS = (params['interfaces']['eth0']['ip'], int(config.get('port', 80)))           

    def path_to_address(self, path):
        """Parses the attribute path, and returns the tuple(addr, key)
        where addr is a dotted IP address of the target node, and key
        is the unqualified attribute key."""
        components = path.split('/')
        address_base = house_gateway.AddressMap[components[0]]
        address_base += int(components[1])
        return (house_gateway.NODE_BASE_ADDRESS % address_base, components[2])
    
    def relay_set(self, full_key, value):
        addr, key = self.path_to_address(full_key)
        sock = parrot.Socket(self)
        sock.connect((addr, house_gateway.NODE_PORT))
        sock.send('write %s %s' % (key, value))
        response = sock.recv().strip()
        msg = response.split(' ', 1)
        if msg[0] != 'ok':
            self.log("error in relay_set: %s" % response)
    
    def relay_get(self, full_key):
        addr, key = self.path_to_address(full_key)
        sock = parrot.Socket(self)
        sock.connect((addr, house_gateway.NODE_PORT))
        sock.send('read %s' % key)
        response = sock.recv().strip()
        msg = response.split(' ', 1)
        if msg[0] == 'ok':
            return msg[1]
        else:
            self.log("error in relay_get: %s" % response)
            return None

    def activate(self):
        self.start()

    def deactivate(self):
        self.done = True

    def run(self):
        server = ParrotSocketServer.TCPServer(self, self.SERVER_ADDRESS, GatewayRequestHandler)
        server.house_gateway = self
        server.serve_forever()

class GatewayRequestHandler(ParrotSocketServer.StreamRequestHandler):
    
    def handle(self):
        while True:
            content = self.rfile.readline().strip().split(' ', 3)
            
            # print "from client: received '%s'" % content, 5
            if content[0] == 'read':
                self.wfile.write("ok %s\n" % self.server.house_gateway.relay_get(content[1]))
            elif content[0] == 'write':
                self.server.house_gateway.relay_set(content[1], content[2])
                self.wfile.write("ok\n")
            elif content[0] == 'list':
                self.wfile.write("ok ")
                for node_type in ['light', 'temp_sensor', 'door']:
                    for node_nbr in [0, 1, 2]:
                        self.wfile.write("%s/%d " % (node_type, node_nbr))
                self.wfile.write("\n")
            elif content != "":
                self.wfile.write("error unexpected command '%s'\n" % content[0])

Trying it out with a minimal client

The socket interface described above can be accessed using the telnet command-line application, available on all UNIX variants since dinosaurs ruled the earth. However, the telnet client needs to somehow enter the simulation, and this is where the tcp_proxy.py node comes into play. As can be seen in the below snippet from the config file above,

"urn:hodcp:node:house_gateway_proxy":{
    "config":{
        "server":"192.168.0.1",
        "port":2000,
        "proxy_port":2000, 
        "outbound":false
    },
    "interfaces": {
        "eth0": {
            "ip": "192.168.0.221",
            "network": "urn:backplane:subnet:lan"
        }
    },
    "class": "tcp_proxy"
},

we assign a proxy node that will map the house_gateway (192.168.0.1:2000) to port 2000 (proxy_port) on localhost. The setting "outbound":false indicates that the server is inside the simulation and the client is outside. A peculiarity that should be fixed is that the tcp_proxy node occupies a spot in the Parrot network; it really shouldn't. With this we are ready to try out the simulation.

First, start the simulation in a terminal window:

$ parrot-launcher house.json
...

In a separate terminal window, use telnet to connect to the simulation:

$ telnet localhost 2000
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.

The gateway will now accept your commands. To learn about the nodes available, use the list command:

$ list
ok light/0 light/1 light/2 temp_sensor/0 temp_sensor/1 temp_sensor/2 door/0 door/1 door/2

As you can see, the gateway responds with a list of known nodes. To learn more about the capabilities of a particular node, say, light/2, read its capabilities attribute:

read light/2/capabilities
ok {'on': {'type': 'boolean'}, 'manufacturer': {'type': 'string'}}

The capabilities value returned here is precisely the one given in light_switch.py above. From this, we learn that we can access a boolean attribute named on, as follows:

$ read light/2/on
ok True
$ write light/2/on False
ok
$ read light/2/on
ok False

By adding more information to capabilities, such as free-text descriptions of the attributes, you can make the client more self-explanatory.

N.B.: Capabilities is "meta-information" that is not intended to be exposed directly to clients connecting over the network, its primary use is in letting us build the control plane automatically. See next tutorial for more info.

Finally, of course, telnet leaves something to be desired from a usability point-of-view. A friendlier client can be developed on any platform that supports socket connections -- which leaves us with plenty of options. Another instructive exercise for the reader, perhaps?

Next steps

Proceed to Tutorial 6 – Control over Weblink

⚠️ **GitHub.com Fallback** ⚠️