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()
 | 
			
		||||