fix(mesh): restore Meshtastic inbound stream after radio reboot
archy went deaf to inbound LoRa packets after every config write. A config write (region/channel/owner) reboots the radio, which resets the firmware PhoneAPI to STATE_SEND_NOTHING; it won't stream received packets again until the client re-sends want_config. archy ignored FromRadio.rebooted (field 8) so never resubscribed — which is why old messages only arrived after a full restart (restart = fresh want_config). - meshtastic.rs: handle FROM_RADIO_REBOOTED -> set pending_reinit; try_recv_frame re-sends want_config to resubscribe the packet stream. Add send_keepalive (bare heartbeat) and pin modem_preset=LONG_FAST in set_lora_region so all radios share frequency. - listener/session.rs: MeshRadioDevice::send_keepalive; 10s sync_timer sends a keepalive each tick (insurance vs 15-min idle serial close). - mod.rs send_message: device-aware send — Meshtastic archy peers get a plain TEXT_MESSAGE_APP DM (firmware PKC E2E); Meshcore archy peers keep the typed envelope (no meshcore regression). Verified: .198->.228 directed DM arrives as RECEIVED enc=True peer="Arch Optiplex"; all 3 nodes (.116/.198/.228) + 3ccc hear each other. Binary 737b16c3 deployed+active on all three. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
fbfeeeb0f5
commit
a57ae388ec
@@ -1542,21 +1542,45 @@ impl MeshService {
|
||||
/// MeshMessage carries a stable MessageKey — this is what makes replies
|
||||
/// and reactions addressable against plain text bubbles.
|
||||
pub async fn send_message(&self, contact_id: u32, text: &str) -> Result<MeshMessage> {
|
||||
use crate::mesh::message_types::{MeshMessageType, TypedEnvelope};
|
||||
let seq = self.state.next_send_seq(contact_id).await;
|
||||
// Plain chat text — to BOTH archy peers and stock devices — is sent as a
|
||||
// native Meshtastic DM on TEXT_MESSAGE_APP. The firmware end-to-end
|
||||
// (PKC / Curve25519) encrypts a directed DM whenever it knows the
|
||||
// destination's public key, which archy peers exchange via NodeInfo, so
|
||||
// the message is delivered E2E and surfaces as chat on every client.
|
||||
//
|
||||
// We deliberately do NOT wrap archy↔archy text in our binary typed
|
||||
// envelope here. Meshtastic firmware 2.7.x will not deliver an opaque
|
||||
// directed payload as a message: PRIVATE_APP is treated as opaque app
|
||||
// data (never shown as chat), and a base64 envelope overflows a single
|
||||
// LoRa frame and chunk-fails. Wrapping text was exactly what silently
|
||||
// broke archy↔archy LoRa while archy→stock (plain text) kept working.
|
||||
// Rich typed messages (invoice/coordinate/reaction/…) still use the
|
||||
// typed-wire path via `send_typed_wire`; only plain Text goes native.
|
||||
let device_type = self.state.status.read().await.device_type;
|
||||
let archy = self.is_archy_peer(contact_id).await;
|
||||
|
||||
// Transport choice is DEVICE-AWARE so we fix Meshtastic without regressing
|
||||
// Meshcore:
|
||||
// • Meshtastic (any peer) → plain text native DM on TEXT_MESSAGE_APP. The
|
||||
// firmware end-to-end (PKC/Curve25519) encrypts a directed DM to any
|
||||
// peer whose public key it knows (archy peers exchange them via
|
||||
// NodeInfo), so it's delivered E2E and shows as chat on every client.
|
||||
// Meshtastic firmware 2.7.x will NOT deliver our opaque binary typed
|
||||
// envelope as a message (PRIVATE_APP is opaque app-data; a base64
|
||||
// envelope overflows one LoRa frame and chunk-fails) — wrapping text
|
||||
// is exactly what silently broke archy↔archy Meshtastic LoRa.
|
||||
// • Meshcore archy peer → keep the rich signed typed envelope. Meshcore
|
||||
// frames are binary-safe (no UTF-8 mangling) and it carries its own
|
||||
// session E2E + our signature for `!ai` auth / seq reply addressing,
|
||||
// so the envelope works there and we must not drop it.
|
||||
// • Meshcore stock client → plain text (can't decode our envelope).
|
||||
// Rich typed messages (invoice/coordinate/reaction/…) always use the
|
||||
// typed-wire path via `send_typed_wire`; only plain Text is routed here.
|
||||
let use_typed_envelope = archy && device_type == DeviceType::Meshcore;
|
||||
if use_typed_envelope {
|
||||
// Sign with our archipelago identity so the receiver can authenticate
|
||||
// us over LoRa (verifies against our bound `arch_pubkey_hex`). `with_seq`
|
||||
// is applied after signing — seq is not covered by the signature.
|
||||
let envelope = TypedEnvelope::new_signed(
|
||||
MeshMessageType::Text,
|
||||
text.as_bytes().to_vec(),
|
||||
&self.signing_key,
|
||||
)
|
||||
.with_seq(seq);
|
||||
let wire = envelope.to_wire()?;
|
||||
return self
|
||||
.send_typed_wire(contact_id, wire, "text", text, None, seq)
|
||||
.await;
|
||||
}
|
||||
|
||||
let dest_prefix = self.peer_dest_prefix(contact_id).await?;
|
||||
self.state
|
||||
.send_cmd(listener::MeshCommand::SendNativeText {
|
||||
@@ -1569,7 +1593,6 @@ impl MeshService {
|
||||
// archy peers always exchange keys, so mark those Sent rows E2E so the
|
||||
// pill shows immediately. (The receiver independently stamps E2E from the
|
||||
// radio's `pki_encrypted` flag, so an inbound row is accurate regardless.)
|
||||
let e2e = self.is_archy_peer(contact_id).await;
|
||||
Ok(self
|
||||
.record_sent_typed(
|
||||
contact_id,
|
||||
@@ -1578,7 +1601,7 @@ impl MeshService {
|
||||
None,
|
||||
seq,
|
||||
Some("lora".to_string()),
|
||||
e2e,
|
||||
archy,
|
||||
)
|
||||
.await)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user