Started implementing message class, parsing rx and tx pgn list

This commit is contained in:
2026-02-21 08:45:39 +01:00
parent 1f5aac778c
commit 02f92bef2a
4 changed files with 231 additions and 7 deletions

View File

@@ -3,3 +3,4 @@ __version__ = "0.0.1"
from .device import Device from .device import Device
from .boatdata import BoatData from .boatdata import BoatData
from .hbuffer import History, HistoryBuffer from .hbuffer import History, HistoryBuffer
from .message import Message

114
device.py
View File

@@ -12,6 +12,24 @@ import time
import struct import struct
from . import lookup 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(): class Device():
def __init__(self, address): def __init__(self, address):
@@ -21,6 +39,7 @@ class Device():
self.lastpinfo = None # Wann letztes Mal Productinfo erhalten? self.lastpinfo = None # Wann letztes Mal Productinfo erhalten?
self.lastcinfo = None # Wann letztes Mal Configurationinfo erhalten? self.lastcinfo = None # Wann letztes Mal Configurationinfo erhalten?
self.has_cinfo = True # Weitere Abfragen können verhindert werden self.has_cinfo = True # Weitere Abfragen können verhindert werden
self.has_pgnlist = False # PGN-Listen Tx,Rx vorhanden
# Device info # Device info
self.NAME = 0 # Wird über getNAME (address claim) gefüllt, 64bit Integer 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.instlower = 0 # 3bit, ISO ECU Instance
self.instupper = 0 # 5bit, ISO Function Instance self.instupper = 0 # 5bit, ISO Function Instance
self.sysinstance = 0 # used with bridged networks, default 0 self.sysinstance = 0 # used with bridged networks, default 0
self.industrygroup = None
self.devicefunction = None self.devicefunction = None
self.deviceclass = None self.deviceclass = None
# Product info # Product info
self.product = None # Product name self.product = None # Product name (ModelID)
self.productcode = None # Product code self.productcode = None # Product code (16bit unsigned)
self.serial = None self.serial = None # Device serial number
self.modelvers = None # Hardware Version self.modelvers = None # Hardware Version
self.softvers = None # Current Software Version self.softvers = None # Current Software Version
self.n2kvers = None # NMEA2000 Network Message Database Version self.n2kvers = None # NMEA2000 Network Message Database Version
@@ -47,7 +65,11 @@ class Device():
# Configuration info # Configuration info
self.instdesc1 = None self.instdesc1 = None
self.instdesc2 = 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 # Additional data
self.customname = None # User defined device name self.customname = None # User defined device name
@@ -120,7 +142,7 @@ class Device():
print("Ignore collision because of our lower NAME") print("Ignore collision because of our lower NAME")
else: else:
print(f"Answer: DEST={dest}") print(f"Answer: DEST={dest}")
print(msg.data) print("Data:", msg.data)
if dest == self.address: if dest == self.address:
print("We are addressed: WIP!") print("We are addressed: WIP!")
else: else:
@@ -133,6 +155,84 @@ class Device():
print("claim seems ok after 250ms") print("claim seems ok after 250ms")
return claim_ok 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('<H', self.n2kvers)) # 2 Byte
data.extend(struct.pack('<H', self.productcode)) # 2 Byte
data.append(0x01) # String
data.extend(bytes(self.product, 'latin1').ljust(32, b' '))
#data.extend(getStringFix(self.product))
data.append(0x01) # ASCII String
data.extend(bytes(self.softvers, 'latin1').ljust(32, b' '))
data.append(0x01)
data.extend(bytes(self.modelvers, 'latin1').ljust(32, b' '))
data.append(0x01)
data.extend(bytes(self.serial, 'latin1').ljust(32, b' '))
data.append(0x00) # Certification level
data.append(0x00) # Load equivalency 0=Not powered by bus
return data
def getConfigInfo(self):
"""
Configuration info
"""
print("Set Configinfo")
data = bytearray()
data.extend(self.getStringFix(self.instdesc1), 32)
data.extend(self.getStringFix(self.instdesc2), 32)
data.extend(self.getStringFix(self.manufinfo), 32)
#data.append(0x01) # ASCII String
#data.extend(bytes(self.instdesc1 or '', 'ascii').ljust(32, b' '))
#data.append(0x01)
#data.extend(bytes(self.instdesc2 or '', 'ascii').ljust(32, b' '))
#data.append(0x01)
#data.extend(bytes(self.manufinfo or '', 'ascii').ljust(32, b' '))
return data
"""
PGN lists (mandatory)
59904 ISO Request
60928 ISO Address Claim
126208 Group Function (Request / Command / Acknowledge)
126464 PGN List (Transmit / Receive)
126993 Heartbeat
126996 Product Information
126998 Configuration Information
"""
def setTxPGNs(self, pgnlist, mandatory=True):
self.pgns_transmit = set(pgnlist)
if mandatory:
self.pgns_transmit.add((60928, 126208, 126464, 126996))
def setRxPGNs(self, pgnlist, mandatory=True):
self.pgns_receive = set(pgnlist)
if mandatory:
self.pgns_receive.add((59904, 60928, 126208, 126996, 126998))
def __str__(self): def __str__(self):
out = f"Device: {self.address} : '{self.product}'\n" out = f"Device: {self.address} : '{self.product}'\n"
out += " NAME: {} ({})\n".format(self.getNAME(), self.NAME) out += " NAME: {} ({})\n".format(self.getNAME(), self.NAME)
@@ -177,4 +277,6 @@ class Device():
out += f" Manufacturer info: {self.manufinfo}\n" out += f" Manufacturer info: {self.manufinfo}\n"
else: else:
out += " not available\n" out += " not available\n"
out += " PGNs RX: \n{}\n".format('\n'.join(set_to_lines(self.pgns_receive, 72, 4)) or 'n/a')
out += " PGNs TX: \n{}\n".format('\n'.join(set_to_lines(self.pgns_transmit, 72, 4)) or 'n/a')
return out return out

106
message.py Normal file
View File

@@ -0,0 +1,106 @@
"""
NMEA2000 Nachricht
"""
import can
import struct
from math import ceil
from .pgntype import pgntype
class Message():
def __init__(self, bus, source, data=None):
self._bus = bus
self._source = source
self._priority = 5
if data:
print("msg: set data: ", type(data))
self._data = data
else:
self._data = bytearray()
self.pgn = None
#self.sequence = 0xff
self.sequence = 0
def set_pgn(self, pgn):
self.pgn = pgn
def set_priority(self, priority):
self._priority = priority
def get_fast_frames(self, rawdata, sc):
# Erstelle ein Liste von Frames aufgrund vorliegender Rohdaten
# zum transferieren von bis zu 223 Bytes über eine fast-Message
# Der sequence-counter muß extern inkrementiert werden, er hat
# 3 Bit Länge. Der Framecounter fc hat eine Länge von 5 Bit.
# Der erste Frame kann 6 Bytes aufnehmen
# Alle weiteren Frames jeweils 7 Bytes
datalen = len(rawdata)
if datalen > 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()

View File

@@ -26,6 +26,22 @@ def parse_60928(buf, device):
device.sysinstance = buf[7] & 0x0f # 4bit device.sysinstance = buf[7] & 0x0f # 4bit
return struct.unpack_from('>Q', buf, 0)[0] 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): def parse_126983(buf, source):
# Alert # Alert
# PGN 126984 is response # PGN 126984 is response
@@ -93,7 +109,6 @@ def parse_126992(buf, source):
def parse_126993(buf, device): def parse_126993(buf, device):
# Heartbeat # Heartbeat
print(f"Heartbeat from {device.address}") print(f"Heartbeat from {device.address}")
print(buf)
def parse_126996(buf, device): def parse_126996(buf, device):
# Product information # Product information