NMEA2000-Code entfernt. Wird über eigenständige Bibliothek eingebunden.

This commit is contained in:
Thomas Hooge 2025-03-11 14:10:20 +01:00
parent bdbd168123
commit 81836bc5f1
15 changed files with 22 additions and 2472 deletions

6
README
View File

@ -17,7 +17,7 @@ Zusatzhardware:
- NMEA2000 Interface
- PiCAN-M (hiermit wird entwickelt)
- Waveshare RS485 CAN HAT (ungetestet)
- BME280-Sensor
- BME280-Sensor über I2C
- GPS über USB/seriell angeschlossen
Zusatzsoftware:
@ -35,7 +35,7 @@ Für BME280
- smbus2
- bme280
Zur Steuerung des Geräts sind 6 Tasten vorhanen. Numeriert von 1 bis 6 von
Zur Steuerung des Geräts sind 6 Tasten vorhanden. Numeriert von 1 bis 6 von
links nach rechts. Die Tasten können angeklickt werden und führen dann direkt
eine von der jeweiligen Seite abhängige Funktion aus.
Die jeweilige Funktion wird durch ein Symbol oberhalb der Taste dargestellt.
@ -49,7 +49,7 @@ Folgende Wischfunktionen sind implementiert:
1. Programmende durch die Wischfunktion "2" -> "1"
2. Tastensperre an: "6" -> "1"
3. Tastensperre aus: "1" -> "6"
4. Systemseite: "5" -> "6"
4. Systemseite: "4" -> "5"
Routen und Wegepunkte können von OpenCPN empfangen werden. Dazu muß eine
passende serielle Schnittstelle für den NMEA0183-Ausgang definiert werden.

View File

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

View File

@ -1,574 +0,0 @@
'''
!!! 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

View File

@ -1,120 +0,0 @@
"""
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 Address-Claim gefüllt
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
"""
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))
return data
def __str__(self):
intNAME = int.from_bytes(self.getNAME())
out = f"Device: {self.address} : '{self.product}'\n"
out += " NAME: {} ({})\n".format(self.getNAME(), intNAME)
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

View File

@ -1,17 +0,0 @@
'''
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)

View File

@ -1,300 +0,0 @@
"""
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()

View File

@ -1,566 +0,0 @@
# 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",
135: "NMEA 0183 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",
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: { # NEW? WIP
},
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"
}

View File

@ -1,56 +0,0 @@
"""
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

View File

@ -1,262 +0,0 @@
'''
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_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, s+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

View File

@ -1,275 +0,0 @@
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"
}

View File

@ -1,27 +0,0 @@
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

View File

@ -1,61 +0,0 @@
'''
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

View File

@ -1,17 +0,0 @@
'''
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

View File

@ -1,179 +0,0 @@
'''
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"
}
}

View File

@ -1,8 +1,12 @@
#!/usr/bin/env python
'''
"""
Virtuelles Multifunktionsgerät
NMEA2000
deviceclass: 120 - Display
devicefunction: 130 - Display
Benötigte Pakete:
python3-cairo python3-gi python3-gi-cairo gir1.2-rsvg-2.0
@ -38,10 +42,11 @@ Buttonlayout:
Button 6 ist reserviert für Beleuchtung ILUM
Neu
Standardbelegung der Tasten
Button 1: Mode
Button 2: Abbruch
Button 3: zurück
Button 4: hoch
Button 4: weiter
Button 5: Ok/Menu
Verlagerung der Seitenanzeige in den Titel als Ziffer in Klammern
@ -49,12 +54,12 @@ Verlagerung der Seitenanzeige in den Titel als Ziffer in Klammern
Vergleich zum Garmin GMI20:
[ Zurück ] [ Hoch ] [ Menü ] [ Runter ] [ on/off ]
Vergleich zum Raymarine I70s:
[ ON/ILUM ] [ Hoch ] [ Runter ] [ MENU ]
Button 1 wird für AVG verwendet auf mehreren Seiten
Button 5 wird für Trend TRND verwendet
Aber sowas von WIP
Änderungsprotokoll
==================
@ -65,7 +70,7 @@ Version Datum Änderung(en) von
0.2 2024-12-24 Veräffentlichung als Git-Repository tho
'''
"""
import os
import sys
@ -429,14 +434,15 @@ class Frontend(Gtk.Window):
self.curpage = self.pages[self.pageno]
elif selected == 5:
# Ok/Menü
self.curpage.handle_key(5)
elif selected == 6:
if self.button_clicked == 6:
# Backlight on/off
self.curpage.backlight = not self.curpage.backlight
elif self.button_clicked == 5:
if self.button_clicked == 4:
# Umschalten zur Systemseite
self.curpage = self.pages[0]
else:
self.curpage.handle_key(5)
elif selected == 6:
if not self.curpage.handle_key(6):
# Backlight on/off
self.curpage.backlight = not self.curpage.backlight
return True
def on_destroy(self, widget):
@ -551,4 +557,5 @@ if __name__ == "__main__":
shutdown = True
t_rxd_n2k.join()
t_rxd_0183.join()
t_data.join()
print("Another fine product of the Sirius Cybernetics Corporation.")