From 4ba75b5686e8df473b2c8870b63b5cfd914b26e9 Mon Sep 17 00:00:00 2001 From: Thomas Hooge Date: Thu, 11 Sep 2025 15:31:50 +0200 Subject: [PATCH] =?UTF-8?q?Publish=20f=C3=BCr=20MQTT=20integriert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- INSTALL | 2 +- nmea0183.py | 13 +++--- obp60v.conf-sample | 8 ++++ obp60v.py | 109 ++++++++++++++++++++++++++++++++++++++++----- pages/__init__.py | 1 + pages/tracker.py | 14 +++++- 6 files changed, 127 insertions(+), 20 deletions(-) diff --git a/INSTALL b/INSTALL index 2c39629..53fa8f4 100644 --- a/INSTALL +++ b/INSTALL @@ -5,7 +5,7 @@ Python muß mindestens Version 3.10 sein. apt-get install python3-cairo python3-gi python3-gi-cairo gir1.2-rsvg-2.0 \ python3-serial python3-nmea2 python3-smbus2 python3-bme280 python3-astral \ - python3-can + python3-can python3-paho-mqtt Zusätzlich zu diesem Programm muß auch die zugehörige NMEA2000-Bibliothek für Python vorhanden sein. Diese kann momentan am besten parallel zu dem diff --git a/nmea0183.py b/nmea0183.py index 321fb00..42d2168 100644 --- a/nmea0183.py +++ b/nmea0183.py @@ -55,18 +55,17 @@ def GGA(boatdata, msg): def GLL(boatdata, msg): # Position data: position fix, time of position fix, and status # UTC of position ignored - print(msg.data) if not msg.status == 'A': return lat_fac = 1 if msg.lat_dir == 'N' else -1 lat_deg = int(msg.lat[0:2]) lat_min = float(msg.lat[2:]) - print(lat_deg, lat_min) boatdata.setValue("LAT", lat_fac * lat_deg + lat_min / 60) lon_fac = 1 if msg.lon_dir == 'E' else -1 lon_deg = int(msg.lon[0:3]) lon_min = float(msg.lon[3:]) boatdata.setValue("LON", lon_fac * lon_deg + lon_min / 60) + boatdata.setValue("TSPOS", msg.timestamp) # datetime.time, UTC def GSA(boatdata, msg): # Satellites @@ -297,7 +296,7 @@ def VBW(boatdata, msg): print("-> VBW") def VHW(boatdata, msg): - print("-> VHW") + #print("-> VHW") boatdata.setValue("STW", float(msg.water_speed_knots)) def VPW(boatdata, msg): @@ -319,15 +318,17 @@ def VTG(boatdata, msg): ('FAA mode indicator', 'faa_mode')) ['', 'T', '', 'M', '0.117', 'N', '0.216', 'K', 'A'] """ - print("-> VTG") + #print("-> VTG") # msg.true_track true_track_sym # msg.mag_track mag_track_sym # msg.faa_mode #TODO klären was für Typen hier ankommen können # bytearray, str, decimal.Decimal? - sog = float(msg.spd_over_grnd_kts) + #sog = float(msg.spd_over_grnd_kts) #str von OpenCPN: sog = float(msg.spd_over_grnd_kts[:-1]) - boatdata.setValue("SOG", sog) + #boatdata.setValue("SOG", sog) + #print("VTG", msg.spd_over_grnd_kts) + print("VTG", msg) def VWR(boatdata, msg): # Relative Wind Speed and Angle diff --git a/obp60v.conf-sample b/obp60v.conf-sample index aa97eb7..076c5a0 100644 --- a/obp60v.conf-sample +++ b/obp60v.conf-sample @@ -46,6 +46,14 @@ mqtt_pass = 123456 orgname = demo passcode = 123456 +[boat] +name = MY boat +sailno = GER 4711 +class = One off +handicap = 100.0 +club = NONE +team = OBP + [settings] timezone = 1 boat_draft = 1.3 diff --git a/obp60v.py b/obp60v.py index d5a033a..7dfa928 100755 --- a/obp60v.py +++ b/obp60v.py @@ -126,41 +126,85 @@ cfg = { 'industrygroup': 4, # Marine 'gps': False, 'bme280': False, - 'tracker': { 'type': 'NONE' } + 'tracker': { 'type': 'NONE' }, + 'boat': { } } def mqtt_on_connect(client, userdata, flags, rc): print(f"MQTT connected with result code {rc}") - client.subscribe("regattahero/orgstatus/thomas") - #client.subscribe(topic_racestatus) + #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) + 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"): - racestatus = json.loads(msg.payload) - print(racestatus) + # 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_tracker(cfg): +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") - print(cfg) client = mqtt.Client() client.on_connect = mqtt_on_connect client.on_message = mqtt_on_message @@ -169,11 +213,43 @@ def mqtt_tracker(cfg): client.connect(cfg['host'], cfg['port'], 60) except ConnectionRefusedError: print("MQTT connection refused. Check username and password.") - return + 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) - #TODO publish here + if tracker_active: + mqtt_publish(client, topic, payload, bv_lat, bv_lon, bv_sog) client.loop_stop() client.disconnect() @@ -258,7 +334,7 @@ def rxd_0183(devname): setthreadtitle("0183listener") while not shutdown: raw = ser.readline().decode('ascii') - if len(raw) == 0: + if len(raw.strip()) == 0: continue try: msg = pynmea2.parse(raw) @@ -722,6 +798,7 @@ if __name__ == "__main__": setproctitle("obp60v") shutdown = False + tracker_active = False owndevice = Device(100) # Hardcoding device, not intended to change @@ -791,6 +868,14 @@ if __name__ == "__main__": cfg['tracker']['orgname'] = config.get('tracker', 'orgname') cfg['tracker']['passcode'] = config.get('tracker', 'passcode') + # Boat data + cfg['boat']['name'] = config.get('boat', 'name') + cfg['boat']['sailno'] = config.get('boat', 'sailno') + cfg['boat']['class'] = config.get('boat', 'class') + cfg['boat']['handicap'] = config.getfloat('boat', 'handicap') + 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: @@ -831,7 +916,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'],)) + t_tracker = threading.Thread(target=mqtt_tracker, args=(cfg['tracker'],cfg['boat'])) t_tracker.start() if not cfg['simulation']: if cfg['bme280']: diff --git a/pages/__init__.py b/pages/__init__.py index 5e6476b..2a488fe 100644 --- a/pages/__init__.py +++ b/pages/__init__.py @@ -37,6 +37,7 @@ from .mob import MOB 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 .windrose import WindRose diff --git a/pages/tracker.py b/pages/tracker.py index b9c3e9a..f3983d5 100644 --- a/pages/tracker.py +++ b/pages/tracker.py @@ -1,3 +1,8 @@ +""" +Tracker with MQTT client + - currentry only Ragatta hero supported +""" + import cairo from .page import Page @@ -9,7 +14,14 @@ class Tracker(Page): def draw(self, ctx): # Name ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD) - ctx.set_font_size(60) + ctx.set_font_size(32) ctx.move_to(20, 100) ctx.show_text("Tracker") + ctx.set_font_size(16) + ctx.move_to(20, 140) + ctx.show_text("active: ") + if tracker_active: + ctx.show_text("yes") + else: + ctx.show_text("no")