OBP60v/tracker.py

271 lines
9.2 KiB
Python

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