Commit Graph
18 Commits
Author SHA1 Message Date
CoderNemesis 5175558dd7 fix(send): split oversized channel text before it stalls the radio (#299)
An oversized webhook or channel body was handed to the firmware whole. The device dropped it without an ack, retries stalled the transport, and the radio looked dead. Split to the frame budget, keep links intact, and tag multi-part sends so they can be reassembled.

Reserve the wx location prefix from that same budget, and let NOAA alert polling be turned off without disabling the rest of the weather service.
2026-09-22 16:21:54 -07:00
agessaman cc6ca00b4e docs(keywords): say the newline escape is a backslash, not a slash (#277)
The report used /n and got two literal characters. The escape works the same in
every keyword path — I checked the test command's template engine too — so the
defect is that the instructions never said "backslash" and used `test` as their
example, which is the one keyword a built-in command owns. That command renders
through a different engine and silently drops the mesh-info placeholders listed
in the same block.

Also documents the configparser continuation form, which needs no escape, and
corrects two packet-capture examples that still showed a naive local timestamp
where the bot has published UTC with a Z suffix since v1.0.0 (#276).
2026-09-20 10:58:17 -07:00
Adam Gessaman 9939e22575 fix(core): release the radio lock while waiting for ACKs (#291)
Radio commands were serialized per method call, so a composite command held the lock for everything it awaited. A DM's send_msg_with_retry kept every other command waiting through all of its ACK timeouts (up to ~36 s with three attempts), stalling channel replies, other DMs, scheduled sends, and automatic message fetching. req_regions_sync did the same for each neighbor scope reply.

Serialize at the frame instead: wrap CommandHandler.send() on the handler instance, which is where every meshcore command writes its frame and waits for the radio's immediate reply. There is still at most one in-flight companion frame, paced as before, and library methods that call self.send are covered without a proxy. Waits for ACKs and remote responses now run outside the lock.

Add MeshCoreBot.radio_session() for short sequences that must not interleave, and use it for region-scoped channel sends so set scope, send, and restore stay together. The per-call lock never guaranteed that: a DM retry loop queued between set_flood_scope and send_chan_msg sent its flood attempts under the channel's scope.
2026-09-19 09:12:07 -07:00
agessaman 98893a4e4f chore(release): prepare v1.1.0 2026-09-14 20:47:09 -07:00
Gerard Hickey 97dc2bbf91 docs: Update docs concerning waev.app broker authentication
Signed-off-by: Gerard Hickey <hickey@kinetic-compute.com>
2026-09-04 21:49:53 -04:00
Adam Gessaman 1740ca8072 fix(mqtt): resolve reconnect storm and improve token renewal handling
This commit addresses several issues related to MQTT connections. It prevents MQTT brokers from entering a reconnect storm by ensuring that the packet-capture watchdog only triggers a reconnect when necessary, avoiding duplicate CONNACKs and spurious disconnects. Additionally, it ensures that renewed MQTT auth tokens are properly enforced by implementing a clean reconnect process.

New configuration options are introduced: `mqttN_keepalive` for setting the PINGREQ interval per broker, and `mqttN_jwt_reconnect_on_renew` to control reconnect behavior after token renewal. Documentation has been updated to reflect these changes and to clarify the importance of unique client IDs for brokers in the same cluster.
2026-08-28 22:13:42 -07:00
AJ Quick 9eeb226def Add observer_name configuration to packet-capture
Add optional observer name configuration for MQTT.
2026-08-08 15:37:46 +00:00
agessaman ae3a3c66fa fix(neighbors): move the airtime cooldown into the service
Move the 15-minute gap between cycles from the DM command to the service, as
MIN_CYCLE_GAP_SECONDS. The command was one caller among several: the scheduler
retried a failed cycle on its own 300s backoff, keyed off last_neighbors_publish
which a failed cycle never stamps, so a discover round whose acknowledgement was
lost went back on the air every five minutes. run_neighbors_cycle now refuses a
cycle inside the gap whoever asks, and the scheduler's retry backoff waits out
whatever remains of it. Failures that never reached the radio still stamp
nothing, so re-checking a disconnected radio stays on the short backoff.

The command asks the service for the remaining wait instead of computing its own,
so the two cannot drift, and rewinds the sender's per-user cooldown to expire
with the shared one. The command manager records an execution before calling
execute(), so a refusal had been consuming the sender's full 15 minutes: told to
wait one more minute, they would retry and be refused for another fourteen.
Rewound rather than cleared — a refusal reply is airtime too.
2026-08-05 19:38:36 -07:00
agessaman ce68e7ab7e fix(neighbors): close three gaps in the cycle guards
Follow-up to 7cffe1d; all three confirmed against meshcore 2.3.8.

Charge the node-wide cooldown for the transmission, not the result.
last_neighbors_publish is stamped only by a cycle that completes, so a discover
request whose acknowledgement was lost spent the airtime and left the clock at
zero -- another sender could immediately start a second round. The service now
stamps last_neighbors_attempt before the request, and the command rations on
whichever stamp is later. A cycle that bails out before touching the radio still
records nothing, so retries stay possible.

Restore the contact path when req_regions_sync returns None. send_anon_req gives
up early when change_contact_path reports an error -- which is also what a lost
acknowledgement for an applied path change looks like -- and that return skips
the library's own reset_path. req_regions_sync collapses it to None, previously
treated as a plain no-response. On the common "neighbour did not answer" path
the extra reset is a redundant device command: no airtime, idempotent, and it
re-syncs the contact cache.

Stop reporting a rejected restore as a success. reset_path returns an ERROR
event for a device rejection or its own response timeout rather than raising, so
the helper logged "restored flood path" either way and hid a contact left pinned
to zero-hop. It now inspects the event and warns with the reason.
2026-08-05 19:01:23 -07:00
agessaman 7cffe1d7c0 fix(neighbors): bound cycle airtime and correct evidence labelling
Four review findings on this branch, all confirmed against the code and the
installed meshcore 2.3.8:

Single-flight the discovery cycle. The command guarded only its own task, so
the scheduler's independent call could overlap a manual cycle and each round
would collect into the other's discover window. run_neighbors_cycle is now a
guard around the cycle body, refusing whichever trigger arrives second.

Make the 15-minute cooldown per node rather than per sender. The base class
rations per user, but the cost here is mesh airtime: users could take turns and
keep the radio discovering continuously. Measured from the last cycle that
produced a result, so the scheduler's cycles count and a cycle that bailed out
without transmitting does not start the clock.

Window the neighbours evidence label. The combined viewer applied `days` only
to mesh_connections while reading every lifetime row from neighbor_links, which
is deliberately never pruned — so a link last heard years ago kept claiming a
recent path-derived edge was a current direct neighbour.

Match that label on full public keys too. MeshGraph.add_edge does not promote a
1-byte edge that has no public key, but still fills in the keys discovery
supplied, so the 3-byte prefix comparison alone left confirmed neighbours
labelled singlebyte. Truncating our keys to 2 chars instead would relabel every
other node sharing that byte.

Restore a contact's flood path after an interrupted scope request. send_anon_req
pins a path-less contact to zero-hop and restores it after the send with no
try/finally, so our own budget cancelling the request left the contact pinned
and every later message to it sent direct-only.

The meshcore>=2.3.8 pin needed no change: PyPI publishes 2.3.8 now.
2026-08-05 18:32:42 -07:00
agessaman ebc66992dd feat(neighbors): zero-hop neighbour discovery in packet capture
Port the observer firmware's neighbours feature into the bot's packet capture
service, by way of meshcore-packet-capture (upstream PRs #42/#43). On a long
interval the bot asks which repeaters it hears directly and records each
confirmed link with its measured SNR.

This is the strongest link evidence the bot collects: a first-party RF
measurement between two full 32-byte public keys. Path inference works from
1-3 byte prefixes with no keys, and complete_contact_tracking.hop_count
over-claims zero-hop (800 claimed vs 68 corroborated on the live database).

modules/neighbors_discovery.py keeps upstream's public names so its fixes and
tests stay portable. Two deliberate divergences:

- No command_lock plumbing. _SerializedCommands in modules/core.py already
  serialises and paces every radio command, strictly more than upstream's
  reentrant lock did.
- neighbors_collect_scopes defaults off. Upstream's zero-hop scope probe
  relies on a neighbour not being a known contact; this bot tracks contacts,
  and for a repeater with no stored path the library reaches zero-hop by
  calling change_contact_path() then reset_path() -- mutating the device's
  contact table per neighbour. Scope requests also hold the radio lock for
  their whole round trip (~25s), stalling bot replies. The default cycle is
  one command plus a passive listen window, during which the bot stays
  responsive.

Evidence lands in neighbor_links and neighbor_observations (migration 22)
rather than mesh_connections, which cannot persist provenance. The viewer
exposes it as evidence=neighbors on /api/mesh/edges and a Neighbours Only
mode on the mesh page, with populated public keys and real SNR; confirmed
neighbours also relabel edges in the combined view and count as
provenance-trusted when framing the initial map.

neighbors_enabled is the single switch. Every enabled broker publishes once
it is on (mqttN_neighbors defaults true; set false to hold one back). The
topic derives from each broker's packets topic with the last segment swapped,
so a templated broker gets meshcore/{IATA}/{PUBLIC_KEY}/neighbors -- the
topic the firmware uses -- instead of an unrelated flat one. A derived
location-routed topic is skipped with a warning when no iata is set, rather
than publishing into meshcore/XYZ/... on a shared namespace. Snapshots are
non-retained: heard_secs_ago is relative to publish time, so a retained copy
would read as current days later.

Also adds a DM-gated `neighbors` command (the 12h interval floor makes
waiting for the scheduler impractical), which acks immediately and reports in
a second message once the window closes.

Requires meshcore >= 2.3.8 for send_node_discover_req / req_regions_sync.
2026-08-04 19:24:18 -07:00
agessaman 9a24ed1bf2 chore: update documentation and improve configuration handling
- Added a tracked `LICENSE` file and updated `pyproject.toml` to include license metadata.
- Enhanced `CHANGELOG.md` with recent changes and clarifications.
- Updated `config.ini.example` and related documentation to reflect clamping behavior for numeric limits in `[Feed_Manager]`.
- Improved startup validation to suggest corrections for unknown and misspelled keys.
- Refactored geocoding and HTTP request handling to run off-thread, preventing event loop stalls.
- Added thread safety to cache management in geocoding functions to avoid race conditions.
2026-07-28 16:56:43 -07:00
agessaman 7e7279875c feat(packet_capture): decode packet payloads to MQTT and log
Add optional payload decoding to the packet capture service. GRP_TXT
channel messages are decrypted (sender/text), ADVERTs are parsed
(name/role/lat-lon), and a nested "decoded" object is attached to each
packet alongside the unchanged raw fields.

- Comprehensive channel key store: bot's configured radio channels plus
  decode_hashtag_channels, [Channels_List], decode_channel_keys, and the
  built-in default Public key.
- Publishing the decoded object to MQTT is off by default and
  configurable per broker via mqttN_include_decoded.
- Configurable packet-log rotation (off/size/time) for historical dumps.

The decoder lives in a standalone, dependency-free module
(modules/meshcore_payload_decode.py) so it can be shared verbatim with
the meshcore-packet-capture project.

Closes #197
Closes #35
2026-07-08 10:49:06 -07:00
agessaman 9d24e4824c feat(config, docs): add JWT configuration options for MQTT authentication
- Introduced `jwt_ttl_seconds` and `jwt_renewal_interval` settings in `config.ini.example` for global JWT management, allowing for better control over token expiration and renewal intervals.
- Updated documentation in `packet-capture.md` to clarify the usage of global and per-broker JWT settings, enhancing user understanding of authentication configurations.
- Refactored `PacketCaptureService` to incorporate new JWT settings, ensuring consistent handling of token lifetimes and renewal processes.
2026-05-15 20:46:05 -07:00
agessaman 263c73c2e2 feat(packet_capture): enhance MQTT packet publishing controls and path length decoding
- Added configuration options `mqtt_skip_unparseable_packets` and `advert_require_valid_signature` to control MQTT publishing behavior based on packet validity.
- Updated `decode_path_len_byte` function to return `None` for reserved size codes, improving path length validation.
- Implemented logic in `PacketCaptureService` to skip publishing unparseable packets and ADVERT packets with invalid signatures.
- Introduced `verify_meshcore_advert_ed25519` function for signature verification of ADVERT packets, with corresponding unit tests to ensure functionality.
- Enhanced documentation to reflect new configuration options and their effects on packet processing.
2026-05-01 21:32:40 -07:00
agessaman 8b363afed7 Update IATA code in configuration files and packet capture service
- Changed the default IATA code from 'LOC' to 'XYZ' in `config.ini.example` and `packet-capture.md` to reflect updated routing requirements.
- Updated the `PacketCaptureService` to use 'XYZ' as the fallback IATA code, ensuring consistency across the application.
2026-03-09 22:09:39 -07:00
agessaman 99f4ed32d9 Add packet type filtering documentation to packet-capture.md 2026-02-21 09:34:29 -08:00
agessaman 652712eb0c Update documentation links and file references for consistency
- Renamed various documentation files to use lowercase for improved consistency, including `WEB_VIEWER.md`, `DOCKER.md`, and `PATH_COMMAND_CONFIG.md`.
- Updated references in `config.ini.example`, `docker-setup.sh`, `README.md`, and multiple documentation files to reflect the new file names.
- Removed obsolete documentation files related to the Discord Bridge, Map Uploader, Packet Capture, and Weather Service, streamlining the documentation structure.
2026-02-16 17:15:52 -08:00