Logging und Tracker GUI weiterprogrammiert

This commit is contained in:
Thomas Hooge 2025-09-17 20:37:12 +02:00
parent d3a984075e
commit eab6cdf4c9
8 changed files with 403 additions and 186 deletions

View File

@ -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

BIN
images/flags/foxtrot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 B

View File

@ -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

104
obp60v.py
View File

@ -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.")

View File

@ -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

View File

@ -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

View File

@ -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)

View File

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