First commit
This commit is contained in:
574
boatdata.py
Normal file
574
boatdata.py
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user