diff --git a/appdata.py b/appdata.py index 6570c5f..61a1276 100644 --- a/appdata.py +++ b/appdata.py @@ -8,9 +8,10 @@ from tracker import Tracker class AppData(): - def __init__(self): + def __init__(self, logger, cfg): self.shutdown = False # Globaler Ausschalter - self.track = Tracker('NONE') + self.log = logger + self.track = Tracker(logger, cfg) self.frontend = None self.bv_lat = None self.bv_lon = None diff --git a/images/flags/foxtrot.png b/images/flags/foxtrot.png new file mode 100644 index 0000000..ed743ee Binary files /dev/null and b/images/flags/foxtrot.png differ diff --git a/obp60v.conf-sample b/obp60v.conf-sample index d5022a9..e10d60c 100644 --- a/obp60v.conf-sample +++ b/obp60v.conf-sample @@ -40,16 +40,20 @@ config = ~/.opencpn/opencpn.conf [tracker] type = NONE host = 127.0.0.1 -port = 1883 +port = 80 +ssl = false +username = demo +password = secret +mqtt_host = 127.0.0.1 +mqtt_port = 1883 +mqtt_ssl = False mqtt_user = demo mqtt_pass = 123456 -orgname = demo -passcode = 123456 trace = false [boat] name = My boat -sailno = GER 4711 +sailno = GER 815 class = One off handicap = 100.0 club = NONE diff --git a/obp60v.py b/obp60v.py index 445736d..513b415 100755 --- a/obp60v.py +++ b/obp60v.py @@ -124,7 +124,9 @@ 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'), 'deviceid': 100, 'manufcode': 2046, # Open Boat Projects (OBP) 'devfunc': 120, # Display @@ -212,7 +214,7 @@ def rxd_gps(devname, devspeed): try: ser = serial.Serial(devname, devspeed, timeout=3) except serial.SerialException as e: - print("GPS serial port not available") + log.error("GPS serial port not available") return setthreadtitle("GPSlistener") while not appdata.shutdown: @@ -624,7 +626,7 @@ def init_profile(config, cfg, boatdata): cls = getattr(pages, p['type']) except AttributeError: # Klasse nicht vorhanden, Seite wird nicht benutzt - print(f"Klasse '{p['type']}' nicht gefunden") + 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 @@ -643,7 +645,7 @@ def set_loglevel(nr): nr = 0 return level[nr] -def init_logging(logdir, logfile='obp60v.log'): +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])) @@ -651,30 +653,16 @@ def init_logging(logdir, logfile='obp60v.log'): 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(logging.INFO) + log.setLevel(loglevel) console = logging.StreamHandler() console.setFormatter(logging.Formatter('%(levelname)s:%(message)s')) - console.setLevel(logging.INFO) + console.setLevel(loglevel) log.addHandler(console) if __name__ == "__main__": setproctitle("obp60v") - # Globale Daten, u.a. auch Shutdown-Indikator - appdata = AppData() - - 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']) @@ -683,11 +671,11 @@ if __name__ == "__main__": 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') - print("Setting GUI style to '{}'".format(cfg['guistyle'])) cfg['mouseptr'] = config.getboolean('system', 'mouseptr') try: cfg['win_x'] = config.getint('gui', 'win_x') @@ -724,13 +712,15 @@ if __name__ == "__main__": boatdata.addHistory(history, "press") # Tracker data - cfg['tracker']['type'] = config.get('tracker', 'type') + 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']['orgname'] = config.get('tracker', 'orgname') - cfg['tracker']['passcode'] = config.get('tracker', 'passcode') cfg['tracker']['logdir'] = cfg['logdir'] cfg['tracker']['trace'] = config.getboolean('tracker', 'trace') @@ -742,55 +732,76 @@ if __name__ == "__main__": cfg['boat']['club'] = config.get('boat', 'club') cfg['boat']['team'] = config.get('boat', 'team') - # Client UUID. Automatisch erzeugen wenn noch nicht vorhanden - create_uuid = False - try: - cfg['tracker']['uuid'] = config.get('tracker', 'uuid') - except configparser.NoOptionError: - create_uuid = True - if create_uuid or (len(cfg['tracker']['uuid']) != 36): - cfg['tracker']['uuid'] = str(uuid.uuid4()) - config.set('tracker', 'uuid', cfg['tracker']['uuid']) - with open(config_path, 'w') as fh: - config.write(fh) - - if cfg['simulation']: - boatdata.enableSimulation() # Protokollierung - init_logging(os.path.expanduser(cfg['logdir']), cfg['logfile']) - log.info("Logging initialized") + 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) + + 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) + + # 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 + # Schnittstellen aktivieren, jew. eigener Thread if cfg['can']: - print("CAN enabled") + log.info("CAN enabled") t_rxd_n2k = threading.Thread(target=rxd_n2k, args=(cfg['can_intf'],)) t_rxd_n2k.start() if cfg['nmea0183']: - print("NMEA0183 enabled, library version {}".format(pynmea2.version)) + 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']: - print("GPS enabled (local)") + log.info("GPS enabled (local)") t_rxd_gps = threading.Thread(target=rxd_gps, args=(cfg['gps_port'],)) t_rxd_gps.start() if cfg['network']: - print("Networking enabled") + 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': - appdata.track.set_type( cfg['tracker']['type']) + log.info(f"Tracking enabled, mode {cfg['tracker']['type']}") + #appdata.track.set_type( cfg['tracker']['type']) t_tracker = threading.Thread(target=appdata.track.mqtt_tracker, args=(cfg['tracker'],cfg['boat'],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: - print("Simulation mode enabled") + log.info("Simulation mode enabled") app = Frontend(cfg, appdata, owndevice, boatdata, profile) app.run() @@ -808,5 +819,6 @@ if __name__ == "__main__": 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.") diff --git a/pages/__init__.py b/pages/__init__.py index 422f104..3497b88 100644 --- a/pages/__init__.py +++ b/pages/__init__.py @@ -32,10 +32,10 @@ from .dst810 import DST810 from .epropulsion import EPropulsion from .keel import Keel from .mob import MOB +from .racetracker import RaceTracker from .rollpitch import RollPitch from .skyview import SkyView from .solar import Solar -from .tracker import Tracker from .rudder import Rudder from .voltage import Voltage from .wind import Wind diff --git a/pages/page.py b/pages/page.py index cf1c0b6..e063736 100644 --- a/pages/page.py +++ b/pages/page.py @@ -68,7 +68,7 @@ class Page(): self.pageno = pageno self.cfg = cfg self.fullscreen = cfg['guistyle'] == 'fullscreen' - self.appdata = appdata + self.app = appdata self.bd = boatdata self.header = True self.footer = True @@ -142,7 +142,7 @@ class Page(): 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(' '.join([s for s in self.appdata.status if self.appdata.status[s]])) + ctx.show_text(' '.join([s for s in self.app.status if self.app.status[s]])) ctx.stroke() # Tastenstatus diff --git a/pages/tracker.py b/pages/racetracker.py similarity index 77% rename from pages/tracker.py rename to pages/racetracker.py index 79cb1da..551bdd2 100644 --- a/pages/tracker.py +++ b/pages/racetracker.py @@ -25,7 +25,7 @@ import os import cairo from .page import Page -class Tracker(Page): +class RaceTracker(Page): def __init__(self, pageno, cfg, appdata, boatdata): super().__init__(pageno, cfg, appdata, boatdata) @@ -33,7 +33,7 @@ class Tracker(Page): self.bv_lon = boatdata.getRef("LON") self.bv_sog = boatdata.getRef("SOG") self.races = None - self.raceid = None # Ausgewählte Regatta + self.raceid = self.app.track.hero_raceid # Ausgewählte Regatta self.menupos = 0 self.buttonlabel[1] = 'MODE' self.buttonlabel[2] = 'INFO' @@ -45,9 +45,9 @@ class Tracker(Page): # Flaggen laden flag = ('alpha', 'answer', 'black', 'blue', 'charlie', 'class', - 'finish', 'hotel', 'india', 'november', 'orange', - 'papa', 'repeat_one', 'sierra', 'start', 'uniform', - 'xray', 'yankee', 'zulu') + 'finish', 'foxtrot', 'hotel', 'india', 'november', + 'orange', 'papa', 'repeat_one', 'sierra', 'start', + 'uniform', 'xray', 'yankee', 'zulu') # Mapping self.flagmap = { 3: 'blue', # Zielflagge @@ -81,7 +81,7 @@ class Tracker(Page): self.buttonlabel[2] = '#UP' self.buttonlabel[3] = '#DOWN' self.buttonlabel[4] = 'SET' - if self.appdata.track.is_active(): + if self.app.track.is_active(): self.buttonlabel[5] = 'OFF' else: self.buttonlabel[5] = 'ON' @@ -118,38 +118,33 @@ class Tracker(Page): # Set / Select regatta if self.menupos > 0: self.raceid = self.races[self.menupos - 1] # Nullbasiert - self.appdata.track.hero_raceid = self.raceid + self.app.track.hero_raceid = self.raceid print(f"Selected race '{self.raceid}'") return True elif buttonid == 5: if self.mode == 'C': # Tracking ein/-ausschalten - if self.appdata.track.is_active(): - self.appdata.track.set_active(False) + if self.app.track.is_active(): + self.app.track.set_active(False) self.buttonlabel[5] = 'ON' else: - self.appdata.track.set_active(True) + self.app.track.set_active(True) self.buttonlabel[5] = 'OFF' elif self.mode == 'M': - self.appdata.frontend.flashled.setColor('yellow') - #self.appdata.frontend.flashled.switchOn(4) - self.appdata.frontend.flashled.doFlash(2) + self.app.frontend.flashled.setColor('yellow') + #self.app.frontend.flashled.switchOn(4) + self.app.frontend.flashled.doFlash(2) return True return False def draw_normal(self, ctx): - # Name - #ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD) - #ctx.set_font_size(32) - #ctx.move_to(20, 80) - #ctx.show_text("Tracker") ctx.select_font_face("DSEG7 Classic") ctx.set_font_size(80) - if self.appdata.track.is_active(): - if self.appdata.track.hero_racestatus: - counter = self.appdata.track.hero_racestatus['time'] + if self.app.track.is_active(): + if self.app.track.hero_racestatus: + counter = self.app.track.hero_racestatus['time'] minutes, seconds = divmod(abs(counter), 60) if counter < 0: ctx.move_to(16, 120) @@ -164,11 +159,11 @@ class Tracker(Page): ctx.move_to(100, 120) ctx.show_text("off") - if self.appdata.track.hero_timedelta > 5: + if self.app.track.hero_timedelta > 5: ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD) ctx.set_font_size(16) ctx.move_to(8, 260) - ctx.show_text(f"!!! Time drift of {self.appdata.track.hero_timedelta} seconds") + ctx.show_text(f"!!! Time drift of {self.app.track.hero_timedelta} seconds") x0 = 8 x1 = 96 @@ -179,20 +174,20 @@ class Tracker(Page): ctx.move_to(x0, y0) ctx.show_text("Type") ctx.move_to(x1, y0) - ctx.show_text(self.appdata.track.ttype) + ctx.show_text(self.app.track.ttype) y0 += yoffset ctx.move_to(x0, y0) ctx.show_text("Regatta") ctx.move_to(x1, y0) - ctx.show_text(self.appdata.track.hero_raceid or '[not selected]') + ctx.show_text(self.app.track.hero_raceid or '[not selected]') y0 += yoffset ctx.move_to(x0, y0) ctx.show_text("Course") ctx.move_to(x1, y0) - if self.appdata.track.hero_orgstatus and self.appdata.track.hero_raceid: - ctx.show_text(self.appdata.track.hero_orgstatus['races'][self.appdata.track.hero_raceid]['courseid']) + if self.app.track.hero_orgstatus and self.app.track.hero_raceid: + ctx.show_text(self.app.track.hero_orgstatus['races'][self.app.track.hero_raceid]['courseid']) else: ctx.show_text('[not selected]') @@ -215,9 +210,9 @@ class Tracker(Page): ctx.show_text(self.bv_sog.format()) # Flaggen - if self.appdata.track.hero_racestatus: + if self.app.track.hero_racestatus: pos = 0 - for f in self.appdata.track.hero_racestatus['flags']: + for f in self.app.track.hero_racestatus['flags']: if f in self.flagmap: # TODO Context save/restore erforderlich? ctx.save() @@ -234,14 +229,19 @@ class Tracker(Page): ctx.move_to(4, 42) ctx.show_text("Tracker configuration") - x0 = 8 - x1 = 88 - y0 = 96 + # Linke Spalte mit Daten + x0 = 8 # Labelspalte + x1 = 88 # Datenspalte + y0 = 75 + yoffset = 16 + + # Bootsdaten ctx.set_font_size(20) - ctx.move_to(x0, 75) + ctx.move_to(x0, y0) ctx.show_text("Boat data") + y0 += yoffset + 5 ctx.set_font_size(16) ctx.move_to(x0, y0) @@ -249,48 +249,50 @@ class Tracker(Page): ctx.move_to(x1, y0) ctx.show_text(self.cfg['boat']['name']) - ctx.move_to(x0, y0 + 16) + y0 += yoffset + ctx.move_to(x0, y0) ctx.show_text("Class") - ctx.move_to(x1, y0 + 16) + ctx.move_to(x1, y0) ctx.show_text(self.cfg['boat']['class']) - ctx.move_to(x0, y0 + 32) + y0 += yoffset + ctx.move_to(x0, y0) ctx.show_text("Handicap") - ctx.move_to(x1, y0 + 32) + ctx.move_to(x1, y0) ctx.show_text(str(self.cfg['boat']['handicap'])) - ctx.move_to(x0, y0 + 48) + y0 += yoffset + ctx.move_to(x0, y0) ctx.show_text("Club") - ctx.move_to(x1, y0 + 48) + ctx.move_to(x1, y0) ctx.show_text(self.cfg['boat']['club']) - ctx.move_to(x0, y0 + 64) + y0 += yoffset + ctx.move_to(x0, y0) ctx.show_text("Sailno.") - ctx.move_to(x1, y0 + 64) + ctx.move_to(x1, y0) ctx.show_text(self.cfg['boat']['sailno']) - x0 = 208 - x1 = 272 - y0 = 96 - yoffset = 16 - + # Trackerdaten + y0 += yoffset + 10 ctx.set_font_size(20) - ctx.move_to(x0, 75) + ctx.move_to(x0, y0) ctx.show_text("Tracker info") + y0 += yoffset + 5 ctx.set_font_size(16) ctx.move_to(x0, y0) ctx.show_text("Type") ctx.move_to(x1, y0) - ctx.show_text(self.appdata.track.ttype) + ctx.show_text(self.app.track.ttype) y0 += yoffset ctx.move_to(x0, y0) ctx.show_text("Org.") ctx.move_to(x1, y0) - if self.appdata.track.hero_orgstatus: - ctx.show_text(self.appdata.track.hero_orgstatus['orgname']) + if self.app.track.hero_orgstatus: + ctx.show_text(self.app.track.hero_orgstatus['orgname']) else: ctx.show_text("n/a") @@ -298,7 +300,7 @@ class Tracker(Page): ctx.move_to(x0, y0) ctx.show_text("Status") ctx.move_to(x1, y0) - if not self.appdata.track.hero_racestatus: + if not self.app.track.hero_racestatus: ctx.show_text("inactive") else: #TODO Mehr Details @@ -308,18 +310,29 @@ class Tracker(Page): ctx.move_to(x0, y0) ctx.show_text("Team") ctx.move_to(x1, y0) - if self.appdata.track.hero_racestatus: - ctx.show_text(self.appdata.track.hero_racestatus['team']) + ctx.show_text(self.app.track.team) + + y0 += yoffset + ctx.move_to(x0, y0) + ctx.show_text("Server") + ctx.move_to(x1, y0) + if self.app.track.mqtt_connected: + ctx.show_text("MQTT") else: - ctx.show_text("n/a") + ctx.show_text("offline") + + # Rechte Spalte mit Regattaauswahl + + x = 208 + y = 75 + yoffset = 16 # Mögliche Regatten - self.races = self.appdata.track.hero_get_races() + self.races = self.app.track.hero_get_races() if len(self.races) == 1: - self.raceid = self.races[0] + self.hero_raceid = self.races[0] self.menupos = 1 - x = 208 - y = 180 + ctx.set_font_size(20) ctx.move_to(x, y) ctx.show_text("Select Regatta") @@ -333,7 +346,7 @@ class Tracker(Page): for r in self.races: i += 1 if r == self.raceid: - r += '*' + r = f"\xbb {r} \xab" self.draw_text_boxed(ctx, x, y, 180, 20, r, (self.menupos == i)) y += 20 if i == 0: @@ -350,11 +363,11 @@ class Tracker(Page): ctx.show_text("Message from race officers") ctx.set_font_size(16) - if not self.appdata.track.hero_orgstatus: + if not self.app.track.hero_orgstatus or self.app.track.hero_orgstatus['message'] == '': ctx.move_to(8, 72) ctx.show_text("[ empty ]") else: - lines = self.appdata.track.hero_orgstatus['message'].splitlines() + lines = self.app.track.hero_orgstatus['message'].splitlines() y = 72 for l in lines: ctx.move_to(8, y) diff --git a/tracker.py b/tracker.py index 220e9f3..3e30891 100644 --- a/tracker.py +++ b/tracker.py @@ -14,49 +14,87 @@ Wiederherstellung der Verbindung übertragen. TODO - Nach einem Disconnect manuelle Neuverbindung ermöglichen +- Audioausgabe für Regatta Hero """ import os import time import paho.mqtt.client as mqtt +import http.client +import ssl import json import socket +import ssl +import math +import subprocess # für Audioausgabe / mpg123 class Tracker(): - def __init__(self, trackertype='NONE'): - self.ttype = 'NONE' - self.set_type(trackertype) - + def __init__(self, logger, cfg): + self.log = logger self.appdata = None + validtypes = ('HERO', 'SDCARD', 'SERVER', 'LOCAL', 'NONE') + if cfg['tracker']['type'] not in validtypes: + raise TypeError("Invalid tracker type: '{}'. Only supported: {}".format(cfg['tracker']['type'], ','.join(validtypes))) + self.ttype = cfg['tracker']['type'] + + self.trace = cfg['tracker']['trace'] # Debugging + self.trace_fh = None # File Handle der Tracedatei + + self.buoys = {} # Tonnen (Hero=20) + self.courses = [] # Bahnen + self.races = [] # Regatten + + self.mqtt_connected = False self.activated = False - self.trace = False # Debugging - self.trace_fh = None # File Handle der Tracedatei - - self.races = set() # Liste der Regatten, eindeutige Namen - self.courses = set() # Liste der Bahnen, eindeutige Namen - self.lat = None # last latitude self.lon = None # last longitude self.tspos = None # timestamp (hh:ss:mm) as datetime.time self.sog = None - self.hero_orgid = None # Eingestellt in Gerätekonfiguration + # Statische Daten + # TODO Wirklich alles hier im Tracker oder ist einiges generisch? + # appdata? + self.boatid = cfg['boat']['uuid'] + self.sailno = cfg['boat']['sailno'] + self.boatname = cfg['boat']['name'] + self.boatclass = cfg['boat']['class'] + self.handicap = cfg['boat']['handicap'] + self.club = cfg['boat']['club'] + self.team = cfg['boat']['team'] + + # Regatta Hero + self.hero_orgid = cfg['tracker']['username'] # Eingestellt in Gerätekonfiguration + self.hero_passcode = cfg['tracker']['password'] + self.hero_host = cfg['tracker']['host'] + self.hero_port = cfg['tracker']['port'] + self.hero_viewerpass = None # Wird vom Server in "org" gesendet + + # Vorlage für Anfragen + self.http_payload_template = { + "orgid": self.hero_orgid, + "passcode": self.hero_passcode, + "raceid": "", + "replay": "live", + "replaytime": 0, + "updateType": "timerUpdate" + } + + self.hero_raceid = None # Aktuell ausgewählte Regatta + + # MQTT + self.client = mqtt.Client() + self.client.on_connect = self.mqtt_on_connect + self.client.on_message = self.mqtt_on_message + self.hero_orgstatus = None self.hero_racestatus = None - self.hero_raceid = None # Aktuelle Regatta self.hero_timedelta = 0 # Zeitdifferenz zum Server in sec - # TODO Wirklich alles im Tracker oder ist einiges generisch? - self.boatid = None - self.sailno = None - self.boatname = None - self.boatclass = None - self.handicap = None - self.club = None - self.team = None + # Hole erste Daten vom Server + self.hero_query_org() def is_server_active(self, hostname, port): """ @@ -81,12 +119,157 @@ class Tracker(): def set_hero_raceid(self, newraceid): self.hero_raceid = newraceid - def set_type(self, newtype): - validtypes = ('HERO', 'SDCARD', 'SERVER', 'NONE') - newtype = newtype.upper() - if newtype not in validtypes: - raise TypeError(f"Invalid tracker type: '{newtype}'. Only supported: {validtypes}") - self.ttype = newtype + #def set_type(self, newtype): + # validtypes = ('HERO', 'SDCARD', 'SERVER', 'LOCAL', 'NONE') + # newtype = newtype.upper() + # if newtype not in validtypes: + # raise TypeError(f"Invalid tracker type: '{newtype}'. Only supported: {validtypes}") + # self.ttype = newtype + + """ + Audioevents: + + Einfach nur die Zahl + 'eins', 'zwei', 'drei', 'vier', 'fuenf', 'sechs', 'sieben', + 'acht', 'neun', 'zehn', + + Minuten bis zum Start + 'eineMin', 'zweiMin', 'dreiMin', 'vierMin', 'fuenfMin' + + Sekunden bis zum Start + 'fuenfzehn', 'zwanzig', 'dreissig', 'vierzig', 'fuenfzig' + + '30sek_alerter' 30 sec kein GPS. Smartphonetext + 'abbruch' Abbruch der Wettfahrt + 'alive' sehr leises Geräusch + 'allgmRueck' + 'bahnmarke' + 'bahnVerk' Bahnverkürzung + 'batteryLevel' Ladezustand unter 20%. Smartphonetext + 'cellback' Mobilfunkverbindung wiederhergestellt, Daten übertragen + 'einzelRueck' + 'endeEineMinute' Zeitrennen in einer Minute zuende + 'endeWettfahrt' Ende der Wettfahrt + 'endstartVerschiebung' + 'jumping' Springende Koordinaten. Smartphonetext. + 'neueAnsage' Achtung neue Bekanntmachung + 'noconnection' Kein Mobilfunk. GPS-Daten werden zwischengespeichert. + 'runde' + 'startErfolgt' BEEEP + 'startlinie' Startlinie überquert + 'startnotready' Startlinie nicht bereit + 'startready' + 'startVerschiebung' + 'wartenAnkuend' Warten auf Ankündigungssignal + 'ziellinie' Ziellinie überquert + + """ + + def hero_play_audio(self, event, lang='de'): + """ + Ein Event ist mit einer Audiodatei gekoppelt. Das Event trägt + den Basisnamen der Audiodatei. Es werden nur MP3-Dateien unterstützt + """ + lang = lang.lower() + if lang == 'de': + filename = f"{event}.mp3" + else: + filename = f"{event}_{lang}.mp3" + mp3file = os.path.join(cfg['audiopath'], filename) + subprocess.run(["mpg123", "-q", mp3_file]) + + def hero_get_degrees(azimut): + """ + Winkel von einer Bahnmarke zur nächsten basierend auf dem Datenfeld azimutNext + """ + if (azimut <= math.pi * 3/2): + return math.degrees(2 * math.pi - azimut - math.pi / 2) + else: + return math.degrees(2 * math.pi - azimut + math.pi * 3 / 2) + + def hero_query_org(self): + """ + Abfrage des Datenservers / Basisdaten + - Namen der Regatten und Kurse + """ + ssl_context = ssl.create_default_context() + conn = http.client.HTTPSConnection(self.hero_host, self.hero_port, context=ssl_context) + endpoint = '/mapupdate' + payload = self.http_payload_template + payload['raceid'] = '[No race]' + json_data = json.dumps(payload) + headers = { + 'Content-Type': 'application/json', + 'Content-Length': str(len(json_data)) + } + try: + conn.request("POST", endpoint, body=json_data, headers=headers) + response = conn.getresponse() + if response.status == 200: + self.log.info("HTTP: Response received successfully!") + data = json.loads(response.read().decode()) + else: + self.log.warning(f"HTTP: Failed to retrieve data. Status code: {response.status}") + return + except http.client.HTTPException as e: + self.log.warning(f"HTTP error occurred: {e}") + except ssl.SSLError as ssl_error: + self.log.warning(f"SSL error occurred: {ssl_error}") + finally: + conn.close() + self.viewerpass = data['org']['viewerPasscode'] + self.courses = [] + for c in data['org']['courses']: + self.courses.append(c) + self.races = [] + for r in data['org']['races'].values(): + if not r['hiderace']: + self.races.append(r['raceid']) + # Regatta automatisch auswählen wenn nur genau eine aktivierbar ist + if len(self.races) == 1: + self.hero_raceid = self.races[0] + + def hero_query_course(self, raceid): + # Bojen und Kurs für ein gegebenes Rennen + ssl_context = ssl.create_default_context() + endpoint = '/mapupdate' + conn = http.client.HTTPSConnection(self.hero_host, self.hero_port, context=ssl_context) + payload = self.http_payload_template + payload['raceid'] = raceid + json_data = json.dumps(payload) + headers = { + 'Content-Type': 'application/json', + 'Content-Length': str(len(json_data)) + } + try: + conn.request("POST", endpoint, body=json_data, headers=headers) + response = conn.getresponse() + if response.status == 200: + self.log.info("HTTP: Response received successfully!") + data = json.loads(response.read().decode()) + else: + self.log.warning(f"HTTP: Failed to retrieve data. Status code: {response.status}") + return + except http.client.HTTPException as e: + self.log.warning(f"HTTP error occurred: {e}") + except ssl.SSLError as ssl_error: + self.log.warning(f"SSL error occurred: {ssl_error}") + finally: + conn.close() + + print(self.hero_orgdata) + + # Bojen + self.buoys.clear() + for key, val in self.hero_orgdata['org']['buoysets']['default']['buoyset']['marks'].items(): + self.buoys[key] = { + 'label': val['label'], + 'lat': val['label'], + 'lon': val['label'] + } + print(self.buoys) + + # Kurse def get_position(self): # Positionsabfrage für die Payload @@ -109,21 +292,24 @@ class Tracker(): return races def mqtt_on_connect(self, client, userdata, flags, rc): - print(f"MQTT connected with result code {rc}") - #userdata['connect_rc'] = rc - if rc != 0: - # Result codes: - # 1: Connection Refused, unacceptable protocol version - # 2: Connection Refused, identifier rejected - # 3: Connection Refused, Server unavailable - # 4: Connection Refused, bad user name or password - # 5: Connection Refused, not authorized - #userdata['connect_ok'] = True - pass - else: + self.mqtt_connected = False + if rc == 0: + self.log.info(f"MQTT connected, subscribing to topics") client.subscribe("regattahero/orgstatus/thomas") client.subscribe("regattahero/racestatus/thomas/#") - #userdata['connect_ok'] = False + self.mqtt_connected = True + elif rc == 1: + self.log.error("MQTT connection refused, unacceptable protocol version") + elif rc == 2: + self.log.error("MQTT connection refused, identifier rejected") + elif rc == 3: + self.log.error("MQTT connection refused, server unavailable") + elif rc == 4: + self.log.error("MQTT connection refused, bad user name or password") + elif rc == 5: + self.log.error("MQTT connection refused, not authorized") + else: + self.log.info(f"MQTT connection refused, error #{rc}") def mqtt_on_message(self, client, userdata, msg): """ @@ -147,7 +333,7 @@ class Tracker(): self.hero_timedelta = abs(sec1 - sec2) if self.hero_orgstatus['allLogout']: - print("All logout received!") + self.log.info("All logout received!") client.disconnect() self.activated = False return @@ -180,8 +366,8 @@ class Tracker(): """ else: - print(f"UNKNOWN TOPIC: {msg.topic}") - print(msg.payload) + self.log.warning(f"UNKNOWN TOPIC: {msg.topic}") + self.log.debug(msg.payload) def mqtt_publish(self, client, topic, payload, bv_lat, bv_lon, bv_sog): """ @@ -201,40 +387,41 @@ class Tracker(): payload['gps']['timestamp'] = time.strftime("%Y-%m-%dT%H:%M:%S.000Z", time.gmtime()) client.publish(topic, json.dumps(payload)) else: - #print("No GPS data available. Nothing published!") - pass + self.log.debug("No GPS data available. Nothing published!") def mqtt_tracker(self, cfg, boat, appdata, boatdata): - print("MQTT tracker enabled") + self.log.info("MQTT tracker enabled") self.appdata = appdata - self.boatid = cfg['uuid'] + """ + self.boatid = boat['uuid'] self.sailno = boat['sailno'] self.boatname = boat['name'] self.boatclass = boat['class'] self.handicap = boat['handicap'] self.club = boat['club'] - self.team = boat['team'] # TODO eher zu Tracker gehörig? + self.team = boat['team'] self.trace = cfg['trace'] - self.hero_orgid = cfg['orgname'] - client = mqtt.Client() - client.on_connect = self.mqtt_on_connect - client.on_message = self.mqtt_on_message - client.username_pw_set(username=cfg['mqtt_user'], password=cfg['mqtt_pass']) + self.hero_orgid = cfg['username'] + """ + self.client.username_pw_set(username=cfg['mqtt_user'], password=cfg['mqtt_pass']) try: - client.connect(cfg['host'], cfg['port'], 60) + self.client.connect(cfg['mqtt_host'], cfg['mqtt_port'], 60) except ConnectionRefusedError: - print("MQTT connection refused. Check username and password.") + self.log.error("MQTT connection refused. Check username and password.") return - if cfg['trace']: - # TODO Log Hinweis + # Initial die Organisationsdaten abfragen um Tonnen und Kurse zu erhalten + # self.hero_query_org(cfg['host'], cfg['port'], cfg['username'], cfg['password']) + + if self.trace: tracefile = os.path.join(os.path.expanduser(cfg['logdir']), 'tracker.log') self.trace_fh = open(tracefile, 'w+') + self.log.info(f"MQTT trace file '{tracefile}' enabled") - topic = "regattahero/tracker/" + cfg['orgname'] + topic = "regattahero/tracker/" + self.hero_orgid payload = { - "passcode": cfg['passcode'], - "orgid": cfg['orgname'], + "passcode": cfg['password'], + "orgid": self.hero_orgid, "raceid": None, # Nach Auswahl einstellen "gps": { "lat": 0.0, @@ -246,13 +433,13 @@ class Tracker(): "timestamp": "" # ISO8601 Format mit Millisekunden in UTC }, "boat": { - "boatid": cfg['uuid'], - "sailno": boat['sailno'], - "team": boat['team'], - "boatclass": boat['class'], - "handicap": boat['handicap'], - "club": boat['club'], - "boatname": boat['name'], + "boatid": self.boatid, + "sailno": self.sailno, + "team": self.team, + "boatclass": self.boatclass, + "handicap": self.handicap, + "club": self.club, + "boatname": self.boatname, "isTracking": True, "hasGivenUp": False }, @@ -270,12 +457,12 @@ class Tracker(): bv_lon = boatdata.getRef("LON") bv_sog = boatdata.getRef("SOG") - client.loop_start() + self.client.loop_start() while not appdata.shutdown: time.sleep(1) if self.activated and self.hero_raceid is not None: - self.mqtt_publish(client, topic, payload, bv_lat, bv_lon, bv_sog) - client.loop_stop() - client.disconnect() + self.mqtt_publish(topic, payload, bv_lat, bv_lon, bv_sog) + self.client.loop_stop() + self.client.disconnect() if cfg['trace']: self.trace_fh.close()