From 105d1c1a3c812559212a380750586f964159524f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Wed, 27 Dec 2017 00:31:43 +0100 Subject: [PATCH] code import --- .gitignore | 2 + gex/TinyFrame.py | 367 +++++++++++++++++++++++++++++++++++++++++++++++ gex/__init__.py | 102 +++++++++++++ main.py | 31 ++++ 4 files changed, 502 insertions(+) create mode 100644 gex/TinyFrame.py create mode 100644 gex/__init__.py create mode 100644 main.py diff --git a/.gitignore b/.gitignore index 7bbc71c..35d3f7d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ __pycache__/ *.py[cod] *$py.class +.idea/ + # C extensions *.so diff --git a/gex/TinyFrame.py b/gex/TinyFrame.py new file mode 100644 index 0000000..9ba0c87 --- /dev/null +++ b/gex/TinyFrame.py @@ -0,0 +1,367 @@ +class TF_Msg: + def __init__(self): + self.data = bytearray() + self.len = 0 + self.type = 0 + self.id = 0 + + def __str__(self): + return f"ID {self.id:X}h, type {self.type:X}h, len {self.len:d}, body: {self.data}" + +class TF: + STAY = 'STAY' + RENEW = 'RENEW' + CLOSE = 'CLOSE' + NEXT = 'NEXT' + +class TinyFrame: + def __init__(self, peer=1): + self.write = None # the writer function should be attached here + + self.id_listeners = {} + self.type_listeners = {} + self.fallback_listener = None + self.peer = peer # the peer bit + + # ----------------------------- FRAME FORMAT --------------------------------- + # The format can be adjusted to fit your particular application needs + + # If the connection is reliable, you can disable the SOF byte and checksums. + # That can save up to 9 bytes of overhead. + + # ,-----+----+-----+------+------------+- - - -+------------, + # | SOF | ID | LEN | TYPE | HEAD_CKSUM | DATA | PLD_CKSUM | + # | 1 | ? | ? | ? | ? | ... | ? | <- size (bytes) + # '-----+----+-----+------+------------+- - - -+------------' + + # !!! BOTH SIDES MUST USE THE SAME SETTINGS !!! + + # Adjust sizes as desired (1,2,4) + self.ID_BYTES = 2 + self.LEN_BYTES = 2 + self.TYPE_BYTES = 1 + + # Checksum type + # ('none', 'xor', 'crc16, 'crc32' + self.CKSUM_TYPE = 'xor' + + # Use a SOF byte to mark the start of a frame + self.USE_SOF_BYTE = True + # Value of the SOF byte (if TF_USE_SOF_BYTE == 1) + self.SOF_BYTE = 0x01 + + self.next_frame_id = 0 + + self.reset_parser() + + def reset_parser(self): + # parser state: SOF, ID, LEN, TYPE, HCK, PLD, PCK + self.ps = 'SOF' + # buffer for receiving bytes + self.rbuf = None + # expected number of bytes to receive + self.rlen = 0 + # buffer for payload or checksum + self.rpayload = None + # received frame + self.rf = TF_Msg() + + @property + def _CKSUM_BYTES(self): + if self.CKSUM_TYPE == 'none' or self.CKSUM_TYPE is None: + return 0 + elif self.CKSUM_TYPE == 'xor': + return 1 + elif self.CKSUM_TYPE == 'crc16': + return 2 + elif self.CKSUM_TYPE == 'crc32': + return 2 + else: + raise Exception("Bad cksum type!") + + def _cksum(self, buffer): + if self.CKSUM_TYPE == 'none' or self.CKSUM_TYPE is None: + return 0 + + elif self.CKSUM_TYPE == 'xor': + acc = 0 + for b in buffer: + acc ^= b + return (~acc) & ((1<<(self._CKSUM_BYTES*8))-1) + + elif self.CKSUM_TYPE == 'crc16': + raise Exception("CRC16 not implemented!") + + elif self.CKSUM_TYPE == 'crc32': + raise Exception("CRC32 not implemented!") + + else: + raise Exception("Bad cksum type!") + + @property + def _SOF_BYTES(self): + return 1 if self.USE_SOF_BYTE else 0 + + def _gen_frame_id(self): + """ + Get a new frame ID + """ + + frame_id = self.next_frame_id + + self.next_frame_id += 1 + if self.next_frame_id > ((1<<(8*self.ID_BYTES-1))-1): + self.next_frame_id = 0 + + if self.peer == 1: + frame_id |= 1<<(8*self.ID_BYTES-1) + + return frame_id + + def _pack(self, num, bytes): + """ Pack a number for a TF field """ + return num.to_bytes(bytes, byteorder='big', signed=False) + + def _unpack(self, buf): + """ Unpack a number from a TF field """ + return int.from_bytes(buf, byteorder='big', signed=False) + + def query(self, type, listener, pld=None, id=None): + """ Send a query """ + (id, buf) = self._compose(type=type, pld=pld, id=id) + + if listener is not None: + self.add_id_listener(id, listener) + + self.write(buf) + + def send(self, type, pld=None, id=None): + """ Like query, but with no listener """ + self.query(type=type, pld=pld, id=id, listener=None) + + def _compose(self, type, pld=None, id=None): + """ + Compose a frame. + + frame_id can be an ID of an existing session, None for a new session. + """ + + if pld is None: + pld = b'' + + if id is None: + id = self._gen_frame_id() + + buf = bytearray() + if self.USE_SOF_BYTE: + buf.extend(self._pack(self.SOF_BYTE, 1)) + + buf.extend(self._pack(id, self.ID_BYTES)) + buf.extend(self._pack(len(pld), self.LEN_BYTES)) + buf.extend(self._pack(type, self.TYPE_BYTES)) + + if self._CKSUM_BYTES > 0: + buf.extend(self._pack(self._cksum(buf), self._CKSUM_BYTES)) + + if len(pld) > 0: + buf.extend(pld) + buf.extend(self._pack(self._cksum(pld), self._CKSUM_BYTES)) + + return (id, buf) + + def accept(self, bytes): + """ + Parse bytes received on the serial port + """ + for b in bytes: + self.accept_byte(b) + + def accept_byte(self, b): + if self.ps == 'SOF': + if self.USE_SOF_BYTE: + if b != self.SOF_BYTE: + return + + self.rpayload = bytearray() + self.rpayload.append(b) + + self.ps = 'ID' + self.rlen = self.ID_BYTES + self.rbuf = bytearray() + + if self.USE_SOF_BYTE: + return + + if self.ps == 'ID': + self.rpayload.append(b) + self.rbuf.append(b) + + if len(self.rbuf) == self.rlen: + self.rf.id = self._unpack(self.rbuf) + + self.ps = 'LEN' + self.rlen = self.LEN_BYTES + self.rbuf = bytearray() + return + + if self.ps == 'LEN': + self.rpayload.append(b) + self.rbuf.append(b) + + if len(self.rbuf) == self.rlen: + self.rf.len = self._unpack(self.rbuf) + + self.ps = 'TYPE' + self.rlen = self.TYPE_BYTES + self.rbuf = bytearray() + return + + if self.ps == 'TYPE': + self.rpayload.append(b) + self.rbuf.append(b) + + if len(self.rbuf) == self.rlen: + self.rf.type = self._unpack(self.rbuf) + + if self._CKSUM_BYTES > 0: + self.ps = 'HCK' + self.rlen = self._CKSUM_BYTES + self.rbuf = bytearray() + else: + self.ps = 'PLD' + self.rlen = self.rf.len + self.rbuf = bytearray() + return + + if self.ps == 'HCK': + self.rbuf.append(b) + + if len(self.rbuf) == self.rlen: + hck = self._unpack(self.rbuf) + actual = self._cksum(self.rpayload) + + if hck != actual: + self.reset_parser() + else: + if self.rf.len == 0: + self.handle_rx_frame() + self.reset_parser() + else: + self.ps = 'PLD' + self.rlen = self.rf.len + self.rbuf = bytearray() + self.rpayload = bytearray() + return + + if self.ps == 'PLD': + self.rpayload.append(b) + self.rbuf.append(b) + + if len(self.rbuf) == self.rlen: + self.rf.data = self.rpayload + + if self._CKSUM_BYTES > 0: + self.ps = 'PCK' + self.rlen = self._CKSUM_BYTES + self.rbuf = bytearray() + else: + self.handle_rx_frame() + self.reset_parser() + return + + if self.ps == 'PCK': + self.rbuf.append(b) + + if len(self.rbuf) == self.rlen: + pck = self._unpack(self.rbuf) + actual = self._cksum(self.rpayload) + + if pck != actual: + self.reset_parser() + else: + self.handle_rx_frame() + self.reset_parser() + return + + def handle_rx_frame(self): + frame = self.rf + + if frame.id in self.id_listeners and self.id_listeners[frame.id] is not None: + lst = self.id_listeners[frame.id] + rv = lst['fn'](self, frame) + if rv == TF.CLOSE: + self.id_listeners[frame.id] = None + return + elif rv == TF.RENEW: + lst.age = 0 + return + elif rv == TF.STAY: + return + # TF.NEXT lets another handler process it + + if frame.type in self.type_listeners and self.type_listeners[frame.type] is not None: + lst = self.type_listeners[frame.type] + rv = lst['fn'](self, frame) + if rv == TF.CLOSE: + self.type_listeners[frame.type] = None + return + elif rv != TF.NEXT: + return + + if self.fallback_listener is not None: + lst = self.fallback_listener + rv = lst['fn'](self, frame) + if rv == TF.CLOSE: + self.fallback_listener = None + + def add_id_listener(self, id, lst, lifetime=None): + """ + Add a ID listener that expires in "lifetime" ticks + + listener function takes two arguments: + tinyframe instance and a msg object + """ + self.id_listeners[id] = { + 'fn': lst, + 'lifetime': lifetime, + 'age': 0, + } + + def add_type_listener(self, type, lst): + """ + Add a type listener + + listener function takes two arguments: + tinyframe instance and a msg object + """ + self.type_listeners[type] = { + 'fn': lst, + } + + def add_fallback_listener(self, lst): + """ + Add a fallback listener + + listener function takes two arguments: + tinyframe instance and a msg object + """ + self.fallback_listener = { + 'fn': lst, + } + + + + + + + + + + + + + + + + + diff --git a/gex/__init__.py b/gex/__init__.py new file mode 100644 index 0000000..cdbdafe --- /dev/null +++ b/gex/__init__.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 + +import serial + +from gex.TinyFrame import TinyFrame + +class Gex: + # General, low level + MSG_SUCCESS = 0x00 # Generic success response; used by default in all responses; payload is transaction-specific + MSG_PING = 0x01 # Ping request (or response), used to test connection + MSG_ERROR = 0x02 # Generic failure response (when a request fails to execute) + + MSG_BULK_READ_OFFER = 0x03 # Offer of data to read. Payload: u32 total len + MSG_BULK_READ_POLL = 0x04 # Request to read a previously announced chunk. Payload: u32 max chunk + MSG_BULK_WRITE_OFFER = 0x05 # Offer to receive data in a write transaction. Payload: u32 max size, u32 max chunk + MSG_BULK_DATA = 0x06 # Writing a chunk, or sending a chunk to master. + MSG_BULK_END = 0x07 # Bulk transfer is done, no more data to read or write. + # Recipient shall check total len and discard it on mismatch. There could be a checksum ... + MSG_BULK_ABORT = 0x08 # Discard the ongoing transfer + + # Unit messages + MSG_UNIT_REQUEST = 0x10 # Command addressed to a particular unit + MSG_UNIT_REPORT = 0x11 # Spontaneous report from a unit + + # System messages + MSG_LIST_UNITS = 0x20 # Get all unit call-signs and names + MSG_INI_READ = 0x21 # Read the ini file via bulk + MSG_INI_WRITE = 0x22 # Write the ini file via bulk + MSG_PERSIST_SETTINGS = 0x23 # Write current settings to Flash + + def __init__(self, port='/dev/ttyACM0', timeout=0.2): + self.port = port + self.serial = serial.Serial(port=port, timeout=timeout) + self.tf = TinyFrame() + self.tf.write = self._write + + def _write(self, data): + self.serial.write(data) + pass + + def poll(self): + attempts = 10 + + first = True + while attempts > 0: + rv = bytearray() + + # Blocking read with a timeout + if first: + rv.extend(self.serial.read(1)) + first = False + + # Non-blocking read of the rest + rv.extend(self.serial.read(self.serial.in_waiting)) + + if 0 == len(rv): + # nothing was read + if self.tf.ps == 'SOF': + # TF is in base state, we're done + return + else: + # Wait for TF to finish the frame + attempts -= 1 + first = True + else: + self.tf.accept(rv) + + def _send(self, type, id=None, pld=None, listener=None): + self.tf.query(type=type, pld=pld, id=id, listener=listener) + + def send(self, cs, cmd, id=None, pld=None, listener=None): + if cs is None: + return self._send(type=cmd, id=id, pld=pld, listener=listener) + + if pld is None: + pld = b'' + + buf = bytearray([cs, cmd]) + buf.extend(pld) + self._send(type=self.MSG_UNIT_REQUEST, id=id, pld=buf, listener=listener) + + def query(self, cs, cmd, id=None, pld=None): + """ Query a unit """ + self._theframe = None + def lst(tf, frame): + self._theframe = frame + + self.send(cs, cmd, id=id, pld=pld, listener=lst) + self.poll() + + if self._theframe is None: + raise Exception("No response to query") + + return self._theframe + + def query_raw(self, type, id=None, pld=None): + """ Query without addressing a unit """ + return self.query(cs=None, cmd=type, id=id, pld=pld) + + def send_raw(self, type, id=None, pld=None): + """ Send without addressing a unit """ + return self.send(cs=None, cmd=type, id=id, pld=pld) \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..f1d460a --- /dev/null +++ b/main.py @@ -0,0 +1,31 @@ +#!/bin/env python3 +import time + +from gex import Gex + +gex = Gex() + +# Check connection +resp = gex.query_raw(type=Gex.MSG_PING) +print("Ping resp = ", resp.data.decode("ascii")) + +# Blink a LED at call-sign 1, command 0x02 = toggle +for i in range(0,10): + gex.send(cs=1, cmd=0x02) + time.sleep(.2) + + +# +# port = serial.Serial( +# port='/dev/ttyACM0', +# timeout=0.1 +# ) +# +# print("Send request") +# port.write(b'\x01\x80\x00\x00\x00\x01\x7f') +# +# print("Wait for response") +# rv = port.read(1) +# rv += port.read(port.in_waiting) +# +# print(rv)