From 45848e9f96109eb1048502068e46837dcc6a37c1 Mon Sep 17 00:00:00 2001 From: Marcus Eliasson Date: Tue, 20 Oct 2026 15:52:26 +0200 Subject: [PATCH] Support udp:// Crazyflies, the firmware simulator crazyflie-link opens udp://: links to the Simmyflie, the Crazyflie firmware built for the sim platform. Swarms can now hold udp:// URIs, so the same swarm file can start the simulators and be used with cfcli, and swarm scan probes them instead of counting them as USB Crazyflies that are not attached. The simulator runs the STM32 firmware only. Commands that need the nRF51 or the bootloader (reboot, power off, sleep, wakeup, bootloading and DFU) now fail at once with a clear message for udp:// URIs, instead of doing nothing or timing out waiting for the nRF51. --- src/main.rs | 4 ++++ src/modules/bootloader.rs | 5 +++++ src/modules/swarm/commands.rs | 5 +++++ src/modules/swarm/mod.rs | 14 +++++++++----- src/modules/swarm/runner.rs | 16 +++++++++++----- src/modules/swarm/store.rs | 4 ++-- src/utils/udp.rs | 25 +++++++++++++++++++++++++ 7 files changed, 61 insertions(+), 12 deletions(-) create mode 100644 src/utils/udp.rs 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()); + } +}