From 3f90494508e92568a00f545e48a1704a4d6c4605 Mon Sep 17 00:00:00 2001 From: martin Date: Thu, 30 Apr 2026 13:43:06 +0200 Subject: [PATCH] auto: update RTK rover/base + config (2026-04-30 13:43:06) --- .gitignore | 4 +- config.py | 38 +++ drop-in-receiver.py | 42 +++ git_push.sh | 59 ++++ rtk_rover.env | 1 + rtk_rover_recorder.py.bak | 645 ++++++++++++++++++++++++++++++++++++++ schema.sql | 39 +++ 7 files changed, 825 insertions(+), 3 deletions(-) create mode 100755 config.py create mode 100755 drop-in-receiver.py create mode 100755 git_push.sh create mode 100644 rtk_rover.env create mode 100755 rtk_rover_recorder.py.bak create mode 100644 schema.sql diff --git a/.gitignore b/.gitignore index ebdf601..b240754 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -cat > .gitignore <<'EOF' # runtime / logs / databases *.db *.db-* @@ -13,6 +12,5 @@ __pycache__/ .venv/ venv/ -# backups from patching +# backups *.bak_* -EOF diff --git a/config.py b/config.py new file mode 100755 index 0000000..b51a2fd --- /dev/null +++ b/config.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +import serial, time + +PORT="/dev/serial/by-id/usb-1a86_USB_Serial-if00-port0" +BAUD=9600 + +def send(ser, cmd, wait=0.4): + ser.write((cmd+"\r\n").encode()) + ser.flush() + time.sleep(wait) + rx = ser.read(2000).decode(errors="ignore") + print(f"[{cmd}] {rx.strip()}") + return rx + +ser=serial.Serial(PORT, BAUD, timeout=0.5) +ser.reset_input_buffer() +ser.reset_output_buffer() + +# AT-Mode sauber betreten +time.sleep(1.5) +ser.write(b"+++\r\n"); ser.flush() +time.sleep(0.8) +print(ser.read(200).decode(errors="ignore").strip()) + +# Konfig setzen +send(ser, "AT+MODE0") +send(ser, "AT+SLEEP2") +send(ser, "AT+LEVEL7") # mehr Durchsatz-Reserve [oai_citation:4‡DX-LR03-433T30D Serial port application guide .pdf](sediment://file_000000005c3c7243bef470aff82f39f0) +send(ser, "AT+CR2") # 4/6 default [oai_citation:5‡DX-LR03-433T30D Serial port application guide .pdf](sediment://file_000000005c3c7243bef470aff82f39f0) +send(ser, "AT+CRC1") # LoRa CRC on [oai_citation:6‡DX-LR03-433T30D Serial port application guide .pdf](sediment://file_000000005c3c7243bef470aff82f39f0) +send(ser, "AT+POWE5") # max power [oai_citation:7‡DX-LR03-433T30D Serial port application guide .pdf](sediment://file_000000005c3c7243bef470aff82f39f0) + +# Anzeigen + Reset (damit es wirklich aktiv wird) +send(ser, "AT+HELP", wait=0.8) +send(ser, "AT+RESET", wait=0.6) + +ser.close() +print("done") diff --git a/drop-in-receiver.py b/drop-in-receiver.py new file mode 100755 index 0000000..83c26d9 --- /dev/null +++ b/drop-in-receiver.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +import serial, time, string +from datetime import datetime + +PORT="/dev/serial/by-id/usb-1a86_USB_Serial-if00-port0" +BAUD=9600 + +PRINTABLE=set(bytes(string.printable, "ascii")) + +def classify(line: bytes): + # strip CR/LF + raw=line.strip(b"\r\n") + if not raw: + return None, None + printable = sum(b in PRINTABLE for b in raw) + ratio = printable / max(1, len(raw)) + try: + txt = raw.decode("ascii", errors="replace") + except Exception: + txt = repr(raw) + return ratio, txt + +ser=serial.Serial(PORT, BAUD, timeout=0.2) +buf=b"" +cnt=0 +try: + while True: + buf += ser.read(4096) + while b"\n" in buf: + line, buf = buf.split(b"\n", 1) + ratio, txt = classify(line+b"\n") + if ratio is None: + continue + cnt += 1 + ts=datetime.now().strftime("%H:%M:%S") + tag="OK" if txt.startswith("Test #") else "GARBAGE" + print(f"[{ts}] #{cnt:04d} {tag} printable_ratio={ratio:.2f} {txt}") + time.sleep(0.01) +except KeyboardInterrupt: + pass +finally: + ser.close() diff --git a/git_push.sh b/git_push.sh new file mode 100755 index 0000000..3e1bd1d --- /dev/null +++ b/git_push.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +set -e + +REPO_DIR="$HOME/rtk" +REMOTE_URL="https://gitea.haltenhopf47.ddns.net/DEINUSER/rtk-rover.git" + +cd "$REPO_DIR" + +echo "➡️ Git Repo prüfen..." +if [ ! -d .git ]; then + git init + git branch -M main +fi + +echo "➡️ .gitignore sicherstellen..." +cat > .gitignore <<'EOGI' +# runtime / logs / databases +*.db +*.db-* +*.sqlite +logs/ +*.log +*.tap + +# Python +__pycache__/ +*.pyc +.venv/ +venv/ + +# backups +*.bak_* +EOGI + +echo "➡️ systemd Files kopieren..." +mkdir -p systemd +sudo cp /etc/systemd/system/rtk-rover.service systemd/ 2>/dev/null || true +sudo cp /etc/systemd/system/rtk-base.service systemd/ 2>/dev/null || true +sudo chown -R "$USER":"$USER" systemd + +echo "➡️ Dateien hinzufügen..." +git add -A + +echo "➡️ Commit erstellen..." +if ! git diff --cached --quiet; then + git commit -m "auto: update RTK rover/base + config ($(date '+%F %T'))" +else + echo "ℹ️ Keine Änderungen zum Committen" +fi + +echo "➡️ Remote prüfen..." +if ! git remote | grep -q origin; then + git remote add origin "$REMOTE_URL" +fi + +echo "➡️ Push..." +git push -u origin main + +echo "✅ Fertig." diff --git a/rtk_rover.env b/rtk_rover.env new file mode 100644 index 0000000..ab539a3 --- /dev/null +++ b/rtk_rover.env @@ -0,0 +1 @@ +DEVICE_ID=rtkrover diff --git a/rtk_rover_recorder.py.bak b/rtk_rover_recorder.py.bak new file mode 100755 index 0000000..51f3a32 --- /dev/null +++ b/rtk_rover_recorder.py.bak @@ -0,0 +1,645 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import sys, time, struct, argparse, threading, os, sqlite3, uuid, select, termios, tty +from typing import Optional, Tuple, Dict +import serial +from serial.serialutil import SerialException + +# -------------------- Defaults -------------------- +DEF_ZED_PORT = "/dev/serial/by-id/usb-u-blox_AG_-_www.u-blox.com_u-blox_GNSS_receiver-if00" +DEF_ZED_BAUD = 115200 +DEF_LORA_PORT = "/dev/serial/by-id/usb-1a86_USB_Serial-if00-port0" +DEF_LORA_BAUD = 9600 + +PRINT_EVERY_S = 1.0 +RTCM_GAP_WARN = 3.0 +READ_TIMEOUT_S = 0.1 +REOPEN_WAIT_S = 1.0 + +DEF_DB_PATH = os.path.expanduser("~/rtk/rtk_rover.db") +DEF_LOG_DIR = os.path.expanduser("~/rtk/logs") + +# empirischer Offset: hMSL_raw + offset = hMSL_corr +DEFAULT_MSL_OFFSET = -31.5 + +# -------------------- UBX helpers -------------------- +def ubx_checksum(payload: bytes) -> bytes: + a = b = 0 + for x in payload: + a = (a + x) & 0xFF + b = (b + a) & 0xFF + return bytes([a, b]) + +def send_ubx(ser: serial.Serial, cls: int, mid: int, payload: bytes = b"") -> None: + head = b"\xB5\x62" + bytes([cls, mid]) + struct.pack(" bool: + end = time.time() + timeout + while time.time() < end: + f = read_ubx_frame(ser, end - time.time()) + if not f: + continue + c, m, p = f + if c == 0x05 and m in (0x00, 0x01) and len(p) == 2 and (p[0], p[1]) == (exp_cls, exp_id): + return m == 0x01 + return False + +# -------------------- CFG / MON -------------------- +def mon_ver(ser) -> Tuple[str, str]: + send_ubx(ser, 0x0A, 0x04) + v1 = v2 = "?" + end = time.time() + 0.6 + while time.time() < end: + f = read_ubx_frame(ser, 0.1) + if not f or f[0] != 0x0A or f[1] != 0x04: + continue + p = f[2] + for i in range(0, len(p), 30): + s = p[i:i+30].split(b"\x00", 1)[0].decode("ascii", errors="ignore") + if s.startswith(("ROM", "EXT", "PROT")): v1 = s + if s.startswith("FWVER"): v2 = s + return v1, v2 + +def get_cfg_tmode3(ser): + send_ubx(ser, 0x06, 0x71) + f = read_ubx_frame(ser, 0.8) + if not f or f[0] != 0x06 or f[1] != 0x71 or len(f[2]) < 40: + return None + p = f[2] + mode = p[2] + return (mode, p) + +def set_tmode3_disabled(ser) -> bool: + payload = bytearray(40) + payload[0] = 0 + payload[2] = 0 + send_ubx(ser, 0x06, 0x71, payload) + return wait_ack(ser, 0x06, 0x71, 1.0) + +def set_nav_rate_1hz(ser): + payload = struct.pack(" rate=1 + send_ubx(ser, 0x06, 0x01, payload) + wait_ack(ser, 0x06, 0x01, 0.5) + +# -------------------- NAV Parser -------------------- +def parse_nav_pvt(p): + if len(p) < 92: return None + iTOW = struct.unpack_from("> 6) & 0x03 # 0 none,1 float,2 fix + return dict(iTOW=iTOW, time=(year,month,day,hour,minute,second), + fixType=fixType, gnssFixOK=(flags&1)!=0, diffSoln=(flags&2)!=0, + carrSoln=carrSoln, numSV=numSV, lat=lat, lon=lon, + height=height, hMSL=hMSL, hAcc=hAcc, vAcc=vAcc) + +def parse_nav_hpposllh(p): + if len(p) < 36: return None + iTOW = struct.unpack_from(" int: + crc = 0 + for b in data: + crc ^= (b << 16) + for _ in range(8): + crc <<= 1 + if crc & 0x1000000: + crc ^= CRC24Q_POLY + return crc & 0xFFFFFF + +def parse_rtcm3_header_and_type(buf: bytes) -> Optional[Tuple[int,int]]: + if len(buf) < 6 or buf[0] != 0xD3: + return None + length = ((buf[1] & 0x03) << 8) | buf[2] + need = 3 + length + 3 + if len(buf) < need: + return None + body = buf[:3+length] + crc_rx = (buf[3+length] << 16) | (buf[3+length+1] << 8) | buf[3+length+2] + if crc24q(body) != crc_rx: + return (-1, need) + msgnum = ((buf[3] & 0xFC) >> 2) << 8 | buf[4] + return (msgnum, need) + +class RTCMRouter(threading.Thread): + def __init__(self, lora_port, lora_baud, zed_ser: serial.Serial, stats: Dict): + super().__init__(daemon=True) + self.lora_port = lora_port + self.lora_baud = lora_baud + self.zed_ser = zed_ser + self.stats = stats + self.stop_flag = threading.Event() + self.ser = None + self.buf = bytearray() + + def open_lora(self): + if self.ser and self.ser.is_open: return + self.ser = serial.Serial(self.lora_port, self.lora_baud, timeout=READ_TIMEOUT_S) + + def close_lora(self): + if self.ser: + try: self.ser.close() + except: pass + self.ser = None + + def run(self): + while not self.stop_flag.is_set(): + try: + if not self.ser or not self.ser.is_open: + self.open_lora() + data = self.ser.read(512) + if data: + self.stats["lora_bytes"] = self.stats.get("lora_bytes", 0) + len(data) + self.buf.extend(data) + i = 0 + while i < len(self.buf): + if self.buf[i] != 0xD3: + i += 1 + continue + parsed = parse_rtcm3_header_and_type(self.buf[i:]) + if not parsed: + break + msgnum, framelen = parsed + frame = self.buf[i:i+framelen] + if msgnum == -1: + self.stats["rtcm_crc_err"] = self.stats.get("rtcm_crc_err", 0) + 1 + i += framelen + continue + self.stats["rtcm_last_ok"] = time.time() + self.stats["rtcm_count"] = self.stats.get("rtcm_count", 0) + 1 + self.stats["rtcm_preambles"] = self.stats.get("rtcm_preambles", 0) + 1 + ty = self.stats.setdefault("rtcm_types", {}) + ty[msgnum] = ty.get(msgnum, 0) + 1 + try: + self.zed_ser.write(frame) + except SerialException: + pass + i += framelen + if i > 0: + del self.buf[:i] + else: + time.sleep(0.01) + except SerialException: + self.close_lora() + time.sleep(REOPEN_WAIT_S) + except Exception: + time.sleep(0.05) + + def stop(self): + self.stop_flag.set() + self.close_lora() + +# -------------------- Plausibility Filter -------------------- +def is_plausible_fix(lat: Optional[float], lon: Optional[float], h_m: Optional[float], + hacc_m: Optional[float], vacc_m: Optional[float]) -> bool: + if lat is None or lon is None: + return False + if not (-90.0 <= lat <= 90.0): + return False + if not (-180.0 <= lon <= 180.0): + return False + if h_m is not None and not (-500.0 <= h_m <= 10000.0): + return False + if hacc_m is not None and hacc_m > 1000.0: + return False + if vacc_m is not None and vacc_m > 2000.0: + return False + return True + +# -------------------- Recording / DB -------------------- +SCHEMA_SQL = """ +PRAGMA journal_mode=WAL; +PRAGMA synchronous=NORMAL; + +CREATE TABLE IF NOT EXISTS sessions ( + session_id TEXT PRIMARY KEY, + start_ts REAL NOT NULL, + end_ts REAL, + device_id TEXT, + note TEXT +); + +CREATE TABLE IF NOT EXISTS gnss_pvt ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + ts_utc TEXT, + unix_ts REAL NOT NULL, + iTOW_ms INTEGER, + lat REAL, + lon REAL, + height_m REAL, + hmsl_m REAL, + hmsl_corr_m REAL, + hacc_m REAL, + vacc_m REAL, + fix_type INTEGER, + carr_soln INTEGER, + diff_soln INTEGER, + gnss_ok INTEGER, + num_sats INTEGER, + source TEXT, + synced INTEGER DEFAULT 0, + FOREIGN KEY(session_id) REFERENCES sessions(session_id) +); + +CREATE INDEX IF NOT EXISTS idx_gnss_session_time ON gnss_pvt(session_id, unix_ts); +CREATE INDEX IF NOT EXISTS idx_gnss_unsynced ON gnss_pvt(synced) WHERE synced=0; +""" + +def iso_utc_from_pvt_time(tup) -> Optional[str]: + try: + y,mo,d,hh,mm,ss = tup + return f"{y:04d}-{mo:02d}-{d:02d}T{hh:02d}:{mm:02d}:{ss:02d}Z" + except Exception: + return None + +class Recorder: + def __init__(self, db_path: str, device_id: str = None, log_dir: Optional[str] = None, + csv: bool = False, msl_offset: float = 0.0): + self.db_path = db_path + self.device_id = device_id + self.log_dir = log_dir + self.csv_enabled = csv + self.msl_offset = msl_offset + self.conn = sqlite3.connect(self.db_path, check_same_thread=False) + self.conn.execute("PRAGMA foreign_keys=ON;") + self.conn.executescript(SCHEMA_SQL) + self.conn.commit() + + self.session_id: Optional[str] = None + self.csv_fh = None + + if self.log_dir: + os.makedirs(self.log_dir, exist_ok=True) + + self.lock = threading.Lock() + + def is_recording(self) -> bool: + return self.session_id is not None + + def start(self, note: str = None): + with self.lock: + if self.session_id: + return + sid = str(uuid.uuid4()) + now = time.time() + self.conn.execute( + "INSERT INTO sessions(session_id,start_ts,device_id,note) VALUES(?,?,?,?)", + (sid, now, self.device_id, note) + ) + self.conn.commit() + self.session_id = sid + + if self.csv_enabled and self.log_dir: + fn = os.path.join(self.log_dir, f"session_{sid}.csv") + self.csv_fh = open(fn, "w", encoding="utf-8") + self.csv_fh.write("unix_ts,ts_utc,iTOW_ms,lat,lon,height_m,hmsl_m,hmsl_corr_m,hacc_m,vacc_m,fix_type,carr_soln,diff_soln,gnss_ok,num_sats,source\n") + self.csv_fh.flush() + + def stop(self): + with self.lock: + if not self.session_id: + return + now = time.time() + self.conn.execute("UPDATE sessions SET end_ts=? WHERE session_id=?", (now, self.session_id)) + self.conn.commit() + self.session_id = None + if self.csv_fh: + try: self.csv_fh.close() + except: pass + self.csv_fh = None + + def add_fix(self, pvt: Optional[dict], hpp: Optional[dict]): + with self.lock: + if not self.session_id: + return + + unix_ts = time.time() + ts_utc = iso_utc_from_pvt_time(pvt["time"]) if pvt else None + iTOW = int(pvt["iTOW"]) if pvt else (int(hpp["iTOW"]) if hpp else None) + + use_hp = False + if hpp: + use_hp = is_plausible_fix(hpp.get("lat"), hpp.get("lon"), hpp.get("height"), hpp.get("hAcc"), hpp.get("vAcc")) + + if use_hp: + lat, lon = hpp["lat"], hpp["lon"] + height, hmsl = hpp["height"], hpp["hMSL"] + hacc, vacc = hpp["hAcc"], hpp["vAcc"] + source = "HPPOSLLH" + elif pvt: + lat, lon = pvt["lat"], pvt["lon"] + height, hmsl = pvt["height"], pvt["hMSL"] + hacc, vacc = pvt["hAcc"], pvt["vAcc"] + source = "PVT" + else: + return + + hmsl_corr = (hmsl + self.msl_offset) if (hmsl is not None) else None + + fix_type = int(pvt["fixType"]) if pvt else None + carr_soln = int(pvt["carrSoln"]) if pvt else None + diff_soln = int(1 if pvt and pvt.get("diffSoln") else 0) if pvt else None + gnss_ok = int(1 if pvt and pvt.get("gnssFixOK") else 0) if pvt else None + num_sats = int(pvt["numSV"]) if pvt else None + + self.conn.execute( + """INSERT INTO gnss_pvt(session_id,ts_utc,unix_ts,iTOW_ms,lat,lon,height_m,hmsl_m,hmsl_corr_m,hacc_m,vacc_m, + fix_type,carr_soln,diff_soln,gnss_ok,num_sats,source) + VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", + (self.session_id, ts_utc, unix_ts, iTOW, lat, lon, height, hmsl, hmsl_corr, hacc, vacc, + fix_type, carr_soln, diff_soln, gnss_ok, num_sats, source) + ) + self.conn.commit() + + if self.csv_fh: + self.csv_fh.write(f"{unix_ts},{ts_utc or ''},{iTOW or ''},{lat},{lon},{height},{hmsl},{hmsl_corr},{hacc},{vacc},{fix_type},{carr_soln},{diff_soln},{gnss_ok},{num_sats},{source}\n") + self.csv_fh.flush() + + def close(self): + with self.lock: + try: + if self.session_id: + self.stop() + finally: + try: self.conn.close() + except: pass + +# -------------------- UI helpers -------------------- +def fix_type_str(pvt) -> str: + if not pvt: return "—" + base = {0:"NO FIX",1:"DEAD RECK",2:"2D",3:"3D",4:"GNSS+DR",5:"TIME"}.get(pvt["fixType"], "?") + if pvt["carrSoln"] == 2: return base + " / RTK FIX" + if pvt["carrSoln"] == 1: return base + " / RTK FLOAT" + if pvt["diffSoln"]: return base + " / DGNSS" + return base + +class StdinKeyReader: + def __init__(self): + self.fd = sys.stdin.fileno() + self.old = termios.tcgetattr(self.fd) + tty.setcbreak(self.fd) + + def read_key(self) -> Optional[str]: + r, _, _ = select.select([sys.stdin], [], [], 0) + if r: + return sys.stdin.read(1) + return None + + def close(self): + termios.tcsetattr(self.fd, termios.TCSADRAIN, self.old) + +# -------------------- Main -------------------- +def main(): + ap = argparse.ArgumentParser(description="RTK Rover Recorder (ZED-F9P + LoRa + SQLite)") + ap.add_argument("--zed", default=DEF_ZED_PORT) + ap.add_argument("--zed-baud", type=int, default=DEF_ZED_BAUD) + ap.add_argument("--lora", default=DEF_LORA_PORT) + ap.add_argument("--lora-baud", type=int, default=DEF_LORA_BAUD) + ap.add_argument("--print-every", type=float, default=PRINT_EVERY_S) + + ap.add_argument("--db", default=DEF_DB_PATH) + ap.add_argument("--device-id", default=None) + ap.add_argument("--log-dir", default=DEF_LOG_DIR) + ap.add_argument("--csv", action="store_true") + ap.add_argument("--msl-offset", type=float, default=DEFAULT_MSL_OFFSET, + help="Apply correction: hMSL_corr = hMSL_raw + msl_offset") + + args = ap.parse_args() + + print(f"🔌 Öffne ZED {args.zed} @ {args.zed_baud} …") + try: + zed = serial.Serial(args.zed, args.zed_baud, timeout=READ_TIMEOUT_S) + except Exception as e: + print(f"❌ Konnte ZED-Port nicht öffnen: {e}"); sys.exit(1) + print("✅ ZED-Port offen.") + + print("🔎 Diagnose…") + v1, v2 = mon_ver(zed) + print(f" MON-VER: ('{v1}', '{v2}')") + st0 = get_cfg_tmode3(zed) + print(f" TMODE3 vorab: {st0}") + + print("\n↩️ TMODE3=DISABLED (Rover)") + print(f" ACK: {'OK' if set_tmode3_disabled(zed) else 'kein ACK'}") + + print("⚙️ NAV-Ausgaben (USB) setzen: NAV-PVT + HPPOSLLH (USB=1)") + enable_nav_msgs_on_usb(zed) + set_nav_rate_1hz(zed) + quiet_nmea_usb(zed, usb_rate=0) + + stats = {"rtcm_last_ok": 0.0, "rtcm_count": 0, "rtcm_types": {}, + "lora_bytes": 0, "rtcm_crc_err": 0, "rtcm_preambles": 0} + + rtcm_router = RTCMRouter(args.lora, args.lora_baud, zed, stats) + try: + rtcm_router.start() + except Exception as e: + print(f"⚠️ Konnte LoRa-Thread nicht starten: {e}") + + recorder = Recorder(args.db, device_id=args.device_id, log_dir=args.log_dir, csv=args.csv, + msl_offset=args.msl_offset) + + print(f"\n🧭 msl_offset={args.msl_offset:+.3f} m (hMSL_corr = hMSL_raw + offset)") + print("\n🎛️ Steuerung: 'r' = Recording Start/Stop | 'q' = Quit (sauber)") + print(f"🗃️ DB: {args.db}") + if args.csv: + print(f"🧾 CSV: aktiv → {args.log_dir}") + + keyr = None + try: + keyr = StdinKeyReader() + except Exception: + print("⚠️ Kein TTY – Keyboard-Steuerung deaktiviert (normal im systemd Service).") + + last_print = 0.0 + last_nav_seen = 0.0 + first_poll_deadline = time.time() + 3.0 + + last_pvt = None + last_hpp = None + + try: + while True: + if keyr: + k = keyr.read_key() + if k: + if k.lower() == "q": + break + if k.lower() == "r": + if recorder.is_recording(): + recorder.stop() + print("⏹️ Recording STOP") + else: + recorder.start(note="manual") + print(f"⏺️ Recording START (session={recorder.session_id})") + + f = read_ubx_frame(zed, timeout=0.05) + pvt = None + hpp = None + if f: + frames = [f] + t_end = time.time() + 0.04 + while time.time() < t_end: + nf = read_ubx_frame(zed, timeout=0.01) + if not nf: + break + frames.append(nf) + for c, m, p in frames: + if (c, m) == (0x01, 0x07): + pvt = parse_nav_pvt(p); last_nav_seen = time.time() + elif (c, m) == (0x01, 0x14): + hpp = parse_nav_hpposllh(p); last_nav_seen = time.time() + + if pvt: last_pvt = pvt + if hpp: last_hpp = hpp + + if time.time() > first_poll_deadline and last_nav_seen == 0.0: + send_ubx(zed, 0x01, 0x07) + first_poll_deadline = float("inf") + + if recorder.is_recording() and (pvt or hpp): + recorder.add_fix(pvt if pvt else last_pvt, hpp if hpp else last_hpp) + + now = time.time() + if now - last_print >= args.print_every: + last_print = now + + if stats["rtcm_count"] == 0: + rtcm_info = f"RTCM: (noch nichts empfangen) | LoRa-Bytes={stats['lora_bytes']}" + else: + dt = now - stats["rtcm_last_ok"] + warn = " ⚠️" if dt > RTCM_GAP_WARN else "" + rtcm_info = f"RTCM: ok (letzte gültige vor {dt:.1f}s){warn} | Frames={stats['rtcm_count']} | CRC-Err={stats['rtcm_crc_err']} | LoRa-Bytes={stats['lora_bytes']}" + + p = last_pvt + hp = last_hpp + fixs = fix_type_str(p) if p else "—" + + use_hp = False + if hp: + use_hp = is_plausible_fix(hp.get("lat"), hp.get("lon"), hp.get("height"), hp.get("hAcc"), hp.get("vAcc")) + + if use_hp: + lat, lon = hp["lat"], hp["lon"] + h_msl_raw = hp["hMSL"] + h_ell = hp["height"] + hacc, vacc = hp["hAcc"], hp["vAcc"] + src = "HPPOSLLH" + elif p: + lat, lon = p["lat"], p["lon"] + h_msl_raw = p["hMSL"] + h_ell = p["height"] + hacc, vacc = p["hAcc"], p["vAcc"] + src = "PVT" + else: + lat = lon = None + h_msl_raw = h_ell = None + hacc = vacc = None + src = "—" + + h_msl_corr = (h_msl_raw + args.msl_offset) if (h_msl_raw is not None) else None + N = (h_ell - h_msl_raw) if (h_ell is not None and h_msl_raw is not None) else None + + rec = "REC" if recorder.is_recording() else "idle" + + print("—"*72) + print(rtcm_info) + if last_nav_seen == 0.0: + print("UBX: (noch keine NAV-Daten gesehen) – prüfe CFG-MSG (USB) / Kabel") + else: + ago = now - last_nav_seen + if ago > 3.0: + print(f"UBX: letzte NAV vor {ago:.1f}s ⚠️") + + if p: + print(f"[{rec}] Fix: {fixs} | Sats: {p['numSV']} | hAcc: {hacc:.3f} m | vAcc: {vacc:.3f} m | src={src}") + else: + print(f"[{rec}] Fix: — | src={src}") + + if lat is not None: + print(f"Pos: lat={lat:.7f}, lon={lon:.7f}, hMSL_raw={h_msl_raw:.3f} m | hMSL_corr={h_msl_corr:.3f} m | hEll={h_ell:.3f} m | N={N:.3f} m") + else: + print("Pos: —") + + time.sleep(0.02) + + except KeyboardInterrupt: + pass + finally: + print("\n⏹️ Stoppe …") + try: + if keyr: keyr.close() + except: pass + try: + recorder.close() + except: pass + try: + rtcm_router.stop() + except: pass + try: + zed.close() + except: pass + print("✓ Beendet.") + +if __name__ == "__main__": + main() diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..3979050 --- /dev/null +++ b/schema.sql @@ -0,0 +1,39 @@ +PRAGMA journal_mode=WAL; +PRAGMA synchronous=NORMAL; +PRAGMA foreign_keys=ON; + +CREATE TABLE IF NOT EXISTS sessions ( + session_id TEXT PRIMARY KEY, + start_ts REAL NOT NULL, + end_ts REAL, + device_id TEXT, + note TEXT +); + +CREATE TABLE IF NOT EXISTS gnss_pvt ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + ts_utc TEXT, + unix_ts REAL NOT NULL, + iTOW_ms INTEGER, + lat REAL, + lon REAL, + height_m REAL, + hmsl_m REAL, + hacc_m REAL, + vacc_m REAL, + fix_type INTEGER, + carr_soln INTEGER, + diff_soln INTEGER, + gnss_ok INTEGER, + num_sats INTEGER, + source TEXT, + synced INTEGER DEFAULT 0, + FOREIGN KEY(session_id) REFERENCES sessions(session_id) +); + +CREATE INDEX IF NOT EXISTS idx_gnss_session_time + ON gnss_pvt(session_id, unix_ts); + +CREATE INDEX IF NOT EXISTS idx_gnss_unsynced + ON gnss_pvt(synced) WHERE synced=0;