diff --git a/src/main.rs b/src/main.rs index d3486a6..8a0a43f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -47,6 +47,7 @@ pub mod utils { pub mod firmware; pub mod flash_source; pub mod radio; + pub mod udp; } use error::CliError; @@ -1246,12 +1247,15 @@ async fn run() -> Result<()> { modules::bootloader::reboot(&link_context, uri.as_str()).await?; }, PlatformCommands::PowerOff => { + utils::udp::check_not_udp(&uri, "Powering off")?; crazyflie_lib::Crazyflie::power_off_all(&link_context, uri.as_str()).await?; }, PlatformCommands::Sleep => { + utils::udp::check_not_udp(&uri, "Sleep")?; crazyflie_lib::Crazyflie::power_off_stm32_domain(&link_context, uri.as_str()).await?; }, PlatformCommands::Wakeup => { + utils::udp::check_not_udp(&uri, "Wakeup")?; crazyflie_lib::Crazyflie::power_on_stm32_domain(&link_context, uri.as_str()).await?; } PlatformCommands::Dfu => { diff --git a/src/modules/bootloader.rs b/src/modules/bootloader.rs index d17fa99..5abdb10 100644 --- a/src/modules/bootloader.rs +++ b/src/modules/bootloader.rs @@ -15,6 +15,7 @@ use crate::modules::memory::{ use crate::utils::firmware::{Firmware, FirmwareUpgrade, FlashStartOverride}; use crate::utils::display::*; use crate::utils::radio::RadioUri; +use crate::utils::udp::check_not_udp; const DECK_CTRL_DFU_FLASH_OFFSET: usize = 0x10000; const DECK_CTRL_DFU_CFG_DEFAULT_OFFSET: usize = 0x17800; @@ -235,6 +236,7 @@ async fn reset_and_get_bootloader_address(link: &Connection) -> Result> /// return the URI to reach the bootloader at. The bootloader is reached /// with the same Crazyradio as the firmware, the first one for usb://. async fn enter_bootloader(context: &LinkContext, uri: &str) -> Result { + check_not_udp(uri, "Restarting into the bootloader")?; let radio = RadioUri::parse(uri).ok().flatten().and_then(|r| r.radio).unwrap_or(0); let separator = if uri.contains('?') { "&" } else { "?" }; let link = context.open_link(&format!("{}{}safelink=0", uri, separator)).await?; @@ -314,6 +316,7 @@ async fn read_info(link: &Connection) -> Result<(BootloaderInfo, BootloaderInfo) /// Restart a Crazyflie that runs its firmware into its bootloader, and return /// the address the bootloader listens on (on channel 0), for cfloader. pub async fn enter_bootloader_for_cfloader(context: &LinkContext, uri: &str) -> Result<[u8; 5]> { + check_not_udp(uri, "Restarting into the bootloader")?; let separator = if uri.contains('?') { "&" } else { "?" }; let link = context.open_link(&format!("{}{}safelink=0", uri, separator)).await?; let new_address = reset_and_get_bootloader_address(&link).await; @@ -422,6 +425,7 @@ pub async fn bootloader_versions(link_context: &LinkContext, uri: &str) -> Resul /// which only receives it over the radio: the STM32 firmware does not pass it /// on from USB. Returns whether the STM32 then showed up on USB in DFU mode. pub async fn stm32_dfu(link_context: &LinkContext, uri: &str) -> Result { + check_not_udp(uri, "Restarting the STM32 into DFU")?; if uri.starts_with("usb://") { bail!("Restarting the STM32 into DFU needs a radio URI, the nRF51 does not get this command over USB"); } @@ -451,6 +455,7 @@ fn stm32_dfu_present() -> Result { } pub async fn reboot(link_context: &LinkContext, uri: &str,) -> Result<()> { + check_not_udp(uri, "Rebooting")?; let link = link_context.open_link(uri).await?; send_command(&link, BootloaderCommand::ResetInit, None).await?; diff --git a/src/modules/swarm/commands.rs b/src/modules/swarm/commands.rs index adc34fc..2c92a90 100644 --- a/src/modules/swarm/commands.rs +++ b/src/modules/swarm/commands.rs @@ -17,6 +17,7 @@ use tabled::Tabled; use super::runner::{csv_row_for, split, Outcome, Runner, SwarmRow}; use crate::error::CliError; use crate::modules::{bootloader, debug, deck, log, param, platform}; +use crate::utils::udp::check_not_udp; use crate::utils::display::{csv_row, print_table, table, Column, StreamTable}; use crate::{ AssertArgs, SwarmDebugCommands, SwarmDeckCommands, SwarmLogCommands, SwarmParamCommands, @@ -34,6 +35,7 @@ pub async fn platform(runner: &Runner<'_>, command: &SwarmPlatformCommands, csv: SwarmPlatformCommands::Reboot => ( runner .each("Rebooting", async |t| { + check_not_udp(&t.uri, "Rebooting")?; t.check_answers(link_context).await?; bootloader::reboot(link_context, &t.link_uri).await }) @@ -43,6 +45,7 @@ pub async fn platform(runner: &Runner<'_>, command: &SwarmPlatformCommands, csv: SwarmPlatformCommands::PowerOff => ( runner .each("Powering off", async |t| { + check_not_udp(&t.uri, "Powering off")?; t.check_answers(link_context).await?; Ok(Crazyflie::power_off_all(link_context, &t.link_uri).await?) }) @@ -52,6 +55,7 @@ pub async fn platform(runner: &Runner<'_>, command: &SwarmPlatformCommands, csv: SwarmPlatformCommands::Sleep => ( runner .each("Putting to sleep", async |t| { + check_not_udp(&t.uri, "Sleep")?; t.check_answers(link_context).await?; Ok(Crazyflie::power_off_stm32_domain(link_context, &t.link_uri).await?) }) @@ -61,6 +65,7 @@ pub async fn platform(runner: &Runner<'_>, command: &SwarmPlatformCommands, csv: SwarmPlatformCommands::Wakeup => ( runner .each("Waking up", async |t| { + check_not_udp(&t.uri, "Wakeup")?; t.check_answers(link_context).await?; Ok(Crazyflie::power_on_stm32_domain(link_context, &t.link_uri).await?) }) diff --git a/src/modules/swarm/mod.rs b/src/modules/swarm/mod.rs index 301b9bf..1ed7bae 100644 --- a/src/modules/swarm/mod.rs +++ b/src/modules/swarm/mod.rs @@ -1088,15 +1088,15 @@ struct ScanRow { /// connection. async fn scan(runner: &Runner<'_>, csv: bool) -> Result<()> { let link_context = runner.link_context(); - let usb_present = if runner.targets.iter().any(|t| t.radio.is_none()) { + let usb_present = if runner.targets.iter().any(|t| t.is_usb()) { crate::scan_usb(link_context).await? } else { Vec::new() }; let results = runner - .each("Checking", async |t| match t.radio { - Some(_) => Ok(t.answers(link_context).await), - None => Ok(usb_present.contains(&t.uri)), + .each("Checking", async |t| match t.is_usb() { + true => Ok(usb_present.contains(&t.uri)), + false => Ok(t.answers(link_context).await), }) .await; let online: Vec = results.into_iter().map(|r| r.unwrap_or(false)).collect(); @@ -1108,7 +1108,11 @@ async fn scan(runner: &Runner<'_>, csv: bool) -> Result<()> { .map(|(t, online)| ScanRow { name: t.name.clone(), uri: t.uri.clone(), - radio: t.radio.map(|r| r.to_string()).unwrap_or_else(|| "USB".to_string()), + radio: match t.radio { + Some(radio) => radio.to_string(), + None if t.is_usb() => "USB".to_string(), + None => "UDP".to_string(), + }, online: if *online { "yes" } else { "no" }.to_string(), }) .collect(); diff --git a/src/modules/swarm/runner.rs b/src/modules/swarm/runner.rs index 200f542..8d127a5 100644 --- a/src/modules/swarm/runner.rs +++ b/src/modules/swarm/runner.rs @@ -54,7 +54,7 @@ pub struct Target { /// The URI to connect with: a real Crazyradio, and the timeout from the /// settings. pub link_uri: String, - /// The Crazyradio it was given, `None` for `usb://`. + /// The Crazyradio it was given, `None` for `usb://` and `udp://`. pub radio: Option, } @@ -300,11 +300,17 @@ impl<'a> Runner<'a> { } impl Target { - /// Whether the Crazyflie answers a packet over the radio, without - /// connecting to it. `usb://` targets aren't checked and count as - /// answering: opening a link to a missing USB device fails anyway. + /// Whether the Crazyflie is attached over USB. + pub fn is_usb(&self) -> bool { + self.uri.starts_with("usb://") + } + + /// Whether the Crazyflie answers a packet over the radio, or the + /// simulator over UDP, without connecting to it. `usb://` targets aren't + /// checked and count as answering: opening a link to a missing USB device + /// fails anyway. pub async fn answers(&self, link_context: &LinkContext) -> bool { - if self.radio.is_none() { + if self.is_usb() { return true; } for _ in 0..ANSWER_ATTEMPTS { diff --git a/src/modules/swarm/store.rs b/src/modules/swarm/store.rs index bbc9287..2e919e9 100644 --- a/src/modules/swarm/store.rs +++ b/src/modules/swarm/store.rs @@ -87,9 +87,9 @@ pub fn same_link(a: &str, b: &str) -> bool { /// Check a URI that is about to go into a swarm. pub fn check_uri(uri: &str) -> Result<()> { - if RadioUri::parse(uri)?.is_none() && !uri.starts_with("usb://") { + if RadioUri::parse(uri)?.is_none() && !uri.starts_with("usb://") && !uri.starts_with("udp://") { bail!(CliError::InvalidValue(format!( - "URI '{}': only radio:// and usb:// URIs can be added to a swarm", + "URI '{}': only radio://, usb:// and udp:// URIs can be added to a swarm", uri ))); } diff --git a/src/utils/udp.rs b/src/utils/udp.rs new file mode 100644 index 0000000..559975b --- /dev/null +++ b/src/utils/udp.rs @@ -0,0 +1,25 @@ +//! `udp://:` URIs reach the Crazyflie firmware simulator +//! (Simmyflie). It runs the STM32 firmware only: there is no nRF51 to take +//! power commands and no bootloader to flash through. + +use anyhow::{bail, Result}; + +/// Fail for a `udp://` URI: `what` needs the nRF51 or the bootloader. +pub fn check_not_udp(uri: &str, what: &str) -> Result<()> { + if uri.starts_with("udp://") { + bail!("{} is not supported over udp://, the firmware simulator has no nRF51 or bootloader", what); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn check_not_udp_rejects_udp_only() { + assert!(check_not_udp("udp://localhost:50000", "Rebooting").is_err()); + assert!(check_not_udp("radio://0/80/2M/E7E7E7E7E7", "Rebooting").is_ok()); + assert!(check_not_udp("usb://0", "Rebooting").is_ok()); + } +}