Skip to content

Instantly share code, notes, and snippets.

@toxicantidote
Created October 26, 2018 11:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save toxicantidote/a2feb04fd9fe75cec24da8e37518c6c2 to your computer and use it in GitHub Desktop.
Save toxicantidote/a2feb04fd9fe75cec24da8e37518c6c2 to your computer and use it in GitHub Desktop.
Controlling MCP2200 (Denkovi relay) via Websockets and NanoPi switches
## Automation relay switcher server
## Allows control of MCP2200 based four port relay board via a websocket. Also interfaces with two pins
## on the NanoPi GPIO to read the state of two buttons using only two wires.
## Address to listen on. Set to 0.0.0.0 for all
listenAddress = '0.0.0.0'
## Port to listen on
listenPort = 8000
## Pin sets that have switches connected
switchSets = [[11,13]]
### NO EDITING BELOW HERE ###
## Import the required modules
import threading
import websockets
import re
import time
import asyncio
import usb.core
import usb.util
import RPi.GPIO as GPIO
clients = []
## Websocket server class. Lets the websocket server run in its own class
class webSocketServer(threading.Thread):
## Class init
def __init__(self, listenAddress, listenPort, relay_control):
## Call self as a thread
threading.Thread.__init__(self)
## Create local references to configuration arguments
self.listenAddress = listenAddress
self.listenPort = listenPort
self.relay_control = relay_control
self.switch = []
## Main function. Polls for data until the connection is terminated
def run(self):
## Initialise the server instance
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
self.server = websockets.serve(self.handleConnection, self.listenAddress, self.listenPort)
## The websockets library creates a generator for some stupid reason, so we have to use asyncio to actually start it
self.loop.run_until_complete(self.server)
self.loop.run_forever()
## Handles incoming connections
@asyncio.coroutine
def handleConnection(self, websock, path):
global clients
print('Connection established')
## Poll until broken
while True:
clients.append(websock)
## Get received data
try:
received = yield from websock.recv()
except:
break
## If the received data is None type, the connection has been terminated, either intentionally or otherwise.
if received == None:
## Print a debug message and then break out of the receive loop
print('Connection terminated')
try:
clients.remove(websock)
except:
pass
break
## If data has been received, pass it to the message handler function
else:
print('Received data')
yield from self.handleMessage(websock, received)
## Handle received messages
def handleMessage(self, websock, message):
## Convert the message to a string. If a byte produces an error during conversion, it is changed to a space
message = str(message)
print('RX: ' + message)
## Use regular expressions with match groups to find and parse commands
regexp_echo = re.search(r'ECHO (.+)', message, re.IGNORECASE)
regexp_on = re.search(r'ON ([0-3])', message, re.IGNORECASE)
regexp_off = re.search(r'OFF ([0-3])', message, re.IGNORECASE)
regexp_state = re.search(r'STATE', message, re.IGNORECASE)
## Check for matches against valid commands
if regexp_echo:
## We got the echo command. Grab the rest of the command from the regexp match group. Make sure it's a string
reply = str(regexp_echo.group(1))
## Send the message back as we received it (but without ECHO)
yield from websock.send(reply)
print('Echoed back data.')
## On command
elif regexp_on:
port = int(regexp_on.group(1))
print('Turning on ' + str(port))
self.relay_control.turn_on(port)
yield from self.updateState()
## Off command
elif regexp_off:
port = int(regexp_off.group(1))
print('Turning off ' + str(port))
self.relay_control.turn_off(port)
yield from self.updateState()
## state command
elif regexp_state:
yield from self.updateState()
## Fallthrough for unknown commands
else:
## Send back an error message
yield from websock.send('Error: Unknown command')
print('Unknown command')
## send the state to all clients
def updateState(self):
global clients
print('Sending state to all clients')
switch_state = []
for switch_thread in self.switch:
switch_state.append(str(switch_thread.query_state()))
for websock in clients:
try:
yield from websock.send('STATE ' + self.relay_control.query_state() + ' ' + ','.join(switch_state))
except:
print('Could not send state to ' + str(websock) + '. Ignoring client for future updates')
try:
clients.remove(websock)
except:
pass
## used by the switch threads to let us know of their existence, because
## they start after us
def add_switch_thread(self, switch):
self.switch.append(switch)
## Relay control thread
class relayControl(threading.Thread):
## Interfaces with MCP2200 based Denkovi four port relay switch board using
## USB HID control.
def __init__(self, vendor = 0x04d8, product = 0x00df, hid_child = 2):
## Call self as a thread
threading.Thread.__init__(self)
self.hid_child = hid_child
self.state = [False, False, False, False]
self.terminate = False
## find the device
self.device = usb.core.find(idVendor = vendor, idProduct = product)
if self.device is None:
raise RuntimeError('Device not found. Is it plugged in?')
## set the endpoint
self.endpoint = self.device[0][(hid_child, 0)][1]
## claim it from the kernel
try:
if self.device.is_kernel_driver_active(hid_child):
self.device.detach_kernel_driver(hid_child)
usb.util.claim_interface(self.device, hid_child)
except:
self.__del__()
## configure the device
data = [0]*16
data[0] = 0x10
self.write(data)
## turn everything off
self.all_off()
## writes to the device
def write(self, data):
try:
print('Device TX: ' + str(data))
self.device.write(self.endpoint, data)
except:
print('Could not write to USB device!')
## turn everything off
def all_off(self):
data = [0]*16
data[0] = 0x8
data[12] = 0xF
self.write(data)
## turn a relay on
def turn_on(self, index):
data = [0]*16
data[0] = 0x8
mask = (0x1 << index)
self.state[index] = True
## put the mask in the byte array
data[11] = mask
## send the command
self.write(data)
## turn a relay off
def turn_off(self, index):
data = [0]*16
data[0] = 0x8
mask = (0x1 << index)
self.state[index] = False
## put the mask in the byte array
data[12] = mask
## send the command
self.write(data)
## return a comma seperated list of relays that are currently on
def query_state(self):
on_relays = []
for i in range(0, 4):
if self.state[i] == True:
on_relays.append(str(i))
return ','.join(on_relays)
## triggered on thread destruction
def __del__(self):
## give the USB device back to the kernel when this thread is destroyed
try:
usb.util.release_interface(self.device, self.hid_child)
if not self.device.is_kernel_driver_active(self.hid_child):
self.device.attach_kernel_driver(self.hid_child)
except:
pass
## main loop. do nothing
def run(self):
while self.terminate == False:
time.sleep(0.1)
## Switch read thread
class switchRead(threading.Thread):
## Reads the state of two buttons/switches over a 2 wire connection
## Circuit:
## PIN1 -->|-- SWITCH ---- PIN2
## PIN2 -->|-- SWITCH ---- PIN1
##
## The diodes allow the use of only two digital pins to read all four
## possible states (off, one, two, both).
##
## We set one pin as a high output and the other as an input to read the
## state of one button and then repeat the same thing with the opposite pins
## to read the other button.
##
## Thread init
def __init__(self, pin1, pin2, wss):
## Call self as a thread
threading.Thread.__init__(self)
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BOARD)
self.pin1 = pin1
self.pin2 = pin2
self.state = 0
self.wss = wss
self.last_change = time.time() * 1000
## tell the websocketserver thread where to find our instance
self.wss.add_switch_thread(self)
self.terminate = False
## poll the buttons forever per above method
def run(self):
while self.terminate == False:
GPIO.setup(self.pin1, GPIO.OUT)
GPIO.setup(self.pin2, GPIO.IN, pull_up_down = GPIO.PUD_DOWN)
GPIO.output(self.pin1, True)
state_pin2 = GPIO.input(self.pin2)
GPIO.setup(self.pin2, GPIO.OUT)
GPIO.setup(self.pin1, GPIO.IN, pull_up_down = GPIO.PUD_DOWN)
GPIO.output(self.pin2, True)
state_pin1 = GPIO.input(self.pin1)
if state_pin1 == False and state_pin2 == False:
state_new = 0
elif state_pin1 == True and state_pin2 == False:
state_new = 1
elif state_pin1 == False and state_pin2 == True:
state_new = 2
elif state_pin1 == True and state_pin2 == True:
state_new = 3
now_ms = time.time() * 1000
threshold_ms = now_ms - self.last_change
if state_new != self.state and now_ms > 50:
self.state = state_new
self.last_change = time.time() * 1000
print('New switch state for ' + str(self.pin1) + '/' + str(self.pin2) + ' is ' + str(self.state))
for ignore in self.wss.updateState():
pass
## return the current button state
def query_state(self):
return self.state
print('Starting control thread..')
rly = relayControl()
rly.start()
print('Starting websocket server..')
wss = webSocketServer(listenAddress, listenPort, rly)
wss.start()
print('Starting switch monitor threads..')
for pin1, pin2 in switchSets:
print('\tPins ' + str(pin1) + ' and ' + str(pin2))
swt = switchRead(pin1, pin2, wss)
swt.start()
print('Running!')
## Do nothing forever
while True:
## we could do something here later to extend functionality
time.sleep(1)
@toxicantidote
Copy link
Author

Quick HTML for it:
`

	<title>WebSocket Test</title>
	
	<script language="javascript" type="text/javascript">
		var wsUri = "ws://nanopi-neo:8000";
		var output;
		var websocket;
		
		function init() {
			output = document.getElementById("output");				
        }

		function doOpen() {
			websocket = new WebSocket(wsUri);
			websocket.onopen = function(evt) { onOpen(evt) };
			websocket.onclose = function(evt) { onClose(evt) };
			websocket.onmessage = function(evt) { onMessage(evt) };
			websocket.onerror = function(evt) { onError(evt) };
		}
		
		function onOpen(evt) {
			writeToScreen("CONNECTED");
			doSend("ECHO Hello world!");
            doSend("STATE");
            document.getElementById("connect_button").setAttribute( "onClick", "javascript: doClose();" );
            document.getElementById("connect_button_img").src = 'connected.png';
            document.getElementById("ports").style.display = 'block';
		}
		
		function onClose(evt) {
			writeToScreen("DISCONNECTED");
            document.getElementById("connect_button").setAttribute( "onClick", "javascript: doOpen();" );
            document.getElementById("connect_button_img").src = 'disconnected.png';
            document.getElementById("ports").style.display = 'none';
		}

		function onMessage(evt) {                
			writeToScreen('RX: ' + evt.data);
            if (evt.data.split(" ")[0] == "STATE") {
                for (var x = 0; x <= 3; x++) {
                    document.getElementById("portstate_" + x + '_img').src = 'off.png';
                    document.getElementById("portstate_" + x).setAttribute( "onClick", "javascript: turnOn(" + x + ");" );                        
                }
                var ports = evt.data.split(" ")[1].split(",")
                console.log('Ports on: ' + ports);
                if (ports[0] != '') {
                    for (var a = 0; a < ports.length; a++) {
                        document.getElementById("portstate_" + ports[a] + '_img').src = 'on.png';
                        document.getElementById("portstate_" + ports[a]).setAttribute( "onClick", "javascript: turnOff(" + ports[a] + ");" );  
                    }                        
                }
                var switches = evt.data.split(" ")[2].split(",");
                console.log('Switches state: ' + switches);
                var new_switch_HTML = '<table>';
                for (var b = 0; b < switches.length; b++) {                    
                    new_switch_HTML += '<tr><th>Set ' + b + '</th>';
                    if (switches[b] == '1' || switches[b] == '3') {
                        new_switch_HTML += '<td><img src="on.png" alt="Switch on" /></td>';
                    } else {
                        new_switch_HTML += '<td><img src="off.png" alt="Switch off" /></td>';
                    }
                    if (switches[b] == '2' || switches[b] == '3') {
                        new_switch_HTML += '<td><img src="on.png" alt="Switch on" /></td>';
                    } else {
                        new_switch_HTML += '<td><img src="off.png" alt="Switch off" /></td>';
                    }
                    new_switch_HTML += '</tr>';
                }
                new_switch_HTML += '</table>';
                document.getElementById("switch_container").innerHTML = new_switch_HTML;
            }
		}

		function onError(evt) {
			writeToScreen('ERROR: ' + evt.data);
		}

		function doSend(message) {
			writeToScreen("TX: " + message);
			websocket.send(message);
		}
		
		function doClose() {
			websocket.close();
		}

		function writeToScreen(message) {
			console.log(message);
		}
        
        function turnOn(port) {
            doSend("ON " + port);
        }
        
        function turnOff(port) {
            doSend("OFF " + port);
        }

		window.addEventListener("load", init, false);
	</script>
    <style type="text/css">
        #ports {
            display: none;
        }
        table {
            border-collapse: collapse;
        }
    </style>
</head>
<body>
    <button onClick="doOpen()" id="connect_button"><img src="disconnected.png" id="connect_button_img" /></button>
    <table id="ports">
        <tr>
            <th>Relay control</th>
            <td>                    
                <table>
                    <tr>
                        <td><button onClick="turnOn(0)" id="portstate_0"><img src="off.png" id="portstate_0_img" /></button></td>
                        <td><button onClick="turnOn(1)" id="portstate_1"><img src="off.png" id="portstate_1_img" /></button></td>
                        <td><button onClick="turnOn(2)" id="portstate_2"><img src="off.png" id="portstate_2_img" /></button></td>
                        <td><button onClick="turnOn(3)" id="portstate_3"><img src="off.png" id="portstate_3_img" /></button></td>
                    </tr>
                </table>
            </td>
        </tr>
        
        <tr>
            <th>Switch state</th>
            <td id="switch_container"></td>
        </tr>
    </table>
</body>    
`

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment