From 13eac9508d554a043d4099678352c55888ba82b7 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Tue, 23 Sep 2025 18:29:43 +0200 Subject: [PATCH 1/7] add some helper tools for converting candumps --- tools/getPgnType.py | 29 +++ tools/sendN2K.py | 527 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 556 insertions(+) create mode 100755 tools/getPgnType.py create mode 100644 tools/sendN2K.py diff --git a/tools/getPgnType.py b/tools/getPgnType.py new file mode 100755 index 0000000..edbdbb2 --- /dev/null +++ b/tools/getPgnType.py @@ -0,0 +1,29 @@ +#! /usr/bin/env python3 +import sys +import json + +def err(txt): + print(txt,file=sys.stderr) + sys.exit(1) + +HDR=''' +PGNM_Fast=0 +PGNM_Single=1 +PGNM_ISO=2 +PGN_MODES={ +''' +FOOTER=''' + } +''' +with open(sys.argv[1],"r") as ih: + data=json.load(ih) + pgns=data.get('PGNs') + if pgns is None: + err("no pgns") + print(HDR) + for p in pgns: + t=p['Type'] + pgn=p['PGN'] + if t and pgn: + print(f" {pgn}: PGNM_{t},") + print(FOOTER) \ No newline at end of file diff --git a/tools/sendN2K.py b/tools/sendN2K.py new file mode 100644 index 0000000..c05bbda --- /dev/null +++ b/tools/sendN2K.py @@ -0,0 +1,527 @@ +#! /usr/bin/env python3 +import re +import sys +import os +import datetime + +###generated with getPgnType.py from canboat pgns.json +PGNM_Fast=0 +PGNM_Single=1 +PGNM_ISO=2 +PGN_MODES={ + + 59392: PGNM_Single, + 59904: PGNM_Single, + 60160: PGNM_Single, + 60416: PGNM_Single, + 60416: PGNM_Single, + 60416: PGNM_Single, + 60416: PGNM_Single, + 60416: PGNM_Single, + 60928: PGNM_Single, + 61184: PGNM_Single, + 61184: PGNM_Single, + 61184: PGNM_Single, + 65001: PGNM_Single, + 65002: PGNM_Single, + 65003: PGNM_Single, + 65004: PGNM_Single, + 65005: PGNM_Single, + 65006: PGNM_Single, + 65007: PGNM_Single, + 65008: PGNM_Single, + 65009: PGNM_Single, + 65010: PGNM_Single, + 65011: PGNM_Single, + 65012: PGNM_Single, + 65013: PGNM_Single, + 65014: PGNM_Single, + 65015: PGNM_Single, + 65016: PGNM_Single, + 65017: PGNM_Single, + 65018: PGNM_Single, + 65019: PGNM_Single, + 65020: PGNM_Single, + 65021: PGNM_Single, + 65022: PGNM_Single, + 65023: PGNM_Single, + 65024: PGNM_Single, + 65025: PGNM_Single, + 65026: PGNM_Single, + 65027: PGNM_Single, + 65028: PGNM_Single, + 65029: PGNM_Single, + 65030: PGNM_Single, + 65240: PGNM_ISO, + 65280: PGNM_Single, + 65284: PGNM_Single, + 65285: PGNM_Single, + 65285: PGNM_Single, + 65286: PGNM_Single, + 65286: PGNM_Single, + 65287: PGNM_Single, + 65287: PGNM_Single, + 65288: PGNM_Single, + 65289: PGNM_Single, + 65290: PGNM_Single, + 65292: PGNM_Single, + 65293: PGNM_Single, + 65293: PGNM_Single, + 65302: PGNM_Single, + 65305: PGNM_Single, + 65305: PGNM_Single, + 65305: PGNM_Single, + 65305: PGNM_Single, + 65305: PGNM_Single, + 65309: PGNM_Single, + 65312: PGNM_Single, + 65340: PGNM_Single, + 65341: PGNM_Single, + 65345: PGNM_Single, + 65350: PGNM_Single, + 65359: PGNM_Single, + 65360: PGNM_Single, + 65361: PGNM_Single, + 65371: PGNM_Single, + 65374: PGNM_Single, + 65379: PGNM_Single, + 65408: PGNM_Single, + 65409: PGNM_Single, + 65410: PGNM_Single, + 65420: PGNM_Single, + 65480: PGNM_Single, + 126208: PGNM_Fast, + 126208: PGNM_Fast, + 126208: PGNM_Fast, + 126208: PGNM_Fast, + 126208: PGNM_Fast, + 126208: PGNM_Fast, + 126208: PGNM_Fast, + 126464: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126983: PGNM_Fast, + 126984: PGNM_Fast, + 126985: PGNM_Fast, + 126986: PGNM_Fast, + 126987: PGNM_Fast, + 126988: PGNM_Fast, + 126992: PGNM_Single, + 126993: PGNM_Single, + 126996: PGNM_Fast, + 126998: PGNM_Fast, + 127233: PGNM_Fast, + 127237: PGNM_Fast, + 127245: PGNM_Single, + 127250: PGNM_Single, + 127251: PGNM_Single, + 127252: PGNM_Single, + 127257: PGNM_Single, + 127258: PGNM_Single, + 127488: PGNM_Single, + 127489: PGNM_Fast, + 127490: PGNM_Fast, + 127491: PGNM_Fast, + 127493: PGNM_Single, + 127494: PGNM_Fast, + 127495: PGNM_Fast, + 127496: PGNM_Fast, + 127497: PGNM_Fast, + 127498: PGNM_Fast, + 127500: PGNM_Single, + 127501: PGNM_Single, + 127502: PGNM_Single, + 127503: PGNM_Fast, + 127504: PGNM_Fast, + 127505: PGNM_Single, + 127506: PGNM_Fast, + 127507: PGNM_Fast, + 127508: PGNM_Single, + 127509: PGNM_Fast, + 127510: PGNM_Fast, + 127511: PGNM_Single, + 127512: PGNM_Single, + 127513: PGNM_Fast, + 127514: PGNM_Single, + 127744: PGNM_Single, + 127745: PGNM_Single, + 127746: PGNM_Single, + 127750: PGNM_Single, + 127751: PGNM_Single, + 128000: PGNM_Single, + 128001: PGNM_Single, + 128002: PGNM_Single, + 128003: PGNM_Single, + 128006: PGNM_Single, + 128007: PGNM_Single, + 128008: PGNM_Single, + 128259: PGNM_Single, + 128267: PGNM_Single, + 128275: PGNM_Fast, + 128520: PGNM_Fast, + 128538: PGNM_Fast, + 128768: PGNM_Single, + 128769: PGNM_Single, + 128776: PGNM_Single, + 128777: PGNM_Single, + 128778: PGNM_Single, + 128780: PGNM_Single, + 129025: PGNM_Single, + 129026: PGNM_Single, + 129027: PGNM_Single, + 129028: PGNM_Single, + 129029: PGNM_Fast, + 129033: PGNM_Single, + 129038: PGNM_Fast, + 129039: PGNM_Fast, + 129040: PGNM_Fast, + 129041: PGNM_Fast, + 129044: PGNM_Fast, + 129045: PGNM_Fast, + 129283: PGNM_Single, + 129284: PGNM_Fast, + 129285: PGNM_Fast, + 129291: PGNM_Single, + 129301: PGNM_Fast, + 129302: PGNM_Fast, + 129538: PGNM_Fast, + 129539: PGNM_Single, + 129540: PGNM_Fast, + 129541: PGNM_Fast, + 129542: PGNM_Fast, + 129545: PGNM_Fast, + 129546: PGNM_Single, + 129547: PGNM_Fast, + 129549: PGNM_Fast, + 129550: PGNM_Fast, + 129551: PGNM_Fast, + 129556: PGNM_Fast, + 129792: PGNM_Fast, + 129793: PGNM_Fast, + 129794: PGNM_Fast, + 129795: PGNM_Fast, + 129796: PGNM_Fast, + 129797: PGNM_Fast, + 129798: PGNM_Fast, + 129799: PGNM_Fast, + 129800: PGNM_Fast, + 129801: PGNM_Fast, + 129802: PGNM_Fast, + 129803: PGNM_Fast, + 129804: PGNM_Fast, + 129805: PGNM_Fast, + 129806: PGNM_Fast, + 129807: PGNM_Fast, + 129808: PGNM_Fast, + 129808: PGNM_Fast, + 129809: PGNM_Fast, + 129810: PGNM_Fast, + 130052: PGNM_Fast, + 130053: PGNM_Fast, + 130054: PGNM_Fast, + 130060: PGNM_Fast, + 130061: PGNM_Fast, + 130064: PGNM_Fast, + 130065: PGNM_Fast, + 130066: PGNM_Fast, + 130067: PGNM_Fast, + 130068: PGNM_Fast, + 130069: PGNM_Fast, + 130070: PGNM_Fast, + 130071: PGNM_Fast, + 130072: PGNM_Fast, + 130073: PGNM_Fast, + 130074: PGNM_Fast, + 130306: PGNM_Single, + 130310: PGNM_Single, + 130311: PGNM_Single, + 130312: PGNM_Single, + 130313: PGNM_Single, + 130314: PGNM_Single, + 130315: PGNM_Single, + 130316: PGNM_Single, + 130320: PGNM_Fast, + 130321: PGNM_Fast, + 130322: PGNM_Fast, + 130323: PGNM_Fast, + 130324: PGNM_Fast, + 130330: PGNM_Fast, + 130560: PGNM_Single, + 130561: PGNM_Fast, + 130562: PGNM_Fast, + 130563: PGNM_Fast, + 130564: PGNM_Fast, + 130565: PGNM_Fast, + 130566: PGNM_Fast, + 130567: PGNM_Fast, + 130569: PGNM_Fast, + 130570: PGNM_Fast, + 130571: PGNM_Fast, + 130572: PGNM_Fast, + 130573: PGNM_Fast, + 130574: PGNM_Fast, + 130576: PGNM_Single, + 130577: PGNM_Fast, + 130578: PGNM_Fast, + 130579: PGNM_Single, + 130580: PGNM_Fast, + 130581: PGNM_Fast, + 130582: PGNM_Single, + 130583: PGNM_Fast, + 130584: PGNM_Fast, + 130585: PGNM_Single, + 130586: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130817: PGNM_Fast, + 130817: PGNM_Fast, + 130818: PGNM_Fast, + 130819: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130821: PGNM_Fast, + 130821: PGNM_Fast, + 130822: PGNM_Fast, + 130823: PGNM_Fast, + 130824: PGNM_Fast, + 130824: PGNM_Fast, + 130825: PGNM_Fast, + 130827: PGNM_Fast, + 130828: PGNM_Fast, + 130831: PGNM_Fast, + 130832: PGNM_Fast, + 130833: PGNM_Fast, + 130834: PGNM_Fast, + 130835: PGNM_Fast, + 130836: PGNM_Fast, + 130836: PGNM_Fast, + 130837: PGNM_Fast, + 130837: PGNM_Fast, + 130838: PGNM_Fast, + 130839: PGNM_Fast, + 130840: PGNM_Fast, + 130842: PGNM_Fast, + 130842: PGNM_Fast, + 130842: PGNM_Fast, + 130843: PGNM_Fast, + 130843: PGNM_Fast, + 130845: PGNM_Fast, + 130845: PGNM_Fast, + 130846: PGNM_Fast, + 130846: PGNM_Fast, + 130847: PGNM_Fast, + 130850: PGNM_Fast, + 130850: PGNM_Fast, + 130850: PGNM_Fast, + 130851: PGNM_Fast, + 130856: PGNM_Fast, + 130860: PGNM_Fast, + 130880: PGNM_Fast, + 130881: PGNM_Fast, + 130944: PGNM_Fast, + + } + + + + +def logError(fmt,*args): + print("ERROR:" +fmt%(args)) + +def dataToSep(data,maxbytes=None): + pd=None + dl=int(len(data)/2) + if maxbytes is not None and maxbytes < dl: + dl=maxbytes + for p in range(0,dl): + i=2*p + if pd is None: + pd=data[i:i+2] + else: + pd+=","+data[i:i+2] + return pd + +class CanFrame: + DUMP_PAT=re.compile(r'\(([^)]*)\) *([^ ]*) *([^#]*)#(.*)') + + def __init__(self,ts,pgn,src=1,dst=255,prio=1,data=None): + self.pgn=pgn + self.mode=PGN_MODES.get(pgn) + self.ts=ts + self.src=src + self.dst=dst + self.data=data + self.prio=prio + self.sequence=None + self.frame=None + if self.mode == PGNM_Fast and data is not None and len(self.data) >= 2: + fb=int(data[0:2],16) + self.frame=fb & 0x1f + self.sequence=fb >> 5 + + def key(self): + if self.sequence is None or self.pgn == 0: + return None + return f"{self.pgn}-{self.sequence}" + def getFPNum(self,bytes=False): + if self.frame != 0: + return None + if len(self.data) < 4: + return None + numbytes=int(self.data[2:4],16) + if bytes: + return numbytes + frames=int((numbytes-6-1)/7)+1+1 if numbytes > 6 else 1 + return frames + + def __str__(self): + return f"{self.ts},{self.prio},{self.pgn},{self.src},{self.dst},{int(len(self.data)/2 if self.data else 0)},{dataToSep(self.data)}" + + + @classmethod + def fromDump(cls,line): + '''(1658058069.867835) can0 09F80103#ACAF6C20B79AAC06''' + match=cls.DUMP_PAT.search(line) + if match is None: + logError("no dump pattern in line %s",line) + return + ts=match[1] + dt=datetime.datetime.fromtimestamp(float(ts),tz=datetime.UTC) + tstr=dt.strftime("%F-%T.")+dt.strftime("%f")[0:3] + data=match[4] + hdr=match[3] + hdrval=int(hdr,16) + #see candump2analyzer + src=hdrval & 0xff + prio=(hdrval >> 26) & 0x7 + PF=(hdrval >> 16) & 0xff + PS=(hdrval >> 8) & 0xff + RDP=(hdrval >> 24) & 3 + pgn=0 + if PF < 240: + dst=PS + pgn=(RDP << 16) + (PF << 8) + else: + dst=0xff + pgn=(RDP << 16) + (PF << 8)+PS + return CanFrame(tstr,pgn,src=src,dst=dst,prio=prio,data=data) + +class MultiFrame: + def __init__(self,firstFrame: CanFrame): + self.bytes="" + self.firstFrame=firstFrame + self.numFrames=firstFrame.getFPNum(bytes=False) + self.numBytes=firstFrame.getFPNum(bytes=True) + self.finished=False + self.addFrame(firstFrame) + def addFrame(self,frame:CanFrame): + if self.finished: + return False + if frame.frame is None: + return False + if frame.frame == 0: + self.bytes+=frame.data[4:] + else: + self.bytes+=frame.data[2:] + if frame.frame >= (self.numFrames-1): + self.finished=True + return True + + def __str__(self): + return f"{self.firstFrame.ts},{self.firstFrame.prio},{self.firstFrame.pgn},{self.firstFrame.src},{self.firstFrame.dst},{self.numBytes},{dataToSep(self.bytes,self.numBytes)}" + + +if __name__ == '__main__': + with open (sys.argv[1],"r") as fh: + buffer={} + lnr=0 + for line in fh: + lnr+=1 + frame=CanFrame.fromDump(line) + if frame.sequence is None: + print(frame) + else: + key=frame.key() + mf=buffer.get(key) + mustDelete=False + if mf is None: + if frame.frame != 0: + print(f"floating multi frame in line {lnr}: {frame}",file=sys.stderr) + continue + mf=MultiFrame(frame) + if not mf.finished: + buffer[key]=mf + else: + mf.addFrame(frame) + mustDelete=True + if mf.finished: + print(mf) + del buffer[key] + \ No newline at end of file From 4b03fa5a236ed16010c0ff1dc9fe600e8db948ce Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Tue, 23 Sep 2025 19:07:53 +0200 Subject: [PATCH 2/7] add filter to sendN2K --- tools/sendN2K.py | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) mode change 100644 => 100755 tools/sendN2K.py diff --git a/tools/sendN2K.py b/tools/sendN2K.py old mode 100644 new mode 100755 index c05bbda..0b84c96 --- a/tools/sendN2K.py +++ b/tools/sendN2K.py @@ -3,6 +3,7 @@ import re import sys import os import datetime +import getopt ###generated with getPgnType.py from canboat pgns.json PGNM_Fast=0 @@ -429,7 +430,7 @@ class CanFrame: def key(self): if self.sequence is None or self.pgn == 0: return None - return f"{self.pgn}-{self.sequence}" + return f"{self.pgn}-{self.sequence}-{self.src}" def getFPNum(self,bytes=False): if self.frame != 0: return None @@ -497,14 +498,38 @@ class MultiFrame: def __str__(self): return f"{self.firstFrame.ts},{self.firstFrame.prio},{self.firstFrame.pgn},{self.firstFrame.src},{self.firstFrame.dst},{self.numBytes},{dataToSep(self.bytes,self.numBytes)}" +def usage(): + print(f"usage: {sys.argv[0]} [-q] [-p pgn,pgn,...] file") + sys.exit(1) if __name__ == '__main__': - with open (sys.argv[1],"r") as fh: + try: + opts,args=getopt.getopt(sys.argv[1:],"hp:q") + except getopt.GetoptError as err: + err(err) + pgnlist=[] + quiet=False + for o,a in opts: + if o == '-h': + usage() + elif o == '-q': + quiet=True + elif o == '-p': + pgns=(int(x) for x in a.split(",")) + pgnlist.extend(pgns) + if len(args) < 1: + usage() + hasFilter=len(pgnlist) > 0 + if not quiet and hasFilter: + print(f"PGNs: {','.join(str(x) for x in pgnlist)}") + with open (args[0],"r") as fh: buffer={} lnr=0 for line in fh: lnr+=1 frame=CanFrame.fromDump(line) + if hasFilter and not frame.pgn in pgnlist: + continue if frame.sequence is None: print(frame) else: @@ -513,7 +538,8 @@ if __name__ == '__main__': mustDelete=False if mf is None: if frame.frame != 0: - print(f"floating multi frame in line {lnr}: {frame}",file=sys.stderr) + if not quiet: + print(f"floating multi frame in line {lnr}: {frame}",file=sys.stderr) continue mf=MultiFrame(frame) if not mf.finished: From e5c4f0b17958b5c640d98aa03bb2660a10efb491 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Tue, 23 Sep 2025 20:33:34 +0200 Subject: [PATCH 3/7] add actisense mode to sendN2K --- tools/sendN2K.py | 126 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 112 insertions(+), 14 deletions(-) diff --git a/tools/sendN2K.py b/tools/sendN2K.py index 0b84c96..2d3d7d3 100755 --- a/tools/sendN2K.py +++ b/tools/sendN2K.py @@ -4,6 +4,7 @@ import sys import os import datetime import getopt +import time ###generated with getPgnType.py from canboat pgns.json PGNM_Fast=0 @@ -422,6 +423,7 @@ class CanFrame: self.prio=prio self.sequence=None self.frame=None + self.len=8 if self.mode == PGNM_Fast and data is not None and len(self.data) >= 2: fb=int(data[0:2],16) self.frame=fb & 0x1f @@ -443,7 +445,7 @@ class CanFrame: return frames def __str__(self): - return f"{self.ts},{self.prio},{self.pgn},{self.src},{self.dst},{int(len(self.data)/2 if self.data else 0)},{dataToSep(self.data)}" + return f"{self.ts},{self.prio},{self.pgn},{self.src},{self.dst},{self.len},{dataToSep(self.data)}" @classmethod @@ -476,10 +478,14 @@ class CanFrame: class MultiFrame: def __init__(self,firstFrame: CanFrame): - self.bytes="" - self.firstFrame=firstFrame + self.data="" + self.prio=firstFrame.prio + self.pgn=firstFrame.pgn + self.src=firstFrame.src + self.dst=firstFrame.dst + self.ts=firstFrame.ts self.numFrames=firstFrame.getFPNum(bytes=False) - self.numBytes=firstFrame.getFPNum(bytes=True) + self.len=firstFrame.getFPNum(bytes=True) self.finished=False self.addFrame(firstFrame) def addFrame(self,frame:CanFrame): @@ -488,27 +494,102 @@ class MultiFrame: if frame.frame is None: return False if frame.frame == 0: - self.bytes+=frame.data[4:] + self.data+=frame.data[4:] else: - self.bytes+=frame.data[2:] + self.data+=frame.data[2:] if frame.frame >= (self.numFrames-1): self.finished=True return True def __str__(self): - return f"{self.firstFrame.ts},{self.firstFrame.prio},{self.firstFrame.pgn},{self.firstFrame.src},{self.firstFrame.dst},{self.numBytes},{dataToSep(self.bytes,self.numBytes)}" + return f"{self.ts},{self.prio},{self.pgn},{self.src},{self.dst},{self.len},{dataToSep(self.data,self.numBytes)}" def usage(): - print(f"usage: {sys.argv[0]} [-q] [-p pgn,pgn,...] file") + print(f"usage: {sys.argv[0]} [-q] [-p pgn,pgn,...] [-w waitsec] [ -f plain|actisense] file") sys.exit(1) +F_PLAIN=0 +F_ACT=1 +FORMATS={ + 'plain':F_PLAIN, + 'actisense':F_ACT +} + +MAX_ACT=400 +ACT_ESC=0x10 +ACT_START=0x2 +ACT_N2K=0x93 +ACT_END=0x3 + +class ActBuffer: + def __init__(self): + self.buf=bytearray(MAX_ACT) + self.sum=0 + self.idx=0 + self.clear() + def clear(self): + self.sum=0 + self.idx=2 + self.buf[0:2]=(ACT_ESC,ACT_START) + def add(self,val): + #TODO: len check? + val=val & 0xff + self.buf[self.idx]=val + self.sum = (self.sum + val) & 0xff + self.idx+=1 + if val == ACT_ESC: + self.buf[self.idx]=ACT_ESC + self.idx+=1 + def finalize(self): + self.sum=self.sum % 256 + self.sum = 256 - self.sum if self.sum != 0 else 0 + self.add(self.sum) + self.buf[self.idx]=ACT_ESC + self.idx+=1 + self.buf[self.idx]=ACT_END + self.idx+=1 + +actBuffer=ActBuffer() + +def send_act(frame_like,quiet): + try: + actBuffer.clear() + actBuffer.add(ACT_N2K) + actBuffer.add(frame_like.len+11) + actBuffer.add(frame_like.prio) + pgn=frame_like.pgn + actBuffer.add(pgn) + pgn = pgn >> 8 + actBuffer.add(pgn) + pgn = pgn >> 8; + actBuffer.add(pgn) + actBuffer.add(frame_like.dst) + actBuffer.add(frame_like.src) + #Time + actBuffer.add(0) + actBuffer.add(0) + actBuffer.add(0) + actBuffer.add(0) + + actBuffer.add(frame_like.len) + for i in range(0,frame_like.len*2,2): + actBuffer.add(int(frame_like.data[i:i+2],16)) + actBuffer.finalize() + sys.stdout.buffer.write(memoryview(actBuffer.buf)[0:actBuffer.idx]) + sys.stdout.buffer.flush() + except Exception as e: + if not quiet: + print(f"Error writing actisense for pgn {frame_like.pgn}, idx={actBuffer.idx}: {e}",file=sys.stderr) + if __name__ == '__main__': try: - opts,args=getopt.getopt(sys.argv[1:],"hp:q") - except getopt.GetoptError as err: - err(err) + opts,args=getopt.getopt(sys.argv[1:],"hp:qw:f:") + except getopt.GetoptError as e: + logError(e) pgnlist=[] quiet=False + delay=0.0 + format=F_PLAIN for o,a in opts: if o == '-h': usage() @@ -517,6 +598,12 @@ if __name__ == '__main__': elif o == '-p': pgns=(int(x) for x in a.split(",")) pgnlist.extend(pgns) + elif o == '-w': + delay=float(a) + elif o == '-f': + format=FORMATS.get(a) + if format is None: + logError(f"invalid format {a}, allowed {','.join(FORMATS.keys())}") if len(args) < 1: usage() hasFilter=len(pgnlist) > 0 @@ -531,7 +618,12 @@ if __name__ == '__main__': if hasFilter and not frame.pgn in pgnlist: continue if frame.sequence is None: - print(frame) + if format == F_PLAIN: + print(frame) + else: + send_act(frame,quiet) + if delay > 0: + time.sleep(delay) else: key=frame.key() mf=buffer.get(key) @@ -548,6 +640,12 @@ if __name__ == '__main__': mf.addFrame(frame) mustDelete=True if mf.finished: - print(mf) - del buffer[key] + if format == F_PLAIN: + print(mf) + else: + send_act(mf,quiet) + if mustDelete: + del buffer[key] + if delay > 0: + time.sleep(delay) \ No newline at end of file From e5968b84805e12c6dfedb8f86382baa195d94deb Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Wed, 24 Sep 2025 18:10:06 +0200 Subject: [PATCH 4/7] some error handling and stats to sendN2K --- tools/sendN2K.py | 119 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 87 insertions(+), 32 deletions(-) diff --git a/tools/sendN2K.py b/tools/sendN2K.py index 2d3d7d3..194bbd7 100755 --- a/tools/sendN2K.py +++ b/tools/sendN2K.py @@ -394,8 +394,10 @@ PGN_MODES={ -def logError(fmt,*args): - print("ERROR:" +fmt%(args)) +def logError(fmt,*args,keep=False): + print("ERROR:" +fmt%(args),file=sys.stderr) + if not keep: + sys.exit(1) def dataToSep(data,maxbytes=None): pd=None @@ -453,7 +455,7 @@ class CanFrame: '''(1658058069.867835) can0 09F80103#ACAF6C20B79AAC06''' match=cls.DUMP_PAT.search(line) if match is None: - logError("no dump pattern in line %s",line) + logError("no dump pattern in line %s",line,keep=True) return ts=match[1] dt=datetime.datetime.fromtimestamp(float(ts),tz=datetime.UTC) @@ -476,18 +478,16 @@ class CanFrame: pgn=(RDP << 16) + (PF << 8)+PS return CanFrame(tstr,pgn,src=src,dst=dst,prio=prio,data=data) -class MultiFrame: +class MultiFrame(CanFrame): def __init__(self,firstFrame: CanFrame): + super().__init__(firstFrame.ts,firstFrame.pgn, + src=firstFrame.src,dst=firstFrame.dst,prio=firstFrame.prio) self.data="" - self.prio=firstFrame.prio - self.pgn=firstFrame.pgn - self.src=firstFrame.src - self.dst=firstFrame.dst - self.ts=firstFrame.ts self.numFrames=firstFrame.getFPNum(bytes=False) self.len=firstFrame.getFPNum(bytes=True) self.finished=False self.addFrame(firstFrame) + def addFrame(self,frame:CanFrame): if self.finished: return False @@ -502,18 +502,27 @@ class MultiFrame: return True def __str__(self): - return f"{self.ts},{self.prio},{self.pgn},{self.src},{self.dst},{self.len},{dataToSep(self.data,self.numBytes)}" + return f"{self.ts},{self.prio},{self.pgn},{self.src},{self.dst},{self.len},{dataToSep(self.data,self.len)}" def usage(): print(f"usage: {sys.argv[0]} [-q] [-p pgn,pgn,...] [-w waitsec] [ -f plain|actisense] file") sys.exit(1) -F_PLAIN=0 -F_ACT=1 -FORMATS={ - 'plain':F_PLAIN, - 'actisense':F_ACT -} + +class Format: + F_PLAIN=0 + N_PLAIN='plain' + F_ACT=1 + N_ACT='actisense' + def __init__(self,name,key,merge=True): + self.key=key + self.name=name + self.merge=merge + +FORMATS=[ + Format(Format.N_PLAIN,Format.F_PLAIN), + Format(Format.N_ACT,Format.F_ACT) +] MAX_ACT=400 ACT_ESC=0x10 @@ -551,7 +560,7 @@ class ActBuffer: actBuffer=ActBuffer() -def send_act(frame_like,quiet): +def send_act(frame_like:CanFrame,quiet): try: actBuffer.clear() actBuffer.add(ACT_N2K) @@ -575,11 +584,59 @@ def send_act(frame_like,quiet): for i in range(0,frame_like.len*2,2): actBuffer.add(int(frame_like.data[i:i+2],16)) actBuffer.finalize() - sys.stdout.buffer.write(memoryview(actBuffer.buf)[0:actBuffer.idx]) + written=sys.stdout.buffer.write(memoryview(actBuffer.buf)[0:actBuffer.idx]) + if (written != actBuffer.idx): + if not quiet: + logError(f"actisense not all bytes written {written}/{actBuffer.idx} for pgn={frame_like.pgn} ts={frame_like.ts}",keep=True) sys.stdout.buffer.flush() + return True except Exception as e: if not quiet: print(f"Error writing actisense for pgn {frame_like.pgn}, idx={actBuffer.idx}: {e}",file=sys.stderr) + return False + +class Counters: + C_OK=1 + C_FAIL=2 + C_FRAME=3 + TITLES={ + C_OK:'OK', + C_FAIL:'FAIL', + C_FRAME:'FRAMES' + } + def __init__(self): + self.counters={} + for i in self.TITLES.keys(): + self.counters[i]=0 + def add(self,idx:int): + if idx not in self.TITLES.keys(): + return + self.counters[idx]+=1 + def __str__(self): + rt=None + for i in self.TITLES.keys(): + v=f"{self.TITLES[i]}:{self.counters[i]}" + if rt is None: + rt=v + else: + rt+=","+v + return rt + +def writeOut(frame:CanFrame,format:Format,quiet:bool,counters:Counters): + rt=False + if format.key == Format.F_ACT: + rt= send_act(frame,quiet) + elif format.key == Format.F_PLAIN: + print(frame) + rt=True + counters.add(Counters.C_OK if rt else Counters.C_FAIL) + return rt + +def findFormat(name:str)->Format: + for f in FORMATS: + if f.name == name: + return f + return None if __name__ == '__main__': try: @@ -589,7 +646,7 @@ if __name__ == '__main__': pgnlist=[] quiet=False delay=0.0 - format=F_PLAIN + format=findFormat(Format.N_PLAIN) for o,a in opts: if o == '-h': usage() @@ -601,14 +658,15 @@ if __name__ == '__main__': elif o == '-w': delay=float(a) elif o == '-f': - format=FORMATS.get(a) + format=findFormat(a) if format is None: - logError(f"invalid format {a}, allowed {','.join(FORMATS.keys())}") + logError(f"invalid format {a}, allowed {','.join(x.name for x in FORMATS)}") if len(args) < 1: usage() hasFilter=len(pgnlist) > 0 if not quiet and hasFilter: - print(f"PGNs: {','.join(str(x) for x in pgnlist)}") + print(f"PGNs: {','.join(str(x) for x in pgnlist)}",file=sys.stderr) + counters=Counters() with open (args[0],"r") as fh: buffer={} lnr=0 @@ -617,11 +675,9 @@ if __name__ == '__main__': frame=CanFrame.fromDump(line) if hasFilter and not frame.pgn in pgnlist: continue - if frame.sequence is None: - if format == F_PLAIN: - print(frame) - else: - send_act(frame,quiet) + counters.add(Counters.C_FRAME) + if frame.sequence is None or not format.merge: + writeOut(frame,format,quiet,counters=counters) if delay > 0: time.sleep(delay) else: @@ -640,12 +696,11 @@ if __name__ == '__main__': mf.addFrame(frame) mustDelete=True if mf.finished: - if format == F_PLAIN: - print(mf) - else: - send_act(mf,quiet) + writeOut(mf,format,quiet,counters=counters) if mustDelete: del buffer[key] if delay > 0: - time.sleep(delay) + time.sleep(delay) + if not quiet: + print(f"STATISTICS: {counters}",file=sys.stderr) \ No newline at end of file From b7cd8c6bdd971d885ea6575ed9648c57f8a2ad43 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Wed, 24 Sep 2025 18:29:22 +0200 Subject: [PATCH 5/7] add pass format to sendN2K --- tools/sendN2K.py | 53 +++++++++++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/tools/sendN2K.py b/tools/sendN2K.py index 194bbd7..4d7c11f 100755 --- a/tools/sendN2K.py +++ b/tools/sendN2K.py @@ -411,11 +411,22 @@ def dataToSep(data,maxbytes=None): else: pd+=","+data[i:i+2] return pd +class Format: + F_PLAIN=0 + N_PLAIN='plain' + F_ACT=1 + N_ACT='actisense' + F_PASS=2 + N_PASS='pass' + def __init__(self,name,key,merge=True): + self.key=key + self.name=name + self.merge=merge class CanFrame: DUMP_PAT=re.compile(r'\(([^)]*)\) *([^ ]*) *([^#]*)#(.*)') - def __init__(self,ts,pgn,src=1,dst=255,prio=1,data=None): + def __init__(self,ts,pgn,src=1,dst=255,prio=1,dev=None,hdr=None,data=None): self.pgn=pgn self.mode=PGN_MODES.get(pgn) self.ts=ts @@ -426,6 +437,8 @@ class CanFrame: self.sequence=None self.frame=None self.len=8 + self.dev=dev + self.hdr=hdr if self.mode == PGNM_Fast and data is not None and len(self.data) >= 2: fb=int(data[0:2],16) self.frame=fb & 0x1f @@ -446,9 +459,19 @@ class CanFrame: frames=int((numbytes-6-1)/7)+1+1 if numbytes > 6 else 1 return frames + def _formatTs(self): + dt=datetime.datetime.fromtimestamp(self.ts,tz=datetime.UTC) + return dt.strftime("%F-%T.")+dt.strftime("%f")[0:3] + def __str__(self): - return f"{self.ts},{self.prio},{self.pgn},{self.src},{self.dst},{self.len},{dataToSep(self.data)}" + return f"{self._formatTs()},{self.prio},{self.pgn},{self.src},{self.dst},{self.len},{dataToSep(self.data)}" + def printOut(self,format:Format): + if format.key == Format.F_PASS: + return f"({self.ts}) {self.dev} {self.hdr}#{self.data}" + else: + return str(self) + @classmethod def fromDump(cls,line): @@ -458,8 +481,6 @@ class CanFrame: logError("no dump pattern in line %s",line,keep=True) return ts=match[1] - dt=datetime.datetime.fromtimestamp(float(ts),tz=datetime.UTC) - tstr=dt.strftime("%F-%T.")+dt.strftime("%f")[0:3] data=match[4] hdr=match[3] hdrval=int(hdr,16) @@ -476,12 +497,13 @@ class CanFrame: else: dst=0xff pgn=(RDP << 16) + (PF << 8)+PS - return CanFrame(tstr,pgn,src=src,dst=dst,prio=prio,data=data) + return CanFrame(float(ts),pgn,src=src,dst=dst,prio=prio,data=data,dev=match[2],hdr=hdr) class MultiFrame(CanFrame): def __init__(self,firstFrame: CanFrame): super().__init__(firstFrame.ts,firstFrame.pgn, - src=firstFrame.src,dst=firstFrame.dst,prio=firstFrame.prio) + src=firstFrame.src,dst=firstFrame.dst,prio=firstFrame.prio, + dev=firstFrame.dev,hdr=firstFrame.hdr) self.data="" self.numFrames=firstFrame.getFPNum(bytes=False) self.len=firstFrame.getFPNum(bytes=True) @@ -502,26 +524,19 @@ class MultiFrame(CanFrame): return True def __str__(self): - return f"{self.ts},{self.prio},{self.pgn},{self.src},{self.dst},{self.len},{dataToSep(self.data,self.len)}" + return f"{self._formatTs()},{self.prio},{self.pgn},{self.src},{self.dst},{self.len},{dataToSep(self.data,self.len)}" def usage(): print(f"usage: {sys.argv[0]} [-q] [-p pgn,pgn,...] [-w waitsec] [ -f plain|actisense] file") sys.exit(1) -class Format: - F_PLAIN=0 - N_PLAIN='plain' - F_ACT=1 - N_ACT='actisense' - def __init__(self,name,key,merge=True): - self.key=key - self.name=name - self.merge=merge + FORMATS=[ Format(Format.N_PLAIN,Format.F_PLAIN), - Format(Format.N_ACT,Format.F_ACT) + Format(Format.N_ACT,Format.F_ACT), + Format(Format.N_PASS,Format.F_PASS,False) ] MAX_ACT=400 @@ -626,8 +641,8 @@ def writeOut(frame:CanFrame,format:Format,quiet:bool,counters:Counters): rt=False if format.key == Format.F_ACT: rt= send_act(frame,quiet) - elif format.key == Format.F_PLAIN: - print(frame) + else: + print(frame.printOut(format)) rt=True counters.add(Counters.C_OK if rt else Counters.C_FAIL) return rt From 78aafd308a54753ed97da0ea9a9d264487fbf462 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Wed, 24 Sep 2025 20:30:44 +0200 Subject: [PATCH 6/7] seasmart for sendN2K --- tools/sendN2K.py | 73 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 5 deletions(-) diff --git a/tools/sendN2K.py b/tools/sendN2K.py index 4d7c11f..d3dca5b 100755 --- a/tools/sendN2K.py +++ b/tools/sendN2K.py @@ -418,6 +418,8 @@ class Format: N_ACT='actisense' F_PASS=2 N_PASS='pass' + F_SEASMART=3 + N_SEASMART="seasmart" def __init__(self,name,key,merge=True): self.key=key self.name=name @@ -536,7 +538,8 @@ def usage(): FORMATS=[ Format(Format.N_PLAIN,Format.F_PLAIN), Format(Format.N_ACT,Format.F_ACT), - Format(Format.N_PASS,Format.F_PASS,False) + Format(Format.N_PASS,Format.F_PASS,False), + Format(Format.N_SEASMART,Format.F_SEASMART) ] MAX_ACT=400 @@ -575,7 +578,41 @@ class ActBuffer: actBuffer=ActBuffer() -def send_act(frame_like:CanFrame,quiet): + +LB=b'0000000000000' +B_STAR=0x2a +class SeasmartBuffer: + def __init__(self): + self.buf=bytearray(500) + self.idx=0 + self.clear() + def clear(self): + self.idx=0 + def addB(self,bv): + l=len(bv) + self.buf[self.idx:self.idx+l]=bv + self.idx+=l + def addVal(self,val,blen=2): + hs=hex(val)[2:].encode() + if len(hs) != blen: + hs=(LB+hs)[-blen:] + self.addB(hs) + + def finalize(self): + sum=0 + self.buf[self.idx]=B_STAR + self.idx+=1 + for b in memoryview(self.buf)[1:]: + if b == B_STAR: + break + sum ^= b + sum = sum & 0xff + self.addVal(sum) + self.addB(b'\x0d\x0a') + +seasmartBuffer=SeasmartBuffer() + +def send_act(frame_like:CanFrame,quiet,stream): try: actBuffer.clear() actBuffer.add(ACT_N2K) @@ -599,17 +636,41 @@ def send_act(frame_like:CanFrame,quiet): for i in range(0,frame_like.len*2,2): actBuffer.add(int(frame_like.data[i:i+2],16)) actBuffer.finalize() - written=sys.stdout.buffer.write(memoryview(actBuffer.buf)[0:actBuffer.idx]) + written=stream.write(memoryview(actBuffer.buf)[0:actBuffer.idx]) if (written != actBuffer.idx): if not quiet: logError(f"actisense not all bytes written {written}/{actBuffer.idx} for pgn={frame_like.pgn} ts={frame_like.ts}",keep=True) - sys.stdout.buffer.flush() + stream.flush() return True except Exception as e: if not quiet: print(f"Error writing actisense for pgn {frame_like.pgn}, idx={actBuffer.idx}: {e}",file=sys.stderr) return False +BK=b',' +def send_seasmart(frame_like:CanFrame,quiet,stream): + try: + seasmartBuffer.clear() + seasmartBuffer.addB(b'$PCDIN,') + seasmartBuffer.addVal(frame_like.pgn,6) + seasmartBuffer.addB(BK) + seasmartBuffer.addVal(int(time.time()),8) + seasmartBuffer.addB(BK) + seasmartBuffer.addVal(frame_like.src) + seasmartBuffer.addB(BK) + seasmartBuffer.addB(frame_like.data.encode()) + seasmartBuffer.finalize() + written=stream.write(memoryview(seasmartBuffer.buf)[0:seasmartBuffer.idx]) + if (written != seasmartBuffer.idx): + if not quiet: + raise Exception(f"seasmart not all bytes written {written}/{seasmartBuffer.idx} for pgn={frame_like.pgn} ts={frame_like.ts}") + stream.flush() + return True + except Exception as e: + if not quiet: + logError(f"writing seasmart for pgn {frame_like.pgn}, idx={seasmartBuffer.idx}: {e}",keep=True) + return False + class Counters: C_OK=1 C_FAIL=2 @@ -640,7 +701,9 @@ class Counters: def writeOut(frame:CanFrame,format:Format,quiet:bool,counters:Counters): rt=False if format.key == Format.F_ACT: - rt= send_act(frame,quiet) + rt= send_act(frame,quiet,sys.stdout.buffer) + elif format.key == Format.F_SEASMART: + rt= send_seasmart(frame,quiet,sys.stdout.buffer) else: print(frame.printOut(format)) rt=True From 448af708d4f1a58d987e286460955dd0ee8a031f Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Thu, 25 Sep 2025 17:37:59 +0200 Subject: [PATCH 7/7] fill timestamp for actisense with frame timestamp in sendN2K --- tools/sendN2K.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tools/sendN2K.py b/tools/sendN2K.py index d3dca5b..8921348 100755 --- a/tools/sendN2K.py +++ b/tools/sendN2K.py @@ -529,7 +529,7 @@ class MultiFrame(CanFrame): return f"{self._formatTs()},{self.prio},{self.pgn},{self.src},{self.dst},{self.len},{dataToSep(self.data,self.len)}" def usage(): - print(f"usage: {sys.argv[0]} [-q] [-p pgn,pgn,...] [-w waitsec] [ -f plain|actisense] file") + print(f"usage: {sys.argv[0]} [-q] [-p pgn,pgn,...] [-w waitsec] [ -f plain|actisense] file",file=sys.stderr) sys.exit(1) @@ -627,10 +627,11 @@ def send_act(frame_like:CanFrame,quiet,stream): actBuffer.add(frame_like.dst) actBuffer.add(frame_like.src) #Time - actBuffer.add(0) - actBuffer.add(0) - actBuffer.add(0) - actBuffer.add(0) + ts=int(frame_like.ts) + actBuffer.add(ts>>24) + actBuffer.add(ts>>16) + actBuffer.add(ts>>8) + actBuffer.add(ts) actBuffer.add(frame_like.len) for i in range(0,frame_like.len*2,2): @@ -654,7 +655,7 @@ def send_seasmart(frame_like:CanFrame,quiet,stream): seasmartBuffer.addB(b'$PCDIN,') seasmartBuffer.addVal(frame_like.pgn,6) seasmartBuffer.addB(BK) - seasmartBuffer.addVal(int(time.time()),8) + seasmartBuffer.addVal(int(frame_like.ts),8) seasmartBuffer.addB(BK) seasmartBuffer.addVal(frame_like.src) seasmartBuffer.addB(BK)