fix(mesh): DM-via-channel tunnel + disable presence spam

Meshcore direct unicast silently drops between our two Archy nodes
(firmware reports flood sends with resp_code=6 but nothing arrives).
Wrap DMs as channel-1 broadcasts with a [0xD1][dest_prefix(6)][inner]
header; receivers filter by prefix and dispatch the inner payload
through the existing typed/base64/chunk ladder. Shrink chunk body to
125B so the wrapper still fits the 160B LoRa budget. Auto-heal
routing: CMD_RESET_PATH (0x0D) any type-1 contact with path_len=0 on
refresh so floods take over. send_text now returns the firmware's
flood/direct mode flag for diagnostics.

Disable the 120s presence heartbeat broadcaster — its CBOR payload
was being re-echoed as plaintext by the shared repeater, spamming
every visible node with garbled "Archy-…: av�…fstatusfonline…"
messages on channel 0. mesh.broadcast-presence RPC stays registered
but no longer transmits. Re-enable only once presence moves off the
shared broadcast path.

Also: MeshState.cmd_tx behind RwLock so stop()→start() cycles don't
fail with "command channel already consumed"; MeshService.send_cmd
helper; drop_message_by_id for control envelopes that shouldn't
appear as Sent bubbles; self_advert_name reflected into MeshStatus
after set; path_len/flags parsed out of RESP_CONTACT.

Frontend: unified inbox merges mesh peers with federation nodes by
DID/pubkey/name; hide presence/read_receipt/edit/channel_invite/
contact_card from chat stream; publicChannel index → 1 to match the
new DM-via-channel routing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-04-14 10:24:27 -04:00
co-authored by Claude Opus 4.6
parent bdacc06a2b
commit d514e0e5e4
10 changed files with 771 additions and 111 deletions
+31 -1
View File
@@ -24,6 +24,12 @@ pub const CMD_SET_DEVICE_TIME: u8 = 0x06;
pub const CMD_SEND_SELF_ADVERT: u8 = 0x07;
pub const CMD_SET_ADVERT_NAME: u8 = 0x08;
pub const CMD_SYNC_NEXT_MESSAGE: u8 = 0x0A;
/// CMD_RESET_PATH (0x0D): Tell the firmware to drop the stored route for
/// a contact and fall back to flood routing (out_path_len = 0xFF). Used to
/// unstick direct messages to contacts whose `path_len=0` means "no route
/// known" — without this, the firmware silently drops outbound TXT_MSG
/// frames to such contacts.
pub const CMD_RESET_PATH: u8 = 0x0D;
pub const CMD_SET_RADIO_PARAMS: u8 = 0x0B;
pub const CMD_SET_RADIO_TX_POWER: u8 = 0x0C;
pub const CMD_SET_TUNING_PARAMS: u8 = 0x15;
@@ -72,6 +78,16 @@ pub const ERR_ILLEGAL_ARG: u8 = 0x06;
/// Maximum payload size for a single LoRa message.
pub const MAX_MESSAGE_LEN: usize = 160;
/// Marker byte for "direct message wrapped as channel broadcast". Our
/// meshcore devices can hear each other's channel broadcasts (via
/// repeater flooding) but direct unicast frames don't reach between
/// archipelago nodes — so we emulate DMs by sending them on the shared
/// channel with a recipient pubkey-prefix header. Format:
/// `[DM_VIA_CHANNEL_MARKER][dest_pubkey_prefix(6B)][inner_payload…]`
/// The inner payload is whatever we would have sent directly — a typed
/// envelope, a chunked MC frame, or plain text.
pub const DM_VIA_CHANNEL_MARKER: u8 = 0xD1;
/// Minimum frame size: marker (1) + length (2) + command/response (1) = 4 bytes.
const MIN_FRAME_SIZE: usize = 4;
@@ -224,6 +240,15 @@ pub fn build_get_contacts() -> Vec<u8> {
encode_frame(&[CMD_GET_CONTACTS])
}
/// CMD_RESET_PATH (0x0D): `[0x0D][pub_key:32]`. Clears the stored route
/// for a contact so subsequent sends route via flood instead of being
/// silently dropped.
pub fn build_reset_path(pubkey: &[u8; 32]) -> Vec<u8> {
let mut data = vec![CMD_RESET_PATH];
data.extend_from_slice(pubkey);
encode_frame(&data)
}
/// CMD_SYNC_NEXT_MESSAGE (0x0A): Retrieve the next queued message.
pub fn build_sync_next_message() -> Vec<u8> {
encode_frame(&[CMD_SYNC_NEXT_MESSAGE])
@@ -294,6 +319,8 @@ pub struct ParsedContact {
pub advert_name: String,
pub last_advert: u32,
pub contact_type: u8,
pub path_len: u8,
pub flags: u8,
}
/// Parse RESP_CONTACT (0x03) response.
@@ -305,7 +332,8 @@ pub fn parse_contact(data: &[u8]) -> Result<ParsedContact> {
let public_key_hex = hex::encode(&data[0..32]);
let contact_type = data[32];
// flags at data[33], path_len at data[34]
let flags = if data.len() > 33 { data[33] } else { 0 };
let path_len = if data.len() > 34 { data[34] } else { 0 };
// path at data[35..99] (64 bytes)
// name at data[99..131] (32 bytes)
let name_start = 99.min(data.len());
@@ -330,6 +358,8 @@ pub fn parse_contact(data: &[u8]) -> Result<ParsedContact> {
advert_name,
last_advert,
contact_type,
path_len,
flags,
})
}