NAVTEX-Feature hinzugefügt. Daten nur über das Netzwerk
This commit is contained in:
parent
99dcf00242
commit
fb7c688a99
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue