diff --git a/core/archipelago/src/mesh/meshtastic.rs b/core/archipelago/src/mesh/meshtastic.rs index d2ee9606..4b2ec14b 100644 --- a/core/archipelago/src/mesh/meshtastic.rs +++ b/core/archipelago/src/mesh/meshtastic.rs @@ -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 { 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 { + let short: String = name + .chars() + .filter(|c| c.is_alphanumeric()) + .take(MESHTASTIC_SHORT_NAME_MAX) + .collect::() + .to_uppercase(); + if short.is_empty() { + None + } else { + Some(short) + } +} + fn encode_heartbeat() -> Vec { encode_to_radio_variant(TO_RADIO_HEARTBEAT, &[]) }