diff --git a/rtk_rover_recorder.py b/rtk_rover_recorder.py index b8120b7..c387724 100644 --- a/rtk_rover_recorder.py +++ b/rtk_rover_recorder.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -import sys, time, struct, argparse, threading, os, sqlite3, uuid, select, termios, tty +import sys, time, struct, argparse, threading, os, sqlite3, uuid, select, termios, tty, json from typing import Optional, Tuple, Dict import serial from serial.serialutil import SerialException @@ -570,6 +570,8 @@ def main(): help="Start SQLite recording automatically on service start") ap.add_argument("--record-note", default="auto", help="Note stored in sessions table when --auto-record is used") + ap.add_argument("--cmd-file", default="/home/naue/rtk/rtk_rover_cmd.json", + help="Command file for web UI start/stop recording") ap.add_argument("--site-log", action="store_true", help="Enable construction-site FIX-only CSV/GeoJSON/status logging") @@ -667,6 +669,28 @@ def main(): recorder.start(note="manual") print(f"⏺️ Recording START (session={recorder.session_id})") + # Web/API command file: {"cmd":"start"} or {"cmd":"stop"} + try: + if args.cmd_file and os.path.exists(args.cmd_file): + with open(args.cmd_file, "r", encoding="utf-8") as f: + cmd = json.load(f) + try: + os.remove(args.cmd_file) + except Exception: + pass + + action = str(cmd.get("cmd", "")).lower() + note = str(cmd.get("note", "web")) + + if action == "start" and not recorder.is_recording(): + recorder.start(note=note) + print(f"⏺️ Web Recording START (session={recorder.session_id})", flush=True) + elif action == "stop" and recorder.is_recording(): + recorder.stop() + print("⏹️ Web Recording STOP", flush=True) + except Exception as e: + print(f"WEB CMD ERROR: {e}", flush=True) + f = read_ubx_frame(zed, timeout=0.05) pvt = None hpp = None diff --git a/web/app.py b/web/app.py new file mode 100644 index 0000000..e04e47b --- /dev/null +++ b/web/app.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +import sqlite3, json, time, os +from flask import Flask, render_template, request, Response, redirect, url_for + +DB_PATH = "/home/naue/rtk/rtk_rover.db" +CMD_FILE = "/home/naue/rtk/rtk_rover_cmd.json" + +app = Flask(__name__) + +def db(): + con = sqlite3.connect(DB_PATH) + con.row_factory = sqlite3.Row + return con + +def write_cmd(cmd): + tmp = CMD_FILE + ".tmp" + with open(tmp, "w", encoding="utf-8") as f: + json.dump({"cmd": cmd, "note": "web", "ts": time.time()}, f) + os.replace(tmp, CMD_FILE) + +@app.route("/", methods=["GET"]) +def index(): + con = db() + sessions = con.execute(""" + SELECT session_id, + datetime(start_ts,'unixepoch','localtime') AS start_time, + CASE WHEN end_ts IS NULL THEN '' ELSE datetime(end_ts,'unixepoch','localtime') END AS end_time, + note, + end_ts + FROM sessions + ORDER BY start_ts DESC + LIMIT 50 + """).fetchall() + + active_session = con.execute(""" + SELECT session_id, datetime(start_ts,'unixepoch','localtime') AS start_time, note + FROM sessions + WHERE end_ts IS NULL + ORDER BY start_ts DESC + LIMIT 1 + """).fetchone() + + session_id = request.args.get("session_id") or (sessions[0]["session_id"] if sessions else "") + only_fix = request.args.get("only_fix", "0") == "1" + + params = [] + where = "" + if session_id: + where += " WHERE session_id=?" + params.append(session_id) + if only_fix: + where += " AND carr_soln=2" if where else " WHERE carr_soln=2" + + points = con.execute(f""" + SELECT datetime(unix_ts,'unixepoch','localtime') AS t, + lat, lon, hmsl_corr_m, hacc_m, vacc_m, carr_soln, num_sats, source + FROM gnss_pvt + {where} + ORDER BY unix_ts DESC + LIMIT 200 + """, params).fetchall() + + count = con.execute(f"SELECT COUNT(*) AS n FROM gnss_pvt {where}", params).fetchone()["n"] + + return render_template("index.html", + sessions=sessions, + session_id=session_id, + only_fix=only_fix, + points=points, + count=count, + active_session=active_session) + +@app.route("/record/start", methods=["POST"]) +def record_start(): + write_cmd("start") + return redirect(url_for("index")) + +@app.route("/record/stop", methods=["POST"]) +def record_stop(): + write_cmd("stop") + return redirect(url_for("index")) + +@app.route("/export.csv") +def export_csv(): + session_id = request.args.get("session_id") + only_fix = request.args.get("only_fix", "0") == "1" + + params = [] + where = "" + if session_id: + where += " WHERE session_id=?" + params.append(session_id) + if only_fix: + where += " AND carr_soln=2" if where else " WHERE carr_soln=2" + + con = db() + rows = con.execute(f""" + SELECT datetime(unix_ts,'unixepoch','localtime') AS t, + lat, lon, hmsl_m, hmsl_corr_m, hacc_m, vacc_m, + fix_type, carr_soln, diff_soln, gnss_ok, num_sats, source + FROM gnss_pvt + {where} + ORDER BY unix_ts ASC + """, params).fetchall() + + def generate(): + yield "time,lat,lon,hmsl_m,hmsl_corr_m,hacc_m,vacc_m,fix_type,carr_soln,diff_soln,gnss_ok,num_sats,source\n" + for r in rows: + yield ",".join(str(r[k]) if r[k] is not None else "" for k in r.keys()) + "\n" + + return Response(generate(), mimetype="text/csv", + headers={"Content-Disposition": "attachment; filename=rtk_export.csv"}) + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=8080) diff --git a/web/templates/index.html b/web/templates/index.html new file mode 100644 index 0000000..8005e4e --- /dev/null +++ b/web/templates/index.html @@ -0,0 +1,76 @@ + + + + + RTK Rover DB + + + +

RTK Rover Datenbank

+ +
+
+ + + + + + + CSV exportieren +
+
+ +
+ Punkte: {{count}} | + Anzeige: letzte {{points|length}} +
+ + + + + + + + + + + + + + {% for p in points %} + + + + + + + + + + + + {% endfor %} +
ZeitLatLonhMSL corrhAccvAccRTKSatsQuelle
{{p.t}}{{"%.8f"|format(p.lat)}}{{"%.8f"|format(p.lon)}}{{"%.3f"|format(p.hmsl_corr_m)}}{{"%.3f"|format(p.hacc_m)}}{{"%.3f"|format(p.vacc_m)}} + {% if p.carr_soln == 2 %}FIX{% elif p.carr_soln == 1 %}FLOAT{% else %}—{% endif %} + {{p.num_sats}}{{p.source}}
+ +