From f3e5e06937fa98b8e12c81219ef6e038bf3dd9d5 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Tue, 29 Sep 2026 15:31:53 +0200 Subject: [PATCH] unified example types and clients --- README.md | 8 +- examples/client/.gitignore | 1 + examples/client/Cargo.toml | 3 +- examples/client/build.rs | 13 + examples/client/config.toml.template | 7 + examples/client/src/main.rs | 114 ++++++- examples/embedded-client/config.toml | 1 - .../embedded-client/src/bin/stm32h7-client.rs | 95 ------ examples/embedded-client/src/lib.rs | 3 +- examples/embedded-types/src/lib.rs | 22 -- examples/stm32h7-nucleo-embassy/Cargo.lock | 116 ++++++- examples/stm32h7-nucleo-embassy/Cargo.toml | 3 +- examples/stm32h7-nucleo-embassy/README.md | 25 +- examples/stm32h7-nucleo-embassy/src/lib.rs | 17 + examples/stm32h7-nucleo-embassy/src/main.rs | 263 +++++++++----- examples/stm32h7-nucleo-rtic/Cargo.lock | 117 ++++++- examples/stm32h7-nucleo-rtic/Cargo.toml | 4 +- examples/stm32h7-nucleo-rtic/README.md | 25 +- examples/stm32h7-nucleo-rtic/src/lib.rs | 17 + examples/stm32h7-nucleo-rtic/src/main.rs | 323 +++++++++++------- examples/types/src/ccsds.rs | 17 +- examples/types/src/led.rs | 53 +++ examples/types/src/lib.rs | 21 ++ examples/types/tests/ccsds_len.rs | 21 +- 24 files changed, 906 insertions(+), 383 deletions(-) create mode 100644 examples/client/.gitignore create mode 100644 examples/client/build.rs create mode 100644 examples/client/config.toml.template delete mode 100644 examples/embedded-client/src/bin/stm32h7-client.rs create mode 100644 examples/types/src/led.rs diff --git a/README.md b/README.md index 40b8a98..8f306ea 100644 --- a/README.md +++ b/README.md @@ -48,9 +48,9 @@ All examples and their helper crates are located inside the * [`minisim`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/minisim): Mini-Simulator based on [nexosim](https://github.com/asynchronics/nexosim) which simulates some physical devices for the `example-std` application device handlers. -* [`client`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/client): Ground client to command the `example-std` application. +* [`client`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/client): Ground client to command the `example-std` application and the STM32H7 examples. * [`types`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/types): Telecommand and telemetry definitions shared by the - `example-std` application and the `client`. + `example-std` application, the STM32H7 examples and the `client`. * [`stm32f3-disco-rtic`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/stm32f3-disco-rtic): Simple example using low-level sat-rs components on a bare-metal system with constrained resources. This example uses the [RTIC](https://github.com/rtic-rs/rtic) framework on the @@ -63,8 +63,8 @@ All examples and their helper crates are located inside the Same as `stm32h7-nucleo-rtic`, but using the [embassy](https://embassy.dev/) executor instead of RTIC. * [`embedded-client`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/embedded-client) and - [`embedded-types`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/embedded-types): Client and shared telecommand and telemetry - definitions for the embedded examples. + [`embedded-types`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/embedded-types): Client and telecommand and telemetry + definitions for the STM32F3 example, which uses its own protocol. The library crates and the `example-std` application have their own `CHANGELOG.md`. diff --git a/examples/client/.gitignore b/examples/client/.gitignore new file mode 100644 index 0000000..91cc9ca --- /dev/null +++ b/examples/client/.gitignore @@ -0,0 +1 @@ +/config.toml diff --git a/examples/client/Cargo.toml b/examples/client/Cargo.toml index f2d3573..d60d35f 100644 --- a/examples/client/Cargo.toml +++ b/examples/client/Cargo.toml @@ -8,7 +8,8 @@ clap = { version = "4", features = ["derive"] } log = "0.4" fern = "0.7" humantime = "2" -serde = { version = "1" } +serde = { version = "1", features = ["derive"] } +toml = "1" satrs = { path = "../../satrs" } example-std = { path = "../example-std" } minisim-types = { path = "../minisim-types" } diff --git a/examples/client/build.rs b/examples/client/build.rs new file mode 100644 index 0000000..1e2a2f7 --- /dev/null +++ b/examples/client/build.rs @@ -0,0 +1,13 @@ +use std::path::PathBuf; +use std::{env, fs}; + +fn main() { + let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); + let config = manifest_dir.join("config.toml"); + let config_template = manifest_dir.join("config.toml.template"); + + if !config.exists() { + fs::copy(&config_template, &config).unwrap(); + } + println!("cargo::rerun-if-changed=config.toml.template"); +} diff --git a/examples/client/config.toml.template b/examples/client/config.toml.template new file mode 100644 index 0000000..a4ae81c --- /dev/null +++ b/examples/client/config.toml.template @@ -0,0 +1,7 @@ +# Copied to config.toml by the build script if config.toml does not exist yet. config.toml is not +# tracked by git, so it can hold local settings like the address of a development board. + +[interface] +# Address of the commanded application. Defaults to the example-std OBSW on the local host. +# Can be overridden with the --udp-addr argument. +# udp_addr = "192.168.1.50:7301" diff --git a/examples/client/src/main.rs b/examples/client/src/main.rs index a206c51..acd0b9e 100644 --- a/examples/client/src/main.rs +++ b/examples/client/src/main.rs @@ -23,11 +23,40 @@ pub struct Cli { ping: bool, #[arg(short, long)] test_event: bool, + /// Address of the commanded application. Overrides the address inside `config.toml`. + #[arg(long, global = true)] + udp_addr: Option, #[command(subcommand)] commands: Option, } +#[derive(Debug, Default, serde::Deserialize)] +struct Config { + #[serde(default)] + interface: InterfaceConfig, +} + +#[derive(Debug, Default, serde::Deserialize)] +struct InterfaceConfig { + /// Defaults to the example-std OBSW on the local host. + udp_addr: Option, +} + +impl Config { + /// The build script creates `config.toml` from the template. A missing file is still accepted, + /// because all parameters have defaults. + fn load() -> anyhow::Result { + let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("config.toml"); + match std::fs::read_to_string(&path) { + Ok(content) => toml::from_str(&content) + .with_context(|| format!("parsing {} failed", path.display())), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Self::default()), + Err(e) => Err(e).with_context(|| format!("reading {} failed", path.display())), + } + } +} + #[derive(clap::Subcommand)] enum Commands { Mgm0(MgmArgs), @@ -36,6 +65,8 @@ enum Commands { Mgt(MgtArgs), AcsSubsystem(SubsystemArgs), EventManager(EventManagerArgs), + /// Blinking LEDs of the embedded examples. + Led(LedArgs), } #[derive(clap::Parser)] @@ -194,6 +225,27 @@ struct MgtArgs { health: Option, } +#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::Parser)] +struct LedArgs { + #[arg(short, long)] + ping: bool, + /// Mode of the red and the orange LED. + #[arg(short, long, value_enum)] + mode: Option, + /// Toggle period of the toggle modes. + #[arg(long, default_value_t = 500)] + toggle_period_ms: u64, +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)] +enum LedModeSelect { + AllOff, + RedOn, + OrangeOn, + AlternatingToggle, + UnifiedToggle, +} + #[derive(Debug, PartialEq, Eq, Clone, Copy, clap::Parser)] struct MgmAssemblyArgs { #[arg(short, long)] @@ -274,6 +326,40 @@ fn send_mgt_request( client.send_to(&packet.to_vec(), addr).unwrap(); } +fn send_led_request(client: &UdpSocket, addr: SocketAddr, request: types::led::request::Request) { + let packet = types::ccsds::CcsdsTcPacketOwned::new_with_request( + SpacePacketHeader::new_from_apid(u11::new(Apid::Tmtc as u16)), + TcHeader::new(types::ComponentId::Led, request.message_type()), + request, + ); + let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header); + log::info!( + "sending LED request {:?} with TC ID {:#010x}", + request, + sent_tc_id.raw() + ); + client.send_to(&packet.to_vec(), addr).unwrap(); +} + +fn handle_led_command(client: &UdpSocket, addr: SocketAddr, args: LedArgs) { + use types::led::request::Request; + + if args.ping { + send_led_request(client, addr, Request::Ping); + } + if let Some(mode) = args.mode { + let toggle_period = Duration::from_millis(args.toggle_period_ms); + let mode = match mode { + LedModeSelect::AllOff => types::led::Mode::AllOff, + LedModeSelect::RedOn => types::led::Mode::RedOn, + LedModeSelect::OrangeOn => types::led::Mode::OrangeOn, + LedModeSelect::AlternatingToggle => types::led::Mode::AlternatingToggle(toggle_period), + LedModeSelect::UnifiedToggle => types::led::Mode::UnifiedToggle(toggle_period), + }; + send_led_request(client, addr, Request::SetMode(mode)); + } +} + fn handle_mgt_command(client: &UdpSocket, addr: SocketAddr, args: MgtArgs) -> anyhow::Result<()> { use types::acs::mgt::request::{ModeRequest, Request}; @@ -456,9 +542,14 @@ fn main() -> anyhow::Result<()> { let ctrl_kill_signal = kill_signal.clone(); ctrlc::set_handler(move || ctrl_kill_signal.store(true, Ordering::Relaxed)).unwrap(); let cli = Cli::parse(); + let config = Config::load()?; - let addr = SocketAddr::new(IpAddr::V4(OBSW_SERVER_ADDR), SERVER_PORT); - let client = UdpSocket::bind("127.0.0.1:7302").expect("Connecting to UDP server failed"); + let addr = cli + .udp_addr + .or(config.interface.udp_addr) + .unwrap_or(SocketAddr::new(IpAddr::V4(OBSW_SERVER_ADDR), SERVER_PORT)); + // Bind to all interfaces, so embedded targets inside the local network can be reached too. + let client = UdpSocket::bind("0.0.0.0:7302").expect("Connecting to UDP server failed"); client.set_nonblocking(true)?; client.set_read_timeout(Some(Duration::from_millis(200)))?; @@ -584,6 +675,7 @@ fn main() -> anyhow::Result<()> { } } Commands::EventManager(args) => handle_event_manager_command(&client, addr, args), + Commands::Led(args) => handle_led_command(&client, addr, args), } } @@ -763,6 +855,11 @@ fn handle_raw_tm_packet(data: &[u8]) -> anyhow::Result<()> { postcard::from_bytes::(remainder); log::info!("Received response from MGT: {:?}", response.unwrap()); } + types::ComponentId::Led => { + let response = + postcard::from_bytes::(remainder); + log::info!("Received response from LED: {:?}", response.unwrap()); + } } } Err(_) => todo!(), @@ -788,6 +885,19 @@ mod tests { assert!(parse_dipole("1,2,3,4").is_err()); } + #[test] + fn test_config_template_is_valid() { + let template = include_str!("../config.toml.template"); + let config: Config = toml::from_str(template).unwrap(); + assert_eq!(config.interface.udp_addr, None); + let config: Config = + toml::from_str("[interface]\nudp_addr = \"192.168.1.50:7301\"").unwrap(); + assert_eq!( + config.interface.udp_addr, + Some("192.168.1.50:7301".parse().unwrap()) + ); + } + #[test] fn test_negative_torque_argument() { let cli = Cli::try_parse_from(["client", "mgt", "--torque", "-200,200,1000"]).unwrap(); diff --git a/examples/embedded-client/config.toml b/examples/embedded-client/config.toml index 14a0ccc..5b018b6 100644 --- a/examples/embedded-client/config.toml +++ b/examples/embedded-client/config.toml @@ -1,3 +1,2 @@ [interface] serial_port = "/dev/ttyUSB0" -# udp_addr = "192.168.XXX.XX:7301" diff --git a/examples/embedded-client/src/bin/stm32h7-client.rs b/examples/embedded-client/src/bin/stm32h7-client.rs deleted file mode 100644 index bfb6af9..0000000 --- a/examples/embedded-client/src/bin/stm32h7-client.rs +++ /dev/null @@ -1,95 +0,0 @@ -use std::{net::UdpSocket, time::Duration}; - -use anyhow::{Context as _, bail}; -use clap::Parser; -use embedded_client::setup_logger; -use embedded_types::{TmHeader, stm32h7}; -use spacepackets::{CcsdsPacketCreatorOwned, CcsdsPacketReader, SpHeader}; -use tmtc_utils::transport::udp::PacketTransportUdp; - -#[derive(Parser, Debug)] -struct Cli { - #[arg(short, long)] - ping: bool, - - /// Set frequency in milliseconds. - #[arg(short, long)] - set_led_frequency: Option, - - /// UDP address to bind to. - #[arg(short, long)] - udp_addr: Option, -} - -fn main() -> anyhow::Result<()> { - setup_logger().expect("failed to initialize logger"); - println!("-- STM32H7 TMTC client --"); - let cli = Cli::parse(); - let config = embedded_client::Config::new_from_file(); - let mut udp_addr = cli.udp_addr; - if udp_addr.is_none() { - udp_addr = config.interface.udp_addr; - } - if udp_addr.is_none() { - bail!("UDP address not specified in config.toml or via command line"); - } - let udp_addr = udp_addr.unwrap(); - log::info!("binding to UDP address: {}", udp_addr); - let local_socket = UdpSocket::bind("0.0.0.0:0").expect("failed to bind UDP socket"); - let mut transport = PacketTransportUdp::new(local_socket, udp_addr) - .with_context(|| "crateing UDP transport failed")?; - - if cli.ping { - let tc = create_stm32h7_tc(&embedded_types::stm32h7::Request::Ping); - log::info!( - "Sending ping request with TC ID: {:#010x}", - tc.ccsds_packet_id_and_psc().raw() - ); - transport.send(&tc.to_vec()).unwrap(); - } - - if let Some(freq_ms) = cli.set_led_frequency { - let request = stm32h7::Request::ChangeBlinkFrequency(Duration::from_millis(freq_ms as u64)); - let tc = create_stm32h7_tc(&request); - log::info!( - "Sending change blink frequency request {:?} with TC ID: {:#010x}", - request, - tc.ccsds_packet_id_and_psc().raw() - ); - transport.send(&tc.to_vec()).unwrap(); - } - - log::info!("Waiting for response..."); - loop { - transport - .receive(|packet: &[u8]| { - let reader = CcsdsPacketReader::new_with_checksum(packet); - log::debug!("Received packet: {:?}", reader); - if let Ok(reader) = reader { - let packet_data = reader.packet_data(); - let tm_header = postcard::take_from_bytes::(packet_data); - if let Ok((tm_header, remainder)) = tm_header { - let response = postcard::from_bytes::(remainder); - if let Ok(response) = response { - log::info!( - "Received TM with header: {:?} and response: {:?}", - tm_header, - response - ); - } else { - log::error!("Failed to deserialize response: {:?}", response.err()); - } - } else { - log::error!("Failed to deserialize TM header: {:?}", tm_header.err()); - } - } - }) - .unwrap(); - } -} - -fn create_stm32h7_tc(request: &stm32h7::Request) -> CcsdsPacketCreatorOwned { - let req_raw = postcard::to_allocvec(&request).unwrap(); - let sp_header = SpHeader::new_from_apid(embedded_types::stm32h7::PUS_APID); - CcsdsPacketCreatorOwned::new_tc_with_checksum(sp_header, &req_raw).unwrap() -} diff --git a/examples/embedded-client/src/lib.rs b/examples/embedded-client/src/lib.rs index ceff88b..f5d2c4d 100644 --- a/examples/embedded-client/src/lib.rs +++ b/examples/embedded-client/src/lib.rs @@ -1,4 +1,4 @@ -use std::{fs::File, io::Read as _, net::SocketAddr, path::Path, time::SystemTime}; +use std::{fs::File, io::Read as _, path::Path, time::SystemTime}; #[derive(Debug, serde::Deserialize)] pub struct Config { @@ -8,7 +8,6 @@ pub struct Config { #[derive(Debug, serde::Deserialize)] pub struct Interface { pub serial_port: Option, - pub udp_addr: Option, } impl Config { diff --git a/examples/embedded-types/src/lib.rs b/examples/embedded-types/src/lib.rs index e3d74b6..6de874d 100644 --- a/examples/embedded-types/src/lib.rs +++ b/examples/embedded-types/src/lib.rs @@ -25,28 +25,6 @@ pub mod stm32f3 { } } -/// This might look like a duplication, but we intentionally keep those separate so they can -/// change independently. -pub mod stm32h7 { - use arbitrary_int::u11; - use core::time::Duration; - - pub const PUS_APID: u11 = u11::new(0x03); - - #[derive(Copy, Clone, Debug, serde::Serialize, serde::Deserialize)] - #[cfg_attr(feature = "defmt", derive(defmt::Format))] - pub enum Request { - Ping, - ChangeBlinkFrequency(Duration), - } - - #[derive(Debug, serde::Serialize, serde::Deserialize)] - #[cfg_attr(feature = "defmt", derive(defmt::Format))] - pub enum Response { - Ok, - } -} - #[derive(Debug, serde::Serialize, serde::Deserialize)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct TmHeader { diff --git a/examples/stm32h7-nucleo-embassy/Cargo.lock b/examples/stm32h7-nucleo-embassy/Cargo.lock index ef8b69f..935885e 100644 --- a/examples/stm32h7-nucleo-embassy/Cargo.lock +++ b/examples/stm32h7-nucleo-embassy/Cargo.lock @@ -154,6 +154,16 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cobs" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd93fd2c1b27acd030440c9dbd9d14c1122aad622374fe05a670b67a4bc034be" +dependencies = [ + "heapless 0.9.3", + "thiserror", +] + [[package]] name = "const-default" version = "1.0.0" @@ -343,6 +353,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "derive-new" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cdc8d50f426189eef89dac62fabfa0abb27d5cc008f25bf4156a0203325becc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "document-features" version = "0.2.12" @@ -642,6 +663,12 @@ dependencies = [ "nb 1.1.0", ] +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + [[package]] name = "embedded-io" version = "0.6.1" @@ -710,17 +737,6 @@ dependencies = [ "embedded-storage", ] -[[package]] -name = "embedded-types" -version = "0.1.0" -dependencies = [ - "arbitrary-int", - "defmt 1.1.0", - "postcard", - "serde", - "spacepackets", -] - [[package]] name = "find-msvc-tools" version = "0.1.14" @@ -817,9 +833,16 @@ checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4" dependencies = [ "defmt 1.1.0", "hash32 0.3.1", + "serde_core", "stable_deref_trait", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "ident_case" version = "1.0.1" @@ -993,8 +1016,9 @@ version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" dependencies = [ - "cobs", - "defmt 1.1.0", + "cobs 0.3.0", + "embedded-io 0.4.0", + "embedded-io 0.6.1", "heapless 0.7.17", "serde", ] @@ -1117,6 +1141,32 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "satrs" +version = "0.3.0-alpha.3" +dependencies = [ + "arbitrary-int", + "cobs 0.5.1", + "delegate", + "derive-new", + "num_enum", + "paste", + "postcard", + "satrs-shared", + "serde", + "spacepackets", + "static_cell", + "thiserror", +] + +[[package]] +name = "satrs-shared" +version = "0.2.4" +dependencies = [ + "serde", + "spacepackets", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -1314,11 +1364,12 @@ dependencies = [ "embassy-sync", "embassy-time", "embedded-alloc", - "embedded-types", "panic-probe", "postcard", + "serde", "spacepackets", "static_cell", + "types", ] [[package]] @@ -1327,6 +1378,27 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "svgbobdoc" version = "0.3.0" @@ -1463,6 +1535,22 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "types" +version = "0.1.0" +dependencies = [ + "arbitrary-int", + "bitbybit", + "heapless 0.9.3", + "num_enum", + "postcard", + "satrs", + "serde", + "spacepackets", + "strum", + "thiserror", +] + [[package]] name = "unicode-ident" version = "1.0.24" diff --git a/examples/stm32h7-nucleo-embassy/Cargo.toml b/examples/stm32h7-nucleo-embassy/Cargo.toml index 3f37c13..95676de 100644 --- a/examples/stm32h7-nucleo-embassy/Cargo.toml +++ b/examples/stm32h7-nucleo-embassy/Cargo.toml @@ -13,7 +13,7 @@ name = "integration" harness = false [dependencies] -embedded-types = { path = "../embedded-types", features = ["defmt"] } +types = { path = "../types" } cortex-m = { version = "0.7", features = ["critical-section-single-core"] } arbitrary-int = "2" @@ -25,6 +25,7 @@ embedded-alloc = "0.7" static_cell = "2" spacepackets = { version = "0.18", default-features = false, features = ["defmt"] } postcard = "1" +serde = { version = "1", default-features = false } embassy-stm32 = { version = "0.6", features = ["stm32h753zi", "memory-x", "defmt", "time-driver-any"] } embassy-executor = { version = "0.10", features = ["platform-cortex-m", "executor-thread", "defmt"] } diff --git a/examples/stm32h7-nucleo-embassy/README.md b/examples/stm32h7-nucleo-embassy/README.md index c34753f..918c9ce 100644 --- a/examples/stm32h7-nucleo-embassy/README.md +++ b/examples/stm32h7-nucleo-embassy/README.md @@ -76,30 +76,37 @@ Make sure to install this plugin first. The board is commanded via UDP on port 7301. It gets its IP address via DHCP, so it needs to be connected to a network with a DHCP server. The network configuration including the IP address is logged after startup. According to the board user manual UM2407, jumper JP6 and solder bridge SB72 -must be ON when using Ethernet. The telecommands are CCSDS space packets with a -[`postcard`](https://docs.rs/postcard) serialized payload. +must be ON when using Ethernet. The board uses the same TMTC protocol as the +[`example-std`](../example-std) application, which is defined inside the [`types`](../types) +crate. -The [`embedded-client`](../embedded-client) application is used to command the board. Set the -address of the board inside `embedded-client/config.toml`, for example: +The [`client`](../client) application is used to command the board. Set the address of the board +inside `client/config.toml`, which is created from `client/config.toml.template` on the first +build: ```toml [interface] udp_addr = "192.168.1.50:7301" ``` -Then run the client from inside the `embedded-client` directory. For example, you can send a ping -to the MCU using +For example, you can then send a ping to the MCU using ```sh -cargo run --bin stm32h7-client -- --ping +cargo run -p client -- --ping ``` -and set the LED blink frequency to 500 ms using +Like the `example-std` application, the board has a controller component which handles pings and +test events. A test event can be triggered with `--test-event`. + +The green LED blinks every 0.5 seconds as a heartbeat. The red and the orange LED are controlled +with a mode. For example, you can let both toggle together every 200 ms using ```sh -cargo run --bin stm32h7-client -- --set-led-frequency 500 +cargo run -p client -- led --mode unified-toggle --toggle-period-ms 200 ``` +Use `cargo run -p client -- led --help` to list all modes. + You can also pass the board address with `--udp-addr` instead of setting it inside the configuration file. diff --git a/examples/stm32h7-nucleo-embassy/src/lib.rs b/examples/stm32h7-nucleo-embassy/src/lib.rs index b5b3332..8ea1320 100644 --- a/examples/stm32h7-nucleo-embassy/src/lib.rs +++ b/examples/stm32h7-nucleo-embassy/src/lib.rs @@ -5,6 +5,23 @@ use defmt_rtt as _; use embassy_stm32 as _; use panic_probe as _; +use core::mem::MaybeUninit; +use embedded_alloc::LlffHeap as Heap; + +const HEAP_SIZE: usize = 131_072; + +// Part of the library, because all binaries depend on crates which require an allocator. +#[global_allocator] +static HEAP: Heap = Heap::empty(); + +/// # Safety +/// +/// Must be called exactly once, before the first allocation. +pub unsafe fn init_heap() { + static mut HEAP_MEM: [MaybeUninit; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE]; + unsafe { HEAP.init(&raw mut HEAP_MEM as usize, HEAP_SIZE) } +} + // same panicking *behavior* as `panic-probe` but doesn't print a panic message // this prevents the panic message being printed *twice* when `defmt::panic` is invoked #[defmt::panic_handler] diff --git a/examples/stm32h7-nucleo-embassy/src/main.rs b/examples/stm32h7-nucleo-embassy/src/main.rs index f00bc79..6ca4346 100644 --- a/examples/stm32h7-nucleo-embassy/src/main.rs +++ b/examples/stm32h7-nucleo-embassy/src/main.rs @@ -2,42 +2,36 @@ #![no_std] extern crate alloc; -use core::mem::MaybeUninit; -use core::sync::atomic::{AtomicU32, Ordering}; - use alloc::vec::Vec; use arbitrary_int::u14; +use defmt::Debug2Format; use embassy_executor::Spawner; -use embassy_futures::select::{Either3, select3}; +use embassy_futures::select::{Either, Either3, select, select3}; use embassy_net::StackResources; use embassy_net::udp::{PacketMetadata, UdpSocket}; use embassy_stm32::{bind_interrupts, eth, gpio, peripherals, rng}; -use embassy_sync::blocking_mutex::raw::NoopRawMutex; +use embassy_sync::blocking_mutex::raw::{CriticalSectionRawMutex, NoopRawMutex}; use embassy_sync::channel::{Channel, Receiver, Sender}; -use embassy_time::Timer; -use embedded_alloc::LlffHeap as Heap; -use embedded_types::{TmHeader, create_tm_packet, stm32h7, tm_size}; -use spacepackets::{CcsdsPacketCreationError, CcsdsPacketIdAndPsc, CcsdsPacketReader, SpHeader}; +use embassy_sync::signal::Signal; +use embassy_time::{Duration, Timer}; +use spacepackets::{CcsdsPacketIdAndPsc, CcsdsPacketReader, SpHeader}; use static_cell::{ConstStaticCell, StaticCell}; -// global logger + panicking-behavior + memory layout -use stm32h7_nucleo_embassy as _; +use types::ccsds::{CcsdsCreationError, CcsdsTmPacketOwned}; +use types::{Apid, ComponentId, Message, TcHeader, TmHeader, control, led, tmtc}; -const DEFAULT_BLINK_FREQ_MS: u32 = 1000; +const HEARTBEAT_PERIOD: Duration = Duration::from_millis(500); +const DEFAULT_LED_MODE: led::Mode = + led::Mode::AlternatingToggle(core::time::Duration::from_millis(1000)); const PORT: u16 = 7301; const MTU: usize = 1500; -const HEAP_SIZE: usize = 131_072; - -#[global_allocator] -static HEAP: Heap = Heap::empty(); - /// Locally administered MAC address const MAC_ADDRESS: [u8; 6] = [0x02, 0x00, 0x11, 0x22, 0x33, 0x44]; const TC_QUEUE_DEPTH: usize = 32; const TM_QUEUE_DEPTH: usize = 32; -static BLINK_FREQ_MS: AtomicU32 = AtomicU32::new(DEFAULT_BLINK_FREQ_MS); +static LED_MODE: Signal = Signal::new(); bind_interrupts!(struct Irqs { ETH => eth::InterruptHandler; @@ -55,17 +49,17 @@ type TcReceiver = Receiver<'static, NoopRawMutex, Vec, TC_QUEUE_DEPTH>; type TmSender = Sender<'static, NoopRawMutex, Vec, TM_QUEUE_DEPTH>; type TmReceiver = Receiver<'static, NoopRawMutex, Vec, TM_QUEUE_DEPTH>; -struct BlinkyLeds { - led1: gpio::Output<'static>, - led2: gpio::Output<'static>, +struct Leds { + red: gpio::Output<'static>, + orange: gpio::Output<'static>, } #[embassy_executor::main] async fn main(spawner: Spawner) { defmt::println!("Starting sat-rs demo application for the STM32H753ZIT"); - static mut HEAP_MEM: [MaybeUninit; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE]; - unsafe { HEAP.init(&raw mut HEAP_MEM as usize, HEAP_SIZE) } + // Safety: Called once, before the first allocation. + unsafe { stm32h7_nucleo_embassy::init_heap() }; let mut config = embassy_stm32::Config::default(); { @@ -91,11 +85,10 @@ async fn main(spawner: Spawner) { } let periphs = embassy_stm32::init(config); - let link_led = gpio::Output::new(periphs.PB0, gpio::Level::Low, gpio::Speed::Medium); - // Criss-cross pattern looks cooler. - let leds = BlinkyLeds { - led1: gpio::Output::new(periphs.PE1, gpio::Level::High, gpio::Speed::Medium), - led2: gpio::Output::new(periphs.PB14, gpio::Level::Low, gpio::Speed::Medium), + let green_led = gpio::Output::new(periphs.PB0, gpio::Level::Low, gpio::Speed::Medium); + let leds = Leds { + red: gpio::Output::new(periphs.PB14, gpio::Level::Low, gpio::Speed::Medium), + orange: gpio::Output::new(periphs.PE1, gpio::Level::Low, gpio::Speed::Medium), }; static PACKETS: StaticCell> = StaticCell::new(); @@ -143,22 +136,73 @@ async fn main(spawner: Spawner) { spawner.spawn(net_stack_task(runner).expect("spawning net stack task failed")); spawner.spawn( - udp_task(stack, link_led, tc_channel.sender(), tm_channel.receiver()) + udp_task(stack, tc_channel.sender(), tm_channel.receiver()) .expect("spawning UDP task failed"), ); - spawner.spawn(blinky(leds).expect("spawning blinky task failed")); + spawner.spawn(heartbeat(green_led).expect("spawning heartbeat task failed")); + spawner.spawn(led_task(leds).expect("spawning LED task failed")); spawner.spawn( - tc_handler(tc_channel.receiver(), tm_channel.sender()) - .expect("spawning TC handler task failed"), + tc_handler( + tc_channel.receiver(), + Telemetry { + tx: tm_channel.sender(), + sequence_count: u14::new(0), + }, + ) + .expect("spawning TC handler task failed"), ); } #[embassy_executor::task] -async fn blinky(mut leds: BlinkyLeds) { +async fn heartbeat(mut led: gpio::Output<'static>) { loop { - leds.led1.toggle(); - leds.led2.toggle(); - Timer::after_millis(BLINK_FREQ_MS.load(Ordering::Relaxed) as u64).await; + led.toggle(); + Timer::after(HEARTBEAT_PERIOD).await; + } +} + +/// Applies the current mode to the red and orange LED. A new mode is applied immediately. +#[embassy_executor::task] +async fn led_task(mut leds: Leds) { + let mut mode = DEFAULT_LED_MODE; + loop { + let toggle_period = match mode { + led::Mode::AllOff => { + leds.red.set_low(); + leds.orange.set_low(); + None + } + led::Mode::RedOn => { + leds.red.set_high(); + leds.orange.set_low(); + None + } + led::Mode::OrangeOn => { + leds.red.set_low(); + leds.orange.set_high(); + None + } + led::Mode::AlternatingToggle(period) => { + leds.red.toggle(); + leds.orange.set_level((!leds.red.is_set_high()).into()); + Some(period) + } + led::Mode::UnifiedToggle(period) => { + leds.red.toggle(); + leds.orange.set_level(leds.red.is_set_high().into()); + Some(period) + } + }; + mode = match toggle_period { + Some(period) => { + let period = Duration::try_from(period).unwrap_or(Duration::MAX); + match select(Timer::after(period), LED_MODE.wait()).await { + Either::First(()) => mode, + Either::Second(new_mode) => new_mode, + } + } + None => LED_MODE.wait().await, + }; } } @@ -168,12 +212,7 @@ async fn net_stack_task(mut runner: embassy_net::Runner<'static, Device>) -> ! { } #[embassy_executor::task] -async fn udp_task( - stack: embassy_net::Stack<'static>, - mut link_led: gpio::Output<'static>, - tc_tx: TcSender, - tm_rx: TmReceiver, -) { +async fn udp_task(stack: embassy_net::Stack<'static>, tc_tx: TcSender, tm_rx: TmReceiver) { // Task futures are allocated statically, so these buffers do not live on the stack. let mut rx_udp_meta = [PacketMetadata::EMPTY; 8]; let mut tx_udp_meta = [PacketMetadata::EMPTY; 8]; @@ -183,7 +222,6 @@ async fn udp_task( loop { stack.wait_link_up().await; - link_led.set_high(); defmt::info!("Network link is up"); // Ensure DHCP configuration is up before trying connect @@ -197,8 +235,12 @@ async fn udp_task( &mut tx_udp_meta, &mut tx_udp_buf, ); + if let Err(e) = udp.bind(PORT) { + defmt::error!("Failed to bind UDP socket: {}", e); + Timer::after_secs(1).await; + continue; + } defmt::info!("UDP socket bound to port {}", PORT); - udp.bind(PORT).expect("failed to bind UDP socket"); let mut remote_endpoint = None; loop { match select3( @@ -227,7 +269,6 @@ async fn udp_task( }, Either3::Third(()) => { defmt::warn!("Network link is down"); - link_led.set_low(); break; } } @@ -236,61 +277,117 @@ async fn udp_task( } #[embassy_executor::task] -async fn tc_handler(tc_rx: TcReceiver, tm_tx: TmSender) { - let mut sequence_count = u14::new(0); +async fn tc_handler(tc_rx: TcReceiver, mut telemetry: Telemetry) { loop { let tc = tc_rx.receive().await; - defmt::info!("Received from UDP client: {}", tc.as_slice()); let packet = match CcsdsPacketReader::new_with_checksum(&tc) { Ok(packet) => packet, Err(e) => { defmt::warn!("Failed to parse received TC packet: {}", e); + send_tmtc_event(&mut telemetry, tmtc::Event::InvalidTcPacket).await; continue; } }; - let tc_packet_id = CcsdsPacketIdAndPsc { + let tc_id = CcsdsPacketIdAndPsc { packet_id: packet.packet_id(), psc: packet.psc(), }; - let Ok(request) = postcard::from_bytes::(packet.packet_data()) else { - defmt::warn!("Failed to deserialize TC request"); + let Ok((tc_header, payload)) = postcard::take_from_bytes::(packet.user_data()) + else { + defmt::warn!("Failed to deserialize TC header"); + send_tmtc_event(&mut telemetry, tmtc::Event::InvalidTcHeader).await; continue; }; - let response = match request { - stm32h7::Request::Ping => { - defmt::info!("Received Ping request"); - stm32h7::Response::Ok + match tc_header.target_id { + ComponentId::Controller => handle_controller_tc(payload, tc_id, &mut telemetry).await, + ComponentId::Led => handle_led_tc(payload, tc_id, &mut telemetry).await, + target_id => { + defmt::warn!("No TC handler for target ID {}", Debug2Format(&target_id)); + send_tmtc_event(&mut telemetry, tmtc::Event::UnknownTargetId(target_id)).await; } - stm32h7::Request::ChangeBlinkFrequency(duration) => { - defmt::info!( - "Received blinky frequency change request: {} ms", - duration.as_millis() - ); - let freq_ms = u32::try_from(duration.as_millis()).unwrap_or(u32::MAX); - BLINK_FREQ_MS.store(freq_ms, Ordering::Relaxed); - stm32h7::Response::Ok - } - }; - if let Err(e) = send_tm(tc_packet_id, response, sequence_count, &tm_tx).await { - defmt::warn!("Failed to send TM response: {}", e); } - sequence_count = sequence_count.wrapping_add(u14::new(1)); } } -async fn send_tm( - tc_packet_id: CcsdsPacketIdAndPsc, - response: stm32h7::Response, - sequence_count: u14, - sender: &TmSender, -) -> Result<(), CcsdsPacketCreationError> { - let sp_header = SpHeader::new_for_unseg_tm(stm32h7::PUS_APID, sequence_count, 0); - let tm_header = TmHeader { - tc_packet_id: Some(tc_packet_id), - uptime_millis: embassy_time::Instant::now().as_millis(), - }; - let mut packet = alloc::vec![0; tm_size(&tm_header, &response)]; - create_tm_packet(&mut packet, sp_header, tm_header, response)?; - sender.send(packet).await; - Ok(()) +/// All TCs are received via UDP, so the UDP server is the sender of TMTC events. +async fn send_tmtc_event(telemetry: &mut Telemetry, event: tmtc::Event) { + telemetry.send(ComponentId::UdpServer, None, &event).await; +} + +/// The controller does not control anything yet, but handles generic requests like pings. +async fn handle_controller_tc( + payload: &[u8], + tc_id: CcsdsPacketIdAndPsc, + telemetry: &mut Telemetry, +) { + let Ok(request) = postcard::from_bytes::(payload) else { + defmt::warn!("Failed to deserialize controller request"); + return; + }; + match request { + control::request::Request::Ping => defmt::info!("Received controller ping request"), + control::request::Request::TestEvent => { + defmt::info!("Received test event request"); + let event = types::Event::ControllerEvent(control::Event::TestEvent); + telemetry.send(ComponentId::Controller, None, &event).await; + } + } + telemetry + .send( + ComponentId::Controller, + Some(tc_id), + &control::response::Response::Ok, + ) + .await; +} + +async fn handle_led_tc(payload: &[u8], tc_id: CcsdsPacketIdAndPsc, telemetry: &mut Telemetry) { + let Ok(request) = postcard::from_bytes::(payload) else { + defmt::warn!("Failed to deserialize LED request"); + return; + }; + match request { + led::request::Request::Ping => defmt::info!("Received LED ping request"), + led::request::Request::SetMode(mode) => { + defmt::info!("Received LED mode request: {}", Debug2Format(&mode)); + LED_MODE.signal(mode); + } + } + telemetry + .send(ComponentId::Led, Some(tc_id), &led::response::Response::Ok) + .await; +} + +/// Packs TM and passes it to the UDP task. +struct Telemetry { + tx: TmSender, + sequence_count: u14, +} + +impl Telemetry { + /// TM without a TC ID is sent unsolicited, for example events. + async fn send( + &mut self, + sender_id: ComponentId, + tc_id: Option, + payload: &(impl serde::Serialize + Message), + ) { + let sp_header = SpHeader::new_for_unseg_tm(Apid::Tmtc.raw_value(), self.sequence_count, 0); + let tm_header = TmHeader::new_without_timestamp( + sender_id, + ComponentId::Ground, + payload.message_type(), + tc_id, + ); + match CcsdsTmPacketOwned::new_with_serde_payload(sp_header, &tm_header, payload) + .map_err(CcsdsCreationError::from) + .and_then(|packet| packet.try_to_vec()) + { + Ok(raw_packet) => { + self.tx.send(raw_packet).await; + self.sequence_count = self.sequence_count.wrapping_add(u14::new(1)); + } + Err(e) => defmt::warn!("Failed to create TM packet: {}", Debug2Format(&e)), + } + } } diff --git a/examples/stm32h7-nucleo-rtic/Cargo.lock b/examples/stm32h7-nucleo-rtic/Cargo.lock index fb70f89..65cba1e 100644 --- a/examples/stm32h7-nucleo-rtic/Cargo.lock +++ b/examples/stm32h7-nucleo-rtic/Cargo.lock @@ -150,6 +150,16 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cobs" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd93fd2c1b27acd030440c9dbd9d14c1122aad622374fe05a670b67a4bc034be" +dependencies = [ + "heapless 0.9.3", + "thiserror", +] + [[package]] name = "const-default" version = "1.0.0" @@ -294,6 +304,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "derive-new" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cdc8d50f426189eef89dac62fabfa0abb27d5cc008f25bf4156a0203325becc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "document-features" version = "0.2.12" @@ -567,6 +588,12 @@ dependencies = [ "nb 1.1.0", ] +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + [[package]] name = "embedded-io" version = "0.6.1" @@ -635,17 +662,6 @@ dependencies = [ "embedded-storage", ] -[[package]] -name = "embedded-types" -version = "0.1.0" -dependencies = [ - "arbitrary-int", - "defmt 1.1.0", - "postcard", - "serde", - "spacepackets", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -727,9 +743,16 @@ checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4" dependencies = [ "defmt 1.1.0", "hash32 0.3.1", + "serde_core", "stable_deref_trait", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "indexmap" version = "2.14.0" @@ -858,8 +881,9 @@ version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" dependencies = [ - "cobs", - "defmt 1.1.0", + "cobs 0.3.0", + "embedded-io 0.4.0", + "embedded-io 0.6.1", "heapless 0.7.17", "serde", ] @@ -1015,6 +1039,32 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "satrs" +version = "0.3.0-alpha.3" +dependencies = [ + "arbitrary-int", + "cobs 0.5.1", + "delegate", + "derive-new", + "num_enum", + "paste", + "postcard", + "satrs-shared", + "serde", + "spacepackets", + "static_cell", + "thiserror", +] + +[[package]] +name = "satrs-shared" +version = "0.2.4" +dependencies = [ + "serde", + "spacepackets", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1178,17 +1228,40 @@ dependencies = [ "defmt 1.1.0", "defmt-rtt", "defmt-test", + "embassy-futures", "embassy-net", "embassy-stm32", "embassy-sync", "embassy-time", "embedded-alloc", - "embedded-types", "panic-probe", "postcard", "rtic", + "serde", "spacepackets", "static_cell", + "types", +] + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -1257,6 +1330,22 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "types" +version = "0.1.0" +dependencies = [ + "arbitrary-int", + "bitbybit", + "heapless 0.9.3", + "num_enum", + "postcard", + "satrs", + "serde", + "spacepackets", + "strum", + "thiserror", +] + [[package]] name = "unicode-ident" version = "1.0.24" diff --git a/examples/stm32h7-nucleo-rtic/Cargo.toml b/examples/stm32h7-nucleo-rtic/Cargo.toml index 06aa18c..769dcbe 100644 --- a/examples/stm32h7-nucleo-rtic/Cargo.toml +++ b/examples/stm32h7-nucleo-rtic/Cargo.toml @@ -13,7 +13,7 @@ name = "integration" harness = false [dependencies] -embedded-types = { path = "../embedded-types", features = ["defmt"] } +types = { path = "../types" } cortex-m = { version = "0.7", features = ["critical-section-single-core"] } arbitrary-int = "2" @@ -26,11 +26,13 @@ static_cell = "2" rtic = { version = "2", features = ["thumbv7-backend"] } spacepackets = { version = "0.18", default-features = false, features = ["defmt"] } postcard = "1" +serde = { version = "1", default-features = false } embassy-stm32 = { version = "0.6", features = ["stm32h753zi", "memory-x", "defmt", "time-driver-any"] } embassy-time = { version = "0.5", features = ["defmt-timestamp-uptime-ms", "generic-queue-16"] } embassy-net = { version = "0.9", features = ["medium-ethernet", "proto-ipv4", "tcp", "udp", "auto-icmp-echo-reply", "dhcpv4", "defmt"] } embassy-sync = "0.8" +embassy-futures = "0.1" [dev-dependencies] defmt-test = "0.5" diff --git a/examples/stm32h7-nucleo-rtic/README.md b/examples/stm32h7-nucleo-rtic/README.md index 360507b..8090e8d 100644 --- a/examples/stm32h7-nucleo-rtic/README.md +++ b/examples/stm32h7-nucleo-rtic/README.md @@ -73,30 +73,37 @@ Make sure to install this plugin first. The board is commanded via UDP on port 7301. It gets its IP address via DHCP, so it needs to be connected to a network with a DHCP server. The network configuration including the IP address is logged after startup. According to the board user manual UM2407, jumper JP6 and solder bridge SB72 -must be ON when using Ethernet. The telecommands are CCSDS space packets with a -[`postcard`](https://docs.rs/postcard) serialized payload. +must be ON when using Ethernet. The board uses the same TMTC protocol as the +[`example-std`](../example-std) application, which is defined inside the [`types`](../types) +crate. -The [`embedded-client`](../embedded-client) application is used to command the board. Set the -address of the board inside `embedded-client/config.toml`, for example: +The [`client`](../client) application is used to command the board. Set the address of the board +inside `client/config.toml`, which is created from `client/config.toml.template` on the first +build: ```toml [interface] udp_addr = "192.168.1.50:7301" ``` -Then run the client from inside the `embedded-client` directory. For example, you can send a ping -to the MCU using +For example, you can then send a ping to the MCU using ```sh -cargo run --bin stm32h7-client -- --ping +cargo run -p client -- --ping ``` -and set the LED blink frequency to 500 ms using +Like the `example-std` application, the board has a controller component which handles pings and +test events. A test event can be triggered with `--test-event`. + +The green LED blinks every 0.5 seconds as a heartbeat. The red and the orange LED are controlled +with a mode. For example, you can let both toggle together every 200 ms using ```sh -cargo run --bin stm32h7-client -- --set-led-frequency 500 +cargo run -p client -- led --mode unified-toggle --toggle-period-ms 200 ``` +Use `cargo run -p client -- led --help` to list all modes. + You can also pass the board address with `--udp-addr` instead of setting it inside the configuration file. diff --git a/examples/stm32h7-nucleo-rtic/src/lib.rs b/examples/stm32h7-nucleo-rtic/src/lib.rs index 1d92093..516e35d 100644 --- a/examples/stm32h7-nucleo-rtic/src/lib.rs +++ b/examples/stm32h7-nucleo-rtic/src/lib.rs @@ -5,6 +5,23 @@ use defmt_rtt as _; use embassy_stm32 as _; use panic_probe as _; +use core::mem::MaybeUninit; +use embedded_alloc::LlffHeap as Heap; + +const HEAP_SIZE: usize = 131_072; + +// Part of the library, because all binaries depend on crates which require an allocator. +#[global_allocator] +static HEAP: Heap = Heap::empty(); + +/// # Safety +/// +/// Must be called exactly once, before the first allocation. +pub unsafe fn init_heap() { + static mut HEAP_MEM: [MaybeUninit; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE]; + unsafe { HEAP.init(&raw mut HEAP_MEM as usize, HEAP_SIZE) } +} + // same panicking *behavior* as `panic-probe` but doesn't print a panic message // this prevents the panic message being printed *twice* when `defmt::panic` is invoked #[defmt::panic_handler] diff --git a/examples/stm32h7-nucleo-rtic/src/main.rs b/examples/stm32h7-nucleo-rtic/src/main.rs index 4c5b487..3516c2b 100644 --- a/examples/stm32h7-nucleo-rtic/src/main.rs +++ b/examples/stm32h7-nucleo-rtic/src/main.rs @@ -2,33 +2,32 @@ #![no_std] extern crate alloc; -use rtic::app; -// global logger + panicking-behavior + memory layout use embassy_stm32::bind_interrupts; -use stm32h7_nucleo_rtic as _; +use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; +use embassy_sync::signal::Signal; +use rtic::app; +use types::led; -use core::mem::MaybeUninit; -use embedded_alloc::LlffHeap as Heap; - -const DEFAULT_BLINK_FREQ_MS: u32 = 1000; +const HEARTBEAT_PERIOD: embassy_time::Duration = embassy_time::Duration::from_millis(500); +const DEFAULT_LED_MODE: led::Mode = + led::Mode::AlternatingToggle(core::time::Duration::from_millis(1000)); const PORT: u16 = 7301; -const HEAP_SIZE: usize = 131_072; - -#[global_allocator] -static HEAP: Heap = Heap::empty(); - /// Locally administered MAC address const MAC_ADDRESS: [u8; 6] = [0x02, 0x00, 0x11, 0x22, 0x33, 0x44]; const TC_QUEUE_DEPTH: usize = 32; const TM_QUEUE_DEPTH: usize = 32; +static LED_MODE: Signal = Signal::new(); + #[app(device = embassy_stm32, peripherals = false)] mod app { use super::*; use arbitrary_int::u14; + use defmt::Debug2Format; + use embassy_futures::select::{Either, select}; use embassy_net::StackResources; use embassy_net::udp::UdpSocket; use embassy_stm32::eth; @@ -39,15 +38,12 @@ mod app { use embassy_time::Duration; use embassy_time::Timer; use embassy_time::WithTimeout as _; - use embedded_types::TmHeader; - use embedded_types::create_tm_packet; - use embedded_types::stm32h7; - use embedded_types::tm_size; - use spacepackets::CcsdsPacketCreationError; use spacepackets::CcsdsPacketIdAndPsc; use spacepackets::CcsdsPacketReader; use spacepackets::SpHeader; use static_cell::StaticCell; + use types::ccsds::{CcsdsCreationError, CcsdsTmPacketOwned}; + use types::{Apid, ComponentId, Message, TcHeader, TmHeader, control, tmtc}; bind_interrupts!(struct Irqs { ETH => eth::InterruptHandler; @@ -60,17 +56,17 @@ mod app { eth::GenericPhy>, >; - struct BlinkyLeds { - led1: gpio::Output<'static>, - led2: gpio::Output<'static>, + struct Leds { + red: gpio::Output<'static>, + orange: gpio::Output<'static>, } #[local] struct Local { net_runner: embassy_net::Runner<'static, Device>, net_stack: embassy_net::Stack<'static>, - leds: BlinkyLeds, - link_led: gpio::Output<'static>, + leds: Leds, + green_led: gpio::Output<'static>, tc_rx: embassy_sync::channel::Receiver< 'static, NoopRawMutex, @@ -89,24 +85,19 @@ mod app { alloc::vec::Vec, TM_QUEUE_DEPTH, >, - tm_tx: embassy_sync::channel::Sender< - 'static, - NoopRawMutex, - alloc::vec::Vec, - TM_QUEUE_DEPTH, - >, + telemetry: Telemetry, } #[shared] - struct Shared { - sequence_count: u14, - blink_freq: embassy_time::Duration, - } + struct Shared {} #[init] fn init(_cx: init::Context) -> (Shared, Local) { defmt::println!("Starting sat-rs demo application for the STM32H753ZIT"); + // Safety: Called once, before the first allocation. + unsafe { stm32h7_nucleo_rtic::init_heap() }; + let mut config = embassy_stm32::Config::default(); { use embassy_stm32::rcc::*; @@ -131,14 +122,11 @@ mod app { } let periphs = embassy_stm32::init(config); - let link_led = gpio::Output::new(periphs.PB0, gpio::Level::Low, gpio::Speed::Medium); - let mut led1 = gpio::Output::new(periphs.PE1, gpio::Level::Low, gpio::Speed::Medium); - let mut led2 = gpio::Output::new(periphs.PB14, gpio::Level::Low, gpio::Speed::Medium); - - // Criss-cross pattern looks cooler. - led1.set_high(); - led2.set_low(); - let leds = BlinkyLeds { led1, led2 }; + let green_led = gpio::Output::new(periphs.PB0, gpio::Level::Low, gpio::Speed::Medium); + let leds = Leds { + red: gpio::Output::new(periphs.PB14, gpio::Level::Low, gpio::Speed::Medium), + orange: gpio::Output::new(periphs.PE1, gpio::Level::Low, gpio::Speed::Medium), + }; static PACKETS: StaticCell> = StaticCell::new(); // warning: Not all STM32H7 devices have the exact same pins here @@ -173,11 +161,6 @@ mod app { let (stack, runner) = embassy_net::new(device, config, RESOURCES.init(StackResources::new()), seed); - // Set up global allocator. Use AXISRAM for the heap. - #[unsafe(link_section = ".axisram")] - static mut HEAP_MEM: [MaybeUninit; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE]; - unsafe { HEAP.init(&raw mut HEAP_MEM as usize, HEAP_SIZE) } - static TC_CHANNEL: static_cell::ConstStaticCell< embassy_sync::channel::Channel, TC_QUEUE_DEPTH>, > = static_cell::ConstStaticCell::new(embassy_sync::channel::Channel::new()); @@ -194,35 +177,79 @@ mod app { net_lib_task::spawn().expect("spawning net library task failed"); net_app_task::spawn().expect("spawning net application task failed"); - blinky::spawn().expect("spawning blink task failed"); + heartbeat::spawn().expect("spawning heartbeat task failed"); + led_task::spawn().expect("spawning LED task failed"); tc_handler::spawn().expect("spawning TC handler task failed"); ( - Shared { - blink_freq: Duration::from_millis(DEFAULT_BLINK_FREQ_MS as u64), - sequence_count: u14::new(0), - }, + Shared {}, Local { - link_led, + green_led, leds, net_runner: runner, net_stack: stack, tc_tx: tc_sender, tc_rx: tc_receiver, - tm_tx: tm_sender, + telemetry: Telemetry { + tx: tm_sender, + sequence_count: u14::new(0), + }, tm_rx: tm_receiver, }, ) } - #[task(local = [leds], shared=[blink_freq])] - async fn blinky(mut cx: blinky::Context) { - let leds = cx.local.leds; + #[task(local = [green_led])] + async fn heartbeat(cx: heartbeat::Context) { loop { - leds.led1.toggle(); - leds.led2.toggle(); - let current_blink_freq = cx.shared.blink_freq.lock(|current| *current); - Timer::after_millis(current_blink_freq.as_millis()).await; + cx.local.green_led.toggle(); + Timer::after(HEARTBEAT_PERIOD).await; + } + } + + /// Applies the current mode to the red and orange LED. A new mode is applied immediately. + #[task(local = [leds])] + async fn led_task(cx: led_task::Context) { + let leds = cx.local.leds; + let mut mode = DEFAULT_LED_MODE; + loop { + let toggle_period = match mode { + led::Mode::AllOff => { + leds.red.set_low(); + leds.orange.set_low(); + None + } + led::Mode::RedOn => { + leds.red.set_high(); + leds.orange.set_low(); + None + } + led::Mode::OrangeOn => { + leds.red.set_low(); + leds.orange.set_high(); + None + } + led::Mode::AlternatingToggle(period) => { + leds.red.toggle(); + leds.orange.set_level((!leds.red.is_set_high()).into()); + Some(period) + } + led::Mode::UnifiedToggle(period) => { + leds.red.toggle(); + leds.orange.set_level(leds.red.is_set_high().into()); + Some(period) + } + }; + mode = match toggle_period { + Some(period) => { + let period = Duration::try_from(period).unwrap_or(Duration::MAX); + match select(Timer::after(period), LED_MODE.wait()).await { + Either::First(()) => mode, + Either::Second(new_mode) => new_mode, + } + } + None => LED_MODE.wait().await, + }; } } @@ -231,7 +258,7 @@ mod app { cx.local.net_runner.run().await; } - #[task(local = [net_stack, link_led, tc_tx, tm_rx])] + #[task(local = [net_stack, tc_tx, tm_rx])] async fn net_app_task(cx: net_app_task::Context) { pub const MTU: usize = 1500; @@ -254,7 +281,6 @@ mod app { loop { cx.local.net_stack.wait_link_up().await; - cx.local.link_led.set_high(); defmt::info!("Network link is up"); // Ensure DHCP configuration is up before trying connect @@ -270,13 +296,16 @@ mod app { tx_udp_meta, tx_udp_bufs, ); + if let Err(e) = udp.bind(PORT) { + defmt::error!("Failed to bind UDP socket: {}", e); + Timer::after_secs(1).await; + continue; + } defmt::info!("UDP socket bound to port {}", PORT); - udp.bind(PORT).expect("failed to bind UDP socket"); let mut remote_endpoint = None; loop { if !cx.local.net_stack.is_link_up() { defmt::warn!("Network link is down"); - cx.local.link_led.set_low(); break; } match udp @@ -311,75 +340,127 @@ mod app { } } - #[task(local = [tc_rx, tm_tx], shared=[sequence_count, blink_freq])] - async fn tc_handler(mut cx: tc_handler::Context) { + #[task(local = [tc_rx, telemetry])] + async fn tc_handler(cx: tc_handler::Context) { + let telemetry = cx.local.telemetry; loop { let tc = cx.local.tc_rx.receive().await; - - match CcsdsPacketReader::new_with_checksum(&tc) { - Ok(packet) => { - let packet_id = packet.packet_id(); - let psc = packet.psc(); - let tc_packet_id = CcsdsPacketIdAndPsc { packet_id, psc }; - if let Ok(request) = - postcard::from_bytes::(packet.packet_data()) - { - let response = match request { - stm32h7::Request::Ping => { - defmt::info!("Received Ping request"); - stm32h7::Response::Ok - } - stm32h7::Request::ChangeBlinkFrequency(duration) => { - defmt::info!( - "Received blinky frequency change request: {} ms", - duration.as_millis() - ); - cx.shared.blink_freq.lock(|current| { - *current = Duration::from_millis(duration.as_millis() as u64) - }); - stm32h7::Response::Ok - } - }; - let sequence_count = cx.shared.sequence_count.lock(|v| { - let current = *v; - *v = v.wrapping_add(u14::new(1)); - current - }); - - // Send Pong/OK response immediately. - if let Err(e) = - send_tm(tc_packet_id, response, sequence_count, cx.local.tm_tx).await - { - defmt::warn!("Failed to send TM response: {}", e); - } - } + let packet = match CcsdsPacketReader::new_with_checksum(&tc) { + Ok(packet) => packet, + Err(e) => { + defmt::warn!("Failed to parse received TC packet: {}", e); + send_tmtc_event(telemetry, tmtc::Event::InvalidTcPacket).await; + continue; + } + }; + let tc_id = CcsdsPacketIdAndPsc { + packet_id: packet.packet_id(), + psc: packet.psc(), + }; + let Ok((tc_header, payload)) = + postcard::take_from_bytes::(packet.user_data()) + else { + defmt::warn!("Failed to deserialize TC header"); + send_tmtc_event(telemetry, tmtc::Event::InvalidTcHeader).await; + continue; + }; + match tc_header.target_id { + ComponentId::Controller => handle_controller_tc(payload, tc_id, telemetry).await, + ComponentId::Led => handle_led_tc(payload, tc_id, telemetry).await, + target_id => { + defmt::warn!("No TC handler for target ID {}", Debug2Format(&target_id)); + send_tmtc_event(telemetry, tmtc::Event::UnknownTargetId(target_id)).await; } - Err(e) => defmt::warn!("Failed to parse received TC packet: {}", e,), } - defmt::info!("Received from UDP client: {}", tc.as_slice()); } } - async fn send_tm( - tc_packet_id: CcsdsPacketIdAndPsc, - response: stm32h7::Response, - current_seq_count: u14, - sender: &embassy_sync::channel::Sender< + /// All TCs are received via UDP, so the UDP server is the sender of TMTC events. + async fn send_tmtc_event(telemetry: &mut Telemetry, event: tmtc::Event) { + telemetry.send(ComponentId::UdpServer, None, &event).await; + } + + /// The controller does not control anything yet, but handles generic requests like pings. + async fn handle_controller_tc( + payload: &[u8], + tc_id: CcsdsPacketIdAndPsc, + telemetry: &mut Telemetry, + ) { + let Ok(request) = postcard::from_bytes::(payload) else { + defmt::warn!("Failed to deserialize controller request"); + return; + }; + match request { + control::request::Request::Ping => defmt::info!("Received controller ping request"), + control::request::Request::TestEvent => { + defmt::info!("Received test event request"); + let event = types::Event::ControllerEvent(control::Event::TestEvent); + telemetry.send(ComponentId::Controller, None, &event).await; + } + } + telemetry + .send( + ComponentId::Controller, + Some(tc_id), + &control::response::Response::Ok, + ) + .await; + } + + async fn handle_led_tc(payload: &[u8], tc_id: CcsdsPacketIdAndPsc, telemetry: &mut Telemetry) { + let Ok(request) = postcard::from_bytes::(payload) else { + defmt::warn!("Failed to deserialize LED request"); + return; + }; + match request { + led::request::Request::Ping => defmt::info!("Received LED ping request"), + led::request::Request::SetMode(mode) => { + defmt::info!("Received LED mode request: {}", Debug2Format(&mode)); + LED_MODE.signal(mode); + } + } + telemetry + .send(ComponentId::Led, Some(tc_id), &led::response::Response::Ok) + .await; + } + + /// Packs TM and passes it to the network task. + struct Telemetry { + tx: embassy_sync::channel::Sender< 'static, NoopRawMutex, alloc::vec::Vec, TM_QUEUE_DEPTH, >, - ) -> Result<(), CcsdsPacketCreationError> { - let sp_header = SpHeader::new_for_unseg_tm(stm32h7::PUS_APID, current_seq_count, 0); - let tm_header = TmHeader { - tc_packet_id: Some(tc_packet_id), - uptime_millis: embassy_time::Instant::now().as_millis(), - }; - let tm_size = tm_size(&tm_header, &response); - let mut packet = alloc::vec![0; tm_size]; - create_tm_packet(&mut packet, sp_header, tm_header, response)?; - sender.send(packet).await; - Ok(()) + sequence_count: u14, + } + + impl Telemetry { + /// TM without a TC ID is sent unsolicited, for example events. + async fn send( + &mut self, + sender_id: ComponentId, + tc_id: Option, + payload: &(impl serde::Serialize + Message), + ) { + let sp_header = + SpHeader::new_for_unseg_tm(Apid::Tmtc.raw_value(), self.sequence_count, 0); + let tm_header = TmHeader::new_without_timestamp( + sender_id, + ComponentId::Ground, + payload.message_type(), + tc_id, + ); + match CcsdsTmPacketOwned::new_with_serde_payload(sp_header, &tm_header, payload) + .map_err(CcsdsCreationError::from) + .and_then(|packet| packet.try_to_vec()) + { + Ok(raw_packet) => { + self.tx.send(raw_packet).await; + self.sequence_count = self.sequence_count.wrapping_add(u14::new(1)); + } + Err(e) => defmt::warn!("Failed to create TM packet: {}", Debug2Format(&e)), + } + } } } diff --git a/examples/types/src/ccsds.rs b/examples/types/src/ccsds.rs index af5ee16..132fc22 100644 --- a/examples/types/src/ccsds.rs +++ b/examples/types/src/ccsds.rs @@ -75,6 +75,8 @@ pub enum CcsdsCreationError { Postcard(#[from] postcard::Error), #[error("timestamp generation error")] Time, + #[error("user data length {0} exceeds the maximum CCSDS packet length")] + PacketTooLarge(usize), } /// Unserialized owned TM packet which can be cloned and sent around. @@ -121,10 +123,19 @@ impl CcsdsTmPacketOwned { .unwrap() } + /// Panics if the packet is too large. Use [Self::try_to_vec] where a panic is not acceptable. pub fn to_vec(&self) -> alloc::vec::Vec { - let mut buf = alloc::vec![0u8; self.len_written()]; - let len = self.write_to_bytes(&mut buf).unwrap(); + self.try_to_vec().expect("creating TM packet failed") + } + + pub fn try_to_vec(&self) -> Result, CcsdsCreationError> { + let user_data_len = + postcard::experimental::serialized_size(&self.tm_header)? + self.payload.len(); + let packet_len = ccsds_packet_len_for_user_data_len_with_checksum(user_data_len) + .ok_or(CcsdsCreationError::PacketTooLarge(user_data_len))?; + let mut buf = alloc::vec![0u8; packet_len]; + let len = self.write_to_bytes(&mut buf)?; buf.truncate(len); - buf + Ok(buf) } } diff --git a/examples/types/src/led.rs b/examples/types/src/led.rs new file mode 100644 index 0000000..20b928d --- /dev/null +++ b/examples/types/src/led.rs @@ -0,0 +1,53 @@ +//! LEDs of the embedded examples. +//! +//! One LED blinks periodically as a heartbeat. The red and the orange LED are controlled with +//! the [Mode]. + +use core::time::Duration; + +#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug, PartialEq, Eq)] +pub enum Mode { + AllOff, + RedOn, + OrangeOn, + /// The red and the orange LED toggle in turns with the given period. + AlternatingToggle(Duration), + /// The red and the orange LED toggle together with the given period. + UnifiedToggle(Duration), +} + +pub mod request { + use crate::{Message, MessageType}; + + use super::Mode; + + #[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug, PartialEq, Eq)] + pub enum Request { + Ping, + SetMode(Mode), + } + + impl Message for Request { + fn message_type(&self) -> MessageType { + match self { + Request::Ping => MessageType::Ping, + Request::SetMode(_) => MessageType::Mode, + } + } + } +} + +pub mod response { + use crate::{Message, MessageType}; + + #[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug, PartialEq, Eq)] + pub enum Response { + Ok, + } + + impl Message for Response { + fn message_type(&self) -> MessageType { + MessageType::Verification + } + } +} diff --git a/examples/types/src/lib.rs b/examples/types/src/lib.rs index a89352d..479e5e1 100644 --- a/examples/types/src/lib.rs +++ b/examples/types/src/lib.rs @@ -11,6 +11,7 @@ pub mod acs; pub mod ccsds; pub mod control; pub mod event_manager; +pub mod led; pub mod pcdu; pub mod tmtc; @@ -45,6 +46,9 @@ pub enum ComponentId { EventManager, Ground, + + /// Blinking LEDs of the embedded examples. + Led, } #[derive(Debug, PartialEq, Eq, strum::EnumIter)] @@ -107,6 +111,23 @@ impl TmHeader { timestamp: Some(stamp_buf), } } + + /// For systems without an absolute time source. + pub fn new_without_timestamp( + sender_id: ComponentId, + target_id: ComponentId, + message_type: MessageType, + tc_id: Option, + ) -> Self { + Self { + sender_id, + target_id, + message_type, + tc_id, + timestamp: None, + } + } + pub fn new_for_unsolicited_tm( sender_id: ComponentId, target_id: ComponentId, diff --git a/examples/types/tests/ccsds_len.rs b/examples/types/tests/ccsds_len.rs index 72f4e66..50b0429 100644 --- a/examples/types/tests/ccsds_len.rs +++ b/examples/types/tests/ccsds_len.rs @@ -2,7 +2,7 @@ use arbitrary_int::u11; use spacepackets::{SpacePacketHeader, time::cds::CdsTime}; use types::{ Apid, ComponentId, MessageType, TcHeader, TmHeader, - ccsds::{CcsdsTcPacketOwned, CcsdsTmPacketOwned}, + ccsds::{CcsdsCreationError, CcsdsTcPacketOwned, CcsdsTmPacketOwned}, }; #[test] @@ -47,3 +47,22 @@ fn tm_len_written_matches_actual_serialized_length() { assert_eq!(packet.len_written(), actual.len()); } + +#[test] +fn tm_try_to_vec_fails_for_oversized_payload() { + let tm_header = TmHeader::new_without_timestamp( + ComponentId::Controller, + ComponentId::Ground, + MessageType::Verification, + None, + ); + let packet = CcsdsTmPacketOwned { + sp_header: SpacePacketHeader::new_from_apid(u11::new(Apid::Tmtc as u16)), + tm_header, + payload: vec![0; u16::MAX as usize + 1], + }; + assert!(matches!( + packet.try_to_vec(), + Err(CcsdsCreationError::PacketTooLarge(_)) + )); +}