Files
meshcore-gui/meshcore_gui/api/routes.py
T
pe1hvh 8080bcc203 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.
2026-04-04 22:59:44 +02:00

127 lines
4.2 KiB
Python

"""
Public REST API route definitions for MeshCore GUI.
Registers four read-only GET endpoints under ``/api/v1/`` on the
NiceGUI/FastAPI application instance:
GET /api/v1/stats
GET /api/v1/nodes
GET /api/v1/messages
GET /api/v1/channels
Call :func:`register_routes` once from ``__main__.py`` after
:class:`~meshcore_gui.core.shared_data.SharedData` is constructed and
before ``ui.run()`` is called.
All routes are async and access shared data read-only. CORS is
handled via response headers on each endpoint to avoid conflicts with
NiceGUI's frozen middleware stack.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Dict, List
from fastapi import Query
from fastapi.responses import JSONResponse
from nicegui import app as _nicegui_app
import meshcore_gui.config as config
from meshcore_gui.services.public_api_service import (
get_channels_payload,
get_messages_payload,
get_nodes_payload,
get_stats_payload,
)
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.
Must be called after :class:`~meshcore_gui.core.shared_data.SharedData`
is constructed and **before** ``ui.run()`` so that FastAPI registers
the routes on startup.
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.
"""
# ── Routes ──────────────────────────────────────────────────────────
@_nicegui_app.get(
"/api/v1/stats",
tags=["MeshCore Public API"],
summary="Network statistics for the last 72 hours",
response_class=JSONResponse,
)
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.
"""
return _cors_response(get_stats_payload(shared))
@_nicegui_app.get(
"/api/v1/nodes",
tags=["MeshCore Public API"],
summary="All known mesh nodes",
response_class=JSONResponse,
)
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_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"),
) -> JSONResponse:
"""Return paginated messages from public and hashtag channels only.
Private channel messages are **never** returned.
"""
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_class=JSONResponse,
)
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, "
"/api/v1/messages, /api/v1/channels"
)