Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 8 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,13 @@ num_enum = "0.7.4"
half = "2.6.0"
env_logger = "0.11.8"
anyhow = "1.0"
crazyflie-link = { version = "0.5.0", default-features = false }
crazyflie-lib = { version = "0.8.0", default-features = false }
crazyflie-link = { version = "0.5.1", default-features = false }
# TODO: Return to a crates.io version before merging this PR.
crazyflie-lib = {
git = "https://github.com/bitcraze/crazyflie-lib-rs",
rev = "e7e1b594d4d015c06d6ba066a516199450aef69a",
default-features = false,
}
crazyradio = { version = "0.7.0", features = ["async"] }
rusb = "0.9"
cfloader = "0.1.0"
Expand Down Expand Up @@ -87,7 +92,7 @@ hex = "0.4.3"
chrono = "0.4.44"

[target.'cfg(unix)'.dependencies]
crazyflie-link = { version = "0.5.0", default-features = false, features = ["packet_capture"] }
crazyflie-link = { version = "0.5.1", default-features = false, features = ["packet_capture"] }
crazyradio = { version = "0.7.0", features = ["async", "packet_capture"] }

# Used by build.rs to generate shell-completion scripts from the same clap
Expand Down
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,13 @@ selected URI for a single command using the `--uri` flag:
cfcli --uri radio://0/80/2M/E7E7E7E7E7 console
```

Discover and stream a sourced console advertised by compatible firmware:

```text
cfcli console --list-sources
cfcli --timeout 3000 console --source deck:bcCam
```

You can preserve console output across connections using the `-p` flag. Console data is accumulated
during each connection and printed when the `console` command is run:

Expand Down Expand Up @@ -213,8 +220,8 @@ prompt) a few flags make the output predictable:
intended way to stop them and the command exits **0**. For all other
commands a timeout means the command got stuck and the command exits **40**.
* `--csv` — machine-readable CSV output for the read commands (`scan`,
`param list`/`get`, `log list`/`print`, `mem list`, `platform info`). Other
commands ignore the flag.
`param list`/`get`, `log list`/`print`, `mem list`, `platform info`,
`console --list-sources`). Other commands ignore the flag.

Exit codes:

Expand Down
48 changes: 45 additions & 3 deletions docs/console.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

This module provides access to the Crazyflie console.

**NOTE:** The console data history clears when you connect to the Crazyflie (i.e is downloaded on connect)
The legacy Crazyflie console and any sourced consoles are selected separately.
Running `cfcli console` without source options keeps the legacy behavior.

## Show console prints

Expand All @@ -18,6 +19,33 @@ If you do not want any formatting of the text then use the ```--no-format``` par
cfcli console --no-format
```

## Show a sourced console

Firmware using CRTP protocol 13 or newer may advertise additional console
sources, such as a console retained by a deck. List their paths with:

```text
cfcli console --list-sources
```

The list contains source paths only. Use global `--csv` to emit a `path`
header and one CSV row per source. Older firmware and Crazyflies without
sourced consoles report an empty list successfully.

Select one source by its exact, case-sensitive path:

```text
cfcli console --source deck:bcCam
```

The command first replays the source history retained for this connection and
then continues with live output. Formatting and `--no-format` behave like the
legacy console. Only one source can be selected at a time in this first
implementation; concurrent multi-source output may be added later.

If a requested source does not exist, cfcli exits with resource-not-found code
20 and reports the available paths.

## Preserve console across connections

Normally, console data is only available while connected. With the ```--preserve-console``` (```-p```) global flag, console output is saved to a file during every connection. When running multiple commands in a row the console data is accumulated:
Expand All @@ -35,6 +63,10 @@ cfcli console

This is useful for capturing console debug output that was printed during other operations (e.g. parameter changes or log sessions).

Preservation currently applies only to the legacy Crazyflie console. A sourced
console uses its own retained history and `cfcli console --source ...` neither
prints nor clears the locally preserved legacy-console file.

## Clear preserved console history

The `--clear` flag deletes the preserved console history file and exits without connecting to a Crazyflie. Useful when you want to discard accumulated output between runs:
Expand All @@ -47,10 +79,20 @@ The file path is shown by `cfcli settings show`.

## Stop streaming after a fixed duration

`console` is a streaming command — by default it runs until the link is broken. Combine it with the global `--timeout` flag to stop cleanly after a fixed wall-clock duration:
Legacy and sourced console output are streaming commands — by default they run
until the link is broken. Combine either with the global `--timeout` flag to
stop cleanly after a fixed wall-clock duration:

```text
cfcli --timeout 3000 console
cfcli --timeout 3000 console --source deck:bcCam
```

When `--timeout` fires on a streaming command, the process exits **0** (the timer is the intended way to stop it). This is the recommended pattern when running `cfcli console` from a script or CI step.
When `--timeout` fires on a streaming command, the process exits **0** (the
timer is the intended way to stop it). For a sourced console, cfcli then makes
a clean disable attempt bounded to one additional second before disconnecting.
This is the recommended pattern when running `cfcli console` from a script or
CI step.

`--list-sources` is bounded rather than streaming. If its global timeout
expires, cfcli exits with timeout code 40.
12 changes: 10 additions & 2 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,12 +295,20 @@ enum Commands {
/// Print the console text from a Crazyflie
Console {
/// Output raw console data without processing
#[clap(long)]
#[clap(long, conflicts_with = "list_sources")]
no_format: bool,

/// Delete the preserved console history file and exit without connecting
#[clap(long)]
#[clap(long, conflicts_with_all = ["source", "list_sources"])]
clear: bool,

/// Print one sourced console selected by its catalog path
#[clap(long, value_name = "PATH", conflicts_with_all = ["list_sources", "clear"])]
source: Option<String>,

/// List the available sourced-console paths and exit
#[clap(long, conflicts_with_all = ["source", "no_format", "clear"])]
list_sources: bool,
},

/// Local CLI settings (scan addresses, timeout, etc.)
Expand Down
101 changes: 86 additions & 15 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,48 @@ mod tests {

assert_eq!(unsupported_flash_targets(&selected), vec!["stm32ohnooo-fw".to_string()]);
}

#[test]
fn sourced_console_is_a_streaming_command() {
let args = CliArgs::try_parse_from(["cfcli", "console", "--source", "deck:bcCam"]).unwrap();

assert!(is_streaming_command(&args.command));
}

#[test]
fn listing_console_sources_is_a_bounded_command() {
let args = CliArgs::try_parse_from(["cfcli", "console", "--list-sources"]).unwrap();

assert!(!is_streaming_command(&args.command));
}

#[test]
fn console_source_conflicts_with_source_listing() {
let result = CliArgs::try_parse_from([
"cfcli",
"console",
"--source",
"deck:bcCam",
"--list-sources",
]);

assert!(result.is_err());
}

#[test]
fn source_listing_conflicts_with_console_formatting() {
let result = CliArgs::try_parse_from(["cfcli", "console", "--list-sources", "--no-format"]);

assert!(result.is_err());
}

#[test]
fn sourced_console_conflicts_with_clearing_legacy_history() {
let result =
CliArgs::try_parse_from(["cfcli", "console", "--source", "deck:bcCam", "--clear"]);

assert!(result.is_err());
}
}

impl MemoryTypeArg {
Expand Down Expand Up @@ -298,9 +340,15 @@ fn require_arg(non_interactive: bool, missing_arg: &str) -> Result<()> {
fn is_streaming_command(cmd: &Commands) -> bool {
matches!(
cmd,
Commands::Console { .. }
| Commands::Log { command: LogCommands::Print(_) }
| Commands::Cr { command: CrCommands::Sniff(_) }
Commands::Console {
clear: false,
list_sources: false,
..
} | Commands::Log {
command: LogCommands::Print(_)
} | Commands::Cr {
command: CrCommands::Sniff(_)
}
)
}

Expand Down Expand Up @@ -550,6 +598,7 @@ async fn run() -> Result<()> {
let link_context = crazyflie_link::LinkContext::new();

let mut connected_cf: Option<crazyflie_lib::Crazyflie> = None;
let mut enabled_console_source = None;
let preserve_console = args.preserve_console;
let timeout_ms = args.timeout;
let non_interactive = args.non_interactive || !std::io::stdin().is_terminal();
Expand Down Expand Up @@ -677,7 +726,7 @@ async fn run() -> Result<()> {
});

}
Commands::Console { no_format, clear } => {
Commands::Console { no_format, clear, source, list_sources } => {
if *clear {
let path = console_preserve_path();
if path.exists() {
Expand All @@ -689,21 +738,34 @@ async fn run() -> Result<()> {
return Ok(());
}

let saved = read_and_clear_console_file()?;
if !saved.is_empty() {
if *no_format {
print!("{}", saved);
} else {
for line in saved.lines() {
print!("{}", modules::console::format_console_line(line));
println!();
if source.is_none() && !list_sources {
let saved = read_and_clear_console_file()?;
if !saved.is_empty() {
if *no_format {
print!("{}", saved);
} else {
for line in saved.lines() {
print!("{}", modules::console::format_console_line(line));
println!();
}
}
}
}

let cf = connect_cf(&mut connected_cf, &link_context, uri.as_str(), toc_cache, args.debug).await?;

modules::console::print(cf, *no_format).await?;
if *list_sources {
modules::console::list_sources(cf, csv).await?;
} else if let Some(source) = source {
modules::console::print_source(
cf,
source,
*no_format,
&mut enabled_console_source,
).await?;
} else {
modules::console::print(cf, *no_format).await?;
}
// Cleanup at end of run() handles disconnect.
}
Commands::Log { command } => {
Expand Down Expand Up @@ -1280,12 +1342,12 @@ async fn run() -> Result<()> {
match command {
HlCommands::Arm => {
println!("Arming Crazyflie...");
cf.platform.send_arming_request(true).await?;
cf.supervisor.send_arming_request(true).await?;
println!("Crazyflie armed!");
}
HlCommands::Disarm => {
println!("Disarming Crazyflie...");
cf.platform.send_arming_request(false).await?;
cf.supervisor.send_arming_request(false).await?;
println!("Crazyflie disarmed!");
}
HlCommands::Takeoff(params) => {
Expand Down Expand Up @@ -1579,6 +1641,15 @@ async fn run() -> Result<()> {
body.await
};

if let (Some(cf), Some(selector)) = (connected_cf.as_ref(), enabled_console_source) {
const SOURCE_DISABLE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(1);
match tokio::time::timeout(SOURCE_DISABLE_TIMEOUT, cf.console.disable(selector)).await {
Ok(Ok(())) => {}
Ok(Err(err)) => eprintln!("Warning: could not disable console source: {}", err),
Err(_) => eprintln!("Warning: timed out while disabling console source"),
}
}

// Save console and disconnect any remaining connection
if let Some(ref cf) = connected_cf {
save_and_disconnect(cf, preserve_console).await;
Expand Down
Loading
Loading