""" Tracker-Daten Mögliche Typen: HERO - Regatta Hero SDCARD LOCAL SERVER - spezielle Software benötigt, kann auch ein Raspi an Bord sein 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 """ import os import time import paho.mqtt.client as mqtt import json import socket class Tracker(): def __init__(self, trackertype='NONE'): self.ttype = 'NONE' self.set_type(trackertype) self.appdata = None 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 self.hero_orgstatus = None self.hero_racestatus = None self.hero_raceid = None # Aktuelle Regatta # 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 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 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 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 #return [r for r in self.hero_orgstatus['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: client.subscribe("regattahero/orgstatus/thomas") client.subscribe("regattahero/racestatus/thomas/#") #userdata['connect_ok'] = False def mqtt_on_message(self, client, userdata, msg): """ 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 == "regattahero/orgstatus/thomas": # kommt alle 10s self.hero_orgstatus = json.loads(msg.payload) if self.hero_orgstatus['allLogout']: print("All logout received!") client.disconnect() self.activated = False return if self.hero_orgstatus['message']: # TODO Alarm-Funktion nutzen? print("Nachricht der Wettfahrtkeitung:") print(self.hero_orgstatus['message']) #print(self.hero_orgstatus) elif msg.topic.startswith("regattahero/racestatus/thomas"): # kommt alle 1s # dem Topic angehängt ist noch die raceid payload = json.loads(msg.payload) self.hero_racestatus = payload['racestatus'] print(self.hero_racestatus['flags']) #print(self.hero_racestatus) """ 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 phase: 0 vor dem Start racephase: 1 racephase: 4 5 Vorbereitungssignal racephase: 6 nach vorbereitnug wieder runter 7: Rennen gestartet """ else: print(f"UNKNOWN TOPIC: {msg.topic}") print(msg.payload) def mqtt_publish(self, client, 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()) print(payload) client.publish(topic, json.dumps(payload)) else: print("No GPS data available. Nothing published!") def mqtt_tracker(self, cfg, boat, appdata, boatdata): print("MQTT tracker enabled") self.appdata = appdata self.boatid = cfg['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.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']) try: client.connect(cfg['host'], cfg['port'], 60) except ConnectionRefusedError: print("MQTT connection refused. Check username and password.") return if cfg['trace']: # TODO Log Hinweis tracefile = os.path.join(os.path.expanduser(cfg['logdir']), 'tracker.log') self.trace_fh = open(tracefile, 'w+') topic = "regattahero/tracker/" + cfg['orgname'] payload = { "passcode": cfg['passcode'], "orgid": cfg['orgname'], "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": cfg['uuid'], "sailno": boat['sailno'], "team": boat['team'], "boatclass": boat['class'], "handicap": boat['handicap'], "club": boat['club'], "boatname": boat['name'], "isTracking": True, "hasGivenUp": False } } # Zugriff auf Boatdata: Referenzen für leichten schnellen Zugriff bv_lat = boatdata.getRef("LAT") bv_lon = boatdata.getRef("LON") bv_sog = boatdata.getRef("SOG") 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() if cfg['trace']: self.trace_fh.close()