618 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			618 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			Python
		
	
	
	
| """
 | |
| Tracker-Daten
 | |
| 
 | |
| Mögliche Typen:
 | |
|   HERO - Regatta Hero
 | |
|   SDCARD - nur für ESP32 relevant
 | |
|   LOCAL - Logdatei. GPX ist ungeeignet, weil das Gerät zu jedem Zeitpunkt
 | |
|           ausgeschaltet werden kann und somit die Datei in einem 
 | |
|           ungültigen Zustand sein kann.
 | |
|           Es wird eine Datei track.log.geschrieben.
 | |
|           Um fehlende Daten festzustellen wird ein fortlaufender Zähler
 | |
|           geschrieben.
 | |
|   SERVER - spezielle Software benötigt, kann auch ein Raspi an Bord sein
 | |
|            Der Tracker sendet einen HTTP-POST. Der Server speichert
 | |
|            die Daten für mehrere Geräte. Vorläufiger Servername:
 | |
|            trackserver
 | |
|   NONE - kein Tracking
 | |
| 
 | |
| Wenn die Verbindung zum Server im Internet nicht funktioniert, werden
 | |
| die Positionen in eine Warteschlange gesichert und nach 
 | |
| 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 math
 | |
| import subprocess # für Audioausgabe / mpg123
 | |
| 
 | |
| class Tracker():
 | |
| 
 | |
|     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.local_lfdno = 0
 | |
|         self.local_dt = cfg['tracker']['interval'] # Eintrag alle <n> Sekunden schreiben
 | |
| 
 | |
|         self.trace =  cfg['tracker']['trace']   # Debugging
 | |
|         self.trace_fh = None                    # File Handle der Tracedatei
 | |
| 
 | |
|         self.audiopath = cfg['audiopath']
 | |
| 
 | |
|         self.buoys = {}      # Tonnen (Hero=20)
 | |
|         self.courses = []    # Bahnen
 | |
|         self.races = []      # Regatten
 | |
| 
 | |
|         self.mqtt_connected = False
 | |
|         self.activated = False
 | |
|         self.lat = None    # last latitude
 | |
|         self.lon = None    # last longitude
 | |
|         self.tspos = None  # timestamp (hh:ss:mm) as datetime.time
 | |
|         self.sog = None
 | |
| 
 | |
|         # 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']
 | |
| 
 | |
|         if self.ttype == 'HERO':
 | |
|             # 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
 | |
|             self.hero_racephase = 0  # Bei Änderung Event auslösen
 | |
|             self.hero_shortened = False # Bahnverkürzung aktiv
 | |
| 
 | |
|             # MQTT
 | |
|             self.client = mqtt.Client()
 | |
|             self.client.on_connect = self.mqtt_on_connect
 | |
|             self.client.on_message = self.mqtt_on_message
 | |
|             self.client_race_age = 0 # Age of last mqtt race data in seconds
 | |
| 
 | |
|             self.hero_orgstatus = None 
 | |
|             self.hero_racestatus = None
 | |
|             self.hero_timedelta = 0 # Zeitdifferenz zum Server in sec
 | |
|             self.hero_givenup = False
 | |
| 
 | |
|             # Hole erste Daten vom Server
 | |
|             self.hero_query_org()
 | |
| 
 | |
|     def is_server_active(self, hostname, port):
 | |
|         """
 | |
|         ohne Netzwerkverbindung wirft socket.gethostbyname eine Exception,
 | |
|         ggf. "Erro -3 temporary failure in name resolution"
 | |
|         """
 | |
|         try:
 | |
|             host = socket.gethostbyname(hostname) # DNS Lookup
 | |
|             s = socket.create_connection((host, port), 2)
 | |
|             s.close()
 | |
|             return True
 | |
|         except Exception:
 | |
|             pass
 | |
|         return False
 | |
| 
 | |
|     def is_active(self):
 | |
|        return self.activated
 | |
| 
 | |
|     def set_active(self, newval):
 | |
|        self.activated = newval
 | |
| 
 | |
|     def local_tracker(self, cfg, appdata, boatdata):
 | |
|         self.log.info("Local tracker enabled")
 | |
|         if not os.path.exists(cfg['histpath']):
 | |
|             os.makedirs(cfg['histpath'])
 | |
|             self.log.info(f"History path created: '{cfg['histpath']}'")
 | |
| 
 | |
|         # Zugriff auf Boatdata: Referenzen für leichten schnellen Zugriff
 | |
|         bv_lat = boatdata.getRef("LAT")
 | |
|         bv_lon = boatdata.getRef("LON")
 | |
|         bv_hdop = boatdata.getRef("HDOP")
 | |
|         bv_tspos = boatdata.getRef("TSPOS")
 | |
| 
 | |
|         trackerfile = os.path.join(cfg['histpath'], "localtrack.log")
 | |
|         fh = open(trackerfile, 'a+')
 | |
|         n = 0
 | |
|         while not appdata.shutdown:
 | |
|             time.sleep(1)
 | |
|             if not self.activated:
 | |
|                 continue
 | |
|             n += 1
 | |
|             if n % self.local_dt != 0:
 | |
|                 continue
 | |
|             self.local_lfdno += 1
 | |
|             data = "{},{}Z,{},{},{}\n".format(
 | |
|                 self.local_lfdno,
 | |
|                 bv_tspos.getValue(),
 | |
|                 bv_lat.getValue(),
 | |
|                 bv_lon.getValue(),
 | |
|                 bv_hdop.getValue())
 | |
|             fh.write(data)
 | |
|             fh.flush()
 | |
|         fh.close()
 | |
| 
 | |
|     def set_hero_raceid(self, newraceid):
 | |
|         self.hero_raceid = newraceid
 | |
| 
 | |
|     #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', 
 | |
| 
 | |
|     <n> Minuten bis zum Start
 | |
|     'eineMin', 'zweiMin', 'dreiMin', 'vierMin', 'fuenfMin'
 | |
| 
 | |
|     <n> 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_mqtt_subscribe(self, raceid, lastid=None):
 | |
|         topicbase = f"regattahero/racestatus/{self.hero_orgid}"
 | |
|         if lastid is not None:
 | |
|             topic = '/'.join((topicbase, lastid))
 | |
|             self.client.unsubscribe(topic)
 | |
|             self.log.info(f"Sent unsubscribe for '{topic}'")
 | |
|         topic = '/'.join((topicbase, raceid))
 | |
|         self.client.subscribe(topic)
 | |
|         self.log.info(f"Sent subscribe for '{topic}'")
 | |
| 
 | |
|     def hero_giveup(self):
 | |
|         # TODO nach Aufgabe noch ein paar Pakete senden bis dier
 | |
|         # Tracker dann abgestellt wird.
 | |
|         self.hero_givenup = True
 | |
| 
 | |
|     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(self.audiopath, filename)
 | |
|         subprocess.Popen(["mpg123", "-q", mp3file])
 | |
| 
 | |
|     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(self, raceid):
 | |
|         """
 | |
|         Query Regatta Hero HTTP-Server
 | |
|         """
 | |
|         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'] = raceid
 | |
|         json_data = json.dumps(payload)
 | |
|         headers =  {
 | |
|             'Content-Type': 'application/json',
 | |
|             'Content-Length': str(len(json_data))
 | |
|         }
 | |
|         data = {} # empty
 | |
|         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 data
 | |
|         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()
 | |
|         return data
 | |
| 
 | |
|     def hero_query_org(self):
 | |
|         """
 | |
|         Abfrage des Datenservers / Basisdaten
 | |
|           - Namen der Regatten und Kurse
 | |
|         """
 | |
|         data = self.hero_query('[No race]')
 | |
|         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
 | |
|         """
 | |
|         data = self.hero_query(raceid)
 | |
| 
 | |
|         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
 | |
|         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'])
 | |
| 
 | |
|     def hero_query_race(self, raceid):
 | |
|         """
 | |
|         Aktuelle Regattadaten vom HTTP-Server holen, ggf. weil MQTT nichts mehr sendet
 | |
|         """
 | |
|         data = self.hero_query(raceid)
 | |
|         self.hero_racestatus = data['race']['racestatus']
 | |
|         print(self.hero_racestatus)
 | |
|         # 'raceactive': False
 | |
|         # 'racefinished': True
 | |
| 
 | |
|     def get_position(self):
 | |
|         # Positionsabfrage für die Payload
 | |
|         # LAT, LON, TSPOS, SOG
 | |
|         return (self.lat, self.lon, self.tspos, self.sog)
 | |
|     """
 | |
|     def hero_add_race(self, raceid):
 | |
|         self.races.add(raceid)
 | |
| 
 | |
|     def hero_set_races(self, newraces):
 | |
|         self.races = set(newraces)
 | |
|     """
 | |
|     def hero_get_races(self):
 | |
|         if not self.hero_orgstatus:
 | |
|             return []
 | |
|         races = []
 | |
|         for r in self.hero_orgstatus['races'].values():
 | |
|             if not r['hiderace']:
 | |
|                 races.append(r['raceid'])
 | |
|         return races
 | |
| 
 | |
|     def mqtt_on_connect(self, client, userdata, flags, rc):
 | |
|         self.mqtt_connected = False
 | |
|         if rc == 0:
 | |
|             self.log.info("MQTT connected, subscribing to organisation topic")
 | |
|             client.subscribe(f"regattahero/orgstatus/{self.hero_orgid}")
 | |
|             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):
 | |
|         """
 | |
|         Meldungen vom Server empfangen
 | |
|         TODO raceid über userdata? dann topic prüfen?
 | |
|         """
 | |
|         if self.trace:
 | |
|             self.trace_fh.write(msg.topic)
 | |
|             self.trace_fh.write("\n")
 | |
|             self.trace_fh.write(msg.payload.decode())
 | |
|             self.trace_fh.write("\n\n")
 | |
|             self.trace_fh.flush()
 | |
|         if msg.topic == f"regattahero/orgstatus/{self.hero_orgid}":
 | |
|             # kommt alle 10s
 | |
|             self.hero_orgstatus = json.loads(msg.payload)
 | |
| 
 | |
|             # Zeitdifferenz Client/Server ermitteln für ggf. spätere Warnung
 | |
|             server_ts = self.appdata.track.hero_orgstatus['timestamp'].split(':')
 | |
|             sec1 = int(server_ts[0]) * 3600 + int(server_ts[1]) * 60 + int(server_ts[2])
 | |
|             ts = time.localtime()
 | |
|             sec2 = ts.tm_hour * 3600 + ts.tm_min * 60 + ts.tm_sec
 | |
|             self.hero_timedelta = abs(sec1 - sec2)
 | |
| 
 | |
|             if self.hero_orgstatus['allLogout']:
 | |
|                 self.log.info("All logout received!")
 | |
|                 client.disconnect()
 | |
|                 self.activated = False
 | |
|                 return
 | |
|             if self.hero_orgstatus['message']:
 | |
|                 # TODO Alarm-Funktion nutzen?
 | |
|                 pass
 | |
|         elif msg.topic.startswith(f"regattahero/racestatus/{self.hero_orgid}"):
 | |
|             # kommt alle 1s
 | |
|             # dem Topic angehängt ist noch die raceid
 | |
|             self.client_race_age = 0
 | |
|             payload = json.loads(msg.payload)
 | |
|             self.hero_racestatus = payload['racestatus']
 | |
|             racephase = payload['racestatus']['racephase']
 | |
|             if self.hero_racephase != racephase:
 | |
|                 # Phasenänderung! Event!
 | |
|                 print("Event: Phasenübergang {} -> {}".format(self.hero_racephase, racephase))
 | |
|                 if self.hero_racephase == 0:
 | |
|                     if racephase == 1:
 | |
|                         # Startlinie bereit
 | |
|                         self.hero_play_audio('startready')
 | |
|                         print(payload['racestatus'])
 | |
|                         #subprocess.popen("mpg123 -q " + os.path.join(self.audiopath, "startready.mp3"))
 | |
|                 elif self.hero_racephase == 1:
 | |
|                     if racephase == 1:
 | |
|                         # Startlinie bereit
 | |
|                         self.hero_play_audio('startVerschiebung')
 | |
|                     elif racephase == 3:
 | |
|                         # Startverschiebung aufgehoben
 | |
|                         pass
 | |
|                     elif racephase == 4:
 | |
|                         self.hero_play_audio('dreiMin')
 | |
|                 elif self.hero_racephase == 2:
 | |
|                     if racephase == 3:
 | |
|                         self.hero_play_audio('vierMin')
 | |
|                     elif racephase == 4:
 | |
|                         self.hero_play_audio('dreiMin')
 | |
|                 elif self.hero_racephase == 3:
 | |
|                     if racephase == 4:
 | |
|                          self.hero_play_audio('dreiMin')
 | |
|                     if racephase == 1:
 | |
|                         self.hero_play_audio('startVerschiebung')
 | |
| 
 | |
|                 elif self.hero_racephase == 4:
 | |
|                     if racephase == 1:
 | |
|                         self.hero_play_audio('startVerschiebung')
 | |
|                     if racephase == 5:
 | |
|                         # Blauer Peter oben
 | |
|                         self.hero_play_audio('zweiMin')
 | |
|                 elif self.hero_racephase == 5:
 | |
|                     if racephase == 1:
 | |
|                         self.hero_play_audio('startVerschiebung')
 | |
|                     elif racephase == 6:
 | |
|                         # Blauer Peter runter
 | |
|                         pass
 | |
|                 elif self.hero_racephase == 6:
 | |
|                     if racephase == 1:
 | |
|                         self.hero_play_audio('startVerschiebung')
 | |
|                 elif self.hero_racephase == 7:
 | |
|                     if racephase == 8:
 | |
|                         # Gestartet
 | |
|                         pass
 | |
|                 elif self.hero_racephase == 8:
 | |
|                     if racephase == 2:
 | |
|                         if payload['racestatus']['recallgeneral']:
 | |
|                             self.hero_play_audio('allgmRueck')
 | |
|                         elif payload['racestatus']['racecancelled']:
 | |
|                             self.hero_play_audio('abbruch')
 | |
|                         elif payload['racestatus']['racefinished']:
 | |
|                             self.hero_play_audio('endeWettfahrt')
 | |
|             self.hero_racephase = racephase
 | |
| 
 | |
|             # TODO Einzelrückruf, keine Änderung in der Phase!
 | |
|             #if payload['racestatus']['recallindiv']:
 | |
|             #    self.hero_play_audio('einzelRueck')
 | |
| 
 | |
|             # Timing
 | |
|             timer = payload['racestatus']['time']
 | |
|             countdown = {
 | |
|                -60: 'eineMin',
 | |
|                -50: 'fuenfzig', -40: 'vierzig', -30: 'dreissig',
 | |
|                -20: 'zwanzig', -15: 'fuenfzehn', -10: 'zehn',
 | |
|                 -9: 'neun', -8: 'acht', -7: 'sieben', -6: 'sechs',
 | |
|                 -5: 'fuenf', -4: 'vier', -3: 'drei', -2: 'zwei',
 | |
|                 -1: 'eins', 0 : 'startErfolgt'
 | |
|             }
 | |
|             # Komplexere Bedingung die nach Start fast keinen Aufwand
 | |
|             # mehr bedeutet
 | |
|             if (timer <= 0) and (timer >= -60) and timer in countdown:
 | |
|                 self.hero_play_audio(countdown[timer])
 | |
| 
 | |
|             # payload['racestatus']['racestarted']
 | |
|             # payload['racesettings']
 | |
| 
 | |
|             # Bahnverkürzung
 | |
|             if payload['racestatus']['shortend'] and not self.hero_shortened:
 | |
|                 self.hero_shortened = True
 | |
|                 self.hero_play_audio('bahnVerk')
 | |
|                 self.log.info("Bahnverkürzung: Zu Bahnmarke {}".format(payload['racestatus']['shortendsel']))
 | |
| 
 | |
|             """
 | |
|             time: negativ: Zeit vor dem Start, positiv: Zeit nach dem Start
 | |
|                   in Sekunden
 | |
|             flags: [0, 0, 14, 10] 
 | |
|             raceactive: true bedeutet orange Flagge ist oben
 | |
|             racestarted: true
 | |
| 
 | |
|             Signale der Wettfahrtleitung hier anzeigen
 | |
|                Regattaabbruch
 | |
|                Bahnverkürzung
 | |
|                Rückrufe
 | |
| 
 | |
|             """
 | |
|         else:
 | |
|             self.log.warning(f"UNKNOWN TOPIC: {msg.topic}")
 | |
|             self.log.debug(msg.payload)
 | |
| 
 | |
|     def mqtt_publish(self, topic, payload, bv_lat, bv_lon, bv_sog):
 | |
|         """
 | |
|         Payload vorbelegt als Template, so daß nur noch die veränderlichen
 | |
|         GPS-Daten eingefügt werden müssen: LAT LON SOG TIMESTAMP
 | |
|         
 | |
|         isTracking kann ausgeschaltet werden,
 | |
|         """
 | |
|         lat = bv_lat.getValueRaw()
 | |
|         lon = bv_lon.getValueRaw()
 | |
|         sog = bv_sog.getValueRaw()
 | |
|         if lat and lon and (sog is not None):
 | |
|             payload['raceid'] = self.hero_raceid
 | |
|             payload['gps']['lat'] = round(lat, 5)
 | |
|             payload['gps']['lon'] = round(lon, 5)
 | |
|             payload['gps']['speed'] = sog
 | |
|             payload['gps']['timestamp'] = time.strftime("%Y-%m-%dT%H:%M:%S.000Z", time.gmtime())
 | |
|             self.client.publish(topic, json.dumps(payload))
 | |
|         else:
 | |
|             self.log.debug("No GPS data available. Nothing published!")
 | |
| 
 | |
|     def mqtt_tracker(self, cfg, boat, appdata, boatdata):
 | |
|         self.log.info("MQTT tracker enabled")
 | |
|         self.appdata = appdata
 | |
|         self.client.username_pw_set(username=cfg['mqtt_user'], password=cfg['mqtt_pass'])
 | |
|         try:
 | |
|             self.client.connect(cfg['mqtt_host'], cfg['mqtt_port'], 60)
 | |
|         except ConnectionRefusedError:
 | |
|             self.log.error("MQTT connection refused. Check username and password.")
 | |
|             return
 | |
| 
 | |
|         # 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/" + self.hero_orgid
 | |
|         payload = {
 | |
|             "passcode": cfg['password'],
 | |
|             "orgid": self.hero_orgid,
 | |
|             "raceid": None, # Nach Auswahl einstellen
 | |
|             "gps": {
 | |
|                 "lat": 0.0,
 | |
|                 "lon": 0.0,
 | |
|                 "speed": 0.0,
 | |
|                 "age": 500,         # letzter Kontakt zum GPS empfänger in ms
 | |
|                 # "odo": 1000,      # deprecated
 | |
|                 "bat": 1.0,
 | |
|                 "timestamp": ""     # ISO8601 Format mit Millisekunden in UTC
 | |
|                 },
 | |
|             "boat": {
 | |
|                 "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
 | |
|                 },
 | |
|             "device": {             # optional
 | |
|                 "os" : "linux",
 | |
|                 "osVer" : "OBP60v 0.1",
 | |
|                 "isIOS" : False,
 | |
|                 "isAndroid" : False,
 | |
|                 "isTracker" : True
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|         # Zugriff auf Boatdata: Referenzen für leichten schnellen Zugriff
 | |
|         bv_lat = boatdata.getRef("LAT")
 | |
|         bv_lon = boatdata.getRef("LON")
 | |
|         bv_sog = boatdata.getRef("SOG")
 | |
| 
 | |
|         self.client.loop_start()
 | |
|         while not appdata.shutdown:
 | |
|             time.sleep(1)
 | |
|             self.client_race_age += 1
 | |
|             if self.activated and self.hero_raceid is not None:
 | |
|                 self.mqtt_publish(topic, payload, bv_lat, bv_lon, bv_sog)
 | |
|                 if self.client_race_age > 15:
 | |
|                     # Server sendet keine Daten mehr oder Verbindung verloren
 | |
|                     # TODO Frage den http-Server nach den letzten Racestatus
 | |
|                     self.log.warning(f"Keine Daten vom MQTT-Sever seit ca. {self.client_race_age} Sekunden")
 | |
|                     self.log.warning("Hole Daten vom HTTP-Server")
 | |
|                     self.hero_query_race(self.hero_raceid)
 | |
|                     if self.hero_racestatus['racefinished'] == True:
 | |
|                         self.hero_racephase = 0
 | |
|                         self.hero_play_audio('endeWettfahrt')
 | |
|                     self.activated = False
 | |
|         self.client.loop_stop()
 | |
|         self.client.disconnect()
 | |
|         if cfg['trace']:
 | |
|             self.trace_fh.close()
 |