diff --git a/AGENTS.md b/AGENTS.md
index a143bd7..9b1cbab 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -28,6 +28,9 @@ Current version: `1.0.7` (see `VERSIONS.md`).
## Coding Style & Naming Conventions
- Python in `backend/*.py` uses **2-space indentation**; keep it consistent.
+- The project enforces an **80-character column limit** for Python code to maintain readability.
+- Formatting is handled by `yapf` using the config in `backend/.style.yapf`.
+ - To format code manually: `yapf --in-place --recursive --style backend/.style.yapf backend/`
- HTML/CSS/JS in `backend/static/index.html` uses 2 spaces as well.
- Use lowercase, underscore-separated names for Python variables/functions.
- Prefer small helper functions for parsing/normalization; keep logging concise.
diff --git a/backend/.style.yapf b/backend/.style.yapf
index ad1cef8..1c085ea 100644
--- a/backend/.style.yapf
+++ b/backend/.style.yapf
@@ -1,5 +1,6 @@
[style]
based_on_style = facebook
indent_width = 2
-column_limit = 200
+column_limit = 80
continuation_indent_width = 2
+
diff --git a/backend/app.py b/backend/app.py
index 0c4b675..8fa74da 100644
--- a/backend/app.py
+++ b/backend/app.py
@@ -239,8 +239,10 @@ def _load_neighbor_overrides() -> None:
_add_pair(src.strip(), targets.strip())
elif isinstance(data, list):
for item in data:
- if (isinstance(item, (list, tuple)) and len(item) >= 2 and
- isinstance(item[0], str) and isinstance(item[1], str)):
+ if (
+ isinstance(item, (list, tuple)) and len(item) >= 2 and
+ isinstance(item[0], str) and isinstance(item[1], str)
+ ):
_add_pair(item[0].strip(), item[1].strip())
elif isinstance(item, dict):
src = item.get("from") or item.get("src") or item.get("a")
@@ -249,13 +251,14 @@ def _load_neighbor_overrides() -> None:
_add_pair(src.strip(), dst.strip())
if added:
- print(f"[neighbors] loaded {added} override pairs from {NEIGHBOR_OVERRIDES_FILE}")
+ print(
+ f"[neighbors] loaded {added} override pairs from {NEIGHBOR_OVERRIDES_FILE}"
+ )
-def _touch_neighbor(src_id: str,
- dst_id: str,
- ts: float,
- manual: bool = False) -> None:
+def _touch_neighbor(
+ src_id: str, dst_id: str, ts: float, manual: bool = False
+) -> None:
if not src_id or not dst_id or src_id == dst_id:
return
neighbors = neighbor_edges.setdefault(src_id, {})
@@ -270,8 +273,7 @@ def _touch_neighbor(src_id: str,
entry["last_seen"] = max(float(entry.get("last_seen", 0.0)), float(ts))
-def _record_neighbors(point_ids: List[Optional[str]],
- ts: float) -> None:
+def _record_neighbors(point_ids: List[Optional[str]], ts: float) -> None:
if not point_ids or len(point_ids) < 2:
return
for idx in range(len(point_ids) - 1):
@@ -302,7 +304,8 @@ def _serialize_state() -> Dict[str, Any]:
"version": 1,
"saved_at": time.time(),
"devices": {
- k: asdict(v) for k, v in devices.items()
+ k: asdict(v)
+ for k, v in devices.items()
},
"trails": trails,
"seen_devices": seen_devices,
@@ -338,7 +341,8 @@ def _check_git_updates() -> None:
stderr=subprocess.DEVNULL,
)
inside = _run_git(
- ["git", "-C", GIT_CHECK_PATH, "rev-parse", "--is-inside-work-tree"])
+ ["git", "-C", GIT_CHECK_PATH, "rev-parse", "--is-inside-work-tree"]
+ )
if inside.lower() != "true":
git_update_info["error"] = "not_git_repo"
return
@@ -404,8 +408,8 @@ def _iso_from_ts(ts: Optional[float]) -> Optional[str]:
if ts is None:
return None
try:
- return datetime.fromtimestamp(
- float(ts), tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
+ return datetime.fromtimestamp(float(ts), tz=timezone.utc
+ ).strftime("%Y-%m-%dT%H:%M:%SZ")
except Exception:
return None
@@ -540,8 +544,8 @@ def _require_prod_token(request: Request) -> None:
return
if not PROD_TOKEN:
raise HTTPException(status_code=503, detail="prod_token_not_set")
- token = request.query_params.get("token") or request.query_params.get(
- "access_token")
+ token = request.query_params.get("token"
+ ) or request.query_params.get("access_token")
if not token:
token = _extract_token(request.headers)
if token != PROD_TOKEN:
@@ -579,8 +583,8 @@ def _load_state() -> None:
state = DeviceState(**value)
except Exception:
continue
- if _coords_are_zero(
- state.lat, state.lon) or not _within_map_radius(state.lat, state.lon):
+ if _coords_are_zero(state.lat, state.lon
+ ) or not _within_map_radius(state.lat, state.lon):
dropped_ids.add(str(key))
continue
loaded_devices[key] = state
@@ -631,9 +635,12 @@ def _load_state() -> None:
raw_names = data.get("device_names") or {}
if isinstance(raw_names, dict):
device_names.clear()
- device_names.update({
- str(k): str(v) for k, v in raw_names.items() if str(v).strip()
- })
+ device_names.update(
+ {
+ str(k): str(v)
+ for k, v in raw_names.items() if str(v).strip()
+ }
+ )
else:
device_names.clear()
if dropped_ids:
@@ -642,9 +649,12 @@ def _load_state() -> None:
raw_role_sources = data.get("device_role_sources") or {}
if isinstance(raw_role_sources, dict):
device_role_sources.clear()
- device_role_sources.update({
- str(k): str(v) for k, v in raw_role_sources.items() if str(v).strip()
- })
+ device_role_sources.update(
+ {
+ str(k): str(v)
+ for k, v in raw_role_sources.items() if str(v).strip()
+ }
+ )
else:
device_role_sources.clear()
if dropped_ids:
@@ -703,12 +713,9 @@ def mqtt_on_connect(client, userdata, flags, reason_code, properties=None):
client.subscribe(topic, qos=0)
-def mqtt_on_disconnect(client,
- userdata,
- reason_code,
- properties=None,
- *args,
- **kwargs):
+def mqtt_on_disconnect(
+ client, userdata, reason_code, properties=None, *args, **kwargs
+):
print(f"[mqtt] disconnected reason_code={reason_code}")
@@ -762,8 +769,9 @@ def mqtt_on_message(client, userdata, msg: mqtt.MQTTMessage):
role_target_id = origin_id
if device_role and result.startswith("decoded"):
role_target_id = None
- loc_meta = (decoder_meta.get("location")
- if isinstance(decoder_meta, dict) else None)
+ loc_meta = (
+ decoder_meta.get("location") if isinstance(decoder_meta, dict) else None
+ )
loc_pubkey = loc_meta.get("pubkey") if isinstance(loc_meta, dict) else None
if isinstance(loc_pubkey, str) and loc_pubkey.strip():
role_target_id = loc_pubkey
@@ -788,15 +796,17 @@ def mqtt_on_message(client, userdata, msg: mqtt.MQTTMessage):
}
debug_last.append(debug_entry)
if msg.topic.endswith("/status"):
- status_last.append({
- "ts": debug_entry["ts"],
- "topic": msg.topic,
- "device_name": debug.get("device_name"),
- "device_role": debug.get("device_role"),
- "origin_id": origin_id,
- "json_keys": debug_entry.get("json_keys"),
- "payload_preview": debug_entry["payload_preview"],
- })
+ status_last.append(
+ {
+ "ts": debug_entry["ts"],
+ "topic": msg.topic,
+ "device_name": debug.get("device_name"),
+ "device_role": debug.get("device_role"),
+ "origin_id": origin_id,
+ "json_keys": debug_entry.get("json_keys"),
+ "payload_preview": debug_entry["payload_preview"],
+ }
+ )
result_counts[result] = result_counts.get(result, 0) + 1
@@ -844,8 +854,8 @@ def mqtt_on_message(client, userdata, msg: mqtt.MQTTMessage):
direction = debug.get("direction")
receiver_id = _device_id_from_topic(msg.topic)
route_origin_id = None
- loc_meta = decoder_meta.get("location") if isinstance(decoder_meta,
- dict) else None
+ loc_meta = decoder_meta.get("location"
+ ) if isinstance(decoder_meta, dict) else None
if isinstance(loc_meta, dict):
decoded_pubkey = loc_meta.get("pubkey")
if decoded_pubkey:
@@ -932,12 +942,15 @@ def mqtt_on_message(client, userdata, msg: mqtt.MQTTMessage):
)
route_emitted = True
- if (not route_emitted and direction_value == "rx" and
- msg.topic.endswith("/packets") and receiver_id and route_origin_id and
- receiver_id != route_origin_id and
- payload_type in ROUTE_PAYLOAD_TYPES_SET):
- fallback_id = (message_hash or
- f"{route_origin_id}-{receiver_id}-{int(time.time() * 1000)}")
+ if (
+ not route_emitted and direction_value == "rx" and
+ msg.topic.endswith("/packets") and receiver_id and route_origin_id and
+ receiver_id != route_origin_id and payload_type in ROUTE_PAYLOAD_TYPES_SET
+ ):
+ fallback_id = (
+ message_hash or
+ f"{route_origin_id}-{receiver_id}-{int(time.time() * 1000)}"
+ )
loop.call_soon_threadsafe(
update_queue.put_nowait,
{
@@ -972,10 +985,12 @@ def mqtt_on_message(client, userdata, msg: mqtt.MQTTMessage):
f"[mqtt] PARSED topic={msg.topic} device={parsed['device_id']} lat={parsed['lat']} lon={parsed['lon']}"
)
- loop.call_soon_threadsafe(update_queue.put_nowait, {
- "type": "device",
- "data": parsed
- })
+ loop.call_soon_threadsafe(
+ update_queue.put_nowait, {
+ "type": "device",
+ "data": parsed
+ }
+ )
# =========================
@@ -986,8 +1001,8 @@ async def broadcaster():
event = await update_queue.get()
if isinstance(event, dict) and event.get("type") in (
- "device_name",
- "device_role",
+ "device_name",
+ "device_role",
):
device_id = event.get("device_id")
device_state = devices.get(device_id)
@@ -1066,29 +1081,36 @@ async def broadcaster():
)
if not points and route_mode == "fanout":
- points = _route_points_from_device_ids(event.get("origin_id"),
- event.get("receiver_id"))
- if (points and event.get("origin_id") and event.get("receiver_id") and
- len(points) == 2):
+ points = _route_points_from_device_ids(
+ event.get("origin_id"), event.get("receiver_id")
+ )
+ if (
+ points and event.get("origin_id") and event.get("receiver_id") and
+ len(points) == 2
+ ):
point_ids = [event.get("origin_id"), event.get("receiver_id")]
# Fallback: if path hashes are missing/unknown, draw a direct link when possible.
if not points:
- points = _route_points_from_device_ids(event.get("origin_id"),
- event.get("receiver_id"))
+ points = _route_points_from_device_ids(
+ event.get("origin_id"), event.get("receiver_id")
+ )
if points:
route_mode = "direct"
- if (event.get("origin_id") and event.get("receiver_id") and
- len(points) == 2):
+ if (
+ event.get("origin_id") and event.get("receiver_id") and
+ len(points) == 2
+ ):
point_ids = [event.get("origin_id"), event.get("receiver_id")]
if not points:
continue
if MAP_RADIUS_KM > 0:
- outside = any(not _within_map_radius(point[0], point[1])
- for point in points
- if isinstance(point, (list, tuple)) and len(point) >= 2)
+ outside = any(
+ not _within_map_radius(point[0], point[1]) for point in points
+ if isinstance(point, (list, tuple)) and len(point) >= 2
+ )
if outside:
continue
@@ -1156,8 +1178,10 @@ async def broadcaster():
clients.discard(ws)
continue
- upd = (event.get("data") if isinstance(event, dict) and
- event.get("type") == "device" else event)
+ upd = (
+ event.get("data")
+ if isinstance(event, dict) and event.get("type") == "device" else event
+ )
device_id = upd["device_id"]
if not _within_map_radius(upd.get("lat"), upd.get("lon")):
@@ -1196,11 +1220,13 @@ async def broadcaster():
if device_state.role:
device_roles[device_id] = device_state.role
- if TRAIL_LEN > 0 and not _coords_are_zero(device_state.lat,
- device_state.lon):
+ if TRAIL_LEN > 0 and not _coords_are_zero(
+ device_state.lat, device_state.lon
+ ):
trails.setdefault(device_id, [])
trails[device_id].append(
- [device_state.lat, device_state.lon, device_state.ts])
+ [device_state.lat, device_state.lon, device_state.ts]
+ )
if len(trails[device_id]) > TRAIL_LEN:
trails[device_id] = trails[device_id][-TRAIL_LEN:]
elif device_id in trails:
@@ -1255,9 +1281,9 @@ async def reaper():
if not isinstance(points, list):
continue
if any(
- _coords_are_zero(p[0], p[1])
- for p in points
- if isinstance(p, list) and len(p) >= 2):
+ _coords_are_zero(p[0], p[1])
+ for p in points if isinstance(p, list) and len(p) >= 2
+ ):
bad_routes.append(route_id)
if bad_routes:
payload = {"type": "route_remove", "route_ids": bad_routes}
@@ -1299,13 +1325,17 @@ async def reaper():
json.dumps({
"type": "history_edges",
"edges": history_updates
- }))
+ })
+ )
if history_removed:
await ws.send_text(
- json.dumps({
- "type": "history_edges_remove",
- "edge_ids": history_removed,
- }))
+ json.dumps(
+ {
+ "type": "history_edges_remove",
+ "edge_ids": history_removed,
+ }
+ )
+ )
except Exception:
dead.append(ws)
for ws in dead:
@@ -1324,8 +1354,9 @@ async def reaper():
_prune_neighbors(now)
- prune_after = (max(DEVICE_TTL_SECONDS *
- 3, 900) if DEVICE_TTL_SECONDS > 0 else 86400)
+ prune_after = (
+ max(DEVICE_TTL_SECONDS * 3, 900) if DEVICE_TTL_SECONDS > 0 else 86400
+ )
for dev_id, last in list(seen_devices.items()):
if now - last > prune_after:
seen_devices.pop(dev_id, None)
@@ -1348,8 +1379,10 @@ def root(request: Request):
# Check for lat/lon parameters for dynamic preview image
query_params = request.query_params
lat_param = query_params.get("lat") or query_params.get("latitude")
- lon_param = (query_params.get("lon") or query_params.get("lng") or
- query_params.get("long") or query_params.get("longitude"))
+ lon_param = (
+ query_params.get("lon") or query_params.get("lng") or
+ query_params.get("long") or query_params.get("longitude")
+ )
zoom_param = query_params.get("zoom")
og_image_tag = ""
@@ -1367,13 +1400,15 @@ def root(request: Request):
# Generate preview image URL pointing to our own server
# Use absolute URL for better compatibility with Discord and other platforms
base_url = str(request.url).split("?")[0].rstrip("/")
- preview_params = urlencode({
- "lat": lat,
- "lon": lon,
- "zoom": zoom,
- "marker": "blue",
- "theme": "dark",
- })
+ preview_params = urlencode(
+ {
+ "lat": lat,
+ "lon": lon,
+ "zoom": zoom,
+ "marker": "blue",
+ "theme": "dark",
+ }
+ )
preview_url = f"{base_url}/preview.png?{preview_params}"
# Ensure absolute URL (use SITE_URL if available, otherwise construct from request)
@@ -1394,7 +1429,8 @@ def root(request: Request):
f'\n'
f' \n'
f' \n'
- f' ')
+ f' '
+ )
twitter_image_tag = f''
# If static image is configured, add it as a fallback
@@ -1413,7 +1449,8 @@ def root(request: Request):
safe_image = html.escape(str(SITE_OG_IMAGE), quote=True)
og_image_tag = f''
twitter_image_tag = (
- f'')
+ f''
+ )
elif SITE_OG_IMAGE:
safe_image = html.escape(str(SITE_OG_IMAGE), quote=True)
og_image_tag = f''
@@ -1498,11 +1535,11 @@ def root(request: Request):
@app.get("/preview.png")
async def preview_image(
- lat: Optional[float] = Query(None, alias="lat"),
- lon: Optional[float] = Query(None, alias="lon"),
- zoom: Optional[int] = Query(13, alias="zoom"),
- marker: Optional[str] = Query("blue", alias="marker"),
- theme: Optional[str] = Query("dark", alias="theme"),
+ lat: Optional[float] = Query(None, alias="lat"),
+ lon: Optional[float] = Query(None, alias="lon"),
+ zoom: Optional[int] = Query(13, alias="zoom"),
+ marker: Optional[str] = Query("blue", alias="marker"),
+ theme: Optional[str] = Query("dark", alias="theme"),
):
"""
Generate a preview image of the map with a pin marker at the specified coordinates.
@@ -1572,8 +1609,9 @@ async def preview_image(
start_tile_y = center_tile_y - tiles_y // 2
# Create blank image with theme-appropriate background
- bg_color = ((18, 18, 18) if theme_str == "dark" else
- (242, 239, 233)) # Dark or light background
+ bg_color = (
+ (18, 18, 18) if theme_str == "dark" else (242, 239, 233)
+ ) # Dark or light background
final_image = Image.new("RGB", (width, height), bg_color)
# Fetch and composite tiles
@@ -1599,10 +1637,14 @@ async def preview_image(
tile_img = Image.open(BytesIO(response.content))
# Calculate position: center the marker at the center of the image
# The center tile should place the marker at the center pixel position
- x_offset = ((tx - tiles_x // 2) * tile_size + width // 2 -
- center_tile_pixel_x)
- y_offset = ((ty - tiles_y // 2) * tile_size + height // 2 -
- center_tile_pixel_y)
+ x_offset = (
+ (tx - tiles_x // 2) * tile_size + width // 2 -
+ center_tile_pixel_x
+ )
+ y_offset = (
+ (ty - tiles_y // 2) * tile_size + height // 2 -
+ center_tile_pixel_y
+ )
final_image.paste(
tile_img,
(x_offset, y_offset),
@@ -1629,8 +1671,9 @@ async def preview_image(
lat_rad = math.radians(lat_deg)
n = 2.0**zoom_level
x_px = (lon_deg + 180.0) / 360.0 * n * tile_size
- y_px = ((1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n *
- tile_size)
+ y_px = (
+ (1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n * tile_size
+ )
return (x_px, y_px)
draw = ImageDraw.Draw(final_image)
@@ -1644,14 +1687,16 @@ async def preview_image(
dev_lon = float(state.lon)
except Exception:
continue
- if _coords_are_zero(
- dev_lat, dev_lon) or not _within_map_radius(dev_lat, dev_lon):
+ if _coords_are_zero(dev_lat, dev_lon
+ ) or not _within_map_radius(dev_lat, dev_lon):
continue
dev_px_x, dev_px_y = latlon_to_global_px(dev_lat, dev_lon, zoom_val)
img_x = width / 2 + (dev_px_x - center_px_x)
img_y = height / 2 + (dev_px_y - center_px_y)
- if (img_x < -node_radius or img_x > width + node_radius or
- img_y < -node_radius or img_y > height + node_radius):
+ if (
+ img_x < -node_radius or img_x > width + node_radius or
+ img_y < -node_radius or img_y > height + node_radius
+ ):
continue
draw.ellipse(
[
@@ -1674,8 +1719,9 @@ async def preview_image(
"black": (0, 0, 0),
"white": (255, 255, 255),
}
- marker_color = marker_color_map.get(marker_str,
- (0, 123, 255)) # Default to blue
+ marker_color = marker_color_map.get(
+ marker_str, (0, 123, 255)
+ ) # Default to blue
# Calculate marker position (center of image)
marker_x = width // 2
@@ -1748,7 +1794,8 @@ async def preview_image(
img_bytes.seek(0)
print(
- f"[preview] Returning fallback image with marker (tile fetch failed)")
+ f"[preview] Returning fallback image with marker (tile fetch failed)"
+ )
return Response(
content=img_bytes.getvalue(),
media_type="image/png",
@@ -1756,7 +1803,8 @@ async def preview_image(
)
except Exception as fallback_error:
print(
- f"[preview] Fallback image generation also failed: {fallback_error}")
+ f"[preview] Fallback image generation also failed: {fallback_error}"
+ )
# Only redirect to static image if even fallback fails
if SITE_OG_IMAGE and SITE_OG_IMAGE.startswith("http"):
from fastapi.responses import RedirectResponse
@@ -1829,13 +1877,13 @@ def snapshot(request: Request):
_require_prod_token(request)
return {
"devices": {
- k: _device_payload(k, v) for k, v in devices.items()
+ k: _device_payload(k, v)
+ for k, v in devices.items()
},
"trails": trails,
"routes": [_route_payload(r) for r in routes.values()],
- "history_edges": [
- _history_edge_payload(e) for e in route_history_edges.values()
- ],
+ "history_edges":
+ [_history_edge_payload(e) for e in route_history_edges.values()],
"history_window_seconds": int(max(0, ROUTE_HISTORY_HOURS * 3600)),
"heat": _serialize_heat_events(),
"update": git_update_info,
@@ -1847,13 +1895,14 @@ def snapshot(request: Request):
def get_stats():
if PROD_MODE:
return {
- "stats": {
- "received_total": stats.get("received_total"),
- "parsed_total": stats.get("parsed_total"),
- "unparsed_total": stats.get("unparsed_total"),
- "last_rx_ts": stats.get("last_rx_ts"),
- "last_parsed_ts": stats.get("last_parsed_ts"),
- },
+ "stats":
+ {
+ "received_total": stats.get("received_total"),
+ "parsed_total": stats.get("parsed_total"),
+ "unparsed_total": stats.get("unparsed_total"),
+ "last_rx_ts": stats.get("last_rx_ts"),
+ "last_parsed_ts": stats.get("last_parsed_ts"),
+ },
"result_counts": result_counts,
"mapped_devices": len(devices),
"route_count": len(routes),
@@ -1883,19 +1932,21 @@ def get_stats():
sorted(seen_devices.items(), key=lambda kv: kv[1], reverse=True)[:20],
"top_topics":
top_topics,
- "decoder": {
- "decode_with_node": DECODE_WITH_NODE,
- "node_ready": _node_ready_once,
- "node_unavailable": _node_unavailable_once,
- },
+ "decoder":
+ {
+ "decode_with_node": DECODE_WITH_NODE,
+ "node_ready": _node_ready_once,
+ "node_unavailable": _node_unavailable_once,
+ },
"route_payload_types":
sorted(ROUTE_PAYLOAD_TYPES_SET),
- "direct_coords": {
- "mode": DIRECT_COORDS_MODE,
- "topic_regex": DIRECT_COORDS_TOPIC_REGEX,
- "regex_valid": DIRECT_COORDS_TOPIC_RE is not None,
- "allow_zero": DIRECT_COORDS_ALLOW_ZERO,
- },
+ "direct_coords":
+ {
+ "mode": DIRECT_COORDS_MODE,
+ "topic_regex": DIRECT_COORDS_TOPIC_REGEX,
+ "regex_valid": DIRECT_COORDS_TOPIC_RE is not None,
+ "allow_zero": DIRECT_COORDS_ALLOW_ZERO,
+ },
"server_time":
time.time(),
}
@@ -1954,18 +2005,20 @@ def get_peers(device_id: str, request: Request, limit: int = 8):
if state and not _coords_are_zero(state.lat, state.lon):
payload["lat"] = float(state.lat)
payload["lon"] = float(state.lon)
- payload["name"] = ((state.name if state else None) or
- device_names.get(device_id) or "")
+ payload["name"] = (
+ (state.name if state else None) or device_names.get(device_id) or ""
+ )
payload["role"] = (state.role
if state else None) or device_roles.get(device_id)
- payload["last_seen_ts"] = seen_devices.get(device_id) or (state.ts
- if state else None)
+ payload["last_seen_ts"] = seen_devices.get(device_id
+ ) or (state.ts if state else None)
payload["server_time"] = time.time()
return payload
-def _peer_device_payload(peer_id: str, count: int, total: int,
- last_ts: Optional[float]) -> Dict[str, Any]:
+def _peer_device_payload(
+ peer_id: str, count: int, total: int, last_ts: Optional[float]
+) -> Dict[str, Any]:
state = devices.get(peer_id)
name = None
role = None
@@ -2034,14 +2087,14 @@ def _peer_stats_for_device(device_id: str, limit: int) -> Dict[str, Any]:
outbound_total = sum(outbound.values())
inbound_items = [
- _peer_device_payload(peer_id, count, inbound_total,
- inbound_last.get(peer_id))
- for peer_id, count in inbound.items()
+ _peer_device_payload(
+ peer_id, count, inbound_total, inbound_last.get(peer_id)
+ ) for peer_id, count in inbound.items()
]
outbound_items = [
- _peer_device_payload(peer_id, count, outbound_total,
- outbound_last.get(peer_id))
- for peer_id, count in outbound.items()
+ _peer_device_payload(
+ peer_id, count, outbound_total, outbound_last.get(peer_id)
+ ) for peer_id, count in outbound.items()
]
inbound_items.sort(key=lambda item: item.get("count", 0), reverse=True)
outbound_items.sort(key=lambda item: item.get("count", 0), reverse=True)
@@ -2061,11 +2114,9 @@ def _peer_stats_for_device(device_id: str, limit: int) -> Dict[str, Any]:
@app.get("/los")
-def line_of_sight(lat1: float,
- lon1: float,
- lat2: float,
- lon2: float,
- profile: bool = False):
+def line_of_sight(
+ lat1: float, lon1: float, lat2: float, lon2: float, profile: bool = False
+):
include_points = bool(profile)
start = _normalize_lat_lon(lat1, lon1)
end = _normalize_lat_lon(lat2, lon2)
@@ -2091,11 +2142,13 @@ def line_of_sight(lat1: float,
if distance_m > 0:
for (lat, lon, t), elev in zip(points, elevations):
line_elev = start_elev + (end_elev - start_elev) * t
- profile_samples.append([
- round(distance_m * t, 2),
- round(float(elev), 2),
- round(float(line_elev), 2),
- ])
+ profile_samples.append(
+ [
+ round(distance_m * t, 2),
+ round(float(elev), 2),
+ round(float(line_elev), 2),
+ ]
+ )
peaks = _find_los_peaks(points, elevations, distance_m)
response = {
@@ -2106,11 +2159,12 @@ def line_of_sight(lat1: float,
"distance_km": round(distance_m / 1000.0, 3),
"distance_mi": round(distance_m / 1609.344, 3),
"samples": len(points),
- "elevation_m": {
- "start": round(start_elev, 2),
- "end": round(end_elev, 2),
- "max_terrain": round(max_terrain, 2),
- },
+ "elevation_m":
+ {
+ "start": round(start_elev, 2),
+ "end": round(end_elev, 2),
+ "max_terrain": round(max_terrain, 2),
+ },
"provider": LOS_ELEVATION_URL,
"note": "Straight-line LOS using SRTM90m. No curvature/refraction.",
"suggested": suggestion,
@@ -2118,12 +2172,13 @@ def line_of_sight(lat1: float,
"peaks": peaks,
}
if include_points:
- response["profile_points"] = [[
- round(lat, 6),
- round(lon, 6),
- round(t, 4),
- round(float(elev), 2)
- ] for (lat, lon, t), elev in zip(points, elevations)]
+ response["profile_points"] = [
+ [round(lat, 6),
+ round(lon, 6),
+ round(t, 4),
+ round(float(elev), 2)]
+ for (lat, lon, t), elev in zip(points, elevations)
+ ]
return response
@@ -2143,8 +2198,10 @@ async def get_coverage():
response.raise_for_status()
data = response.json()
# /get-samples returns { keys: [...] }, extract the keys array
- samples = (data.get("keys", []) if isinstance(data, dict) else
- (data if isinstance(data, list) else []))
+ samples = (
+ data.get("keys", []) if isinstance(data, dict) else
+ (data if isinstance(data, list) else [])
+ )
print(
f"[coverage] Received {len(samples) if isinstance(samples, list) else 'non-list'} items from coverage API"
)
@@ -2164,8 +2221,9 @@ async def get_coverage():
except httpx.HTTPError as e:
raise HTTPException(status_code=502, detail=f"coverage_api_error: {str(e)}")
except Exception as e:
- raise HTTPException(status_code=500,
- detail=f"coverage_fetch_error: {str(e)}")
+ raise HTTPException(
+ status_code=500, detail=f"coverage_fetch_error: {str(e)}"
+ )
@app.get("/debug/last")
@@ -2200,20 +2258,23 @@ async def ws_endpoint(ws: WebSocket):
clients.add(ws)
await ws.send_text(
- json.dumps({
- "type": "snapshot",
- "devices": {
- k: _device_payload(k, v) for k, v in devices.items()
- },
- "trails": trails,
- "routes": [_route_payload(r) for r in routes.values()],
- "history_edges": [
- _history_edge_payload(e) for e in route_history_edges.values()
- ],
- "history_window_seconds": int(max(0, ROUTE_HISTORY_HOURS * 3600)),
- "heat": _serialize_heat_events(),
- "update": git_update_info,
- }))
+ json.dumps(
+ {
+ "type": "snapshot",
+ "devices": {
+ k: _device_payload(k, v)
+ for k, v in devices.items()
+ },
+ "trails": trails,
+ "routes": [_route_payload(r) for r in routes.values()],
+ "history_edges":
+ [_history_edge_payload(e) for e in route_history_edges.values()],
+ "history_window_seconds": int(max(0, ROUTE_HISTORY_HOURS * 3600)),
+ "heat": _serialize_heat_events(),
+ "update": git_update_info,
+ }
+ )
+ )
try:
while True:
diff --git a/backend/config.py b/backend/config.py
index 40b95ef..2dd2d36 100644
--- a/backend/config.py
+++ b/backend/config.py
@@ -23,8 +23,9 @@ MQTT_CLIENT_ID = os.getenv("MQTT_CLIENT_ID", "")
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_ROLES_FILE = os.getenv(
+ "DEVICE_ROLES_FILE", os.path.join(STATE_DIR, "device_roles.json")
+)
NEIGHBOR_OVERRIDES_FILE = os.getenv(
"NEIGHBOR_OVERRIDES_FILE",
os.path.join(STATE_DIR, "neighbor_overrides.json"),
@@ -42,26 +43,34 @@ ROUTE_HISTORY_ENABLED = os.getenv("ROUTE_HISTORY_ENABLED",
"true").lower() == "true"
ROUTE_HISTORY_HOURS = float(os.getenv("ROUTE_HISTORY_HOURS", "24"))
ROUTE_HISTORY_MAX_SEGMENTS = int(
- os.getenv("ROUTE_HISTORY_MAX_SEGMENTS", "40000"))
-ROUTE_HISTORY_FILE = os.getenv("ROUTE_HISTORY_FILE",
- os.path.join(STATE_DIR, "route_history.jsonl"))
-ROUTE_HISTORY_PAYLOAD_TYPES = os.getenv("ROUTE_HISTORY_PAYLOAD_TYPES",
- ROUTE_PAYLOAD_TYPES)
+ os.getenv("ROUTE_HISTORY_MAX_SEGMENTS", "40000")
+)
+ROUTE_HISTORY_FILE = os.getenv(
+ "ROUTE_HISTORY_FILE", os.path.join(STATE_DIR, "route_history.jsonl")
+)
+ROUTE_HISTORY_PAYLOAD_TYPES = os.getenv(
+ "ROUTE_HISTORY_PAYLOAD_TYPES", ROUTE_PAYLOAD_TYPES
+)
ROUTE_HISTORY_ALLOWED_MODES = os.getenv("ROUTE_HISTORY_ALLOWED_MODES", "path")
ROUTE_HISTORY_COMPACT_INTERVAL = float(
- os.getenv("ROUTE_HISTORY_COMPACT_INTERVAL", "120"))
+ os.getenv("ROUTE_HISTORY_COMPACT_INTERVAL", "120")
+)
HISTORY_EDGE_SAMPLE_LIMIT = 3
MESSAGE_ORIGIN_TTL_SECONDS = int(os.getenv("MESSAGE_ORIGIN_TTL_SECONDS", "300"))
HEAT_TTL_SECONDS = int(os.getenv("HEAT_TTL_SECONDS", "600"))
MQTT_ONLINE_SECONDS = int(os.getenv("MQTT_ONLINE_SECONDS", "300"))
MQTT_SEEN_BROADCAST_MIN_SECONDS = float(
- os.getenv("MQTT_SEEN_BROADCAST_MIN_SECONDS", "5"))
-MQTT_ONLINE_TOPIC_SUFFIXES = tuple(s.strip() for s in os.getenv(
- "MQTT_ONLINE_TOPIC_SUFFIXES", "/status,/internal").split(",") if s.strip())
+ os.getenv("MQTT_SEEN_BROADCAST_MIN_SECONDS", "5")
+)
+MQTT_ONLINE_TOPIC_SUFFIXES = tuple(
+ s.strip()
+ for s in os.getenv("MQTT_ONLINE_TOPIC_SUFFIXES", "/status,/internal"
+ ).split(",") if s.strip()
+)
MQTT_ONLINE_FORCE_NAMES = tuple(
s.strip()
- for s in os.getenv("MQTT_ONLINE_FORCE_NAMES", "").split(",")
- if s.strip())
+ for s in os.getenv("MQTT_ONLINE_FORCE_NAMES", "").split(",") if s.strip()
+)
MQTT_ONLINE_FORCE_NAMES_SET = {s.lower() for s in MQTT_ONLINE_FORCE_NAMES}
DEBUG_PAYLOAD = os.getenv("DEBUG_PAYLOAD", "false").lower() == "true"
@@ -69,18 +78,22 @@ DEBUG_PAYLOAD_MAX = int(os.getenv("DEBUG_PAYLOAD_MAX", "400"))
DECODE_WITH_NODE = os.getenv("DECODE_WITH_NODE", "true").lower() == "true"
NODE_DECODE_TIMEOUT_SECONDS = float(
- os.getenv("NODE_DECODE_TIMEOUT_SECONDS", "2.0"))
+ os.getenv("NODE_DECODE_TIMEOUT_SECONDS", "2.0")
+)
DEBUG_LAST_MAX = int(os.getenv("DEBUG_LAST_MAX", "50"))
DEBUG_STATUS_MAX = int(os.getenv("DEBUG_STATUS_MAX", "50"))
PAYLOAD_PREVIEW_MAX = int(os.getenv("PAYLOAD_PREVIEW_MAX", "800"))
DIRECT_COORDS_MODE = os.getenv("DIRECT_COORDS_MODE", "topic").strip().lower()
-DIRECT_COORDS_TOPIC_REGEX = os.getenv("DIRECT_COORDS_TOPIC_REGEX",
- r"(position|location|gps|coords)")
-DIRECT_COORDS_ALLOW_ZERO = (os.getenv("DIRECT_COORDS_ALLOW_ZERO",
- "false").lower() == "true")
+DIRECT_COORDS_TOPIC_REGEX = os.getenv(
+ "DIRECT_COORDS_TOPIC_REGEX", r"(position|location|gps|coords)"
+)
+DIRECT_COORDS_ALLOW_ZERO = (
+ os.getenv("DIRECT_COORDS_ALLOW_ZERO", "false").lower() == "true"
+)
ROUTE_HISTORY_ALLOWED_MODES_SET = {
- s.strip() for s in ROUTE_HISTORY_ALLOWED_MODES.split(",") if s.strip()
+ s.strip()
+ for s in ROUTE_HISTORY_ALLOWED_MODES.split(",") if s.strip()
}
SITE_TITLE = os.getenv("SITE_TITLE", "Greater Boston Mesh Live Map")
@@ -98,7 +111,8 @@ GIT_CHECK_FETCH = os.getenv("GIT_CHECK_FETCH", "false").lower() == "true"
GIT_CHECK_PATH = os.getenv("GIT_CHECK_PATH", os.getcwd()).strip()
try:
GIT_CHECK_INTERVAL_SECONDS = float(
- os.getenv("GIT_CHECK_INTERVAL_SECONDS", "43200"))
+ os.getenv("GIT_CHECK_INTERVAL_SECONDS", "43200")
+ )
except ValueError:
GIT_CHECK_INTERVAL_SECONDS = 43200.0
DISTANCE_UNITS = os.getenv("DISTANCE_UNITS", "km").strip().lower()
@@ -140,8 +154,9 @@ MAP_RADIUS_SHOW = os.getenv("MAP_RADIUS_SHOW", "false").lower() == "true"
PROD_MODE = os.getenv("PROD_MODE", "false").lower() == "true"
PROD_TOKEN = os.getenv("PROD_TOKEN", "").strip()
-LOS_ELEVATION_URL = os.getenv("LOS_ELEVATION_URL",
- "https://api.opentopodata.org/v1/srtm90m")
+LOS_ELEVATION_URL = os.getenv(
+ "LOS_ELEVATION_URL", "https://api.opentopodata.org/v1/srtm90m"
+)
LOS_SAMPLE_MIN = int(os.getenv("LOS_SAMPLE_MIN", "10"))
LOS_SAMPLE_MAX = int(os.getenv("LOS_SAMPLE_MAX", "80"))
LOS_SAMPLE_STEP_METERS = int(os.getenv("LOS_SAMPLE_STEP_METERS", "250"))
diff --git a/backend/decoder.py b/backend/decoder.py
index ea28c0b..71bbe7d 100644
--- a/backend/decoder.py
+++ b/backend/decoder.py
@@ -286,8 +286,9 @@ def _rebuild_node_hash_map() -> None:
node_hash_to_device.update(mapping)
-def _choose_closest_device(node_hash: str, ref_lat: float, ref_lon: float,
- ts: float) -> Optional[str]:
+def _choose_closest_device(
+ node_hash: str, ref_lat: float, ref_lon: float, ts: float
+) -> Optional[str]:
"""
If we have multiple candidates for a hash, pick the one physically closest
to (ref_lat, ref_lon). If only one, return it.
@@ -304,8 +305,9 @@ def _choose_closest_device(node_hash: str, ref_lat: float, ref_lon: float,
continue
# If infrastructure-only mode is active, only allow repeaters and rooms
- if ROUTE_INFRA_ONLY and (not state.role or
- state.role not in ("repeater", "room")):
+ if ROUTE_INFRA_ONLY and (
+ not state.role or state.role not in ("repeater", "room")
+ ):
continue
# skip invalid coords
@@ -345,8 +347,9 @@ def _choose_device_for_hash(node_hash: str, ts: float) -> Optional[str]:
continue
# If infrastructure-only mode is active, only allow repeaters and rooms
- if ROUTE_INFRA_ONLY and (not state.role or
- state.role not in ("repeater", "room")):
+ if ROUTE_INFRA_ONLY and (
+ not state.role or state.role not in ("repeater", "room")
+ ):
continue
if _coords_are_zero(state.lat, state.lon):
@@ -385,8 +388,9 @@ def _choose_neighbor_device(
continue
# If infrastructure-only mode is active, only allow repeaters and rooms
- if ROUTE_INFRA_ONLY and (not state.role or
- state.role not in ("repeater", "room")):
+ if ROUTE_INFRA_ONLY and (
+ not state.role or state.role not in ("repeater", "room")
+ ):
continue
if _coords_are_zero(state.lat, state.lon):
@@ -428,16 +432,21 @@ def _route_points_from_hashes(
return None, [], []
receiver_hash = _node_hash_from_device_id(
- receiver_id) if receiver_id else None
+ receiver_id
+ ) if receiver_id else None
origin_hash = _node_hash_from_device_id(origin_id) if origin_id else None
if receiver_hash and receiver_hash in normalized:
- if (normalized and normalized[0] == receiver_hash and
- normalized[-1] != receiver_hash):
+ if (
+ normalized and normalized[0] == receiver_hash and
+ normalized[-1] != receiver_hash
+ ):
normalized.reverse()
elif origin_hash and origin_hash in normalized:
- if (normalized and normalized[-1] == origin_hash and
- normalized[0] != origin_hash):
+ if (
+ normalized and normalized[-1] == origin_hash and
+ normalized[0] != origin_hash
+ ):
normalized.reverse()
points: List[List[float]] = []
@@ -452,8 +461,9 @@ def _route_points_from_hashes(
if origin_id:
origin_state = devices.get(origin_id)
- if origin_state and not _coords_are_zero(origin_state.lat,
- origin_state.lon):
+ if origin_state and not _coords_are_zero(
+ origin_state.lat, origin_state.lon
+ ):
try:
current_lat = float(origin_state.lat)
current_lon = float(origin_state.lon)
@@ -526,11 +536,13 @@ def _route_points_from_hashes(
origin_point = None
if origin_id:
origin_state = devices.get(origin_id)
- if origin_state and not _coords_are_zero(origin_state.lat,
- origin_state.lon):
+ if origin_state and not _coords_are_zero(
+ origin_state.lat, origin_state.lon
+ ):
# If infrastructure-only, only add infra nodes
- if ROUTE_INFRA_ONLY and (not origin_state.role or
- origin_state.role not in ("repeater", "room")):
+ if ROUTE_INFRA_ONLY and (
+ not origin_state.role or origin_state.role not in ("repeater", "room")
+ ):
pass # skip
else:
try:
@@ -547,11 +559,14 @@ def _route_points_from_hashes(
receiver_point = None
if receiver_id:
receiver_state = devices.get(receiver_id)
- if receiver_state and not _coords_are_zero(receiver_state.lat,
- receiver_state.lon):
+ if receiver_state and not _coords_are_zero(
+ receiver_state.lat, receiver_state.lon
+ ):
# If infrastructure-only, only add infra nodes
- if ROUTE_INFRA_ONLY and (not receiver_state.role or
- receiver_state.role not in ("repeater", "room")):
+ if ROUTE_INFRA_ONLY and (
+ not receiver_state.role or
+ receiver_state.role not in ("repeater", "room")
+ ):
pass # skip
else:
try:
@@ -574,8 +589,8 @@ def _route_points_from_hashes(
def _route_points_from_device_ids(
- origin_id: Optional[str],
- receiver_id: Optional[str]) -> Optional[List[List[float]]]:
+ origin_id: Optional[str], receiver_id: Optional[str]
+) -> Optional[List[List[float]]]:
if not origin_id or not receiver_id or origin_id == receiver_id:
return None
origin_state = devices.get(origin_id)
@@ -586,12 +601,16 @@ def _route_points_from_device_ids(
# If infrastructure-only mode is active, only allow repeaters and rooms
if ROUTE_INFRA_ONLY and (
(not origin_state.role or origin_state.role not in ("repeater", "room")) or
- (not receiver_state.role or
- receiver_state.role not in ("repeater", "room"))):
+ (
+ not receiver_state.role or
+ receiver_state.role not in ("repeater", "room")
+ )
+ ):
return None
if _coords_are_zero(origin_state.lat, origin_state.lon) or _coords_are_zero(
- receiver_state.lat, receiver_state.lon):
+ receiver_state.lat, receiver_state.lon
+ ):
return None
points = [
[origin_state.lat, origin_state.lon],
@@ -602,29 +621,34 @@ def _route_points_from_device_ids(
return points
-def _append_heat_points(points: List[List[float]], ts: float,
- payload_type: Optional[int]) -> None:
+def _append_heat_points(
+ points: List[List[float]], ts: float, payload_type: Optional[int]
+) -> None:
if HEAT_TTL_SECONDS <= 0:
return
for point in points:
- heat_events.append({
- "lat": float(point[0]),
- "lon": float(point[1]),
- "ts": float(ts),
- "weight": 0.7,
- })
+ heat_events.append(
+ {
+ "lat": float(point[0]),
+ "lon": float(point[1]),
+ "ts": float(ts),
+ "weight": 0.7,
+ }
+ )
def _serialize_heat_events() -> List[List[float]]:
if HEAT_TTL_SECONDS <= 0:
return []
cutoff = time.time() - HEAT_TTL_SECONDS
- return [[
- entry.get("lat"),
- entry.get("lon"),
- entry.get("ts"),
- entry.get("weight", 0.7)
- ] for entry in heat_events if entry.get("ts", 0) >= cutoff]
+ return [
+ [
+ entry.get("lat"),
+ entry.get("lon"),
+ entry.get("ts"),
+ entry.get("weight", 0.7)
+ ] for entry in heat_events if entry.get("ts", 0) >= cutoff
+ ]
def _extract_device_name(obj: Any, topic: str) -> Optional[str]:
@@ -632,15 +656,15 @@ def _extract_device_name(obj: Any, topic: str) -> Optional[str]:
return None
for key in (
- "name",
- "device_name",
- "deviceName",
- "node_name",
- "nodeName",
- "display_name",
- "displayName",
- "callsign",
- "label",
+ "name",
+ "device_name",
+ "deviceName",
+ "node_name",
+ "nodeName",
+ "display_name",
+ "displayName",
+ "callsign",
+ "label",
):
value = obj.get(key)
if isinstance(value, str) and value.strip():
@@ -672,17 +696,17 @@ def _extract_device_role(obj: Any, topic: str) -> Optional[str]:
return None
for key in (
- "role",
- "device_role",
- "deviceRole",
- "node_role",
- "nodeRole",
- "node_type",
- "nodeType",
- "device_type",
- "deviceType",
- "class",
- "profile",
+ "role",
+ "device_role",
+ "deviceRole",
+ "node_role",
+ "nodeRole",
+ "node_type",
+ "nodeType",
+ "device_type",
+ "deviceType",
+ "class",
+ "profile",
):
value = obj.get(key)
if isinstance(value, str):
@@ -693,8 +717,9 @@ def _extract_device_role(obj: Any, topic: str) -> Optional[str]:
return None
-def _apply_meta_role(debug: Dict[str, Any], meta: Optional[Dict[str,
- Any]]) -> None:
+def _apply_meta_role(
+ debug: Dict[str, Any], meta: Optional[Dict[str, Any]]
+) -> None:
if debug.get("device_role"):
return
if not isinstance(meta, dict):
@@ -720,14 +745,14 @@ def _has_location_hints(obj: Any) -> bool:
for k, v in obj.items():
key = str(k).lower()
if key in (
- "location",
- "gps",
- "position",
- "coords",
- "coordinate",
- "geo",
- "geolocation",
- "latlon",
+ "location",
+ "gps",
+ "position",
+ "coords",
+ "coordinate",
+ "geo",
+ "geolocation",
+ "latlon",
):
return True
if isinstance(v, (dict, list)) and _has_location_hints(v):
@@ -915,7 +940,7 @@ try {
def _decode_meshcore_hex(
hex_str: str,
) -> Tuple[Optional[float], Optional[float], Optional[str], Optional[str], Dict[
- str, Any]]:
+ str, Any]]:
if not _ensure_node_decoder():
return (
None,
@@ -941,10 +966,12 @@ def _decode_meshcore_hex(
out = (proc.stdout or "").strip()
if not out:
- return (None, None, None, None, {
- "ok": False,
- "error": "empty_decoder_output"
- })
+ return (
+ None, None, None, None, {
+ "ok": False,
+ "error": "empty_decoder_output"
+ }
+ )
try:
data = json.loads(out)
@@ -1002,8 +1029,9 @@ def _device_id_from_topic(topic: str) -> Optional[str]:
def _find_packet_blob(
- obj: Any,
- path: str = "root") -> Tuple[Optional[str], Optional[str], Optional[str]]:
+ obj: Any,
+ path: str = "root"
+) -> Tuple[Optional[str], Optional[str], Optional[str]]:
if isinstance(obj, str):
if _looks_like_hex(obj):
return (obj.strip(), path, "hex")
@@ -1039,8 +1067,10 @@ def _find_packet_blob(
b64hex = _try_base64_to_hex(v)
if b64hex:
return (b64hex, sub_path, "base64")
- if (isinstance(v, list) and v and
- all(isinstance(x, int) for x in v[:min(20, len(v))])):
+ if (
+ isinstance(v, list) and v and
+ all(isinstance(x, int) for x in v[:min(20, len(v))])
+ ):
try:
raw = bytes(v)
if len(raw) >= 10:
@@ -1055,13 +1085,16 @@ def _find_packet_blob(
return (None, None, None)
-def _extract_device_id(obj: Any, topic: str,
- decoded_pubkey: Optional[str]) -> str:
+def _extract_device_id(
+ obj: Any, topic: str, decoded_pubkey: Optional[str]
+) -> str:
if decoded_pubkey:
return str(decoded_pubkey)
if isinstance(obj, dict):
- device_id = (obj.get("device_id") or obj.get("id") or obj.get("from") or
- obj.get("origin_id"))
+ device_id = (
+ obj.get("device_id") or obj.get("id") or obj.get("from") or
+ obj.get("origin_id")
+ )
if device_id:
return str(device_id)
jwt = obj.get("jwt_payload")
@@ -1071,8 +1104,8 @@ def _extract_device_id(obj: Any, topic: str,
def _try_parse_payload(
- topic: str,
- payload_bytes: bytes) -> Tuple[Optional[Dict[str, Any]], Dict[str, Any]]:
+ topic: str, payload_bytes: bytes
+) -> Tuple[Optional[Dict[str, Any]], Dict[str, Any]]:
debug: Dict[str, Any] = {
"result": "no_coords",
"found_path": None,
@@ -1105,10 +1138,12 @@ def _try_parse_payload(
debug["device_name"] = _extract_device_name(obj, topic)
debug["device_role"] = _extract_device_role(obj, topic)
debug["direction"] = obj.get("direction")
- debug["packet_hash"] = (obj.get("hash") or obj.get("message_hash") or
- obj.get("messageHash"))
- debug["packet_type"] = (obj.get("packet_type") or
- obj.get("packetType") or obj.get("type"))
+ debug["packet_hash"] = (
+ obj.get("hash") or obj.get("message_hash") or obj.get("messageHash")
+ )
+ debug["packet_type"] = (
+ obj.get("packet_type") or obj.get("packetType") or obj.get("type")
+ )
except Exception as exc:
debug["parse_error"] = str(exc)
@@ -1173,7 +1208,8 @@ def _try_parse_payload(
debug["result"] = "direct_blocked"
return (None, debug)
if not DIRECT_COORDS_ALLOW_ZERO and _coords_are_zero(
- got2[0], got2[1]):
+ got2[0], got2[1]
+ ):
debug["result"] = "direct_zero_coords"
return (None, debug)
device_id = _extract_device_id(obj, topic, None)
@@ -1213,8 +1249,9 @@ def _try_parse_payload(
},
debug,
)
- debug["result"] = ("decoded_no_location"
- if meta.get("ok") else "decode_failed")
+ debug["result"] = (
+ "decoded_no_location" if meta.get("ok") else "decode_failed"
+ )
return (None, debug)
debug["result"] = "json_no_packet_blob"
@@ -1261,8 +1298,9 @@ def _try_parse_payload(
},
debug,
)
- debug["result"] = ("decoded_no_location"
- if meta.get("ok") else "decode_failed")
+ debug["result"] = (
+ "decoded_no_location" if meta.get("ok") else "decode_failed"
+ )
return (None, debug)
b64hex = _try_base64_to_hex(text)
@@ -1286,15 +1324,17 @@ def _try_parse_payload(
},
debug,
)
- debug["result"] = ("decoded_no_location"
- if meta.get("ok") else "decode_failed")
+ debug["result"] = (
+ "decoded_no_location" if meta.get("ok") else "decode_failed"
+ )
return (None, debug)
if _is_probably_binary(payload_bytes) and len(payload_bytes) >= 10:
debug["found_path"] = "payload_bytes"
debug["found_hint"] = "raw_bytes"
lat, lon, decoded_pubkey, name, meta = _decode_meshcore_hex(
- payload_bytes.hex())
+ payload_bytes.hex()
+ )
debug["decoded_pubkey"] = decoded_pubkey
debug["decoder_meta"] = meta
_apply_meta_role(debug, meta)
@@ -1312,7 +1352,8 @@ def _try_parse_payload(
debug,
)
debug["result"] = "decoded_no_location" if meta.get(
- "ok") else "decode_failed"
+ "ok"
+ ) else "decode_failed"
return (None, debug)
return (None, debug)
diff --git a/backend/history.py b/backend/history.py
index bbbd052..33b710f 100644
--- a/backend/history.py
+++ b/backend/history.py
@@ -63,9 +63,8 @@ def _normalize_history_point(point: Any) -> Optional[Tuple[float, float]]:
def _history_edge_key(
- a: Tuple[float, float],
- b: Tuple[float,
- float]) -> Tuple[str, Tuple[float, float], Tuple[float, float]]:
+ a: Tuple[float, float], b: Tuple[float, float]
+) -> Tuple[str, Tuple[float, float], Tuple[float, float]]:
if a <= b:
key = f"{a[0]:.6f},{a[1]:.6f}|{b[0]:.6f},{b[1]:.6f}"
return key, a, b
@@ -86,8 +85,9 @@ def _history_sample_from_route(route: Dict[str, Any],
}
-def _update_history_edge_recent(edge: Dict[str, Any],
- sample: Dict[str, Any]) -> None:
+def _update_history_edge_recent(
+ edge: Dict[str, Any], sample: Dict[str, Any]
+) -> None:
if not edge or not sample:
return
recent = edge.get("recent")
@@ -101,7 +101,8 @@ def _update_history_edge_recent(edge: Dict[str, Any],
def _record_route_history(
- route: Dict[str, Any]) -> Tuple[List[Dict[str, Any]], List[str]]:
+ route: Dict[str, Any]
+) -> Tuple[List[Dict[str, Any]], List[str]]:
if not ROUTE_HISTORY_ENABLED:
return [], []
if ROUTE_HISTORY_ALLOWED_MODES_SET:
@@ -112,8 +113,8 @@ def _record_route_history(
if not _history_payload_allowed(payload_type):
return [], []
points = route.get("points")
- point_ids = route.get("point_ids") if isinstance(route.get("point_ids"),
- list) else None
+ point_ids = route.get("point_ids"
+ ) if isinstance(route.get("point_ids"), list) else None
if not isinstance(points, list) or len(points) < 2:
return [], []
@@ -133,19 +134,21 @@ def _record_route_history(
a_id = point_ids[idx]
b_id = point_ids[idx + 1]
key, first, second = _history_edge_key(a, b)
- new_entries.append({
- "ts": float(ts),
- "a": [first[0], first[1]],
- "b": [second[0], second[1]],
- "a_id": a_id,
- "b_id": b_id,
- "message_hash": sample.get("message_hash"),
- "payload_type": sample.get("payload_type"),
- "origin_id": sample.get("origin_id"),
- "receiver_id": sample.get("receiver_id"),
- "route_mode": sample.get("route_mode"),
- "topic": sample.get("topic"),
- })
+ new_entries.append(
+ {
+ "ts": float(ts),
+ "a": [first[0], first[1]],
+ "b": [second[0], second[1]],
+ "a_id": a_id,
+ "b_id": b_id,
+ "message_hash": sample.get("message_hash"),
+ "payload_type": sample.get("payload_type"),
+ "origin_id": sample.get("origin_id"),
+ "receiver_id": sample.get("receiver_id"),
+ "route_mode": sample.get("route_mode"),
+ "topic": sample.get("topic"),
+ }
+ )
edge = state.route_history_edges.get(key)
if not edge:
edge = {
@@ -169,12 +172,12 @@ def _record_route_history(
updates = [
state.route_history_edges[key]
- for key in updated_keys
- if key in state.route_history_edges
+ for key in updated_keys if key in state.route_history_edges
]
removed: List[str] = []
if ROUTE_HISTORY_MAX_SEGMENTS > 0 and len(
- state.route_history_segments) > ROUTE_HISTORY_MAX_SEGMENTS:
+ state.route_history_segments
+ ) > ROUTE_HISTORY_MAX_SEGMENTS:
extra_updates, extra_removed = _prune_route_history(force_limit=True)
updates.extend(extra_updates)
removed.extend(extra_removed)
@@ -183,7 +186,8 @@ def _record_route_history(
def _prune_route_history(
- force_limit: bool = False) -> Tuple[List[Dict[str, Any]], List[str]]:
+ force_limit: bool = False
+) -> Tuple[List[Dict[str, Any]], List[str]]:
if not ROUTE_HISTORY_ENABLED or not state.route_history_segments:
return [], []
@@ -204,7 +208,8 @@ def _prune_route_history(
if not force_limit and ts >= cutoff:
break
if force_limit and ROUTE_HISTORY_MAX_SEGMENTS > 0 and len(
- state.route_history_segments) <= ROUTE_HISTORY_MAX_SEGMENTS:
+ state.route_history_segments
+ ) <= ROUTE_HISTORY_MAX_SEGMENTS:
break
state.route_history_segments.popleft()
a = entry.get("a")
@@ -291,19 +296,21 @@ def _load_route_history() -> None:
"topic": entry.get("topic"),
}
key, first, second = _history_edge_key(a_point, b_point)
- state.route_history_segments.append({
- "ts": float(ts),
- "a": [first[0], first[1]],
- "b": [second[0], second[1]],
- "a_id": entry.get("a_id"),
- "b_id": entry.get("b_id"),
- "message_hash": sample.get("message_hash"),
- "payload_type": sample.get("payload_type"),
- "origin_id": sample.get("origin_id"),
- "receiver_id": sample.get("receiver_id"),
- "route_mode": sample.get("route_mode"),
- "topic": sample.get("topic"),
- })
+ state.route_history_segments.append(
+ {
+ "ts": float(ts),
+ "a": [first[0], first[1]],
+ "b": [second[0], second[1]],
+ "a_id": entry.get("a_id"),
+ "b_id": entry.get("b_id"),
+ "message_hash": sample.get("message_hash"),
+ "payload_type": sample.get("payload_type"),
+ "origin_id": sample.get("origin_id"),
+ "receiver_id": sample.get("receiver_id"),
+ "route_mode": sample.get("route_mode"),
+ "topic": sample.get("topic"),
+ }
+ )
edge = state.route_history_edges.get(key)
if not edge:
edge = {
@@ -326,7 +333,8 @@ def _load_route_history() -> None:
return
if ROUTE_HISTORY_MAX_SEGMENTS > 0 and len(
- state.route_history_segments) > ROUTE_HISTORY_MAX_SEGMENTS:
+ state.route_history_segments
+ ) > ROUTE_HISTORY_MAX_SEGMENTS:
_prune_route_history(force_limit=True)
state.route_history_compact = True
diff --git a/backend/los.py b/backend/los.py
index 6d5b3a7..54db278 100644
--- a/backend/los.py
+++ b/backend/los.py
@@ -22,8 +22,8 @@ def _haversine_m(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
phi2 = math.radians(lat2)
dphi = math.radians(lat2 - lat1)
dlambda = math.radians(lon2 - lon1)
- a = math.sin(
- dphi / 2)**2 + math.cos(phi1) * math.cos(phi2) * math.sin(dlambda / 2)**2
+ a = math.sin(dphi / 2
+ )**2 + math.cos(phi1) * math.cos(phi2) * math.sin(dlambda / 2)**2
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
return radius * c
@@ -105,9 +105,10 @@ def _sample_los_points(lat1: float, lon1: float, lat2: float,
return points
-def _los_max_obstruction(points: List[Tuple[float, float,
- float]], elevations: List[float],
- start_idx: int, end_idx: int) -> float:
+def _los_max_obstruction(
+ points: List[Tuple[float, float, float]], elevations: List[float],
+ start_idx: int, end_idx: int
+) -> float:
if end_idx <= start_idx + 1:
return 0.0
start_t = points[start_idx][2]
@@ -126,8 +127,9 @@ def _los_max_obstruction(points: List[Tuple[float, float,
return max_obstruction
-def _find_los_suggestion(points: List[Tuple[float, float, float]],
- elevations: List[float]) -> Optional[Dict[str, Any]]:
+def _find_los_suggestion(
+ points: List[Tuple[float, float, float]], elevations: List[float]
+) -> Optional[Dict[str, Any]]:
if len(points) < 3:
return None
best_idx = None
@@ -189,18 +191,21 @@ def _find_los_peaks(
except ValueError:
return []
- peak_indices = sorted(peak_indices, key=lambda i: elevations[i],
- reverse=True)[:LOS_PEAKS_MAX]
+ peak_indices = sorted(
+ peak_indices, key=lambda i: elevations[i], reverse=True
+ )[:LOS_PEAKS_MAX]
peak_indices = sorted(peak_indices, key=lambda i: points[i][2])
peaks = []
for i, idx in enumerate(peak_indices, start=1):
t = points[idx][2]
- peaks.append({
- "index": i,
- "lat": round(points[idx][0], 6),
- "lon": round(points[idx][1], 6),
- "elevation_m": round(float(elevations[idx]), 2),
- "distance_m": round(distance_m * t, 2),
- })
+ peaks.append(
+ {
+ "index": i,
+ "lat": round(points[idx][0], 6),
+ "lon": round(points[idx][1], 6),
+ "elevation_m": round(float(elevations[idx]), 2),
+ "distance_m": round(distance_m * t, 2),
+ }
+ )
return peaks