NAVTEX-Feature hinzugefügt. Daten nur über das Netzwerk

This commit is contained in:
Thomas Hooge 2025-10-01 06:54:56 +02:00
parent 99dcf00242
commit fb7c688a99
9 changed files with 329 additions and 7 deletions

View File

@ -5,12 +5,15 @@ Generische Applikationsdaten
import os import os
from tracker import Tracker from tracker import Tracker
from navtex import NAVTEX
class AppData(): class AppData():
def __init__(self, logger, cfg): def __init__(self, logger, cfg):
self.shutdown = False # Globaler Ausschalter self.shutdown = False # Globaler Ausschalter
self.log = logger self.log = logger
if cfg['navtex']:
self.navtex = NAVTEX(logger, cfg)
self.track = Tracker(logger, cfg) self.track = Tracker(logger, cfg)
self.frontend = None self.frontend = None
self.bv_lat = None self.bv_lat = None

197
navtex.py Normal file
View File

@ -0,0 +1,197 @@
"""
DWD NAVTEX
Funkmodul (radio) noch nicht implementiert mangels Hardware
B1: Transmitter identity
B2: Subject indicator character
A Navigational warnings
B Meteorological warnings
C Ice reports
D Search & rescue information, and pirate warnings
E Meteorological forecasts
F Pilot service messages
G AIS messages (formerly Decca messages[6])
H LORAN messages
I Not used (formerly OMEGA messages[6])
J SATNAV messages (i.e. GPS or GLONASS)
K Other electronic navaid messages
L Navigational warnings additional to letter A (Should not be rejected by the receiver)
T Test transmissions (UK only not official)
V Notice to fishermen (U.S. only currently not used)
W Environmental (U.S. only currently not used)
X Special services allocation by IMO NAVTEX Panel
Y Special services allocation by IMO NAVTEX Panel
Z No message on hand
B3, B4: Serial number 01-99, 00: immediate printout
Timecode: DDHHmm UTC MMM YY
Sqlite database schema
.schema message
msgid TEXT PRIMARY KEY
timestamp TEXT
station TEXT
content TEXT
received TEXT
"""
import os
import http.client
import ssl
import re
import sqlite3
import datetime
from gi.repository import GLib
class NAVTEX():
def __init__(self, logger, cfg):
self.log = logger
self.source = cfg['ntx_source'].lower() # net | radio
self.maxage = cfg['ntx_housekeeping'] # message hold time in hours
self.running = False
dbpath = os.path.join(cfg['histpath'], "navtex.db")
try:
self.conn = sqlite3.connect(dbpath)
self.cur = self.conn.cursor()
except:
self.log.error(f"Failed to open local database: {dbpath}")
return
# Datenbank erstellen wenn nicht vorhanden
sql = "SELECT name FROM sqlite_master WHERE type='table' AND name='message'"
self.cur.execute(sql)
if self.cur.fetchone() == None:
sql = ("CREATE TABLE IF NOT EXISTS message ("
"msgid TEXT PRIMARY KEY NOT NULL,"
"station TEXT,"
"timestamp TEXT,"
"content TEXT NOT NULL,"
"received TEXT NOT NULL DEFAULT current_timestamp)"
)
self.cur.execute(sql)
self.log.info(f"Created NAVTEX database: {dbpath}")
# Aktualisieren bei Programmstart
# TODO Ausgeschaltet für Programmentwicklung
if self.source == 'net':
self.refresh()
# In der Konfiguration werden Minuten angegeben
GLib.timeout_add_seconds(cfg['ntx_refresh'] * 60, self.on_timer)
self.running = True
def __del__(self):
self.conn.close()
def on_timer(self):
"""
NAVTEX data handling
"""
self.refresh()
self.housekeeping()
return True
def parse_message(self, plainmsg):
"""
Zeile 1: ZCZC<space><BBBB>
Zeile 2: Stationskennung
Zeile 3: Zeitstempel (meistens)
Zeile 4 bis n-1: Nachrichteninhalt
Zeile n: NNNN
Je nach Code B1, B2 kann das folgende Format unterschiedlich sein
"""
msg = {}
data = plainmsg.splitlines()
msg['id'] = data[0][5:9]
msg['station'] = data[1].strip()
timestamp = data[2]
if len(timestamp) == 17:
day = int(timestamp[0:2])
hour = int(timestamp[2:4])
minute = int(timestamp[4:6])
monmap = ('JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC')
month = monmap.index(timestamp[11:14]) + 1
year = 2000 + int(timestamp[15:17])
try:
msg['timestamp'] = datetime.datetime(year, month, day, hour, minute, 0)
except:
msg['timestamp'] = None
else:
msg['timestamp'] = None
return msg
def dwd_get_data(self, local=False):
"""
net: Webseite auslesen
https://www.dwd.de/DE/fachnutzer/schifffahrt/funkausstrahlung/navtex
"""
if local:
# Für Tests um nicht permanent die Webseite abzufragen
with open("490_emd.html", "r") as fh:
content = fh.read()
else:
ssl_context = ssl.create_default_context()
conn = http.client.HTTPSConnection("www.dwd.de", 443, context=ssl_context)
url = "https://www.dwd.de/DE/fachnutzer/schifffahrt/funkausstrahlung/navtex/490_emd.html"
try:
conn.request("GET", url)
response = conn.getresponse()
if response.status == 200:
content = response.read().decode()
else:
print(f"Error: {response.status}")
return []
except http.client.HTTPException as e:
self.log.warning(f"HTTP error occurred: {e}")
return []
except ssl.SSLError as ssl_error:
self.log.warning(f"SSL error occurred: {ssl_error}")
return []
expr = re.compile("(ZCZC.*?NNNN)", re.DOTALL)
matches = re.findall(expr, content)
return matches
def refresh(self):
self.log.info("NAVTEX refresh")
messages = self.dwd_get_data(False)
sql = "INSERT INTO message (msgid, station, content) VALUES (?, ?, ?)"
for m in messages:
msg = self.parse_message(m)
self.cur.execute("SELECT COUNT(*) FROM message WHERE msgid=?", (msg['id'],))
result = self.cur.fetchone()
if result[0] == 0:
self.log.debug(f"NAVTEX: insert new message '{msg['id']}'")
self.cur.execute(sql, (msg['id'], msg['station'], m))
self.conn.commit()
def housekeeping(self):
self.log.info("NAVTEX housekeeping")
sql = "DELETE FROM message WHERE (julianday('now') - julianday(received)) * 24 > ?"
self.cur.execute(sql, (self.maxage, ))
def get_count(self):
sql = "SELECT COUNT(*) FROM message"
self.cur.execute(sql)
result = self.cur.fetchone()
return result[0]
def get_ids(self):
sql = "SELECT msgid FROM message"
result = self.cur.execute(sql)
msgids = []
for row in result.fetchall():
msgids.append(row[0])
return msgids
def get_message(self, msgid):
sql = "SELECT content FROM message WHERE msgid=?"
self.cur.execute(sql, (msgid, ))
result = self.cur.fetchone()
return result[0]

View File

@ -33,6 +33,12 @@ address = 0x76
enabled = false enabled = false
port = /dev/ttyACM0 port = /dev/ttyACM0
[navtex]
enabled = false
source = net
housekeeping = 72
refresh = 30
[opencpn] [opencpn]
navobj = ~/.opencpn/navobj.xml navobj = ~/.opencpn/navobj.xml
config = ~/.opencpn/opencpn.conf config = ~/.opencpn/opencpn.conf

View File

@ -734,6 +734,12 @@ if __name__ == "__main__":
if cfg['gps']: if cfg['gps']:
cfg['gps_port'] = config.get('gps', 'port') cfg['gps_port'] = config.get('gps', 'port')
cfg['navtex'] = config.getboolean('navtex', 'enabled')
if cfg['navtex']:
cfg['ntx_source'] = config.get('navtex', 'source') # Datenquelle: net | radio
cfg['ntx_housekeeping'] = config.getint('navtex', 'housekeeping') # Max. Nachrichtenalter in Stunden
cfg['ntx_refresh'] = config.getint('navtex', 'refresh') # Aktualisierung alle <n> Minuten
cfg['network'] = config.getboolean('network', 'enabled') cfg['network'] = config.getboolean('network', 'enabled')
if cfg['network']: if cfg['network']:
cfg['net_addr'] = config.get('network', 'address') cfg['net_addr'] = config.get('network', 'address')
@ -773,7 +779,6 @@ 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')
# Protokollierung # Protokollierung
loglevel = set_loglevel(cfg['loglevel']) loglevel = set_loglevel(cfg['loglevel'])
init_logging(os.path.expanduser(cfg['logdir']), cfg['logfile'], loglevel) init_logging(os.path.expanduser(cfg['logdir']), cfg['logfile'], loglevel)
@ -796,7 +801,6 @@ if __name__ == "__main__":
# Globale Daten, u.a. auch Shutdown-Indikator # Globale Daten, u.a. auch Shutdown-Indikator
appdata = AppData(log, cfg) appdata = AppData(log, cfg)
# Ggf. Simulationsdaten einschalten # Ggf. Simulationsdaten einschalten
if cfg['simulation']: if cfg['simulation']:
boatdata.enableSimulation() boatdata.enableSimulation()

View File

@ -32,6 +32,7 @@ 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 .navtex import Navtex
from .racetracker import RaceTracker from .racetracker import RaceTracker
from .rollpitch import RollPitch from .rollpitch import RollPitch
from .skyview import SkyView from .skyview import SkyView

View File

@ -25,7 +25,7 @@ class Clock(Page):
self.buttonlabel[1] = 'MODE' self.buttonlabel[1] = 'MODE'
self.buttonlabel[2] = 'TZ' self.buttonlabel[2] = 'TZ'
self.mode = ('A', 'D', 'T') # (A)nalog (D)igital (T)imer self.mode = ('A', 'D', 'T') # (A)nalog (D)igital (T)imer
self.modeindex = 1 self.modeindex = 0
self.utc = True self.utc = True
self.tzoffset = cfg['tzoffset'] self.tzoffset = cfg['tzoffset']
self.bv_lat = boatdata.getRef("LAT") self.bv_lat = boatdata.getRef("LAT")

112
pages/navtex.py Normal file
View File

@ -0,0 +1,112 @@
"""
NAVTEX
- Meldungen anzeigen
"""
import cairo
from .page import Page
class Navtex(Page):
def __init__(self, pageno, cfg, appdata, boatdata):
super().__init__(pageno, cfg, appdata, boatdata)
self.disabled = self.app.navtex is None
self.ids = self.app.navtex.get_ids()
if len(self.ids) > 0:
self.current = 1
self.msgid = self.ids[self.current - 1]
else:
self.current = 0
self.msgid = None
self.buttonlabel[1] = 'PREV'
self.buttonlabel[2] = 'NEXT'
self.buttonlabel[5] = 'MORE'
self.skip = 0
def handle_key(self, buttonid):
if buttonid == 1:
if self.current > 1:
self.current -= 1
else:
self.current = len(self.ids)
self.msgid = self.ids[self.current - 1]
self.skip = 0
return True
if buttonid == 2:
if self.current < len(self.ids):
self.current += 1
else:
self.current = 1
self.msgid = self.ids[self.current - 1]
self.skip = 0
return True
if buttonid == 5:
self.skip += 1
return True
return False
def draw(self, ctx):
# Title
ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
ctx.set_font_size(24)
ctx.move_to(8, 40)
ctx.show_text("NAVTEX")
ctx.set_line_width(1)
ctx.move_to(4.5, 42.5)
ctx.line_to(395.5, 42.5)
ctx.move_to(4.5, 272.5)
ctx.line_to(395.5, 272.5)
ctx.move_to(4.5, 32.5)
ctx.line_to(4.5, 272.5)
ctx.move_to(396.5, 32.5)
ctx.line_to(396.5, 272.5)
#ctx.rectangle(4.5, 20.5, 392, 250)
ctx.stroke()
ctx.set_font_size(16)
if self.disabled:
ctx.move_to(8, 75)
ctx.show_text("Feature ist disabled by configuration")
return
ctx.move_to(150, 40)
self.draw_text_ralign(ctx, 392, 38, "Message {} of {}".format(self.current, len(self.ids)))
ctx.select_font_face("AtariST8x16SystemFont")
ctx.move_to(8, 59)
if self.current == 0:
ctx.show_text("NIL")
return
# 48 Zeichen je Zeile möglich
# Max. 14 Zeilen auf dem Bildschirm
rawmsg = self.app.navtex.get_message(self.msgid).splitlines()
output = []
for line in rawmsg:
if len(line) <= 48:
output.append(line)
else:
i = 0
j = 48
while i < len(line):
output.append(line[i:j])
i += 48
j += 48
x = 8
y = 59
n = 0
for line in output:
if n >= self.skip:
ctx.move_to(x, y)
ctx.show_text(line)
y += 16
n += 1
if n >= 14 + self.skip:
break

View File

@ -190,7 +190,7 @@ class RaceTracker(Page):
Absätzen auf dem Bildschirma ausgibt. Absätzen auf dem Bildschirma ausgibt.
""" """
x = 8 x = 8
y = 50 y = 48
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(24) ctx.set_font_size(24)
ctx.move_to(x, y) ctx.move_to(x, y)
@ -199,7 +199,7 @@ class RaceTracker(Page):
y += 25 y += 25
ctx.set_font_size(16) ctx.set_font_size(16)
ctx.move_to(x, y) ctx.move_to(x, y)
ctx.show_text("Disabled by 'NONE in configuration'.") ctx.show_text("Disabled by 'NONE' in configuration.")
y += 30 y += 30
ctx.move_to(x, y) ctx.move_to(x, y)
ctx.show_text("Currently only tracker types 'HERO' and 'LOCAL'") ctx.show_text("Currently only tracker types 'HERO' and 'LOCAL'")
@ -225,7 +225,7 @@ class RaceTracker(Page):
def draw_local(self, ctx): def draw_local(self, ctx):
x = 8 x = 8
x1 = 130 x1 = 130
y = 50 y = 48
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(24) ctx.set_font_size(24)
ctx.move_to(x, y) ctx.move_to(x, y)

View File

@ -33,7 +33,6 @@ import http.client
import ssl import ssl
import json import json
import socket import socket
import ssl
import math import math
import subprocess # für Audioausgabe / mpg123 import subprocess # für Audioausgabe / mpg123