First commit

This commit is contained in:
Thomas Hooge 2025-03-11 13:05:11 +01:00
commit 9ce295f085
14 changed files with 2491 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*~

3
__init__.py Normal file
View File

@ -0,0 +1,3 @@
from .device import Device
from .boatdata import BoatData
from .hbuffer import History, HistoryBuffer

574
boatdata.py Normal file
View File

@ -0,0 +1,574 @@
'''
!!! Dies ist noch im Ideen-Stadium
WIP TBD
Die Werte der Daten werden wie in NMEA2000 gespeichert:
Längen: m
Geschwindigkeiten: m/s
Winkel, Kurse: radiant (Bogenmaß)
Temperaturen: K
Druck: Pa
Geokoordinaten sind eine vorzeichenbehaftete Fileßkommazahl
Liste der Daten mit Langbezeichnung siehe: valdesc.py
Die format()-Funktion liefert immer einen String zurück.
Unterscheidung zwischen "kein Wert" und "ungültiger Wert"?
Normale Daten
-------------
ALT - Altitude, Höhe über Grund
AWA - Apparant Wind Angle, scheinbare Windrichtung
AWS - Apparant Wind Speed, scheinbare Windgeschwindigkeit
BTW - Bearing To Waipoynt, Winkel zum aktuellen Wegpunkt
COG - Course over Ground, Kurs über Grund
DBS - Depth Below Surface, Tiefe unter Wasseroberfläche
DBT - Depth Below Transducer, Tiefe unter Sensor
DEV - Deviation, Kursabweichung
DTW - Distance To Waypoint, Entfernung zum aktuellen Wegpunkt
GPSD - GPS Date, GPS-Datum
GPDT - GPS Time, GPS-Zeit als UTC (Weltzeit)
HDM - Magnetic Heading, magnetischer rechtweisender Kurs
HDT - Heading, wahrer rechtweisender Kurs
HDOP - GPS-Genauigkeit in der Horizontalen
LAT - Latitude, geografische Breite
LON - Longitude, geografische Höhe
Log - Log, Entfernung
MaxAws - Maximum Apperant Wind Speed, Maximum der relativen Windgeschwindigkeit seit Gerätestart
MaxTws - Maximum True Wind Speed, Maximum der wahren Windgeschwindigkeit seit Gerätestart
PDOP - GPS-Genauigkeit über alle 3 Raumachsen
PRPOS - Auslenkung Sekundärruder
ROT - Rotation, Drehrate
RPOS - Rudder Position, Auslenkung Hauptruder
SOG - Speed Over Ground, Geschwindigkeit über Grund
STW - Speed Through Water, Geschwindigkeit durch das Wasser
SatInfo - Satellit Info, Anzahl der sichtbaren Satelliten
TWD - True Wind Direction, wahre Windrichtung
TWS - True Wind Speed, wahre Windgeschwindigkeit
TZ - Time Zone, Zeitzone
TripLog - Trip Log, Tages-Entfernungszähler
VAR - Variation, Abweichung vom Sollkurs
VDOP - GPS-Genauigkeit in der Vertikalen
WPLat - Waypoint Latitude, geogr. Breite des Wegpunktes
WPLon - Waypoint Longitude, geogr. Länge des Wegpunktes
WTemp - Water Temperature, Wassertemperatur
XTE - Cross Track Error, Kursfehler
Normale Daten erweitert
-----------------------
ROLL - Roll - Krängen / Rotation in Querrichtung
PTCH - Pitch - Rollen / Rotation in Längsrichtung
YAW - Yaw - Gieren / Rotation um die Senkrechte Achse
XDR-Daten
---------
xdrVBat - Bordspannung
xdrHum - Luftfeuchte
xdrPress - Luftdruck
xdrTemp - Temperatur
xdrRotK - Kielrotation
xdrRoll
xdrPitch
xdrYaw
'''
import datetime
import time
import math
import random
#from .lookup import fluidtype
from . import lookup
class BoatValue():
"""
Wert mit Datentyp, Einheit, Validekennzeichen und Skalierungsfaktor
"""
placeholder = '---'
def __init__(self, shortname, unit=''):
self.valname = shortname
self.unit = unit
self.value = None
self.valid = False
self.resolution = 1
self.decpl = None # decimal places for format
self.desc = "" # long description
self.timestamp = time.time()
self.simulated = False
self.history = False
self.hbuf = None
def getValue(self):
# Wert unter Beachtung der Unit zurückgeben
if self.value and valid:
return self.value
else:
return self.placeholder
def getValueRaw(self, ignorevalid=False):
if ignorevalid or self.valid:
return self.value
return None
def setValue(self, newvalue):
self.value = newvalue
self.timestamp = time.time()
self.valid = True
# if self.history:
# TODO es kann mehrere verschiedene Zeitreihen geben!
# Implementierung nich unklar.
def enableHistory(self, size, delta_t):
if self.history:
# Wiederholter Aufruf löscht die bisherige Historie
self.hbuf.clear()
return
self.history = True
self.hbuf = HistoryBuffer(size, delta_t)
def format(self):
if self.simulated:
if self.valname == "xdrVBat":
return "{:.1f}".format(random.uniform(11.8, 14.2))
else:
return "{:.0f}".format(random.uniform(95, 130))
if not self.value or not self.valid:
return self.placeholder
if not self.decpl:
if self.value < 10:
self.decpl = 1
else:
self.decpl = 0
return "{0:3.{1}f}".format(self.value, self.decpl)
def __str__(self):
out = self.valname
#out += self.format()
if not self.valid:
out += (" (invalid)")
return out
class BoatValueGeo(BoatValue):
# geofmt = lat | lon
# unit = rad | deg
def __init__(self, shortname, geofmt, unit='deg'):
super().__init__(shortname, unit)
self.geofmt = geofmt
self.decpl = 3
#print(self.valname)
def format(self):
# latitude, longitude
if not self.value or not self.valid:
return self.placeholder
degrees = int(self.value)
minutes = (self.value - degrees) * 60
if self.geofmt == 'lat':
direction = ('E' if self.value > 0 else 'W')
formatted = "{0}° {1:.{3}f}' {2}".format(degrees, minutes, direction, decpl)
elif self.geofmt == 'lon':
direction = 'N' if self.value > 0 else 'S'
formatted = "{0}° {1:.{3}f}' {2}".format(degrees, minutes, direction, decpl)
else:
formatted = str(self.placeholder)
return formatted
class BoatValueDate(BoatValue):
# datefmt = GB | US | DE | ISO
def __init__(self, shortname, datefmt='ISO'):
super().__init__(shortname)
self.datefmt = datefmt
def format(self):
if self.datefmt == 'DE':
formatted = self.value.strftime("%d.%m.%Y")
elif self.datefmt == 'GB':
formatted = self.value.strftime("%d/%m/%Y")
elif self.datefmt == 'US':
formatted = self.value.strftime("%m/%d/%Y")
elif self.datefmt == 'ISO':
formatted = self.value.strftime("%Y-%m-%d")
return formatted
class BoatValueTime(BoatValue):
def __init__(self, shortname, timezone='UTC'):
super().__init__(shortname)
self.tz = timezone
self.timefmt = 'hh:mm:' # TODO hh:mm:ss | ...?
def format(self):
formatted = self.value
return formatted
class BoatValueSpeed(BoatValue):
# unsigned? Was ist mit Rückwärts?
def format(self):
if self.simulated:
return "5.3"
if not self.value or not self.valid:
return self.placeholder
if self.value < 20:
formatted = f"{self.value:3.1f}"
else:
formatted = f"{self.value:3.0f}"
return formatted
class BoatValueAngle(BoatValue):
# course, wind, heading, bearing
# roll, pitch, yaw, rudder, keel
def format(self):
if self.simulated:
if self.valname == "BTW":
return "253"
else:
return "120"
if self.value:
return f"{self.value:03.0f}"
else:
return self.placeholder
class BoatValueRotation(BoatValue):
# signed
def format(self):
if self.value < 10 and self.value > -10:
formatted = f"{self.value:.1f}"
else:
formatted = f"{self.value:.1f}"
return formatted
class BoatValueDepth(BoatValue):
# unsigned
def format(self):
if self.simulated:
if self.valname == "DBT":
return "6.2"
else:
return "6.5"
if not self.value or not self.valid:
return self.placeholder
if self.value < 100:
formatted = f"{self.value:3.1f}"
else:
formatted = f"{self.value:3.0f}"
return formatted
class BoatValueDistance(BoatValue):
# unsigned integer?
def format(self, signed=False):
if self.value:
return f"{self.value:d}"
else:
return self.placeholder
class BoatValueTemperature(BoatValue):
# signed
def __init__(self, shortname, unit='K'):
super().__init__(shortname, unit)
self.instance = None
self.sensortype = None
self.decpl = 0
def format(self):
if self.value < 100:
formatted = f"{self.value:3.1f}"
else:
formatted = f"{self.value:3.0f}"
return formatted
class BoatValueHumidity(BoatValue):
# unsigned integer
# range 0 .. 100
def __init__(self, shortname, unit='%'):
super().__init__(shortname, unit)
self.instance = None
self.sensortype = None
self.decpl = 0
def format(self):
return f"{self.value:d}"
class BoatValuePressure(BoatValue):
# unsigned integer
# range ca. 800 .. 1100 for athmospheric pressure
def __init__(self, shortname, unit='Pa'):
super().__init__(shortname, unit)
self.instance = None
self.sensortype = None
self.decpl = 1
def format(self):
if self.value and self.valid:
return f"{self.value:4.{self.decpl}f}"
else:
return self.placeholder
class Tank():
"""
Die Instanz beziegt sich auf den Typ. So kann die Instanz 0
für einen Wassertank und einen Dieseltank existieren
"""
def __init__(self, instance=0):
self.fluidtype = 1 # water -> lookup
self.instance = instance
self.level = None # percent
self.capacity = None # liter
self.desc = "" # long description
def __str__(self):
typedesc = lookup.fluidtype[self.fluidtype]
out = f" Tank / {typedesc}: #{self.instance}\n"
out += f" Capacity: {self.capacity} l\n"
out += f" Fluid level: {self.level} %\n"
return out
class Engine():
def __init__(self, instance=0):
self.instance = instance
self.speed_rpm = None
self.exhaust_temp = None
self.desc = "" # long description
def __str__(self):
out = f" Engine #{self.instance}\n"
if self.exhaust_temp:
out += f" Exhaust temp: {self.exhaust_temp:.1f} °C\n"
else:
out += " Exhaust temp: no data\n"
return out
class Satellite():
def __init__(self, prn_num):
self.prn_num = prn_num
self.elevation = None
self.azimuth = None
self.snr = None # signal noise ratio
self.rres = None # range residuals
self.status = None # lookup -> prnusage
self.lastseen = None
def __str__(self):
out = f"SAT {self.prn_num:02d}: "
if self.snr:
out += f"snr={self.snr}dB elevation={self.elevation:.4f} azimuth={self.azimuth:.4f} status={self.status}\n"
else:
out += "no signal\n"
return out
class SatelliteList():
def __init__(self):
sat = {}
rrmode = None
maxage = 300 # sec
def getCount(self):
return len(sat)
def addSat(self, pnr_num):
pass
def delSat(self, pnr_num):
pass
class BoatData():
def __init__(self):
self.simulation = False
# nach Überschreiten dieser Schwelle in Sekunden wird
# ein Meßwert als ungültig angesehen
self.maxage = 5
# Systemspannung; temporär. später auf bessere Weise speichern
self.voltage = BoatValue("xdrVBat", "V")
# Navigationsdaten
self.awa = BoatValueAngle("AWA", "kn")
self.aws = BoatValueSpeed("AWS", "kn")
self.twd = BoatValueAngle("TWD", "kn")
self.tws = BoatValueSpeed("TWS", "kn")
self.lat = BoatValueGeo("LAT", "lat", "deg")
self.lon = BoatValueGeo("LON", "lon", "deg")
self.gpsd = BoatValueDate("GPSD", "ISO")
self.gpst = BoatValueTime("GPST")
self.sog = BoatValueSpeed("SOG", "kn")
self.cog = BoatValueAngle("COG", "deg")
self.xte = BoatValueDistance("XTE", "m")
self.stw = BoatValueSpeed("STW", "kn")
self.dbt = BoatValueDepth("DBT", "m")
self.roll = BoatValueAngle("ROLL", "deg")
self.pitch = BoatValueAngle("PTCH", "deg")
self.yaw = BoatValueAngle("YAW", "deg")
self.rpos = BoatValueAngle("RPOS", "deg")
self.prpos = BoatValueAngle("PRPOS", "deg")
# Nächster Wegepunkt
self.wpno = BoatValue("WP")
self.wpname = BoatValue("WPname")
self.wplat = BoatValueGeo("WPLat", "lat", "deg")
self.wplon = BoatValueGeo("WPLon", "lon", "deg")
self.wpdist = BoatValueDistance("DTW", "m")
self.bearing = BoatValueAngle("BTW", "kn")
# Umgebung
self.temp_water = BoatValueTemperature("WTemp", "°C")
self.temp_air = BoatValueTemperature("xdrTemp", "°C")
self.pressure = BoatValuePressure("xdrPress", "hPa")
self.humidity = BoatValueHumidity("xdrHum", "%")
self.temp = {} # Erweiterte Temperaturdaten
self.press = {} # Erweiterte Druckdaten
# Sonderdaten
self.rotk = BoatValueAngle("xdrRotK", "deg") # Kielrotation
# Maschinen
self.engine = {}
# Tanks
self.tank = {}
# Mehrere getrennte Batteriekreise
# - Starter
# - Verbrauchen
# - Ankerwinsch / Bugstrahlruder
# Stromerzeugung
# Solarleistung
# Generatorleistung
# Benzingenerator
# Windgenerator
# Wasser-/Schleppgenerator
# Maschine Rekuperation
# Satelliten
self.sat = {}
# Zeitreihen für diverse Daten
self.history = {}
self.valref = {
'AWA': self.awa,
'AWS': self.aws,
'BTW': self.bearing,
'COG': self.cog,
'DBT': self.dbt,
'DTW': self.wpdist,
'GPSD': self.gpsd,
'GPST': self.gpst,
'LAT': self.lat,
'LON': self.lon,
'PRPOS': self.prpos,
'PTCH': self.pitch,
'RPOS': self.rpos,
'ROLL': self.roll,
'SOG': self.sog,
'STW': self.stw,
'TWD': self.twd,
'TWS': self.tws,
'WTemp': self.temp_water,
'WPLat': self.wplat,
'WPLon': self.wplon,
'XTE': self.xte,
'xdrRotK': self.rotk,
'xdrVBat': self.voltage,
'xdrTemp': self.temp_air,
'xdrPress': self.pressure,
'xdrHum': self.humidity,
'YAW': self.yaw
}
def addTank(self, instance):
self.tank[instance] = Tank(instance)
def addEngine(self, instance):
self.engine[instance] = Engine(instance)
def addSensor(self, sensortype, instance):
if sensortype == 'temp':
if not instance in self.temp:
self.temp[instance] = BoatValueTemperature()
else:
raise ValueError(f"duplicate key '{instance}'")
elif sensortype == 'press':
if not instance in self.press:
self.press[instance] = BoatValuePressure()
else:
raise ValueError(f"duplicate key '{instance}'")
def updateSatellite(self, prn_num, elevation, azimuth, snr, rres, status):
if not prn_num in self.sat:
self.sat[prn_num] = Satellite(prn_num)
self.sat[prn_num].elevation = elevation
self.sat[prn_num].azimuth = azimuth
self.sat[prn_num].snr = snr
self.sat[prn_num].rres = rres
self.sat[prn_num].status = status
self.sat[prn_num].lastseen = time.time()
def __str__(self):
out = "Boat Data\n"
out += f" Voltage: {self.voltage}\n"
out += f" Latitude: {self.lat.value}\n"
out += f" Longitude: {self.lon.value}\n"
out += f" SOG: {self.sog}\n"
for e in self.engine.values():
out += str(e)
for t in self.tank.values():
out += str(t)
out += " Satellite info\n"
for s in self.sat.values():
out += str(s)
return out
def updateValid(self, age=None):
# age: Alter eines Meßwerts in Sekunden
if not age:
age = self.maxage
t = time.time()
for v in vars(self).values():
if isinstance(v,BoatValue):
if t - v.timestamp > age:
v.valid = False
def getRef(self, shortname):
'''
Referenz auf ein BoatValue-Objekt
'''
try:
bv = self.valref[shortname]
except KeyError:
bv = None
return bv
def getValue(self, shortname):
'''
Wert aufgrund textuellem Kurznamen zurückliefern
'''
try:
value = self.valref[shortname].value
except KeyError:
value = None
return value
def setValue(self, shortname, newvalue):
'''
Rückgabewert True bei erfolgreichem Speichern des Werts
'''
if not shortname in self.valref:
return False
field = self.valref[shortname]
field.value = newvalue
field.timestamp = time.time()
field.valid = True
return True
def enableSimulation(self):
self.simulation = True
for v in self.valref.values():
v.simulated = True
def addHistory(self, history, htype):
"""
htype: press, temp, hum
"""
self.history[htype] = history

121
device.py Normal file
View File

@ -0,0 +1,121 @@
"""
NMEA2000-Gerät
- auf dem Bus erkannte Geräte
- für das eigene Gerät steht initUid() zur Verfügung
"""
import time
import struct
from . import lookup
class Device():
def __init__(self, address):
# WIP: Felder können sich noch ändern!
self.address = address # Kann sich zur Laufzeit ändern
self.lastseen = time.time()
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
# Device info
self.NAME = 0 # Wird über getNAME (address claim) gefüllt, 64bit Integer
self.uniqueid = None # Z.B. aus der Geräteseriennummer abgeleitet
self.manufacturercode = 2046 # Open Boat Projects
self.instance = 0 # default 0
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.modelvers = None # Hardware Version
self.softvers = None # Current Software Version
self.n2kvers = None # NMEA2000 Network Message Database Version
self.certlevel = None # NMEA2000 Certification Level
self.loadequiv = None # NMEA2000 LEN
# Configuration info
self.instdesc1 = None
self.instdesc2 = None
self.manufinfo = None
# Additional data
self.customname = None # User defined device name
def _ISOtime(self, epoch):
if not epoch:
return "n/a"
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(epoch))
def initUid(self):
# Initialize unique id from raspi cpu id and return 21bit value
with open("/sys/firmware/devicetree/base/serial-number", "r") as f:
hexid = f.read().rstrip('\x00')
return int(hexid, 16) & 0x001FFFFF # 21bit mask
def getNAME(self):
"""
NAME is unique on bus
Returns bytearray and sets integer NAME for later easy and fast access
"""
data = bytearray()
data.extend(struct.pack('<I', (self.uniqueid & 0x001fffff) | (self.manufacturercode << 21)))
data.append((self.instlower & 0x07) | ((self.instupper & 0x1f) << 3))
data.append(self.devicefunction)
data.append((self.deviceclass & 0x7f) << 1)
data.append(0x80 | ((self.industrygroup & 0x07) << 4) | (self.sysinstance & 0x0f))
self.NAME = struct.unpack_from('<Q', data, 0)[0]
return data
def __str__(self):
out = f"Device: {self.address} : '{self.product}'\n"
out += " NAME: {} ({})\n".format(self.getNAME(), self.NAME)
out += " last seen: {}\n".format(self._ISOtime(self.lastseen))
out += " Device info\n"
out += f" Unique ID: {self.uniqueid}\n"
out += f" Instance: {self.instance} ({self.instupper}/{self.instlower})\n"
out += f" System instance: {self.sysinstance}\n"
try:
devfnname = lookup.devicefunction[self.deviceclass][self.devicefunction]
except KeyError:
devfnname = "*key error*"
out += f" Device function: {devfnname} ({self.devicefunction})\n"
try:
devclassname = lookup.deviceclass[self.deviceclass]
except KeyError:
devclassname = "*key error*"
out += f" Device class: {devclassname} ({self.deviceclass})\n"
try:
igrpname = lookup.industrygroup[self.industrygroup]
except KeyError:
igrpname = "*key error*"
out += f" Industry group: {igrpname} ({self.industrygroup})\n"
try:
manufname = lookup.manufacturer[self.manufacturercode]
except KeyError:
manufname = "*key error*"
out += f" Manufacturer code: {manufname} ({self.manufacturercode})\n"
out += " Product info at {}\n".format(self._ISOtime(self.lastpinfo))
out += f" Product Code: {self.productcode}\n"
out += f" Product: {self.product}\n"
out += f" Serial: {self.serial}\n"
out += f" Model Version: {self.modelvers}\n"
out += f" Software Version: {self.softvers}\n"
out += f" NMEA2000 Version: {self.n2kvers}\n"
out += f" Cert-Level: {lookup.certlevel[self.certlevel]} ({self.certlevel})\n"
out += f" LEN: {self.loadequiv}\n"
out += " Configuration info at {}\n".format(self._ISOtime(self.lastcinfo))
if self.has_cinfo:
out += f" Installation description 1: {self.instdesc1}\n"
out += f" Installation description 2: {self.instdesc2}\n"
out += f" Manufacturer info: {self.manufinfo}\n"
else:
out += " not available\n"
return out

17
devicelist.py Normal file
View File

@ -0,0 +1,17 @@
'''
Platzhalter WIP
- ausprogrammieren nach Bedarf
Geräteliste
- wird regelmäßig aktualisiert
'''
class DeviceList():
def __init__(self):
self.devices = list()
def print(self):
for d in self.devicelist:
print(d)

300
hbuffer.py Normal file
View File

@ -0,0 +1,300 @@
"""
History Buffer
Permanent storage backed buffer for sensordata
Only supported at the moment: file system storage
Values can be 1 to 4 bytes in length
Header: 32 bytes of size
0 0x00 HB00 4 magic number
4 0x04 xxxxxxxxxxxxxxxx 16 name, space padded
20 0x14 n 1 byte size of values in buffer
21 0x15 mm 2 buffer size in count of values
23 0x17 dd 2 time step in seconds between values
25 0x19 tttt 4 unix timestamp of head
29 0x1d hh 2 head pointer
31 0x1f 0xff 1 header end sign
32 0x20 ... start of buffer data
Usage example: 7 hours of data collected every 75 seconds
def g_tick(n=1):
t = time.time()
count = 0
while True:
count += n
yield max(t + count - time.time(), 0)
hb = HistoryBuffer("test", 336, 75)
g = g_tick(hb.dt)
hb.filename = "/tmp/test.dat"
hb.begin()
while True:
time.sleep(next(g))
hb.add(measured_new_value)
hb.finish()
TODO
- Logging
- Additional backend: I2C FRAM module
- Sync to next tick after loading from storage
"""
import os
import time
import struct
class HistoryBuffer():
def __init__(self, name, size, delta_t):
"""
Buffer can have an optional name of max. 16 characters
"""
self.magic = b'HB00'
self.name = name[:16] or ''
self.bytesize = 2
self.size = size
self.dt = delta_t
self.headdate = int(time.time())
self.head = 0
self.buf = [0 for _ in range(size)]
self.filename = f"/tmp/hb{name}_{size}-{delta_t}.dat"
self.fp = None
def begin(self):
# Check if data exists and format is correct
if not os.path.exists(self.filename):
self.createfile()
else:
if not self.checkfile():
print(f"Incompatible data file: {self.filename}")
return False
# Read old data to continue processing
self.fp = open(self.filename, 'r+b')
self.headdate = int(time.time())
self.fp.seek(25)
timestamp = struct.unpack('I', self.fp.read(4))[0]
self.head = struct.unpack('H', self.fp.read(2))[0]
self.fp.seek(32)
data = self.fp.read(self.bytesize * self.size)
# Fix difference between current time and data time
missing = (self.headdate - timestamp) // self.dt
if missing > self.size:
# too old start new
self.clearfile
self.head = 0
else:
# usable data found, fix missing
self.fp.seek(32)
data = self.fp.read(self.bytesize * self.size)
i = 0
for d in range(0, self.size, self.bytesize):
if self.bytesize == 1:
self.buf[i] = data[d]
elif self.bytesize == 2:
self.buf[i] = data[d] + data[d+1] * 256
elif self.bytesize == 3:
self.buf[i] = data[d] + (data[d+1] << 8) + (data[d+2] << 16)
elif self.bytesize == 4:
self.buf[i] = data[d] + (data[d+1] << 8) + (data[d+2] << 16) + (data[d+3] << 24)
i += 1
# add empty data for missing steps
for s in range(missing):
self.add(0)
return True
def finish(self):
if not self.fp.closed:
self.fp.close()
def add(self, value):
# check if add request perhaps too early
timestamp = int(time.time())
if timestamp - self.headdate < self.dt * 0.98: # a little bit of tolerance
print("add new value too early, ignored")
return False
self.headdate = timestamp
self.buf[self.head] = value
self.updatefile(value)
self.head += 1
if self.head == self.size:
self.head = 0
return True
def get(self):
"""
Return buffer in linear sequence, newest values first
"""
for i in range(self.head -1, -1, -1):
yield self.buf[i]
for i in range(self.size - 1, self.head -1, -1):
yield self.buf[i]
def getvalue(self, delta):
"""
Return a single value dt seconds ago
delta has to be smaller than self.dt * self.size
TODO check if value is missing, perhaps allow tolerance (+/- <n>)
"""
index = self.head - abs(delta) // self.dt
if index < 0:
index = self.size - index - 1
return self.buf[index]
def getvalue3(self, delta):
"""
same as getvalue but calculate mean with two neighbor values
TODO check for missing values (=0)
"""
index = self.head - abs(delta) // self.dt
if index < 0:
index = self.size - index - 1
ixprev = index - 1
if ixprev < 0:
ixprev = self.size - 1
ixnext = index + 1
if ixnext > self.size - 1:
ixnext = 0
return round((self.buf[index] + self.buf[ixprev] + self.buf[ixnext]) / 3)
def setname(self, newname):
"""
set new name in buffer and storage backend
"""
self.name = newname[:16] or ''
self.fp.seek(4)
fp.write(self.name.ljust(16, ' ').encode())
def createfile(self):
""""
Creates new file from current buffer
"""
with open(self.filename, 'wb') as fp:
fp.write(self.magic)
fp.write(self.name.ljust(16, ' ').encode())
fp.write(struct.pack('B', self.bytesize))
fp.write(struct.pack('H', self.size))
fp.write(struct.pack('H', self.dt))
fp.write(struct.pack('I', self.headdate))
fp.write(struct.pack('H', self.head))
fp.write(b"\xff") # header end
if self.bytesize == 1:
fp.write(bytes(self.buf))
elif self.bytesize == 2:
for val in self.buf:
fp.write(struct.pack('H', val))
elif self.bytesize == 3:
for val in self.buf:
fp.write((val >> 16) & 0xff)
fp.write((val >> 8) & 0xff)
fp.write(val & 0xff)
elif self.bytesize == 4:
for val in self.buf:
fp.write(struct.pack('I', val))
return True
def checkfile(self):
"""
Check if file header matches buffer metadata
Name is not taken into account because it is optional
"""
with open(self.filename, 'rb') as fp:
header = fp.read(32)
magic = header[:4]
if not (header[:4] == self.magic):
print(f"Invalid magic: {magic}")
return False
bs = header[20]
if not (bs == self.bytesize):
print(f"Invalid bytesize: {bs}")
return False
vc = struct.unpack('H', header[21:23])[0]
if not (vc == self.size):
eprint(f"Invalid value count: {vc}")
return False
ts = struct.unpack('H', header[23:25])[0]
if not (ts == self.dt):
eprint(f"Invalid time step: {ts}")
return False
return True
def updatefile(self, value):
"""
Write value to file and update header accordingly
"""
pos = 32 + self.head * self.bytesize
self.fp.seek(25)
self.fp.write(struct.pack('IH', self.headdate, self.head + 1))
self.fp.seek(pos)
if self.bytesize == 1:
self.fp.write(struct.pack('B', value))
elif self.bytesize == 2:
self.fp.write(struct.pack('H', value))
elif self.bytesize == 3:
self.fp.write((value >> 16) & 0xff)
self.fp.write((value >> 8) & 0xff)
self.fp.write(value & 0xff)
elif self.bytesize == 4:
self.fp.write(struct.pack('I', value))
def clearfile(self):
"""
Clear data part of history file
"""
self.fp.seek(25)
self.fp.write(struct.pack('IH', int(time.time()), 0))
fp.seek(32)
for p in range(32, self.size * self.bytesize):
fp.write(0)
class History():
"""
A history can consist of different time series with different
temporal resolutions
TODO implement type (e.g. pressure humidity temp etc.) to identify data
"""
def __init__(self, basename, delta_min):
self.delta_t = delta_min # smallest unit of time in the series
self.series = dict()
self.basepath = "/tmp"
self.basename = basename
def __str__(self):
out = f"History {self.basename} (min. {self.delta_t}s) in {self.basepath}\n"
n = 0
for ser in self.series.values():
out += f" Series: {ser.name} {ser.dt}s {ser.filename}\n"
n += 1
if n == 0:
out += " No series found\n"
return out
def addSeries(self, name, size, delta_t):
"""
Check whether a series already exists and throw an error if so.
The new delta t must also be divisible by delta_min
"""
if delta_t in self.series:
raise KeyError(f"Error: delta t {delta_t} already exists")
if delta_t < self.delta_t:
raise ValueError(f"Error: delta t {delta_t} too small, minimum is {self.delta_t}")
if delta_t % self.delta_t != 0:
raise ValueError(f"Error: delta t have to be a multiple of {self.delta_t}")
hb = HistoryBuffer(name, size, delta_t)
histfilename = f"hb{self.basename}_{size}-{delta_t}.dat"
hb.filename = os.path.join(self.basepath, histfilename)
self.series[delta_t] = hb
return hb
def clear(self):
# Clear all time series buffer
self.series.clear()

572
lookup.py Normal file
View File

@ -0,0 +1,572 @@
# Lookup tables
accesslevel = {
0: "Locked",
1: "unlocked level 1",
2: "unlocked level 2"
}
alarmgroup = {
0: "Instrument",
1: "Autopilot",
2: "Radar",
3: "Chart Plotter",
4: "AIS"
}
alarmid = {
0: "No Alarm",
1: "Shallow Depth",
2: "Deep Depth",
3: "Shallow Anchor",
4: "Deep Anchor",
5: "Off Course",
6: "AWA High",
7: "AWA Low",
8: "AWS High",
9: "AWS Low",
10: "TWA High",
11: "TWA Low",
12: "TWS High",
13: "TWS Low",
14: "WP Arrival",
15: "Boat Speed High",
16: "Boat Speed Low",
17: "Sea Temperature High",
18: "Sea Temperature Low",
19: "Pilot Watch",
20: "Pilot Off Course",
21: "Pilot Wind Shift",
22: "Pilot Low Battery",
23: "Pilot Last Minute Of Watch",
24: "Pilot No NMEA Data",
25: "Pilot Large XTE",
26: "Pilot NMEA DataError",
27: "Pilot CU Disconnected",
28: "Pilot Auto Release",
29: "Pilot Way Point Advance",
30: "Pilot Drive Stopped",
31: "Pilot Type Unspecified",
32: "Pilot Calibration Required",
33: "Pilot Last Heading",
34: "Pilot No Pilot",
35: "Pilot Route Complete",
36: "Pilot Variable Text",
37: "GPS Failure",
38: "MOB",
39: "Seatalk1 Anchor",
40: "Pilot Swapped Motor Power",
41: "Pilot Standby Too Fast To Fish",
42: "Pilot No GPS Fix",
43: "Pilot No GPS COG",
44: "Pilot Start Up",
45: "Pilot Too Slow",
46: "Pilot No Compass",
47: "Pilot Rate Gyro Fault",
48: "Pilot Current Limit",
49: "Pilot Way Point Advance Port",
50: "Pilot Way Point Advance Stbd",
51: "Pilot No Wind Data",
52: "Pilot No Speed Data",
53: "Pilot Seatalk Fail1",
54: "Pilot Seatalk Fail2",
55: "Pilot Warning Too Fast To Fish",
56: "Pilot Auto Dockside Fail",
57: "Pilot Turn Too Fast",
58: "Pilot No Nav Data",
59: "Pilot Lost Waypoint Data",
60: "Pilot EEPROM Corrupt",
61: "Pilot Rudder Feedback Fail",
62: "Pilot Autolearn Fail1",
63: "Pilot Autolearn Fail2",
64: "Pilot Autolearn Fail3",
65: "Pilot Autolearn Fail4",
66: "Pilot Autolearn Fail5",
67: "Pilot Autolearn Fail6",
68: "Pilot Warning Cal Required",
69: "Pilot Warning OffCourse",
70: "Pilot Warning XTE",
71: "Pilot Warning Wind Shift",
72: "Pilot Warning Drive Short",
73: "Pilot Warning Clutch Short",
74: "Pilot Warning Solenoid Short",
75: "Pilot Joystick Fault",
76: "Pilot No Joystick Data",
80: "Pilot Invalid Command",
81: "AIS TX Malfunction",
82: "AIS Antenna VSWR fault",
83: "AIS Rx channel 1 malfunction",
84: "AIS Rx channel 2 malfunction",
85: "AIS No sensor position in use",
86: "AIS No valid SOG information",
87: "AIS No valid COG information",
88: "AIS 12V alarm",
89: "AIS 6V alarm",
90: "AIS Noise threshold exceeded channel A",
91: "AIS Noise threshold exceeded channel B",
92: "AIS Transmitter PA fault",
93: "AIS 3V3 alarm",
94: "AIS Rx channel 70 malfunction",
95: "AIS Heading lost/invalid",
96: "AIS internal GPS lost",
97: "AIS No sensor position",
98: "AIS Lock failure",
99: "AIS Internal GGA timeout",
100: "AIS Protocol stack restart",
101: "Pilot No IPS communications",
102: "Pilot Power-On or Sleep-Switch Reset While Engaged",
103: "Pilot Unexpected Reset While Engaged",
104: "AIS Dangerous Target",
105: "AIS Lost Target",
106: "AIS Safety Related Message (used to silence)",
107: "AIS Connection Lost",
108: "No Fix"
}
alarmstatus = {
0: "Alarm condition not met",
1: "Alarm condition met and not silenced",
2: "Alarm condition met and silenced"
}
# Class 1, 2, Level A, B?
certlevel = { # Not yet verified!
0: "None",
1: "Certified",
2: "Not applicable"
}
control = {
0: "ACK",
1: "NAK",
2: "Access Denied",
3: "Address Busy"
}
deviceclass = {
0: "Reserved for 2000 Use",
10: "System tools",
11: "WEMA Custom?",
20: "Safety systems",
25: "Internetwork device",
30: "Electrical Distribution",
35: "Electrical Generation",
40: "Steering and Control surfaces",
50: "Propulsion",
60: "Navigation",
70: "Communication",
75: "Sensor Communication Interface",
80: "Instrumentation/general systems", # deprecated
85: "External Environment",
90: "Internal Environment",
100: "Deck + cargo + fishing equipment systems",
110: "Human Interface",
120: "Display",
125: "Entertainment"
}
devicefunction = { # dependent of deviceclass above
0: {},
10: {130: "Diagnostic",
140: "Bus Traffic Logger"
},
11: {150: "WEMA Fluid level" # Custom?
},
20: {110: "Alarm Enunciator",
130: "Emergency Positon Indicating Radia Beacon (EPIRB)",
135: "Man Overboard",
140: "Voyage Date Recorder",
150: "Camera"
},
25: {130: "PC Gateway",
131: "NMEA 2000 to Analog Gateway",
132: "Analog to NMEA 2000 Gateway",
133: "NMEA 2000 to Serial Gateway",
135: "NMEA 0183 Gateway",
136: "NMEA Network Gateway",
137: "NMEA 2000 Wireless Gateway",
140: "Router",
150: "Bridge",
160: "Repeater"
},
30: {130: "Binary Event Monitor",
140: "Load Controller",
141: "AC/DC Input",
150: "Function Controller"
},
35: {140: "Engine",
141: "DC Generator/Alternator",
142: "Solar Panel (Solar Array)",
143: "Wind Generator (DC)",
144: "Fuel Cell",
145: "Network Power Supply",
151: "AC Generator",
152: "AC Bus",
153: "AC Mains (Utility/Shore)",
154: "AC Output",
160: "Power Converter - Battery Charger",
161: "Power Converter - Battery Charger+Inverter",
162: "Power Converter - Inverter",
163: "Power Converter DC",
170: "Battery",
180: "Engine Gateway"
},
40: {130: "Follow-up Controller",
140: "Mode Controller",
150: "Autopilot",
155: "Rudder",
160: "Heading Sensors", # deprecated
170: "Trim (Tabs)/Interceptors",
180: "Attitude (Pitch, Roll, Yaw) Control"
},
50: {130: "Engineroom Monitoring", # deprecated
140: "Engine",
141: "DC Generator/Alternator",
150: "Engine Controller", # deprecated
151: "AC Generator",
155: "Motor",
160: "Engine Gateway",
165: "Transmission",
170: "Throttle/Shift Control",
180: "Actuator", # deprecated
190: "Gauge Interface", #deprecated
200: "Gauge Large", # deprecated
210: "Gauge Small" # deprecated
},
60: {130: "Bottom Depth",
135: "Bottom Depth/Speed",
136: "Bottom Depth/Speed/Temperature",
140: "Ownship Attitude",
145: "Ownship Position (GNSS)",
150: "Ownship Position (Loran C)",
155: "Speed",
160: "Turn Rate Indicator", # deprecated
170: "Integrated Navigaton", # deprecated
175: "Integrated Navigation System",
190: "Navigation Management",
195: "Automatic Identification System (AIS)",
200: "Radar",
201: "Infrared Imaging",
205: "ECDIS", # deprecated
210: "ECS", # deprecated
220: "Direction Finder", # deprecated
230: "Voyage Status"
},
70: {130: "EPIRB", # deprecated
140: "AIS", # deprecated
150: "DSC", # deprecated
160: "Data Receiver/Transceiver",
170: "Satellite",
180: "Radio-telephone (MF/HF)", # deprecated
190: "Radiotelephone"
},
75: {130: "Temperature",
140: "Pressure",
150: "Fluid Level",
160: "Flow",
170: "Humidity"
},
80: {130: "Time/Date Systems", # deprecated
140: "VDR", # deprecated
150: "Integrated Instrumentation", # deprecated
160: "General Purpose Displays", # deprecated
170: "General Sensor Box", # deprecated
180: "Wheather Instruments", # deprecated
190: "Transducer/General", # deprecated
200: "NMEA 0183 Converter" # deprecated
},
85: {130: "Athmospheric",
140: "Aquatic"
},
90: {130: "HVAC"
},
100: {130: "Scale (Catch)"
},
110: {130: "Button Interface",
135: "Switch Interface",
140: "Analog Interface"
},
120: {130: "Display",
140: "Alarm Enunciator"
},
125: {130: "Multimedia Player",
140: "Multimedia Controller"
}
}
fluidtype = {
0: "Fuel",
1: "Water",
2: "Gray Water",
3: "Live Well",
4: "Oil",
5: "Black Water",
6: "Fuel Gasoline",
14: "Error",
15: "Unavailable"
}
industrygroup = {
0: "Global",
1: "Highway",
2: "Agriculture",
3: "Construction",
4: "Marine",
5: "Industrial"
}
manufacturer = {
69: "ARKS Enterprises, Inc.",
78: "FW Murphy/Enovation Controls",
80: "Twin Disc",
85: "Kohler Power Systems",
88: "Hemisphere GPS Inc",
116: "BEP Marine",
135: "Airmar",
137: "Maretron",
140: "Lowrance",
144: "Mercury Marine",
147: "Nautibus Electronic GmbH",
148: "Blue Water Data",
154: "Westerbeke",
161: "Offshore Systems (UK) Ltd.",
163: "Evinrude/BRP",
165: "CPAC Systems AB",
168: "Xantrex Technology Inc.",
172: "Yanmar Marine",
174: "Volvo Penta",
175: "Honda Marine",
176: "Carling Technologies Inc. (Moritz Aerospace)",
185: "Beede Instruments",
192: "Floscan Instrument Co. Inc.",
193: "Nobletec",
198: "Mystic Valley Communications",
199: "Actia",
200: "Honda Marine",
201: "Disenos Y Technologia",
211: "Digital Switching Systems",
215: "Xintex/Atena",
224: "EMMI NETWORK S.L.",
225: "Honda Marine",
228: "ZF",
229: "Garmin",
233: "Yacht Monitoring Solutions",
235: "Sailormade Marine Telemetry/Tetra Technology LTD",
243: "Eride",
250: "Honda Marine",
257: "Honda Motor Company LTD",
272: "Groco",
273: "Actisense",
274: "Amphenol LTW Technology",
275: "Navico",
283: "Hamilton Jet",
285: "Sea Recovery",
286: "Coelmo SRL Italy",
295: "BEP Marine",
304: "Empir Bus",
305: "NovAtel",
306: "Sleipner Motor AS",
307: "MBW Technologies",
311: "Fischer Panda",
315: "ICOM",
328: "Qwerty",
329: "Dief",
341: "Böning Automationstechnologie GmbH & Co. KG",
345: "Korean Maritime University",
351: "Thrane and Thrane",
355: "Mastervolt",
356: "Fischer Panda Generators",
358: "Victron Energy",
370: "Rolls Royce Marine",
373: "Electronic Design",
374: "Northern Lights",
378: "Glendinning",
381: "B & G",
384: "Rose Point Navigation Systems",
385: "Johnson Outdoors Marine Electronics Inc Geonav",
394: "Capi 2",
396: "Beyond Measure",
400: "Livorsi Marine",
404: "ComNav",
409: "Chetco",
419: "Fusion Electronics",
421: "Standard Horizon",
422: "True Heading AB",
426: "Egersund Marine Electronics AS",
427: "em-trak Marine Electronics",
431: "Tohatsu Co, JP",
437: "Digital Yacht",
438: "Comar Systems Limited",
440: "Cummins",
443: "VDO (aka Continental-Corporation)",
451: "Parker Hannifin aka Village Marine Tech",
459: "Alltek Marine Electronics Corp",
460: "SAN GIORGIO S.E.I.N",
466: "Veethree Electronics & Marine",
467: "Humminbird Marine Electronics",
470: "SI-TEX Marine Electronics",
471: "Sea Cross Marine AB",
475: "GME aka Standard Communications Pty LTD",
476: "Humminbird Marine Electronics",
478: "Ocean Sat BV",
481: "Chetco Digitial Instruments",
493: "Watcheye",
499: "Lcj Capteurs",
502: "Attwood Marine",
503: "Naviop S.R.L.",
504: "Vesper Marine Ltd",
510: "Marinesoft Co. LTD",
517: "NoLand Engineering",
518: "Transas USA",
529: "National Instruments Korea",
532: "Onwa Marine",
571: "Marinecraft (South Korea)",
573: "McMurdo Group aka Orolia LTD",
578: "Advansea",
579: "KVH",
580: "San Jose Technology",
583: "Yacht Control",
586: "Suzuki Motor Corporation",
591: "US Coast Guard",
595: "Ship Module aka Customware",
600: "Aquatic AV",
605: "Aventics GmbH",
606: "Intellian",
612: "SamwonIT",
614: "Arlt Tecnologies",
637: "Bavaria Yacts",
641: "Diverse Yacht Services",
644: "Wema U.S.A dba KUS",
645: "Garmin",
658: "Shenzhen Jiuzhou Himunication",
688: "Rockford Corp",
704: "JL Audio",
715: "Autonnic",
717: "Yacht Devices",
734: "REAP Systems",
735: "Au Electronics Group",
739: "LxNav",
743: "DaeMyung",
744: "Woosung",
773: "Clarion US",
776: "HMI Systems",
777: "Ocean Signal",
778: "Seekeeper",
781: "Poly Planar",
785: "Fischer Panda DE",
795: "Broyda Industries",
796: "Canadian Automotive",
797: "Tides Marine",
798: "Lumishore",
799: "Still Water Designs and Audio",
802: "BJ Technologies (Beneteau)",
803: "Gill Sensors",
811: "Blue Water Desalination",
815: "FLIR",
824: "Undheim Systems",
838: "TeamSurv",
844: "Fell Marine",
847: "Oceanvolt",
862: "Prospec",
868: "Data Panel Corp",
890: "L3 Technologies",
894: "Rhodan Marine Systems",
896: "Nexfour Solutions",
905: "ASA Electronics",
909: "Marines Co (South Korea)",
911: "Nautic-on",
930: "Ecotronix",
962: "Timbolier Industries",
963: "TJC Micro",
968: "Cox Powertrain",
969: "Blue Seas",
1850: "Teleflex Marine (SeaStar Solutions)",
1851: "Raymarine",
1852: "Navionics",
1853: "Japan Radio Co",
1854: "Northstar Technologies",
1855: "Furuno",
1856: "Trimble",
1857: "Simrad",
1858: "Litton",
1859: "Kvasar AB",
1860: "MMP",
1861: "Vector Cantech",
1862: "Yamaha Marine",
1863: "Faria Instruments",
2046: "Open Boat Projects"
}
pilotmode = {
64: "Standby",
66: "Auto",
70: "Wind",
74: "Track"
}
pressure = {
0: "Athmospheric",
1: "Water",
2: "Steam",
3: "Compressed Air",
4: "Hydraulic",
5: "Filter",
6: "AltimeterSetting",
7: "Oil",
8: "Fuel"
}
prnusage = {
0: "Not Tracked",
1: "Tracked",
2: "Used",
3: "Not tracked+Diff",
4: "Tracked+Diff",
5: "Used+Diff",
14: "Error",
15: "No Selection"
}
speedwater = {
0: "Paddle wheel",
1: "Pitot tube",
2: "Doppler",
3: "Correlation (ultra sound)",
4: "Electro Magnetic"
}
timesource = {
0: "GPS",
1: "GLONASS",
2: "Radio Station",
3: "Local Cesium clock",
4: "Local Rubidium clock",
5: "Local Crystal clock"
}
temperature = {
0: "Sea Temperature",
1: "Outside Temperature",
2: "Inside Temperature",
3: "Engine Room Temperature",
4: "Main Cabin Temperature",
5: "Live Well Temperature",
6: "Bait Well Temperature",
7: "Refrigeration Temperature",
8: "Heating System Temperature",
9: "Dew Point Temperature",
10: "Apparent Wind Chill Temperature",
11: "Theoretical Wind Chill Temperature",
12: "Heat Index Temperature",
13: "Freezer Temperature",
14: "Exhaust Gas Temperature",
15: "Shaft Seal Temperature"
}
xtemode = {
0: "auto",
1: "differential",
2: "estimated",
3: "simulation",
4: "manual"
}

56
mavg.py Normal file
View File

@ -0,0 +1,56 @@
"""
Moving Average
returns a float so no integer rounding applied
"""
class mAvg():
def __init__(self, size):
self.size = size
self.data = [0] * size
self.nval = 0
self.sum = 0
self.next = 0
def addVal(self, value):
# add a new value und return new current avg
if self.nval < self.size:
# list is not filled
self.nval += 1
self.sum += value
else:
# list is filled
self.sum = self.sum - self.data[self.next] - value
self.data.[self.next] = value
self.next += 1
# if end of list reached, start over from beginning
if self.next = self.size:
self.next = 0
return self.sum / self.nval
def getAvg(self, points=None):
if self.nval == 0:
return None
# get avg of all collected data
if not points:
return self.sum / self.nval
# get avg of subset
if points > self.nval:
return None
sum = 0
i = self.next
p = points
while p > 0:
p -= 1
if i == 0:
i = self.size - 1
else:
i -= 1
sum += self.data[i]
return sum / points
def reset(self):
self.nval = 0
self.next = 0
self.sum = 0

288
parser.py Normal file
View File

@ -0,0 +1,288 @@
"""
PGNs verarbeiten
"""
import struct
import time
from datetime import timedelta, date
from . import lookup
def parse_60928(buf, device):
"""
Sets data in device and returns the 64bit NAME of device
"""
device.lastseen = time.time()
# 21 bits Unique-ID und 11 bits Manuf.-Code
device.uniqueid = ((buf[0] << 16) + (buf[1] << 8) + buf[2]) >> 3
device.manufacturercode = (buf[3] * 256 + buf[2]) >> 5
device.instance = buf[4]
device.instlower = buf[4] & 0x07
device.instupper = buf[4] >> 3
device.devicefunction = buf[5]
device.deviceclass = (buf[6] & 0x7f) >> 1
device.industrygroup = (buf[7] >> 4) & 0x07 # 3bit
device.sysinstance = buf[7] & 0x0f # 4bit
return struct.unpack_from('>Q', buf, 0)[0]
def parse_126992(buf, source):
# System time
print(f"PGN 126992 System time from {source}")
sid = buf[0]
src = buf[1] & 0x0f
dval = date(1970,1,1) + timedelta(days=(buf[3] << 8) + buf[2])
secs = struct.unpack_from('<L', buf, 4)[0] * 0.0001
print(f" source={source}, date={dval}, secs={secs}, ts={lookup.timesource[src]}")
def parse_126993(buf, device):
# Heartbeat
print(f"Heartbeat from {device.address}")
print(buf)
def parse_126996(buf, device):
# Product information
n2kvers = (buf[0] + buf[1] * 256) / 1000
prodcode = buf[2] + buf[3] * 256
modelid = buf[4:36] # 256bit modelid ascii text
softvers = buf[36:68] # 256bit software version code ascii text
modelvers = buf[68:100] # 256bit model version ascii text
serial = buf[100:132] # 256bit model serial code ascii text
# Füllzeichen entfernen. 0x20, 0xff, '@' für AIS
# Die N2K-Bibliothek von ttlappalainen liefert 0xff
modelid = modelid.rstrip(b'\xff')
softvers = softvers.rstrip(b'\xff')
modelvers = modelvers.rstrip(b'\xff')
serial = serial.rstrip(b'\xff')
# Übertragen in die Gerätedaten
device.n2kvers = n2kvers
device.productcode = prodcode
device.modelvers = modelvers.decode('ascii').rstrip()
device.softvers = softvers.decode('ascii').rstrip()
device.product = modelid.decode('ascii').rstrip()
device.serial = serial.decode('ascii').rstrip()
device.certlevel = buf[132]
device.loadequiv = buf[133]
def parse_126998(buf, source, device):
# Configuration information
# Installation Description 1
txtlen = buf[0]
if txtlen > 2:
device.instdesc1 = buf[2:txtlen].decode('ascii')
p = txtlen
else:
device.instdesc1 = ""
p = 2
# Installation Description 2
txtlen = buf[p]
if txtlen > 2:
device.instdesc2 = buf[p+2:p+txtlen].decode('ascii')
p += txtlen
else:
device.instdesc2 = ""
p += 2
# Manufacturer Info
txtlen = buf[p]
if txtlen > 2:
device.manufinfo = buf[p+2:p+txtlen].decode('ascii')
else:
device.manufinfo = ""
def parse_127257(buf, boatdata):
# Attitude
sid = buf[0]
yaw = struct.unpack_from('<h', buf, 1)[0] * 0.0001
pitch = struct.unpack_from('<h', buf, 3)[0] * 0.0001
roll = struct.unpack_from('<h', buf, 5)[0] * 0.0001
boatdata.setValue("YAW", yaw)
boatdata.setValue("PTCH", pitch)
boatdata.setValue("ROLL", roll)
def parse_127501(buf):
# Switch control status
# 7 byte for switches every switch takes 2 bits
# 0 = Off, 1=On, 2 and 3 unknown
instance = buf[0]
switch = [0] * 28
ix = 0
for b in range(1, 8):
val = buf[b]
for x in range(4):
switch[ix] = val & 0x03
val = val >> 2
ix += 1
def parse_127502(buf):
# Switch control
# 7 byte for switches every switch takes 2 bits
# 0 = Off, 1=On, 2 and 3 unknown
instance = buf[0]
switch = [0] * 28
ix = 0
for b in range(1, 8):
val = buf[b]
for x in range(4):
switch[ix] = val & 0x03
val = val >> 2
ix += 1
def parse_127505(buf, boatdata):
# Fluid Level
instance = buf[0] & 0x0f
if instance in boatdata.tank:
boatdata.tank[instance].fluidtype = buf[0] >> 4
boatdata.tank[instance].level = struct.unpack_from('<H', buf, 1)[0] * 0.004
boatdata.tank[instance].capacity = struct.unpack_from('<L', buf, 3)[0] * 0.1
print(boatdata.tank[instance])
else:
print(f"Tank {instance} not defined!")
def parse_127508(buf, boatdata):
# Battery status
instance = buf[0]
voltage = (buf[2] * 256 + buf[1]) * 0.01
current = (buf[4] * 256 + buf[3]) * 0.1
temp = (buf[6] * 256 + buf[5]) * 0.01 - 273.15
sid = buf[7]
def parse_129025(buf, boatdata):
# Position, Rapid Update
lat = struct.unpack_from('<l', buf, 0)[0] * 1e-07
lon = struct.unpack_from('<l', buf, 4)[0] * 1e-07
boatdata.setValue("LAT", lat)
boatdata.setValue("LON", lon)
def parse_129026(buf, boatdata):
# COG & SOG, Rapid Update
sid = buf[0]
cogref = buf[1] >> 6 # 0: true, 1: magnetic, 2: error
cog = struct.unpack_from('<H', buf, 2)[0] * 0.0001 # rad
sog = struct.unpack_from('<H', buf, 4)[0] * 0.01 # m/s
# 2 Byte reserved
boatdata.setValue("COG", cog)
boatdata.setValue("SOG", sog)
def parse_129027(buf, boatdata):
# TODO
# Position Delta Rapid Update
sid = buf[0]
dt = struct.unpack_from('<H', buf, 1)[0],
dlat = struct.unpack_from('<h', buf, 3)[0]
dlon = struct.unpack_from('<h', buf, 5)[0]
# 1 Byte reserved
def parse_129029(buf, boatdata):
# GNSS Position Data
'''
1 sid
2 date
4 time seconds since midn
8 lat
8 lon
8 alt
4 bit gnss type
{"name": "GPS", "value": 0},
{"name": "GLONASS", "value": 1},
{"name": "GPS+GLONASS", "value": 2},
{"name": "GPS+SBAS/WAAS", "value": 3},
{"name": "GPS+SBAS/WAAS+GLONASS", "value": 4},
{"name": "Chayka", "value": 5},
{"name": "integrated", "value": 6},
{"name": "surveyed", "value": 7},
{"name": "Galileo", "value": 8}]},
4bit method
{"name": "no GNSS", "value": 0},
{"name": "GNSS fix", "value": 1},
{"name": "DGNSS fix", "value": 2},
{"name": "Precise GNSS", "value": 3},
{"name": "RTK Fixed Integer", "value": 4},
{"name": "RTK float", "value": 5},
{"name": "Estimated (DR) mode", "value": 6},
{"name": "Manual Input", "value": 7},
{"name": "Simulate mode", "value": 8}]},
2bit integrity
{"name": "No integrity checking", "value": 0},
{"name": "Safe", "value": 1},
{"name": "Caution", "value": 2}]},
bit reserved
1 byte uint numberOfSvs Number of satellites used in solution
2byte hdop
2 byte tdop
4 byte geoidalSeparation
1 byte Number of reference stations
4bit referenceStationType
12bit referenceStationId
2byte sageOfDgnssCorrections
'''
def parse_129033(buf, boatdata):
# Time & Date
gpsdate = struct.unpack_from('<H', buf, 0)[0] # days
gpstime = struct.unpack_from('<L', buf, 2)[0] # seconds since midnight
offset = struct.unpack_from('<h', buf, 6)[0] # local offset
# TODO
def parse_129283(buf, boatdata):
# TODO Cross Track Error XTE
sid = buf[0]
mode = buf[1] >> 4
navterm = buf[1] & 0x03
xte = struct.unpack_from('<l', buf, 2)[0] * 0.01 # m
# 2 Byte reserved
boatdata.setValue("XTE", xte)
def parse_129540(buf, boatdata):
#sid = buf[0]
#rrmode = buf[1]
nsats = buf[2]
# Datensatz je Sat Länge: 12 Byte
smax = nsats * 12
for s in range(0, smax, 12):
prn = buf[3 + s]
elevation = struct.unpack_from('<h', buf, s+4)[0] * 0.0001
azimuth = struct.unpack_from('<H', buf, s+6)[0] * 0.0001
snr = struct.unpack_from('<H', buf, s+8)[0] * 0.01
rres = struct.unpack_from('<l', buf, s+10)[0]
status = buf[s+14] & 0x0f
boatdata.updateSatellite(prn, elevation, azimuth, snr, rres, status)
def parse_130312(buf, boatdata):
# Temperature
src = buf[2] # lookup "temperature" (0 .. 15)
val = (buf[4] * 256 + buf[3]) * 0.01 # Kelvin
if instance == 0 and src == 1:
boatdata.setValue("xdrTemp", val)
elif instance in boatdata.temp:
boatdata.temp[instance].value = val
def parse_130314(buf, boatdata):
# Pressure
sid = buf[0]
instance = buf[1]
src = buf[2] # lookup "pressure"
pressure = struct.unpack_from('<L', buf, 3)[0] * 0.1 # Pa
if instance == 0 and src == 0:
# Generischer Luftdruckwert
boatdata.setValue("xdrPress", pressure)
if instance in boatdata.press:
# Verschiedene weitere Drücke
# TODO sensortype "src"
boatdata.press[instance].value = pressure
def parse_130316(buf, boatdata):
# Temperature, extended range
sid = buf[0]
instance = buf[1]
src = buf[2] # lookup "temperature" (0 .. 15)
val = ((buf[5] << 16) | (buf[4] << 8) | buf[3]) * 0.001
# TODO save in global temp data
# Konflikt mit 130312?
#if instance == 0 and src == 2:
# boatdata.setValue("xdrTemp", val)
# save in engine data
if src == 14 and instance in boatdata.engine:
boatdata.engine[instance].exhaust_temp = val

275
pgntype.py Normal file
View File

@ -0,0 +1,275 @@
pgntype = {
59392: "S",
59904: "S",
60160: "S",
60416: "S",
60928: "S",
61184: "S",
65001: "S",
65002: "S",
65003: "S",
65004: "S",
65005: "S",
65006: "S",
65007: "S",
65008: "S",
65009: "S",
65010: "S",
65011: "S",
65012: "S",
65013: "S",
65014: "S",
65015: "S",
65016: "S",
65017: "S",
65018: "S",
65019: "S",
65020: "S",
65021: "S",
65022: "S",
65023: "S",
65024: "S",
65025: "S",
65026: "S",
65027: "S",
65028: "S",
65029: "S",
65030: "S",
65240: "I",
65280: "S",
65284: "S",
65285: "S",
65286: "S",
65287: "S",
65288: "S",
65289: "S",
65290: "S",
65292: "S",
65293: "S",
65302: "S",
65305: "S",
65309: "S",
65312: "S",
65340: "S",
65341: "S",
65345: "S",
65350: "S",
65359: "S",
65360: "S",
65361: "S",
65371: "S",
65374: "S",
65379: "S",
65408: "S",
65409: "S",
65410: "S",
65420: "S",
65480: "S",
126208: "F",
126464: "F",
126720: "F",
126983: "F",
126984: "F",
126985: "F",
126986: "F",
126987: "F",
126988: "F",
126992: "S",
126993: "S",
126996: "F",
126998: "F",
127233: "F",
127237: "F",
127245: "S",
127250: "S",
127251: "S",
127252: "S",
127257: "S",
127258: "S",
127488: "S",
127489: "F",
127490: "F",
127491: "F",
127493: "S",
127494: "F",
127495: "F",
127496: "F",
127497: "F",
127498: "F",
127500: "S",
127501: "S",
127502: "S",
127503: "F",
127504: "F",
127505: "S",
127506: "F",
127507: "F",
127508: "S",
127509: "F",
127510: "F",
127511: "S",
127512: "S",
127513: "F",
127514: "S",
127744: "S",
127745: "S",
127746: "S",
127750: "S",
127751: "S",
128000: "S",
128001: "S",
128002: "S",
128003: "S",
128006: "S",
128007: "S",
128008: "S",
128259: "S",
128267: "S",
128275: "F",
128520: "F",
128538: "F",
128768: "S",
128769: "S",
128776: "S",
128777: "S",
128778: "S",
128780: "S",
129025: "S",
129026: "S",
129027: "S",
129028: "S",
129029: "F",
129033: "S",
129038: "F",
129039: "F",
129040: "F",
129041: "F",
129044: "F",
129045: "F",
129283: "S",
129284: "F",
129285: "F",
129291: "S",
129301: "F",
129302: "F",
129538: "F",
129539: "S",
129540: "F",
129541: "F",
129542: "F",
129545: "F",
129546: "S",
129547: "F",
129549: "F",
129550: "F",
129551: "F",
129556: "F",
129792: "F",
129793: "F",
129794: "F",
129795: "F",
129796: "F",
129797: "F",
129798: "F",
129799: "F",
129800: "F",
129801: "F",
129802: "F",
129803: "F",
129804: "F",
129805: "F",
129806: "F",
129807: "F",
129808: "F",
129809: "F",
129810: "F",
130052: "F",
130053: "F",
130054: "F",
130060: "F",
130061: "F",
130064: "F",
130065: "F",
130066: "F",
130067: "F",
130068: "F",
130069: "F",
130070: "F",
130071: "F",
130072: "F",
130073: "F",
130074: "F",
130306: "S",
130310: "S",
130311: "S",
130312: "S",
130313: "S",
130314: "S",
130315: "S",
130316: "S",
130320: "F",
130321: "F",
130322: "F",
130323: "F",
130324: "F",
130330: "F",
130560: "S",
130561: "F",
130562: "F",
130563: "F",
130564: "F",
130565: "F",
130566: "F",
130567: "F",
130569: "F",
130570: "F",
130571: "F",
130572: "F",
130573: "F",
130574: "F",
130576: "S",
130577: "F",
130578: "F",
130579: "S",
130580: "F",
130581: "F",
130582: "S",
130583: "F",
130584: "F",
130585: "S",
130586: "F",
130816: "F",
130817: "F",
130818: "F",
130819: "F",
130820: "F",
130821: "F",
130822: "F",
130823: "F",
130824: "F",
130825: "F",
130827: "F",
130828: "F",
130831: "F",
130832: "F",
130833: "F",
130834: "F",
130835: "F",
130836: "F",
130837: "F",
130838: "F",
130839: "F",
130840: "F",
130842: "F",
130843: "F",
130845: "F",
130846: "F",
130847: "F",
130850: "F",
130851: "F",
130856: "F",
130860: "F",
130880: "F",
130881: "F",
130944: "F"
}

27
queue.py Normal file
View File

@ -0,0 +1,27 @@
from heapdict import heapdict
import time
import threading
class FrameQueue():
def __init__(self):
self.heap = heapdict()
self.age_interval = 5 # seconds
def push(self, frame, priority):
timestamp = time.time() # microsecond resolution
self.heap[timestamp] = (priority, timestamp, frame)
def pop(self):
p, f = self.heap.popitem()
self.age()
return f[2]
def age(self):
current_time = time.time()
for key in list(self.heap.keys()):
if current_time - key > self.age_interval:
self.heap[key][0] -= 1
def is_empty(self):
return len(self.heap) == 0

61
receiver.py Normal file
View File

@ -0,0 +1,61 @@
'''
Momentan nur Idee
Verarbeiten von Frames
Nur Empfang von Paketen / Frames
- single
- fast
- transport
fast packets können parallel von mehreren Quellen verarbeitet werden
'''
import math
class FpReceiver():
def __init__(self):
self.sc0 = {}
self.nf = {}
self.frame = {}
self.wip = []
def receive(self, data, source)
'''
Liefert True wenn Paket komplett empfangen wurde
'''
sc = (data[0] & 0xf0) >> 5
fc = data[0] & 0x1f
if not source in self.wip:
# Erster Frame
if fc != 0:
# unbekannte Herkunft und kein Startframe
continue
self.sc0[source] = sc
datalen = data[1]
self.nf[source] = math.ceil((datalen - 6) / 7) + 1 # Anzahl Frames
self.frame[source] = {fc : data[2:]} # Ersten Frame merken
else:
# Folgeframes
if (sc == self.sc0[source]):
# TODO prüfe ob der Framecounter fc schon vorgekommen ist
if not fc in self.frame[source]:
self.frame[source][fc] = data[1:8]
else:
# TODO Fehler im Fast-Packet: doppelter fc!
raise('Frame error: duplicate fc')
if len(self.frame[source]) == self.nf[source]:
self.wip.remove(source)
return True
return False
def getPacket(self, source)
# TODO Frames in der richtigen reihenfolge zusammensetzen
packet = bytearray()
for frame in sorted(self.frame.items()):
print(frame)
#packet.extend()
return packet

17
routing.py Normal file
View File

@ -0,0 +1,17 @@
'''
Routen und Wegepunkte
'''
class Waypoint():
def __init__(self, number, name):
self.number = number
self.name = name
self.lat = None
self.lon = None
class Route():
def __init__(self, number, name)
self.number = number
self.name = name
self.wps = dict()
def getActiveWP(self):
return None

179
valdesc.py Normal file
View File

@ -0,0 +1,179 @@
'''
Lange Beschreibung der Daten, mehrsprachig
'''
desc = {
"ALT": {
"en": "Altitude",
"de": "Höhe über Grund"
},
"AWA": {
"en": "Apparant Wind Angle",
"de": "Scheinbare Windrichtung"
},
"AWS": {
"en": "Apparant Wind Speed",
"de": "Scheinbare Windgeschwindigkeit"
},
"BTW": {
"en": "Bearing To Waypoint",
"de": "Kurs zum nächsten Wegepunkt"
},
"COG": {
"en": "Course Over Ground",
"de": "Kurs über Grund"
},
"DBS": {
"en": "Depth Below Surface",
"de": "Tiefe unter Wasseroberfläche"
},
"DBT": {
"en": "Depth Below Transducer",
"de": "Tiefe unter Sensor"
},
"DEV": {
"en": "Deviation",
"de": "Kursabweichung"
},
"DTW": {
"en": "Distance To Waypoint",
"de": "Entfernung zum nächsten Wegepunkt"
},
"GPSD": {
"en": "GPS Date",
"de": "GPS-Datum"
},
"GPST": {
"en": "GPS Time",
"de": "GPS-Zeit"
},
"HDM": {
"en": "Magnetic Heading",
"de": "Magnetischer Kurs"
},
"HDT": {
"en": "Heading",
"de": "Wahrer rechtweisender Kurs"
},
"HDOP": {
"en": "Horizontal Dilation Of Position",
"de": "Positionsgenauigkeit in der Horizontalen"
},
"LAT": {
"en": "Latitude",
"de": "Geographische Breite"
},
"LON": {
"en": "Longitude",
"de": "Geographische Länge"
},
"Log": {
"en": "Logged distance",
"de": "Entfernung"
},
"MaxAws": {
"en": "Maximum Apperant Wind Speed",
"de": "Maximum der relativen Windgeschwindigkeit"
},
"MaxTws": {
"en": "Maximum True Wind Speed",
"de": "Maximum der wahren Windgeschwindigkeit"
},
"PDOP": {
"en": "Position dilation",
"de": "Positionsgenauigkeit im Raum"
},
"PRPOS": {
"en": "Secondary Rudder Position",
"de": "Auslenkung Sekundärruder"
},
"ROT": {
"en": "Rotation",
"de": "Drehrate"
},
"RPOS": {
"en": "Rudder Position",
"de": "Auslenkung Ruder"
},
"SOG": {
"en": "Speed Over Ground",
"de": "Geschwindigkeit über Grund"
},
"STW": {
"en": "Speed Through Water",
"de": "Geschwindigkeit durch das Wasser"
},
"SatInfo": {
"en": "Satellit Info",
"de": "Anzahl der sichtbaren Satelliten"
},
"TWD": {
"en": "True Wind Direction",
"de": "Wahre Windrichtung"
},
"TWS": {
"en": "True Wind Speed",
"de": "Wahre Windgeschwindigkeit"
},
"TZ": {
"en": "Timezone",
"de": "Zeitzone"
},
"TripLog": {
"en": "Trip Log",
"de": "Tages-Entfernungszähler"
},
"VAR": {
"en": "Course Variation",
"de": "Abweichung vom Sollkurs"
},
"VDOP": {
"en": "Vertical Dilation Of Position",
"de": "Positionsgenauigkeit in der Vertikalen"
},
"WPLat": {
"en": "Waypoint Latitude",
"de": "Geo. Breite des Wegpunktes"
},
"WPLon": {
"en": "Waypoint Longitude",
"de": "Geo. Länge des Wegpunktes"
},
"WTemp": {
"en": "Water Temperature",
"de": "Wassertemperatur"
},
"XTE": {
"en": "Cross Track Error",
"de": "Kursfehler"
},
# Sonderwerte
"xdrHum": {
"en": "Humidity",
"de": "Luftfeuchte"
},
"xdrPress": {
"en": "Pressure",
"de": "Luftdruck"
},
"xdrRotK": {
"en": "Keel Rotation",
"de": "Kielrotation"
},
"tdrTemp": {
"en": "Temperature",
"de": "Temperatur"
},
"xdrVBat": {
"en": "Battery Voltage",
"de": "Batteriespannung"
},
# WIP
"xdrCurr": {
"en": "Current",
"de": "Stromstärke"
}
}