mirror of
https://github.com/pe1hvh/meshcore-gui.git
synced 2026-08-12 03:42:43 +02:00
fix: Multibyte path hash support
### FIXED - **Multibyte path hash support** (`ble/events.py`, `core/shared_data.py`): corrected docstrings in both `_resolve_path_names` methods that incorrectly described path hashes as "2-char hex strings". The actual contact lookup uses `startswith` matching, which is hash-size agnostic and correctly handles 1-byte (2 hex chars), 2-byte (4 hex chars) and 3-byte (6 hex chars) path hashes as introduced in MeshCore firmware v1.14.0. No functional code was changed — only the documentation was incorrect. - **MariaDB schema** (`meshcore_schema.sql`): `meshcore_messages.path_hashes` column widened from `VARCHAR(128)` to `VARCHAR(255)`. The old limit caused silent truncation for paths longer than ~40 hops in 1-byte mode or ~25 hops in 2-byte mode. Migration is backward-compatible; existing data is unchanged. ### CHANGED - `config.py`: version bump `1.17.0 → 1.17.1`. ### RATIONALE - MeshCore firmware v1.14.0 (2026-03-06) introduced configurable path hash sizes (1-, 2- or 3-byte per repeater). Verification confirmed that `meshcoredecoder 0.3.2` already returns correctly sized hex strings via `_decode_path_len_byte`. The GUI path-resolution logic was already forward-compatible; only the docstrings and the MariaDB column width required correction. ### IMPACT - No BLE handler, GUI panel, service or API endpoint modified. - `meshcoredecoder` library unchanged; no pip update required. - MariaDB migration: single `ALTER TABLE` statement, no downtime, no data loss.
This commit is contained in:
+36
-62
@@ -14,7 +14,8 @@ Call :func:`register_routes` once from ``__main__.py`` after
|
||||
before ``ui.run()`` is called.
|
||||
|
||||
All routes are async and access shared data read-only. CORS is
|
||||
configured from :data:`~meshcore_gui.config.API_CORS_ORIGINS`.
|
||||
handled via response headers on each endpoint to avoid conflicts with
|
||||
NiceGUI's frozen middleware stack.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -22,7 +23,7 @@ from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Any, Dict, List
|
||||
|
||||
from fastapi import Query
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from nicegui import app as _nicegui_app
|
||||
|
||||
import meshcore_gui.config as config
|
||||
@@ -37,6 +38,22 @@ if TYPE_CHECKING:
|
||||
from meshcore_gui.core.shared_data import SharedData
|
||||
|
||||
|
||||
def _cors_response(data: Any) -> JSONResponse:
|
||||
"""Wrap API data in a JSONResponse with CORS headers.
|
||||
|
||||
Using response-level CORS headers avoids touching the NiceGUI
|
||||
middleware stack (which is frozen by the time routes are registered).
|
||||
"""
|
||||
origins = ", ".join(config.API_CORS_ORIGINS)
|
||||
return JSONResponse(
|
||||
content=data,
|
||||
headers={
|
||||
"Access-Control-Allow-Origin": origins,
|
||||
"Access-Control-Allow-Methods": "GET",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def register_routes(shared: "SharedData") -> None:
|
||||
"""Wire public API routes into the NiceGUI/FastAPI application.
|
||||
|
||||
@@ -44,107 +61,64 @@ def register_routes(shared: "SharedData") -> None:
|
||||
is constructed and **before** ``ui.run()`` so that FastAPI registers
|
||||
the routes on startup.
|
||||
|
||||
CORS middleware is added once using the origins configured in
|
||||
:data:`~meshcore_gui.config.API_CORS_ORIGINS`. The middleware is
|
||||
idempotent — calling this function more than once is safe (NiceGUI
|
||||
guards against duplicate middleware).
|
||||
CORS is handled via response headers on each endpoint rather than
|
||||
middleware, which avoids conflicts with NiceGUI's frozen middleware stack.
|
||||
|
||||
Args:
|
||||
shared: Application shared-data instance. Passed to service
|
||||
functions as a read-only data source.
|
||||
"""
|
||||
# ── CORS ────────────────────────────────────────────────────────────
|
||||
_nicegui_app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=config.API_CORS_ORIGINS,
|
||||
allow_methods=["GET"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# ── Routes ──────────────────────────────────────────────────────────
|
||||
|
||||
@_nicegui_app.get(
|
||||
"/api/v1/stats",
|
||||
tags=["MeshCore Public API"],
|
||||
summary="Network statistics for the last 72 hours",
|
||||
response_model=None,
|
||||
response_class=JSONResponse,
|
||||
)
|
||||
async def api_stats() -> Dict[str, Any]:
|
||||
async def api_stats() -> JSONResponse:
|
||||
"""Return aggregate statistics for the last 72 hours.
|
||||
|
||||
Only public (index 0) and hashtag channels are included in message
|
||||
counts. Node counts reflect the live contact list.
|
||||
|
||||
Returns:
|
||||
JSON object with ``generated_at``, ``period_hours``,
|
||||
``total_messages``, ``unique_senders``, ``active_clients``,
|
||||
``active_repeaters``, ``active_room_servers``, ``avg_hops``
|
||||
and ``peak_hour``.
|
||||
"""
|
||||
return get_stats_payload(shared)
|
||||
return _cors_response(get_stats_payload(shared))
|
||||
|
||||
@_nicegui_app.get(
|
||||
"/api/v1/nodes",
|
||||
tags=["MeshCore Public API"],
|
||||
summary="All known mesh nodes",
|
||||
response_model=None,
|
||||
response_class=JSONResponse,
|
||||
)
|
||||
async def api_nodes() -> List[Dict[str, Any]]:
|
||||
"""Return all contacts from the live contact list.
|
||||
|
||||
Fields not tracked by the current firmware interface (``last_seen``,
|
||||
``battery_mv``) are returned as ``null``.
|
||||
|
||||
Returns:
|
||||
JSON array of node objects with ``name``, ``pubkey_prefix``,
|
||||
``type``, ``last_seen``, ``adv_lat``, ``adv_lon`` and
|
||||
``battery_mv``.
|
||||
"""
|
||||
return get_nodes_payload(shared)
|
||||
async def api_nodes() -> JSONResponse:
|
||||
"""Return all contacts from the live contact list."""
|
||||
return _cors_response(get_nodes_payload(shared))
|
||||
|
||||
@_nicegui_app.get(
|
||||
"/api/v1/messages",
|
||||
tags=["MeshCore Public API"],
|
||||
summary="Paginated public and hashtag channel messages",
|
||||
response_model=None,
|
||||
response_class=JSONResponse,
|
||||
)
|
||||
async def api_messages(
|
||||
limit: int = Query(default=100, ge=1, le=500, description="Maximum items to return"),
|
||||
offset: int = Query(default=0, ge=0, description="Items to skip"),
|
||||
) -> Dict[str, Any]:
|
||||
) -> JSONResponse:
|
||||
"""Return paginated messages from public and hashtag channels only.
|
||||
|
||||
Private channel messages are **never** returned, regardless of
|
||||
authentication. The filtering is enforced server-side.
|
||||
|
||||
Args:
|
||||
limit: Number of messages to return (1–500, default 100).
|
||||
offset: Number of messages to skip for pagination (default 0).
|
||||
|
||||
Returns:
|
||||
JSON object with ``total``, ``limit``, ``offset`` and ``items``
|
||||
(list of message objects).
|
||||
Private channel messages are **never** returned.
|
||||
"""
|
||||
return get_messages_payload(shared, limit=limit, offset=offset)
|
||||
return _cors_response(get_messages_payload(shared, limit=limit, offset=offset))
|
||||
|
||||
@_nicegui_app.get(
|
||||
"/api/v1/channels",
|
||||
tags=["MeshCore Public API"],
|
||||
summary="Channel list with privacy flag",
|
||||
response_model=None,
|
||||
response_class=JSONResponse,
|
||||
)
|
||||
async def api_channels() -> List[Dict[str, Any]]:
|
||||
"""Return all channels discovered from the device.
|
||||
|
||||
Each entry includes an ``is_private`` flag. Private channels appear
|
||||
in this list (so callers know they exist) but no keys or message
|
||||
content is exposed.
|
||||
|
||||
Returns:
|
||||
JSON array of channel objects with ``idx``, ``name`` and
|
||||
``is_private``.
|
||||
"""
|
||||
return get_channels_payload(shared)
|
||||
async def api_channels() -> JSONResponse:
|
||||
"""Return all channels discovered from the device."""
|
||||
return _cors_response(get_channels_payload(shared))
|
||||
|
||||
config.debug_print(
|
||||
"Public API registered: /api/v1/stats, /api/v1/nodes, "
|
||||
|
||||
Reference in New Issue
Block a user