#!/usr/bin/env python

"""
Virtuelles Multifunktionsgerät OBP60v

Zwei Displayvarianten
  1. Fliegendes OBP60 auf großem Bildschirm. 400x300 Display
  2. Fullscreen Display skaliert mit 1,6 ergibt 640x480
     mit Platz für große Touch-Flächen am rechten Rand

Das Gerät kann Daten von NMEA2000 und NMEA0183 empfangen, sowie von
einem lokal angeschlossenen GPS-Empfänger.

NMEA2000
  deviceclass: 120 - Display
  devicefunction: 130 - Display

Benötigte Pakete:
  python3-cairo python3-gi python3-gi-cairo gir1.2-rsvg-2.0
  python3-astral

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

Standardbelegung der Tasten
Button 1: Mode
Button 2: Abbruch
Button 3: zurück
Button 4: weiter
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 ]

Vergleich zum Raymarine I70s:
[ ON/ILUM ]   [ Hoch ]  [ Runter ]  [ MENU ]

Button 1 wird für AVG verwendet auf mehreren Seiten
Button 5 wird für Trend TRND verwendet


Ä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
0.3      2025        WIP                                                    tho 

"""

import os
import sys
if sys.version_info < (3, 10):
    print("Python version not compatible!")
    sys.exit(1)

import configparser
import logging, logging.handlers
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 socket
import pynmea2
import can
import serial
import smbus2
import bme280
import math
import time
from datetime import datetime
from nmea2000 import Device, BoatData, History, HistoryBuffer
from nmea2000 import parser
import struct
import uuid
import json

from appdata import AppData
from led import LED
import nmea0183
import pages

__author__ = "Thomas Hooge"
__copyright__ = "Copyleft 2024-2025, all rights reversed"
__version__ = "0.3"
__email__ = "thomas@hoogi.de"
__status__ = "Development"

# Standardkonfiguration, kann durch Konfigdatei überschrieben werden
# TODO prüfen ob defaults ziehen wenn kein Eintrag in Konfigdatei
cfg = {
    'cfgfile': 'obp60v.conf',
    'logdir': '~/.local/share/obp60v',
    'logfile': 'obp60v.log',
    'loglevel': 3,
    'imgpath': os.path.join(sys.path[0], 'images'),
    'audiopath': os.path.join(sys.path[0], 'audio'),
    'histpath': '~/.local/lib/obp60v',
    'deviceid': 100,
    'manufcode': 2046,  # Open Boat Projects (OBP)
    'devfunc': 120,     # Display
    'devclass': 120,    # Display
    'industrygroup': 4, # Marine
    'gps': False,
    'bme280': False,
    'tracker': { 'type': 'NONE' },
    'boat': { }
}

def rxd_n2k(device):
    setthreadtitle("N2Klistener")
    bus = can.Bus(interface='socketcan', channel=device, bitrate=250000);
    wip = False
    sc = 0
    nf = 0
    while not appdata.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_gps(devname, devspeed):
    # Prüfe ob GPS-Port vorhanden ist und sich öffnen läßt
    try:
        ser = serial.Serial(devname, devspeed, timeout=3)
    except serial.SerialException as e:
        log.error("GPS serial port not available")
        return
    setthreadtitle("GPSlistener")
    while not appdata.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)
        if msg.sentence_type == 'GSV':
            # Satellites in view
            # TODO
            pass
        else:
            print(msg)
            print(msg.fields)
    ser.close()

def rxd_network(address, port):
    # WIP Daten über Netzwerk empfangen
    # Wir verwenden UDP. Ein verlorenes Paket tut uns nicht weh.
    setthreadtitle("NETlistener")
    sock = socket.socket()
    sock.connect((address, port))
    while not appdata.shutdown:
        time.sleep(0.5)
    sock.close()

def datareader(cfg, 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(cfg['histpath']):
        os.makedirs(cfg['histpath'])
    history.basepath = cfg['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 appdata.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)
        n += 1

    for s in history.series.values():
        s.finish()

class Frontend(Gtk.Window):

    def __init__(self, cfg, appdata, device, boatdata, profile):
        super().__init__()
        self.appdata = appdata
        self.owndev = device
        self.boatdata = boatdata
        self._config = cfg['_config']
        self._cfgfile = cfg['cfgfile']
        self._fullscreen = cfg['guistyle'] == 'fullscreen'
        self._mouseptr = cfg['mouseptr']

        self.connect("delete-event", self.on_delete)
        self.connect("destroy", self.on_destroy)
        self.appdata.setFrontend(self)

        if self._fullscreen:
            self.fullscreen()
            #self.set_size_request(800, 480)
            # Schaltflächen am unteren Bildschirmrand sind berührbar,
            # zusätzlich gibt es die großen Flächen am rechten Rand
            self.button_round = False
            self.button = { # linke obere Ecke
                1: (640, 96),
                2: (640, 160),
                3: (640, 224),
                4: (640, 288),
                5: (640, 352),
                6: (640, 416)
            }
            self.button_w = 160
            self.button_h = 64
        else:
            if cfg['win_x'] >= 0 and cfg['win_y'] >= 0:
                self.move(cfg['win_x'], cfg['win_y'])
            self.button_round = True
            # Runde Tasten wie beim Original-Gerät
            self.button = { # Mittelpunkt
                1: (75, 485),
                2: (150, 492),
                3: (227, 496),
                4: (306, 496),
                5: (382, 492),
                6: (459, 485)
            }
            self.button_radius = 30
            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()
        if self._fullscreen:
            self._svg = handle.new_from_file(os.path.join(sys.path[0], "fullscreen.svg"))
        else:
            self._svg = handle.new_from_file(os.path.join(sys.path[0], "obp60.svg"))

        self.connect("draw", self.on_draw)

        self.fixed = Gtk.Fixed()
        self.add(self.fixed)

        # OBP GUI
        # Geräterahmen mit Buttons
        self.da_device = Gtk.DrawingArea()
        self.fixed.put(self.da_device, 0, 0)
        if self._fullscreen:
            self.da_device.set_size_request(800, 480)
        else:
            self.da_device.set_size_request(530, 555) # für fixed benötigt
        self.da_device.add_events(Gdk.EventMask.BUTTON_PRESS_MASK|Gdk.EventMask.BUTTON_RELEASE_MASK)
        self.da_device.connect("draw", self.da_device_draw)
        self.da_device.connect('button-press-event', self.da_button_press)
        self.da_device.connect('button-release-event', self.da_button_release)
        # Displayfläche
        self.da = Gtk.DrawingArea()
        if self._fullscreen:
            self.fixed.put(self.da, 0, 0)
            self.da.set_size_request(640, 480) # Faktor 1,6 zu 400x300
        else:
            self.fixed.put(self.da, 64, 95)
            self.da.set_size_request(400, 300) # für fixed benötigt
        self.da.connect("draw", self.da_draw)
        # Flash LED
        self.flashled = LED(self.fixed, self._fullscreen) # Soft Flash-LED

        self.button_clicked = 0 # Geklickter Button vor Loslassen
        self.keylock = False
        self.pages = profile
        self.pageno = 1
        self.pageno_last = 1
        self.curpage = self.pages[self.pageno]
        self.curpage.display_new()
        
        print("Wische von 2 nach 1 für Programmende")

    def on_realize(self, widget):
        if self._fullscreen and not self._mouseptr:
            # Mauszeiger ggf. ausschalten
            self.get_window().set_cursor(Gdk.Cursor(Gdk.CursorType.BLANK_CURSOR))

    def run(self):
        appdata.refreshStatus()
        GLib.timeout_add_seconds(1, self.on_timer_fast)
        GLib.timeout_add_seconds(10, self.on_timer_slow)
        self.show_all()
        Gtk.main()

    def on_timer_fast(self):
        # Boatdata validator
        boatdata.updateValid(5)
        # Tastaturstatus an Seite durchreichen
        self.curpage.keylock = self.keylock
        # Neuzeichnen
        if self._fullscreen:
            self.da_device.queue_draw()
        else:
            self.da.queue_draw()
        return True

    def on_timer_slow(self):
        appdata.refreshStatus()
        return True

    def on_draw(self, widget, ctx):
        # Fenstertransparenz
        ctx.set_source_rgba(0, 0, 0, 0)
        ctx.paint()

    def da_device_draw(self, widget, ctx):
        viewport = Rsvg.Rectangle()
        viewport.x = 0
        viewport.y = 0
        if self._fullscreen:
            viewport.width = 800
            viewport.height = 480
        else:
            viewport.width = 530
            viewport.height = 555
        self._svg.render_document(ctx, viewport)
        if self._fullscreen:
            # Seitliche Buttons zeichnen
            self.curpage.draw_sidebuttons(ctx)

    def da_draw(self, widget, ctx):
        ctx.set_source_rgb(1.0, 0, 0)
        if not self._fullscreen:
            ctx.rectangle(0, 0, 400, 300)
            ctx.clip()
        else:
            ctx.scale(1.6, 1.6)
        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 self.button_round:
            # Horizontale runde Buttons
            if (event.x < self.button[1][0] - self.button_radius or event.x > self.button[6][0] + self.button_radius):
               return True
            if (event.y < self.button[1][1] - self.button_radius or event.y > self.button[3][1] + self.button_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.button_radius:
                    self.button_clicked = b
                    break
        else:
            # Vertikale eckige Buttons
            if (event.x < self.button[1][0]) or (event.x > self.button[1][0] + self.button_w):
                return True
            if (event.y < self.button[1][1]) or (event.y > self.button[6][1] + self.button_h):
                return True
            for b, v in self.button.items():
                if event.x >= v[0] and event.x <= v[0] + self.button_w and \
                   event.y >= v[1] and event.y <= v[1] + self.button_h:
                    self.button_clicked = b
                    break
        hand_cursor = Gdk.Cursor(Gdk.CursorType.HAND2)
        widget.get_window().set_cursor(hand_cursor)
        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
        widget.get_window().set_cursor(None)
        if self.button_round:
            if (event.x < self.button[1][0] - self.button_radius or event.x > self.button[6][0] + self.button_radius):
               return True
            if (event.y < self.button[1][1] - self.button_radius or event.y > self.button[3][1] + self.button_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.button_radius:
                    selected = b
                    break
        else:
            if event.x < self.button[1][0] or event.x > self.button[6][0] + self.button_w:
                return True
            if event.y < self.button[1][1] or event.y > self.button[6][1] + self.button_h:
                return True
            selected = 0
            for b, v in self.button.items():
                if event.x >= v[0] and event.x <= v[0] + self.button_w and \
                    event.y >= v[1] and event.y <= v[1] + self.button_h:
                    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
        # Alarm! Only botton "1" so dismiss
        if self.boatdata.alarm:
            if selected == 1:
                self.boatdata.alarm = 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))
                self.on_destroy(self)
            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]
                self.curpage.display_new()
        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]
                self.curpage.display_new()
        elif selected == 5:
            # Ok/Menü
            if self.button_clicked == 4:
                # Umschalten zur Systemseite
                self.curpage = self.pages[0]
            else:
                self.curpage.handle_key(5)
        elif selected == 6:
            if not self.curpage.handle_key(6):
                # Backlight on/off
                self.curpage.backlight = not self.curpage.backlight
        return True

    def on_delete(self, widget, event):
        # Einhängepunkt für zukünftige Erweiterungen
        # Hier kann der Vorgang noch durch z.B. Benutzerinteraktion
        # abgebrochen werden
        print("Window delete event")
        return False

    def on_destroy(self, widget):
        # Letzte Fensterposition speichern
        position = self.get_position()
        if not self._config.has_section('gui'):
            config.add_section('gui')
        self._config.set('gui', 'win_x', str(position[0]))
        self._config.set('gui', 'win_y', str(position[1]))
        with open(self._cfgfile, 'w') as fh:
            self._config.write(fh)
        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 6 Werte je Seite
            values = {}
            valno = 1
            for i in (1, 2, 3, 4, 5, 6):
                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, appdata, boatdata)
    }
    for i, p in pagedef.items():
        try:
            cls = getattr(pages, p['type'])
        except AttributeError:
            # Klasse nicht vorhanden, Seite wird nicht benutzt
            log.error(f"Klasse '{p['type']}' nicht gefunden")
            continue
        c = cls(i, cfg, appdata, boatdata, *[v for v in p['values'].values()])
        clist[i] = c
    return clist

def set_loglevel(nr):
    """
    Umsetzen der Nummer auf einen für "logging" passenden Wert
    Die Nummer kann ein Wert zwischen 0 - kein Logging und 5 - Debug sein
    """
    level = (None, logging.CRITICAL, logging.ERROR, logging.WARNING,
             logging.INFO, logging.DEBUG)
    if nr > 5:
        nr = 5
    elif nr < 0:
        nr = 0
    return level[nr]

def init_logging(logdir, logfile='obp60v.log', loglevel=logging.INFO):
    global log
    os.makedirs(logdir, exist_ok=True)
    log = logging.getLogger(os.path.basename(sys.argv[0]))
    hdlr = logging.handlers.RotatingFileHandler(os.path.join(logdir, logfile), maxBytes=5242880, backupCount=5)
    formatter = logging.Formatter('%(asctime)s %(name)s %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
    hdlr.setFormatter(formatter)
    log.addHandler(hdlr)
    log.setLevel(loglevel)
    console = logging.StreamHandler()
    console.setFormatter(logging.Formatter('%(levelname)s:%(message)s'))
    console.setLevel(loglevel)
    log.addHandler(console)

if __name__ == "__main__":

    setproctitle("obp60v")

    owndevice = Device(100)
    # Hardcoding device, not intended to change
    owndevice.manufacturercode = cfg['manufcode']
    owndevice.industrygroup = cfg['industrygroup']
    owndevice.deviceclass = cfg['devclass']
    owndevice.devicefunction = cfg['devfunc']

    boatdata = BoatData()
    boatdata.addTank(0)
    boatdata.addEngine(0)

    # Basiskonfiguration aus Datei lesen
    config = configparser.ConfigParser()
    config_path = os.path.join(sys.path[0], cfg['cfgfile'])
    ret = config.read(config_path)
    if len(ret) == 0:
        print("Konfigurationsdatei '{}' konnte nicht gelesen werden!".format(cfg['cfgfile']))
        sys.exit(1)
    cfg['_config'] = config # Objekt zum späteren schreiben
    cfg['loglevel'] = config.getint('system', 'loglevel')
    cfg['deviceid'] = config.getint('system', 'deviceid')
    cfg['simulation'] = config.getboolean('system', 'simulation')
    cfg['histpath'] = os.path.expanduser(config.get('system', 'histpath'))
    cfg['guistyle'] = config.get('system', 'guistyle')
    cfg['mouseptr'] = config.getboolean('system', 'mouseptr')
    try:
        cfg['win_x'] = config.getint('gui', 'win_x')
        cfg['win_y'] = config.getint('gui', 'win_y')
    except (configparser.NoSectionError, configparser.NoOptionError):
        # Keine vorgegebene Position
        cfg['win_x'] = -1
        cfg['win_y'] = -1

    cfg['can'] = config.getboolean('can', 'enabled')
    if cfg['can']:
        cfg['can_intf'] = config.get('can', 'interface')

    cfg['nmea0183'] = config.getboolean('nmea0183', 'enabled')
    if cfg['nmea0183']:
        cfg['0183_port'] = config.get('nmea0183', 'port')

    cfg['gps'] = config.getboolean('gps', 'enabled')
    if cfg['gps']:
        cfg['gps_port'] = config.get('gps', 'port')

    cfg['navtex'] = config.getboolean('navtex', 'enabled')
    if cfg['navtex']:
        cfg['ntx_source'] = config.get('navtex', 'source') # Datenquelle: net | radio
        cfg['ntx_housekeeping'] = config.getint('navtex', 'housekeeping') # Max. Nachrichtenalter in Stunden
        cfg['ntx_refresh'] = config.getint('navtex', 'refresh') # Aktualisierung alle <n> Minuten

    cfg['network'] = config.getboolean('network', 'enabled')
    if cfg['network']:
        cfg['net_addr'] = config.get('network', 'address')
        cfg['net_port'] = config.get('network', '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")

    # Zeitzonenoffset, aus Kompatibilitätsgründen zum OBP60
    cfg['tzoffset'] = config.getint('settings', 'timezone')

    # Tracker data
    cfg['tracker']['type'] = config.get('tracker', 'type').upper()
    cfg['tracker']['host'] = config.get('tracker', 'host')
    cfg['tracker']['port'] = config.getint('tracker', 'port')
    cfg['tracker']['username'] = config.get('tracker', 'username')
    cfg['tracker']['password'] = config.get('tracker', 'password')
    cfg['tracker']['mqtt_host'] = config.get('tracker', 'mqtt_host')
    cfg['tracker']['mqtt_port'] = config.getint('tracker', 'mqtt_port')
    cfg['tracker']['mqtt_user'] = config.get('tracker', 'mqtt_user')
    cfg['tracker']['mqtt_pass'] = config.get('tracker', 'mqtt_pass')
    cfg['tracker']['logdir'] = cfg['logdir']
    cfg['tracker']['trace'] =  config.getboolean('tracker', 'trace')
    cfg['tracker']['interval'] =  config.getint('tracker', 'interval')

    # Boat data
    cfg['boat']['name'] = config.get('boat', 'name')
    cfg['boat']['sailno'] = config.get('boat', 'sailno')
    cfg['boat']['class'] = config.get('boat', 'class')
    cfg['boat']['handicap'] = config.getfloat('boat', 'handicap')
    cfg['boat']['club'] = config.get('boat', 'club')
    cfg['boat']['team'] = config.get('boat', 'team')

    # Protokollierung
    loglevel = set_loglevel(cfg['loglevel'])
    init_logging(os.path.expanduser(cfg['logdir']), cfg['logfile'], loglevel)
    log.info("Client started")
    log.info("Setting GUI style to '{}'".format(cfg['guistyle']))

    # Eindeutige Bootskennung UUID. Automatisch erzeugen wenn noch nicht vorhanden
    create_uuid = False
    try:
        cfg['boat']['uuid'] = config.get('boat', 'uuid')
    except configparser.NoOptionError:
        create_uuid = True
    if create_uuid or (len(cfg['boat']['uuid']) != 36):
        cfg['boat']['uuid'] = str(uuid.uuid4())
        config.set('boat', 'uuid', cfg['boat']['uuid'])
        with open(config_path, 'w') as fh:
            config.write(fh)
        log.info("Created new boat UUID: {}".format(cfg['boat']['uuid']))

    # Globale Daten, u.a. auch Shutdown-Indikator
    appdata = AppData(log, cfg)

    # Ggf. Simulationsdaten einschalten
    if cfg['simulation']:
        boatdata.enableSimulation()

    # Gerät initialisieren u.a. mit den genutzten Seiten
    profile = init_profile(config, cfg, boatdata)

    # Schnittstellen aktivieren, jew. eigener Thread
    if cfg['can']:
        log.info("CAN enabled")
        t_rxd_n2k = threading.Thread(target=rxd_n2k, args=(cfg['can_intf'],))
        t_rxd_n2k.start()
    if cfg['nmea0183']:
        log.info("NMEA0183 enabled, library version {}".format(pynmea2.version))
        t_rxd_0183 = threading.Thread(target=nmea0183.rxd_0183, args=(appdata,boatdata,cfg['0183_port'],))
        t_rxd_0183.start()
    if cfg['gps']:
        log.info("GPS enabled (local)")
        t_rxd_gps = threading.Thread(target=rxd_gps, args=(cfg['gps_port'],))
        t_rxd_gps.start()
    if cfg['network']:
        log.info("Networking enabled")
        t_rxd_net = threading.Thread(target=rxd_network, args=(cfg['net_port'],cfg['net_addr']))
        t_rxd_net.start()
    if cfg['tracker']['type'] != 'NONE':
        log.info(f"Tracking enabled, mode {cfg['tracker']['type']}")
        #appdata.track.set_type( cfg['tracker']['type'])
        if cfg['tracker']['type'] == 'HERO':
            t_tracker = threading.Thread(target=appdata.track.mqtt_tracker, args=(cfg['tracker'],cfg['boat'],appdata,boatdata))
            t_tracker.start()
        elif cfg['tracker']['type'] in ['LOCAL', 'SDCARD']:
            t_tracker = threading.Thread(target=appdata.track.local_tracker, args=(cfg,appdata,boatdata))
            t_tracker.start()
    if not cfg['simulation']:
        if cfg['bme280']:
            log.info("Environment sensor enabled")
            t_data = threading.Thread(target=datareader, args=(cfg, history))
            t_data.start()
    else:
        log.info("Simulation mode enabled")

    app = Frontend(cfg, appdata, owndevice, boatdata, profile)
    app.run()
    appdata.shutdown = True
    if cfg['can']:
        t_rxd_n2k.join()
    if cfg['nmea0183']:
        t_rxd_0183.join()
    if cfg['gps']:
        t_rxd_gps.join()
    if cfg['network']:
        t_rxd_net.join()
    if cfg['tracker']['type'] != 'NONE':
        t_tracker.join()
    if not cfg['simulation'] and cfg['bme280']:
        t_data.join()

    log.info("Client terminated")
    print(boatdata)
    print("Another fine product of the Sirius Cybernetics Corporation.")
