Raspberry Pi as a Node

We have the remote nodes that work with NodeMCU board with message handler. But I found out that I forgot to implement such mechanics for RPi 🙂 I’m talking about handlers and listeners. On NodeMCU we start a server that listens to broadcasts on port 5053 and passes the message to registered handlers.

This time we will create a Python server and we will write our first handler to support char LCDs. It will react to the same messages as NodeMCU’s one. This will allow us to use ProxyLCD with it and it is a first step to support TFT LCD.

Code on GitHub
Installation from PyPi:

pip3 install message_listener

And handler incorporated into CharLCD. It is also available as submodule in Doton project

Planning

We need to create two modules.
First and most important is server listener. This one will start UDP server and listen to broadcast messages. It will parse and verify them. Then if everything seems okey it will pass this message to registered handlers.

Second is a handler for the LCD. I still have a 20×4 char LCD connected to RPi. It will be a good setup for test-field.
Handler on MCU supports three events: lcd.cmd, lcd.char and lcd.content. First two are for direct mode and are not very useful. But to sustain compatibility we need to implement them.
And this is something that will be added to CharLCD package.

Server

It requires an IP address and a port. Those we gonna send via the constructor. We also need Message instance.
In __init__ we need to initialize a socket, to keep the main loop running and not to halt on socket reading we will set socket timeout to 0.5.
And I think it is a good idea to put our class in its own thread.

import socket
from threading import Thread


class Server(Thread):
    def __init__(self, message, port=5053, ip='0.0.0.0'):
        Thread.__init__(self)
        self.port = port
        self.ip = ip
        self.handlers = {}
        self.work = True
        self.message = message
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
        self.socket.settimeout(0.5)
        self.socket.bind((ip, port))

    def run(self):
        try:
            while self.work:
                try:
                    data, address = self.socket.recvfrom(1024)
                    message = self.message.decode_message(data.decode())
                    if message:
                        print("Message from %s: %s" % (address, message))
                except socket.timeout:
                    pass
        finally:
            self.socket.close()

    def join(self, timeout=None):
        """stop server"""
        self.work = False
        self.socket.close()
        Thread.join(self, timeout)

The main loop is in run function. For now, we only read incoming packets, decode them to messages and print theirs content.

To see how it works let’s create a demo file in demos directory.

import sys
from iot_message.message import Message
sys.path.append("../../")

from message_listener.server import Server

msg = Message('rpi1')
svr = Server(msg)
svr.run()

and another file, client.py to broadcast message:

import socket
import json

address = ('192.168.1.255', 5053)

packet = {
    'protocol': 'iot:1',
    'node': 'computer',
    'chip_id': 'd45656b45afb58b1f0a46',
    'event': 'temperature.current',
    'parameters': {
        'round': True
    },
    'targets': [
        'rpi1'
    ]
}

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
msg = json.dumps(packet)
print(msg)
s.sendto(msg.encode(), address)

This one I already used few times 🙂
Started demo, launched client and it worked 🙂
We have a basic structure, time to create our first handler in Python.

Handler interface

What handler needs to know? For start on what object it works. We can have more than one registered a handler for the same device type. We can hook few thermometers, relays or LCDs.
This goes to a constructor. But we also need a function handle to err handle an event.
Create abstract package and add handler_interface file with:

class Handler(object):
    def __init__(self, worker):
        self.worker = worker

    def handle(self, message):
        """handle a message"""
        raise NotImplementedError("handle not implemented")

CharLCD handler

Our task, support three events, lcd.cmd, lcd.char and lcd.content. But we already done this in lua, so we need to re-implement it in Python:

from message_listener.abstract.handler_interface import Handler as HandlerInterface
import charlcd.abstract.lcd as lcd

class Handler(HandlerInterface):
    """CharLCD handler - best used with buffered CharLCD
    supports 3 events: lcd.cmd, lcd.char and lcd.content
    """
    def handle(self, message):
        handled = False
        if message is not None and 'event' in message:
            if message['event'] == 'lcd.cmd':
                self.worker.drv.command(
                    message['parameters']['data'],
                    message['parameters']['enable']
                )
                handled = True

            if message['event'] == 'lcd.char':
                self.worker.drv.write(
                    message['parameters']['data'],
                    message['parameters']['enable']
                )
                handled = True

            if message['event'] == 'lcd.content':
                idx = 0
                for row in message['parameters']['content']:
                    self.worker.write(row, 0, idx)
                    idx += 1

                if self.worker.display_mode == lcd.DISPLAY_MODE_BUFFERED:
                    self.worker.flush()
                handled = True

        return handled

How to see if handler works if we have no server? Of course except for tests:) ? Mock a message and call handler manually. And yes it works!

Server and handler execution

Time to implement an ability to add handlers to the listener and run them. First functions add_handler and serve_message:

    def serve_message(self, message):
        """pass message to registred handlers"""
        for handler in self.handlers:
            self.handlers[handler].handle(message)

    def add_handler(self, name, handler):
        if name is self.handlers:
            raise AttributeError("name already used!")

        self.handlers[name] = handler

next replace if with:

if message:
    print("Message from %s: %s" % (address, message))
    self.serve_message(message)

Demo script

To see if it really works lets write a demo code:

from message_listener.server import Server
from charlcd.drivers.i2c import I2C
from charlcd import buffered as lcd
from iot_message.message import Message
from charlcd.handler import Handler

i2c_20x4 = I2C(0x3b, 1)
i2c_20x4.pins = {
    'RS': 6,
    'E': 4,
    'E2': None,
    'DB4': 0,
    'DB5': 1,
    'DB6': 2,
    'DB7': 3
}

lcd = lcd.CharLCD(20, 4, i2c_20x4)
lcd.init()

msg = Message('rpi1')

svr = Server(msg)
svr.add_handler('20x4', Handler(lcd))
svr.start()
while True:
    pass

svr.join()

Stop! Jenkins time

Jenkins is back thanks to docker. Quick pep8 an pylint check reveals small problems. Nothing strange or funny.

Summary

From now on we can use Raspberry Pi as a node. We created the server, abstract class and first handler. There is still 2-channel relay connected to RPi so next handler may be for it.

Update

I found out a few bugs in the code, first in server we have:

self.handlers['name'] = handler

It should be:

self.handlers[name] = handler

Another mistake is in demo script, we have

while True:
    svr.run()

and this should be:

svr.start()
while True:
    pass
Advertisements

One comment

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s