fix(mesh): rename Meshtastic radio to the node's server name

Meshtastic device rename was a no-op — set_advert_name only updated an
in-memory field and never told the radio, so the device kept its firmware
default ('Meshtastic xxxx') and wasn't findable from external Meshtastic
apps. MeshCore already renamed correctly (CMD_SET_ADVERT_NAME); this brings
Meshtastic to parity.

Send an AdminMessage{set_owner=User{long_name,short_name}} to the locally
connected node (admin packet to our own node_num on the ADMIN_APP port).
Local serial admin needs no session passkey, matching the official client.
long_name = server name (<=39 chars); short_name = first 4 alphanumerics,
upper-cased. Verified on real hardware: .120 -> 'Archy-X250-EXP', .5 ->
'Archy-X250-Beta' (name read back from the radio after reconnect).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago 2026-06-20 05:06:25 -04:00
parent b00c5247f5
commit d00d1b20d7

View File

@ -22,6 +22,13 @@ const START2: u8 = 0xc3;
const TO_RADIO_MAX: usize = 512;
const BROADCAST_NUM: u32 = 0xffff_ffff;
const TEXT_MESSAGE_APP: u32 = 1;
/// Meshtastic PortNum for admin (config) packets.
const ADMIN_APP: u32 = 6;
/// AdminMessage.set_owner oneof field number (carries a `User`).
const ADMIN_SET_OWNER_FIELD: u64 = 32;
/// Meshtastic firmware caps long_name at ~40 bytes and short_name at 4 bytes.
const MESHTASTIC_LONG_NAME_MAX: usize = 39;
const MESHTASTIC_SHORT_NAME_MAX: usize = 4;
const TO_RADIO_PACKET: u64 = 1;
const TO_RADIO_WANT_CONFIG_ID: u64 = 3;
@ -143,8 +150,56 @@ impl MeshtasticDevice {
})
}
/// Rename the connected Meshtastic radio to match the node's server name so
/// it's findable from external Meshtastic apps (phone/desktop) on the same
/// mesh. Previously this only updated the in-memory field and never told the
/// device — so the radio kept its firmware-default name ("Meshtastic xxxx").
///
/// We push an `AdminMessage { set_owner: User { long_name, short_name } }` to
/// the locally-connected node (an admin packet addressed to our own
/// `node_num`, on the ADMIN_APP port). Local admin over the serial link needs
/// no session passkey, so this is the same path the official phone/CLI client
/// uses for "set owner".
pub async fn set_advert_name(&mut self, name: &str) -> Result<()> {
self.long_name = Some(name.to_string());
let long_name: String = name.chars().take(MESHTASTIC_LONG_NAME_MAX).collect();
let short_name = derive_short_name(name).unwrap_or_else(|| {
self.short_name
.clone()
.unwrap_or_else(|| "NODE".to_string())
});
let Some(node_num) = self.node_num else {
// No local node number yet (initialize() not completed) — can't
// address a local admin packet. Record the intent so advert_name()
// still reflects it, but skip the device write.
warn!("Meshtastic set_advert_name: node_num unknown, skipping device write");
self.long_name = Some(long_name);
self.short_name = Some(short_name);
return Ok(());
};
// User { id?(1), long_name(2), short_name(3) }. Echo back the existing id
// when known so the firmware keeps the node's stable `!xxxxxxxx` id.
let mut user = Vec::new();
if let Some(id) = &self.user_id {
encode_len_field(1, id.as_bytes(), &mut user);
}
encode_len_field(2, long_name.as_bytes(), &mut user);
encode_len_field(3, short_name.as_bytes(), &mut user);
// AdminMessage { set_owner(32): User }
let mut admin = Vec::new();
encode_len_field(ADMIN_SET_OWNER_FIELD, &user, &mut admin);
// Admin packet to ourselves on the ADMIN_APP port.
let packet = encode_mesh_packet(node_num, ADMIN_APP, &admin);
self.send_to_radio(&encode_to_radio_variant(TO_RADIO_PACKET, &packet))
.await
.context("Failed to send Meshtastic set_owner admin packet")?;
info!(node_num, long_name = %long_name, short_name = %short_name, "Set Meshtastic device owner");
self.long_name = Some(long_name);
self.short_name = Some(short_name);
Ok(())
}
@ -428,6 +483,23 @@ fn encode_want_config() -> Vec<u8> {
encode_varint_field(TO_RADIO_WANT_CONFIG_ID, 1)
}
/// Derive a Meshtastic short_name (≤4 chars, the label shown on node icons) from
/// the human node name: the first few alphanumeric characters, upper-cased.
/// Returns `None` when the name has no usable alphanumeric characters.
fn derive_short_name(name: &str) -> Option<String> {
let short: String = name
.chars()
.filter(|c| c.is_alphanumeric())
.take(MESHTASTIC_SHORT_NAME_MAX)
.collect::<String>()
.to_uppercase();
if short.is_empty() {
None
} else {
Some(short)
}
}
fn encode_heartbeat() -> Vec<u8> {
encode_to_radio_variant(TO_RADIO_HEARTBEAT, &[])
}