From 02f92bef2a1d051a099d303b44cea78c62b30a5f Mon Sep 17 00:00:00 2001 From: Thomas Hooge Date: Sat, 21 Feb 2026 08:45:39 +0100 Subject: [PATCH] Started implementing message class, parsing rx and tx pgn list --- __init__.py | 1 + device.py | 114 +++++++++++++++++++++++++++++++++++++++++++++++++--- message.py | 106 ++++++++++++++++++++++++++++++++++++++++++++++++ parser.py | 17 +++++++- 4 files changed, 231 insertions(+), 7 deletions(-) create mode 100644 message.py diff --git a/__init__.py b/__init__.py index bd9905b..f145913 100644 --- a/__init__.py +++ b/__init__.py @@ -3,3 +3,4 @@ __version__ = "0.0.1" from .device import Device from .boatdata import BoatData from .hbuffer import History, HistoryBuffer +from .message import Message diff --git a/device.py b/device.py index 443e7d3..7c191e5 100644 --- a/device.py +++ b/device.py @@ -12,6 +12,24 @@ import time import struct from . import lookup +def set_to_lines(values, maxlen, indent=0): + spaces = ' ' * indent + lines = [] + current = "" + for v in values: + part = str(v) + emax = maxlen - indent + if not current: + current = part + elif len(current) + 1 + len(part) <= emax: + current += ',' + part + else: + lines.append(spaces + current + ',') + current = part + if current: + lines.append(spaces + current) + return lines + class Device(): def __init__(self, address): @@ -21,6 +39,7 @@ class Device(): self.lastpinfo = None # Wann letztes Mal Productinfo erhalten? self.lastcinfo = None # Wann letztes Mal Configurationinfo erhalten? self.has_cinfo = True # Weitere Abfragen können verhindert werden + self.has_pgnlist = False # PGN-Listen Tx,Rx vorhanden # Device info self.NAME = 0 # Wird über getNAME (address claim) gefüllt, 64bit Integer @@ -30,14 +49,13 @@ class Device(): self.instlower = 0 # 3bit, ISO ECU Instance self.instupper = 0 # 5bit, ISO Function Instance self.sysinstance = 0 # used with bridged networks, default 0 - self.industrygroup = None self.devicefunction = None self.deviceclass = None # Product info - self.product = None # Product name - self.productcode = None # Product code - self.serial = None + self.product = None # Product name (ModelID) + self.productcode = None # Product code (16bit unsigned) + self.serial = None # Device serial number self.modelvers = None # Hardware Version self.softvers = None # Current Software Version self.n2kvers = None # NMEA2000 Network Message Database Version @@ -47,7 +65,11 @@ class Device(): # Configuration info self.instdesc1 = None self.instdesc2 = None - self.manufinfo = None + self.manufinfo = None + + # PGN lists, fill with functions defined below + self.pgns_transmit = set() + self.pgns_receive = set() # Additional data self.customname = None # User defined device name @@ -120,7 +142,7 @@ class Device(): print("Ignore collision because of our lower NAME") else: print(f"Answer: DEST={dest}") - print(msg.data) + print("Data:", msg.data) if dest == self.address: print("We are addressed: WIP!") else: @@ -133,6 +155,84 @@ class Device(): print("claim seems ok after 250ms") return claim_ok + # TODO String handling + # Spec says ASCII is only 0x20 to 0x7E. But some devices use full + # Latin-1 / ISO-8859-1 code set. + # Unicode: + # UTF-16 Little Endian + # UTF-8 eventually used in some PGNs? + def getStringFix(self, text, maxlength=32, filler=b' '): + if not text: + text = '' + if text.isascii(): + # text.encode('latin1') + # text.encode('ascii') can throw errors + # text.encode('ascii', 'replace') sets question mark for unknown codes + #return b'x01' + bytes(text or '', 'ascii').ljust(maxlength, b' ') + return b'x01' + text.encode('ascii', 'ignore').ljust(maxlength, filler) + else: + # convert to UTF-16 Little Endian + return b'x00' + text.encode('utf-16-le').ljust(maxlength, filler) + + def getProductInfo(self): + """ + Returns data for msg prepared to be sent out as fast packet + """ + print("Set Productinfo:", self.product) + data = bytearray() + data.extend(struct.pack(' 223: + raise ValueError("data for fast packet too long") + if datalen < 7: + raise ValueError("data for fast packet too short") + nf = ceil((datalen - 6) / 7 + 1) # number of frames + fc = 0 # frame counter, nibble 2 + frames = list() + # Frame 1 + data = bytearray() + data.append((sc << 5) + fc) + data.append(datalen) + for b in rawdata[:6]: + data.append(b) + frames.append(data) + nf -= 1 + # Frame 2..n + p = 6 + while nf > 0: + data = bytearray() + fc += 1 + data.append((sc << 5) + fc) + for b in rawdata[p:p+7]: + data.append(b) + frames.append(data) + p += 7 + nf -= 1 + print("message:get_fast_frames") + print(frames) + return frames + + def send_single(self): + msg = can.Message( + arbitration_id = (((self._priority << 18) + self.pgn) << 8) + self._source, + data = self._data, + is_extended_id = True + ); + try: + self._bus.send(msg) + except can.CanError: + print(f"Message {self.pgn} NOT sent") + return False + return True + + def send_fast(self): + id = (((self._priority << 18) + self.pgn) << 8) + self._source + try: + for frame in self.get_fast_frames(self._data, self.sequence): + msg = can.Message( + arbitration_id = id, + data = frame, + is_extended_id = True + ); + self._bus.send(msg) + except can.CanError: + print(f"Message {self.pgn} NOT sent") + return False + finally: + self.sequence += 1 + self.sequence %= 8 + print("send fast: adjusted sequence {}".format(self.sequence)) + return True + + def send(self): + if pgntype(self.pgn) == "S": + self.send_single() + else: + self.send_fast() diff --git a/parser.py b/parser.py index d7ba736..06be9d8 100644 --- a/parser.py +++ b/parser.py @@ -26,6 +26,22 @@ def parse_60928(buf, device): device.sysinstance = buf[7] & 0x0f # 4bit return struct.unpack_from('>Q', buf, 0)[0] +def parse_126464(buf, device): + # PGN Lists: tx and rx + # Byte 1: 0 = Transmit PGN list + # 1 = Receive PGN list + functioncode = buf[0] + # set of 24bit PGNs (little endian) + values = { buf[i] | (buf[i+1] << 8) | (buf[i+2] << 16) + for i in range(1, len(buf)-4, 3) } + if functioncode == 0: + device.pgns_transmit = values + elif functioncode == 1: + device.pgns_receive = values + else: + return + device.has_pgnlist = True + def parse_126983(buf, source): # Alert # PGN 126984 is response @@ -93,7 +109,6 @@ def parse_126992(buf, source): def parse_126993(buf, device): # Heartbeat print(f"Heartbeat from {device.address}") - print(buf) def parse_126996(buf, device): # Product information