unified example types and clients

This commit is contained in:
Robin Mueller committed 2026-09-29 15:45:03 +02:00
1 parent 7b33855ba1
commit f3e5e06937
24 files changed
+906 -383

No files matched your search

+4 -4
View File
@@ -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`.
+1
View File
@@ -0,0 +1 @@
/config.toml
+2 -1
View File
@@ -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" }
+13
View File
@@ -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");
}
+7
View File
@@ -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"
+112 -2
View File
@@ -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<SocketAddr>,
#[command(subcommand)]
commands: Option<Commands>,
}
#[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<SocketAddr>,
}
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<Self> {
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<HealthStateSelect>,
}
#[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<LedModeSelect>,
/// 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::<types::acs::mgt::response::Response>(remainder);
log::info!("Received response from MGT: {:?}", response.unwrap());
}
types::ComponentId::Led => {
let response =
postcard::from_bytes::<types::led::response::Response>(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();
-1
View File
@@ -1,3 +1,2 @@
[interface]
serial_port = "/dev/ttyUSB0"
# udp_addr = "192.168.XXX.XX:7301"
@@ -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<u32>,
/// UDP address to bind to.
#[arg(short, long)]
udp_addr: Option<std::net::SocketAddr>,
}
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::<TmHeader>(packet_data);
if let Ok((tm_header, remainder)) = tm_header {
let response = postcard::from_bytes::<stm32h7::Response>(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()
}
+1 -2
View File
@@ -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<String>,
pub udp_addr: Option<SocketAddr>,
}
impl Config {
-22
View File
@@ -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 {
+102 -14
View File
@@ -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"
+2 -1
View File
@@ -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"] }
+16 -9
View File
@@ -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.
@@ -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<u8>; 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]
+180 -83
View File
@@ -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<CriticalSectionRawMutex, led::Mode> = Signal::new();
bind_interrupts!(struct Irqs {
ETH => eth::InterruptHandler;
@@ -55,17 +49,17 @@ type TcReceiver = Receiver<'static, NoopRawMutex, Vec<u8>, TC_QUEUE_DEPTH>;
type TmSender = Sender<'static, NoopRawMutex, Vec<u8>, TM_QUEUE_DEPTH>;
type TmReceiver = Receiver<'static, NoopRawMutex, Vec<u8>, 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<u8>; 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<eth::PacketQueue<4, 4>> = 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::<stm32h7::Request>(packet.packet_data()) else {
defmt::warn!("Failed to deserialize TC request");
let Ok((tc_header, payload)) = postcard::take_from_bytes::<TcHeader>(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::<control::request::Request>(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::<led::request::Request>(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<CcsdsPacketIdAndPsc>,
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)),
}
}
}
+103 -14
View File
@@ -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"
+3 -1
View File
@@ -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"
+16 -9
View File
@@ -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.
+17
View File
@@ -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<u8>; 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]
+202 -121
View File
@@ -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<CriticalSectionRawMutex, led::Mode> = 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<eth::Sma<'static, peripherals::ETH_SMA>>,
>;
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<u8>,
TM_QUEUE_DEPTH,
>,
tm_tx: embassy_sync::channel::Sender<
'static,
NoopRawMutex,
alloc::vec::Vec<u8>,
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<eth::PacketQueue<4, 4>> = 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<u8>; 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<NoopRawMutex, alloc::vec::Vec<u8>, 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::<stm32h7::Request>(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::<TcHeader>(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::<control::request::Request>(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::<led::request::Request>(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<u8>,
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<CcsdsPacketIdAndPsc>,
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)),
}
}
}
}
+14 -3
View File
@@ -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<u8> {
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<alloc::vec::Vec<u8>, 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)
}
}
+53
View File
@@ -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
}
}
}
+21
View File
@@ -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<CcsdsPacketIdAndPsc>,
) -> Self {
Self {
sender_id,
target_id,
message_type,
tc_id,
timestamp: None,
}
}
pub fn new_for_unsolicited_tm(
sender_id: ComponentId,
target_id: ComponentId,
+20 -1
View File
@@ -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(_))
));
}