Tracker in eigene Klasse und AppData eingeführt

This commit is contained in:
Thomas Hooge 2025-09-12 11:31:24 +02:00
parent fd673d5e55
commit acbcfac425
22 changed files with 225 additions and 167 deletions

10
appdata.py Normal file
View File

@ -0,0 +1,10 @@
"""
Generische Applikationsdaten
"""
from tracker import Tracker
class AppData():
def __init__(self):
self.shutdown = False # Globaler Ausschalter
self.track = Tracker('NONE')

173
obp60v.py
View File

@ -104,12 +104,14 @@ import time
from datetime import datetime from datetime import datetime
from nmea2000 import Device, BoatData, History, HistoryBuffer from nmea2000 import Device, BoatData, History, HistoryBuffer
from nmea2000 import parser from nmea2000 import parser
import nmea0183
import pages
import struct import struct
import uuid import uuid
import json import json
import nmea0183
from appdata import AppData
import pages
__author__ = "Thomas Hooge" __author__ = "Thomas Hooge"
__copyright__ = "Copyleft 2024-2025, all rights reversed" __copyright__ = "Copyleft 2024-2025, all rights reversed"
__version__ = "0.2" __version__ = "0.2"
@ -118,6 +120,8 @@ __status__ = "Development"
cfg = { cfg = {
'cfgfile': 'obp60v.conf', 'cfgfile': 'obp60v.conf',
'logdir': '~/.local/share/obp60v',
'logfile': 'obp60v.log',
'imgpath': os.path.join(sys.path[0], 'images'), 'imgpath': os.path.join(sys.path[0], 'images'),
'deviceid': 100, 'deviceid': 100,
'manufcode': 2046, # Open Boat Projects (OBP) 'manufcode': 2046, # Open Boat Projects (OBP)
@ -130,136 +134,13 @@ cfg = {
'boat': { } 'boat': { }
} }
def mqtt_on_connect(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(client, userdata, msg):
"""
TODO raceid über userdata? dann topic prüfen?
"""
if msg.topic == "regattahero/orgstatus/thomas":
# kommt alle 10s
orgstatus = json.loads(msg.payload)
if orgstatus['allLogout']:
print("All logout received!")
client.disconnect()
sys.exit(0) # TODO nur die MQTT-Task beenden
if orgstatus['message']:
# TODO Alarm-Funktion nutzen?
print("Nachricht der Wettfahrtkeitung:")
print(orgstatus['message'])
print(orgstatus['races'])
#for r in orgstatus['races']:
# print(f"Race: {r}")
elif msg.topic.startswith("regattahero/racestatus/thomas"):
# kommt alle 1s
# dem Topic angehängt ist noch die raceid
payload = json.loads(msg.payload)
racestatus = payload['racestatus']
"""
time: negativ: Zeit vor dem Start, positiv: Zeit nach dem Start
in Sekunden
Signale der Wettfahrtleitung hier anzeigen
Regattaabbruch
Bahnverkürzung
Rückrufe
"""
else:
print(f"UNKNOWN TOPIC: {msg.topic}")
print(msg.payload)
def mqtt_publish(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
"""
lat = bv_lat.getValueRaw()
lon = bv_lon.getValueRaw()
sog = bv_sog.getValueRaw()
if lat and lon and sog:
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())
client.publish(topic, json.dumps(payload))
else:
print("No GPS data available. Nothing published!")
def mqtt_tracker(cfg, boat):
import paho.mqtt.client as mqtt
print("MQTT tracker enabled")
client = mqtt.Client()
client.on_connect = mqtt_on_connect
client.on_message = 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
topic = "regattahero/tracker/" + cfg['orgname']
payload = {
"passcode": cfg['passcode'],
"orgid": cfg['orgname'],
"raceid": "Demo Regatta", # TODO aus Selektion einstellen
"gps": {
"lat": 0.0,
"lon": 0.0,
"speed": 0.0,
"age": 1000,
"odo": 1000,
"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']
}
}
# 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 shutdown:
time.sleep(1)
if tracker_active:
mqtt_publish(client, topic, payload, bv_lat, bv_lon, bv_sog)
client.loop_stop()
client.disconnect()
def rxd_n2k(device): def rxd_n2k(device):
setthreadtitle("N2Klistener") setthreadtitle("N2Klistener")
bus = can.Bus(interface='socketcan', channel=device, bitrate=250000); bus = can.Bus(interface='socketcan', channel=device, bitrate=250000);
wip = False wip = False
sc = 0 sc = 0
nf = 0 nf = 0
while not shutdown: while not appdata.shutdown:
msg = bus.recv(2) msg = bus.recv(2)
if not msg: if not msg:
continue continue
@ -332,7 +213,7 @@ def rxd_0183(devname):
print("NMEA0183 serial port not available") print("NMEA0183 serial port not available")
return return
setthreadtitle("0183listener") setthreadtitle("0183listener")
while not shutdown: while not appdata.shutdown:
raw = ser.readline().decode('ascii') raw = ser.readline().decode('ascii')
if len(raw.strip()) == 0: if len(raw.strip()) == 0:
continue continue
@ -372,7 +253,7 @@ def rxd_gps(devname, devspeed):
print("GPS serial port not available") print("GPS serial port not available")
return return
setthreadtitle("GPSlistener") setthreadtitle("GPSlistener")
while not shutdown: while not appdata.shutdown:
try: try:
msg = pynmea2.parse(ser.readline().decode('ascii')) msg = pynmea2.parse(ser.readline().decode('ascii'))
except pynmea2.nmea.ParseError: except pynmea2.nmea.ParseError:
@ -394,7 +275,7 @@ def rxd_network(address, port):
# Wir verwenden UDP. Ein verlorenes Paket tut uns nicht weh. # Wir verwenden UDP. Ein verlorenes Paket tut uns nicht weh.
sock = socket.socket() sock = socket.socket()
sock.connect((address, port)) sock.connect((address, port))
while not shutdown: while not appdata.shutdown:
time.sleep(0.5) time.sleep(0.5)
sock.close() sock.close()
@ -433,7 +314,7 @@ def datareader(cfg, history):
g = g_tick(1) g = g_tick(1)
n = 0 n = 0
while not shutdown: while not appdata.shutdown:
time.sleep(next(g)) time.sleep(next(g))
# BME280 abfragen # BME280 abfragen
if cfg['bme280']: if cfg['bme280']:
@ -454,8 +335,9 @@ def datareader(cfg, history):
class Frontend(Gtk.Window): class Frontend(Gtk.Window):
def __init__(self, cfg, device, boatdata, profile): def __init__(self, cfg, appdata, device, boatdata, profile):
super().__init__() super().__init__()
self.appdata = appdata
self.owndev = device self.owndev = device
self.boatdata = boatdata self.boatdata = boatdata
self._config = cfg['_config'] self._config = cfg['_config']
@ -740,10 +622,10 @@ def init_profile(config, cfg, boatdata):
# Nummer und Art ermitteln # Nummer und Art ermitteln
pageno = int(s[4:]) pageno = int(s[4:])
pagedef[pageno] = {'type': config.get(s, "type")} pagedef[pageno] = {'type': config.get(s, "type")}
# Hole ein bin maximal 4 Werte je Seite # Hole ein bin maximal 6 Werte je Seite
values = {} values = {}
valno = 1 valno = 1
for i in (1, 2, 3, 4): for i in (1, 2, 3, 4, 5, 6):
try: try:
values[i] = config.get(s, f"value{i}") values[i] = config.get(s, f"value{i}")
except configparser.NoOptionError: except configparser.NoOptionError:
@ -762,10 +644,10 @@ def init_profile(config, cfg, boatdata):
# Klasse nicht vorhanden, Seite wird nicht benutzt # Klasse nicht vorhanden, Seite wird nicht benutzt
print(f"Klasse '{p['type']}' nicht gefunden") print(f"Klasse '{p['type']}' nicht gefunden")
continue continue
c = cls(i, cfg, 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
return clist return clist
def set_loglevel(nr): def set_loglevel(nr):
""" """
Umsetzen der Nummer auf einen für "logging" passenden Wert Umsetzen der Nummer auf einen für "logging" passenden Wert
@ -779,11 +661,11 @@ def set_loglevel(nr):
nr = 0 nr = 0
return level[nr] return level[nr]
def init_logging(logdir): def init_logging(logdir, logfile='obp60v.log'):
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]))
hdlr = logging.handlers.RotatingFileHandler(os.path.join(logdir, 'obp60v.log'), maxBytes=5242880, backupCount=5) hdlr = logging.handlers.RotatingFileHandler(os.path.join(logdir, logfile), maxBytes=5242880, backupCount=5)
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)
@ -797,8 +679,8 @@ if __name__ == "__main__":
setproctitle("obp60v") setproctitle("obp60v")
shutdown = False # Globale Daten, u.a. auch Shutdown-Indikator
tracker_active = False appdata = AppData()
owndevice = Device(100) owndevice = Device(100)
# Hardcoding device, not intended to change # Hardcoding device, not intended to change
@ -867,6 +749,7 @@ if __name__ == "__main__":
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']['orgname'] = config.get('tracker', 'orgname')
cfg['tracker']['passcode'] = config.get('tracker', 'passcode') cfg['tracker']['passcode'] = config.get('tracker', 'passcode')
cfg['tracker']['logdir'] = cfg['logdir']
# Boat data # Boat data
cfg['boat']['name'] = config.get('boat', 'name') cfg['boat']['name'] = config.get('boat', 'name')
@ -892,7 +775,7 @@ if __name__ == "__main__":
boatdata.enableSimulation() boatdata.enableSimulation()
# Protokollierung # Protokollierung
init_logging(os.path.expanduser("~/.local/share/obp60v")) init_logging(os.path.expanduser(cfg['logdir']), cfg['logfile'])
log.info("Logging initialized") log.info("Logging initialized")
# Gerät initialisieren u.a. mit den genutzten Seiten # Gerät initialisieren u.a. mit den genutzten Seiten
@ -916,7 +799,7 @@ if __name__ == "__main__":
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':
t_tracker = threading.Thread(target=mqtt_tracker, args=(cfg['tracker'],cfg['boat'])) 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']:
@ -925,9 +808,9 @@ if __name__ == "__main__":
else: else:
print("Simulation mode enabled") print("Simulation mode enabled")
app = Frontend(cfg, owndevice, boatdata, profile) app = Frontend(cfg, appdata, owndevice, boatdata, profile)
app.run() app.run()
shutdown = True appdata.shutdown = True
if cfg['can']: if cfg['can']:
t_rxd_n2k.join() t_rxd_n2k.join()
if cfg['nmea0183']: if cfg['nmea0183']:
@ -936,8 +819,10 @@ if __name__ == "__main__":
t_rxd_gps.join() t_rxd_gps.join()
if cfg['network']: if cfg['network']:
t_rxd_net.join() t_rxd_net.join()
if cfg['tracker']['type'] != 'NONE':
t_tracker.join()
if not cfg['simulation'] and cfg['bme280']: if not cfg['simulation'] and cfg['bme280']:
t_data.join() t_data.join()
print("Another fine product of the Sirius Cybernetics Corporation.")
print(boatdata) print(boatdata)
print("Another fine product of the Sirius Cybernetics Corporation.")

View File

@ -36,7 +36,7 @@ from .page import Page
class Anchor(Page): class Anchor(Page):
def __init__(self, pageno, cfg, boatdata): def __init__(self, pageno, cfg, appdata, boatdata):
super().__init__(pageno, cfg, boatdata) super().__init__(pageno, cfg, boatdata)
self.sym_anchor = cairo.ImageSurface.create_from_png(os.path.join(cfg['imgpath'], "anchor.png")) self.sym_anchor = cairo.ImageSurface.create_from_png(os.path.join(cfg['imgpath'], "anchor.png"))
self.buttonlabel[1] = 'MODE' self.buttonlabel[1] = 'MODE'

View File

@ -5,7 +5,7 @@ from .page import Page
class ApparentWind(Page): class ApparentWind(Page):
def __init__(self, pageno, cfg, boatdata): def __init__(self, pageno, cfg, appdata, boatdata):
super().__init__(pageno, cfg, boatdata) super().__init__(pageno, cfg, boatdata)
self.buttonlabel[1] = 'MODE' self.buttonlabel[1] = 'MODE'
self.mode = 'L' # (W)ind (L)ens self.mode = 'L' # (W)ind (L)ens

View File

@ -11,7 +11,7 @@ from .page import Page
class Autobahn(Page): class Autobahn(Page):
def __init__(self, pageno, cfg, boatdata): def __init__(self, pageno, cfg, appdata, boatdata):
super().__init__(pageno, cfg, boatdata) super().__init__(pageno, cfg, boatdata)
self.xte = self.bd.getRef("XTE") self.xte = self.bd.getRef("XTE")
self.cog = self.bd.getRef("COG") self.cog = self.bd.getRef("COG")

View File

@ -40,7 +40,7 @@ from .page import Page
class Barograph(Page): class Barograph(Page):
def __init__(self, pageno, cfg, boatdata): def __init__(self, pageno, cfg, appdata, boatdata):
super().__init__(pageno, cfg, boatdata) super().__init__(pageno, cfg, boatdata)
# Meßwert alle 15 Minuten: # Meßwert alle 15 Minuten:
# 84 Stunden * 4 Werte je Stunde = 336 Meßwerte # 84 Stunden * 4 Werte je Stunde = 336 Meßwerte

View File

@ -12,7 +12,7 @@ class Battery(Page):
avg = (1, 10, 60, 300); avg = (1, 10, 60, 300);
def __init__(self, pageno, cfg, boatdata): def __init__(self, pageno, cfg, appdata, boatdata):
super().__init__(pageno, cfg, boatdata) super().__init__(pageno, cfg, boatdata)
self.avgindex = 0 self.avgindex = 0
self.buttonlabel[1] = 'AVG' self.buttonlabel[1] = 'AVG'

View File

@ -9,7 +9,7 @@ from .page import Page
class BME280(Page): class BME280(Page):
def __init__(self, pageno, cfg, boatdata): def __init__(self, pageno, cfg, appdata, boatdata):
super().__init__(pageno, cfg, boatdata) super().__init__(pageno, cfg, boatdata)
#self.ref1 = self.bd.getRef(boatvalue1) #self.ref1 = self.bd.getRef(boatvalue1)
#self.ref2 = self.bd.getRef(boatvalue2) #self.ref2 = self.bd.getRef(boatvalue2)

View File

@ -20,7 +20,7 @@ import astral
class Clock(Page): class Clock(Page):
def __init__(self, pageno, cfg, boatdata): def __init__(self, pageno, cfg, appdata, boatdata):
super().__init__(pageno, cfg, boatdata) super().__init__(pageno, cfg, boatdata)
self.buttonlabel[1] = 'MODE' self.buttonlabel[1] = 'MODE'
self.buttonlabel[2] = 'TZ' self.buttonlabel[2] = 'TZ'

View File

@ -22,7 +22,7 @@ import nmea2000.lookup
class Fluid(Page): class Fluid(Page):
def __init__(self, pageno, cfg, boatdata, fluidtype): def __init__(self, pageno, cfg, appdata, boatdata, fluidtype):
super().__init__(pageno, cfg, boatdata) super().__init__(pageno, cfg, boatdata)
self.fluidtype = int(fluidtype) self.fluidtype = int(fluidtype)
if self.fluidtype == 0: if self.fluidtype == 0:

View File

@ -20,7 +20,7 @@ from .page import Page
class FourValues(Page): class FourValues(Page):
def __init__(self, pageno, cfg, boatdata, boatvalue1, boatvalue2, boatvalue3, boatvalue4): def __init__(self, pageno, cfg, appdata, boatdata, boatvalue1, boatvalue2, boatvalue3, boatvalue4):
super().__init__(pageno, cfg, boatdata) super().__init__(pageno, cfg, boatdata)
self.value1 = boatvalue1 self.value1 = boatvalue1
self.value2 = boatvalue2 self.value2 = boatvalue2

View File

@ -18,7 +18,7 @@ from .page import Page
class FourValues2(Page): class FourValues2(Page):
def __init__(self, pageno, cfg, boatdata, boatvalue1, boatvalue2, boatvalue3, boatvalue4): def __init__(self, pageno, cfg, appdata, boatdata, boatvalue1, boatvalue2, boatvalue3, boatvalue4):
super().__init__(pageno, cfg, boatdata) super().__init__(pageno, cfg, boatdata)
self.value1 = boatvalue1 self.value1 = boatvalue1
self.value2 = boatvalue2 self.value2 = boatvalue2

View File

@ -15,7 +15,7 @@ from .page import Page
class Keel(Page): class Keel(Page):
def __init__(self, pageno, cfg, boatdata): def __init__(self, pageno, cfg, appdata, boatdata):
super().__init__(pageno, cfg, boatdata) super().__init__(pageno, cfg, boatdata)
# Wert für Kielrotation # Wert für Kielrotation
self.valref = self.bd.getRef("xdrRotK") self.valref = self.bd.getRef("xdrRotK")

View File

@ -3,7 +3,7 @@ from .page import Page
class OneValue(Page): class OneValue(Page):
def __init__(self, pageno, cfg, boatdata, boatvalue): def __init__(self, pageno, cfg, appdata, boatdata, boatvalue):
super().__init__(pageno, cfg, boatdata) super().__init__(pageno, cfg, boatdata)
self.ref1 = self.bd.getRef(boatvalue) self.ref1 = self.bd.getRef(boatvalue)

View File

@ -4,7 +4,7 @@ from .page import Page
class Rudder(Page): class Rudder(Page):
def __init__(self, pageno, cfg, boatdata): def __init__(self, pageno, cfg, appdata, boatdata):
super().__init__(pageno, cfg, boatdata) super().__init__(pageno, cfg, boatdata)
self.buttonlabel[1] = 'MODE' self.buttonlabel[1] = 'MODE'
self.mode = 'P' self.mode = 'P'

View File

@ -18,7 +18,8 @@ from .page import Page
class SixValues(Page): class SixValues(Page):
def __init__(self, pageno, cfg, boatdata, boatvalue1, boatvalue2, boatvalue3, boatvalue4): def __init__(self, pageno, cfg, appdata, boatdata, boatvalue1, boatvalue2,
boatvalue3, boatvalue4, boatvalue5, boatvalue6):
super().__init__(pageno, cfg, boatdata) super().__init__(pageno, cfg, boatdata)
self.value1 = boatvalue1 self.value1 = boatvalue1
self.value2 = boatvalue2 self.value2 = boatvalue2

View File

@ -14,7 +14,7 @@ from .page import Page
class SkyView(Page): class SkyView(Page):
def __init__(self, pageno, cfg, boatdata): def __init__(self, pageno, cfg, appdata, boatdata):
super().__init__(pageno, cfg, boatdata) super().__init__(pageno, cfg, boatdata)
def pol2cart(azimut, elevation): def pol2cart(azimut, elevation):

View File

@ -3,7 +3,7 @@ from .page import Page
class ThreeValues(Page): class ThreeValues(Page):
def __init__(self, pageno, cfg, boatdata, boatvalue1, boatvalue2, boatvalue3): def __init__(self, pageno, cfg, appdata, boatdata, boatvalue1, boatvalue2, boatvalue3):
super().__init__(pageno, cfg, boatdata) super().__init__(pageno, cfg, boatdata)
self.ref1 = self.bd.getRef(boatvalue1) self.ref1 = self.bd.getRef(boatvalue1)
self.ref2 = self.bd.getRef(boatvalue2) self.ref2 = self.bd.getRef(boatvalue2)

View File

@ -11,14 +11,18 @@ from .page import Page
class Tracker(Page): class Tracker(Page):
def __init__(self, pageno, cfg, boatdata): def __init__(self, pageno, cfg, appdata, boatdata):
super().__init__(pageno, cfg, boatdata) super().__init__(pageno, cfg, boatdata)
self._appdata = appdata
self.buttonlabel[1] = 'MODE' self.buttonlabel[1] = 'MODE'
print(cfg)
def handle_key(self, buttonid): def handle_key(self, buttonid):
global tracker_active;
if buttonid == 1: if buttonid == 1:
tracker_active = not tracker_active if self._appdata.track.is_active():
self._appdata.track.set_active(False)
else:
self._appdata.track.set_active(True)
def draw(self, ctx): def draw(self, ctx):
# Name # Name
@ -30,7 +34,7 @@ class Tracker(Page):
ctx.set_font_size(16) ctx.set_font_size(16)
ctx.move_to(20, 140) ctx.move_to(20, 140)
ctx.show_text("active: ") ctx.show_text("active: ")
if tracker_active: if self._appdata.track.is_active():
ctx.show_text("yes") ctx.show_text("yes")
else: else:
ctx.show_text("no") ctx.show_text("no")

View File

@ -16,7 +16,7 @@ from .page import Page
class TwoValues(Page): class TwoValues(Page):
def __init__(self, pageno, cfg, boatdata, boatvalue1, boatvalue2): def __init__(self, pageno, cfg, appdata, boatdata, boatvalue1, boatvalue2):
super().__init__(pageno, cfg, boatdata) super().__init__(pageno, cfg, boatdata)
self.ref1 = self.bd.getRef(boatvalue1) self.ref1 = self.bd.getRef(boatvalue1)
self.ref2 = self.bd.getRef(boatvalue2) self.ref2 = self.bd.getRef(boatvalue2)

View File

@ -17,7 +17,7 @@ class Voltage(Page):
avg = (1, 10, 60, 300); avg = (1, 10, 60, 300);
def __init__(self, pageno, cfg, boatdata): def __init__(self, pageno, cfg, appdata, boatdata):
super().__init__(pageno, cfg, boatdata) super().__init__(pageno, cfg, boatdata)
self.trend = True self.trend = True
self.mode = 'A' self.mode = 'A'

View File

@ -13,12 +13,170 @@ Wiederherstellung der Verbindung übertragen.
""" """
import os
import time
import paho.mqtt.client as mqtt
import json
class Tracker(): class Tracker():
def __init__(self, trackertype): def __init__(self, trackertype='NONE'):
validtypes = ('HERO', 'SDCARD', 'SERVER', 'NONE') validtypes = ('HERO', 'SDCARD', 'SERVER', 'NONE')
trackertype = trackertype.upper() trackertype = trackertype.upper()
if trackertype not in validtypes: if trackertype not in validtypes:
raise TypeError(f"Invalid tracker type: '{valtype}'. Only supported: {validtypes}") raise TypeError(f"Invalid tracker type: '{valtype}'. Only supported: {validtypes}")
self.ttype = trackertype self.ttype = trackertype
self.activated = False
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
def is_active(self):
return self.activated
def set_active(self, newval):
self.activated = newval
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 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 msg.topic == "regattahero/orgstatus/thomas":
# kommt alle 10s
orgstatus = json.loads(msg.payload)
if orgstatus['allLogout']:
print("All logout received!")
client.disconnect()
sys.exit(0) # TODO nur die MQTT-Task beenden
if orgstatus['message']:
# TODO Alarm-Funktion nutzen?
print("Nachricht der Wettfahrtkeitung:")
print(orgstatus['message'])
print(orgstatus['races'])
#for r in orgstatus['races']:
# print(f"Race: {r}")
elif msg.topic.startswith("regattahero/racestatus/thomas"):
# kommt alle 1s
# dem Topic angehängt ist noch die raceid
payload = json.loads(msg.payload)
racestatus = payload['racestatus']
"""
time: negativ: Zeit vor dem Start, positiv: Zeit nach dem Start
in Sekunden
Signale der Wettfahrtleitung hier anzeigen
Regattaabbruch
Bahnverkürzung
Rückrufe
"""
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
"""
lat = bv_lat.getValueRaw()
lon = bv_lon.getValueRaw()
sog = bv_sog.getValueRaw()
if lat and lon and sog:
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())
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")
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
tracefile = os.path.join(os.path.expanduser(cfg['logdir']), 'tracker.log')
trace_fh = open(tracefile, 'w+')
client.user_data_set({'trace': trace_fh})
topic = "regattahero/tracker/" + cfg['orgname']
payload = {
"passcode": cfg['passcode'],
"orgid": cfg['orgname'],
"raceid": "Demo Regatta", # TODO aus Selektion einstellen
"gps": {
"lat": 0.0,
"lon": 0.0,
"speed": 0.0,
"age": 1000,
"odo": 1000,
"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']
}
}
# 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 appdata.track.is_active():
self.mqtt_publish(client, topic, payload, bv_lat, bv_lon, bv_sog)
print("MQTT tracker shutdown")
client.loop_stop()
client.disconnect()
trace_fh.close()