Sunday, September 6, 2015

Raspberry Pi: How to create your own WebSocket API - Part 1



A while I posted my very first raspberry pi home automation project (LightSwitchPi) and since then I've learned a lot more python and circuitry.  I, being one of many, achieving exactly what the Raspberry Pi Foundation wanted, Education.

Here's a quick tutorial on how to create and form your own home automation api for expansion.  I am going to use Autobahn Python for the WebSocket backend and in Part 2 going to build a simple iOS app to communicate with the Pi via WebSockets (SocketRocket).

Part 1: Custom Sensor/Control Modules:

Install the following if you don't have it:

  • For WebSockets, Autobahn Python :
    • sudo apt-get install python-twisted python-pip 
    • sudo pip install autobahn
    • github example
In the github example there are several files to note:
  • websocket.py
    • The autobahn python based websocket broadcast server with ssl encryption.
  • ButtonListener.py
    • The lightswitch (module) using the first gen PiFace Relay Board.  
  • PlugPoller.py
    • A wifi plug I built (link here, forgive the sloppy design :-P ) with a uart-to-wifi interface.
  • web folder
    • simple webpage showing the broadcasts from the websocket server for testing.
First we need to set up our modules.  I'm using the term modules to clarify that you can at anytime add to your websocket server "things" your Raspberry pi can do (I.E temperature sensing, motion, etc...).  In each module you set it up as a separate thread.  

For example ButtonListener is as Follows:

import threading
import os
import pifacedigitalio
from sys import exit
from time import sleep
import time

class Buttons(threading.Thread):
    cmd = None
    loop = True
    button1Callback = None
    button2Callback = None #set up a callback for each button
    button2LongPressCallback = None #set up custom stuff if needed

We start by importing our needed libraries and creating a class of the "threading.Thread) type. I am creating several callback variables so that when we instantiate our class in the websocket server we can assign those callbacks to broadcast to our clients the status of the button presses.  We continue with the init and the run.  Take note that when input[x] is pressed it sets a variable to true and then doesn't react till you release the button. A way to programmatically set a rising/falling edge detection for button presses.  

def __init__(self):
        threading.Thread.__init__(self)
        self.cmd = pifacedigitalio.PiFaceDigital()
        self.loop = True

    def run(self):
        time_pushed = 0
        pushed_1 = False
        pushed_2 = False
        inputs = None
        # toggle button turn relay 1 on
        while self.loop:
            sleep(0.05)
            inputs = self.input_status()
            outputs = self.output_status()
            # print inputs, ' ', outputs
            if inputs[0] == '1' and pushed_1 is not True:
                pushed_1 = True
                if self.button1Callback:  # json callback for button press
                    self.button1Callback('{"Inputs":"' + self.input_status() + '"}')
            if inputs[0] == '0' and pushed_1:
                pushed_1 = False
                self.cmd.relays[0].toggle() 
                if self.button1Callback:  # json callback for button press
                    self.button1Callback('{"Outputs":"' + self.output_status() + '"}')
                    self.button1Callback('{"Inputs":"' + self.input_status() + '"}')
            if inputs[1] == '1' and pushed_2 is not True:
                pushed_2 = True
                time_pushed = time.time()
                if self.button2Callback:
                    self.button2Callback('{"Inputs":"' + self.input_status() + '"}')
            if inputs[1] == '0' and pushed_2:
                time_taken = time.time() - time_pushed
                pushed_2 = False
                if(self.button2Callback):
                    self.button2Callback('{"Inputs":"' + self.input_status() + '"}')
                if time_taken < .5:
                    self.cmd.relays[1].toggle()
                    if self.button2Callback:
                        self.button2Callback('{"Outputs":"' + self.output_status() + '"}')
                    time_pushed = 0
                if (time_taken > .5):
                    try:
                        if self.button2LongPressCallback:
                            self.button2LongPressCallback()
                    except:
                        pass
                time_pushed = 0

We also need to create a stop function so we can exit cleanly from the thread if needed. "def output_status" and "def input_status" are reading the inputs and outputs of the PiFace board and outputting them to a string for our JSON broadcast. "def output_cmd" is a quick method for changing outputs along with outputting the result to the callbacks.

    def stop(self):
        self.loop = False

    def output_status(self):
        list1 = self.cmd.output_port.value
        status = '{0:08b}'.format(list1)[::-1]
        return status

    def output_cmd(self, pin, value, local=True):
        self.cmd.leds[pin].value=value
        if self.button2Callback and not local:
            self.button2Callback('{"Outputs":"' + self.output_status() + '"}')
        return self.output_status()

    def input_status(self):
        list1 = self.cmd.input_port.value
        status = '{0:08b}'.format(list1)[::-1]
        return status


Part 2: WebSocket Broadcast Server Broadcasting

*(For the complete github code see here)
In a broadcast server you need to keep track of the following:
  • Registering clients
  • UnRegistering clients
  • Broadcasting to clients
  • When to broadcast
In my previous post I usually have a polling timer in my websocket server having the server check status of each item and sending it out on change. In this example the "modules" are doing that anyway so we will only need to assign what the callback methods do.
We will put the light switch button module in the init part of the broadcast portion of the server:

class BroadcastServerFactory(WebSocketServerFactory):

    def __init__(self, url, debug=False, debugCodePaths=False):
        WebSocketServerFactory.__init__(self, url, debug=debug, debugCodePaths=debugCodePaths)
        self.clients = []
        self.lighting = Buttons()
        self.lighting.button1Callback = self.broadcast
        self.lighting.button2Callback = self.broadcast
        self.lighting.start()
        self.plugs = PlugPoller(plugIp, 8080)
        self.plugs.statusChangeCallback = self.broadcast
        self.lighting.button2LongPressCallback = self.plugs.toggleAll

Note what we did: 

  1.  We instantiate our Button class and assign the two regular button press callbacks.  Since self.broadcast only takes one argument self.broadcast(msg) it works great for how we set it up in the button class. 
  2. Set up the plugs module along with it's callbacks. 
  3.  We also set up the long button press callback to toggle some plugs.
With those we will broadcast to our clients status changes for inputs and outputs in a JSON format. {"Outputs":"00000000"},{"Inputs":"00000000"}

Since we are not polling periodically we need to let the clients know the status of all the modules when they are registered as a client:

    def register(self, client):
        if client not in self.clients:
            client.sendMessage(json.dumps([{"Outputs":self.lighting.output_status()},
                                {"Inputs":self.lighting.input_status()},
                                {"Lamps":self.plugs.getstatus()}]))
            print("registered client {}".format(client.peer))
            self.clients.append(client)

Part 3: WebSocket Commands from clients:

When our server receives a message from a client we need to handle it if is supposed to control something.  In mine I decided since it's an output I'm toggling I just send a JSON string {"Output",pinNum}.  If it was an analog device you'd need to send a bit more.  With python you can convert json to a dictionary using json.loads(json_string).

    def onMessage(self, payload, isBinary):
        if not isBinary:
            data = json.loads(payload)
            if(data["Output"]):
                output = data["Output"]
                #get current status to toggle the output. 
                current = factory.lighting.output_status()[int(output)]
                newval = not int(current)
                #print(output,' ',current,' ',newval)
                factory.lighting.output_cmd(int(output), newval, False)
                
Remember we put a status change callback whenever we call the output_cmd.  That way all clients get the new status change.


For the iOS portion continue here

No comments:

Post a Comment