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

171
obp60v.py
View File

@ -104,12 +104,14 @@ import time
from datetime import datetime
from nmea2000 import Device, BoatData, History, HistoryBuffer
from nmea2000 import parser
import nmea0183
import pages
import struct
import uuid
import json
import nmea0183
from appdata import AppData
import pages
__author__ = "Thomas Hooge"
__copyright__ = "Copyleft 2024-2025, all rights reversed"
__version__ = "0.2"
@ -118,6 +120,8 @@ __status__ = "Development"
cfg = {
'cfgfile': 'obp60v.conf',
'logdir': '~/.local/share/obp60v',
'logfile': 'obp60v.log',
'imgpath': os.path.join(sys.path[0], 'images'),
'deviceid': 100,
'manufcode': 2046, # Open Boat Projects (OBP)
@ -130,136 +134,13 @@ cfg = {
'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):
setthreadtitle("N2Klistener")
bus = can.Bus(interface='socketcan', channel=device, bitrate=250000);
wip = False
sc = 0
nf = 0
while not shutdown:
while not appdata.shutdown:
msg = bus.recv(2)
if not msg:
continue
@ -332,7 +213,7 @@ def rxd_0183(devname):
print("NMEA0183 serial port not available")
return
setthreadtitle("0183listener")
while not shutdown:
while not appdata.shutdown:
raw = ser.readline().decode('ascii')
if len(raw.strip()) == 0:
continue
@ -372,7 +253,7 @@ def rxd_gps(devname, devspeed):
print("GPS serial port not available")
return
setthreadtitle("GPSlistener")
while not shutdown:
while not appdata.shutdown:
try:
msg = pynmea2.parse(ser.readline().decode('ascii'))
except pynmea2.nmea.ParseError:
@ -394,7 +275,7 @@ def rxd_network(address, port):
# Wir verwenden UDP. Ein verlorenes Paket tut uns nicht weh.
sock = socket.socket()
sock.connect((address, port))
while not shutdown:
while not appdata.shutdown:
time.sleep(0.5)
sock.close()
@ -433,7 +314,7 @@ def datareader(cfg, history):
g = g_tick(1)
n = 0
while not shutdown:
while not appdata.shutdown:
time.sleep(next(g))
# BME280 abfragen
if cfg['bme280']:
@ -454,8 +335,9 @@ def datareader(cfg, history):
class Frontend(Gtk.Window):
def __init__(self, cfg, device, boatdata, profile):
def __init__(self, cfg, appdata, device, boatdata, profile):
super().__init__()
self.appdata = appdata
self.owndev = device
self.boatdata = boatdata
self._config = cfg['_config']
@ -740,10 +622,10 @@ def init_profile(config, cfg, boatdata):
# Nummer und Art ermitteln
pageno = int(s[4:])
pagedef[pageno] = {'type': config.get(s, "type")}
# Hole ein bin maximal 4 Werte je Seite
# Hole ein bin maximal 6 Werte je Seite
values = {}
valno = 1
for i in (1, 2, 3, 4):
for i in (1, 2, 3, 4, 5, 6):
try:
values[i] = config.get(s, f"value{i}")
except configparser.NoOptionError:
@ -762,7 +644,7 @@ def init_profile(config, cfg, boatdata):
# Klasse nicht vorhanden, Seite wird nicht benutzt
print(f"Klasse '{p['type']}' nicht gefunden")
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
return clist
@ -779,11 +661,11 @@ def set_loglevel(nr):
nr = 0
return level[nr]
def init_logging(logdir):
def init_logging(logdir, logfile='obp60v.log'):
global log
os.makedirs(logdir, exist_ok=True)
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')
hdlr.setFormatter(formatter)
log.addHandler(hdlr)
@ -797,8 +679,8 @@ if __name__ == "__main__":
setproctitle("obp60v")
shutdown = False
tracker_active = False
# Globale Daten, u.a. auch Shutdown-Indikator
appdata = AppData()
owndevice = Device(100)
# Hardcoding device, not intended to change
@ -867,6 +749,7 @@ if __name__ == "__main__":
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']
# Boat data
cfg['boat']['name'] = config.get('boat', 'name')
@ -892,7 +775,7 @@ if __name__ == "__main__":
boatdata.enableSimulation()
# Protokollierung
init_logging(os.path.expanduser("~/.local/share/obp60v"))
init_logging(os.path.expanduser(cfg['logdir']), cfg['logfile'])
log.info("Logging initialized")
# 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.start()
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()
if not cfg['simulation']:
if cfg['bme280']:
@ -925,9 +808,9 @@ if __name__ == "__main__":
else:
print("Simulation mode enabled")
app = Frontend(cfg, owndevice, boatdata, profile)
app = Frontend(cfg, appdata, owndevice, boatdata, profile)
app.run()
shutdown = True
appdata.shutdown = True
if cfg['can']:
t_rxd_n2k.join()
if cfg['nmea0183']:
@ -936,8 +819,10 @@ if __name__ == "__main__":
t_rxd_gps.join()
if cfg['network']:
t_rxd_net.join()
if cfg['tracker']['type'] != 'NONE':
t_tracker.join()
if not cfg['simulation'] and cfg['bme280']:
t_data.join()
print("Another fine product of the Sirius Cybernetics Corporation.")
print(boatdata)
print("Another fine product of the Sirius Cybernetics Corporation.")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,7 +20,7 @@ from .page import 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)
self.value1 = boatvalue1
self.value2 = boatvalue2

View File

@ -18,7 +18,7 @@ from .page import 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)
self.value1 = boatvalue1
self.value2 = boatvalue2

View File

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

View File

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

View File

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

View File

@ -18,7 +18,8 @@ from .page import 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)
self.value1 = boatvalue1
self.value2 = boatvalue2

View File

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

View File

@ -3,7 +3,7 @@ from .page import 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)
self.ref1 = self.bd.getRef(boatvalue1)
self.ref2 = self.bd.getRef(boatvalue2)

View File

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

View File

@ -16,7 +16,7 @@ from .page import 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)
self.ref1 = self.bd.getRef(boatvalue1)
self.ref2 = self.bd.getRef(boatvalue2)

View File

@ -17,7 +17,7 @@ class Voltage(Page):
avg = (1, 10, 60, 300);
def __init__(self, pageno, cfg, boatdata):
def __init__(self, pageno, cfg, appdata, boatdata):
super().__init__(pageno, cfg, boatdata)
self.trend = True
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():
def __init__(self, trackertype):
def __init__(self, trackertype='NONE'):
validtypes = ('HERO', 'SDCARD', 'SERVER', 'NONE')
trackertype = trackertype.upper()
if trackertype not in validtypes:
raise TypeError(f"Invalid tracker type: '{valtype}'. Only supported: {validtypes}")
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()