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(): class AppData():
def __init__(self): def __init__(self, logger, cfg):
self.shutdown = False # Globaler Ausschalter self.shutdown = False # Globaler Ausschalter
self.track = Tracker('NONE') self.log = logger
self.track = Tracker(logger, cfg)
self.frontend = None self.frontend = None
self.bv_lat = None self.bv_lat = None
self.bv_lon = 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] [tracker]
type = NONE type = NONE
host = 127.0.0.1 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_user = demo
mqtt_pass = 123456 mqtt_pass = 123456
orgname = demo
passcode = 123456
trace = false trace = false
[boat] [boat]
name = My boat name = My boat
sailno = GER 4711 sailno = GER 815
class = One off class = One off
handicap = 100.0 handicap = 100.0
club = NONE club = NONE

104
obp60v.py
View File

@ -124,7 +124,9 @@ cfg = {
'cfgfile': 'obp60v.conf', 'cfgfile': 'obp60v.conf',
'logdir': '~/.local/share/obp60v', 'logdir': '~/.local/share/obp60v',
'logfile': 'obp60v.log', 'logfile': 'obp60v.log',
'loglevel': 3,
'imgpath': os.path.join(sys.path[0], 'images'), 'imgpath': os.path.join(sys.path[0], 'images'),
'audiopath': os.path.join(sys.path[0], 'audio'),
'deviceid': 100, 'deviceid': 100,
'manufcode': 2046, # Open Boat Projects (OBP) 'manufcode': 2046, # Open Boat Projects (OBP)
'devfunc': 120, # Display 'devfunc': 120, # Display
@ -212,7 +214,7 @@ def rxd_gps(devname, devspeed):
try: try:
ser = serial.Serial(devname, devspeed, timeout=3) ser = serial.Serial(devname, devspeed, timeout=3)
except serial.SerialException as e: except serial.SerialException as e:
print("GPS serial port not available") log.error("GPS serial port not available")
return return
setthreadtitle("GPSlistener") setthreadtitle("GPSlistener")
while not appdata.shutdown: while not appdata.shutdown:
@ -624,7 +626,7 @@ def init_profile(config, cfg, boatdata):
cls = getattr(pages, p['type']) cls = getattr(pages, p['type'])
except AttributeError: except AttributeError:
# Klasse nicht vorhanden, Seite wird nicht benutzt # Klasse nicht vorhanden, Seite wird nicht benutzt
print(f"Klasse '{p['type']}' nicht gefunden") log.error(f"Klasse '{p['type']}' nicht gefunden")
continue continue
c = cls(i, cfg, appdata, boatdata, *[v for v in p['values'].values()]) c = cls(i, cfg, appdata, boatdata, *[v for v in p['values'].values()])
clist[i] = c clist[i] = c
@ -643,7 +645,7 @@ def set_loglevel(nr):
nr = 0 nr = 0
return level[nr] return level[nr]
def init_logging(logdir, logfile='obp60v.log'): def init_logging(logdir, logfile='obp60v.log', loglevel=logging.INFO):
global log global log
os.makedirs(logdir, exist_ok=True) os.makedirs(logdir, exist_ok=True)
log = logging.getLogger(os.path.basename(sys.argv[0])) 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') formatter = logging.Formatter('%(asctime)s %(name)s %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
hdlr.setFormatter(formatter) hdlr.setFormatter(formatter)
log.addHandler(hdlr) log.addHandler(hdlr)
log.setLevel(logging.INFO) log.setLevel(loglevel)
console = logging.StreamHandler() console = logging.StreamHandler()
console.setFormatter(logging.Formatter('%(levelname)s:%(message)s')) console.setFormatter(logging.Formatter('%(levelname)s:%(message)s'))
console.setLevel(logging.INFO) console.setLevel(loglevel)
log.addHandler(console) log.addHandler(console)
if __name__ == "__main__": if __name__ == "__main__":
setproctitle("obp60v") 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 # Basiskonfiguration aus Datei lesen
config = configparser.ConfigParser() config = configparser.ConfigParser()
config_path = os.path.join(sys.path[0], cfg['cfgfile']) 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'])) print("Konfigurationsdatei '{}' konnte nicht gelesen werden!".format(cfg['cfgfile']))
sys.exit(1) sys.exit(1)
cfg['_config'] = config # Objekt zum späteren schreiben cfg['_config'] = config # Objekt zum späteren schreiben
cfg['loglevel'] = config.getint('system', 'loglevel')
cfg['deviceid'] = config.getint('system', 'deviceid') cfg['deviceid'] = config.getint('system', 'deviceid')
cfg['simulation'] = config.getboolean('system', 'simulation') cfg['simulation'] = config.getboolean('system', 'simulation')
cfg['histpath'] = os.path.expanduser(config.get('system', 'histpath')) cfg['histpath'] = os.path.expanduser(config.get('system', 'histpath'))
cfg['guistyle'] = config.get('system', 'guistyle') cfg['guistyle'] = config.get('system', 'guistyle')
print("Setting GUI style to '{}'".format(cfg['guistyle']))
cfg['mouseptr'] = config.getboolean('system', 'mouseptr') cfg['mouseptr'] = config.getboolean('system', 'mouseptr')
try: try:
cfg['win_x'] = config.getint('gui', 'win_x') cfg['win_x'] = config.getint('gui', 'win_x')
@ -724,13 +712,15 @@ if __name__ == "__main__":
boatdata.addHistory(history, "press") boatdata.addHistory(history, "press")
# Tracker data # 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']['host'] = config.get('tracker', 'host')
cfg['tracker']['port'] = config.getint('tracker', 'port') 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_user'] = config.get('tracker', 'mqtt_user')
cfg['tracker']['mqtt_pass'] = config.get('tracker', 'mqtt_pass') 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']['logdir'] = cfg['logdir']
cfg['tracker']['trace'] = config.getboolean('tracker', 'trace') cfg['tracker']['trace'] = config.getboolean('tracker', 'trace')
@ -742,55 +732,76 @@ if __name__ == "__main__":
cfg['boat']['club'] = config.get('boat', 'club') cfg['boat']['club'] = config.get('boat', 'club')
cfg['boat']['team'] = config.get('boat', 'team') 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 # Protokollierung
init_logging(os.path.expanduser(cfg['logdir']), cfg['logfile']) loglevel = set_loglevel(cfg['loglevel'])
log.info("Logging initialized") 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 # Gerät initialisieren u.a. mit den genutzten Seiten
profile = init_profile(config, cfg, boatdata) profile = init_profile(config, cfg, boatdata)
# Schnittstellen aktivieren # Schnittstellen aktivieren, jew. eigener Thread
if cfg['can']: 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 = threading.Thread(target=rxd_n2k, args=(cfg['can_intf'],))
t_rxd_n2k.start() t_rxd_n2k.start()
if cfg['nmea0183']: 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 = threading.Thread(target=nmea0183.rxd_0183, args=(appdata,boatdata,cfg['0183_port'],))
t_rxd_0183.start() t_rxd_0183.start()
if cfg['gps']: 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 = threading.Thread(target=rxd_gps, args=(cfg['gps_port'],))
t_rxd_gps.start() t_rxd_gps.start()
if cfg['network']: 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 = threading.Thread(target=rxd_network, args=(cfg['net_port'],cfg['net_addr']))
t_rxd_net.start() t_rxd_net.start()
if cfg['tracker']['type'] != 'NONE': 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 = threading.Thread(target=appdata.track.mqtt_tracker, args=(cfg['tracker'],cfg['boat'],appdata,boatdata))
t_tracker.start() t_tracker.start()
if not cfg['simulation']: if not cfg['simulation']:
if cfg['bme280']: if cfg['bme280']:
log.info("Environment sensor enabled")
t_data = threading.Thread(target=datareader, args=(cfg, history)) t_data = threading.Thread(target=datareader, args=(cfg, history))
t_data.start() t_data.start()
else: else:
print("Simulation mode enabled") log.info("Simulation mode enabled")
app = Frontend(cfg, appdata, owndevice, boatdata, profile) app = Frontend(cfg, appdata, owndevice, boatdata, profile)
app.run() app.run()
@ -808,5 +819,6 @@ if __name__ == "__main__":
if not cfg['simulation'] and cfg['bme280']: if not cfg['simulation'] and cfg['bme280']:
t_data.join() t_data.join()
log.info("Client terminated")
print(boatdata) print(boatdata)
print("Another fine product of the Sirius Cybernetics Corporation.") print("Another fine product of the Sirius Cybernetics Corporation.")

View File

@ -32,10 +32,10 @@ from .dst810 import DST810
from .epropulsion import EPropulsion from .epropulsion import EPropulsion
from .keel import Keel from .keel import Keel
from .mob import MOB from .mob import MOB
from .racetracker import RaceTracker
from .rollpitch import RollPitch from .rollpitch import RollPitch
from .skyview import SkyView from .skyview import SkyView
from .solar import Solar from .solar import Solar
from .tracker import Tracker
from .rudder import Rudder from .rudder import Rudder
from .voltage import Voltage from .voltage import Voltage
from .wind import Wind from .wind import Wind

View File

@ -68,7 +68,7 @@ class Page():
self.pageno = pageno self.pageno = pageno
self.cfg = cfg self.cfg = cfg
self.fullscreen = cfg['guistyle'] == 'fullscreen' self.fullscreen = cfg['guistyle'] == 'fullscreen'
self.appdata = appdata self.app = appdata
self.bd = boatdata self.bd = boatdata
self.header = True self.header = True
self.footer = True self.footer = True
@ -142,7 +142,7 @@ class Page():
ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD) ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
ctx.set_font_size(16) ctx.set_font_size(16)
ctx.move_to(0.5, 14.5) 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() ctx.stroke()
# Tastenstatus # Tastenstatus

View File

@ -25,7 +25,7 @@ import os
import cairo import cairo
from .page import Page from .page import Page
class Tracker(Page): class RaceTracker(Page):
def __init__(self, pageno, cfg, appdata, boatdata): def __init__(self, pageno, cfg, appdata, boatdata):
super().__init__(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_lon = boatdata.getRef("LON")
self.bv_sog = boatdata.getRef("SOG") self.bv_sog = boatdata.getRef("SOG")
self.races = None self.races = None
self.raceid = None # Ausgewählte Regatta self.raceid = self.app.track.hero_raceid # Ausgewählte Regatta
self.menupos = 0 self.menupos = 0
self.buttonlabel[1] = 'MODE' self.buttonlabel[1] = 'MODE'
self.buttonlabel[2] = 'INFO' self.buttonlabel[2] = 'INFO'
@ -45,9 +45,9 @@ class Tracker(Page):
# Flaggen laden # Flaggen laden
flag = ('alpha', 'answer', 'black', 'blue', 'charlie', 'class', flag = ('alpha', 'answer', 'black', 'blue', 'charlie', 'class',
'finish', 'hotel', 'india', 'november', 'orange', 'finish', 'foxtrot', 'hotel', 'india', 'november',
'papa', 'repeat_one', 'sierra', 'start', 'uniform', 'orange', 'papa', 'repeat_one', 'sierra', 'start',
'xray', 'yankee', 'zulu') 'uniform', 'xray', 'yankee', 'zulu')
# Mapping # Mapping
self.flagmap = { self.flagmap = {
3: 'blue', # Zielflagge 3: 'blue', # Zielflagge
@ -81,7 +81,7 @@ class Tracker(Page):
self.buttonlabel[2] = '#UP' self.buttonlabel[2] = '#UP'
self.buttonlabel[3] = '#DOWN' self.buttonlabel[3] = '#DOWN'
self.buttonlabel[4] = 'SET' self.buttonlabel[4] = 'SET'
if self.appdata.track.is_active(): if self.app.track.is_active():
self.buttonlabel[5] = 'OFF' self.buttonlabel[5] = 'OFF'
else: else:
self.buttonlabel[5] = 'ON' self.buttonlabel[5] = 'ON'
@ -118,38 +118,33 @@ class Tracker(Page):
# Set / Select regatta # Set / Select regatta
if self.menupos > 0: if self.menupos > 0:
self.raceid = self.races[self.menupos - 1] # Nullbasiert 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}'") print(f"Selected race '{self.raceid}'")
return True return True
elif buttonid == 5: elif buttonid == 5:
if self.mode == 'C': if self.mode == 'C':
# Tracking ein/-ausschalten # Tracking ein/-ausschalten
if self.appdata.track.is_active(): if self.app.track.is_active():
self.appdata.track.set_active(False) self.app.track.set_active(False)
self.buttonlabel[5] = 'ON' self.buttonlabel[5] = 'ON'
else: else:
self.appdata.track.set_active(True) self.app.track.set_active(True)
self.buttonlabel[5] = 'OFF' self.buttonlabel[5] = 'OFF'
elif self.mode == 'M': elif self.mode == 'M':
self.appdata.frontend.flashled.setColor('yellow') self.app.frontend.flashled.setColor('yellow')
#self.appdata.frontend.flashled.switchOn(4) #self.app.frontend.flashled.switchOn(4)
self.appdata.frontend.flashled.doFlash(2) self.app.frontend.flashled.doFlash(2)
return True return True
return False return False
def draw_normal(self, ctx): 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.select_font_face("DSEG7 Classic")
ctx.set_font_size(80) ctx.set_font_size(80)
if self.appdata.track.is_active(): if self.app.track.is_active():
if self.appdata.track.hero_racestatus: if self.app.track.hero_racestatus:
counter = self.appdata.track.hero_racestatus['time'] counter = self.app.track.hero_racestatus['time']
minutes, seconds = divmod(abs(counter), 60) minutes, seconds = divmod(abs(counter), 60)
if counter < 0: if counter < 0:
ctx.move_to(16, 120) ctx.move_to(16, 120)
@ -164,11 +159,11 @@ class Tracker(Page):
ctx.move_to(100, 120) ctx.move_to(100, 120)
ctx.show_text("off") 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.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
ctx.set_font_size(16) ctx.set_font_size(16)
ctx.move_to(8, 260) 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 x0 = 8
x1 = 96 x1 = 96
@ -179,20 +174,20 @@ class Tracker(Page):
ctx.move_to(x0, y0) ctx.move_to(x0, y0)
ctx.show_text("Type") ctx.show_text("Type")
ctx.move_to(x1, y0) ctx.move_to(x1, y0)
ctx.show_text(self.appdata.track.ttype) ctx.show_text(self.app.track.ttype)
y0 += yoffset y0 += yoffset
ctx.move_to(x0, y0) ctx.move_to(x0, y0)
ctx.show_text("Regatta") ctx.show_text("Regatta")
ctx.move_to(x1, y0) 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 y0 += yoffset
ctx.move_to(x0, y0) ctx.move_to(x0, y0)
ctx.show_text("Course") ctx.show_text("Course")
ctx.move_to(x1, y0) ctx.move_to(x1, y0)
if self.appdata.track.hero_orgstatus and self.appdata.track.hero_raceid: if self.app.track.hero_orgstatus and self.app.track.hero_raceid:
ctx.show_text(self.appdata.track.hero_orgstatus['races'][self.appdata.track.hero_raceid]['courseid']) ctx.show_text(self.app.track.hero_orgstatus['races'][self.app.track.hero_raceid]['courseid'])
else: else:
ctx.show_text('[not selected]') ctx.show_text('[not selected]')
@ -215,9 +210,9 @@ class Tracker(Page):
ctx.show_text(self.bv_sog.format()) ctx.show_text(self.bv_sog.format())
# Flaggen # Flaggen
if self.appdata.track.hero_racestatus: if self.app.track.hero_racestatus:
pos = 0 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: if f in self.flagmap:
# TODO Context save/restore erforderlich? # TODO Context save/restore erforderlich?
ctx.save() ctx.save()
@ -234,14 +229,19 @@ class Tracker(Page):
ctx.move_to(4, 42) ctx.move_to(4, 42)
ctx.show_text("Tracker configuration") ctx.show_text("Tracker configuration")
x0 = 8 # Linke Spalte mit Daten
x1 = 88
y0 = 96
x0 = 8 # Labelspalte
x1 = 88 # Datenspalte
y0 = 75
yoffset = 16
# Bootsdaten
ctx.set_font_size(20) ctx.set_font_size(20)
ctx.move_to(x0, 75) ctx.move_to(x0, y0)
ctx.show_text("Boat data") ctx.show_text("Boat data")
y0 += yoffset + 5
ctx.set_font_size(16) ctx.set_font_size(16)
ctx.move_to(x0, y0) ctx.move_to(x0, y0)
@ -249,48 +249,50 @@ class Tracker(Page):
ctx.move_to(x1, y0) ctx.move_to(x1, y0)
ctx.show_text(self.cfg['boat']['name']) 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.show_text("Class")
ctx.move_to(x1, y0 + 16) ctx.move_to(x1, y0)
ctx.show_text(self.cfg['boat']['class']) 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.show_text("Handicap")
ctx.move_to(x1, y0 + 32) ctx.move_to(x1, y0)
ctx.show_text(str(self.cfg['boat']['handicap'])) 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.show_text("Club")
ctx.move_to(x1, y0 + 48) ctx.move_to(x1, y0)
ctx.show_text(self.cfg['boat']['club']) 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.show_text("Sailno.")
ctx.move_to(x1, y0 + 64) ctx.move_to(x1, y0)
ctx.show_text(self.cfg['boat']['sailno']) ctx.show_text(self.cfg['boat']['sailno'])
x0 = 208 # Trackerdaten
x1 = 272 y0 += yoffset + 10
y0 = 96
yoffset = 16
ctx.set_font_size(20) ctx.set_font_size(20)
ctx.move_to(x0, 75) ctx.move_to(x0, y0)
ctx.show_text("Tracker info") ctx.show_text("Tracker info")
y0 += yoffset + 5
ctx.set_font_size(16) ctx.set_font_size(16)
ctx.move_to(x0, y0) ctx.move_to(x0, y0)
ctx.show_text("Type") ctx.show_text("Type")
ctx.move_to(x1, y0) ctx.move_to(x1, y0)
ctx.show_text(self.appdata.track.ttype) ctx.show_text(self.app.track.ttype)
y0 += yoffset y0 += yoffset
ctx.move_to(x0, y0) ctx.move_to(x0, y0)
ctx.show_text("Org.") ctx.show_text("Org.")
ctx.move_to(x1, y0) ctx.move_to(x1, y0)
if self.appdata.track.hero_orgstatus: if self.app.track.hero_orgstatus:
ctx.show_text(self.appdata.track.hero_orgstatus['orgname']) ctx.show_text(self.app.track.hero_orgstatus['orgname'])
else: else:
ctx.show_text("n/a") ctx.show_text("n/a")
@ -298,7 +300,7 @@ class Tracker(Page):
ctx.move_to(x0, y0) ctx.move_to(x0, y0)
ctx.show_text("Status") ctx.show_text("Status")
ctx.move_to(x1, y0) ctx.move_to(x1, y0)
if not self.appdata.track.hero_racestatus: if not self.app.track.hero_racestatus:
ctx.show_text("inactive") ctx.show_text("inactive")
else: else:
#TODO Mehr Details #TODO Mehr Details
@ -308,18 +310,29 @@ class Tracker(Page):
ctx.move_to(x0, y0) ctx.move_to(x0, y0)
ctx.show_text("Team") ctx.show_text("Team")
ctx.move_to(x1, y0) ctx.move_to(x1, y0)
if self.appdata.track.hero_racestatus: ctx.show_text(self.app.track.team)
ctx.show_text(self.appdata.track.hero_racestatus['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: else:
ctx.show_text("n/a") ctx.show_text("offline")
# Rechte Spalte mit Regattaauswahl
x = 208
y = 75
yoffset = 16
# Mögliche Regatten # Mögliche Regatten
self.races = self.appdata.track.hero_get_races() self.races = self.app.track.hero_get_races()
if len(self.races) == 1: if len(self.races) == 1:
self.raceid = self.races[0] self.hero_raceid = self.races[0]
self.menupos = 1 self.menupos = 1
x = 208
y = 180
ctx.set_font_size(20) ctx.set_font_size(20)
ctx.move_to(x, y) ctx.move_to(x, y)
ctx.show_text("Select Regatta") ctx.show_text("Select Regatta")
@ -333,7 +346,7 @@ class Tracker(Page):
for r in self.races: for r in self.races:
i += 1 i += 1
if r == self.raceid: if r == self.raceid:
r += '*' r = f"\xbb {r} \xab"
self.draw_text_boxed(ctx, x, y, 180, 20, r, (self.menupos == i)) self.draw_text_boxed(ctx, x, y, 180, 20, r, (self.menupos == i))
y += 20 y += 20
if i == 0: if i == 0:
@ -350,11 +363,11 @@ class Tracker(Page):
ctx.show_text("Message from race officers") ctx.show_text("Message from race officers")
ctx.set_font_size(16) 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.move_to(8, 72)
ctx.show_text("[ empty ]") ctx.show_text("[ empty ]")
else: else:
lines = self.appdata.track.hero_orgstatus['message'].splitlines() lines = self.app.track.hero_orgstatus['message'].splitlines()
y = 72 y = 72
for l in lines: for l in lines:
ctx.move_to(8, y) ctx.move_to(8, y)

View File

@ -14,49 +14,87 @@ Wiederherstellung der Verbindung übertragen.
TODO TODO
- Nach einem Disconnect manuelle Neuverbindung ermöglichen - Nach einem Disconnect manuelle Neuverbindung ermöglichen
- Audioausgabe für Regatta Hero
""" """
import os import os
import time import time
import paho.mqtt.client as mqtt import paho.mqtt.client as mqtt
import http.client
import ssl
import json import json
import socket import socket
import ssl
import math
import subprocess # für Audioausgabe / mpg123
class Tracker(): class Tracker():
def __init__(self, trackertype='NONE'): def __init__(self, logger, cfg):
self.ttype = 'NONE' self.log = logger
self.set_type(trackertype)
self.appdata = None self.appdata = None
self.activated = False validtypes = ('HERO', 'SDCARD', 'SERVER', 'LOCAL', 'NONE')
self.trace = False # Debugging 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.trace_fh = None # File Handle der Tracedatei
self.races = set() # Liste der Regatten, eindeutige Namen self.buoys = {} # Tonnen (Hero=20)
self.courses = set() # Liste der Bahnen, eindeutige Namen self.courses = [] # Bahnen
self.races = [] # Regatten
self.mqtt_connected = False
self.activated = False
self.lat = None # last latitude self.lat = None # last latitude
self.lon = None # last longitude self.lon = None # last longitude
self.tspos = None # timestamp (hh:ss:mm) as datetime.time self.tspos = None # timestamp (hh:ss:mm) as datetime.time
self.sog = None 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_orgstatus = None
self.hero_racestatus = None self.hero_racestatus = None
self.hero_raceid = None # Aktuelle Regatta
self.hero_timedelta = 0 # Zeitdifferenz zum Server in sec self.hero_timedelta = 0 # Zeitdifferenz zum Server in sec
# TODO Wirklich alles im Tracker oder ist einiges generisch? # Hole erste Daten vom Server
self.boatid = None self.hero_query_org()
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): def is_server_active(self, hostname, port):
""" """
@ -81,12 +119,157 @@ class Tracker():
def set_hero_raceid(self, newraceid): def set_hero_raceid(self, newraceid):
self.hero_raceid = newraceid self.hero_raceid = newraceid
def set_type(self, newtype): #def set_type(self, newtype):
validtypes = ('HERO', 'SDCARD', 'SERVER', 'NONE') # validtypes = ('HERO', 'SDCARD', 'SERVER', 'LOCAL', 'NONE')
newtype = newtype.upper() # newtype = newtype.upper()
if newtype not in validtypes: # if newtype not in validtypes:
raise TypeError(f"Invalid tracker type: '{newtype}'. Only supported: {validtypes}") # raise TypeError(f"Invalid tracker type: '{newtype}'. Only supported: {validtypes}")
self.ttype = newtype # 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): def get_position(self):
# Positionsabfrage für die Payload # Positionsabfrage für die Payload
@ -109,21 +292,24 @@ class Tracker():
return races return races
def mqtt_on_connect(self, client, userdata, flags, rc): def mqtt_on_connect(self, client, userdata, flags, rc):
print(f"MQTT connected with result code {rc}") self.mqtt_connected = False
#userdata['connect_rc'] = rc if rc == 0:
if rc != 0: self.log.info(f"MQTT connected, subscribing to topics")
# 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/orgstatus/thomas")
client.subscribe("regattahero/racestatus/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): def mqtt_on_message(self, client, userdata, msg):
""" """
@ -147,7 +333,7 @@ class Tracker():
self.hero_timedelta = abs(sec1 - sec2) self.hero_timedelta = abs(sec1 - sec2)
if self.hero_orgstatus['allLogout']: if self.hero_orgstatus['allLogout']:
print("All logout received!") self.log.info("All logout received!")
client.disconnect() client.disconnect()
self.activated = False self.activated = False
return return
@ -180,8 +366,8 @@ class Tracker():
""" """
else: else:
print(f"UNKNOWN TOPIC: {msg.topic}") self.log.warning(f"UNKNOWN TOPIC: {msg.topic}")
print(msg.payload) self.log.debug(msg.payload)
def mqtt_publish(self, client, topic, payload, bv_lat, bv_lon, bv_sog): 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()) payload['gps']['timestamp'] = time.strftime("%Y-%m-%dT%H:%M:%S.000Z", time.gmtime())
client.publish(topic, json.dumps(payload)) client.publish(topic, json.dumps(payload))
else: else:
#print("No GPS data available. Nothing published!") self.log.debug("No GPS data available. Nothing published!")
pass
def mqtt_tracker(self, cfg, boat, appdata, boatdata): def mqtt_tracker(self, cfg, boat, appdata, boatdata):
print("MQTT tracker enabled") self.log.info("MQTT tracker enabled")
self.appdata = appdata self.appdata = appdata
self.boatid = cfg['uuid'] """
self.boatid = boat['uuid']
self.sailno = boat['sailno'] self.sailno = boat['sailno']
self.boatname = boat['name'] self.boatname = boat['name']
self.boatclass = boat['class'] self.boatclass = boat['class']
self.handicap = boat['handicap'] self.handicap = boat['handicap']
self.club = boat['club'] self.club = boat['club']
self.team = boat['team'] # TODO eher zu Tracker gehörig? self.team = boat['team']
self.trace = cfg['trace'] self.trace = cfg['trace']
self.hero_orgid = cfg['orgname'] self.hero_orgid = cfg['username']
client = mqtt.Client() """
client.on_connect = self.mqtt_on_connect self.client.username_pw_set(username=cfg['mqtt_user'], password=cfg['mqtt_pass'])
client.on_message = self.mqtt_on_message
client.username_pw_set(username=cfg['mqtt_user'], password=cfg['mqtt_pass'])
try: try:
client.connect(cfg['host'], cfg['port'], 60) self.client.connect(cfg['mqtt_host'], cfg['mqtt_port'], 60)
except ConnectionRefusedError: except ConnectionRefusedError:
print("MQTT connection refused. Check username and password.") self.log.error("MQTT connection refused. Check username and password.")
return return
if cfg['trace']: # Initial die Organisationsdaten abfragen um Tonnen und Kurse zu erhalten
# TODO Log Hinweis # 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') tracefile = os.path.join(os.path.expanduser(cfg['logdir']), 'tracker.log')
self.trace_fh = open(tracefile, 'w+') 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 = { payload = {
"passcode": cfg['passcode'], "passcode": cfg['password'],
"orgid": cfg['orgname'], "orgid": self.hero_orgid,
"raceid": None, # Nach Auswahl einstellen "raceid": None, # Nach Auswahl einstellen
"gps": { "gps": {
"lat": 0.0, "lat": 0.0,
@ -246,13 +433,13 @@ class Tracker():
"timestamp": "" # ISO8601 Format mit Millisekunden in UTC "timestamp": "" # ISO8601 Format mit Millisekunden in UTC
}, },
"boat": { "boat": {
"boatid": cfg['uuid'], "boatid": self.boatid,
"sailno": boat['sailno'], "sailno": self.sailno,
"team": boat['team'], "team": self.team,
"boatclass": boat['class'], "boatclass": self.boatclass,
"handicap": boat['handicap'], "handicap": self.handicap,
"club": boat['club'], "club": self.club,
"boatname": boat['name'], "boatname": self.boatname,
"isTracking": True, "isTracking": True,
"hasGivenUp": False "hasGivenUp": False
}, },
@ -270,12 +457,12 @@ class Tracker():
bv_lon = boatdata.getRef("LON") bv_lon = boatdata.getRef("LON")
bv_sog = boatdata.getRef("SOG") bv_sog = boatdata.getRef("SOG")
client.loop_start() self.client.loop_start()
while not appdata.shutdown: while not appdata.shutdown:
time.sleep(1) time.sleep(1)
if self.activated and self.hero_raceid is not None: if self.activated and self.hero_raceid is not None:
self.mqtt_publish(client, topic, payload, bv_lat, bv_lon, bv_sog) self.mqtt_publish(topic, payload, bv_lat, bv_lon, bv_sog)
client.loop_stop() self.client.loop_stop()
client.disconnect() self.client.disconnect()
if cfg['trace']: if cfg['trace']:
self.trace_fh.close() self.trace_fh.close()