commit 9ce295f085ff283847c751301564c579dea28bf2 Author: Thomas Hooge Date: Tue Mar 11 13:05:11 2025 +0100 First commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b25c15b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*~ diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..16ba3d3 --- /dev/null +++ b/__init__.py @@ -0,0 +1,3 @@ +from .device import Device +from .boatdata import BoatData +from .hbuffer import History, HistoryBuffer diff --git a/boatdata.py b/boatdata.py new file mode 100644 index 0000000..74b5254 --- /dev/null +++ b/boatdata.py @@ -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 diff --git a/device.py b/device.py new file mode 100644 index 0000000..3557d39 --- /dev/null +++ b/device.py @@ -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(' 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 (+/- ) + """ + 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() diff --git a/lookup.py b/lookup.py new file mode 100644 index 0000000..0e3df9e --- /dev/null +++ b/lookup.py @@ -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" +} diff --git a/mavg.py b/mavg.py new file mode 100644 index 0000000..9f5327f --- /dev/null +++ b/mavg.py @@ -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 diff --git a/parser.py b/parser.py new file mode 100644 index 0000000..63a1e9d --- /dev/null +++ b/parser.py @@ -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(' 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('> 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('> 6 # 0: true, 1: magnetic, 2: error + cog = struct.unpack_from('> 4 + navterm = buf[1] & 0x03 + xte = struct.unpack_from(' self.age_interval: + self.heap[key][0] -= 1 + + def is_empty(self): + return len(self.heap) == 0 diff --git a/receiver.py b/receiver.py new file mode 100644 index 0000000..4108533 --- /dev/null +++ b/receiver.py @@ -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 diff --git a/routing.py b/routing.py new file mode 100644 index 0000000..773fb25 --- /dev/null +++ b/routing.py @@ -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 diff --git a/valdesc.py b/valdesc.py new file mode 100644 index 0000000..fced0e4 --- /dev/null +++ b/valdesc.py @@ -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" + } + +}