- Updated `config.yaml.example` to include optional KISS key-up and CSMA tuning parameters.
- Enhanced `get_radio_for_board` function in `config.py` to forward KISS tuning settings to the modem firmware when specified.
- Added tests to verify correct forwarding of KISS parameters and omission of unset values in `test_radio_config.py`.
- Updated the logic for retrieving the sqlite_handler in APIEndpoints to use a safer approach with getattr, ensuring compatibility with the daemon_instance.
- Adjusted test case to include an additional parameter in the sqlite.companion_push_message assertion for consistency.
- Introduced `enforce_companion_contact_capacity` to manage contact limits during companion loading, with an option to trim non-favourite contacts when exceeding capacity.
- Updated `SQLiteHandler` to support message retention limits, allowing for automatic trimming of older messages based on `offline_queue_size`.
- Enhanced API endpoints to handle contact trimming on overflow, providing feedback on trimmed contacts during updates.
- Added utility functions for selecting and trimming contacts while preserving favourites.
- Improved logging for contact management actions and errors related to capacity.
- Updated LogBuffer to support log entry IDs, enhanced log entry structure with additional metadata, and implemented subscriber management for real-time log streaming.
- Added OpenAPI specifications for new endpoints related to policy management, including retrieval, updating, validation, and group management for network policies.
- Implemented comprehensive tests for new policy endpoints, ensuring correct behavior for creating, updating, validating, and deleting policy groups and entries.
- Introduced policy evaluation tests to validate the functionality of the PolicyEngine, including various scenarios for action decisions based on defined rules.
- Enhanced packet routing tests to ensure proper handling of policy decisions in packet processing.
Integrate latest dev while preserving per-companion bridge settings
and contact capacity validation. Resolve import conflicts in main.py
and api_endpoints.py.
Co-authored-by: Cursor <cursoragent@cursor.com>
- Introduced `CompanionContactCapacityError` to handle cases where persisted contacts exceed configured limits.
- Added utility functions for parsing companion bridge settings and validating contact capacity.
- Updated `RepeaterDaemon` to check contact capacity during companion loading and initialization.
- Enhanced API endpoints to validate companion settings and manage contact limits effectively.
- Implemented logging for bridge limits and errors related to contact capacity.
Push locally-injected TX packets to connected companion frame server
clients as PUSH_CODE_LOG_RX_DATA (0x88) with snr=0/rssi=0, so apps that
decrypt locally from raw RX (e.g. RemoteTerm) see companion-originated
channel traffic. The originating companion is excluded so a node never
hears its own transmission, matching physical firmware behavior.
inject_packet now takes an origin_hash (threaded per-companion via the
packet_injector partial); _on_raw_rx_for_companions gains exclude_hash
to skip that companion's frame server. OTA RX is unaffected.
The merged RepeaterHandler treats a falsy tx_success as failure; update
the duty-cycle deferral test to return True instead of None.
Co-authored-by: Cursor <cursoragent@cursor.com>
- Added a default upper bound for randomized pre-send jitter in discovery responses to avoid collisions when multiple repeaters respond simultaneously.
- Introduced a new parameter `response_jitter_ms` in the `DiscoveryHelper` constructor to configure the jitter.
- Updated the `_send_packet_async` method to apply the jitter before sending responses.
- Added tests to verify the correct application of jitter and ensure functionality when jitter is disabled.
- Updated the `LoginHelper` class to wrap the login handler in an `AnonRequestHandler`, allowing for proper handling of anonymous requests.
- Introduced methods for formatting region names based on flood policies and added support for retrieving transport keys from SQLite storage.
- Enhanced the constructor to accept additional parameters for SQLite handler and configuration, improving flexibility for owner-info and feature-flag replies.
- Added tests to validate the new functionality and ensure correct behavior of region name formatting and owner/features callbacks.
- Introduced `_prefs_bytes_from_json` to convert JSON hex strings back to bytes for NodePrefs fields.
- Updated `_load_prefs` logic to handle bytes conversion when loading preferences.
- Improved the handling of preferences in the `RepeaterCompanionBridge` class to ensure proper type restoration.
- Updated byte representations in tests to use lowercase hex format for consistency.
- Reformatted code for better readability, including line breaks and indentation adjustments.
- Consolidated multiple lines into single lines where appropriate to enhance clarity.
- Ensured that all test cases maintain consistent formatting and style across the test suite.
Also extend the compat scanner to catch datetime.UTC used as an
attribute (datetime.datetime.now(datetime.UTC)) in addition to
direct imports, so this form cannot be reintroduced undetected.
Co-Authored-By: Zindello <josh@zindello.com.au>
Replace the try/except UTC shim in mqtt_handler.py with timezone.utc
directly, consistent with the api_endpoints.py fix. Add a static AST
scan that fails if datetime.UTC is reintroduced anywhere in the codebase,
guarding against future regressions on Python 3.10 (LuckFox Pico Ultra).
Co-Authored-By: Zindello <josh@zindello.com.au>
- Introduced tests for TraceHelper and DiscoveryHelper to validate packet forwarding and discovery request handling.
- Implemented tests for LoginHelper to ensure identity registration and login packet processing.
- Added tests for IdentityManager to cover identity registration, lookup, and filtering.
- Created tests for MeshCLI to verify command handling, configuration setting, and error paths.
Lets a fresh repeater install pick the pymc_usb (USB-CDC) or pymc_tcp
(Wi-Fi/Ethernet) external modem from the first-run /setup wizard
instead of requiring the user to hand-edit config.yaml after install.
radio-settings.json gets two new hardware entries; setup_wizard()
in api_endpoints.py handles them in dedicated branches that mirror
the existing KISS pattern (placeholders if the SPA doesn't yet send
modem-specific inputs, request body overrides if it does).
For pymc_tcp the wizard writes a sentinel host placeholder
('REPLACE_WITH_MODEM_HOST') so the YAML stays valid; on startup
get_radio_for_board() then errors with a clear pointer at
pymc_tcp.host (existing behavior from the PR #240 branch). pymc_usb
defaults to /dev/ttyACM0 at 921600 baud — matches the USB-CDC
device path documented in pymc_usb's README + pymc_driver.
Five new tests in tests/test_setup_wizard_pymc.py verify both
default and overridden code paths plus a KISS regression guard.
- Updated the __call__ method to return a boolean indicating transmission success.
- Adjusted handling of transmission failures to increment dropped_count appropriately.
- Enhanced logging for transmission failures in PacketRouter to ensure visibility of issues.
- Added tests to verify correct behavior during transmission exceptions and failures.
Adds a new read-only endpoint that serves the bundled `repeater/presets/*.yaml`
catalogue so the admin UI can render a network picker without bundling its own
copy of the broker dicts. The UI side of this is paired with
pyMC-dev/pyMC-RepeaterUI#TBD which retires src/assets/broker-templates.json
in favour of authClient.get('/api/broker_presets').
Why
The UI previously shipped a separate JSON snapshot of every supported MC2MQTT
network. The JSON and these YAML files drifted: the Waev entry on the UI side
pointed at mqtt-a.waev.app with audience mqtt.waev.app (single primary, no
failover) while the YAML side here listed two brokers (A + B). The result was
that operators picking 'Waev' from the dropdown silently lost the redundancy
this preset is meant to provide.
What changes
repeater/presets/*.yaml
- Add optional top-level `display_name` and `website` fields. The loader
treats them as advisory metadata for the UI; the runtime connection code
never reads them. `display_name` falls back to the titlecased filename
stem if absent so existing third-party presets keep rendering.
repeater/presets/waev.yaml
- Collapse from two broker entries (waev-a, waev-b) to a single broker on
`mqtt.waev.app`. The Waev edge Worker (see waev/src/router.ts:
MQTT_PRIMARY_FAILOVER_TIMEOUT_MS) already does server-side A/B failover on
the alias host with a 1500 ms timeout. Two independent client connections
would defeat the dedup-on-pubkey-hash contract on the waev ingest side.
Operators who want to pin to a specific container can edit host/audience
after import.
repeater/presets/meshmapper.yaml (new)
- Port of the historical MeshMapper entry from the UI's deprecated JSON.
Single broker on mqtt.meshmapper.cc, format: letsmesh (matches the
published wire contract; bump to a dedicated value if/when wire-level
differentiation lands).
repeater/web/api_endpoints.py
- New `broker_presets` CherryPy handler at `GET /api/broker_presets`.
Unauthenticated to match the existing `mqtt_status` precedent — the
response carries only public hostnames + TLS hints, no PII. Imports the
presets module lazily so a broken YAML never blocks process startup.
Response shape:
{
success: true,
data: [{ id, name, website?, brokers: [ ... raw YAML dicts ... ] }, …]
}
tests/test_presets.py
- Locks the new metadata fields (display_name, website) on all three presets.
- Locks the Waev single-alias-broker design with an explicit comment tying
the test to the waev Worker failover code.
- Adds MeshMapper coverage parallel to the other public-network presets.
- Adds a stub-instance test that drives the new `broker_presets` method on
an APIEndpoints stand-in (bypassing the heavyweight `__init__`) and
asserts the UI-ready response shape.
Verification
- New endpoint serves the expected three presets (letsmesh: 2 brokers,
meshmapper: 1, waev: 1) when exercised end-to-end against a local mock
that imports the real preset loader.
- Existing legacy-config migration tests (broker_index 0/1/-1 → preset +
overrides) still pass — the override pipeline is untouched.
Co-Authored-By: Oz <oz-agent@warp.dev>
Wires the TCPLoRaRadio and USBLoRaRadio drivers that landed in pyMC_core
on 2026-05-13 (PR pyMC-dev/pyMC_core#68) into get_radio_for_board() so
they can be selected from a repeater config file without any code change
in main.py / api_endpoints.
Both branches follow the existing pattern: read host/port (TCP) or
serial port (USB) plus auth/LBT options from their own config section,
share the LoRa parameters from the common `radio` section, fall back to
the firmware-default sync word 0x12, and surface ImportError as a clear
RuntimeError if the installed pymc_core is too old to ship the drivers.
config.yaml.example documents both sections and updates the radio_type
header comment with the full supported list. Five new tests in
tests/test_radio_config.py monkeypatch the radio classes and verify the
section/parameter wiring + missing-required-field errors.
No web UI / endpoint changes — the deployment this targets edits the
config file directly. A GUI wizard for these radio types can land
separately if there's appetite.
Every MQTT-published packet has shipped with duration="0" since the
PacketRecord factory was introduced. The repeater already computes LoRa
time-on-air via AirtimeManager.calculate_airtime() (the canonical
Semtech reference formula) for duty-cycle gating and TX delay, but the
result was thrown away after each packet - never stored on the
packet_record dict that flows to MQTT/SQLite/Glass/websocket.
What changes
- engine.py: RepeaterHandler._build_packet_record() now computes
airtime_ms once per packet (Semtech formula via AirtimeManager) and
stores it as packet_record['airtime_ms']. Single source of truth for
every downstream consumer.
- storage_utils.py: PacketRecord.from_packet_record() reads the new
airtime_ms field and serializes it as a rounded integer in the
'duration' field of the published JSON. Falls back to 0 if the field
is missing (backward compatibility for any older code path).
- storage_collector.py: _publish_packet_to_mqtt() simplified - no
recomputation, no helper. The publish path is now a passthrough.
Why
MQTT consumers (firmware-compatible analyzers, dashboards, the upstream
meshcoretomqtt project) expect the same time-on-air value the firmware
emits. Hard-coded "0" makes airtime/utilization charts derived from the
mqtt stream useless and silently diverges from firmware behavior.
Plumbing the value through packet_record (instead of recomputing in the
publish path) means any future consumer - SQLite schema, web UI charts,
Glass telemetry - reads the same number without separate calculations.
Tests
tests/test_packet_duration.py - 5 tests covering:
- backward compat (legacy packet_record without airtime_ms => '0')
- airtime_ms field flows through to duration as rounded integer string
- explicit zero stays '0'
- AirtimeManager output matches an independently-implemented Semtech
reference for typical MeshCore EU settings (SF8/62.5kHz/CR4-8)
- low-data-rate optimization branch (SF12/125kHz triggers DE=1)
Co-Authored-By: Oz <oz-agent@warp.dev>
Introduces a 'set format and forget' workflow for MQTT brokers. Users
reference a bundled preset by name inside the existing brokers: list,
and the package supplies the endpoints, audiences, and TLS settings.
Endpoint changes ship via 'pip install -U' instead of manual edits.
What changes
- New repeater/presets/ package with a tiny lazy YAML loader and two
bundled presets: waev (mqtt-{a,b}.waev.app) and letsmesh (EU + US).
- New format-family constant MC2MQTT_FORMATS = ('meshcoretomqtt',
'letsmesh', 'waev') replaces the inline tuple in topic resolution.
The legacy 'mqtt' format keeps its custom-topic semantics unchanged.
- Two-pass broker assembly in mqtt_handler.py: pass 1 expands every
{preset: <name>} entry inline; pass 2 collapses duplicates by name
with later-wins semantics. Place override entries AFTER preset
entries.
- Hard-coded LETSMESH_BROKERS constant deleted; its data now lives in
repeater/presets/letsmesh.yaml.
- convert_letsmesh_to_broker_config() collapsed from ~70 to ~25 lines
by emitting {preset: letsmesh} plus disable overrides for unwanted
brokers. Honors broker_index in (-1, 0, 1), additional_brokers, and
enabled flag exactly as before.
- update_mqtt_config API endpoint accepts {preset: <name>} entries and
passes them through unchanged so the web UI can author them when the
frontend is updated.
- config.yaml.example documents the preset entry shape, the override
rule, and the format family hierarchy.
- pyproject.toml ships presets/*.yaml as package data.
How to use
mqtt_brokers:
iata_code: "LAX"
brokers:
- preset: waev
# Override a single preset broker:
brokers:
- preset: waev
- name: waev-b
enabled: false
Tests
- tests/test_presets.py: 9 tests covering loader, expand/merge,
MC2MQTT topic-family parity, and parametrized legacy migration.
Co-Authored-By: Oz <oz-agent@warp.dev>