Logging und Tracker GUI weiterprogrammiert
This commit is contained in:
327
tracker.py
327
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',
|
||||
|
||||
<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_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()
|
||||
|
||||
Reference in New Issue
Block a user