OBP60v/nmea2000/boatdata.py

575 lines
17 KiB
Python

'''
!!! 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