#!/usr/bin/env python3 import json import time import shutil from pathlib import Path from datetime import datetime import paho.mqtt.client as mqtt MQTT_HOST = "192.168.88.56" MQTT_PORT = 1883 MQTT_USER = "mqtt" MQTT_PASSWORD = "" BASE_DIR = Path("/volume2/docker/tesvor-map") CURRENT_FILE = BASE_DIR / "current.json" ARCHIVE_DIR = BASE_DIR / "archive" ARCHIVE_INDEX_FILE = BASE_DIR / "archive_index.json" POINT_TOPIC = "tesvor/x500/map/points" STATE_TOPIC = "tesvor/x500/state" BASE_DIR.mkdir(parents=True, exist_ok=True) ARCHIVE_DIR.mkdir(parents=True, exist_ok=True) points_by_seq = {} session_started = None last_state = None archived_this_session = False def save_current(): global session_started ordered = [ points_by_seq[k] for k in sorted(points_by_seq.keys()) ] payload = { "device": "tesvor_x500", "updated": datetime.now().isoformat(timespec="seconds"), "started": session_started, "point_count": len(ordered), "points": ordered, } tmp = CURRENT_FILE.with_suffix(".json.tmp") tmp.write_text(json.dumps(payload, ensure_ascii=False, separators=(",", ":")), encoding="utf-8") tmp.replace(CURRENT_FILE) def write_archive_index(): files = [] for f in sorted(ARCHIVE_DIR.glob("*.json"), reverse=True): try: stat = f.stat() files.append({ "name": f.name, "path": f"archive/{f.name}", "size": stat.st_size, "modified": datetime.fromtimestamp(stat.st_mtime).isoformat(timespec="seconds"), }) except Exception: pass tmp = ARCHIVE_INDEX_FILE.with_suffix(".json.tmp") tmp.write_text(json.dumps({ "updated": datetime.now().isoformat(timespec="seconds"), "files": files }, ensure_ascii=False, separators=(",", ":")), encoding="utf-8") tmp.replace(ARCHIVE_INDEX_FILE) def archive_current(reason): global archived_this_session if archived_this_session: return if not CURRENT_FILE.exists(): return if len(points_by_seq) < 5: return ts = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") archive_file = ARCHIVE_DIR / f"tesvor_map_{ts}_{reason}.json" shutil.copy2(CURRENT_FILE, archive_file) archived_this_session = True write_archive_index() print(f"[ARCHIVE] {archive_file}") def reset_session(): global points_by_seq, session_started, archived_this_session points_by_seq = {} session_started = datetime.now().isoformat(timespec="seconds") archived_this_session = False save_current() print("[SESSION] reset") def on_connect(client, userdata, flags, rc): print(f"[MQTT] connected rc={rc}") client.subscribe(POINT_TOPIC) client.subscribe(STATE_TOPIC) def on_message(client, userdata, msg): global session_started, last_state, archived_this_session topic = msg.topic text = msg.payload.decode("utf-8", errors="replace").strip() if topic == STATE_TOPIC: state = text if state == last_state: return print(f"[STATE] {state}") last_state = state if state in ("cleaning", "spot_cleaning", "edge_cleaning"): if session_started is None or archived_this_session: reset_session() if state in ("charging", "docked"): archive_current(state) save_current() return if topic == POINT_TOPIC: if session_started is None: reset_session() try: data = json.loads(text) except Exception as e: print(f"[WARN] invalid json: {e}: {text}") return added = 0 for p in data.get("p", []): if len(p) != 4: continue seq, x, y, kind = p seq = int(seq) if seq not in points_by_seq: points_by_seq[seq] = { "seq": seq, "x": int(x), "y": int(y), "type": int(kind), } added += 1 if added: print(f"[POINTS] +{added}, total={len(points_by_seq)}") save_current() client = mqtt.Client() if MQTT_USER: client.username_pw_set(MQTT_USER, MQTT_PASSWORD) client.on_connect = on_connect client.on_message = on_message client.connect(MQTT_HOST, MQTT_PORT, 60) write_archive_index() client.loop_forever()