diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e1c1e8..b4192f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,39 @@ Format follows [Keep a Changelog](https://keepachangelog.com/) and [Semantic Ver --- +## [1.17.1] - 2026-04-04 + +### 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. + +--- + ## [1.17.0] - 2026-04-04 ### ADDED diff --git a/meshcore_gui/api/routes.py b/meshcore_gui/api/routes.py index b9a4034..2e3b985 100644 --- a/meshcore_gui/api/routes.py +++ b/meshcore_gui/api/routes.py @@ -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, " diff --git a/meshcore_gui/ble/events.py b/meshcore_gui/ble/events.py index af1168a..dcb9a32 100644 --- a/meshcore_gui/ble/events.py +++ b/meshcore_gui/ble/events.py @@ -65,13 +65,17 @@ class EventHandler: # ------------------------------------------------------------------ def _resolve_path_names(self, path_hashes: list) -> list: - """Resolve 2-char path hashes to display names. + """Resolve path hashes to display names. Performs a contact lookup for each hash *now* so the names are captured at receive time and stored in the archive. + Supports 1-byte (2 hex chars), 2-byte (4 hex chars) and + 3-byte (6 hex chars) path hashes as introduced in firmware v1.14. + Contact lookup uses ``startswith`` matching and is hash-size agnostic. + Args: - path_hashes: List of 2-char hex strings. + path_hashes: List of hex strings, 2–6 chars each (1–3 bytes). Returns: List of display names (same length as *path_hashes*). @@ -83,8 +87,7 @@ class EventHandler: names.append('-') continue name = self._shared.get_contact_name_by_prefix(h) - # get_contact_name_by_prefix returns h[:8] as fallback, - # normalise to uppercase hex for 2-char hashes. + # startswith matching is hash-size agnostic (2/4/6-char hashes). if name and name != h[:8]: names.append(name) else: diff --git a/meshcore_gui/config.py b/meshcore_gui/config.py index 6a126b0..8ab28de 100644 --- a/meshcore_gui/config.py +++ b/meshcore_gui/config.py @@ -25,7 +25,7 @@ from typing import Any, Dict, List # ============================================================================== -VERSION: str = "1.17.0" +VERSION: str = "1.17.1" # ============================================================================== diff --git a/meshcore_gui/core/shared_data.py b/meshcore_gui/core/shared_data.py index c69cbc3..29e2b77 100644 --- a/meshcore_gui/core/shared_data.py +++ b/meshcore_gui/core/shared_data.py @@ -394,15 +394,19 @@ class SharedData: return f'Ch {channel_idx}' def _resolve_path_names(self, path_hashes: list) -> list: - """Resolve 2-char path hashes to display names. + """Resolve path hashes to display names. MUST be called with self.lock held. Safety-net for messages whose path_names were not resolved at receive time (e.g. older code path, or contacts not yet loaded). + Supports 1-byte (2 hex chars), 2-byte (4 hex chars) and + 3-byte (6 hex chars) path hashes as introduced in firmware v1.14. + Contact lookup uses ``startswith`` matching and is hash-size agnostic. + Args: - path_hashes: List of 2-char hex strings. + path_hashes: List of hex strings, 2–6 chars each (1–3 bytes). Returns: List of display names (same length as *path_hashes*). diff --git a/meshcore_guiAddChannel.zip b/meshcore_guiAddChannel.zip deleted file mode 100644 index f474271..0000000 Binary files a/meshcore_guiAddChannel.zip and /dev/null differ diff --git a/meshcore_gui_Iteratie_A_result.zip b/meshcore_guiPAth.zip similarity index 62% rename from meshcore_gui_Iteratie_A_result.zip rename to meshcore_guiPAth.zip index 2aecad9..b5d8c64 100644 Binary files a/meshcore_gui_Iteratie_A_result.zip and b/meshcore_guiPAth.zip differ