mirror of
https://github.com/pe1hvh/meshcore-gui.git
synced 2026-07-04 08:50:59 +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:
@@ -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
|
||||
|
||||
+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, "
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -25,7 +25,7 @@ from typing import Any, Dict, List
|
||||
# ==============================================================================
|
||||
|
||||
|
||||
VERSION: str = "1.17.0"
|
||||
VERSION: str = "1.17.1"
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
|
||||
@@ -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*).
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user