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:
pe1hvh
2026-04-04 22:59:44 +02:00
parent 1275f78e36
commit 8080bcc203
7 changed files with 83 additions and 69 deletions
+33
View File
@@ -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
View File
@@ -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 (1500, 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, "
+7 -4
View File
@@ -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, 26 chars each (13 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:
+1 -1
View File
@@ -25,7 +25,7 @@ from typing import Any, Dict, List
# ==============================================================================
VERSION: str = "1.17.0"
VERSION: str = "1.17.1"
# ==============================================================================
+6 -2
View File
@@ -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, 26 chars each (13 bytes).
Returns:
List of display names (same length as *path_hashes*).
Binary file not shown.