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.
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:
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:)
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:
under first add in demo file.
And final step replace _command function with new one:
def _command(self, cmd): parameters = cmd.split(' ') if parameters == 'get' and parameters == '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 == 'get' and parameters in self.display.names: return "\n".join(self.display.managers[parameters].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..
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 == "get": return self._command_get(parameters) elif parameters == "list": return self._command_list(parameters) return "Unknown command" def _command_get(self, parameters): """returns lcd content""" if len(parameters) == 2 and parameters in self.display.names: return "\n".join(self.display.managers[parameters].lcd.screen) else: return "Manager not found" def _command_list(self, parameters): """returns managers name""" if len(parameters) == 2 and parameters == 'managers': return ",".join(self.display.names) else: return "Error"
Now we have validation and some protection, it wont crash so easily.
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 🙂
- list managers
- get <name>
There is one problem, I have no idea how to test our threads and servers 😦