From 4b2de69100acdd8aefb52559d08a57fce305925d Mon Sep 17 00:00:00 2001 From: chrisdavis2110 Date: Sat, 10 Jan 2026 14:05:28 -0800 Subject: [PATCH 1/4] added logos --- .gitignore | 3 +-- backend/static/wcmesh_logo.png | Bin 0 -> 17461 bytes backend/static/wcmesh_site_logo.png | Bin 0 -> 6342 bytes 3 files changed, 1 insertion(+), 2 deletions(-) create mode 100644 backend/static/wcmesh_logo.png create mode 100644 backend/static/wcmesh_site_logo.png diff --git a/.gitignore b/.gitignore index eb5c06f..0883cdd 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,4 @@ example2.gif backend/static/logo.png # runtime state -/data/state.json -/data/*.json +/data diff --git a/backend/static/wcmesh_logo.png b/backend/static/wcmesh_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..91cb577e4784b2ee69b61f62a370e0fb25da9c6a GIT binary patch literal 17461 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 literal 0 HcmV?d00001 From 0a1a67938f6b16e0e9815176b6bdae64b2d9d0a3 Mon Sep 17 00:00:00 2001 From: chrisdavis2110 Date: Sun, 18 Jan 2026 21:29:05 -0800 Subject: [PATCH 2/4] added device coords override --- backend/app.py | 122 ++++++++++++++++++++++++++++++++++++++++++++-- backend/config.py | 2 + backend/state.py | 1 + 3 files changed, 120 insertions(+), 5 deletions(-) diff --git a/backend/app.py b/backend/app.py index 0c4b675..a443103 100644 --- a/backend/app.py +++ b/backend/app.py @@ -75,6 +75,7 @@ from config import ( STATE_DIR, STATE_FILE, DEVICE_ROLES_FILE, + DEVICE_COORDS_FILE, NEIGHBOR_OVERRIDES_FILE, STATE_SAVE_INTERVAL, DEVICE_TTL_SECONDS, @@ -161,6 +162,7 @@ from state import ( message_origins, device_roles, device_role_sources, + device_coords, neighbor_edges, ) @@ -207,6 +209,27 @@ def _load_role_overrides() -> Dict[str, str]: return roles +def _load_coord_overrides() -> Dict[str, Dict[str, float]]: + if not DEVICE_COORDS_FILE or not os.path.exists(DEVICE_COORDS_FILE): + return {} + try: + with open(DEVICE_COORDS_FILE, "r", encoding="utf-8") as handle: + data = json.load(handle) + except Exception: + return {} + if not isinstance(data, dict): + return {} + coords: Dict[str, Dict[str, float]] = {} + for key, value in data.items(): + if not isinstance(key, str) or not isinstance(value, dict): + continue + lat = value.get("lat") + lon = value.get("lon") + if isinstance(lat, (int, float)) and isinstance(lon, (int, float)): + coords[key.strip()] = {"lat": float(lat), "lon": float(lon)} + return coords + + def _load_neighbor_overrides() -> None: if not NEIGHBOR_OVERRIDES_FILE or not os.path.exists(NEIGHBOR_OVERRIDES_FILE): return @@ -670,6 +693,14 @@ def _load_state() -> None: if dropped_ids: for device_id in dropped_ids: device_roles.pop(device_id, None) + # Load and apply coordinate overrides + coord_overrides = _load_coord_overrides() + if coord_overrides: + device_coords.clear() + device_coords.update(coord_overrides) + if dropped_ids: + for device_id in dropped_ids: + device_coords.pop(device_id, None) _rebuild_node_hash_map() for device_id, state in devices.items(): @@ -677,6 +708,11 @@ def _load_state() -> None: state.name = device_names[device_id] role_value = device_roles.get(device_id) state.role = role_value if role_value else None + # Apply coordinate overrides to loaded devices + coord_override = device_coords.get(device_id) + if coord_override: + state.lat = coord_override["lat"] + state.lon = coord_override["lon"] async def _state_saver() -> None: @@ -740,18 +776,80 @@ def mqtt_on_message(client, userdata, msg: mqtt.MQTTMessage): parsed, debug = _try_parse_payload(msg.topic, msg.payload) device_id_hint = parsed.get("device_id") if parsed else None - if parsed and _coords_are_zero(parsed.get("lat", 0), parsed.get("lon", 0)): + # Also try to get device_id from topic if parsing failed or no device_id in parsed data + topic_device_id = _device_id_from_topic(msg.topic) + device_id_for_override = device_id_hint or topic_device_id + + # Check if device has coordinate override - try multiple ways to match device_id + coord_override = None + matched_device_id = None + + # Try 1: device_id_for_override (parsed or topic-based) + if device_id_for_override and device_id_for_override in device_coords: + coord_override = device_coords[device_id_for_override] + matched_device_id = device_id_for_override + # Try 2: topic_device_id directly + elif topic_device_id and topic_device_id in device_coords: + coord_override = device_coords[topic_device_id] + matched_device_id = topic_device_id + # Try 3: device_id_hint directly + elif device_id_hint and device_id_hint in device_coords: + coord_override = device_coords[device_id_hint] + matched_device_id = device_id_hint + # Try 4: Check all parts of the topic path + else: + topic_parts = msg.topic.split("/") + for part in topic_parts: + if part and len(part) > 10 and part in device_coords: # Only check parts that look like device IDs (long hex strings) + coord_override = device_coords[part] + matched_device_id = part + if DEBUG_PAYLOAD: + print(f"[mqtt] Found coord override in topic path: topic={msg.topic} device_id={part}") + break + + has_coord_override = coord_override is not None + if has_coord_override and matched_device_id: + # Use override coordinates for filtering checks and inject into parsed data + check_lat = coord_override["lat"] + check_lon = coord_override["lon"] + # If parsing failed or has no location, create/update parsed data with override coords + if not parsed: + parsed = { + "device_id": matched_device_id, + "lat": coord_override["lat"], + "lon": coord_override["lon"], + "ts": time.time(), + } + device_id_hint = matched_device_id + debug["result"] = "coord_override_created" + elif parsed: + # If parsing succeeded but no location, inject override coordinates + if not parsed.get("lat") or not parsed.get("lon") or _coords_are_zero(parsed.get("lat", 0), parsed.get("lon", 0)): + parsed["lat"] = coord_override["lat"] + parsed["lon"] = coord_override["lon"] + if not device_id_hint: + parsed["device_id"] = matched_device_id + device_id_hint = matched_device_id + debug["result"] = debug.get("result") or "coord_override_applied" + else: + check_lat = parsed.get("lat") if parsed else None + check_lon = parsed.get("lon") if parsed else None + + # Don't filter 0,0 coordinates if device has a coordinate override + if parsed and _coords_are_zero(parsed.get("lat", 0), parsed.get("lon", 0)) and not has_coord_override: debug["result"] = "filtered_zero_coords" parsed = None - if parsed and not _within_map_radius(parsed.get("lat"), parsed.get("lon")): + # Check radius using override coordinates if available + if parsed and check_lat is not None and check_lon is not None and not _within_map_radius(check_lat, check_lon): debug["result"] = "filtered_radius" parsed = None - if device_id_hint: + if matched_device_id or device_id_for_override: + remove_id = matched_device_id or device_id_for_override loop.call_soon_threadsafe( update_queue.put_nowait, { "type": "device_remove", - "device_id": device_id_hint, + "device_id": remove_id, "reason": "radius", }, ) @@ -1160,7 +1258,16 @@ async def broadcaster(): event.get("type") == "device" else event) device_id = upd["device_id"] - if not _within_map_radius(upd.get("lat"), upd.get("lon")): + # Check if device has coordinate override before filtering by radius + coord_override = device_coords.get(device_id) + if coord_override: + # Use override coordinates for radius check + check_lat = coord_override["lat"] + check_lon = coord_override["lon"] + else: + check_lat = upd.get("lat") + check_lon = upd.get("lon") + if not _within_map_radius(check_lat, check_lon): if _evict_device(device_id): payload = {"type": "stale", "device_ids": [device_id]} dead = [] @@ -1186,6 +1293,11 @@ async def broadcaster(): role=upd.get("role") or device_roles.get(device_id), raw_topic=upd.get("raw_topic"), ) + # Apply coordinate overrides + coord_override = device_coords.get(device_id) + if coord_override: + device_state.lat = coord_override["lat"] + device_state.lon = coord_override["lon"] devices[device_id] = device_state seen_devices[device_id] = time.time() state.state_dirty = True diff --git a/backend/config.py b/backend/config.py index 40b95ef..3145b58 100644 --- a/backend/config.py +++ b/backend/config.py @@ -25,6 +25,8 @@ STATE_DIR = os.getenv("STATE_DIR", "/data") STATE_FILE = os.getenv("STATE_FILE", os.path.join(STATE_DIR, "state.json")) DEVICE_ROLES_FILE = os.getenv("DEVICE_ROLES_FILE", os.path.join(STATE_DIR, "device_roles.json")) +DEVICE_COORDS_FILE = os.getenv("DEVICE_COORDS_FILE", + os.path.join(STATE_DIR, "device_coords.json")) NEIGHBOR_OVERRIDES_FILE = os.getenv( "NEIGHBOR_OVERRIDES_FILE", os.path.join(STATE_DIR, "neighbor_overrides.json"), diff --git a/backend/state.py b/backend/state.py index b684b75..dcaec33 100644 --- a/backend/state.py +++ b/backend/state.py @@ -54,5 +54,6 @@ device_names: Dict[str, str] = {} message_origins: Dict[str, Dict[str, Any]] = {} device_roles: Dict[str, str] = {} device_role_sources: Dict[str, str] = {} +device_coords: Dict[str, Dict[str, float]] = {} neighbor_edges: Dict[str, Dict[str, Dict[str, Any]]] = {} state_dirty = False From e577e7157825474602bf84695173cab919220d09 Mon Sep 17 00:00:00 2001 From: chrisdavis2110 Date: Mon, 2 Feb 2026 21:42:35 -0800 Subject: [PATCH 3/4] device coords override fix --- .gitignore | 1 + backend/app.py | 113 +++++++++++++++++++++++++++++++++++++------------ 2 files changed, 87 insertions(+), 27 deletions(-) diff --git a/.gitignore b/.gitignore index 0968c68..f5c8bc7 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ backend/static/logo.png # runtime state /data/state.json /data/*.json +/data/*.jsonl # python bytecode __pycache__/ diff --git a/backend/app.py b/backend/app.py index 81e8349..9265a6d 100644 --- a/backend/app.py +++ b/backend/app.py @@ -841,60 +841,109 @@ def mqtt_on_message(client, userdata, msg: mqtt.MQTTMessage): device_id_hint = parsed.get("device_id") if parsed else None # Also try to get device_id from topic if parsing failed or no device_id in parsed data topic_device_id = _device_id_from_topic(msg.topic) - device_id_for_override = device_id_hint or topic_device_id - # Check if device has coordinate override - try multiple ways to match device_id + # Priority: decoded_pubkey (origin/repeater that sent packet) > parsed device_id > topic device_id (receiver) + decoded_pubkey = debug.get("decoded_pubkey") + if isinstance(decoded_pubkey, str) and decoded_pubkey.strip(): + decoded_pubkey = decoded_pubkey.strip() + else: + decoded_pubkey = None + + # Check if device has coordinate override - prioritize decoded packet public key (origin) coord_override = None matched_device_id = None - # Try 1: device_id_for_override (parsed or topic-based) - if device_id_for_override and device_id_for_override in device_coords: - coord_override = device_coords[device_id_for_override] - matched_device_id = device_id_for_override - # Try 2: topic_device_id directly - elif topic_device_id and topic_device_id in device_coords: - coord_override = device_coords[topic_device_id] - matched_device_id = topic_device_id - # Try 3: device_id_hint directly + # Try 1: decoded_pubkey (origin/repeater that sent the packet) - this is what we want! + if decoded_pubkey and decoded_pubkey in device_coords: + coord_override = device_coords[decoded_pubkey] + matched_device_id = decoded_pubkey + # Try 2: device_id_hint from parsed data (should also be decoded_pubkey if available) elif device_id_hint and device_id_hint in device_coords: coord_override = device_coords[device_id_hint] matched_device_id = device_id_hint - # Try 4: Check all parts of the topic path - else: + # Try 3: Check if decoded_pubkey matches any override via substring (for partial matches) + elif decoded_pubkey: + for override_id in device_coords.keys(): + if override_id in decoded_pubkey or decoded_pubkey in override_id: + coord_override = device_coords[override_id] + matched_device_id = override_id + break + # Try 4: topic_device_id (receiver/observer) - only as fallback + if not coord_override and topic_device_id and topic_device_id in device_coords: + coord_override = device_coords[topic_device_id] + matched_device_id = topic_device_id + # Try 5: Check all parts of the topic path (receiver/observer) + if not coord_override: topic_parts = msg.topic.split("/") for part in topic_parts: if part and len(part) > 10 and part in device_coords: # Only check parts that look like device IDs (long hex strings) coord_override = device_coords[part] matched_device_id = part - if DEBUG_PAYLOAD: - print(f"[mqtt] Found coord override in topic path: topic={msg.topic} device_id={part}") break + # Try 6: Check if any device_id in override file is a substring of any topic part (for partial matches) + if not coord_override: + for part in topic_parts: + if part and len(part) > 10: + # Check if any override key is contained in this topic part or vice versa + for override_id in device_coords.keys(): + if override_id in part or part in override_id: + coord_override = device_coords[override_id] + matched_device_id = override_id + break + if coord_override: + break has_coord_override = coord_override is not None + # Initialize check_lat and check_lon - will be set from override or parsed data + check_lat = None + check_lon = None + if has_coord_override and matched_device_id: # Use override coordinates for filtering checks and inject into parsed data check_lat = coord_override["lat"] check_lon = coord_override["lon"] + # Normalize timestamp: if it's too far in the future (> 1 hour), use current time + now_ts = time.time() + parsed_ts = parsed.get("ts") if parsed else None + if parsed_ts and parsed_ts > now_ts + 3600: # More than 1 hour in future + parsed_ts = now_ts + if DEBUG_PAYLOAD: + print(f"[mqtt] Normalized future timestamp: device={matched_device_id} future_ts={parsed.get('ts')} -> now={now_ts}") # If parsing failed or has no location, create/update parsed data with override coords + # Use decoded_pubkey as device_id if available (origin), otherwise use matched_device_id + target_device_id = decoded_pubkey or matched_device_id if not parsed: parsed = { - "device_id": matched_device_id, + "device_id": target_device_id, "lat": coord_override["lat"], "lon": coord_override["lon"], - "ts": time.time(), + "ts": now_ts, } - device_id_hint = matched_device_id + device_id_hint = target_device_id debug["result"] = "coord_override_created" + if DEBUG_PAYLOAD: + print(f"[mqtt] Created parsed data from coord override: device_id={target_device_id} (matched_override={matched_device_id}) lat={coord_override['lat']} lon={coord_override['lon']}") elif parsed: # If parsing succeeded but no location, inject override coordinates if not parsed.get("lat") or not parsed.get("lon") or _coords_are_zero(parsed.get("lat", 0), parsed.get("lon", 0)): parsed["lat"] = coord_override["lat"] parsed["lon"] = coord_override["lon"] - if not device_id_hint: + # Ensure device_id is set to the decoded_pubkey (origin) if available + if decoded_pubkey: + parsed["device_id"] = decoded_pubkey + device_id_hint = decoded_pubkey + elif not device_id_hint: parsed["device_id"] = matched_device_id device_id_hint = matched_device_id debug["result"] = debug.get("result") or "coord_override_applied" - else: + if DEBUG_PAYLOAD: + print(f"[mqtt] Applied coord override to parsed data: device_id={parsed.get('device_id')} (matched_override={matched_device_id}) lat={coord_override['lat']} lon={coord_override['lon']}") + # Always normalize timestamp if it's in the future + if parsed_ts: + parsed["ts"] = parsed_ts + + # Set check_lat/check_lon from parsed data if not already set from override + if check_lat is None and check_lon is None: check_lat = parsed.get("lat") if parsed else None check_lon = parsed.get("lon") if parsed else None @@ -905,9 +954,12 @@ def mqtt_on_message(client, userdata, msg: mqtt.MQTTMessage): # Check radius using override coordinates if available if parsed and check_lat is not None and check_lon is not None and not _within_map_radius(check_lat, check_lon): debug["result"] = "filtered_radius" + if DEBUG_PAYLOAD: + device_id_for_log = matched_device_id or decoded_pubkey or device_id_hint or topic_device_id or parsed.get("device_id") + print(f"[mqtt] Filtered device by radius: device_id={device_id_for_log} lat={check_lat} lon={check_lon} (radius={MAP_RADIUS_KM}km)") parsed = None - if matched_device_id or device_id_for_override: - remove_id = matched_device_id or device_id_for_override + if matched_device_id or decoded_pubkey or device_id_hint: + remove_id = matched_device_id or decoded_pubkey or device_id_hint loop.call_soon_threadsafe( update_queue.put_nowait, { @@ -1317,11 +1369,18 @@ async def broadcaster(): clients.discard(ws) continue is_new_device = device_id not in devices + # Normalize timestamp: if it's too far in the future (> 1 hour), use current time + now_ts = time.time() + device_ts = upd.get("ts", now_ts) + if device_ts > now_ts + 3600: # More than 1 hour in future + if DEBUG_PAYLOAD: + print(f"[mqtt] Normalized future timestamp in device state: device={device_id} future_ts={device_ts} -> now={now_ts}") + device_ts = now_ts device_state = DeviceState( device_id=device_id, lat=upd["lat"], lon=upd["lon"], - ts=upd.get("ts", time.time()), + ts=device_ts, heading=upd.get("heading"), speed=upd.get("speed"), rssi=upd.get("rssi"), @@ -2059,10 +2118,10 @@ def map_page(request: Request): safe_image = html.escape(str(SITE_OG_IMAGE), quote=True) og_image_tag = f'' twitter_image_tag = f'' - + content = content.replace("{{OG_IMAGE_TAG}}", og_image_tag) content = content.replace("{{TWITTER_IMAGE_TAG}}", twitter_image_tag) - + trail_info_suffix = "" if TRAIL_LEN > 0: trail_info_suffix = f" Trails show last ~{TRAIL_LEN} points." @@ -2618,7 +2677,7 @@ async def verify_turnstile(request: Request): }, status_code=200, ) - + # Set auth cookie (expires in TURNSTILE_TOKEN_TTL_SECONDS) response.set_cookie( key="meshmap_auth", @@ -2627,7 +2686,7 @@ async def verify_turnstile(request: Request): path="/", samesite="lax", ) - + return response except json.JSONDecodeError: From 6b127d55225032ddc2d70a25f532bbd0420c8502 Mon Sep 17 00:00:00 2001 From: chrisdavis2110 Date: Thu, 5 Feb 2026 20:33:56 -0800 Subject: [PATCH 4/4] changed device_ttl to hours --- backend/config.py | 3 ++- docker-compose.yaml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/config.py b/backend/config.py index 71bbfa1..e642c59 100644 --- a/backend/config.py +++ b/backend/config.py @@ -33,7 +33,8 @@ NEIGHBOR_OVERRIDES_FILE = os.getenv( ) STATE_SAVE_INTERVAL = float(os.getenv("STATE_SAVE_INTERVAL", "5")) -DEVICE_TTL_SECONDS = int(os.getenv("DEVICE_TTL_SECONDS", "300")) +DEVICE_TTL_HOURS = float(os.getenv("DEVICE_TTL_HOURS", "72")) # 72 hours default +DEVICE_TTL_SECONDS = int(DEVICE_TTL_HOURS * 3600) TRAIL_LEN = int(os.getenv("TRAIL_LEN", "30")) ROUTE_TTL_SECONDS = int(os.getenv("ROUTE_TTL_SECONDS", "120")) ROUTE_PAYLOAD_TYPES = os.getenv("ROUTE_PAYLOAD_TYPES", "8,9,2,5,4") diff --git a/docker-compose.yaml b/docker-compose.yaml index 37cf3e8..e587c98 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -33,7 +33,7 @@ services: MQTT_WS_PATH: "${MQTT_WS_PATH:-/mqtt}" MQTT_TLS: "${MQTT_TLS:-false}" MQTT_TOPIC: "${MQTT_TOPIC:-meshcore/#}" - DEVICE_TTL_SECONDS: "${DEVICE_TTL_SECONDS:-300}" + DEVICE_TTL_HOURS: "${DEVICE_TTL_HOURS:-72}" HEAT_TTL_SECONDS: "${HEAT_TTL_SECONDS:-600}" TRAIL_LEN: "${TRAIL_LEN:-30}" ROUTE_TTL_SECONDS: "${ROUTE_TTL_SECONDS:-120}"