// WIP mesh/transport protocol — suppress dead code warnings #![allow(dead_code)] //! Async serial driver for Meshcore devices. //! //! Handles opening the serial port, reading/writing frames, //! and the initialization handshake sequence. use super::protocol::{self, InboundFrame}; use super::types::DeviceInfo; use anyhow::{Context, Result}; use std::path::Path; use std::time::Duration; use tracing::{debug, info, warn}; /// Serial port configuration for Meshcore Companion USB. const BAUD_RATE: u32 = 115200; /// Timeout for reading a response frame from the device. const READ_TIMEOUT: Duration = Duration::from_secs(5); /// Timeout for writing a frame to the device. const WRITE_TIMEOUT: Duration = Duration::from_secs(2); /// Buffer size for serial reads. const READ_BUF_SIZE: usize = 512; /// Application name sent during handshake. const APP_NAME: &str = "Archipelago"; /// Async Meshcore device handle. pub struct MeshcoreDevice { port: serial2_tokio::SerialPort, read_buf: Vec, pub node_id: Option, pub advert_name: Option, pub device_info: Option, device_path: String, } impl MeshcoreDevice { /// Open a serial port and verify it's a Meshcore device. pub async fn open(path: &str) -> Result { // Check device exists before trying to open (better error message) match tokio::fs::metadata(path).await { Ok(meta) => { debug!(path = %path, permissions = ?meta.permissions(), "Device node exists"); } Err(e) => { anyhow::bail!( "Serial device {} not accessible: {} (check PrivateDevices in systemd, or USB connection)", path, e ); } } let port = serial2_tokio::SerialPort::open(path, BAUD_RATE).context(format!( "Failed to open serial port {} (permission denied? device busy?)", path ))?; info!(path = %path, baud = BAUD_RATE, "Opened serial port"); Ok(Self { port, read_buf: Vec::with_capacity(READ_BUF_SIZE), node_id: None, advert_name: None, device_info: None, device_path: path.to_string(), }) } /// Run the Meshcore initialization handshake. /// Matches the official meshcore_py library sequence: /// 1. CMD_APP_START -> RESP_SELF_INFO (this is the first command, not device_query) /// 2. CMD_SET_DEVICE_TIME (sync clock) pub async fn initialize(&mut self) -> Result { info!("Starting Meshcore handshake on {}", self.device_path); // Step 1: App start (the official library sends this first) self.send_raw(&protocol::build_app_start(APP_NAME)).await?; let frame = self .recv_frame_timeout(READ_TIMEOUT) .await .context("No response to APP_START — is this a Meshcore Companion USB device?")?; info!( code = frame.code, data_len = frame.data.len(), "Got response to APP_START" ); if frame.code == protocol::RESP_ERR { anyhow::bail!("App start failed: {}", protocol::parse_error(&frame.data)); } // The response could be SELF_INFO or something else depending on firmware version let (node_id, name) = if frame.code == protocol::RESP_SELF_INFO { protocol::parse_self_info(&frame.data).context("Failed to parse self info")? } else { // Try to parse whatever we got info!( code = frame.code, "Unexpected response code, trying to parse as self info" ); protocol::parse_self_info(&frame.data).unwrap_or((0, String::new())) }; info!(node_id, name = %name, "Meshcore identity"); self.node_id = Some(node_id); self.advert_name = Some(name.clone()); // Step 2: Sync device clock let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default() .as_secs(); self.send_raw(&protocol::build_set_device_time(now)).await?; // Time set response is best-effort — don't fail if it times out match self.recv_frame_timeout(Duration::from_secs(2)).await { Ok(frame) if frame.code == protocol::RESP_OK => { debug!("Device clock synced"); } Ok(frame) => { warn!(code = frame.code, "Unexpected response to SET_DEVICE_TIME"); } Err(_) => { warn!("No response to SET_DEVICE_TIME (continuing anyway)"); } } let info = DeviceInfo { firmware_version: name.clone(), node_id, max_contacts: 100, device_type: super::types::DeviceType::Meshcore, }; self.device_info = Some(info.clone()); info!("Meshcore initialization complete on {}", self.device_path); Ok(info) } /// Set the advertised name on the mesh network. pub async fn set_advert_name(&mut self, name: &str) -> Result<()> { self.send_raw(&protocol::build_set_advert_name(name)) .await?; let frame = self.recv_frame_timeout(READ_TIMEOUT).await?; if frame.code == protocol::RESP_ERR { anyhow::bail!( "Set advert name failed: {}", protocol::parse_error(&frame.data) ); } self.advert_name = Some(name.to_string()); Ok(()) } /// Broadcast our advertisement to the mesh. pub async fn send_self_advert(&mut self) -> Result<()> { self.send_raw(&protocol::build_send_self_advert()).await?; // Response is RESP_OK or RESP_SENT let frame = self.recv_frame_timeout(READ_TIMEOUT).await?; if frame.code == protocol::RESP_ERR { anyhow::bail!("Self advert failed: {}", protocol::parse_error(&frame.data)); } Ok(()) } /// Send a text message to a contact by their public key prefix (first 6 bytes). /// Returns whether the firmware routed it via flood (true) or direct (false). /// The response frame is `RESP_CODE_SENT | mode | tag[4] | est_timeout[4]` /// where mode == 1 means flood and mode == 0 means direct. pub async fn send_text(&mut self, dest_pubkey_prefix: &[u8; 6], msg: &[u8]) -> Result { let frame_data = protocol::build_send_text(dest_pubkey_prefix, msg)?; self.send_raw(&frame_data).await?; let frame = self.recv_frame_timeout(READ_TIMEOUT).await?; if frame.code == protocol::RESP_ERR { anyhow::bail!("Send text failed: {}", protocol::parse_error(&frame.data)); } // RESP_CODE_SENT layout: [mode(1)][tag(4)][est_timeout(4)] let sent_via_flood = frame.data.first().copied().unwrap_or(0) == 1; tracing::info!( dest = %hex::encode(dest_pubkey_prefix), mode = if sent_via_flood { "flood" } else { "direct" }, resp_code = frame.code, data_len = frame.data.len(), "[diag] send_text response" ); Ok(sent_via_flood) } /// Broadcast a text message on a channel. pub async fn send_channel_text(&mut self, channel: u8, msg: &[u8]) -> Result<()> { let frame_data = protocol::build_send_channel_text(channel, msg)?; self.send_raw(&frame_data).await?; let frame = self.recv_frame_timeout(READ_TIMEOUT).await?; if frame.code == protocol::RESP_ERR { anyhow::bail!( "Channel broadcast failed: {}", protocol::parse_error(&frame.data) ); } Ok(()) } /// Send a NATIVE meshcore direct message (CMD_SEND_TXT_MSG) to a contact, /// addressed by the first 6 bytes of its public key. Unlike the /// `@DM2`-over-channel path, this is a real unicast — it does not appear on /// the public channel, and a stock meshcore client receives it as a normal /// DM. The contact must already exist in the firmware table (with a path). pub async fn send_text_msg(&mut self, dest_pubkey_prefix: &[u8; 6], msg: &[u8]) -> Result<()> { let frame_data = protocol::build_send_text(dest_pubkey_prefix, msg)?; self.send_raw(&frame_data).await?; let frame = self.recv_frame_timeout(READ_TIMEOUT).await?; if frame.code == protocol::RESP_ERR { anyhow::bail!( "Direct text send failed: {}", protocol::parse_error(&frame.data) ); } Ok(()) } /// Clear the stored routing path for a contact so the firmware flood- /// routes future messages instead of dropping them when path_len=0. pub async fn reset_contact_path(&mut self, pubkey: &[u8; 32]) -> Result<()> { self.send_raw(&protocol::build_reset_path(pubkey)).await?; let frame = self.recv_frame_timeout(READ_TIMEOUT).await?; if frame.code == protocol::RESP_ERR { anyhow::bail!("Reset path failed: {}", protocol::parse_error(&frame.data)); } Ok(()) } /// Delete a contact from the firmware's persistent contact table. pub async fn remove_contact(&mut self, pubkey: &[u8; 32]) -> Result<()> { self.send_raw(&protocol::build_remove_contact(pubkey)) .await?; let frame = self.recv_frame_timeout(READ_TIMEOUT).await?; if frame.code == protocol::RESP_ERR { anyhow::bail!( "Remove contact failed: {}", protocol::parse_error(&frame.data) ); } Ok(()) } /// Add/update a contact in the firmware table (CMD_ADD_UPDATE_CONTACT). /// Used to import a heard advert so it shows up as a contact immediately. pub async fn add_contact( &mut self, pubkey: &[u8; 32], contact_type: u8, flags: u8, out_path_len: u8, name: &str, last_advert: u32, ) -> Result<()> { self.send_raw(&protocol::build_add_contact( pubkey, contact_type, flags, out_path_len, name, last_advert, )) .await?; let frame = self.recv_frame_timeout(READ_TIMEOUT).await?; if frame.code == protocol::RESP_ERR { anyhow::bail!("Add contact failed: {}", protocol::parse_error(&frame.data)); } Ok(()) } /// Get the list of known contacts from the device. /// Protocol: CMD_GET_CONTACTS -> CONTACT_START(count) -> N×CONTACT -> CONTACT_END pub async fn get_contacts(&mut self) -> Result> { self.send_raw(&protocol::build_get_contacts()).await?; let mut contacts = Vec::new(); loop { let frame = self.recv_frame_timeout(READ_TIMEOUT).await?; match frame.code { protocol::RESP_CONTACT_START => { // Contains the count of contacts to follow let count = if frame.data.len() >= 4 { u32::from_le_bytes([ frame.data[0], frame.data[1], frame.data[2], frame.data[3], ]) } else { 0 }; debug!(count, "Contact list start"); } protocol::RESP_CONTACT => match protocol::parse_contact(&frame.data) { Ok(contact) => contacts.push(contact), Err(e) => warn!("Failed to parse contact: {}", e), }, protocol::RESP_CONTACT_END => { debug!(count = contacts.len(), "Contact list complete"); break; } protocol::RESP_OK => break, protocol::RESP_ERR => { anyhow::bail!( "Get contacts failed: {}", protocol::parse_error(&frame.data) ); } _ => { debug!(code = frame.code, "Unexpected response during contact list"); // Don't break — might be a push notification interspersed } } } Ok(contacts) } /// Retrieve queued messages from the device. /// Returns raw frames (code + data) for the listener to parse. pub async fn sync_messages(&mut self) -> Result> { self.send_raw(&protocol::build_sync_next_message()).await?; let mut frames = Vec::new(); loop { let frame = self.recv_frame_timeout(READ_TIMEOUT).await?; match frame.code { // All message types (v1 and v3) protocol::RESP_CONTACT_MSG | protocol::RESP_CONTACT_MSG_V3 | protocol::RESP_CHANNEL_MSG | protocol::RESP_CHANNEL_MSG_V3 => { frames.push(frame); // Request next message self.send_raw(&protocol::build_sync_next_message()).await?; } protocol::RESP_NO_MORE_MESSAGES => break, protocol::RESP_OK => break, protocol::RESP_ERR => { anyhow::bail!( "Sync messages failed: {}", protocol::parse_error(&frame.data) ); } _ => { // Push notifications can arrive during sync — skip them if protocol::is_push_notification(frame.code) { continue; } debug!(code = frame.code, "Unexpected response during message sync"); break; } } } Ok(frames) } /// Write raw bytes to the serial port. pub async fn send_raw(&mut self, data: &[u8]) -> Result<()> { tokio::time::timeout(WRITE_TIMEOUT, self.port.write_all(data)) .await .context("Serial write timed out")? .context("Serial write failed")?; Ok(()) } /// Try to read and parse one complete inbound frame. /// Returns the frame if one is available, or reads more data from serial. pub async fn try_recv_frame(&mut self) -> Result> { // First check if we already have a complete frame in the buffer if let Some(frame) = protocol::decode_frame(&self.read_buf) { let consumed = frame.bytes_consumed; let result = frame; self.read_buf.drain(..consumed); return Ok(Some(result)); } // Try to read more data (non-blocking via small timeout) let mut tmp = [0u8; READ_BUF_SIZE]; match tokio::time::timeout(Duration::from_millis(50), self.port.read(&mut tmp)).await { Ok(Ok(n)) if n > 0 => { self.read_buf.extend_from_slice(&tmp[..n]); } _ => return Ok(None), } // Try parsing again with new data if let Some(frame) = protocol::decode_frame(&self.read_buf) { let consumed = frame.bytes_consumed; let result = frame; self.read_buf.drain(..consumed); return Ok(Some(result)); } Ok(None) } /// Read one complete inbound frame with timeout. pub async fn recv_frame_timeout(&mut self, timeout: Duration) -> Result { let deadline = tokio::time::Instant::now() + timeout; loop { // Check buffer for a complete frame if let Some(frame) = protocol::decode_frame(&self.read_buf) { let consumed = frame.bytes_consumed; let result = frame; self.read_buf.drain(..consumed); return Ok(result); } // Read more data from serial let remaining = deadline.saturating_duration_since(tokio::time::Instant::now()); if remaining.is_zero() { anyhow::bail!("Timeout waiting for serial frame"); } let mut tmp = [0u8; READ_BUF_SIZE]; match tokio::time::timeout( remaining.min(Duration::from_millis(100)), self.port.read(&mut tmp), ) .await { Ok(Ok(0)) => anyhow::bail!("Serial port closed"), Ok(Ok(n)) => { self.read_buf.extend_from_slice(&tmp[..n]); } Ok(Err(e)) => return Err(e).context("Serial read error"), Err(_) => continue, // timeout on this read, try again if deadline not reached } } } /// Get the device path this handle is connected to. pub fn path(&self) -> &str { &self.device_path } } // ─── Device detection ─────────────────────────────────────────────────── /// Candidate serial device paths to check on Linux. /// /dev/mesh-radio is a stable udev symlink (see 99-mesh-radio.rules). const SERIAL_CANDIDATES: &[&str] = &[ "/dev/mesh-radio", "/dev/ttyUSB0", "/dev/ttyUSB1", "/dev/ttyUSB2", "/dev/ttyACM0", "/dev/ttyACM1", "/dev/ttyACM2", ]; const SKIP_SERIAL_MODEL_SUBSTRINGS: &[&str] = &["Sierra_Wireless", "Z-Wave", "Zooz"]; fn likely_non_mesh_serial_device(path: &str) -> bool { let Some(name) = Path::new(path).file_name().and_then(|s| s.to_str()) else { return false; }; let by_id = Path::new("/dev/serial/by-id"); let Ok(entries) = std::fs::read_dir(by_id) else { return false; }; for entry in entries.flatten() { let file_name = entry.file_name().to_string_lossy().to_string(); if !SKIP_SERIAL_MODEL_SUBSTRINGS .iter() .any(|needle| file_name.contains(needle)) { continue; } if let Ok(target) = std::fs::read_link(entry.path()) { if target.file_name().and_then(|s| s.to_str()) == Some(name) { return true; } } } false } /// Scan for serial devices that could be Meshcore radios. /// Returns paths to existing serial device files. pub async fn detect_serial_devices() -> Vec { let mut devices = Vec::new(); for path in SERIAL_CANDIDATES { if tokio::fs::metadata(path).await.is_ok() { if likely_non_mesh_serial_device(path) { debug!(path = %path, "Skipping known non-mesh serial device"); continue; } devices.push(path.to_string()); } } devices } /// Try to open and handshake with each detected serial device. /// Returns the first device that responds as Meshcore. pub async fn probe_for_meshcore(paths: &[String]) -> Option<(String, DeviceInfo)> { for path in paths { debug!(path = %path, "Probing for Meshcore device"); match MeshcoreDevice::open(path).await { Ok(mut device) => { match device.initialize().await { Ok(info) => { info!(path = %path, firmware = %info.firmware_version, "Found Meshcore device"); // Drop the device so the listener can open it drop(device); return Some((path.clone(), info)); } Err(e) => { debug!(path = %path, error = %e, "Not a Meshcore device"); } } } Err(e) => { debug!(path = %path, error = %e, "Could not open serial port"); } } } None }