code import
This commit is contained in:
@@ -3,6 +3,8 @@ __pycache__/
|
|||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
|
|
||||||
|
.idea/
|
||||||
|
|
||||||
# C extensions
|
# C extensions
|
||||||
*.so
|
*.so
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
+102
@@ -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)
|
||||||
@@ -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)
|
||||||
Reference in New Issue
Block a user