From ffea9d997ad36e7a8a42cd2384ab314a77d623f0 Mon Sep 17 00:00:00 2001 From: Peter Wu Date: Wed, 10 Jun 2015 14:01:52 +0200 Subject: Initial commit of serial communication --- rpi2/app/__main__.py | 18 ++++ rpi2/app/comm_arduino.py | 226 +++++++++++++++++++++++++++++++++++++++++++++++ rpi2/design.txt | 105 ++++++++++++++++++++++ 3 files changed, 349 insertions(+) create mode 100644 rpi2/app/__main__.py create mode 100644 rpi2/app/comm_arduino.py create mode 100644 rpi2/design.txt diff --git a/rpi2/app/__main__.py b/rpi2/app/__main__.py new file mode 100644 index 0000000..196c817 --- /dev/null +++ b/rpi2/app/__main__.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python2 +import logging +import glob +import os.path +import serial +import termios +import time +from threading import Thread + +from . import comm_arduino + +def main(path): + # TODO consider using a thread for serial I/O and route finding + comm_arduino.main() + +if __name__ == '__main__': + logging.basicConfig(format='%(asctime)s: %(message)s', level=logging.DEBUG) + main(); diff --git a/rpi2/app/comm_arduino.py b/rpi2/app/comm_arduino.py new file mode 100644 index 0000000..884c325 --- /dev/null +++ b/rpi2/app/comm_arduino.py @@ -0,0 +1,226 @@ +""" +Communicate with the Arduino. +""" + +import glob +import os.path +import serial +from termios import tcgetattr, HUPCL, ONLCR, tcsetattr, TCSAFLUSH +import time +import logging +import sys, traceback +_logger = logging.getLogger(__name__) + +class Ctrl(object): + RESET1 = b'\x81' + RESET2 = b'\x82' + ACK1 = b'\x81' + ACK2 = b'\x82' + OUTOFSYNC = b'\x83' + PING = b'\x80' + PONG = b'\x80' + DATA_ESCAPE = b'\x84' + + @staticmethod + def is_control(b): + return (b & 0xC0) == 0x80 + +def get_data_length(b): + if (b & 0xC0) != 0x80: + return 0 + return (b & 0x3f) - 4 + +def configure_serial(path): + # Disables parameters which are not controlled by pyserial + with open(path) as f: + # Known example: [0o2400, 0o5, 0o6275, 0o105073, 0o15, 0o15, [...]] + attrs = tcgetattr(f) + # output mode: disable CRLF translation (ONLCR) + attrs[1] &= ~ONLCR + # control mode: Disable reset after hangup (HUPCL) + attrs[2] &= ~HUPCL + tcsetattr(f, TCSAFLUSH, attrs) + + +### Serial I/O processing + +class SerialStateError(RuntimeError): + '''Requests a RESET.''' + +# Last processed byte +class SerialState(object): + def __init__(self, ser): + self.ser = ser + # None if there is no expected data + self.data_len = None + self.data = None + # Whether escaped data was previously requested + self.data_escape = False + # If not None, then an integer marking the upper half of the length. + self.data_len2 = None + + def _read_one(self): + while True: + try: + return self.ser.read() + except serial.SerialTimeoutException: + pass + + def handle_one(self, ser): + b = self._read_one(ser) + bi = ord(b) + + if Ctrl.is_control(b): + # Check states + if b == Ctrl.OUTOFSYNC: + raise SerialStateError('RESET requested') + elif b in (Ctrl.ACK1, Ctrl.ACK2): + # Should never happen during normal operation. + raise SerialStateError('Unexpected ACKs') + elif self.data is not None: + raise SerialStateError('Unexpected control during data stage') + + if b == Ctrl.Ping: + ser.write(Ctrl.PONG) + elif (bi & 0xF8) == 0x88: # DATA_LEN2 + self.data_len2 = (bi & 7) << 5 + else: + # Now interpret data length + if self.data_len2 is not None: + if (bi & 0xA0) != 0xA0: + raise SerialStateError('Expected DATA_LEN2 cont.') + self.set_data_length((self.data_len2 | (bi - 0xA0)) + 1) + self.data_len2 = None + else: # DATA_LEN1 (strip upper byte and then -7 per spec.) + self.set_data_length(bi - 0x87) + + return True # OK, next byte please + else: # not control + if self.data is None: + raise SerialStateError('Got DATA while not expecting it') + + if b == Ctrl.DATA_ESCAPE: + self.data_escape = True + else: + if self.data_escape: + self.data_escape = False + self.data += chr(0x80 | bi) + else: + self.data += b + if len(self.data) == self.data_len: + data = self.data + self.data = None + return data + + + def set_data_length(self, len): + if self.data is not None: + raise SerialStateError('Got new data while not finished with old') + + self.data = b'' + self.data_len = len + + +state = None + +def serial_handshake(ser): + """ + + :type ser: serial.Serial + """ + state = SerialState(ser) # reset + attempt = 0 + while True: + attempt += 1 + try: + # Keep sending RESET1 until acknowledged. + ser.write(Ctrl.RESET1) + b = ser.read(1) + if b != Ctrl.ACK1: + _logger.warn('Got no ACK1 but %r for attempt %d - retrying', + attempt, b) + continue + + # Move forward to the RESET2 state. + ser.write(Ctrl.RESET2) + except serial.SerialTimeoutException as e: + # Write error, just reset + _logger.warn('Write timeout for attempt %d: %s', attempt, e) + time.sleep(.2) # Avoid spinning, just to be sure. + continue + + # Read byte (ignoring repeated ACK1). + b = ser.read(1) + while b == Ctrl.ACK1: + _logger.warn('Got repeated ACK1 for attempt %d - skipping', attempt) + b = ser.read(1) + # If not ACK2, state is wrong, so start over. + if b != Ctrl.ACK2: + _logger.warn('Got no ACK2 but %r for attempt %d - retrying', + attempt, b); + continue + break + +def handle_data_debug(data): + ''' + :param data: raw bytes as read from serial + :type data: bytes + ''' + try: + # Try to decode as ASCII + data = data.decode('ascii') + except: + # if that fails, just take the representation. + # b'Foo\xff' -> Foo\xff + data = repr(data) + if data.endswith("'"): + data = data[:-1] + if data.startswith("b'"): + data = data[2:] + elif data.startswith("'"): + data = data[1:] + _logger.warn('PI DEBUG: %s', data) + +def handle_serial(path): + configure_serial(path) + ser = serial.Serial(path, 9600, timeout=2) + + while True: + # Reset to a known good state + serial_handshake(ser) + + while True: + try: + data = state.handle_one(ser) + if data and data[0] == b'\x80': # Debug packet + handle_data_debug(data[1:]) + elif data: + _logger.info('PI DATA: %r', data) + except SerialStateError as e: + _logger.warn('%s', e) + +def main(): + while True: + try: + _logger.info("Waiting for serial device") + while True: + try: + path = glob.glob('/dev/ttyACM*')[0] + break + except IndexError: + # Wait for a device to appear... + time.sleep(.5) + + _logger.info("Found %s", path) + handle_serial(path) + except Exception as e: + _logger.error('%s', e) + _logger.debug('%s', traceback.format_exc()) + time.sleep(.5) + +if __name__ == '__main__': + logging.basicConfig(format='%(asctime)s: %(message)s', level=logging.DEBUG) + if len(sys.argv) > 1: + handle_serial(sys.argv[1]) + else: + main() diff --git a/rpi2/design.txt b/rpi2/design.txt new file mode 100644 index 0000000..6957ad5 --- /dev/null +++ b/rpi2/design.txt @@ -0,0 +1,105 @@ +Arduino + +RPi modules: + - Serial communication + - Wireless map distribution + - Routing module + +Considerations: + - Disable XON/XOFF or bytes 0x11 and 0x13 get eaten by the RPi. + - RPi off, Uno turns on. Uno does not need to wait for RPi. + - RPi already on, Uno just turns on. RPi needs to wait for Uno and then perform + a reset. + - RPi is basically the client (controlling the server) + - Uno is basically the server + +[RPi]->Uno serial handshake: + 1. Send RESET1 command. + 2. Read a byte. + - If it is ACK1, goto 3. + - Else repeat 1. + 3. Send RESET2 command. + 4. Read a byte. + - If it is ACK1, repeat 4. + - If it is ACK2, finish. + - Else goto 1. + +RPi->[Uno] serial handshake (state = UNKNOWN): + 1. Read a byte. + - If equal to RESET1, goto 2. + - Else write OUTOFSYNC and finish. + 2. Discard buffers and send ACK1. + 3. Wait for byte. + - If it is RESET1, goto 2. + - If it is RESET2, write ACK2, set state = READY and finish. + - Else ignore and finish. + +Protocol (byte-oriented): +Higher-level header (1-2 bytes): +10.. .... + 00 00.. (Handshake, chosen to reduce need to escape signed bytes) + 01 RESET1 ([RPi]->Uno) / ACK1 ([Uno]->RPi) + 10 RESET2 ([RPi]->Uno) / ACK1 ([Uno]->RPi) + 11 OUTOFSYNC, please reset ([Uno]->RPi) + 00 0000 PING ([Uno]->RPi) / PONG ([RPi]->Uno) + 00 0100 00.. .... DATA_ESCAPE ([Uno]->RPi) Read as 10.. .... + 00 1... 101. .... DATA_LEN2 ([Uno]->RPi) max len 256, len=...+1) + .. .... DATA_LEN1 ([Uno]->RPi) max len 56, len=...-7) + +Rationale for no PING from RPi to Uno is because the Pi is helpless if the +Arduino died. +Rationale for DATA_ESCAPE is to avoid interpretation as handshake. +Rationale for DATA_LEN2 encoding is to avoid interpreting as handshake, but +still pass the length definition to the control handler. + +Note: data length describes the length of decoded bytes following the header. +Example for sending the byte sequence for debug print: + + 1111 0000 0111 1111 1011 1111 + aaaa bbbb cccc dddd eeee ffff + +becomes: + + 1000 0110 1111 0000 0111 1111 1000 0000 0011 1111 + (len=3) aaaa bbbb cccc dddd eeee eeee eeee ffff + + +Data is interpreted as follows: + +0... .... .... .... Bits that need to be updated (max 15). The length of the + following data depends on this. +1000 0000 Debug packet, max len is determined by higher-level hdr. + +Serial state machine: + +init -->-- Device unavailable + / \ + | \ + v device connected ^ IO error + | | + \ / + Device available - + +RPi architecture + + main + | + +-----+------+---------------+ + | | | +serial route map sync + I/O finder with others (optional) + +route finder: + - updates map based on data updates + - suggests alternative directions + +serial I/O + - queues map updates from Uno and fwds to route finder + - takes route suggestions and fwds to Uno + +Linear flow: + - I/O (in) has data update + - data update dispatch to route finder + - route finder calculates optimal route to target + - route finder gives next directions + - I/O (out) to Uno -- cgit v1.2.1