Created
October 26, 2018 11:23
-
-
Save toxicantidote/a2feb04fd9fe75cec24da8e37518c6c2 to your computer and use it in GitHub Desktop.
Controlling MCP2200 (Denkovi relay) via Websockets and NanoPi switches
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
## 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) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Quick HTML for it:
``