LCD Manager – remote display reading

What I need is ability to read content of LCDs via network. With our manager it should be easy. We will add such service to Display class. It will start server and react according to request. For now I think about two commands, one for getting list of all managers and second for getting a content from needed.

Download

First steps

We will create simple client-server module. For this we need two threads. One for listening for connections and after getting client it will spawn second to support connection and run commands.

Create new package remote and new file server.py. Our basic structure looks like this:

#!/usr/bin/python
# -*- coding: utf-8 -*-
#pylint: disable=I0011,W0231
"""server"""
from __future__ import print_function
from future import standard_library
standard_library.install_aliases()
import socket
from threading import Thread

BUFFER_SIZE = 1024


class ListenerThread(Thread):
    """connection listener"""
    def __init__(self, conn, addr, display):
        Thread.__init__(self)
        self.client = conn
        self.addr = addr
        self.work = True
        self.display = display

    def run(self):
        """thread"""
        while self.work:
            try:
                data = self.client.recv(BUFFER_SIZE).decode("UTF-8").strip()
                if not data:
                    break
                else:
                    response = self._command(data)
                    if response:
                        self.client.send(response.encode("UTF-8"))
            except socket.timeout:
                pass

        self.client.close()

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

    def _command(self, cmd):
        return cmd


class ReadThread(Thread):
    """server thread"""
    def __init__(self, display, port=1301, ip='0.0.0.0'):
        Thread.__init__(self)
        self.work = True
        self.display = display
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.socket.settimeout(0.5)
        self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.socket.bind((ip, port))
        self.socket.listen(3)
        self.threads = []

    def run(self):
        """start server thread"""
        try:
            while self.work:
                try:
                    client, address = self.socket.accept()
                    listener = ListenerThread(client, address, self.display)
                    listener.start()
                    self.threads.append(listener)
                except socket.timeout:
                    pass
        finally:
            self.socket.close()
        for thread in self.threads:
            thread.join()

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

Few words about it. We have two classes, ReadThread and ListenerThread, both extends Thread.
ListenerThread starts new server and listen for incoming connections. If connection arrives it is passed to new thread dedicated for it, ListenerThread. ListenerThread receives input from remote, parse it and return response.

Lets integrate them into Display class. Add proper import to import section

from lcdmanager.remote.server import ReadThread

and new function that will start a server:

    def start_remote(self, port, ip):
        """starts remote server"""
        self.read_server = ReadThread(self, port, ip)
        self.read_server.start()

 

To see how it works we need a new demo. Create file remote_read.py in demos folder and copy:

#!/usr/bin/python
# -*- coding: utf-8 -*-

"""LCD Manager - Remote server to read - demo"""

import sys
sys.path.append("../../")
import RPi.GPIO as GPIO  # NOQA pylint: disable=I0011,F0401
from charlcd import buffered as buffered  # NOQA
from charlcd.drivers.i2c import I2C  # NOQA
import charlcd.drivers.gpio as gp  # NOQA
from lcdmanager import manager  # NOQA
from lcdmanager.widget.label import Label  # NOQA
from lcdmanager import display  # NOQA
import time  # NOQA

GPIO.setmode(GPIO.BCM)


def demo1():
    """basic demo"""
    lcd = buffered.CharLCD(20, 4, gp.Gpio())
    lcd.init()
    lcd_manager = manager.Manager(lcd)
    disp = display.Display(0.5, True)
    disp.add(lcd_manager, 'one')
    disp.start()
    disp.start_remote(1301, '0.0.0.0')
    label1 = Label(0, 0)
    lcd_manager.add_widget(label1)

    try:
        while True:
            label1.label = str(time.time())
            time.sleep(1)
    finally:
        print("stoping...")
        disp.join()


demo1()

Whats is going there? We start Display server, create label and on it we are displaying time endlessly.
We are starting out new remote server with:

disp.start_remote(1301, '0.0.0.0')

If you run this demo, server will be listening at port 1301 and localhost. If you want to connect to it use telnet:

telnet 127.0.0.1 1301

Type anything and in response you should receive what you typed. Simple echo service:)

Commands

For now we need two commands, first to get all managers that Display supports and second command to get what is displayed on given manager.
Command get managers will return string with names separated by comma.

Add property to read manager names in display class:

    @property
    def names(self):
        """return manager names"""
        return  self.managers.keys()

Return to demo file and add second display, so we know that it works:

disp.add(lcd_manager, 'two')

under first add in demo file.

And final step replace _command function with new one:

    def _command(self, cmd):
        parameters = cmd.split(' ')
        if parameters[0] == 'get' and parameters[1] == 'managers':
            return ",".join(self.display.names)

Now when you start app, connect to it by telnet and type get managers you will get what we wanted, a string with names.
We have first command. Now lets do something about second one. Under return add:

elif parameters[0] == 'get' and parameters[1] in self.display.names:
    return "\n".join(self.display.managers[parameters[1]].lcd.screen)

We are returning what is currently in screen variable in lcd. We are joining rows with \n – it should be easy to parse it on the other end.
Command parsing is very naive. But this is not all, remote connection is insecure, we are not using any authentication or encryption. Be aware that is not recommended to start remote server on public visible unit. NEVER.
One day we will add some security but not now. I have some goal in mind but please be patient.

Stop! Jenkins time!

Small PEP8 problems detected by flake8. And still pylint is not working properly :/ it is generating report but nothing displays in Jenkins. Time to analyze this problem. After 10 minute… yep my mistake šŸ™‚ wrong .pylintrc šŸ™‚ Pylint is back to annoy me šŸ™‚
Only 58 violations… refactoring in progress… Many problems are just a known sacrifice šŸ™‚ Like using x and y in canvas widget. It is good idea to look thru all reported issues and correct them or add exception.
Sometime it takes lots of patience..

Refactor

If you look at _command and how it parse input you will scream šŸ™‚ It is so bad that it MUST be changed. If you don’t know what I mean start app, connect via telnet and type something and… script will crash.
We have to change parsing. And we will use shlex library.
We will also split commands into separate functions. For now they will stay in same file but in future this may change. With such approach basic validation stays in _command but advanced goes to functions.
Library shlex has a nice function split. It know when we use quote to join longer string and wont break it.
Our basic validation is to check how many arguments are in command. There must be at least one – command name. Draw back is that we must change out function get managers, because get is reserved for lcd content. Lets change it to list managers.
So with all this we have:

    def _command(self, cmd):
        """execute command"""
        parameters = shlex.split(cmd)
        if len(parameters) == 0:
            return None
        if parameters[0] == "get":
            return self._command_get(parameters)
        elif parameters[0] == "list":
            return self._command_list(parameters)

        return "Unknown command"

    def _command_get(self, parameters):
        """returns lcd content"""
        if len(parameters) == 2 and parameters[1] in self.display.names:
            return "\n".join(self.display.managers[parameters[1]].lcd.screen)
        else:
            return "Manager not found"

    def _command_list(self, parameters):
        """returns managers name"""
        if len(parameters) == 2 and parameters[1] == 'managers':
            return ",".join(self.display.names)
        else:
            return "Error"

Now we have validation and some protection, it wont crash so easily.

Summary

We added another functionality to LCD Manager, one of module – Display can now launch a server that serve content from lcds. It makes remote reading much easier and allows other integrations.
You want to display lcd content on WWW – no problem! Maybe just watch by telnet – no problem. And maybe write app on mobile that will be able to be a remote display ? Yes this is in my plan šŸ™‚

Our command:

  • list managers
  • get <name>

There is one problem, I have no idea how to test our threads and servers šŸ˜¦

Download

Advertisements

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