fix(mesh): native E2E DM for archy↔archy text + software radio-reboot
- send_message now sends archy↔archy plain text as a native TEXT_MESSAGE_APP DM (firmware PKC-encrypts E2E), not wrapped in the binary typed envelope that silently broke archy↔archy LoRa delivery. Archy peers' Sent rows are marked encrypted so the E2E pill shows; rich typed msgs still use the typed-wire path. - Add a software radio-reboot to recover a wedged/RX-deaf radio without physical access (and for the Device-tab settings panel): driver reboot() via AdminMessage reboot_seconds=97 (verified vs meshtastic/protobufs), MeshCommand::RebootRadio, MeshService::reboot_radio, RPC mesh.reboot-radio. - Handoff doc: docs/SESSION-1.8.0-OTA-PROGRESS.md "RESUME HERE" — RF link is the proven blocker (radios not hearing each other); modem_preset mismatch is the prime suspect; on-device Meshtastic-app check + fix plan documented. 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
b4531bb4fc
commit
fbfeeeb0f5
@@ -1542,52 +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;
|
||||
// Stock (non-archipelago) radio contacts — e.g. a phone running the
|
||||
// MeshCore app — can't decode our typed envelope and would render it as
|
||||
// garbled bytes. Send them the raw text as a plain native DM instead.
|
||||
// Archipelago peers still get the typed envelope (seq/reply/reaction
|
||||
// addressing + encryption).
|
||||
if !self.is_archy_peer(contact_id).await {
|
||||
let dest_prefix = self.peer_dest_prefix(contact_id).await?;
|
||||
self.state
|
||||
.send_cmd(listener::MeshCommand::SendNativeText {
|
||||
dest_pubkey_prefix: dest_prefix,
|
||||
payload: text.as_bytes().to_vec(),
|
||||
})
|
||||
.await
|
||||
.map_err(|_| anyhow::anyhow!("Mesh listener not running"))?;
|
||||
return Ok(self
|
||||
.record_sent_typed(
|
||||
contact_id,
|
||||
"text",
|
||||
text,
|
||||
None,
|
||||
seq,
|
||||
Some("lora".to_string()),
|
||||
false,
|
||||
)
|
||||
.await);
|
||||
}
|
||||
// Sign the envelope with our archipelago identity key so the receiver
|
||||
// can authenticate us over LoRa (it verifies against our bound
|
||||
// `arch_pubkey_hex`). This is what lets a `!ai` typed in chat to a
|
||||
// trusted node pass the receiver's `trusted_only` gate over the radio —
|
||||
// an unsigned radio packet can never authenticate. The signature is
|
||||
// optional on the wire and ignored by peers that don't know our key, so
|
||||
// it stays backward compatible. (Federation/Tor sends already sign in
|
||||
// `send_typed_wire_via_federation`.) `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()?;
|
||||
self.send_typed_wire(contact_id, wire, "text", text, None, seq)
|
||||
// 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 dest_prefix = self.peer_dest_prefix(contact_id).await?;
|
||||
self.state
|
||||
.send_cmd(listener::MeshCommand::SendNativeText {
|
||||
dest_pubkey_prefix: dest_prefix,
|
||||
payload: text.as_bytes().to_vec(),
|
||||
})
|
||||
.await
|
||||
.map_err(|_| anyhow::anyhow!("Mesh listener not running"))?;
|
||||
// The firmware PKI-encrypts a directed DM to any peer whose key it knows;
|
||||
// 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,
|
||||
"text",
|
||||
text,
|
||||
None,
|
||||
seq,
|
||||
Some("lora".to_string()),
|
||||
e2e,
|
||||
)
|
||||
.await)
|
||||
}
|
||||
|
||||
/// Whether `contact_id` is an archipelago peer (vs a stock meshcore client).
|
||||
@@ -1724,6 +1717,26 @@ impl MeshService {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reboot the locally-connected radio firmware to recover a wedged /
|
||||
/// RX-deaf radio (one that has stopped hearing the mesh while still able to
|
||||
/// transmit). The device reconnects via the listener's reboot→reconnect
|
||||
/// loop. `seconds` is the firmware reboot delay.
|
||||
pub async fn reboot_radio(&self, seconds: i64) -> Result<()> {
|
||||
let status = self.state.status.read().await;
|
||||
if !status.device_connected {
|
||||
anyhow::bail!("No mesh device connected. Check USB connection.");
|
||||
}
|
||||
drop(status);
|
||||
|
||||
self.state
|
||||
.send_cmd(listener::MeshCommand::RebootRadio { seconds })
|
||||
.await
|
||||
.map_err(|_| anyhow::anyhow!("Mesh listener not running"))?;
|
||||
|
||||
info!(seconds, "Mesh radio reboot triggered");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Current mesh-AI assistant settings (issue #50).
|
||||
pub async fn assistant_config(&self) -> listener::AssistantConfig {
|
||||
self.state.assistant.read().await.clone()
|
||||
|
||||
Reference in New Issue
Block a user