575 lines
17 KiB
Python
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
|