over/serial/com.py

190 lines
5.1 KiB
Python

#! /usr/bin/env python3
# encoding: utf-8
import time
import serial
from . import et
prefix = et.prefix
'''
===== OverTalk protocol spec =====
==== Transport layer ====
frame = [ 0x2a | destination (1) | source (1) | payload length (1) | payload (n) | checksum (1) ]
Any time an 0x2a is encountered the parser should flush its state and begin reading a new frame.
Bytes 0x2a, 0x2b, 0x11 and 0x13 are prefixed with 0x2b and inverted (XORed with 0xff) to avoid collisions.
Frames with destinations unknown to an OverShell are sent to the default interface, or dropped if
that's the interface they came from.
==== Command layer ====
Payload consists of one or more commands:
get_command = [ 0x00 | register (1) | target register (1) ]
set_command = [ 0x01 | register (1) | length (1) | value (n) ]
With a get_command the sender requests the receiver to read its own 'register' and issue
a set_command that sets the sender's 'target register' to that value.
A set_command does what is says on the box.
'''
class Transport:
'''
OverTalk Transport layer implementation.
Reads data from multiple interfaces and either router frames or receives them.
A received frame is then made available via a public attribute.
Interfaces are objects that implement methods read() (returns one byte of data as int), write(buffer, len),
and waiting property (contains number of bytes waiting).
'''
def __init__(self, my_id, interfaces, def_route):
'''
@param my_id OverTalk address of this Transport instance
@param interfaces a dict of interfaces keyed by their IDs
@param def_route ID of default interface for sending
'''
assert my_id not in interfaces
assert def_route in interfaces
self._print = et.Output('over.com.Transport', default_suffix='\n', timestamp=True)
self.my_id = my_id
self.def_route = def_route
self.interfaces = interfaces
self.destination_unknown = 0
self.incoming = None
self._print('<Cg>on-line<C/>')
def update(self):
self.incoming = None
for interface_id, interface in self.interfaces.items():
if interface.waiting:
interface.last_data_received = time.time()
byte = interface.read()
if byte == 0x2a:
interface.rxbuffer = []
interface.reading_escape = False
interface.reading_frame = True
if byte == 0x2b:
interface.reading_escape = True
continue
if interface.reading_escape:
byte ^= 0xff
interface.reading_escape = False
if interface.reading_frame:
interface.rxbuffer.append(byte)
if self.verify_frame(interface):
self.process_frame(interface_id, interface.rxbuffer)
interface.rxbuffer = []
def verify_frame(self, interface):
buffer_length = len(interface.rxbuffer)
if buffer_length >= 4:
# at this point we can read frame's declared size
if buffer_length >= interface.rxbuffer[3] + 5: # header (4) + payload + checksum (1)
# a frame has been read, huzzah!
interface.reading_frame = False
# checksum
if sum(interface.rxbuffer[:-1]) % 0x100 == interface.rxbuffer[-1]:
return True
else:
interface.malformed_frames += 1
self._print('broken frame received: <Cr>%s<C/>' %(interface.rxbuffer))
interface.rxbuffer = []
return False
def escape_frame(self, frame):
assert frame[0] == 0x2a
frame_escaped = [0x2a]
for byte in frame[1:]:
if byte in (0x2a, 0x2b, 0x11, 0x13):
frame_escaped.append(0x2b)
byte ^= 0xff
frame_escaped.append(byte)
return frame_escaped
def process_frame(self, source_interface_id, frame):
payload = frame[4:-1]
destination = frame[1]
if destination == self.my_id:
self.incoming = (frame[2], payload)
else:
if destination in self.interfaces:
self._print('routing frame to [%d]' %(destination))
self.interfaces[destination].write(self.escape_frame(frame))
else:
if source_interface_id == self.def_route:
self.destination_unknown += 1
self._print('unknown destination <Cr>%d<C/> for frame: <Cy>%s<C/>' %(destination,
repr(frame)), prefix.fail)
else:
self._print('routing frame to default route [%d]' %(self.def_route))
self.interfaces[self.def_route].write(self.escape_frame(frame))
def send_data(self, destination, data):
frame = [0x2a, destination, self.my_id, len(data)] + list(data)
frame.append(sum(frame) % 0x100)
frame = self.escape_frame(frame)
if destination in self.interfaces:
s = self.interfaces[destination]
else:
s = self.interfaces[self.def_route]
s.write(frame)
class TTL_Interface:
def __init__(self, interface='/dev/ttyUSB0', baudrate=57600):
try:
self.s = serial.Serial(interface, baudrate, timeout=1)
except serial.serialutil.SerialException:
self.s = None
self.rxbuffer = []
self.reading_escape = False
self.reading_frame = False
self.malformed_frames = 0
self.last_data_received = 0
@property
def waiting(self):
if self.s:
return self.s.inWaiting()
else:
return 0
def read(self):
if self.s:
return ord(self.s.read())
else:
return 0
def write(self, data):
print('Sending:', ''.join([hex(x)[2:].zfill(2) for x in data]))
if self.s:
self.s.write(bytes(data))