First commit
This commit is contained in:
commit
9ce295f085
|
@ -0,0 +1 @@
|
|||
*~
|
|
@ -0,0 +1,3 @@
|
|||
from .device import Device
|
||||
from .boatdata import BoatData
|
||||
from .hbuffer import History, HistoryBuffer
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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()
|
|
@ -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"
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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"
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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"
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue