summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPeter Wu <peter@lekensteyn.nl>2015-06-10 14:01:52 +0200
committerPeter Wu <peter@lekensteyn.nl>2015-06-10 14:03:41 +0200
commitffea9d997ad36e7a8a42cd2384ab314a77d623f0 (patch)
tree8b87abb1f9870b1b6bb2a286ce8129a69a11bc18
parent0cd5f1748f0121cd16876da8e5be639743774b3b (diff)
downloadcode-ffea9d997ad36e7a8a42cd2384ab314a77d623f0.tar.gz
Initial commit of serial communication
-rw-r--r--rpi2/app/__main__.py18
-rw-r--r--rpi2/app/comm_arduino.py226
-rw-r--r--rpi2/design.txt105
3 files changed, 349 insertions, 0 deletions
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