Erstveröffentlichung Weihnachten 2024
|
@ -0,0 +1,2 @@
|
|||
*~
|
||||
__pycache__
|
|
@ -0,0 +1,13 @@
|
|||
Das Programm kann direkt gestartet werden. Eine Installation ist nicht
|
||||
erforderlich. Die unten angegebenen Abhängigkeiten müssen erfüllt sein.
|
||||
|
||||
apt-get install python3-cairo python3-gi python3-gi-cairo gir1.2-rsvg-2.0 \
|
||||
python-serial python3-nmea2 python3-smbus2 python3-bme280
|
||||
|
||||
Das Programm wird über eine Konfigurationsdatei obp60.conf im gleichen
|
||||
Verzeichnis wie das Hauptprogramm gesteuert. Die Konfiguration wird
|
||||
einmalig beim Programmstart eingelesen.
|
||||
|
||||
Meßdaten werden im Homeverzeichnis unter ~/.local/lib/obp60 gespeichert.
|
||||
Dies betrifft momentan Luftdruckmessungen mit dem BME280.
|
||||
Das Verzeichnis wird automatisch angelegt.
|
|
@ -0,0 +1,61 @@
|
|||
Multifunktionsdisplay (MFD) virtuell: OBP60v
|
||||
|
||||
Hinweis: Dieses Programm dient in erster Linie dazu die GUI der "echten"
|
||||
OBP60-Hardware zu designen. Eine eigenständige Nutzung ist selbstverständlich
|
||||
"auf eigene Gefahr" hin möglich.
|
||||
|
||||
Für Informationen zum OBP60 in Hardware siehe:
|
||||
- https://open-boat-projects.org/de/diy-multifunktionsdisplay-obp-60/
|
||||
- https://obp60-v2-docu.readthedocs.io/de/latest/
|
||||
|
||||
Fehlermeldungen und Patches gerne an thomas@hoogi.de senden.
|
||||
|
||||
Basishardware
|
||||
- Raspberry Pi 4
|
||||
|
||||
Zusatzhardware:
|
||||
- NMEA2000 Interface
|
||||
- PiCAN-M (hiermit wird entwickelt)
|
||||
- Waveshare RS485 CAN HAT (ungetestet)
|
||||
- BME280-Sensor
|
||||
- GPS über USB/seriell angeschlossen
|
||||
|
||||
Zusatzsoftware:
|
||||
- OpenCPN
|
||||
|
||||
Abhängigkeiten
|
||||
- python-can
|
||||
- heapdict
|
||||
|
||||
Für GPS
|
||||
- python-serial
|
||||
- python3-nmea2
|
||||
|
||||
Für BME280
|
||||
- smbus2
|
||||
- bme280
|
||||
|
||||
Zur Steuerung des Geräts sind 6 Tasten vorhanen. Numeriert von 1 bis 6 von
|
||||
links nach rechts. Die Tasten können angeklickt werden und führen dann direkt
|
||||
eine von der jeweiligen Seite abhängige Funktion aus.
|
||||
Die jeweilige Funktion wird durch ein Symbol oberhalb der Taste dargestellt.
|
||||
Die Tasten 3 und 4 sind für die Seitennavigation vorgesehen: zurück und vor.
|
||||
Sie können jedoch von einer Seite bei Bedarf übersteuert werden.
|
||||
|
||||
Wischgesten werden simuliert, indem die Maustaste auf einer Tastenfläche
|
||||
gedrückt und auf einer anderen Taste losgelassen wird.
|
||||
|
||||
Folgende Wischfunktionen sind implementiert:
|
||||
1. Programmende durch die Wischfunktion "2" -> "1"
|
||||
2. Tastensperre an: "6" -> "1"
|
||||
3. Tastensperre aus: "1" -> "6"
|
||||
4. Systemseite: "5" -> "6"
|
||||
|
||||
Routen und Wegepunkte können von OpenCPN empfangen werden. Dazu muß eine
|
||||
passende serielle Schnittstelle für den NMEA0183-Ausgang definiert werden.
|
||||
Im System kann diese in der Datei rc.local aktiviert werden:
|
||||
# Create virtual serial connection
|
||||
socat pty,rawer,echo=0,group-late=dialout,mode=0660,link=/dev/ttyV0 \
|
||||
pty,rawer,echo=0,group-late=dialout,mode=0660,link=/dev/ttyV1 &
|
||||
OpenCPN sendet dann Datensätze über ttyV0 und dieses Programm
|
||||
empfängt sie über ttyV1.
|
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 626 B |
After Width: | Height: | Size: 841 B |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 569 B |
After Width: | Height: | Size: 847 B |
After Width: | Height: | Size: 559 B |
After Width: | Height: | Size: 150 B |
After Width: | Height: | Size: 148 B |
After Width: | Height: | Size: 136 B |
After Width: | Height: | Size: 135 B |
After Width: | Height: | Size: 157 B |
After Width: | Height: | Size: 124 B |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 140 B |
After Width: | Height: | Size: 144 B |
After Width: | Height: | Size: 143 B |
After Width: | Height: | Size: 146 B |
After Width: | Height: | Size: 143 B |
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 543 B |
After Width: | Height: | Size: 518 B |
After Width: | Height: | Size: 510 B |
After Width: | Height: | Size: 592 B |
After Width: | Height: | Size: 236 B |
After Width: | Height: | Size: 584 B |
After Width: | Height: | Size: 516 B |
After Width: | Height: | Size: 507 B |
After Width: | Height: | Size: 569 B |
|
@ -0,0 +1,3 @@
|
|||
from .device import Device
|
||||
from .boatdata import BoatData
|
||||
from .hbuffer import History, HistoryBuffer
|
|
@ -0,0 +1,567 @@
|
|||
'''
|
||||
!!! 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
|
||||
|
||||
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():
|
||||
|
||||
def __init__(self, instance=0):
|
||||
self.instance = instance
|
||||
self.fluidtype = 1 # water -> lookup
|
||||
self.volume = None
|
||||
self.capacity = None
|
||||
self.desc = "" # long description
|
||||
|
||||
def __str__(self):
|
||||
out = f" Tank #{self.instance}"
|
||||
out += f" Capacity: {self.capacity} l\n"
|
||||
out += f" Fluid level: {self.volume} l\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():
|
||||
print(e)
|
||||
for t in self.tank.values():
|
||||
print(t)
|
||||
out += " Satellite info\n"
|
||||
for s in self.sat.values():
|
||||
out += str(s)
|
||||
return out
|
||||
|
||||
def updateValid(self, age=None):
|
||||
# age: Alter eines Meßwerts in Sekunden
|
||||
if not age:
|
||||
age = self.maxage
|
||||
t = time.time()
|
||||
for v in vars(self).values():
|
||||
if isinstance(v,BoatValue):
|
||||
if t - v.timestamp > age:
|
||||
v.valid = False
|
||||
|
||||
def getRef(self, shortname):
|
||||
'''
|
||||
Referenz auf ein BoatValue-Objekt
|
||||
'''
|
||||
try:
|
||||
bv = self.valref[shortname]
|
||||
except KeyError:
|
||||
bv = None
|
||||
return bv
|
||||
|
||||
def getValue(self, shortname):
|
||||
'''
|
||||
Wert aufgrund textuellem Kurznamen zurückliefern
|
||||
'''
|
||||
try:
|
||||
value = self.valref[shortname].value
|
||||
except KeyError:
|
||||
value = None
|
||||
return value
|
||||
|
||||
def setValue(self, shortname, newvalue):
|
||||
'''
|
||||
Rückgabewert True bei erfolgreichem Speichern des Werts
|
||||
'''
|
||||
if not shortname in self.valref:
|
||||
return False
|
||||
field = self.valref[shortname]
|
||||
field.value = newvalue
|
||||
field.timestamp = time.time()
|
||||
field.valid = True
|
||||
return True
|
||||
|
||||
def enableSimulation(self):
|
||||
self.simulation = True
|
||||
for v in self.valref.values():
|
||||
v.simulated = True
|
||||
|
||||
def addHistory(self, history, htype):
|
||||
"""
|
||||
htype: press, temp, hum
|
||||
"""
|
||||
self.history[htype] = history
|
|
@ -0,0 +1,63 @@
|
|||
|
||||
'''
|
||||
Platzhalter WIP
|
||||
- ausprogrammieren nach Bedarf
|
||||
Geräteliste
|
||||
- wird regelmäßig aktualisiert
|
||||
|
||||
'''
|
||||
|
||||
import time
|
||||
import struct
|
||||
|
||||
class Device():
|
||||
|
||||
def __init__(self, address):
|
||||
#WIP
|
||||
#Felder können sich noch ändern!
|
||||
self.address = address
|
||||
self.instance = 0 # default 0
|
||||
self.sysinstance = 0 # used with bridged networks, default 0
|
||||
self.lastseen = time.time()
|
||||
self.uniqueid = None
|
||||
self.manufacturercode = None
|
||||
self.industrygroup = None
|
||||
self.name = None # User defined device name
|
||||
self.product = None # Product name
|
||||
self.productcode = None # Product code
|
||||
self.devicefunction = None
|
||||
self.deviceclass = None
|
||||
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
|
||||
|
||||
def getName(self):
|
||||
# NAME errechnen aus den Claim-Daten
|
||||
# TODO Das hier ist noch fehlerhaft!
|
||||
data = bytearray()
|
||||
data.append((self.deviceclass << 4) | (self.devicefunction & 0x0f))
|
||||
data.extend(struct.pack('<L', self.uniqueid))
|
||||
data.extend(struct.pack('<L', self.manufacturercode))
|
||||
data.extend(struct.pack('<L', self.industrygroup))
|
||||
return data
|
||||
|
||||
def __str__(self):
|
||||
out = f"Device: {self.address} : '{self.product}'\n"
|
||||
out += f" Instance: {self.instance}\n"
|
||||
out += f" Product Code: {self.productcode}\n"
|
||||
out += f" Product: {self.product}\n"
|
||||
out += f" Serial: {self.serial}\n"
|
||||
out += f" Model Version: {self.modelvers}\n"
|
||||
out += f" Software Version: {self.softvers}\n"
|
||||
out += f" NMEA2000 Version: {self.n2kvers}\n"
|
||||
out += f" Cert-Level: {self.certlevel}\n"
|
||||
out += f" LEN: {self.loadequiv}"
|
||||
return out
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
'''
|
||||
Platzhalter WIP
|
||||
- ausprogrammieren nach Bedarf
|
||||
Geräteliste
|
||||
- wird regelmäßig aktualisiert
|
||||
|
||||
'''
|
||||
|
||||
class DeviceList():
|
||||
|
||||
def __init__(self):
|
||||
self.devices = list()
|
||||
|
||||
def print(self):
|
||||
for d in self.devicelist:
|
||||
print(d)
|
|
@ -0,0 +1,300 @@
|
|||
"""
|
||||
History Buffer
|
||||
|
||||
Permanent storage backed buffer for sensordata
|
||||
Only supported at the moment: file system storage
|
||||
|
||||
Values can be 1 to 4 bytes in length
|
||||
|
||||
Header: 32 bytes of size
|
||||
0 0x00 HB00 4 magic number
|
||||
4 0x04 xxxxxxxxxxxxxxxx 16 name, space padded
|
||||
20 0x14 n 1 byte size of values in buffer
|
||||
21 0x15 mm 2 buffer size in count of values
|
||||
23 0x17 dd 2 time step in seconds between values
|
||||
25 0x19 tttt 4 unix timestamp of head
|
||||
29 0x1d hh 2 head pointer
|
||||
31 0x1f 0xff 1 header end sign
|
||||
|
||||
32 0x20 ... start of buffer data
|
||||
|
||||
|
||||
Usage example: 7 hours of data collected every 75 seconds
|
||||
|
||||
def g_tick(n=1):
|
||||
t = time.time()
|
||||
count = 0
|
||||
while True:
|
||||
count += n
|
||||
yield max(t + count - time.time(), 0)
|
||||
|
||||
hb = HistoryBuffer("test", 336, 75)
|
||||
g = g_tick(hb.dt)
|
||||
hb.filename = "/tmp/test.dat"
|
||||
hb.begin()
|
||||
while True:
|
||||
time.sleep(next(g))
|
||||
hb.add(measured_new_value)
|
||||
hb.finish()
|
||||
|
||||
TODO
|
||||
- Logging
|
||||
- Additional backend: I2C FRAM module
|
||||
- Sync to next tick after loading from storage
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import struct
|
||||
|
||||
class HistoryBuffer():
|
||||
|
||||
def __init__(self, name, size, delta_t):
|
||||
"""
|
||||
Buffer can have an optional name of max. 16 characters
|
||||
"""
|
||||
self.magic = b'HB00'
|
||||
self.name = name[:16] or ''
|
||||
self.bytesize = 2
|
||||
self.size = size
|
||||
self.dt = delta_t
|
||||
self.headdate = int(time.time())
|
||||
self.head = 0
|
||||
self.buf = [0 for _ in range(size)]
|
||||
self.filename = f"/tmp/hb{name}_{size}-{delta_t}.dat"
|
||||
self.fp = None
|
||||
|
||||
def begin(self):
|
||||
# Check if data exists and format is correct
|
||||
if not os.path.exists(self.filename):
|
||||
self.createfile()
|
||||
else:
|
||||
if not self.checkfile():
|
||||
print(f"Incompatible data file: {self.filename}")
|
||||
return False
|
||||
|
||||
# Read old data to continue processing
|
||||
self.fp = open(self.filename, 'r+b')
|
||||
self.headdate = int(time.time())
|
||||
|
||||
self.fp.seek(25)
|
||||
timestamp = struct.unpack('I', self.fp.read(4))[0]
|
||||
self.head = struct.unpack('H', self.fp.read(2))[0]
|
||||
|
||||
self.fp.seek(32)
|
||||
data = self.fp.read(self.bytesize * self.size)
|
||||
|
||||
# Fix difference between current time and data time
|
||||
missing = (self.headdate - timestamp) // self.dt
|
||||
if missing > self.size:
|
||||
# too old start new
|
||||
self.clearfile
|
||||
self.head = 0
|
||||
else:
|
||||
# usable data found, fix missing
|
||||
self.fp.seek(32)
|
||||
data = self.fp.read(self.bytesize * self.size)
|
||||
i = 0
|
||||
for d in range(0, self.size, self.bytesize):
|
||||
if self.bytesize == 1:
|
||||
self.buf[i] = data[d]
|
||||
elif self.bytesize == 2:
|
||||
self.buf[i] = data[d] + data[d+1] * 256
|
||||
elif self.bytesize == 3:
|
||||
self.buf[i] = data[d] + (data[d+1] << 8) + (data[d+2] << 16)
|
||||
elif self.bytesize == 4:
|
||||
self.buf[i] = data[d] + (data[d+1] << 8) + (data[d+2] << 16) + (data[d+3] << 24)
|
||||
i += 1
|
||||
# add empty data for missing steps
|
||||
for s in range(missing):
|
||||
self.add(0)
|
||||
return True
|
||||
|
||||
def finish(self):
|
||||
if not self.fp.closed:
|
||||
self.fp.close()
|
||||
|
||||
def add(self, value):
|
||||
# check if add request perhaps too early
|
||||
timestamp = int(time.time())
|
||||
if timestamp - self.headdate < self.dt * 0.98: # a little bit of tolerance
|
||||
print("add new value too early, ignored")
|
||||
return False
|
||||
self.headdate = timestamp
|
||||
self.buf[self.head] = value
|
||||
self.updatefile(value)
|
||||
self.head += 1
|
||||
if self.head == self.size:
|
||||
self.head = 0
|
||||
return True
|
||||
|
||||
def get(self):
|
||||
"""
|
||||
Return buffer in linear sequence, newest values first
|
||||
"""
|
||||
for i in range(self.head -1, -1, -1):
|
||||
yield self.buf[i]
|
||||
for i in range(self.size - 1, self.head -1, -1):
|
||||
yield self.buf[i]
|
||||
|
||||
def getvalue(self, delta):
|
||||
"""
|
||||
Return a single value dt seconds ago
|
||||
delta has to be smaller than self.dt * self.size
|
||||
TODO check if value is missing, perhaps allow tolerance (+/- <n>)
|
||||
"""
|
||||
index = self.head - abs(delta) // self.dt
|
||||
if index < 0:
|
||||
index = self.size - index - 1
|
||||
return self.buf[index]
|
||||
|
||||
def getvalue3(self, delta):
|
||||
"""
|
||||
same as getvalue but calculate mean with two neighbor values
|
||||
TODO check for missing values (=0)
|
||||
"""
|
||||
index = self.head - abs(delta) // self.dt
|
||||
if index < 0:
|
||||
index = self.size - index - 1
|
||||
ixprev = index - 1
|
||||
if ixprev < 0:
|
||||
ixprev = self.size - 1
|
||||
ixnext = index + 1
|
||||
if ixnext > self.size - 1:
|
||||
ixnext = 0
|
||||
return round((self.buf[ix] + self.buf[ixprev] + self.buf[ixnext]) / 3)
|
||||
|
||||
def setname(self, newname):
|
||||
"""
|
||||
set new name in buffer and storage backend
|
||||
"""
|
||||
self.name = newname[:16] or ''
|
||||
self.fp.seek(4)
|
||||
fp.write(self.name.ljust(16, ' ').encode())
|
||||
|
||||
def createfile(self):
|
||||
""""
|
||||
Creates new file from current buffer
|
||||
"""
|
||||
with open(self.filename, 'wb') as fp:
|
||||
fp.write(self.magic)
|
||||
fp.write(self.name.ljust(16, ' ').encode())
|
||||
fp.write(struct.pack('B', self.bytesize))
|
||||
fp.write(struct.pack('H', self.size))
|
||||
fp.write(struct.pack('H', self.dt))
|
||||
fp.write(struct.pack('I', self.headdate))
|
||||
fp.write(struct.pack('H', self.head))
|
||||
fp.write(b"\xff") # header end
|
||||
if self.bytesize == 1:
|
||||
fp.write(bytes(self.buf))
|
||||
elif self.bytesize == 2:
|
||||
for val in self.buf:
|
||||
fp.write(struct.pack('H', val))
|
||||
elif self.bytesize == 3:
|
||||
for val in self.buf:
|
||||
fp.write((val >> 16) & 0xff)
|
||||
fp.write((val >> 8) & 0xff)
|
||||
fp.write(val & 0xff)
|
||||
elif self.bytesize == 4:
|
||||
for val in self.buf:
|
||||
fp.write(struct.pack('I', val))
|
||||
return True
|
||||
|
||||
def checkfile(self):
|
||||
"""
|
||||
Check if file header matches buffer metadata
|
||||
Name is not taken into account because it is optional
|
||||
"""
|
||||
with open(self.filename, 'rb') as fp:
|
||||
header = fp.read(32)
|
||||
magic = header[:4]
|
||||
if not (header[:4] == self.magic):
|
||||
print(f"Invalid magic: {magic}")
|
||||
return False
|
||||
bs = header[20]
|
||||
if not (bs == self.bytesize):
|
||||
print(f"Invalid bytesize: {bs}")
|
||||
return False
|
||||
vc = struct.unpack('H', header[21:23])[0]
|
||||
if not (vc == self.size):
|
||||
eprint(f"Invalid value count: {vc}")
|
||||
return False
|
||||
ts = struct.unpack('H', header[23:25])[0]
|
||||
if not (ts == self.dt):
|
||||
eprint(f"Invalid time step: {ts}")
|
||||
return False
|
||||
return True
|
||||
|
||||
def updatefile(self, value):
|
||||
"""
|
||||
Write value to file and update header accordingly
|
||||
"""
|
||||
pos = 32 + self.head * self.bytesize
|
||||
self.fp.seek(25)
|
||||
self.fp.write(struct.pack('IH', self.headdate, self.head + 1))
|
||||
self.fp.seek(pos)
|
||||
if self.bytesize == 1:
|
||||
self.fp.write(struct.pack('B', value))
|
||||
elif self.bytesize == 2:
|
||||
self.fp.write(struct.pack('H', value))
|
||||
elif self.bytesize == 3:
|
||||
self.fp.write((value >> 16) & 0xff)
|
||||
self.fp.write((value >> 8) & 0xff)
|
||||
self.fp.write(value & 0xff)
|
||||
elif self.bytesize == 4:
|
||||
self.fp.write(struct.pack('I', value))
|
||||
|
||||
def clearfile(self):
|
||||
"""
|
||||
Clear data part of history file
|
||||
"""
|
||||
self.fp.seek(25)
|
||||
self.fp.write(struct.pack('IH', int(time.time()), 0))
|
||||
fp.seek(32)
|
||||
for p in range(32, self.size * self.bytesize):
|
||||
fp.write(0)
|
||||
|
||||
class History():
|
||||
"""
|
||||
A history can consist of different time series with different
|
||||
temporal resolutions
|
||||
|
||||
TODO implement type (e.g. pressure humidity temp etc.) to identify data
|
||||
"""
|
||||
def __init__(self, basename, delta_min):
|
||||
self.delta_t = delta_min # smallest unit of time in the series
|
||||
self.series = dict()
|
||||
self.basepath = "/tmp"
|
||||
self.basename = basename
|
||||
|
||||
def __str__(self):
|
||||
out = f"History {self.basename} (min. {self.delta_t}s) in {self.basepath}\n"
|
||||
n = 0
|
||||
for ser in self.series.values():
|
||||
out += f" Series: {ser.name} {ser.dt}s {ser.filename}\n"
|
||||
n += 1
|
||||
if n == 0:
|
||||
out += " No series found\n"
|
||||
return out
|
||||
|
||||
def addSeries(self, name, size, delta_t):
|
||||
"""
|
||||
Check whether a series already exists and throw an error if so.
|
||||
The new delta t must also be divisible by delta_min
|
||||
"""
|
||||
if delta_t in self.series:
|
||||
raise KeyError(f"Error: delta t {delta_t} already exists")
|
||||
if delta_t < self.delta_t:
|
||||
raise ValueError(f"Error: delta t {delta_t} too small, minimum is {self.delta_t}")
|
||||
if delta_t % self.delta_t != 0:
|
||||
raise ValueError(f"Error: delta t have to be a multiple of {self.delta_t}")
|
||||
hb = HistoryBuffer(name, size, delta_t)
|
||||
histfilename = f"hb{self.basename}_{size}-{delta_t}.dat"
|
||||
hb.filename = os.path.join(self.basepath, histfilename)
|
||||
self.series[delta_t] = hb
|
||||
return hb
|
||||
|
||||
def clear(self):
|
||||
# Clear all time series buffer
|
||||
self.series.clear()
|
|
@ -0,0 +1,563 @@
|
|||
# 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",
|
||||
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"
|
||||
},
|
||||
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",
|
||||
135: "NMEA 0183 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",
|
||||
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: { # NEW? WIP
|
||||
},
|
||||
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",
|
||||
2001: "Open Boat Projects"
|
||||
}
|
||||
|
||||
pilotmode = {
|
||||
64: "Standby",
|
||||
66: "Auto",
|
||||
70: "Wind",
|
||||
74: "Track"
|
||||
}
|
||||
|
||||
pressure = {
|
||||
0: "Athmospheric",
|
||||
1: "Water",
|
||||
2: "Steam",
|
||||
3: "Compressed Air",
|
||||
4: "Hydraulic",
|
||||
5: "Filter",
|
||||
6: "AltimeterSetting",
|
||||
7: "Oil",
|
||||
8: "Fuel"
|
||||
}
|
||||
|
||||
prnusage = {
|
||||
0: "Not Tracked",
|
||||
1: "Tracked",
|
||||
2: "Used",
|
||||
3: "Not tracked+Diff",
|
||||
4: "Tracked+Diff",
|
||||
5: "Used+Diff",
|
||||
14: "Error",
|
||||
15: "No Selection"
|
||||
}
|
||||
|
||||
speedwater = {
|
||||
0: "Paddle wheel",
|
||||
1: "Pitot tube",
|
||||
2: "Doppler",
|
||||
3: "Correlation (ultra sound)",
|
||||
4: "Electro Magnetic"
|
||||
}
|
||||
|
||||
timesource = {
|
||||
0: "GPS",
|
||||
1: "GLONASS",
|
||||
2: "Radio Station",
|
||||
3: "Local Cesium clock",
|
||||
4: "Local Rubidium clock",
|
||||
5: "Local Crystal clock"
|
||||
}
|
||||
|
||||
temperature = {
|
||||
0: "Sea Temperature",
|
||||
1: "Outside Temperature",
|
||||
2: "Inside Temperature",
|
||||
3: "Engine Room Temperature",
|
||||
4: "Main Cabin Temperature",
|
||||
5: "Live Well Temperature",
|
||||
6: "Bait Well Temperature",
|
||||
7: "Refrigeration Temperature",
|
||||
8: "Heating System Temperature",
|
||||
9: "Dew Point Temperature",
|
||||
10: "Apparent Wind Chill Temperature",
|
||||
11: "Theoretical Wind Chill Temperature",
|
||||
12: "Heat Index Temperature",
|
||||
13: "Freezer Temperature",
|
||||
14: "Exhaust Gas Temperature",
|
||||
15: "Shaft Seal Temperature"
|
||||
}
|
||||
|
||||
xtemode = {
|
||||
0: "auto",
|
||||
1: "differential",
|
||||
2: "estimated",
|
||||
3: "simulation",
|
||||
4: "manual"
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
'''
|
||||
Moving Average
|
||||
|
||||
Sortierung einer Liste nach Alter? FIFO?
|
||||
|
||||
'''
|
||||
|
||||
import time
|
||||
|
||||
class mAvg():
|
||||
|
||||
def __init__(self, interval):
|
||||
self.interval = interval
|
||||
self.avg = None
|
||||
self.data = []
|
||||
|
||||
def addVal(self, value):
|
||||
self.data.append((time.time(), value))
|
||||
|
||||
def setInterval(self, interval):
|
||||
pass
|
||||
|
||||
def getAvg(self):
|
||||
return self.avg
|
||||
|
|
@ -0,0 +1,224 @@
|
|||
'''
|
||||
|
||||
PGNs verarbeiten
|
||||
|
||||
'''
|
||||
|
||||
import struct
|
||||
|
||||
def parse_126996(buf, source, device):
|
||||
# Product information
|
||||
n2kvers = (buf[0] + buf[1] * 256) / 1000
|
||||
prodcode = buf[2] + buf[3] * 256
|
||||
modelid = buf[4:36] # 256bit modelid ascii text
|
||||
softvers = buf[36:68] # 256bit software version code ascii text
|
||||
modelvers = buf[68:100] # 256bit model version ascii text
|
||||
serial = buf[100:132] # 256bit model serial code ascii text
|
||||
# Füllzeichen entfernen. 0x20, 0xff, '@' für AIS
|
||||
# Die N2K-Bibliothek von ttlappalainen liefert 0xff
|
||||
modelid = modelid.rstrip(b'\xff')
|
||||
softvers = softvers.rstrip(b'\xff')
|
||||
modelvers = modelvers.rstrip(b'\xff')
|
||||
serial = serial.rstrip(b'\xff')
|
||||
# Übertragen in die Geräteliste
|
||||
devices[source].n2kvers = n2kvers
|
||||
devices[source].productcode = prodcode
|
||||
devices[source].modelvers = modelvers.decode('ascii').rstrip()
|
||||
devices[source].softvers = softvers.decode('ascii').rstrip()
|
||||
devices[source].product = modelid.decode('ascii').rstrip()
|
||||
devices[source].serial = serial.decode('ascii').rstrip()
|
||||
devices[source].certlevel = buf[132]
|
||||
devices[source].loadequiv = buf[133]
|
||||
|
||||
def parse_126998(buf, source, device):
|
||||
# Configuration information
|
||||
# Installation Description 1
|
||||
txtlen = buf[0]
|
||||
if txtlen > 2:
|
||||
device.instdesc1 = buf[2:txtlen].decode('ascii')
|
||||
p = txtlen
|
||||
else:
|
||||
device.instdesc1 = ""
|
||||
p = 2
|
||||
# Installation Description 2
|
||||
txtlen = buf[p]
|
||||
if txtlen > 2:
|
||||
device.instdesc2 = buf[p+2:p+txtlen].decode('ascii')
|
||||
p += txtlen
|
||||
else:
|
||||
device.instdesc2 = ""
|
||||
p += 2
|
||||
# Manufacturer Info
|
||||
txtlen = buf[p]
|
||||
if txtlen > 2:
|
||||
device.manufinfo = buf[p+2:p+txtlen].decode('ascii')
|
||||
else:
|
||||
device.manufinfo = ""
|
||||
|
||||
def parse_127257(buf, boatdata):
|
||||
# Attitude
|
||||
sid = buf[0]
|
||||
yaw = struct.unpack_from('<h', buf, 1)[0] * 0.0001
|
||||
pitch = struct.unpack_from('<h', buf, 3)[0] * 0.0001
|
||||
roll = struct.unpack_from('<h', buf, 5)[0] * 0.0001
|
||||
boatdata.setValue("YAW", yaw)
|
||||
boatdata.setValue("PTCH", pitch)
|
||||
boatdata.setValue("ROLL", roll)
|
||||
|
||||
def parse_127505(buf, boatdata):
|
||||
# Fluid Level
|
||||
instance = buf[0] & 0x0f
|
||||
boatdata.tank[instance].fluidtype = buf[0] >> 4
|
||||
boatdata.tank[instance].capacity = struct.unpack_from('<L', buf, 2)[0] * 0.1
|
||||
boatdata.tank[instance].volume = struct.unpack_from('<H', buf, 1)[0] * 0.004
|
||||
|
||||
def parse_127508(buf, boatdata):
|
||||
# Battery status
|
||||
instance = buf[0]
|
||||
voltage = (buf[2] * 256 + buf[1]) * 0.01
|
||||
current = (buf[4] * 256 + buf[3]) * 0.1
|
||||
temp = (buf[6] * 256 + buf[5]) * 0.01 - 273.15
|
||||
sid = buf[7]
|
||||
|
||||
def parse_129025(buf, boatdata):
|
||||
# Position, Rapid Update
|
||||
lat = struct.unpack_from('<l', buf, 0)[0] * 1e-07
|
||||
lon = struct.unpack_from('<l', buf, 4)[0] * 1e-07
|
||||
boatdata.setValue("LAT", lat)
|
||||
boatdata.setValue("LON", lon)
|
||||
|
||||
def parse_129026(buf, boatdata):
|
||||
# COG & SOG, Rapid Update
|
||||
sid = buf[0]
|
||||
cogref = buf[1] >> 6 # 0: true, 1: magnetic, 2: error
|
||||
cog = struct.unpack_from('<H', buf, 2)[0] * 0.0001 # rad
|
||||
sog = struct.unpack_from('<H', buf, 4)[0] * 0.01 # m/s
|
||||
# 2 Byte reserved
|
||||
boatdata.setValue("COG", cog)
|
||||
boatdata.setValue("SOG", sog)
|
||||
|
||||
def parse_129027(buf, boatdata):
|
||||
# TODO
|
||||
# Position Delta Rapid Update
|
||||
sid = buf[0]
|
||||
dt = struct.unpack_from('<H', buf, 1)[0],
|
||||
dlat = struct.unpack_from('<h', buf, 3)[0]
|
||||
dlon = struct.unpack_from('<h', buf, 5)[0]
|
||||
# 1 Byte reserved
|
||||
|
||||
def parse_129029(buf, boatdata):
|
||||
# GNSS Position Data
|
||||
'''
|
||||
1 sid
|
||||
2 date
|
||||
4 time seconds since midn
|
||||
8 lat
|
||||
8 lon
|
||||
8 alt
|
||||
4 bit gnss type
|
||||
{"name": "GPS", "value": 0},
|
||||
{"name": "GLONASS", "value": 1},
|
||||
{"name": "GPS+GLONASS", "value": 2},
|
||||
{"name": "GPS+SBAS/WAAS", "value": 3},
|
||||
{"name": "GPS+SBAS/WAAS+GLONASS", "value": 4},
|
||||
{"name": "Chayka", "value": 5},
|
||||
{"name": "integrated", "value": 6},
|
||||
{"name": "surveyed", "value": 7},
|
||||
{"name": "Galileo", "value": 8}]},
|
||||
4bit method
|
||||
{"name": "no GNSS", "value": 0},
|
||||
{"name": "GNSS fix", "value": 1},
|
||||
{"name": "DGNSS fix", "value": 2},
|
||||
{"name": "Precise GNSS", "value": 3},
|
||||
{"name": "RTK Fixed Integer", "value": 4},
|
||||
{"name": "RTK float", "value": 5},
|
||||
{"name": "Estimated (DR) mode", "value": 6},
|
||||
{"name": "Manual Input", "value": 7},
|
||||
{"name": "Simulate mode", "value": 8}]},
|
||||
|
||||
2bit integrity
|
||||
{"name": "No integrity checking", "value": 0},
|
||||
{"name": "Safe", "value": 1},
|
||||
{"name": "Caution", "value": 2}]},
|
||||
bit reserved
|
||||
|
||||
1 byte uint numberOfSvs Number of satellites used in solution
|
||||
2byte hdop
|
||||
2 byte tdop
|
||||
4 byte geoidalSeparation
|
||||
1 byte Number of reference stations
|
||||
|
||||
4bit referenceStationType
|
||||
12bit referenceStationId
|
||||
2byte sageOfDgnssCorrections
|
||||
|
||||
|
||||
'''
|
||||
|
||||
def parse_129033(buf, boatdata):
|
||||
# Time & Date
|
||||
gpsdate = struct.unpack_from('<H', buf, 0)[0] # days
|
||||
gpstime = struct.unpack_from('<L', buf, 2)[0] # seconds since midnight
|
||||
offset = struct.unpack_from('<h', buf, 6)[0] # local offset
|
||||
# TODO
|
||||
|
||||
def parse_129283(buf, boatdata):
|
||||
# TODO Cross Track Error XTE
|
||||
sid = buf[0]
|
||||
mode = buf[1] >> 4
|
||||
navterm = buf[1] & 0x03
|
||||
xte = struct.unpack_from('<l', buf, 2)[0] * 0.01 # m
|
||||
# 2 Byte reserved
|
||||
boatdata.setValue("XTE", xte)
|
||||
|
||||
def parse_129540(buf, boatdata):
|
||||
#sid = buf[0]
|
||||
#rrmode = buf[1]
|
||||
nsats = buf[2]
|
||||
# Datensatz je Sat Länge: 12 Byte
|
||||
smax = nsats * 12
|
||||
for s in range(0, smax, 12):
|
||||
prn = buf[3 + s]
|
||||
elevation = struct.unpack_from('<h', buf, s+4)[0] * 0.0001
|
||||
azimuth = struct.unpack_from('<H', buf, s+6)[0] * 0.0001
|
||||
snr = struct.unpack_from('<H', buf, s+8)[0] * 0.01
|
||||
rres = struct.unpack_from('<l', buf, s+10)[0]
|
||||
status = buf[s+14] & 0x0f
|
||||
boatdata.updateSatellite(prn, elevation, azimuth, snr, rres, status)
|
||||
|
||||
def parse_130312(buf, boatdata):
|
||||
# Temperature
|
||||
src = buf[2] # lookup "temperature" (0 .. 15)
|
||||
val = (buf[4] * 256 + buf[3]) * 0.01 # Kelvin
|
||||
if instance == 0 and src == 1:
|
||||
boatdata.setValue("xdrTemp", val)
|
||||
elif instance in boatdata.temp:
|
||||
boatdata.temp[instance].value = val
|
||||
|
||||
def parse_130314(buf, boatdata):
|
||||
# Pressure
|
||||
sid = buf[0]
|
||||
instance = buf[1]
|
||||
src = buf[2] # lookup "pressure"
|
||||
pressure = struct.unpack_from('<L', buf, s+3)[0] * 0.1 # Pa
|
||||
if instance == 0 and src == 0:
|
||||
# Generischer Luftdruckwert
|
||||
boatdata.setValue("xdrPress", pressure)
|
||||
if instance in boatdata.press:
|
||||
# Verschiedene weitere Drücke
|
||||
# TODO sensortype "src"
|
||||
boatdata.press[instance].value = pressure
|
||||
|
||||
def parse_130316(buf, boatdata):
|
||||
# Temperature, extended range
|
||||
sid = buf[0]
|
||||
instance = buf[1]
|
||||
src = buf[2] # lookup "temperature" (0 .. 15)
|
||||
val = ((buf[5] << 16) | (buf[4] << 8) | buf[3]) * 0.001
|
||||
# TODO save in global temp data
|
||||
# Konflikt mit 130312?
|
||||
#if instance == 0 and src == 2:
|
||||
# boatdata.setValue("xdrTemp", val)
|
||||
# save in engine data
|
||||
if src == 14 and instance in boatdata.engine:
|
||||
boatdata.engine[instance].exhaust_temp = val
|
|
@ -0,0 +1,275 @@
|
|||
pgntype = {
|
||||
59392: "S",
|
||||
59904: "S",
|
||||
60160: "S",
|
||||
60416: "S",
|
||||
60928: "S",
|
||||
61184: "S",
|
||||
65001: "S",
|
||||
65002: "S",
|
||||
65003: "S",
|
||||
65004: "S",
|
||||
65005: "S",
|
||||
65006: "S",
|
||||
65007: "S",
|
||||
65008: "S",
|
||||
65009: "S",
|
||||
65010: "S",
|
||||
65011: "S",
|
||||
65012: "S",
|
||||
65013: "S",
|
||||
65014: "S",
|
||||
65015: "S",
|
||||
65016: "S",
|
||||
65017: "S",
|
||||
65018: "S",
|
||||
65019: "S",
|
||||
65020: "S",
|
||||
65021: "S",
|
||||
65022: "S",
|
||||
65023: "S",
|
||||
65024: "S",
|
||||
65025: "S",
|
||||
65026: "S",
|
||||
65027: "S",
|
||||
65028: "S",
|
||||
65029: "S",
|
||||
65030: "S",
|
||||
65240: "I",
|
||||
65280: "S",
|
||||
65284: "S",
|
||||
65285: "S",
|
||||
65286: "S",
|
||||
65287: "S",
|
||||
65288: "S",
|
||||
65289: "S",
|
||||
65290: "S",
|
||||
65292: "S",
|
||||
65293: "S",
|
||||
65302: "S",
|
||||
65305: "S",
|
||||
65309: "S",
|
||||
65312: "S",
|
||||
65340: "S",
|
||||
65341: "S",
|
||||
65345: "S",
|
||||
65350: "S",
|
||||
65359: "S",
|
||||
65360: "S",
|
||||
65361: "S",
|
||||
65371: "S",
|
||||
65374: "S",
|
||||
65379: "S",
|
||||
65408: "S",
|
||||
65409: "S",
|
||||
65410: "S",
|
||||
65420: "S",
|
||||
65480: "S",
|
||||
126208: "F",
|
||||
126464: "F",
|
||||
126720: "F",
|
||||
126983: "F",
|
||||
126984: "F",
|
||||
126985: "F",
|
||||
126986: "F",
|
||||
126987: "F",
|
||||
126988: "F",
|
||||
126992: "S",
|
||||
126993: "S",
|
||||
126996: "F",
|
||||
126998: "F",
|
||||
127233: "F",
|
||||
127237: "F",
|
||||
127245: "S",
|
||||
127250: "S",
|
||||
127251: "S",
|
||||
127252: "S",
|
||||
127257: "S",
|
||||
127258: "S",
|
||||
127488: "S",
|
||||
127489: "F",
|
||||
127490: "F",
|
||||
127491: "F",
|
||||
127493: "S",
|
||||
127494: "F",
|
||||
127495: "F",
|
||||
127496: "F",
|
||||
127497: "F",
|
||||
127498: "F",
|
||||
127500: "S",
|
||||
127501: "S",
|
||||
127502: "S",
|
||||
127503: "F",
|
||||
127504: "F",
|
||||
127505: "S",
|
||||
127506: "F",
|
||||
127507: "F",
|
||||
127508: "S",
|
||||
127509: "F",
|
||||
127510: "F",
|
||||
127511: "S",
|
||||
127512: "S",
|
||||
127513: "F",
|
||||
127514: "S",
|
||||
127744: "S",
|
||||
127745: "S",
|
||||
127746: "S",
|
||||
127750: "S",
|
||||
127751: "S",
|
||||
128000: "S",
|
||||
128001: "S",
|
||||
128002: "S",
|
||||
128003: "S",
|
||||
128006: "S",
|
||||
128007: "S",
|
||||
128008: "S",
|
||||
128259: "S",
|
||||
128267: "S",
|
||||
128275: "F",
|
||||
128520: "F",
|
||||
128538: "F",
|
||||
128768: "S",
|
||||
128769: "S",
|
||||
128776: "S",
|
||||
128777: "S",
|
||||
128778: "S",
|
||||
128780: "S",
|
||||
129025: "S",
|
||||
129026: "S",
|
||||
129027: "S",
|
||||
129028: "S",
|
||||
129029: "F",
|
||||
129033: "S",
|
||||
129038: "F",
|
||||
129039: "F",
|
||||
129040: "F",
|
||||
129041: "F",
|
||||
129044: "F",
|
||||
129045: "F",
|
||||
129283: "S",
|
||||
129284: "F",
|
||||
129285: "F",
|
||||
129291: "S",
|
||||
129301: "F",
|
||||
129302: "F",
|
||||
129538: "F",
|
||||
129539: "S",
|
||||
129540: "F",
|
||||
129541: "F",
|
||||
129542: "F",
|
||||
129545: "F",
|
||||
129546: "S",
|
||||
129547: "F",
|
||||
129549: "F",
|
||||
129550: "F",
|
||||
129551: "F",
|
||||
129556: "F",
|
||||
129792: "F",
|
||||
129793: "F",
|
||||
129794: "F",
|
||||
129795: "F",
|
||||
129796: "F",
|
||||
129797: "F",
|
||||
129798: "F",
|
||||
129799: "F",
|
||||
129800: "F",
|
||||
129801: "F",
|
||||
129802: "F",
|
||||
129803: "F",
|
||||
129804: "F",
|
||||
129805: "F",
|
||||
129806: "F",
|
||||
129807: "F",
|
||||
129808: "F",
|
||||
129809: "F",
|
||||
129810: "F",
|
||||
130052: "F",
|
||||
130053: "F",
|
||||
130054: "F",
|
||||
130060: "F",
|
||||
130061: "F",
|
||||
130064: "F",
|
||||
130065: "F",
|
||||
130066: "F",
|
||||
130067: "F",
|
||||
130068: "F",
|
||||
130069: "F",
|
||||
130070: "F",
|
||||
130071: "F",
|
||||
130072: "F",
|
||||
130073: "F",
|
||||
130074: "F",
|
||||
130306: "S",
|
||||
130310: "S",
|
||||
130311: "S",
|
||||
130312: "S",
|
||||
130313: "S",
|
||||
130314: "S",
|
||||
130315: "S",
|
||||
130316: "S",
|
||||
130320: "F",
|
||||
130321: "F",
|
||||
130322: "F",
|
||||
130323: "F",
|
||||
130324: "F",
|
||||
130330: "F",
|
||||
130560: "S",
|
||||
130561: "F",
|
||||
130562: "F",
|
||||
130563: "F",
|
||||
130564: "F",
|
||||
130565: "F",
|
||||
130566: "F",
|
||||
130567: "F",
|
||||
130569: "F",
|
||||
130570: "F",
|
||||
130571: "F",
|
||||
130572: "F",
|
||||
130573: "F",
|
||||
130574: "F",
|
||||
130576: "S",
|
||||
130577: "F",
|
||||
130578: "F",
|
||||
130579: "S",
|
||||
130580: "F",
|
||||
130581: "F",
|
||||
130582: "S",
|
||||
130583: "F",
|
||||
130584: "F",
|
||||
130585: "S",
|
||||
130586: "F",
|
||||
130816: "F",
|
||||
130817: "F",
|
||||
130818: "F",
|
||||
130819: "F",
|
||||
130820: "F",
|
||||
130821: "F",
|
||||
130822: "F",
|
||||
130823: "F",
|
||||
130824: "F",
|
||||
130825: "F",
|
||||
130827: "F",
|
||||
130828: "F",
|
||||
130831: "F",
|
||||
130832: "F",
|
||||
130833: "F",
|
||||
130834: "F",
|
||||
130835: "F",
|
||||
130836: "F",
|
||||
130837: "F",
|
||||
130838: "F",
|
||||
130839: "F",
|
||||
130840: "F",
|
||||
130842: "F",
|
||||
130843: "F",
|
||||
130845: "F",
|
||||
130846: "F",
|
||||
130847: "F",
|
||||
130850: "F",
|
||||
130851: "F",
|
||||
130856: "F",
|
||||
130860: "F",
|
||||
130880: "F",
|
||||
130881: "F",
|
||||
130944: "F"
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
from heapdict import heapdict
|
||||
import time
|
||||
import threading
|
||||
|
||||
class FrameQueue():
|
||||
|
||||
def __init__(self):
|
||||
self.heap = heapdict()
|
||||
self.age_interval = 5 # seconds
|
||||
|
||||
def push(self, frame, priority):
|
||||
timestamp = time.time() # microsecond resolution
|
||||
self.heap[timestamp] = (priority, timestamp, frame)
|
||||
|
||||
def pop(self):
|
||||
p, f = self.heap.popitem()
|
||||
self.age()
|
||||
return f[2]
|
||||
|
||||
def age(self):
|
||||
current_time = time.time()
|
||||
for key in list(self.heap.keys()):
|
||||
if current_time - key > self.age_interval:
|
||||
self.heap[key][0] -= 1
|
||||
|
||||
def is_empty(self):
|
||||
return len(self.heap) == 0
|
|
@ -0,0 +1,61 @@
|
|||
'''
|
||||
|
||||
Momentan nur Idee
|
||||
|
||||
Verarbeiten von Frames
|
||||
|
||||
Nur Empfang von Paketen / Frames
|
||||
- single
|
||||
- fast
|
||||
- transport
|
||||
|
||||
fast packets können parallel von mehreren Quellen verarbeitet werden
|
||||
|
||||
'''
|
||||
|
||||
import math
|
||||
|
||||
class FpReceiver():
|
||||
|
||||
def __init__(self):
|
||||
self.sc0 = {}
|
||||
self.nf = {}
|
||||
self.frame = {}
|
||||
self.wip = []
|
||||
|
||||
def receive(self, data, source)
|
||||
'''
|
||||
Liefert True wenn Paket komplett empfangen wurde
|
||||
'''
|
||||
sc = (data[0] & 0xf0) >> 5
|
||||
fc = data[0] & 0x1f
|
||||
if not source in self.wip:
|
||||
# Erster Frame
|
||||
if fc != 0:
|
||||
# unbekannte Herkunft und kein Startframe
|
||||
continue
|
||||
self.sc0[source] = sc
|
||||
datalen = data[1]
|
||||
self.nf[source] = math.ceil((datalen - 6) / 7) + 1 # Anzahl Frames
|
||||
self.frame[source] = {fc : data[2:]} # Ersten Frame merken
|
||||
else:
|
||||
# Folgeframes
|
||||
if (sc == self.sc0[source]):
|
||||
# TODO prüfe ob der Framecounter fc schon vorgekommen ist
|
||||
if not fc in self.frame[source]:
|
||||
self.frame[source][fc] = data[1:8]
|
||||
else:
|
||||
# TODO Fehler im Fast-Packet: doppelter fc!
|
||||
raise('Frame error: duplicate fc')
|
||||
if len(self.frame[source]) == self.nf[source]:
|
||||
self.wip.remove(source)
|
||||
return True
|
||||
return False
|
||||
|
||||
def getPacket(self, source)
|
||||
# TODO Frames in der richtigen reihenfolge zusammensetzen
|
||||
packet = bytearray()
|
||||
for frame in sorted(self.frame.items()):
|
||||
print(frame)
|
||||
#packet.extend()
|
||||
return packet
|
|
@ -0,0 +1,17 @@
|
|||
'''
|
||||
Routen und Wegepunkte
|
||||
'''
|
||||
class Waypoint():
|
||||
def __init__(self, number, name):
|
||||
self.number = number
|
||||
self.name = name
|
||||
self.lat = None
|
||||
self.lon = None
|
||||
|
||||
class Route():
|
||||
def __init__(self, number, name)
|
||||
self.number = number
|
||||
self.name = name
|
||||
self.wps = dict()
|
||||
def getActiveWP(self):
|
||||
return None
|
|
@ -0,0 +1,179 @@
|
|||
'''
|
||||
Lange Beschreibung der Daten, mehrsprachig
|
||||
|
||||
'''
|
||||
desc = {
|
||||
"ALT": {
|
||||
"en": "Altitude",
|
||||
"de": "Höhe über Grund"
|
||||
},
|
||||
"AWA": {
|
||||
"en": "Apparant Wind Angle",
|
||||
"de": "Scheinbare Windrichtung"
|
||||
},
|
||||
"AWS": {
|
||||
"en": "Apparant Wind Speed",
|
||||
"de": "Scheinbare Windgeschwindigkeit"
|
||||
},
|
||||
"BTW": {
|
||||
"en": "Bearing To Waypoint",
|
||||
"de": "Kurs zum nächsten Wegepunkt"
|
||||
},
|
||||
"COG": {
|
||||
"en": "Course Over Ground",
|
||||
"de": "Kurs über Grund"
|
||||
},
|
||||
"DBS": {
|
||||
"en": "Depth Below Surface",
|
||||
"de": "Tiefe unter Wasseroberfläche"
|
||||
},
|
||||
"DBT": {
|
||||
"en": "Depth Below Transducer",
|
||||
"de": "Tiefe unter Sensor"
|
||||
},
|
||||
"DEV": {
|
||||
"en": "Deviation",
|
||||
"de": "Kursabweichung"
|
||||
},
|
||||
"DTW": {
|
||||
"en": "Distance To Waypoint",
|
||||
"de": "Entfernung zum nächsten Wegepunkt"
|
||||
},
|
||||
"GPSD": {
|
||||
"en": "GPS Date",
|
||||
"de": "GPS-Datum"
|
||||
},
|
||||
"GPST": {
|
||||
"en": "GPS Time",
|
||||
"de": "GPS-Zeit"
|
||||
},
|
||||
"HDM": {
|
||||
"en": "Magnetic Heading",
|
||||
"de": "Magnetischer Kurs"
|
||||
},
|
||||
"HDT": {
|
||||
"en": "Heading",
|
||||
"de": "Wahrer rechtweisender Kurs"
|
||||
},
|
||||
"HDOP": {
|
||||
"en": "Horizontal Dilation Of Position",
|
||||
"de": "Positionsgenauigkeit in der Horizontalen"
|
||||
},
|
||||
"LAT": {
|
||||
"en": "Latitude",
|
||||
"de": "Geographische Breite"
|
||||
},
|
||||
"LON": {
|
||||
"en": "Longitude",
|
||||
"de": "Geographische Länge"
|
||||
},
|
||||
"Log": {
|
||||
"en": "Logged distance",
|
||||
"de": "Entfernung"
|
||||
},
|
||||
"MaxAws": {
|
||||
"en": "Maximum Apperant Wind Speed",
|
||||
"de": "Maximum der relativen Windgeschwindigkeit"
|
||||
},
|
||||
"MaxTws": {
|
||||
"en": "Maximum True Wind Speed",
|
||||
"de": "Maximum der wahren Windgeschwindigkeit"
|
||||
},
|
||||
"PDOP": {
|
||||
"en": "Position dilation",
|
||||
"de": "Positionsgenauigkeit im Raum"
|
||||
},
|
||||
"PRPOS": {
|
||||
"en": "Secondary Rudder Position",
|
||||
"de": "Auslenkung Sekundärruder"
|
||||
},
|
||||
"ROT": {
|
||||
"en": "Rotation",
|
||||
"de": "Drehrate"
|
||||
},
|
||||
"RPOS": {
|
||||
"en": "Rudder Position",
|
||||
"de": "Auslenkung Ruder"
|
||||
},
|
||||
"SOG": {
|
||||
"en": "Speed Over Ground",
|
||||
"de": "Geschwindigkeit über Grund"
|
||||
},
|
||||
"STW": {
|
||||
"en": "Speed Through Water",
|
||||
"de": "Geschwindigkeit durch das Wasser"
|
||||
},
|
||||
"SatInfo": {
|
||||
"en": "Satellit Info",
|
||||
"de": "Anzahl der sichtbaren Satelliten"
|
||||
},
|
||||
"TWD": {
|
||||
"en": "True Wind Direction",
|
||||
"de": "Wahre Windrichtung"
|
||||
},
|
||||
"TWS": {
|
||||
"en": "True Wind Speed",
|
||||
"de": "Wahre Windgeschwindigkeit"
|
||||
},
|
||||
"TZ": {
|
||||
"en": "Timezone",
|
||||
"de": "Zeitzone"
|
||||
},
|
||||
"TripLog": {
|
||||
"en": "Trip Log",
|
||||
"de": "Tages-Entfernungszähler"
|
||||
},
|
||||
"VAR": {
|
||||
"en": "Course Variation",
|
||||
"de": "Abweichung vom Sollkurs"
|
||||
},
|
||||
"VDOP": {
|
||||
"en": "Vertical Dilation Of Position",
|
||||
"de": "Positionsgenauigkeit in der Vertikalen"
|
||||
},
|
||||
"WPLat": {
|
||||
"en": "Waypoint Latitude",
|
||||
"de": "Geo. Breite des Wegpunktes"
|
||||
},
|
||||
"WPLon": {
|
||||
"en": "Waypoint Longitude",
|
||||
"de": "Geo. Länge des Wegpunktes"
|
||||
},
|
||||
"WTemp": {
|
||||
"en": "Water Temperature",
|
||||
"de": "Wassertemperatur"
|
||||
},
|
||||
"XTE": {
|
||||
"en": "Cross Track Error",
|
||||
"de": "Kursfehler"
|
||||
},
|
||||
|
||||
# Sonderwerte
|
||||
"xdrHum": {
|
||||
"en": "Humidity",
|
||||
"de": "Luftfeuchte"
|
||||
},
|
||||
"xdrPress": {
|
||||
"en": "Pressure",
|
||||
"de": "Luftdruck"
|
||||
},
|
||||
"xdrRotK": {
|
||||
"en": "Keel Rotation",
|
||||
"de": "Kielrotation"
|
||||
},
|
||||
"tdrTemp": {
|
||||
"en": "Temperature",
|
||||
"de": "Temperatur"
|
||||
},
|
||||
"xdrVBat": {
|
||||
"en": "Battery Voltage",
|
||||
"de": "Batteriespannung"
|
||||
},
|
||||
|
||||
# WIP
|
||||
"xdrCurr": {
|
||||
"en": "Current",
|
||||
"de": "Stromstärke"
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
[system]
|
||||
systemname = OBP60v
|
||||
loglevel = 3
|
||||
deviceid = 100
|
||||
simulation = on
|
||||
histpath = ~/.local/lib/obp60
|
||||
|
||||
[bme280]
|
||||
enabled = true
|
||||
port = 1
|
||||
address = 0x76
|
||||
|
||||
[gps]
|
||||
enabled = false
|
||||
port = /dev/ttyACM0
|
||||
|
||||
[opencpn]
|
||||
port = /dev/ttyV1
|
||||
navobj = ~/.opencpn/navobj.xml
|
||||
config = ~/.opencpn/opencpn.conf
|
||||
|
||||
[settings]
|
||||
timezone = 1
|
||||
boat_draft = 1.3
|
||||
fuel_tank = 17
|
||||
fuel_consumption = 1.5
|
||||
water_tank_1 = 47
|
||||
water_tank_2 = 50
|
||||
battery_voltage = 12
|
||||
battery_type = AGM
|
||||
battery_capacity = 200
|
||||
solar_power = 0
|
||||
generator_power = 0
|
||||
|
||||
[units]
|
||||
length_format = m
|
||||
distance_format = nm
|
||||
speed_format = kn
|
||||
wind_speed_format = ln
|
||||
temperature_format = C
|
||||
date_format = ISO
|
||||
|
||||
[display]
|
||||
hold_values = off
|
||||
backlight_color = red
|
||||
flash_led_mode = limit
|
||||
|
||||
[buzzer]
|
||||
buzzer_gps_error = off
|
||||
buzzer_gps_fix = off
|
||||
buzzer_by_limits = off
|
||||
buzzer_mode = off
|
||||
buzzer_power = 50
|
||||
|
||||
[pages]
|
||||
number_of_pages = 10
|
||||
start_page = 1
|
||||
|
||||
[page1]
|
||||
type=Voltage
|
||||
|
||||
[page2]
|
||||
type=Barograph
|
||||
|
||||
[page3]
|
||||
type=Anchor
|
||||
|
||||
[page4]
|
||||
type=Autobahn
|
||||
|
||||
[page5]
|
||||
type=Clock
|
||||
|
||||
[page6]
|
||||
type=TwoValues
|
||||
value1=LAT
|
||||
value2=LON
|
||||
|
||||
[page7]
|
||||
type=ThreeValues
|
||||
value1=COG
|
||||
value2=STW
|
||||
value3=DBT
|
||||
|
||||
[page8]
|
||||
type=FourValues
|
||||
value1=AWA
|
||||
value2=AWS
|
||||
value3=COG
|
||||
value4=STW
|
||||
|
||||
[page9]
|
||||
type=Rudder
|
||||
|
||||
[page10]
|
||||
type=SkyView
|
|
@ -0,0 +1,543 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
'''
|
||||
Virtuelles Multifunktionsgerät
|
||||
|
||||
Benötigte Pakete:
|
||||
python3-cairo python3-gi python3-gi-cairo gir1.2-rsvg-2.0
|
||||
|
||||
Um transparente Darstellung unter Openbox zu erhalten muß xcompmgr
|
||||
installiert und konfiguriert werden.
|
||||
|
||||
Wenn ein lokaler GPS-Empfänger angeschlossen ist, so kann dieser genutzt
|
||||
werden. Zusätzlich benötigte Pakete:
|
||||
python-serial python3-nmea2
|
||||
|
||||
Für Unterstützung des BME280-Sensors sind die Bibliotheken smbus2 und
|
||||
bme280 erforderlich.
|
||||
|
||||
Routen und Wegepunkte können von OpenCPN empfangen werden. Dazu muß eine
|
||||
passende serielle Schnittstelle für NMEA0183-Ausgang definiert werden.
|
||||
Im System kann diese in der Datei rc.local aktiviert werden:
|
||||
# Create virtual serial connection
|
||||
socat pty,rawer,echo=0,group-late=dialout,mode=0660,link=/dev/ttyV0 \
|
||||
pty,rawer,echo=0,group-late=dialout,mode=0660,link=/dev/ttyV1 &
|
||||
OpenCPN sendet dann Datensätze über ttyV0 und dieses Programm
|
||||
empfängt sie über ttyV1.
|
||||
Die Wegepunkte werden in OpenCPN im Standard auf 6 Zeichen gekürzt.
|
||||
Über die Konfigurationsdatei Settings / MaxWaypointNameLength=<nn>
|
||||
ist es möglich einen anderen Wert einzustellen im Bereich zwischen
|
||||
3 und 32. Lt. NMEA0183 ist die maximale Länge 10 Zeichen.
|
||||
|
||||
|
||||
Zeichensätze müssen, sofern sie noch nicht vorhanden sind in
|
||||
/usr/local/share/fonts abgelegt werden
|
||||
|
||||
Buttonlayout:
|
||||
[ 1 ] [ 2 ] [ 3 ] [ 4 ] [ 5 ] [ 6 ]
|
||||
|
||||
Button 6 ist reserviert für Beleuchtung ILUM
|
||||
|
||||
Neu
|
||||
Button 2: Abbruch
|
||||
Button 3: zurück
|
||||
Button 4: hoch
|
||||
Button 5: Ok/Menu
|
||||
|
||||
Verlagerung der Seitenanzeige in den Titel als Ziffer in Klammern
|
||||
|
||||
Vergleich zum Garmin GMI20:
|
||||
[ Zurück ] [ Hoch ] [ Menü ] [ Runter ] [ on/off ]
|
||||
|
||||
|
||||
Button 1 wird für AVG verwendet auf mehreren Seiten
|
||||
Button 5 wird für Trend TRND verwendet
|
||||
|
||||
Aber sowas von WIP
|
||||
|
||||
|
||||
Änderungsprotokoll
|
||||
==================
|
||||
|
||||
Version Datum Änderung(en) von
|
||||
-------- ----------- ------------------------------------------------------ ----
|
||||
0.1 2024-10-31 Entwicklung begonnen tho
|
||||
0.2 2024-12-24 Veräffentlichung als Git-Repository tho
|
||||
|
||||
|
||||
'''
|
||||
|
||||
import os
|
||||
import sys
|
||||
import configparser
|
||||
from setproctitle import setproctitle, setthreadtitle
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
gi.require_version('Rsvg', '2.0')
|
||||
from gi.repository import GLib, Gtk, Gdk, Rsvg
|
||||
import cairo
|
||||
import math
|
||||
import threading
|
||||
import can
|
||||
import serial
|
||||
import smbus2
|
||||
import pynmea2
|
||||
import bme280
|
||||
import math
|
||||
import time
|
||||
from datetime import datetime
|
||||
from nmea2000 import Device, BoatData, History, HistoryBuffer
|
||||
from nmea2000 import parser
|
||||
import pages
|
||||
import struct
|
||||
|
||||
__author__ = "Thomas Hooge"
|
||||
__copyright__ = "Copyleft 2024, all rights reversed"
|
||||
__version__ = "0.2"
|
||||
__email__ = "thomas@hoogi.de"
|
||||
__status__ = "Development"
|
||||
|
||||
cfg = {
|
||||
'cfgfile': 'obp60.conf',
|
||||
'imgpath': os.path.join(sys.path[0], 'images'),
|
||||
'deviceid': 100,
|
||||
'gps': False,
|
||||
'bme280': False
|
||||
}
|
||||
|
||||
def rxd_n2k():
|
||||
setthreadtitle("N2Klistener")
|
||||
bus = can.Bus(interface='socketcan', channel='can0', bitrate=250000);
|
||||
wip = False
|
||||
sc = 0
|
||||
nf = 0
|
||||
while not shutdown:
|
||||
msg = bus.recv(2)
|
||||
if not msg:
|
||||
continue
|
||||
priority = (msg.arbitration_id & 0x1c000000) >> 26
|
||||
source = msg.arbitration_id & 0x000000ff
|
||||
pgn = (msg.arbitration_id & 0x3ffff00) >> 8
|
||||
match pgn:
|
||||
case 129025:
|
||||
# Position
|
||||
#lat = struct.unpack_from('<l', msg.data, 0)[0] * 1e-07
|
||||
#lon = struct.unpack_from('<l', msg.data, 4)[0] * 1e-07
|
||||
#boatdata.setValue("LAT", lat)
|
||||
#boatdata.setValue("LON", lon)
|
||||
parser.parse_129025(msg.data, boatdata)
|
||||
case 127505:
|
||||
# Fluid level
|
||||
#fluidtype = msg.data[0] >> 4
|
||||
#instance = msg.data[0] & 0x0f
|
||||
#level = struct.unpack_from('<H', msg.data, 1)[0] * 0.004
|
||||
#capacity = struct.unpack_from('<L', msg.data, 2)[0] * 0.1
|
||||
#boatdata.tank[instance].capacity = capacity
|
||||
#boatdata.tank[instance].volume = level
|
||||
#boatdata.tank[instance].fluidtype = fluidtype
|
||||
parser.parse_127505(msg.data, boatdata)
|
||||
case 127508:
|
||||
# Battery status
|
||||
#instance = msg.data[0]
|
||||
#boatdata.voltage = (msg.data[2] * 256 + msg.data[1]) * 0.01 # Spannung
|
||||
#ampere = (msg.data[4] * 256 + msg.data[3]) * 0.1 # Stromstärke
|
||||
#temp = (msg.data[6] * 256 + msg.data[5]) * 0.01 - 273.15 # Temperatur
|
||||
#sid = msg.data[7]
|
||||
parser.parse_127508(msg.data, boatdata)
|
||||
case 129540:
|
||||
# GNS sats in view
|
||||
# TODO es kann mehrere Geräte geben die senden
|
||||
# - source beachten (muß gleich bleiben)
|
||||
# - gemischte Reihenfolge der Pakete ermöglichen
|
||||
sc = (msg.data[0] & 0xf0) >> 5
|
||||
fc = msg.data[0] & 0x1f
|
||||
if not wip:
|
||||
if fc != 0:
|
||||
continue
|
||||
source0 = source # muß über das Fast-packet konstant bleiben
|
||||
sc0 = sc # -"-
|
||||
fc0 = fc # dieser Zähler wird inkrementiert
|
||||
datalen = msg.data[1]
|
||||
nf = math.ceil((datalen - 6) / 7) + 1
|
||||
buf129540 = msg.data[2:]
|
||||
wip = True
|
||||
else:
|
||||
if (source == source0) and (sc == sc0) and (fc == fc0 + 1):
|
||||
buf129540.extend(msg.data[1:8])
|
||||
fc0 = fc
|
||||
else:
|
||||
# Dieser Frame paßt nicht
|
||||
#print("PGN 129540: sc/fc mismatch")
|
||||
pass
|
||||
if fc == nf:
|
||||
wip = False
|
||||
parser.parse_129540(buf129540, boatdata)
|
||||
case _:
|
||||
pass
|
||||
bus.shutdown()
|
||||
|
||||
def rxd_0183(devname):
|
||||
# Prüfe ob Port vorhanden ist und sich öffnen läßt
|
||||
try:
|
||||
ser = serial.Serial(devname, 115200, timeout=3)
|
||||
except serial.SerialException as e:
|
||||
print("OpenCPN serial port not available")
|
||||
return
|
||||
setthreadtitle("0183listener")
|
||||
while not shutdown:
|
||||
try:
|
||||
msg = pynmea2.parse(ser.readline().decode('ascii'))
|
||||
except pynmea2.nmea.ParseError:
|
||||
continue
|
||||
if msg.sentence_type == 'GLL':
|
||||
boatdata.setValue("LAT", msg.latitude)
|
||||
boatdata.setValue("LON", msg.longitude)
|
||||
elif msg.sentence_type == 'VTG':
|
||||
boatdata.setValue("COG", int(msg.true_track))
|
||||
boatdata.setValue("SOG", float(msg.spd_over_grnd_kts[:-1]))
|
||||
elif msg.sentence_type == 'VHW':
|
||||
boatdata.setValue("STW", float(msg.water_speed_knots))
|
||||
elif msg.sentence_type == 'WPL':
|
||||
# Wegepunkt
|
||||
print(msg.fields)
|
||||
elif msg.sentence_type == 'RTE':
|
||||
# Route
|
||||
print(msg.fields)
|
||||
ser.close()
|
||||
|
||||
def datareader(histpath, history):
|
||||
"""
|
||||
Daten zu fest definierten Zeitpunkten lesen
|
||||
|
||||
Die Schleife läuft einmal alle <n> Sekunden immer zum gleichen Zeitpunkt.
|
||||
Die Nutzlast sollte demnach weniger als eine Sekunde Laufzeit haben.
|
||||
Falls durch eine außergewöhnliche Situation doch einmal mehr als eine
|
||||
Sekunde benötigt werden sollte, gleicht sich das in den darauffolgenden
|
||||
Durchläufen wieder aus.
|
||||
|
||||
"""
|
||||
setthreadtitle("datareader")
|
||||
|
||||
# Speicherpfad für Meßwertdaten
|
||||
if not os.path.exists(histpath):
|
||||
os.makedirs(histpath)
|
||||
history.basepath = histpath
|
||||
# Serien initialisieren
|
||||
history.addSeries("BMP280-75", 336 ,75)
|
||||
history.addSeries("BMP280-150", 336 , 150)
|
||||
history.addSeries("BMP280-300", 336 , 300)
|
||||
history.addSeries("BMP280-600", 336 , 600)
|
||||
history.addSeries("BMP280-900", 336 , 900)
|
||||
for s in history.series.values():
|
||||
s.begin()
|
||||
|
||||
def g_tick(n=1):
|
||||
t = time.time()
|
||||
count = 0
|
||||
while True:
|
||||
count += n
|
||||
yield max(t + count - time.time(), 0)
|
||||
g = g_tick(1)
|
||||
|
||||
n = 0
|
||||
while not shutdown:
|
||||
time.sleep(next(g))
|
||||
# BME280 abfragen
|
||||
if cfg['bme280']:
|
||||
sensordata = bme280.sample(smbus, cfg['bme280_address'], cfg['bme280_cp'])
|
||||
# Aktuellen Wert merken
|
||||
boatdata.setValue("xdrTemp", sensordata.temperature)
|
||||
boatdata.setValue("xdrPress", sensordata.pressure)
|
||||
boatdata.setValue("xdrHum", sensordata.humidity)
|
||||
# Historie schreiben
|
||||
pval = int(sensordata.pressure *10)
|
||||
for k, v in history.series.items():
|
||||
if n % k == 0:
|
||||
v.add(pval)
|
||||
# Lokales GPS abfragen
|
||||
# TODO
|
||||
if cfg['gps']:
|
||||
pass
|
||||
n += 1
|
||||
|
||||
for s in history.series.values():
|
||||
s.finish()
|
||||
|
||||
class Frontend(Gtk.Window):
|
||||
|
||||
button = {
|
||||
1: (75, 485),
|
||||
2: (150, 492),
|
||||
3: (227, 496),
|
||||
4: (306, 496),
|
||||
5: (382, 492),
|
||||
6: (459, 485)
|
||||
}
|
||||
radius = 30
|
||||
|
||||
def __init__(self, device, boatdata, profile):
|
||||
super().__init__()
|
||||
self.owndev = device
|
||||
self.boatdata = boatdata
|
||||
|
||||
self.connect("destroy", self.on_destroy)
|
||||
|
||||
self.set_position(Gtk.WindowPosition.CENTER)
|
||||
self.set_size_request(530, 555)
|
||||
self.set_title("OBP60 virt")
|
||||
self.set_app_paintable(True)
|
||||
self.set_decorated(False)
|
||||
self.set_keep_above(True)
|
||||
|
||||
self.screen = self.get_screen()
|
||||
self.visual = self.screen.get_rgba_visual()
|
||||
if (self.visual is not None and self.screen.is_composited()):
|
||||
self.set_visual(self.visual)
|
||||
|
||||
handle = Rsvg.Handle()
|
||||
self._svg = handle.new_from_file(os.path.join(sys.path[0], "obp60.svg"))
|
||||
|
||||
self.connect("draw", self.on_draw)
|
||||
|
||||
self.da = Gtk.DrawingArea()
|
||||
self.da.add_events(Gdk.EventMask.BUTTON_PRESS_MASK|Gdk.EventMask.BUTTON_RELEASE_MASK)
|
||||
self.add(self.da)
|
||||
self.da.connect("draw", self.da_draw)
|
||||
self.da.connect('button-press-event', self.da_button_press)
|
||||
self.da.connect('button-release-event', self.da_button_release)
|
||||
|
||||
self.button_clicked = 0 # Geklickter Button vor Loslassen
|
||||
self.keylock = False
|
||||
self.pages = profile
|
||||
self.pageno = 1
|
||||
self.curpage = self.pages[self.pageno]
|
||||
|
||||
print("Wische von 2 nach 1 für Programmende")
|
||||
|
||||
def run(self):
|
||||
GLib.timeout_add_seconds(2, self.on_timer)
|
||||
self.show_all()
|
||||
Gtk.main()
|
||||
|
||||
def on_timer(self):
|
||||
# Boatdata validator
|
||||
boatdata.updateValid(5)
|
||||
# Tastaturstatus an Seite durchreichen
|
||||
self.curpage.keylock = self.keylock
|
||||
# Neuzeichnen
|
||||
self.queue_draw()
|
||||
return True
|
||||
|
||||
def on_draw(self, widget, ctx):
|
||||
# Fenstertransparenz
|
||||
ctx.set_source_rgba(0, 0, 0, 0)
|
||||
ctx.paint()
|
||||
|
||||
def da_draw(self, widget, ctx):
|
||||
viewport = Rsvg.Rectangle()
|
||||
viewport.x = 0
|
||||
viewport.y = 0
|
||||
viewport.width = 530
|
||||
viewport.height = 555
|
||||
self._svg.render_document(ctx, viewport)
|
||||
ctx.set_source_rgb(1.0, 0, 0)
|
||||
ctx.translate(64, 95) # Koordinatenursprung auf virtuellen Displaybereich setzen
|
||||
ctx.rectangle(0, 0, 400, 300)
|
||||
ctx.clip()
|
||||
|
||||
ctx.set_source_rgb(0, 0, 0) # Schwarz auf Weiß
|
||||
|
||||
# Heartbeat umschalten
|
||||
if self.curpage.header:
|
||||
self.curpage.draw_header(ctx)
|
||||
self.curpage.draw(ctx)
|
||||
if self.curpage.footer:
|
||||
self.curpage.draw_footer(ctx)
|
||||
|
||||
def da_button_press(self, widget, event):
|
||||
# Es gibt eine Liste mit Objekten und hier wird
|
||||
# geprüft ob und in welches Objekt geklickt wurde
|
||||
# Die eigentliche Funktion wird beim Loslassen ausgelöst.
|
||||
# Damit sind Wischgesten simulierbar
|
||||
self.button_clicked = 0
|
||||
if (event.x < self.button[1][0] - self.radius or event.x > self.button[6][0] + self.radius):
|
||||
return True
|
||||
if (event.y < self.button[1][1] - self.radius or event.y > self.button[3][1] + self.radius):
|
||||
return True
|
||||
for b, v in self.button.items():
|
||||
diff = math.sqrt((event.x - v[0])**2 + (event.y - v[1])**2)
|
||||
if diff < self.radius:
|
||||
self.button_clicked = b
|
||||
break
|
||||
return True
|
||||
|
||||
def da_button_release(self, widget, event):
|
||||
# Hier sind die eigentlichen Tastenfunktionen
|
||||
#
|
||||
# Die Auswertung ist abhängig von der jew. angezeigten Seite
|
||||
# Jede Seite kann eine Methode "handle_key" implementieren
|
||||
# Falls der Rückgabewert "True" ist, hat die Seite die Taste
|
||||
# verarbeitet, die Funktion hier wird damit unterdrückt.
|
||||
# TODO
|
||||
if (event.x < self.button[1][0] - self.radius or event.x > self.button[6][0] + self.radius):
|
||||
return True
|
||||
if (event.y < self.button[1][1] - self.radius or event.y > self.button[3][1] + self.radius):
|
||||
return True
|
||||
selected = 0
|
||||
for b, v in self.button.items():
|
||||
diff = math.sqrt((event.x - v[0])**2 + (event.y - v[1])**2)
|
||||
if diff < self.radius:
|
||||
selected = b
|
||||
break
|
||||
if self.keylock:
|
||||
# Bei Tastensperre einzige Möglichkeit: Tastensperre ausschalten
|
||||
if selected == 6 and self.button_clicked == 1:
|
||||
self.keylock = False
|
||||
return True
|
||||
if selected == 1:
|
||||
if self.button_clicked == 2:
|
||||
# Programmende bei Klicken auf 2 und loslassen auf 1
|
||||
self.get_window().set_cursor(Gdk.Cursor(Gdk.CursorType.WATCH))
|
||||
Gtk.main_quit()
|
||||
elif self.button_clicked == 6:
|
||||
# Klick auf 6 und loslassen auf 1 ist Tastatursperre
|
||||
self.keylock = True
|
||||
else:
|
||||
self.curpage.handle_key(1)
|
||||
elif selected == 2:
|
||||
# Abbruch/Zurück
|
||||
self.curpage.handle_key(2)
|
||||
elif selected == 3:
|
||||
# runter / eine Seite vor
|
||||
if not self.curpage.handle_key(3):
|
||||
if self.pageno > 1:
|
||||
self.pageno -= 1
|
||||
else:
|
||||
self.pageno = len(self.pages) - 1
|
||||
self.curpage = self.pages[self.pageno]
|
||||
elif selected == 4:
|
||||
if not self.curpage.handle_key(4):
|
||||
if self.pageno < len(self.pages) - 1:
|
||||
self.pageno += 1
|
||||
else:
|
||||
self.pageno = 1
|
||||
# hoch / eine Seite zurück
|
||||
self.curpage = self.pages[self.pageno]
|
||||
elif selected == 5:
|
||||
# Ok/Menü
|
||||
self.curpage.handle_key(5)
|
||||
elif selected == 6:
|
||||
if self.button_clicked == 6:
|
||||
# Backlight on/off
|
||||
self.curpage.backlight = not self.curpage.backlight
|
||||
elif self.button_clicked == 5:
|
||||
# Umschalten zur Systemseite
|
||||
self.curpage = self.pages[0]
|
||||
return True
|
||||
|
||||
def on_destroy(self, widget):
|
||||
Gtk.main_quit()
|
||||
|
||||
def init_profile(config, cfg, boatdata):
|
||||
'''
|
||||
config: Configparser-Objekt
|
||||
cfg: Laufende Programmkonfiguration
|
||||
Die Liste und Anordnung der Seiten nennen wir "Profil"
|
||||
Seiten-Profil aus Konfiguration erstellen
|
||||
Seite Nummer 0 ist immer die Systemseite. Diese ist nicht
|
||||
über die normale Seitenreihenfolge erreichbar, sondern
|
||||
durch eine spezielle Tastenkombination/Geste.
|
||||
TODO Prüfungen einbauen:
|
||||
Fortlaufende Seitennummern ab 1
|
||||
Fortlaufende Wertenummern ab 1
|
||||
Maximalwerte nicht überschreiten
|
||||
'''
|
||||
pages_max = config.getint('pages', 'number_of_pages')
|
||||
|
||||
# Suche alle Abschnitte, die mit "page" beginnen
|
||||
sects = config.sections()
|
||||
sects.remove('pages')
|
||||
pagedef = {}
|
||||
n = 0
|
||||
for s in sects:
|
||||
if s.startswith('page'):
|
||||
# Nummer und Art ermitteln
|
||||
pageno = int(s[4:])
|
||||
pagedef[pageno] = {'type': config.get(s, "type")}
|
||||
# Hole ein bin maximal 4 Werte je Seite
|
||||
values = {}
|
||||
valno = 1
|
||||
for i in (1, 2, 3, 4):
|
||||
try:
|
||||
values[i] = config.get(s, f"value{i}")
|
||||
except configparser.NoOptionError:
|
||||
break
|
||||
pagedef[pageno]['values'] = values
|
||||
n += 1
|
||||
if n >= pages_max:
|
||||
break
|
||||
clist = {
|
||||
0: pages.System(0, cfg, boatdata)
|
||||
}
|
||||
for i, p in pagedef.items():
|
||||
try:
|
||||
cls = getattr(pages, p['type'])
|
||||
except AttributeError:
|
||||
# Klasse nicht vorhanden, Seite wird nicht benutzt
|
||||
print(f"Klasse '{type}' nicht gefunden")
|
||||
continue
|
||||
c = cls(i, cfg, boatdata, *[v for v in p['values'].values()])
|
||||
clist[i] = c
|
||||
return clist
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
#setproctitle("obp60v")
|
||||
|
||||
shutdown = False
|
||||
owndevice = Device(100)
|
||||
boatdata = BoatData()
|
||||
boatdata.addTank(0)
|
||||
boatdata.addEngine(0)
|
||||
|
||||
# Basiskonfiguration aus Datei lesen
|
||||
config = configparser.ConfigParser()
|
||||
config.read(os.path.join(sys.path[0], cfg['cfgfile']))
|
||||
cfg['deviceid'] = config.getint('system', 'deviceid')
|
||||
cfg['simulation'] = config.getboolean('system', 'simulation')
|
||||
cfg['histpath'] = os.path.expanduser(config.get('system', 'histpath'))
|
||||
|
||||
cfg['gps'] = config.getboolean('gps', 'enabled')
|
||||
if cfg['gps']:
|
||||
cfg['gps_port'] = config.get('gps', 'port')
|
||||
|
||||
cfg['bme280'] = config.getboolean('bme280', 'enabled')
|
||||
if cfg['bme280']:
|
||||
cfg['bme280_port'] = config.getint('bme280', 'port')
|
||||
cfg['bme280_address'] = int(config.get('bme280', 'address'), 16) # convert 0x76
|
||||
smbus = smbus2.SMBus(cfg['bme280_port'])
|
||||
cfg['bme280_cp'] = bme280.load_calibration_params(smbus, cfg['bme280_address'])
|
||||
history = History("press", 75)
|
||||
boatdata.addHistory(history, "press")
|
||||
|
||||
cfg['ocpn_port'] = config.get('opencpn', 'port')
|
||||
|
||||
if cfg['simulation']:
|
||||
boatdata.enableSimulation()
|
||||
|
||||
profile = init_profile(config, cfg, boatdata)
|
||||
|
||||
t_rxd_n2k = threading.Thread(target=rxd_n2k)
|
||||
t_rxd_n2k.start()
|
||||
t_rxd_0183 = threading.Thread(target=rxd_0183, args=(cfg['ocpn_port'],))
|
||||
t_rxd_0183.start()
|
||||
|
||||
t_data = threading.Thread(target=datareader, args=(cfg['histpath'], history))
|
||||
t_data.start()
|
||||
|
||||
app = Frontend(owndevice, boatdata, profile)
|
||||
app.run()
|
||||
shutdown = True
|
||||
t_rxd_n2k.join()
|
||||
t_rxd_0183.join()
|
||||
print("Another fine product of the Sirius Cybernetics Corporation.")
|
|
@ -0,0 +1,106 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="529.258"
|
||||
height="554.04303"
|
||||
viewBox="0 0 140.03284 146.59055"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
xml:space="preserve"
|
||||
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||
sodipodi:docname="obp60.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="px"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.84096521"
|
||||
inkscape:cx="394.78446"
|
||||
inkscape:cy="291.92646"
|
||||
inkscape:current-layer="layer2" /><defs
|
||||
id="defs2" /><g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer2"
|
||||
inkscape:label="Rahmen"
|
||||
style="display:inline"><rect
|
||||
style="fill:#383838;fill-opacity:0.995886;stroke:#000000;stroke-width:0.264583;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect5580"
|
||||
width="111.65156"
|
||||
height="101.83302"
|
||||
x="13.757235"
|
||||
y="13.865232" /><circle
|
||||
style="fill:#5f6d9b;fill-opacity:1;stroke:#000000;stroke-width:0.264583;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path5526"
|
||||
cx="24.958796"
|
||||
cy="18.318338"
|
||||
r="1.1906251" /><rect
|
||||
style="fill:#dcdcdc;fill-opacity:1;stroke:#000000;stroke-width:0.264583;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect6475"
|
||||
width="105.56875"
|
||||
height="79.11042"
|
||||
x="16.723654"
|
||||
y="24.941565" /><path
|
||||
id="rect376"
|
||||
style="fill:#a2a2a2;fill-opacity:1;stroke:#000000;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:round"
|
||||
d="m 4.9387167,0.2645835 c -2.5895378,0 -4.6741332,2.0296907 -4.6741332,4.5511433 V 135.78447 c 0,2.52146 2.527424,4.27506 4.6741332,4.55166 0,0 36.5284973,6.49111 65.5148633,5.95881 28.986364,-0.5323 64.64049,-5.95881 64.64049,-5.95881 2.76316,-0.44318 4.67414,-2.0302 4.67414,-4.55166 V 4.8157268 c 0,-2.5214526 -2.0846,-4.5511433 -4.67414,-4.5511433 z M 13.857552,13.984676 H 125.45952 v 97.921664 c 0,0 -37.032483,3.56413 -55.632775,3.56413 -18.600293,0 -55.969193,-3.56413 -55.969193,-3.56413 z" /><g
|
||||
id="g11099"
|
||||
transform="translate(-5.4497963,-2.865458)"><g
|
||||
id="g11079"><circle
|
||||
style="fill:#2a2a2a;fill-opacity:1;stroke:#000000;stroke-width:0.264583;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path8373"
|
||||
cx="24.618317"
|
||||
cy="130.36757"
|
||||
r="7.9375" /><path
|
||||
sodipodi:type="star"
|
||||
style="fill:#525252;fill-opacity:1;stroke:#000000;stroke-width:0.616746;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path9879"
|
||||
inkscape:flatsided="true"
|
||||
sodipodi:sides="6"
|
||||
sodipodi:cx="16.355543"
|
||||
sodipodi:cy="150.80168"
|
||||
sodipodi:r1="7.745985"
|
||||
sodipodi:r2="4.8291588"
|
||||
sodipodi:arg1="1.0332539"
|
||||
sodipodi:arg2="1.5568527"
|
||||
inkscape:rounded="0"
|
||||
inkscape:randomized="0"
|
||||
d="m 20.321693,157.45525 -7.745232,0.108 -3.9661499,-6.65356 3.7790819,-6.76157 7.745232,-0.10801 3.96615,6.65357 z"
|
||||
inkscape:transform-center-x="-0.093238378"
|
||||
inkscape:transform-center-y="-0.22711284"
|
||||
transform="matrix(0.4296957,0,0,0.42830241,17.59041,65.778845)" /></g><use
|
||||
x="0"
|
||||
y="0"
|
||||
xlink:href="#g11079"
|
||||
id="use11081"
|
||||
transform="rotate(-15.936104,42.162291,58.466326)" /><use
|
||||
x="0"
|
||||
y="0"
|
||||
xlink:href="#g11079"
|
||||
id="use11083"
|
||||
transform="rotate(-5.080918,80.937544,-325.73636)" /><use
|
||||
x="0"
|
||||
y="0"
|
||||
xlink:href="#g11079"
|
||||
id="use11085"
|
||||
transform="rotate(17.398182,44.604565,331.50397)" /><use
|
||||
x="0"
|
||||
y="0"
|
||||
xlink:href="#g11079"
|
||||
id="use11087"
|
||||
transform="translate(81.407112,2.126153)" /><use
|
||||
x="0"
|
||||
y="0"
|
||||
xlink:href="#g11079"
|
||||
id="use11089"
|
||||
transform="rotate(-15.814908,75.67043,-235.37704)" /></g></g></svg>
|
After Width: | Height: | Size: 4.6 KiB |
|
@ -0,0 +1,36 @@
|
|||
# Displayseiten
|
||||
|
||||
from .system import System
|
||||
|
||||
# Generische Seiten
|
||||
from .onevalue import OneValue
|
||||
from .twovalues import TwoValues
|
||||
from .threevalues import ThreeValues
|
||||
from .fourvalues import FourValues
|
||||
from .fourvalues2 import FourValues2
|
||||
|
||||
# Graphen
|
||||
from .onegraph import OneGraph
|
||||
from .twographs import TwoGraphs
|
||||
from .exhaust import Exhaust
|
||||
|
||||
# Analoginstrumente
|
||||
from .clock import Clock
|
||||
from .fluid import Fluid
|
||||
|
||||
# Spezialseiten
|
||||
from .anchor import Anchor
|
||||
from .apparentwind import ApparentWind
|
||||
from .autobahn import Autobahn
|
||||
from .barograph import Barograph
|
||||
from .battery import Battery
|
||||
from .battery2 import Battery2
|
||||
from .bme280 import BME280
|
||||
from .dst810 import DST810
|
||||
from .keel import Keel
|
||||
from .rollpitch import RollPitch
|
||||
from .skyview import SkyView
|
||||
from .solar import Solar
|
||||
from .rudder import Rudder
|
||||
from .voltage import Voltage
|
||||
from .windrose import WindRose
|
|
@ -0,0 +1,52 @@
|
|||
"""
|
||||
|
||||
Ankerinfo / -alarm
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
import cairo
|
||||
import math
|
||||
from .page import Page
|
||||
|
||||
class Anchor(Page):
|
||||
|
||||
def __init__(self, pageno, cfg, boatdata):
|
||||
super().__init__(pageno, cfg, boatdata)
|
||||
self.sym_anchor = cairo.ImageSurface.create_from_png(os.path.join(cfg['imgpath'], "anchor.png"))
|
||||
self.buttonlabel[1] = 'DEC'
|
||||
self.buttonlabel[2] = 'INC'
|
||||
self.buttonlabel[5] = 'SET'
|
||||
|
||||
def draw(self, ctx):
|
||||
|
||||
# Name
|
||||
ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
|
||||
ctx.set_font_size(20)
|
||||
|
||||
ctx.move_to(2, 50)
|
||||
ctx.show_text("Anchor")
|
||||
ctx.move_to(320, 50)
|
||||
ctx.show_text("Chain")
|
||||
|
||||
ctx.set_font_size(16)
|
||||
ctx.move_to(2, 70)
|
||||
ctx.show_text("Alarm: off")
|
||||
ctx.move_to(320, 70)
|
||||
ctx.show_text("45 m")
|
||||
ctx.stroke()
|
||||
|
||||
|
||||
# Spezialseite
|
||||
cx = 200
|
||||
cy = 150
|
||||
r = 125
|
||||
|
||||
ctx.set_line_width(1.5)
|
||||
ctx.arc(cx, cy, r, 0, 2*math.pi)
|
||||
ctx.stroke()
|
||||
|
||||
ctx.save()
|
||||
ctx.set_source_surface(self.sym_anchor, cx-8, cy-8)
|
||||
ctx.paint()
|
||||
ctx.restore()
|
|
@ -0,0 +1,16 @@
|
|||
import cairo
|
||||
import math
|
||||
from .page import Page
|
||||
|
||||
class ApparentWind(Page):
|
||||
|
||||
def __init__(self, pageno, cfg, boatdata):
|
||||
super().__init__(pageno, cfg, boatdata)
|
||||
|
||||
def draw(self, ctx):
|
||||
# Name
|
||||
ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
|
||||
ctx.set_font_size(60)
|
||||
ctx.move_to(20, 100)
|
||||
ctx.show_text("Apparent Wind")
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
"""
|
||||
|
||||
3D-View angelehnt an die NASA Clipper GPS-Darstellung
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
import cairo
|
||||
import math
|
||||
from .page import Page
|
||||
|
||||
class Autobahn(Page):
|
||||
|
||||
def __init__(self, pageno, cfg, boatdata):
|
||||
super().__init__(pageno, cfg, boatdata)
|
||||
self.xte = self.bd.getRef("XTE")
|
||||
self.cog = self.bd.getRef("COG")
|
||||
self.btw = self.bd.getRef("BTW")
|
||||
self.dtw = self.bd.getRef("DTW")
|
||||
self.wpname = "no data"
|
||||
self.symbol = cairo.ImageSurface.create_from_png(os.path.join(cfg['imgpath'], "ship.png"))
|
||||
|
||||
def draw(self, ctx):
|
||||
# Beschriftung unter den Werten
|
||||
ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
|
||||
ctx.set_font_size(16)
|
||||
ctx.move_to(50, 188);
|
||||
ctx.show_text("Cross-track error")
|
||||
ctx.move_to(270, 188)
|
||||
ctx.show_text("Track")
|
||||
ctx.move_to(45, 275);
|
||||
ctx.show_text("Distance to waypoint")
|
||||
ctx.move_to(260, 275);
|
||||
ctx.show_text("Bearing")
|
||||
ctx.stroke()
|
||||
|
||||
# Meßwerte
|
||||
ctx.select_font_face("DSEG7 Classic")
|
||||
ctx.set_font_size(60)
|
||||
|
||||
ctx.move_to(40, 170)
|
||||
#ctx.show_text(self.xte.format())
|
||||
ctx.show_text("2.3")
|
||||
ctx.move_to(220, 170)
|
||||
#ctx.show_text(self.cog.format())
|
||||
ctx.show_text("253")
|
||||
ctx.move_to(40, 257)
|
||||
#ctx.show_text(self.dtw.format())
|
||||
ctx.show_text("5.8")
|
||||
ctx.move_to(220, 257)
|
||||
#ctx.show_text(self.btw.format())
|
||||
ctx.show_text("248")
|
||||
|
||||
# 3D-Ansicht oben
|
||||
# TODO Schiffssymbol
|
||||
ctx.save()
|
||||
ctx.set_source_surface(self.symbol, 186, 68)
|
||||
ctx.paint()
|
||||
ctx.restore()
|
||||
|
||||
# Segmente: 2 1 0 3 4 5
|
||||
seg = [True] * 6
|
||||
points = {
|
||||
2: ((0, 54), (46, 24), (75, 24), (0, 90)),
|
||||
1: ((0, 100), (82, 24), (112, 24), (50, 100)),
|
||||
0: ((60, 100), (117, 24), (147, 24), (110, 100)),
|
||||
3: ((340, 100), (283, 24), (253, 24), (290, 100)),
|
||||
4: ((399, 100), (318, 24), (289, 24), (350, 100)),
|
||||
5: ((399, 54), (354, 24), (325, 24), (399, 90))
|
||||
}
|
||||
|
||||
# Winkeldifferenz
|
||||
diff = (self.cog.value or 0) - (self.btw.value or 0)
|
||||
if diff < -180:
|
||||
diff += 360
|
||||
elif diff > 180:
|
||||
diff -= 360
|
||||
|
||||
if diff > 0:
|
||||
order = (3, 4, 5, 0, 1, 2)
|
||||
else:
|
||||
order = (0, 1, 2, 3, 4, 5)
|
||||
|
||||
# Anzahl aktiver Segmente
|
||||
seg_step = math.radians(3)
|
||||
nseg = min(abs(diff) / seg_step, 5)
|
||||
i = 0
|
||||
while nseg > 0:
|
||||
seg[order[i]] = False
|
||||
i += 1
|
||||
nseg -= 1
|
||||
|
||||
# Segmente zeichnen
|
||||
for p in range(6):
|
||||
ctx.move_to(*points[p][0])
|
||||
ctx.line_to(*points[p][1])
|
||||
ctx.line_to(*points[p][2])
|
||||
ctx.line_to(*points[p][3])
|
||||
if seg[p]:
|
||||
ctx.fill()
|
||||
else:
|
||||
ctx.stroke()
|
|
@ -0,0 +1,366 @@
|
|||
"""
|
||||
|
||||
Siehe auch: Steamrock Digital Barometer
|
||||
|
||||
Meßwert alls 15 Minuten:
|
||||
Es wird in hPa gemessen mit einer Nachkommastelle
|
||||
84 Stunden * 4 Werte je Stunde = 336 Meßwerte
|
||||
Tendenzwert über 3 Stunden
|
||||
|
||||
Je Zoomstufe wird ein eigener Buffer vorgehalten um ein sauberes
|
||||
Diagramm zu erhalten. Überall gilt: Je Pixel ein Meßwert.
|
||||
|
||||
Drucktendenz:
|
||||
- 1 hour tendency
|
||||
- 3 hour tendency
|
||||
|
||||
Verschiedene Datenquellen auswählbar:
|
||||
- intern (BME280, BMP280)
|
||||
- N2K generisch
|
||||
- Historie von
|
||||
- Yacht devices
|
||||
- Capteurs?
|
||||
|
||||
Das Diagramm wird mit Ursprung rechts unten (x0, y0) gezeichnet,
|
||||
da die Werte in der Vergangenhait liegen, also links vom Ursprung.
|
||||
|
||||
Damit eine saubere Skala auf der Y-Achse erreicht wird, gibt einige
|
||||
feste Skalierungen.
|
||||
Standard: 20hPa von unten nach oben, z.B. 1015, 1020, 1025, 1030, 1035
|
||||
|
||||
"""
|
||||
|
||||
import time
|
||||
import cairo
|
||||
from .page import Page
|
||||
|
||||
class Barograph(Page):
|
||||
|
||||
def __init__(self, pageno, cfg, boatdata):
|
||||
super().__init__(pageno, cfg, boatdata)
|
||||
# Meßwert alle 15 Minuten:
|
||||
# 84 Stunden * 4 Werte je Stunde = 336 Meßwerte
|
||||
self.bd = boatdata
|
||||
self.source = 'I' # (I)ntern, e(X)tern
|
||||
self.zoom = (1, 2, 3, 6, 12)
|
||||
self.zoomindex = 4
|
||||
self.series = (75, 150, 300, 600, 900)
|
||||
|
||||
# Y-Axis
|
||||
self.vmin = 0
|
||||
self.vmax = 0
|
||||
self.scalemin = 1000
|
||||
self.scalemax = 1020
|
||||
self.scalestep = 5
|
||||
|
||||
# Tendenzwert über 3 Stunden
|
||||
self.hist3 = None
|
||||
self.hist1 = None
|
||||
|
||||
self.buttonlabel[1] = '+'
|
||||
self.buttonlabel[2] = '-'
|
||||
self.buttonlabel[5] = 'SRC'
|
||||
|
||||
self.refresh = time.time() - 30
|
||||
|
||||
def handle_key(self, buttonid):
|
||||
# TODO Serie auswählen aufgrund Zoomlevel
|
||||
if buttonid == 1:
|
||||
# Zoom in
|
||||
if self.zoomindex > 0:
|
||||
self.zoomindex -= 1
|
||||
self.refresh = time.time() - 30
|
||||
elif buttonid == 2:
|
||||
# Zoom out
|
||||
if self.zoomindex < len(self.zoom) - 1:
|
||||
self.zoomindex += 1
|
||||
self.refresh = time.time() - 30
|
||||
if buttonid == 5:
|
||||
# Source
|
||||
if self.source == 'I':
|
||||
self.source = 'X'
|
||||
else:
|
||||
self.source = 'I'
|
||||
|
||||
# Testausgabe der Datenerfassung
|
||||
data = []
|
||||
vmin = data[0]
|
||||
vmax = data[0]
|
||||
i = self.series[self.zoomindex]
|
||||
for value in self.bd.history['press'].series[i].get():
|
||||
v = value / 10
|
||||
data.append(v)
|
||||
if v < vmin and v != 0:
|
||||
vmin = v
|
||||
elif v > vmax and v != 0:
|
||||
vmax = v
|
||||
print(f"Werte: vmin={vmin}, vmax={vmax}")
|
||||
ymin, ymax, step = self.getYScale(vmin, vmax)
|
||||
print(f"Skala: ymin={ymin}, ymax={ymax}, step={step}")
|
||||
print(f"zoomindex={self.zoomindex}, series={self.series[self.zoomindex]}")
|
||||
|
||||
hist1a = self.bd.history['press'].series[i].getvalue(3600)
|
||||
hist1b = self.bd.history['press'].series[i].getvalue3(3600)
|
||||
trend1 = data[0] - hist1b
|
||||
print(f"{hist1a} / {hist1b} -> Trend1: {trend1:.1f}")
|
||||
|
||||
def loadData(self):
|
||||
"""
|
||||
Transfer data from history to page buffer
|
||||
set y-axis according to data
|
||||
"""
|
||||
self.data = []
|
||||
self.vmin = 9999
|
||||
self.vmax = 0
|
||||
i = self.series[self.zoomindex]
|
||||
for value in self.bd.history['press'].series[i].get():
|
||||
v = value / 10
|
||||
self.data.append(v)
|
||||
if v < self.vmin and v != 0:
|
||||
self.vmin = v
|
||||
elif v > self.vmax and v != 0:
|
||||
self.vmax = v
|
||||
self.scalemin, self.scalemax, self.scalestep = self.getYScale(self.vmin, self.vmax)
|
||||
return True
|
||||
|
||||
def drawTrend(self, ctx, code, x, y, w):
|
||||
"""
|
||||
One hour Trend
|
||||
0: Stationary <= 1 hPa
|
||||
1: Rising >1 and <= 2 hPa
|
||||
2: Rising fast >2 and <= 3 hPa
|
||||
3: Rising very fast >3 hPa
|
||||
-1: Falling
|
||||
-2: Falling fast
|
||||
-3: Falling very fast
|
||||
"""
|
||||
trend1map = {
|
||||
-3: "Falling_Very_Fast.png", # > 3 hPa
|
||||
-2: "Falling_Fast.png", # > 2 and <= 3 hPa
|
||||
-1: "Falling.png", # > 1 and <= 2 hPa
|
||||
0: "Stationary.png", # <= +/- 1 hPa
|
||||
1: "Rising.png", # < -1 and >= -2 hPa
|
||||
2: "Rising_Fast.png", # < -2 and >= -3 hPa
|
||||
3: "Rising_Very_Fast.png" # < -3 hPa
|
||||
}
|
||||
|
||||
if code == 0:
|
||||
# Pfeil horizontal rechts
|
||||
ctx.move_to(x, y - w / 2)
|
||||
ctx.line_to(x + w, y - w / 2)
|
||||
ctx.draw()
|
||||
# Position merken
|
||||
ctx.line_to(x - w / 4, y - w)
|
||||
ctx.line_to(x - w / 4, y)
|
||||
ctx.line_to(x + w, y - w / 2)
|
||||
ctx.fill()
|
||||
elif code == 1:
|
||||
# Pfeil schräg nach oben
|
||||
pass
|
||||
elif code == 2:
|
||||
# Pfeil gerade nach oben
|
||||
pass
|
||||
elif code == 3:
|
||||
# Doppelpfeil nach oben
|
||||
pass
|
||||
elif code == -1:
|
||||
# Pfeil schräg nach unten
|
||||
pass
|
||||
elif code == -2:
|
||||
# Pfeil gerade nach unten
|
||||
pass
|
||||
elif code == -3:
|
||||
# Doppelpfeil nach unten
|
||||
pass
|
||||
|
||||
def drawWMOCode(self, ctx, code, x, y, w):
|
||||
"""
|
||||
Three hour code
|
||||
Code 0 to 8:
|
||||
0: Increasing, then decreasing; athmospheric pressure the same
|
||||
as or higher than three hours ago
|
||||
1: Increasing then steady; or increasing, then increasing more
|
||||
slowly; athmospheric pressure now higher than three hours ago
|
||||
2: Increasing (steadily or unsteadily); athmospheric pressure
|
||||
now higher than three hours ago
|
||||
3: Decreasing or steady, then increasing; or increasing then
|
||||
increasing more rapidly; athmospheric pressure now higher
|
||||
than three hours ago
|
||||
4: Steady; athmospheric pressure is the same as three hours ago
|
||||
5: Decreasing, then increasing; athmospheric pressure now is the
|
||||
same as or lower than three hours ago
|
||||
6:
|
||||
7:
|
||||
8:
|
||||
"""
|
||||
pass
|
||||
|
||||
def getYScale(self, vmin, vmax):
|
||||
# Y-Achse aufgrund Meßwerten einstellen
|
||||
diff = vmax - vmin
|
||||
if diff < 20:
|
||||
step = 5
|
||||
elif diff <= 40:
|
||||
step = 10
|
||||
else:
|
||||
step = 15
|
||||
vmin = int(vmin - (vmin % step)) # Nächstes Vielfaches nach oben
|
||||
vmax = int(vmax + step - (vmax % step)) # Nächstes Vielfaches nach unten
|
||||
return (vmin, vmax, step)
|
||||
|
||||
def draw(self, ctx):
|
||||
"""
|
||||
Darstellung angelehnt an klassisches Gerät
|
||||
Daten werden im nichtflüchtigen Speicher gehalten
|
||||
Da sich die Daten langsam verändern, reicht es, diese z.B. nur alle
|
||||
30 Sekunden oder langsamer zu laden.
|
||||
Der aktuelle Wert oben ist natürlich nicht alt.
|
||||
|
||||
Datenreihen
|
||||
- 1 Woche, stündlich: 7 * 24 = 168 Meßwerte
|
||||
- 1 Tag, alle 10 min: 24 * 6 = 144 Meßwerte
|
||||
Der Druck wird in zwei Bytes abgespeichert. Es wird eine Nachkommastelle
|
||||
verwendet. Um ohne Fließkommazahlen auszukommen wird der Maßwert einfach
|
||||
mit 10 multipliziert.
|
||||
|
||||
Darstellung wie Steamrock:
|
||||
1 Pixel entspricht einem Meßwert alle 15min.
|
||||
1 Tag hat dementsprechend eine Breite von 48px
|
||||
"""
|
||||
|
||||
timestamp = time.time()
|
||||
if timestamp - self.refresh >= 30:
|
||||
self.refresh = timestamp
|
||||
self.loadData()
|
||||
|
||||
ctx.set_source_rgb(0, 0, 0)
|
||||
ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
|
||||
|
||||
# Datenquelle rechts oben
|
||||
ctx.set_font_size(16)
|
||||
ctx.move_to(330, 50)
|
||||
if self.source == 'I':
|
||||
ctx.show_text("BMP280")
|
||||
else:
|
||||
ctx.show_text("N2K Bus")
|
||||
ctx.stroke()
|
||||
# Zoomstufe
|
||||
datastep = self.series[self.zoomindex]
|
||||
if datastep > 120:
|
||||
if datastep % 60 == 0:
|
||||
fmt = "{:.0f} min"
|
||||
else:
|
||||
fmt = "{:.1f} min"
|
||||
datastep /= 60
|
||||
else:
|
||||
fmt = '{} s'
|
||||
self.draw_text_center(ctx, 360, 62, fmt.format(datastep))
|
||||
|
||||
# Aktueller Luftdruck hPa
|
||||
ctx.set_font_size(32)
|
||||
self.draw_text_center(ctx, 200, 40, self.bd.pressure.format())
|
||||
#self.draw_text_center(ctx, 200, 40, "1019.2")
|
||||
ctx.set_font_size(16)
|
||||
self.draw_text_center(ctx, 200, 62, "hPa")
|
||||
|
||||
# Trend
|
||||
ctx.set_font_size(16)
|
||||
# TODO Trend linie
|
||||
#trend =
|
||||
self.draw_text_center(ctx, 295, 62, "0.0")
|
||||
|
||||
# min/max
|
||||
ctx.move_to(10, 38)
|
||||
ctx.show_text(f"min: {self.vmin}")
|
||||
ctx.move_to(10, 50)
|
||||
ctx.show_text(f"max: {self.vmax}")
|
||||
|
||||
# Alarm
|
||||
self.draw_text_center(ctx, 70, 62, "Alarm Off")
|
||||
|
||||
# Hintergrundrahmen
|
||||
ctx.set_line_width(2)
|
||||
|
||||
ctx.move_to(0, 75)
|
||||
ctx.line_to(400, 75)
|
||||
|
||||
ctx.move_to(130, 20)
|
||||
ctx.line_to(130, 75)
|
||||
|
||||
ctx.move_to(270, 20)
|
||||
ctx.line_to(270, 75)
|
||||
|
||||
ctx.move_to(325, 20)
|
||||
ctx.line_to(325, 75)
|
||||
|
||||
ctx.stroke()
|
||||
|
||||
|
||||
# Diagramm
|
||||
# --------
|
||||
ymin = self.scalemin
|
||||
ymax = self.scalemax
|
||||
yn = self.scalestep
|
||||
ystep = (ymax - ymin) / yn
|
||||
|
||||
xstep = 48
|
||||
|
||||
# Ursprung ist rechts unten
|
||||
x0 = 350
|
||||
y0 = 270
|
||||
w = 7 * 48
|
||||
h = 180
|
||||
|
||||
ctx.set_line_width(1)
|
||||
ctx.rectangle(x0 - w + 0.5, y0 - h + 0.5, w, h)
|
||||
ctx.stroke()
|
||||
|
||||
# X-Achse sind Stunden
|
||||
xn = 0
|
||||
for xt in [x * -1 * self.zoom[self.zoomindex] for x in range(1,7)]:
|
||||
xn += 1
|
||||
ctx.move_to(x0 - xn * xstep + 0.5, y0)
|
||||
ctx.line_to(x0 - xn * xstep + 0.5, y0 - h)
|
||||
ctx.stroke()
|
||||
self.draw_text_center(ctx, x0 - xn * xstep + 0.5, y0 - 8, str(xt), fill=True)
|
||||
ctx.stroke()
|
||||
#for x in (1, 2, 3, 4, 5, 6):
|
||||
# ctx.move_to(x0 - x * 48 + 0.5, y0 + 0.5)
|
||||
# ctx.line_to(x0 - x * 48 + 0.5, y0 - h + 0.5)
|
||||
#ctx.stroke()
|
||||
|
||||
# Y-Achse
|
||||
ctx.move_to(x0 + 5.5, y0 + 0.5)
|
||||
ctx.line_to(x0 + 5.5, y0 - h)
|
||||
ctx.move_to(x0 - w - 5.5, y0 + 0.5)
|
||||
ctx.line_to(x0 - w - 5.5, y0 -h )
|
||||
ctx.stroke()
|
||||
|
||||
dy = 9 # Pixel je hPa
|
||||
ysmin = self.scalemin
|
||||
ysmax = self.scalemax
|
||||
|
||||
y = y0 + 0.5
|
||||
ystep = self.scalestep
|
||||
ys = ysmin
|
||||
while y >= y0 - h:
|
||||
if ys % ystep == 0:
|
||||
ctx.move_to(x0 + 10, y + 5.5)
|
||||
ctx.show_text(str(ys))
|
||||
ctx.move_to(x0 - w - 5, y)
|
||||
ctx.line_to(x0 + 5, y)
|
||||
else:
|
||||
ctx.move_to(x0, y)
|
||||
ctx.line_to(x0 + 5, y)
|
||||
ctx.move_to(x0 - w - 5, y)
|
||||
ctx.line_to(x0 - w, y)
|
||||
y -= dy
|
||||
ys += 1
|
||||
ctx.stroke()
|
||||
|
||||
# Meßdaten
|
||||
for v in self.data:
|
||||
x0 -= 1
|
||||
if v > 0:
|
||||
ctx.rectangle(x0, y0 - (v - ysmin) * dy, 1.5, 1.5)
|
||||
ctx.fill()
|
|
@ -0,0 +1,66 @@
|
|||
"""
|
||||
|
||||
Batteriewerte eines INA219 oder INA226 Sensors
|
||||
Ähnlich ThreeValue
|
||||
|
||||
"""
|
||||
|
||||
import cairo
|
||||
from .page import Page
|
||||
|
||||
class Battery(Page):
|
||||
|
||||
avg = (1, 10, 60, 300);
|
||||
|
||||
def __init__(self, pageno, cfg, boatdata):
|
||||
super().__init__(pageno, cfg, boatdata)
|
||||
self.avgindex = 0
|
||||
self.buttonlabel[1] = 'AVG'
|
||||
|
||||
def handle_key(self, buttonid):
|
||||
if buttonid == 1:
|
||||
if self.avgindex < len(self.avg) -1:
|
||||
self.avgindex += 1
|
||||
else:
|
||||
self.avgindex = 0
|
||||
return True
|
||||
return False
|
||||
|
||||
def draw(self, ctx):
|
||||
|
||||
# Aufteilung in 3 Bereiche durch 2 Linien
|
||||
ctx.rectangle(0, 105, 400, 3);
|
||||
ctx.rectangle(0, 195, 400, 3);
|
||||
ctx.fill()
|
||||
|
||||
# Name
|
||||
ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
|
||||
ctx.set_font_size(40)
|
||||
ctx.move_to(20, 55)
|
||||
ctx.show_text("VBat")
|
||||
ctx.move_to(20, 145)
|
||||
ctx.show_text("IBat")
|
||||
ctx.move_to(20, 235)
|
||||
ctx.show_text("PBat")
|
||||
ctx.stroke()
|
||||
|
||||
# Einheit
|
||||
ctx.set_font_size(24)
|
||||
ctx.move_to(20, 90)
|
||||
ctx.show_text("V")
|
||||
ctx.move_to(20, 180)
|
||||
ctx.show_text("A")
|
||||
ctx.move_to(20, 270)
|
||||
ctx.show_text("W")
|
||||
|
||||
|
||||
# Werte
|
||||
ctx.select_font_face("DSEG7 Classic")
|
||||
ctx.set_font_size(60)
|
||||
ctx.move_to(180, 90)
|
||||
ctx.show_text("12.3")
|
||||
ctx.move_to(180, 180)
|
||||
ctx.show_text("3.2")
|
||||
ctx.move_to(180, 270)
|
||||
ctx.show_text("39.4")
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
"""
|
||||
|
||||
Komplexe Batterieübersichtsseite
|
||||
|
||||
"""
|
||||
|
||||
import cairo
|
||||
from .page import Page
|
||||
|
||||
class Battery2(Page):
|
||||
|
||||
def draw_battery(self, ctx, x, y, w, h, level):
|
||||
'''
|
||||
Das Rechteck ist das komplett umschließende
|
||||
Level ist der prozentuale Füllstand
|
||||
'''
|
||||
pass
|
||||
|
||||
'''
|
||||
// Battery graphic with fill level
|
||||
void batteryGraphic(uint x, uint y, float percent, int pcolor, int bcolor){
|
||||
// Show battery
|
||||
int xb = x; // X position
|
||||
int yb = y; // Y position
|
||||
int t = 4; // Line thickness
|
||||
// Percent limits
|
||||
if(percent < 0){
|
||||
percent = 0;
|
||||
}
|
||||
if(percent > 99){
|
||||
percent = 99;
|
||||
}
|
||||
// Battery corpus 100x80 with fill level
|
||||
int level = int((100.0 - percent) * (80-(2*t)) / 100.0);
|
||||
getdisplay().fillRect(xb, yb, 100, 80, pcolor);
|
||||
if(percent < 99){
|
||||
getdisplay().fillRect(xb+t, yb+t, 100-(2*t), level, bcolor);
|
||||
}
|
||||
// Plus pol 20x15
|
||||
int xp = xb + 20;
|
||||
int yp = yb - 15 + t;
|
||||
getdisplay().fillRect(xp, yp, 20, 15, pcolor);
|
||||
getdisplay().fillRect(xp+t, yp+t, 20-(2*t), 15-(2*t), bcolor);
|
||||
// Minus pol 20x15
|
||||
int xm = xb + 60;
|
||||
int ym = yb -15 + t;
|
||||
getdisplay().fillRect(xm, ym, 20, 15, pcolor);
|
||||
getdisplay().fillRect(xm+t, ym+t, 20-(2*t), 15-(2*t), bcolor);
|
||||
'''
|
||||
|
||||
|
||||
def draw(self, ctx):
|
||||
ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
|
||||
ctx.set_font_size(40)
|
||||
ctx.move_to(10, 65)
|
||||
ctx.show_text("Bat.")
|
||||
|
||||
# Batterietyp
|
||||
ctx.move_to(90, 65)
|
||||
ctx.show_text("AGM")
|
||||
|
||||
# Kapazität
|
||||
ctx.move_to(10, 200)
|
||||
ctx.select_font_face("DSEG7 Classic")
|
||||
ctx.set_font_size(40)
|
||||
ctx.show_text("12")
|
||||
|
||||
ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
|
||||
ctx.set_font_size(16)
|
||||
ctx.show_text("Ah")
|
||||
|
||||
ctx.move_to(10, 235)
|
||||
ctx.show_text("Installed")
|
||||
|
||||
ctx.move_to(10, 255)
|
||||
ctx.show_text("Battery Type")
|
||||
|
||||
# Batteriegraphik
|
||||
# Rechteck mit Füllstand 100x80, oben zwei Pole
|
||||
ctx.rectangle(150, 100, 100, 80)
|
||||
ctx.stroke()
|
||||
# Füllstand
|
||||
# Pole
|
||||
|
||||
|
||||
'''
|
||||
ctx.line_to(2.5, 1.5)
|
||||
|
||||
ctx.set_line_width(0.06)
|
||||
'''
|
|
@ -0,0 +1,55 @@
|
|||
"""
|
||||
|
||||
Werte eines lokal angeschlossenen BME280/BMP280
|
||||
|
||||
"""
|
||||
|
||||
import cairo
|
||||
from .page import Page
|
||||
|
||||
class BME280(Page):
|
||||
|
||||
def __init__(self, pageno, cfg, boatdata):
|
||||
super().__init__(pageno, cfg, boatdata)
|
||||
#self.ref1 = self.bd.getRef(boatvalue1)
|
||||
#self.ref2 = self.bd.getRef(boatvalue2)
|
||||
#self.ref3 = self.bd.getRef(boatvalue3)
|
||||
|
||||
def draw(self, ctx):
|
||||
|
||||
# Bildschirmunterteilung mit Linien
|
||||
ctx.rectangle(0, 105, 399, 3)
|
||||
ctx.rectangle(0, 195, 399, 3)
|
||||
ctx.fill()
|
||||
|
||||
# Beschriftung
|
||||
ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
|
||||
ctx.set_font_size(40)
|
||||
# Titel
|
||||
ctx.move_to(20,55)
|
||||
ctx.show_text("Temp")
|
||||
ctx.move_to(20,145)
|
||||
ctx.show_text("Humid")
|
||||
ctx.move_to(20, 235)
|
||||
ctx.show_text("Press")
|
||||
# Einheit
|
||||
ctx.set_font_size(24)
|
||||
ctx.move_to(20, 90)
|
||||
ctx.show_text("Deg C")
|
||||
ctx.move_to(20, 180)
|
||||
ctx.show_text("%")
|
||||
ctx.move_to(20, 270)
|
||||
ctx.show_text("hPa")
|
||||
|
||||
# Meßwerte
|
||||
ctx.select_font_face("DSEG7 Classic")
|
||||
ctx.set_font_size(60)
|
||||
# Temperatur °C
|
||||
ctx.move_to(180, 90)
|
||||
ctx.show_text("{:.1f}".format(self.bd.temp_air))
|
||||
# Feuchte %
|
||||
ctx.move_to(180, 180)
|
||||
ctx.show_text("{}".format(int(self.bd.humidity)))
|
||||
# Luftdruck hPa
|
||||
ctx.move_to(180, 270)
|
||||
ctx.show_text("{}".format(int(self.bd.pressure)))
|
|
@ -0,0 +1,188 @@
|
|||
"""
|
||||
|
||||
Uhr
|
||||
|
||||
TODO: Zeitzone anzeigen. Abhängig von Lat, Lon
|
||||
|
||||
Es sollen verschiedene Modi unterstützt werden
|
||||
- Analoguhr
|
||||
- Digitaluhr
|
||||
- Regattauhr / -timer
|
||||
|
||||
"""
|
||||
|
||||
import cairo
|
||||
import math
|
||||
from .page import Page
|
||||
from datetime import datetime
|
||||
import astral
|
||||
|
||||
class Clock(Page):
|
||||
|
||||
def __init__(self, pageno, cfg, boatdata):
|
||||
super().__init__(pageno, cfg, boatdata)
|
||||
self.buttonlabel[1] = 'MODE'
|
||||
self.buttonlabel[2] = 'TZ'
|
||||
self.mode = ('A', 'D', 'T') # (A)nalog (D)igital (T)imer
|
||||
self.modeindex = 1
|
||||
self.utc = True
|
||||
self.location = astral.Location(('Norderstedt', 'Germany', 53.710105, 10.0574378, 'UTC'))
|
||||
self.location.astral = astral.Astral()
|
||||
|
||||
def handle_key(self, buttonid):
|
||||
if buttonid == 1:
|
||||
if self.modeindex < len(self.mode):
|
||||
self.modeindex += 1
|
||||
else:
|
||||
self.modeindex = 0
|
||||
return True
|
||||
if buttonid == 2:
|
||||
self.utc = not self.utc
|
||||
return True
|
||||
return False
|
||||
|
||||
def draw(self, ctx):
|
||||
ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
|
||||
|
||||
# akuellen Modus anzeigen
|
||||
if mode[modeindex] == 'A':
|
||||
self.draw_analog(ctx)
|
||||
if mode[modeindex] == 'D':
|
||||
self.draw_digital(ctx)
|
||||
else:
|
||||
self.draw_timer(ctx)
|
||||
|
||||
def draw_digital(self, ctx):
|
||||
ctx.set_font_size(24)
|
||||
ctx.move_to(10, 220)
|
||||
ctx.show_text("Digital clock")
|
||||
|
||||
def draw_timer(self, ctx):
|
||||
ctx.set_font_size(24)
|
||||
ctx.move_to(10, 220)
|
||||
ctx.show_text("Timer")
|
||||
|
||||
def draw_analog(self, ctx):
|
||||
|
||||
ts = datetime.now()
|
||||
sunrise = self.location.sunrise(ts)
|
||||
sunset = self.location.sunset(ts)
|
||||
#print(sunrise)
|
||||
#print(sunset)
|
||||
|
||||
# Datum und Uhrzeit
|
||||
# Sonnenaufgang
|
||||
# Sonnenuntergang
|
||||
|
||||
# Wochentag
|
||||
# ts.strftime('%a')
|
||||
|
||||
# Werte in den Ecken der Uhr
|
||||
ctx.set_font_size(24)
|
||||
ctx.move_to(10, 220)
|
||||
ctx.show_text("Time")
|
||||
ctx.move_to(10, 95)
|
||||
ctx.show_text("Date")
|
||||
ctx.move_to(335, 95)
|
||||
ctx.show_text("SunR")
|
||||
ctx.move_to(335, 220)
|
||||
ctx.show_text("SunS")
|
||||
ctx.stroke()
|
||||
|
||||
ctx.set_font_size(16)
|
||||
|
||||
ctx.move_to(10, 65)
|
||||
ctx.show_text(ts.strftime("%d.%m.%Y"))
|
||||
ctx.move_to(10, 250)
|
||||
ctx.show_text(ts.strftime("%H:%M"))
|
||||
ctx.move_to(335, 65)
|
||||
ctx.show_text(sunrise.strftime("%H:%M"))
|
||||
ctx.move_to(335, 250)
|
||||
ctx.show_text(sunset.strftime("%H:%M"))
|
||||
ctx.stroke()
|
||||
|
||||
# Horizontal separators
|
||||
ctx.rectangle(0, 149, 60, 3)
|
||||
ctx.rectangle(340, 149, 60, 3)
|
||||
ctx.fill()
|
||||
|
||||
# Uhr
|
||||
cx = 200
|
||||
cy = 150
|
||||
r = 110
|
||||
ctx.arc(cx, cy, r + 10, 0, 2*math.pi)
|
||||
ctx.arc(cx, cy, r + 7, 0, 2*math.pi)
|
||||
ctx.stroke()
|
||||
|
||||
ctx.set_font_size(20)
|
||||
self.draw_text_center(ctx, cx, cy-40, 'UTC' if self.utc else 'LOT')
|
||||
|
||||
ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
|
||||
ctx.set_font_size(24)
|
||||
|
||||
for i in range(360):
|
||||
x = cx + (r - 30) * math.sin(i/180*math.pi)
|
||||
y = cy - (r - 30) * math.cos(i/180*math.pi)
|
||||
|
||||
char = ""
|
||||
if i == 0:
|
||||
char = "12"
|
||||
elif i == 90:
|
||||
char = "3"
|
||||
elif i == 180:
|
||||
char = "6"
|
||||
elif i == 270:
|
||||
char = "9"
|
||||
|
||||
if i % 90 == 0:
|
||||
#ctx.move_to(x, y)
|
||||
self.draw_text_center(ctx, x, y, char)
|
||||
#ctx.stroke()
|
||||
|
||||
ctx.set_line_width(3.0)
|
||||
if i % 6 == 0:
|
||||
if i % 30 == 0:
|
||||
x0 = cx + (r - 10) * math.sin(i/180*math.pi)
|
||||
y0 = cy - (r - 10) * math.cos(i/180*math.pi)
|
||||
x1 = cx + (r + 10) * math.sin(i/180*math.pi)
|
||||
y1 = cy - (r + 10) * math.cos(i/180*math.pi)
|
||||
ctx.move_to(x0, y0)
|
||||
ctx.line_to(x1, y1)
|
||||
ctx.stroke()
|
||||
else:
|
||||
x = cx + r * math.sin(i/180*math.pi)
|
||||
y = cy - r * math.cos(i/180*math.pi)
|
||||
ctx.arc(x, y, 2, 0, 2*math.pi)
|
||||
ctx.fill()
|
||||
|
||||
# Stundenzeiger
|
||||
p = ((cx - 2, cy - (r - 50)), (cx + 2, cy - (r - 50)), (cx + 6, cy + 16), (cx - 6, cy + 16))
|
||||
angle_h = (ts.hour % 12 + ts.minute / 60) * 30
|
||||
zeiger = self.rotate((cx, cy), p, angle_h)
|
||||
ctx.move_to(*zeiger[0])
|
||||
for point in zeiger[1:]:
|
||||
ctx.line_to(*point)
|
||||
ctx.fill()
|
||||
|
||||
# Minutenzeiger
|
||||
p = ((cx - 1, cy - (r - 15)), (cx + 1, cy - (r - 15)), (cx + 6, cy + 20), (cx - 6, cy + 20))
|
||||
angle_m = ts.minute * 6
|
||||
zeiger = self.rotate((cx, cy), p, angle_m)
|
||||
ctx.move_to(*zeiger[0])
|
||||
for point in zeiger[1:]:
|
||||
ctx.line_to(*point)
|
||||
ctx.fill()
|
||||
|
||||
# Zentraler Kreis
|
||||
ctx.set_source_rgb(0, 0, 0)
|
||||
ctx.arc(cx, cy, 12, 0, 2*math.pi)
|
||||
ctx.fill()
|
||||
|
||||
# Wozu dieses?
|
||||
ctx.set_source_rgb(0.86, 0.86, 0.86)
|
||||
ctx.arc(cx, cy, 10, 0, 2*math.pi)
|
||||
ctx.fill()
|
||||
|
||||
ctx.set_source_rgb(0, 0, 0)
|
||||
ctx.arc(cx, cy, 2, 0, 2*math.pi)
|
||||
ctx.fill()
|
|
@ -0,0 +1,40 @@
|
|||
import cairo
|
||||
from .page import Page
|
||||
|
||||
class DST810(Page):
|
||||
|
||||
# DBT, STW, Log, WTemp
|
||||
|
||||
def draw(self, ctx):
|
||||
|
||||
# Layout
|
||||
ctx.rectangle(0, 105, 400, 3)
|
||||
ctx.rectangle(0, 195, 400, 3)
|
||||
ctx.rectangle(200, 195, 3, 75)
|
||||
ctx.fill()
|
||||
|
||||
ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
|
||||
ctx.set_font_size(40)
|
||||
|
||||
# titel
|
||||
ctx.move_to(20, 55)
|
||||
ctx.show_text("Depth")
|
||||
ctx.move_to(20, 145)
|
||||
ctx.show_text("Speed")
|
||||
ctx.move_to(20, 220)
|
||||
ctx.show_text("Log")
|
||||
ctx.move_to(220, 220)
|
||||
ctx.show_text("Temp")
|
||||
|
||||
# Einheiten
|
||||
ctx.set_font_size(24)
|
||||
ctx.move_to(20, 90)
|
||||
ctx.show_text("m")
|
||||
|
||||
# Meßwerte
|
||||
ctx.select_font_face("DSEG7 Classic")
|
||||
ctx.set_font_size(60)
|
||||
|
||||
ctx.move_to(180, 90)
|
||||
ctx.show_text("m")
|
||||
ctx.set_font_size(40)
|
|
@ -0,0 +1,58 @@
|
|||
"""
|
||||
|
||||
XY-Graphik der Abgastemperatur
|
||||
|
||||
"""
|
||||
|
||||
import cairo
|
||||
import math
|
||||
from .page import Page
|
||||
|
||||
class Exhaust(Page):
|
||||
|
||||
def draw(self, ctx):
|
||||
# Title
|
||||
ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
|
||||
ctx.set_font_size(24)
|
||||
ctx.move_to(10, 45)
|
||||
ctx.show_text("Exhaust Temperature")
|
||||
|
||||
# Graph anzeigen X/Y
|
||||
|
||||
x0 = 55
|
||||
y0 = 255
|
||||
w = 300
|
||||
h = 200
|
||||
|
||||
# X-Achse
|
||||
ctx.move_to(x0 - 20, y0)
|
||||
ctx.line_to(x0 + w, y0)
|
||||
# Y-Achse
|
||||
ctx.move_to(x0, y0 + 20)
|
||||
ctx.line_to(x0, y0 - h)
|
||||
ctx.stroke()
|
||||
# Pfeispitze X
|
||||
ctx.move_to(x0-4, y0 - h + 12)
|
||||
ctx.line_to(x0, y0 - h)
|
||||
ctx.line_to(x0 + 4, y0 - h + 12)
|
||||
ctx.fill()
|
||||
# Pfeilspitze Y
|
||||
ctx.move_to(x0 + w -12, y0 - 4)
|
||||
ctx.line_to(x0 + w, y0)
|
||||
ctx.line_to(x0 + w - 12, y0 + 4)
|
||||
ctx.fill()
|
||||
|
||||
# Achsenbeschriftung
|
||||
ctx.set_font_size(16)
|
||||
ctx.move_to(x0 - 30, y0 - h + 20)
|
||||
ctx.show_text("°C")
|
||||
ctx.move_to(x0 + w - 10, y0 + 15)
|
||||
ctx.show_text("min")
|
||||
|
||||
# Hier wird eine Reihe von Meßwerten erwartet
|
||||
# Aufgrund min und max kann die Y-Achse skaliert werden
|
||||
# Die X-Achse ist die Zeit
|
||||
self.draw_text_center(ctx, x0 - 30, y0 - h / 2, "Temperature", True, False)
|
||||
|
||||
# Einzeichnen von zwei Warnschwellen als horizontale
|
||||
# Linie (gestrichelt)
|
|
@ -0,0 +1,137 @@
|
|||
"""
|
||||
|
||||
Füllstandsanzeige Tank
|
||||
|
||||
0: "Fuel",
|
||||
1: "Water",
|
||||
2: "Gray Water",
|
||||
3: "Live Well",
|
||||
4: "Oil",
|
||||
5: "Black Water",
|
||||
6: "Fuel Gasoline",
|
||||
14: "Error",
|
||||
15: "Unavailable"
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
import cairo
|
||||
import math
|
||||
from .page import Page
|
||||
import nmea2000.lookup
|
||||
|
||||
class Fluid(Page):
|
||||
|
||||
def __init__(self, pageno, cfg, boatdata, fluidtype):
|
||||
super().__init__(pageno, cfg, boatdata)
|
||||
self.fluidtype = int(fluidtype)
|
||||
if self.fluidtype == 0:
|
||||
self.symbol = cairo.ImageSurface.create_from_png(os.path.join(cfg['imgpath'], "fuelpump.png"))
|
||||
else:
|
||||
self.symbol = None
|
||||
|
||||
def draw(self, ctx):
|
||||
|
||||
# Zentrum Instrument
|
||||
cx = 200
|
||||
cy = 150
|
||||
# Radius
|
||||
r = 110
|
||||
|
||||
# Füllstand von 0 - 100%
|
||||
# 0 = -120°, 100 = +120°
|
||||
level = self.bd.tank[0].volume or 0
|
||||
angle = -120 + level * 2.4
|
||||
|
||||
# Rahmen
|
||||
ctx.set_source_rgb(*self.fgcolor)
|
||||
ctx.set_line_width(3)
|
||||
ctx.arc(cx, cy, r, 0, 2*math.pi)
|
||||
ctx.stroke()
|
||||
|
||||
# Symbol, sofern vorhanden
|
||||
if self.symbol:
|
||||
ctx.save()
|
||||
ctx.set_source_surface(self.symbol, cx - 8, cy - 50)
|
||||
ctx.paint()
|
||||
ctx.restore()
|
||||
|
||||
# Fluidtype
|
||||
ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
|
||||
ctx.set_font_size(24)
|
||||
ctx.move_to(20, 60)
|
||||
ctx.show_text(nmea2000.lookup.fluidtype[self.fluidtype])
|
||||
ctx.stroke()
|
||||
|
||||
# Zeigerrahmen im Zentrum
|
||||
ctx.arc(cx, cy, 8, 0, 2*math.pi)
|
||||
ctx.stroke()
|
||||
|
||||
# Zeiger in Nullposition
|
||||
# Variante 1, einseitig
|
||||
#p = ((cx - 1, cy - (r - 20)), (cx + 1, cy - (r - 20)), (cx + 4, cy), (cx - 4, cy))
|
||||
# Variante 2, überstehend
|
||||
p = ((cx - 1, cy - (r - 20)), (cx + 1, cy - (r - 20)), (cx + 6, cy + 15), (cx - 6, cy + 15))
|
||||
# Zeiger für aktuellen Meßwert
|
||||
zeiger = self.rotate((cx, cy), p, angle)
|
||||
|
||||
# Zeiger zeichnen
|
||||
ctx.move_to(*zeiger[0])
|
||||
for point in zeiger[1:]:
|
||||
ctx.line_to(*point)
|
||||
ctx.fill()
|
||||
|
||||
# Lösche das Zentrum heraus
|
||||
ctx.set_source_rgb(*self.bgcolor)
|
||||
ctx.arc(cx, cy, 6, 0, 2*math.pi)
|
||||
ctx.fill()
|
||||
|
||||
ctx.set_source_rgb(*self.fgcolor)
|
||||
|
||||
# Simple Skala direkt zeichnen
|
||||
# 50%-Wert oben in der Mitte
|
||||
|
||||
ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
|
||||
ctx.set_font_size(16)
|
||||
|
||||
ctx.move_to(cx, cy -r)
|
||||
ctx.line_to(cx, cy -r + 16)
|
||||
# linker Anschlag 0%
|
||||
self.draw_text_center(ctx, cx, cy - r + 30, "1/2")
|
||||
l = self.rotate((cx, cy), ((cx, cy - r + 16), (cx, cy - r)), -120)
|
||||
ctx.move_to(*l[0])
|
||||
ctx.line_to(*l[1])
|
||||
# rechter Anschlag 100%
|
||||
l = self.rotate((cx, cy), ((cx, cy - r + 16), (cx, cy - r)), 120)
|
||||
ctx.move_to(*l[0])
|
||||
ctx.line_to(*l[1])
|
||||
|
||||
# 25%
|
||||
l = self.rotate((cx, cy), ((cx, cy - r + 16), (cx, cy - r)), -60)
|
||||
ctx.move_to(*l[0])
|
||||
ctx.line_to(*l[1])
|
||||
tx, ty = self.rotate((cx, cy), ((cx, cy - r + 30),), -60)[0]
|
||||
self.draw_text_center(ctx, tx, ty, "1/4")
|
||||
# 75%
|
||||
l = self.rotate((cx, cy), ((cx, cy - r + 16), (cx, cy - r)), 60)
|
||||
ctx.move_to(*l[0])
|
||||
ctx.line_to(*l[1])
|
||||
tx, ty = self.rotate((cx, cy), ((cx, cy - r + 30),), 60)[0]
|
||||
self.draw_text_center(ctx, tx, ty, "3/4")
|
||||
|
||||
ctx.set_font_size(24)
|
||||
tx, ty = self.rotate((cx, cy), ((cx, cy - r + 30),), -130)[0]
|
||||
self.draw_text_center(ctx, tx, ty, "E")
|
||||
tx, ty = self.rotate((cx, cy), ((cx, cy - r + 30),), 130)[0]
|
||||
self.draw_text_center(ctx, tx, ty, "F")
|
||||
ctx.stroke()
|
||||
|
||||
self.draw_text_center(ctx, cx, cy + r - 20, f"{level:.0f}%")
|
||||
ctx.stroke()
|
||||
|
||||
# Skalenpunkte
|
||||
# Alle 5% ein Punkt aber nur da wo noch kein Strich ist
|
||||
for angle in [x for x in range(-120, 120, 12) if x not in (-120, -60, 0, 60, 120)]:
|
||||
x, y = self.rotate((cx, cy), ((cx, cy - r + 10),), angle)[0]
|
||||
ctx.arc(x, y, 2, 0, 2*math.pi)
|
||||
ctx.fill()
|
|
@ -0,0 +1,75 @@
|
|||
"""
|
||||
|
||||
Vier frei wählbare Meßwerte
|
||||
|
||||
Layout
|
||||
+--------------------+
|
||||
| 1 |
|
||||
+--------------------+
|
||||
| 2 |
|
||||
+--------------------+
|
||||
| 3 |
|
||||
+--------------------+
|
||||
| 4 |
|
||||
+--------------------+
|
||||
|
||||
"""
|
||||
|
||||
import cairo
|
||||
from .page import Page
|
||||
|
||||
class FourValues(Page):
|
||||
|
||||
def __init__(self, pageno, cfg, boatdata, boatvalue1, boatvalue2, boatvalue3, boatvalue4):
|
||||
super().__init__(pageno, cfg, boatdata)
|
||||
self.value1 = boatvalue1
|
||||
self.value2 = boatvalue2
|
||||
self.value3 = boatvalue3
|
||||
self.value4 = boatvalue4
|
||||
|
||||
def draw(self, ctx):
|
||||
# Seitenunterteilung
|
||||
ctx.rectangle(0, 80, 400, 3)
|
||||
ctx.rectangle(0, 146, 400, 3)
|
||||
ctx.rectangle(0, 214, 400, 3)
|
||||
ctx.fill()
|
||||
|
||||
#
|
||||
|
||||
ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
|
||||
ctx.set_font_size(32)
|
||||
ctx.move_to(20, 45)
|
||||
ctx.show_text("AWA")
|
||||
ctx.move_to(20, 113)
|
||||
ctx.show_text("AWS")
|
||||
ctx.move_to(20, 181)
|
||||
ctx.show_text("COG")
|
||||
ctx.move_to(20, 249)
|
||||
ctx.show_text("STW")
|
||||
ctx.stroke()
|
||||
|
||||
# Units
|
||||
ctx.set_font_size(16)
|
||||
ctx.move_to(20, 65)
|
||||
ctx.show_text("Deg")
|
||||
ctx.move_to(20, 133)
|
||||
ctx.show_text("kn")
|
||||
ctx.move_to(20, 201)
|
||||
ctx.show_text("Deg")
|
||||
ctx.move_to(20, 269)
|
||||
ctx.show_text("kn")
|
||||
ctx.stroke()
|
||||
|
||||
# Meßwerte
|
||||
ctx.select_font_face("DSEG7 Classic")
|
||||
ctx.set_font_size(40)
|
||||
|
||||
ctx.move_to(180, 65)
|
||||
ctx.show_text("150")
|
||||
ctx.move_to(180, 133)
|
||||
ctx.show_text("25.3")
|
||||
ctx.move_to(180, 201)
|
||||
ctx.show_text("146")
|
||||
ctx.move_to(180, 269)
|
||||
ctx.show_text("56.4")
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
"""
|
||||
|
||||
Vier frei auswählbare Meßwerte
|
||||
|
||||
Layout
|
||||
+--------------------+
|
||||
| 1 |
|
||||
+--------------------+
|
||||
| 2 |
|
||||
+--------------------+
|
||||
| 3 | 4 |
|
||||
+--------------------+
|
||||
|
||||
"""
|
||||
|
||||
import cairo
|
||||
from .page import Page
|
||||
|
||||
class FourValues2(Page):
|
||||
|
||||
def __init__(self, pageno, cfg, boatdata, boatvalue1, boatvalue2, boatvalue3, boatvalue4):
|
||||
super().__init__(pageno, cfg, boatdata)
|
||||
self.value1 = boatvalue1
|
||||
self.value2 = boatvalue2
|
||||
self.value3 = boatvalue3
|
||||
self.value4 = boatvalue4
|
||||
|
||||
def draw(self, ctx):
|
||||
|
||||
# Seitenunterteilung
|
||||
ctx.rectangle(0, 105, 400, 3)
|
||||
ctx.rectangle(0, 195, 400, 3)
|
||||
ctx.rectangle(200, 195, 3, 75)
|
||||
ctx.fill()
|
||||
|
||||
ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
|
||||
|
||||
# Titel
|
||||
ctx.set_font_size(40)
|
||||
ctx.move_to(20, 55)
|
||||
ctx.show_text("AWA")
|
||||
ctx.move_to(20, 145)
|
||||
ctx.show_text("AWS")
|
||||
|
||||
ctx.set_font_size(24)
|
||||
ctx.move_to(20, 220)
|
||||
ctx.show_text("COG")
|
||||
ctx.move_to(220, 220)
|
||||
ctx.show_text("STW")
|
||||
|
||||
# Einheiten
|
||||
ctx.set_font_size(16)
|
||||
ctx.move_to(20, 90)
|
||||
ctx.show_text("Deg")
|
||||
ctx.move_to(20, 180)
|
||||
ctx.show_text("kn")
|
||||
|
||||
ctx.set_font_size(16)
|
||||
ctx.move_to(20, 240)
|
||||
ctx.show_text("Deg")
|
||||
ctx.move_to(220, 240)
|
||||
ctx.show_text("kn")
|
||||
|
||||
# Meßwerte
|
||||
ctx.select_font_face("DSEG7 Classic")
|
||||
ctx.set_font_size(60)
|
||||
ctx.move_to(180, 90)
|
||||
ctx.show_text("150")
|
||||
ctx.move_to(180, 180)
|
||||
ctx.show_text("33.0")
|
||||
|
||||
ctx.set_font_size(40)
|
||||
ctx.move_to(80, 270)
|
||||
ctx.show_text("146")
|
||||
ctx.move_to(280, 270)
|
||||
ctx.show_text("50.5")
|
||||
ctx.stroke()
|
|
@ -0,0 +1,44 @@
|
|||
"""
|
||||
WIP Mangels Generator keine Überprüfung möglich
|
||||
Dies ist im Prinzip ein Platzhalter
|
||||
|
||||
"""
|
||||
|
||||
import cairo
|
||||
from .page import Page
|
||||
import math
|
||||
|
||||
class Generator(Page):
|
||||
|
||||
def draw_generator(self, ctx, x, y, r):
|
||||
ctx.set_line_width(4.0)
|
||||
ctx.arc(x, y, r)
|
||||
ctx.set_font_size(60)
|
||||
self.draw_text_center(ctx, x, y, "G")
|
||||
|
||||
def draw(self, ctx):
|
||||
# Name
|
||||
ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
|
||||
ctx.set_font_size(40)
|
||||
ctx.move_to(10, 65)
|
||||
ctx.show_text("Power")
|
||||
ctx.move_to(12, 82)
|
||||
ctx.show_text("Generator")
|
||||
|
||||
# Voltage type
|
||||
ctx.select_font_face("DSEG7 Classic")
|
||||
ctx.move_to(10, 140)
|
||||
# 12 or 24
|
||||
ctx.show_text("12V")
|
||||
|
||||
# Generator power
|
||||
# kW or W
|
||||
|
||||
# Show load level in percent
|
||||
|
||||
# Show sensor type info
|
||||
# INA219, INA226
|
||||
|
||||
# Current, A
|
||||
|
||||
# Consumption, W
|
|
@ -0,0 +1,114 @@
|
|||
"""
|
||||
|
||||
Rotationssensor AS5600 mit Funktion "Kiel"
|
||||
WIP
|
||||
|
||||
Idee:
|
||||
- Zusätzlich Anzeigemöglichkeit für die Tiefe eines variablen Kiels
|
||||
- Mode-Taste
|
||||
|
||||
"""
|
||||
|
||||
import cairo
|
||||
import math
|
||||
from .page import Page
|
||||
|
||||
class Keel(Page):
|
||||
|
||||
def __init__(self, pageno, cfg, boatdata):
|
||||
super().__init__(pageno, cfg, boatdata)
|
||||
# Wert für Kielrotation
|
||||
self.valref = self.bd.getRef("xdrRotK")
|
||||
|
||||
def draw(self, ctx):
|
||||
|
||||
# Mitte oben Instrument (Halbkreis)
|
||||
cx = 200
|
||||
cy = 150
|
||||
|
||||
# Radius Kielposition
|
||||
r = 110
|
||||
|
||||
# Titel
|
||||
ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
|
||||
ctx.set_font_size(32)
|
||||
ctx.move_to(100, 70)
|
||||
ctx.show_text("Keel Position")
|
||||
ctx.set_font_size(24)
|
||||
ctx.move_to(175, 110)
|
||||
ctx.show_text(self.valref.unit)
|
||||
ctx.stroke()
|
||||
|
||||
|
||||
# Halbkreis für Skala
|
||||
ctx.set_source_rgb(*self.fgcolor)
|
||||
ctx.set_line_width(3)
|
||||
ctx.arc(cx, cy, r + 10, 0, math.pi)
|
||||
ctx.stroke()
|
||||
|
||||
# Skala mit Strichen, Punkten und Beschriftung
|
||||
char = {
|
||||
90: "45",
|
||||
120: "30",
|
||||
150: "15",
|
||||
180: "0",
|
||||
210: "15",
|
||||
240: "30",
|
||||
270: "45"
|
||||
}
|
||||
# Zeichnen in 10°-Schritten
|
||||
ctx.set_font_size(16)
|
||||
for i in range(90, 271, 10):
|
||||
fx = math.sin(i / 180 * math.pi)
|
||||
fy = math.cos(i / 180 * math.pi)
|
||||
if i in char:
|
||||
x = cx + (r - 30) * fx
|
||||
y = cy - (r - 30) * fy
|
||||
self.draw_text_center(ctx, x, y, char[i])
|
||||
ctx.stroke()
|
||||
if i % 30 == 0:
|
||||
ctx.move_to(cx + (r - 10) * fx, cy - (r - 10) * fy)
|
||||
ctx.line_to(cx + (r + 10) * fx, cy - (r + 10) * fy)
|
||||
ctx.stroke()
|
||||
else:
|
||||
x = cx + r * fx
|
||||
y = cy - r * fy
|
||||
ctx.arc(x, y, 2, 0, 2*math.pi)
|
||||
ctx.fill()
|
||||
|
||||
# Boot und Wasserlinie
|
||||
ctx.arc(cx, cy - 10, 28, 0, math.pi)
|
||||
ctx.fill()
|
||||
ctx.set_line_width(4)
|
||||
ctx.move_to(150, cy)
|
||||
ctx.line_to(250, cy)
|
||||
ctx.stroke()
|
||||
|
||||
#ctx.arc(200, 150, r + 10, 0, 2*math.pi)
|
||||
#ctx.fill()
|
||||
#ctx.set_source_rgb(*self.bgcolor)
|
||||
#ctx.arc(200, 150, r + 7, 0, 2* math.pi)
|
||||
#ctx.rectangle(0, 30, 299, 122)
|
||||
#ctx.fill()
|
||||
|
||||
angle = -15
|
||||
#angle = self.valref.value
|
||||
#TODO Angle limits to +/-45°
|
||||
if angle < -45:
|
||||
angle = -45
|
||||
elif angle > 45:
|
||||
angle = 45
|
||||
angle *= 2 # stretched scale
|
||||
|
||||
# Kiel
|
||||
p = ((cx - 6, cy), (cx + 6, cy), (cx + 2, cy + r - 50), (cx - 2, cy + r - 50))
|
||||
keel = self.rotate((cx, cy), p, angle)
|
||||
ctx.move_to(*keel[0])
|
||||
for point in keel[1:]:
|
||||
ctx.line_to(*point)
|
||||
ctx.fill()
|
||||
|
||||
# Kiel-Bombe
|
||||
x, y = self.rotate((cx, cy), ((cx, cy + r -50),), angle)[0]
|
||||
ctx.arc(x, y, 5, 0, 2*math.pi)
|
||||
ctx.fill()
|
|
@ -0,0 +1,26 @@
|
|||
"""
|
||||
|
||||
Frei auswählbaren Meßwert als Graphen anzeigen
|
||||
|
||||
|
||||
YDGS01
|
||||
History Request PGN 61184
|
||||
History Data PGN 130816
|
||||
|
||||
|
||||
|
||||
"""
|
||||
|
||||
import cairo
|
||||
from .page import Page
|
||||
|
||||
class OneGraph(Page):
|
||||
|
||||
def draw(self, ctx):
|
||||
# Name
|
||||
ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
|
||||
ctx.set_font_size(60)
|
||||
ctx.move_to(20, 100)
|
||||
ctx.show_text("One Graph")
|
||||
|
||||
# Graph anzeigen X/Y
|
|
@ -0,0 +1,31 @@
|
|||
import cairo
|
||||
from .page import Page
|
||||
|
||||
class OneValue(Page):
|
||||
|
||||
def __init__(self, pageno, cfg, boatdata, boatvalue):
|
||||
super().__init__(pageno, cfg, boatdata)
|
||||
self.ref1 = self.bd.getRef(boatvalue)
|
||||
|
||||
def draw(self, ctx):
|
||||
|
||||
# Bezeichnung
|
||||
ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
|
||||
ctx.set_font_size(60)
|
||||
ctx.move_to(20, 100)
|
||||
ctx.show_text(self.ref1.valname)
|
||||
|
||||
# Einheit
|
||||
ctx.set_font_size(40)
|
||||
ctx.move_to(270, 100)
|
||||
ctx.show_text(self.ref1.unit)
|
||||
|
||||
# Meßwert
|
||||
ctx.select_font_face("DSEG7 Classic")
|
||||
ctx.set_font_size(100)
|
||||
ctx.move_to(40, 240)
|
||||
if self.ref1.value:
|
||||
ctx.show_text(self.ref1.format())
|
||||
else:
|
||||
ctx.show_text(self.placeholder)
|
||||
ctx.stroke()
|
|
@ -0,0 +1,202 @@
|
|||
"""
|
||||
|
||||
Basisklasse für alle Darstellungsseiten
|
||||
|
||||
Hinweise zu Cairo:
|
||||
Das Koordinatensystem geht von (0, 0) links oben bis (400, 300) rechts unten.
|
||||
Um exakte Pixel zu treffen müssen Koordinaten mit Offset 0.5 verwendet werden.
|
||||
|
||||
"""
|
||||
import os
|
||||
import cairo
|
||||
import math
|
||||
from datetime import datetime
|
||||
|
||||
class Page():
|
||||
|
||||
pageno = 1 # Nummer der aktuell sichtbaren Seite
|
||||
backlight = False
|
||||
color_normal = "dcdcdc" # Standardhintergrund
|
||||
color_lighted = "d89090" # Hintergrund im Nachtmodus
|
||||
|
||||
bgcolor = (0.86, 0.86, 0.86)
|
||||
fgcolor = (0, 0, 0)
|
||||
|
||||
@staticmethod
|
||||
def hexcolor(hexstr):
|
||||
if (len(hexstr) != 6) or (not all(c.lower in '0123456789abcdef' for c in hexstr)):
|
||||
raise ValueError('Not a valid RGB Hexstring')
|
||||
else:
|
||||
return(int(hexstr[0:2], 16) / 255.0,
|
||||
int(hexstr[2:4], 16) / 255.0,
|
||||
int(hexstr[4:6], 16) / 255.0)
|
||||
|
||||
@staticmethod
|
||||
def rotate (origin, points, angle):
|
||||
# operates on tuples, angle in degrees
|
||||
ox, oy = origin
|
||||
phi = math.radians(angle)
|
||||
fs = math.sin(phi)
|
||||
fc = math.cos(phi)
|
||||
rotated = []
|
||||
for x, y in points:
|
||||
dx = x - ox
|
||||
dy = y - oy
|
||||
rotated.append((ox + fc * dx - fs * dy, oy + fs * dx + fc * dy))
|
||||
return rotated
|
||||
|
||||
def __init__(self, pageno, cfg, boatdata):
|
||||
self.pageno = pageno
|
||||
self.cfg = cfg
|
||||
self.bd = boatdata
|
||||
self.header = True
|
||||
self.footer = True
|
||||
self.hbled = False # Heartbeat LED
|
||||
self.hbfreq = 1000 # Heartbeat Frequenz in ms
|
||||
self.keylock = False
|
||||
self.icon = {}
|
||||
self.icon['PREV'] = cairo.ImageSurface.create_from_png(os.path.join(cfg['imgpath'], "arrow_l1.png"))
|
||||
self.icon['NEXT'] = cairo.ImageSurface.create_from_png(os.path.join(cfg['imgpath'], "arrow_r1.png"))
|
||||
self.icon['ILUM'] = cairo.ImageSurface.create_from_png(os.path.join(cfg['imgpath'], "lighton.png"))
|
||||
self.sym_lock = cairo.ImageSurface.create_from_png(os.path.join(cfg['imgpath'], "lock.png"))
|
||||
self.sym_swipe = cairo.ImageSurface.create_from_png(os.path.join(cfg['imgpath'], "swipe.png"))
|
||||
self.buttonlabel = {
|
||||
1: '',
|
||||
2: '',
|
||||
3: '#PREV',
|
||||
4: '#NEXT',
|
||||
5: '',
|
||||
6: '#ILUM'
|
||||
}
|
||||
|
||||
def handle_key(self, buttonid):
|
||||
"""
|
||||
Diese Methode sollte in der Detailseite überladen werden
|
||||
"""
|
||||
print(f"Button no. {buttonid} ignored")
|
||||
return False
|
||||
|
||||
def heartbeat(self, ctx):
|
||||
"""
|
||||
Wie ausschalten bei Seitenwechsel?
|
||||
"""
|
||||
ctx.save()
|
||||
if self.hbled:
|
||||
ctx.set_source_rgb(0, 0, 0)
|
||||
else:
|
||||
ctx.set_source_rgb(0.86, 0.86, 0.86) # 0xdcdcdc
|
||||
ctx.arc(210, 9, 6, 0, math.pi*2)
|
||||
ctx.fill()
|
||||
ctx.restore()
|
||||
self.hbled = not self.hbled
|
||||
|
||||
def draw_header(self, ctx):
|
||||
"""
|
||||
Mögliche Zeichen für aktivierte Funktionen
|
||||
AP - Accesspoint ist aktiv
|
||||
WIFI - WIFI-Client
|
||||
TCP
|
||||
N2K - NMEA2000
|
||||
183
|
||||
USB
|
||||
GPS - GPS Fix vorhanden
|
||||
# TODO Umstellung auf Symbole je 16 Pixel zum Platz sparen
|
||||
Neu: Nummer der aktiven Seite (1 - 10)
|
||||
"""
|
||||
ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
|
||||
ctx.set_font_size(16)
|
||||
ctx.move_to(0.5, 14.5)
|
||||
ctx.show_text(f"N2K GPS")
|
||||
ctx.stroke()
|
||||
|
||||
# Seitennummer neue Darstellung
|
||||
ctx.set_line_width(1)
|
||||
ctx.move_to(170.5, 1.5)
|
||||
ctx.line_to(190.5, 1.5)
|
||||
ctx.line_to(190.5, 16.5)
|
||||
ctx.line_to(170.5, 16.5)
|
||||
ctx.line_to(170.5, 1.5)
|
||||
ctx.stroke()
|
||||
self.draw_text_center(ctx, 180, 9.5, str(self.pageno))
|
||||
|
||||
# Tastenstatus
|
||||
ctx.save()
|
||||
if self.keylock:
|
||||
ctx.set_source_surface(self.sym_lock, 150, 1)
|
||||
else:
|
||||
ctx.set_source_surface(self.sym_swipe, 150, 1)
|
||||
ctx.paint()
|
||||
ctx.restore()
|
||||
|
||||
# Heartbeat
|
||||
self.heartbeat(ctx)
|
||||
|
||||
# Datum und Uhrzeit
|
||||
ctx.move_to(230, 14.5)
|
||||
ctx.show_text(datetime.today().strftime('%H:%M %Y-%m-%d LOT'))
|
||||
ctx.stroke()
|
||||
|
||||
def draw_footer(self, ctx):
|
||||
"""
|
||||
Nur Belegung der Buttons (label[1] bis label[6])
|
||||
"""
|
||||
ctx.select_font_face("AtariST8x16SystemFont")
|
||||
#ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
|
||||
ctx.set_font_size(16)
|
||||
x = (35, 101, 167, 233, 299, 365)
|
||||
y = 294
|
||||
for i in range(6):
|
||||
if len(self.buttonlabel[i+1]) > 0 :
|
||||
if self.buttonlabel[i+1][0] == "#":
|
||||
# Symbol verwenden 16x16 Pixel
|
||||
ctx.save()
|
||||
key = self.buttonlabel[i+1][1:]
|
||||
ctx.set_source_surface(self.icon[key], x[i]-8, y-13)
|
||||
ctx.paint()
|
||||
ctx.restore()
|
||||
else:
|
||||
text = "[ {} ]".format(self.buttonlabel[i+1])
|
||||
w = ctx.text_extents(text).width
|
||||
ctx.move_to(x[i] - w/2, y)
|
||||
ctx.show_text(text)
|
||||
ctx.stroke()
|
||||
|
||||
def clear(self):
|
||||
ctx.set_source_rgb(1, 1, 1)
|
||||
ctx.rectangle(0, 0, 399, 299)
|
||||
ctx.fill()
|
||||
ctx.set_source_rgb(0, 0, 0)
|
||||
|
||||
def draw_text_center(self, ctx, x, y, content, rotate=False, baseline=False, fill=False):
|
||||
ext = ctx.text_extents(content)
|
||||
if fill:
|
||||
ctx.set_source_rgb(*self.bgcolor)
|
||||
xf = x + ext.x_bearing - 2
|
||||
yf = y + ext.height / 2 + ext.y_bearing - 2
|
||||
wf = ext.width + 4
|
||||
hf = ext.height + 4
|
||||
ctx.rectangle(xf, yf, wf, hf)
|
||||
ctx.fill()
|
||||
ctx.set_source_rgb(*self.fgcolor)
|
||||
if rotate:
|
||||
if baseline:
|
||||
ctx.move_to(x - ext[3] / 2.0, y)
|
||||
else:
|
||||
ctx.move_to(x - ext[3] / 2.0, y + ext[2] / 2.0)
|
||||
ctx.save()
|
||||
ctx.rotate(1.5 * math.pi)
|
||||
ctx.show_text(content)
|
||||
ctx.restore()
|
||||
else:
|
||||
if baseline:
|
||||
ctx.move_to(x - ext[2] / 2.0, y)
|
||||
else:
|
||||
ctx.move_to(x - ext[2] / 2.0, y + ext[3] / 2.0)
|
||||
ctx.show_text(content)
|
||||
ctx.stroke()
|
||||
|
||||
def draw_text_ralign(self, ctx, x, y, content):
|
||||
ext = ctx.text_extents(content)
|
||||
ctx.move_to(x - ext[2], y)
|
||||
ctx.show_text(content)
|
||||
ctx.stroke()
|
|
@ -0,0 +1,13 @@
|
|||
import cairo
|
||||
from .page import Page
|
||||
|
||||
class RollPitch(Page):
|
||||
|
||||
def draw(self, ctx):
|
||||
# Name
|
||||
ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
|
||||
ctx.set_font_size(60)
|
||||
ctx.move_to(20, 100)
|
||||
ctx.show_text("RollPitch")
|
||||
|
||||
# Graph anzeigen X/Y
|
|
@ -0,0 +1,103 @@
|
|||
import cairo
|
||||
import math
|
||||
from .page import Page
|
||||
|
||||
class Rudder(Page):
|
||||
|
||||
def __init__(self, pageno, cfg, boatdata):
|
||||
super().__init__(pageno, cfg, boatdata)
|
||||
self.buttonlabel[1] = 'MODE'
|
||||
self.mode = 'P'
|
||||
# Werte für Ruderausschlag
|
||||
self.valpri = self.bd.getRef("RPOS") # Primäres Ruder
|
||||
self.valsec = self.bd.getRef("PRPOS") # Sekundäres Ruder
|
||||
|
||||
def handle_key(self, buttonid):
|
||||
if buttonid == 1:
|
||||
if self.mode == 'P':
|
||||
self.mode = 'S'
|
||||
else:
|
||||
self.mode = 'P'
|
||||
return True
|
||||
return False
|
||||
|
||||
def draw(self, ctx):
|
||||
|
||||
# Ruder auswählen
|
||||
if self.mode == 'P':
|
||||
valref = self.valpri
|
||||
else:
|
||||
valref = self.valsec
|
||||
|
||||
# Rotationszentrum
|
||||
cx = 200
|
||||
cy = 150
|
||||
|
||||
# Radius des Instruments
|
||||
r = 110
|
||||
|
||||
# Titel
|
||||
ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
|
||||
ctx.set_font_size(32)
|
||||
ctx.move_to(80, 70)
|
||||
ctx.show_text("Rudder Position")
|
||||
|
||||
ctx.set_font_size(24)
|
||||
if valref.valid:
|
||||
ctx.move_to(175, 110)
|
||||
ctx.show_text(valref.unit)
|
||||
else:
|
||||
ctx.move_to(110, 110)
|
||||
ctx.show_text("no data available")
|
||||
ctx.stroke()
|
||||
|
||||
# Debug
|
||||
angle = 5
|
||||
|
||||
# Rahmen
|
||||
ctx.set_source_rgb(*self.fgcolor)
|
||||
ctx.set_line_width(3)
|
||||
ctx.arc(cx, cy, r + 10, 0, math.pi)
|
||||
ctx.stroke()
|
||||
|
||||
# Zentrum
|
||||
ctx.arc(cx, cy, 8, 0, 2*math.pi)
|
||||
ctx.fill()
|
||||
|
||||
# Skala mit Strichen, Punkten und Beschriftung
|
||||
char = {
|
||||
90: "45",
|
||||
120: "30",
|
||||
150: "15",
|
||||
180: "0",
|
||||
210: "15",
|
||||
240: "30",
|
||||
270: "45"
|
||||
}
|
||||
# Zeichnen in 10°-Schritten
|
||||
ctx.set_font_size(16)
|
||||
for i in range(90, 271, 10):
|
||||
fx = math.sin(i / 180 * math.pi)
|
||||
fy = math.cos(i / 180 * math.pi)
|
||||
if i in char:
|
||||
x = cx + (r - 30) * fx
|
||||
y = cy - (r - 30) * fy
|
||||
self.draw_text_center(ctx, x, y, char[i])
|
||||
ctx.stroke()
|
||||
if i % 30 == 0:
|
||||
ctx.move_to(cx + (r - 10) * fx, cy - (r - 10) * fy)
|
||||
ctx.line_to(cx + (r + 10) * fx, cy - (r + 10) * fy)
|
||||
ctx.stroke()
|
||||
else:
|
||||
x = cx + r * fx
|
||||
y = cy - r * fy
|
||||
ctx.arc(x, y, 2, 0, 2*math.pi)
|
||||
ctx.fill()
|
||||
|
||||
if valref.valid:
|
||||
p = ((cx - 6, cy), (cx + 6, cy), (cx + 2, cy + r - 50), (cx - 2, cy + r - 50))
|
||||
rudder = self.rotate((cx, cy), p, angle)
|
||||
ctx.move_to(*rudder[0])
|
||||
for point in rudder[1:]:
|
||||
ctx.line_to(*point)
|
||||
ctx.fill()
|
|
@ -0,0 +1,85 @@
|
|||
"""
|
||||
|
||||
Satelliteninfos
|
||||
|
||||
- Sat mit Fix: ausgefüllter Kreis
|
||||
- Sat ohne fix: leerer Kreis
|
||||
- Slots für 12 SNR-Balken
|
||||
|
||||
"""
|
||||
|
||||
import cairo
|
||||
import math
|
||||
from .page import Page
|
||||
|
||||
class SkyView(Page):
|
||||
|
||||
def __init__(self, pageno, cfg, boatdata):
|
||||
super().__init__(pageno, cfg, boatdata)
|
||||
|
||||
def pol2cart(azimuth, elevation):
|
||||
'''
|
||||
Polar to Cartesian coordinates within the horizon circle.
|
||||
azimuth in radians
|
||||
x = math.sin(azimuth) * elevation * self.radius
|
||||
y = math.cos(azimuth) * elevation * self.radius
|
||||
'''
|
||||
pass
|
||||
# (x, y) = self.pol2cart(sat.az, sat.el)
|
||||
|
||||
def draw(self, ctx):
|
||||
|
||||
# Name
|
||||
ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
|
||||
#ctx.set_font_size(32)
|
||||
#self.draw_text_center(ctx, 200, 40, "Satellite Info")
|
||||
|
||||
# Spezialseite
|
||||
cx = 130
|
||||
cy = 150
|
||||
r = 125
|
||||
r1 = r / 2
|
||||
|
||||
ctx.set_line_width(1.5)
|
||||
ctx.arc(cx, cy, r, 0, 2*math.pi)
|
||||
ctx.stroke()
|
||||
ctx.arc(cx, cy, r1, 0, 2*math.pi)
|
||||
ctx.stroke()
|
||||
ctx.set_dash([4, 4], 0)
|
||||
ctx.move_to(cx, cy - r)
|
||||
ctx.line_to(cx, cy + r)
|
||||
ctx.move_to(cx - r, cy)
|
||||
ctx.line_to(cx + r, cy)
|
||||
ctx.stroke()
|
||||
ctx.set_dash([], 0)
|
||||
|
||||
# Signal/Noise Balken
|
||||
ctx.set_font_size(16)
|
||||
ctx.move_to(325, 34)
|
||||
ctx.show_text("SNR")
|
||||
ctx.stroke()
|
||||
|
||||
ctx.set_line_width(1.5)
|
||||
ctx.rectangle(270, 20, 125, 257)
|
||||
|
||||
# Beispieldaten
|
||||
ctx.set_line_width(0.5)
|
||||
for s in range(12):
|
||||
y = 30 + (s + 1) * 20
|
||||
ctx.move_to(275, y)
|
||||
ctx.show_text(f"{s:02d}")
|
||||
ctx.rectangle(305, y-12, 85, 14)
|
||||
ctx.stroke()
|
||||
|
||||
ctx.set_line_width(1.0)
|
||||
for s in self.bd.sat.values():
|
||||
x = cx + math.sin(s.azimuth) * s.elevation * r
|
||||
y = cy + math.cos(s.azimuth) * s.elevation * r
|
||||
ctx.arc(x, y, 4, 0, 2*math.pi)
|
||||
ctx.move_to(x+4, y+4)
|
||||
ctx.show_text(f"{s.prn_num}")
|
||||
ctx.stroke()
|
||||
|
||||
# Satellitenliste mit SNR-Balken sortiert nach nummer
|
||||
for prn_num in sorted(self.bd.sat):
|
||||
print(prn_num)
|
|
@ -0,0 +1,40 @@
|
|||
import cairo
|
||||
from .page import Page
|
||||
|
||||
class Solar(Page):
|
||||
|
||||
|
||||
def draw_solar(self, ctx, x, y, w, h):
|
||||
pass
|
||||
"""
|
||||
// Solar graphic with fill level
|
||||
void solarGraphic(uint x, uint y, int pcolor, int bcolor){
|
||||
// Show solar modul
|
||||
int xb = x; // X position
|
||||
int yb = y; // Y position
|
||||
int t = 4; // Line thickness
|
||||
int percent = 0;
|
||||
// Solar corpus 100x80
|
||||
int level = int((100.0 - percent) * (80-(2*t)) / 100.0);
|
||||
getdisplay().fillRect(xb, yb, 100, 80, pcolor);
|
||||
if(percent < 99){
|
||||
getdisplay().fillRect(xb+t, yb+t, 100-(2*t), level, bcolor);
|
||||
}
|
||||
// Draw horizontel lines
|
||||
getdisplay().fillRect(xb, yb+28-t, 100, t, pcolor);
|
||||
getdisplay().fillRect(xb, yb+54-t, 100, t, pcolor);
|
||||
// Draw vertical lines
|
||||
getdisplay().fillRect(xb+19+t, yb, t, 80, pcolor);
|
||||
getdisplay().fillRect(xb+39+2*t, yb, t, 80, pcolor);
|
||||
getdisplay().fillRect(xb+59+3*t, yb, t, 80, pcolor);
|
||||
|
||||
}
|
||||
"""
|
||||
|
||||
def draw(self, ctx):
|
||||
# Name
|
||||
ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
|
||||
ctx.set_font_size(60)
|
||||
ctx.move_to(20, 100)
|
||||
ctx.show_text("Solar")
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
import cairo
|
||||
from .page import Page
|
||||
import datetime
|
||||
|
||||
class System(Page):
|
||||
|
||||
def __init__(self, pageno, cfg, boatdata):
|
||||
super().__init__(pageno, cfg, boatdata)
|
||||
self.buttonlabel[1] = 'MODE'
|
||||
self.buttonlabel[2] = 'STBY'
|
||||
self.mode = ('I', 'N') # (I)nformation (N)MEA2000 Device List
|
||||
self.modeindex = 1
|
||||
self.standby = False
|
||||
|
||||
def handle_key(self, buttonid):
|
||||
if self.standby and buttonid != 1:
|
||||
return True
|
||||
if buttonid == 1:
|
||||
if self.standby:
|
||||
self.standby = False
|
||||
self.buttonlabel[1] = 'MODE'
|
||||
self.buttonlabel[2] = 'STBY'
|
||||
self.header = True
|
||||
self.footer = True
|
||||
else:
|
||||
if self.modeindex < len(self.mode):
|
||||
self.modeindex += 1
|
||||
else:
|
||||
self.modeindex = 0
|
||||
return True
|
||||
if buttonid == 2:
|
||||
self.buttonlabel[1] = None
|
||||
self.buttonlabel[2] = None
|
||||
self.header = False
|
||||
self.footer = False
|
||||
self.standby = True
|
||||
return False
|
||||
|
||||
def draw_stby(self, ctx):
|
||||
# Standby
|
||||
# TODO Kopf und Fußzeile ausschalten
|
||||
# Ein Klick auf die Mode-Taste weckt das System wieder auf
|
||||
pass
|
||||
|
||||
def draw_info(self, ctx):
|
||||
|
||||
# Bezeichnung
|
||||
ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
|
||||
ctx.set_font_size(32)
|
||||
self.draw_text_center(ctx, 200, 40 , "System Info")
|
||||
|
||||
ctx.set_font_size(16)
|
||||
|
||||
# System name
|
||||
# Software version
|
||||
|
||||
# Linke Seite
|
||||
ctx.move_to(2, 80)
|
||||
ctx.show_text("Simulation: ")
|
||||
ctx.move_to(140, 80)
|
||||
ctx.show_text('On' if self.cfg['simulation'] else 'Off')
|
||||
|
||||
ctx.move_to(2, 100)
|
||||
ctx.show_text("BME280/BMP280: ")
|
||||
ctx.move_to(140, 100)
|
||||
ctx.show_text('On' if self.cfg['bme280'] else 'Off')
|
||||
|
||||
ctx.move_to(2, 120)
|
||||
ctx.show_text("GPS: ")
|
||||
ctx.move_to(140, 120)
|
||||
ctx.show_text('On' if self.cfg['gps'] else 'Off')
|
||||
|
||||
# Rechte Seite
|
||||
ctx.move_to(202, 80)
|
||||
ctx.show_text("Wifi: ")
|
||||
ctx.move_to(340, 80)
|
||||
ctx.show_text('On')
|
||||
|
||||
ctx.move_to(202, 100)
|
||||
ctx.show_text("Buzzer: ")
|
||||
ctx.move_to(340, 100)
|
||||
ctx.show_text('60%')
|
||||
|
||||
ctx.move_to(202, 120)
|
||||
ctx.show_text("Timezone: ")
|
||||
ctx.move_to(340, 120)
|
||||
ctx.show_text(datetime.datetime.now().astimezone().tzname())
|
||||
|
||||
ctx.stroke()
|
||||
|
||||
# Geräteliste
|
||||
ctx.move_to(2, 150)
|
||||
ctx.show_text("NMEA2000 Device List")
|
||||
|
||||
ctx.set_line_width(1.5)
|
||||
ctx.rectangle(2, 155, 394, 100)
|
||||
ctx.stroke()
|
||||
|
||||
def draw_devlist(self, ctx):
|
||||
# NMEA2000 Geräteliste, Vollbild
|
||||
# scrollen mit Up/Down
|
||||
pass
|
||||
|
||||
def draw(self, ctx):
|
||||
if self.standby:
|
||||
return
|
||||
self.draw_info(ctx)
|
|
@ -0,0 +1,47 @@
|
|||
import cairo
|
||||
from .page import Page
|
||||
|
||||
class ThreeValues(Page):
|
||||
|
||||
def __init__(self, pageno, cfg, boatdata, boatvalue1, boatvalue2, boatvalue3):
|
||||
super().__init__(pageno, cfg, boatdata)
|
||||
self.ref1 = self.bd.getRef(boatvalue1)
|
||||
self.ref2 = self.bd.getRef(boatvalue2)
|
||||
self.ref3 = self.bd.getRef(boatvalue3)
|
||||
|
||||
def draw(self, ctx):
|
||||
|
||||
# Seitenlayout
|
||||
ctx.rectangle(0, 105, 400, 3)
|
||||
ctx.rectangle(0, 195, 400, 3)
|
||||
ctx.fill()
|
||||
|
||||
ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
|
||||
|
||||
# Titel
|
||||
ctx.set_font_size(40)
|
||||
ctx.move_to(20, 55)
|
||||
ctx.show_text(self.ref1.valname)
|
||||
ctx.move_to(20, 145)
|
||||
ctx.show_text(self.ref2.valname)
|
||||
ctx.move_to(20, 235)
|
||||
ctx.show_text(self.ref3.valname)
|
||||
|
||||
# Einheiten
|
||||
ctx.set_font_size(24)
|
||||
ctx.move_to(20, 90)
|
||||
ctx.show_text(self.ref1.unit)
|
||||
ctx.move_to(20, 180)
|
||||
ctx.show_text(self.ref2.unit)
|
||||
ctx.move_to(20, 270)
|
||||
ctx.show_text(self.ref3.unit)
|
||||
|
||||
ctx.select_font_face("DSEG7 Classic")
|
||||
ctx.set_font_size(60)
|
||||
ctx.move_to(180, 90)
|
||||
ctx.show_text(self.ref1.format())
|
||||
ctx.move_to(180, 180)
|
||||
ctx.show_text(self.ref2.format())
|
||||
ctx.move_to(180, 270)
|
||||
ctx.show_text(self.ref3.format())
|
||||
ctx.stroke()
|
|
@ -0,0 +1,14 @@
|
|||
import cairo
|
||||
from .page import Page
|
||||
|
||||
class TwoGraphs(Page):
|
||||
|
||||
def draw(self, ctx):
|
||||
# Name
|
||||
ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
|
||||
ctx.set_font_size(60)
|
||||
ctx.move_to(20, 100)
|
||||
ctx.show_text("Two Graphs")
|
||||
|
||||
# Zwei Graphen anzeigen X/Y nebeneinander
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
"""
|
||||
|
||||
Anzeige von zwei frei definierbaren Werten
|
||||
|
||||
Layout
|
||||
+--------------------+
|
||||
| 1 |
|
||||
+--------------------+
|
||||
| 2 |
|
||||
+--------------------+
|
||||
|
||||
"""
|
||||
|
||||
import cairo
|
||||
from .page import Page
|
||||
|
||||
class TwoValues(Page):
|
||||
|
||||
def __init__(self, pageno, cfg, boatdata, boatvalue1, boatvalue2):
|
||||
super().__init__(pageno, cfg, boatdata)
|
||||
self.ref1 = self.bd.getRef(boatvalue1)
|
||||
self.ref2 = self.bd.getRef(boatvalue2)
|
||||
#print(self.ref1.valname)
|
||||
|
||||
def draw(self, ctx):
|
||||
|
||||
# Seitenunterteilung
|
||||
ctx.rectangle(0, 145, 400, 3)
|
||||
ctx.fill()
|
||||
|
||||
# Name
|
||||
ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
|
||||
ctx.set_font_size(40)
|
||||
ctx.move_to(20, 80)
|
||||
ctx.show_text(self.ref1.valname)
|
||||
ctx.move_to(20, 190)
|
||||
ctx.show_text(self.ref2.valname)
|
||||
|
||||
# Einheit
|
||||
ctx.set_font_size(24)
|
||||
ctx.move_to(20, 130)
|
||||
ctx.show_text(self.ref1.unit)
|
||||
ctx.move_to(20, 240)
|
||||
ctx.show_text(self.ref2.unit)
|
||||
|
||||
# Meßwerte
|
||||
if type(self.ref1 == 'BoatValueGeo'):
|
||||
ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
|
||||
ctx.set_font_size(40)
|
||||
ctx.move_to(140, 100)
|
||||
else:
|
||||
ctx.select_font_face("DSEG7 Classic")
|
||||
ctx.set_font_size(84)
|
||||
ctx.move_to(180, 130)
|
||||
ctx.show_text(self.ref1.format())
|
||||
|
||||
if type(self.ref2 == 'BoatValueGeo'):
|
||||
ctx.select_font_face("Ubuntu")
|
||||
ctx.set_font_size(40)
|
||||
ctx.move_to(140, 210)
|
||||
else:
|
||||
ctx.select_font_face("DSEG7 Classic")
|
||||
ctx.set_font_size(84)
|
||||
ctx.move_to(180, 240)
|
||||
ctx.show_text(self.ref2.format())
|
||||
|
||||
ctx.stroke()
|
|
@ -0,0 +1,216 @@
|
|||
"""
|
||||
|
||||
Integrierte Spannungsmessung
|
||||
|
||||
Ideen:
|
||||
- Umschaltung Datenquelle: intern, N2K
|
||||
- Umschaltung analog / digital / Graphik
|
||||
- Historische Werte vom YD-Batteriemonitor
|
||||
|
||||
"""
|
||||
|
||||
import cairo
|
||||
import math
|
||||
from .page import Page
|
||||
|
||||
class Voltage(Page):
|
||||
|
||||
avg = (1, 10, 60, 300);
|
||||
|
||||
def __init__(self, pageno, cfg, boatdata):
|
||||
super().__init__(pageno, cfg, boatdata)
|
||||
self.trend = True
|
||||
self.mode = 'A'
|
||||
self.avgindex = 0
|
||||
self.buttonlabel[1] = 'AVG'
|
||||
self.buttonlabel[2] = 'MODE'
|
||||
self.buttonlabel[5] = 'TRD'
|
||||
self.lastvalue = self.bd.voltage.value
|
||||
|
||||
def handle_key(self, buttonid):
|
||||
if buttonid == 1:
|
||||
if self.avgindex < len(self.avg) -1:
|
||||
self.avgindex += 1
|
||||
else:
|
||||
self.avgindex = 0
|
||||
elif buttonid == 2:
|
||||
if self.mode == 'A':
|
||||
self.mode = 'D'
|
||||
else:
|
||||
self.mode = 'A'
|
||||
elif buttonid == 5:
|
||||
self.trend = not self.trend
|
||||
|
||||
def setBoatValue(self, boatvalue):
|
||||
# Einstellen welcher Wert dargestellt werden soll
|
||||
self.value1 = boatvalue
|
||||
|
||||
def draw(self, ctx):
|
||||
ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
|
||||
if self.mode == 'A':
|
||||
self.draw_analog(ctx)
|
||||
else:
|
||||
self.draw_digital(ctx)
|
||||
|
||||
def draw_analog(self, ctx):
|
||||
# TODO schönes Viertelkreis-Instrument
|
||||
# Skala von 9V bis 15V
|
||||
# d.h. 90° entsprechend unterteilen in 6 Stücke je 15°
|
||||
# Beschriftung 10, 11, 12, 13, 14
|
||||
|
||||
# Datenquelle
|
||||
ctx.set_font_size(16)
|
||||
ctx.move_to(300, 40)
|
||||
ctx.show_text("Source:")
|
||||
ctx.move_to(300, 60)
|
||||
ctx.show_text("VBat")
|
||||
|
||||
# Batterietyp
|
||||
ctx.move_to(300, 90)
|
||||
ctx.show_text("Type:")
|
||||
ctx.move_to(300, 110)
|
||||
ctx.show_text("AGM")
|
||||
|
||||
# Glättung
|
||||
ctx.move_to(300, 140)
|
||||
ctx.show_text("Avg:")
|
||||
ctx.move_to(300, 160)
|
||||
ctx.show_text("1s")
|
||||
ctx.stroke()
|
||||
|
||||
# Gleichstromsymbol
|
||||
ctx.set_font_size(32)
|
||||
ctx.move_to(20, 80)
|
||||
ctx.show_text("V")
|
||||
ctx.set_line_width(3)
|
||||
ctx.move_to(20, 86.5) # obere Linie
|
||||
ctx.line_to(40, 86.5)
|
||||
ctx.move_to(20, 91.5) # untere drei kurzen Linien
|
||||
ctx.line_to(25, 91.5)
|
||||
ctx.move_to(27, 91.5)
|
||||
ctx.line_to(33, 91.5)
|
||||
ctx.move_to(35, 91.5)
|
||||
ctx.line_to(40, 91.5)
|
||||
ctx.stroke()
|
||||
|
||||
# Kreis-segment 90°
|
||||
|
||||
# Rotationszentrum
|
||||
cx = 260
|
||||
cy = 270
|
||||
|
||||
# Radius des Instruments
|
||||
r = 240
|
||||
|
||||
ctx.set_source_rgb(*self.fgcolor)
|
||||
ctx.set_line_width(2)
|
||||
|
||||
ctx.arc(cx, cy, r, math.pi, 1.5*math.pi)
|
||||
ctx.stroke()
|
||||
|
||||
|
||||
# Beschriftung
|
||||
ctx.set_font_size(20)
|
||||
label = {285: "10", 300: "11", 315: "12", 330: "13", 345: "14"}
|
||||
for angle in label:
|
||||
x, y = self.rotate((cx, cy), ((cx, cy - r + 30),), angle)[0]
|
||||
self.draw_text_center(ctx, x, y, label[angle])
|
||||
|
||||
# grobe Skala
|
||||
p = ((cx, cy-r), (cx, cy - r + 12))
|
||||
ctx.set_line_width(2)
|
||||
for angle in label:
|
||||
line = self.rotate((cx, cy), p, angle)
|
||||
ctx.move_to(*line[0])
|
||||
ctx.line_to(*line[1])
|
||||
ctx.stroke()
|
||||
|
||||
# feine Skala
|
||||
p = ((cx, cy-r), (cx, cy - r + 5))
|
||||
ctx.set_line_width(1)
|
||||
for angle in [x for x in range(273, 360, 3)]:
|
||||
if angle in label:
|
||||
continue
|
||||
line = self.rotate((cx, cy), p, angle)
|
||||
ctx.move_to(*line[0])
|
||||
ctx.line_to(*line[1])
|
||||
ctx.stroke()
|
||||
|
||||
# Zeiger auf 0-Position
|
||||
val = float(self.bd.voltage.format())
|
||||
if not val:
|
||||
angle = -0.5
|
||||
elif val > 15:
|
||||
angle = 91
|
||||
elif val <= 9:
|
||||
angle = -0.5
|
||||
else:
|
||||
angle = (val - 9) * 15
|
||||
p = ((cx - 2, cy + 4),
|
||||
(cx - r + 35, cy + 2),
|
||||
(cx - r + 35, cy + 1),
|
||||
(cx - r + 5, cy + 1),
|
||||
(cx - r + 5, cy - 1),
|
||||
(cx - r + 35, cy - 1),
|
||||
(cx - r + 35, cy - 2),
|
||||
(cx - 2, cy - 4))
|
||||
zeiger = self.rotate((cx, cy), p, angle)
|
||||
|
||||
# Zeiger zeichnen
|
||||
ctx.set_line_width(1)
|
||||
ctx.move_to(*zeiger[0])
|
||||
for point in zeiger[1:]:
|
||||
ctx.line_to(*point)
|
||||
ctx.fill()
|
||||
|
||||
# Zeigerbasis
|
||||
ctx.arc(cx, cy, 6, 0, 2*math.pi)
|
||||
ctx.set_source_rgb(*self.bgcolor)
|
||||
ctx.fill_preserve()
|
||||
ctx.set_line_width(2)
|
||||
ctx.set_source_rgb(*self.fgcolor)
|
||||
ctx.stroke()
|
||||
|
||||
def draw_digital(self, ctx):
|
||||
# Name
|
||||
ctx.set_font_size(60)
|
||||
ctx.move_to(20, 100)
|
||||
ctx.show_text("VBat")
|
||||
# Unit
|
||||
ctx.set_font_size(40)
|
||||
ctx.move_to(270, 100)
|
||||
ctx.show_text("V")
|
||||
# Battery type
|
||||
ctx.set_font_size(16)
|
||||
ctx.move_to(294, 100)
|
||||
ctx.show_text("AGM")
|
||||
# Mittelwert
|
||||
ctx.move_to(320, 84)
|
||||
ctx.show_text("Avg: {}s".format(self.avg[self.avgindex]))
|
||||
ctx.stroke()
|
||||
|
||||
ctx.select_font_face("DSEG7 Classic")
|
||||
ctx.set_font_size(100)
|
||||
ctx.move_to(40, 240)
|
||||
ctx.show_text(self.bd.voltage.format())
|
||||
ctx.stroke()
|
||||
|
||||
# Trendanzeige
|
||||
if self.trend and self.bd.voltage.value and self.lastvalue:
|
||||
ctx.rectangle(315, 183, 35, 4)
|
||||
ctx.fill()
|
||||
size = 11
|
||||
if self.lastvalue < self.bd.voltage.value:
|
||||
ctx.move_to(320, 174)
|
||||
ctx.line_to(320+size*2, 174)
|
||||
ctx.line_to(320+size, 174-size*2)
|
||||
ctx.line_to(320, 174)
|
||||
ctx.fill()
|
||||
elif self.lastvalue > self.bd.voltage.value:
|
||||
ctx.move_to(320, 195)
|
||||
ctx.line_to(320+size*2, 195)
|
||||
ctx.line_to(320+size, 195+size*2)
|
||||
ctx.line_to(320, 195)
|
||||
ctx.fill()
|
||||
|
||||
self.lastvalue = self.bd.voltage.value
|
|
@ -0,0 +1,119 @@
|
|||
"""
|
||||
|
||||
Windrose und Windroseflex
|
||||
Benötigt 6 Werte
|
||||
Hauptwerte: AWA, AWS, TWD, TWS
|
||||
Nebenwerte: DBT, STW, oder COG, SOG
|
||||
|
||||
"""
|
||||
|
||||
import cairo
|
||||
import math
|
||||
from .page import Page
|
||||
|
||||
class WindRose(Page):
|
||||
|
||||
def draw(self, ctx):
|
||||
|
||||
# Rahmen
|
||||
cx = 200
|
||||
cy = 150
|
||||
r = 110
|
||||
|
||||
ctx.set_line_width(3)
|
||||
ctx.arc(cx, cy, r + 9, 0, 2*math.pi)
|
||||
ctx.stroke()
|
||||
ctx.arc(cx, cy, r - 11, 0, 2*math.pi)
|
||||
ctx.stroke()
|
||||
|
||||
for angle in range(0, 360, 10):
|
||||
if angle % 30 != 0:
|
||||
x, y = self.rotate((cx, cy), ((cx, cy - r),), angle)[0]
|
||||
ctx.arc(x, y, 2, 0, 2*math.pi)
|
||||
ctx.fill()
|
||||
else:
|
||||
p = ((cx, cy - r + 10), (cx, cy - r - 10), (cx, cy - r + 30))
|
||||
pr = self.rotate((cx, cy), p, angle)
|
||||
ctx.move_to(*pr[0])
|
||||
ctx.line_to(*pr[1])
|
||||
ctx.stroke()
|
||||
self.draw_text_center(ctx, *pr[2], str(angle))
|
||||
|
||||
|
||||
ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
|
||||
|
||||
# Namen
|
||||
ctx.set_font_size(24)
|
||||
ctx.move_to(10, 95) # links oben
|
||||
ctx.show_text("AWA")
|
||||
ctx.move_to(335, 95) # rechts oben
|
||||
ctx.show_text("TWD")
|
||||
ctx.move_to(10, 220) # links unten
|
||||
ctx.show_text("AWS")
|
||||
ctx.move_to(335, 220) # rechts unten
|
||||
ctx.show_text("TWS")
|
||||
|
||||
# Units
|
||||
ctx.set_font_size(16)
|
||||
ctx.move_to(10, 115) # links oben
|
||||
ctx.show_text("Deg")
|
||||
ctx.move_to(335, 115) # rechts oben
|
||||
ctx.show_text("Deg")
|
||||
ctx.move_to(10, 190) # links unten
|
||||
ctx.show_text("kn")
|
||||
ctx.move_to(335, 190) # rechts unten
|
||||
ctx.show_text("kn")
|
||||
|
||||
# Horiz. Trennlinien
|
||||
#ctx.rectangle(0, 149, 60, 3)
|
||||
#ctx.fill()
|
||||
ctx.set_line_width(3)
|
||||
# links
|
||||
ctx.move_to(0, 149)
|
||||
ctx.line_to(60, 149)
|
||||
# rechts
|
||||
ctx.move_to(340, 149)
|
||||
ctx.line_to(400, 149)
|
||||
ctx.stroke()
|
||||
|
||||
# Meßwerte
|
||||
ctx.select_font_face("DSEG7 Classic")
|
||||
ctx.set_font_size(40)
|
||||
# links oben
|
||||
ctx.move_to(10, 65)
|
||||
ctx.show_text("148")
|
||||
# rechts oben
|
||||
ctx.move_to(295, 65)
|
||||
ctx.show_text("---")
|
||||
# links unten
|
||||
ctx.move_to(10, 270)
|
||||
ctx.show_text("46.7")
|
||||
# rechts unten
|
||||
ctx.move_to(295, 270)
|
||||
ctx.show_text("77.8")
|
||||
|
||||
ctx.set_font_size(32)
|
||||
# innen oben
|
||||
ctx.move_to(160, 130)
|
||||
ctx.show_text("38.9")
|
||||
# innen unten
|
||||
ctx.move_to(160, 200)
|
||||
ctx.show_text("19.9")
|
||||
ctx.stroke()
|
||||
|
||||
|
||||
# Zeiger
|
||||
angle = 148
|
||||
p = ((cx - 1, cy - (r - 15)), (cx + 1, cy - (r - 15)), (cx + 4, cy), (cx - 4, cy))
|
||||
zeiger = self.rotate((cx, cy), p, angle)
|
||||
ctx.move_to(*zeiger[0])
|
||||
for point in zeiger[1:]:
|
||||
ctx.line_to(*point)
|
||||
ctx.fill()
|
||||
|
||||
ctx.set_source_rgb(*self.bgcolor)
|
||||
ctx.arc(cx, cy, 8, 0, 2*math.pi)
|
||||
ctx.fill()
|
||||
ctx.set_source_rgb(*self.fgcolor)
|
||||
ctx.arc(cx, cy, 7, 0, 2*math.pi)
|
||||
ctx.fill()
|