Compare commits

...
Author SHA1 Message Date
muellerr 1db716b166 Merge pull request 'removed missing file' (#299) from missing-file-removal into main
Reviewed-on: #299
2026-09-29 15:56:22 +02:00
Robin Mueller 2a5b8f95d3 removed missing file 2026-09-29 15:56:03 +02:00
muellerr 26c0b042c9 Merge pull request 'remove STM32F3 Disco example' (#298) from remove-stm32f3-example into main
Reviewed-on: #298
2026-09-29 15:54:25 +02:00
Robin Mueller e103702184 remove STM32F3 Disco example
- target MCU not large enough for larger example featuring sat-rs and also does not have
  ethernet/networking support
2026-09-29 15:51:08 +02:00
muellerr 233e5abb23 Merge pull request 'unified example types and clients' (#297) from unify-example-types-client into main
Reviewed-on: #297
2026-09-29 15:46:31 +02:00
Robin Mueller f3e5e06937 unified example types and clients 2026-09-29 15:45:03 +02:00
muellerr 7b33855ba1 Merge pull request 'STM32H7 embassy example' (#296) from stm32h7-embassy into main
Reviewed-on: #296
2026-09-29 15:02:30 +02:00
Robin Mueller 8660dbae90 STM32H7 embassy example 2026-09-29 15:01:30 +02:00
muellerr 394ffec6c9 Merge pull request 'Reuse simulator' (#295) from reuse-simulator into main
Reviewed-on: #295
2026-09-29 14:22:52 +02:00
Robin Mueller 7cc2ff3ff9 first step in unifying the examples: one example folder
One example folder for all examples, including std and embedded examples
2026-09-29 13:39:39 +02:00
muellerr bb8decfc74 Merge pull request 'MGT fdir' (#292) from mgt-fdir into main
Reviewed-on: #292
2026-09-29 11:50:39 +02:00
Robin Mueller 23ac7cf0ad MGT fdir 2026-09-29 11:49:37 +02:00
muellerr cb6c8b84bb Merge pull request 'switch to new nucleo H753 board' (#293) from embedded-readmes into main
Reviewed-on: #293
2026-09-24 19:39:25 +02:00
Robin Mueller 390d7aaff5 switch to new nucleo H753 board 2026-09-24 19:37:12 +02:00
149 changed files with 3708 additions and 3016 deletions

No files matched your search

+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
run: sudo apt update && sudo apt install -y libudev-dev
- run: cargo check
# Check example with static pool configuration
- run: cargo check -p satrs-example --no-default-features
- run: cargo check -p example-std --no-default-features
test:
name: Run Tests
+7 -8
View File
@@ -3,17 +3,16 @@ resolver = "2"
members = [
"satrs",
"satrs-mib",
"satrs-example",
"satrs-example/types",
"satrs-example/client",
"satrs-example/minisim",
"examples/example-std",
"examples/types",
"examples/client",
"examples/minisim",
"examples/minisim-types",
"satrs-shared",
"tmtc-utils",
"embedded-examples/embedded-client",
"embedded-examples/types",
]
exclude = [
"embedded-examples/stm32f3-disco-rtic",
"embedded-examples/stm32h7-nucleo-rtic",
"examples/stm32h7-nucleo-rtic",
"examples/stm32h7-nucleo-embassy",
]
+24 -16
View File
@@ -26,7 +26,7 @@ and [EIVE](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-
# Overview
This project currently contains following crates:
This project currently contains the following crates:
* [`satrs-book`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-book):
Primary information resource in addition to the API documentation, hosted
@@ -34,24 +34,32 @@ This project currently contains following crates:
this first before delving into the example application and the API documentation.
* [`satrs`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs):
Primary crate.
* [`satrs-example`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example):
Example of a simple example on-board software using various sat-rs components which can be run
on a host computer or on any system with a standard runtime like a Raspberry Pi.
* [`satrs-minisim`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-minisim):
Mini-Simulator based on [nexosim](https://github.com/asynchronics/nexosim) which
simulates some physical devices for the `satrs-example` application device handlers.
* [`satrs-mib`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-mib):
Components to build a mission information base from the on-board software directly.
* [`satrs-stm32f3-disco-rtic`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/embedded-examples/stm32f3-disco-rtic):
Example of a 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 STM32F3-Discovery device.
* [`satrs-stm32h-nucleo-rtic`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/embedded-examples/stm32h7-nucleo-rtic):
Example of a simple example using 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 STM32H743ZIT device.
Each project has its own `CHANGELOG.md`.
## Examples
All examples and their helper crates are located inside the
[`examples`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples) folder:
* [`example-std`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/example-std):
Example on-board software using various sat-rs components which can be run on a host computer
or on any system with a standard runtime like a Raspberry Pi.
* [`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 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, the STM32H7 examples and the `client`.
* [`stm32h7-nucleo-rtic`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/stm32h7-nucleo-rtic):
Simple example using 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 NUCLEO-H753ZI
board.
* [`stm32h7-nucleo-embassy`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/stm32h7-nucleo-embassy):
Same as `stm32h7-nucleo-rtic`, but using the [embassy](https://embassy.dev/) executor instead
of RTIC.
The library crates and the `example-std` application have their own `CHANGELOG.md`.
# Related projects
+1 -1
View File
@@ -47,7 +47,7 @@ def main():
parser.add_argument(
"-p",
"--package",
choices=["satrs", "satrs-minisim", "satrs-example"],
choices=["satrs", "minisim", "example-std"],
default="satrs",
help="Choose project to generate coverage for",
)
@@ -1,19 +0,0 @@
[package]
name = "embedded-client"
version = "0.1.0"
edition = "2024"
[dependencies]
clap = { version = "4", features = ["derive"] }
serialport = "4"
toml = "0.9"
serde = { version = "1", features = ["derive"] }
spacepackets = { version = "0.18" }
embedded-types = { path = "../types" }
tmtc-utils = { path = "../../tmtc-utils" }
postcard = { version = "1", features = ["alloc"] }
anyhow = "1"
cobs = "0.5"
fern = "0.7"
humantime = "2"
log = "0.4"
@@ -1,3 +0,0 @@
[interface]
serial_port = "/dev/ttyUSB0"
# udp_addr = "192.168.XXX.XX:7301"
@@ -1,71 +0,0 @@
use std::time::Duration;
use anyhow::bail;
use clap::Parser;
use cobs::CobsDecoderOwned;
use embedded_client::setup_logger;
use embedded_types::stm32f3;
use spacepackets::{CcsdsPacketCreatorOwned, CcsdsPacketReader, SpHeader};
use tmtc_utils::transport::serial::PacketTransportSerialCobs;
#[derive(Parser, Debug)]
struct Cli {
#[arg(short, long)]
ping: bool,
/// Set frequency in milliseconds.
#[arg(short, long)]
set_led_frequency: Option<u32>,
}
fn main() -> anyhow::Result<()> {
setup_logger().expect("failed to initialize logger");
println!("-- STM32F3 TMTC client --");
let cli = Cli::parse();
let config = embedded_client::Config::new_from_file();
if config.interface.serial_port.is_none() {
bail!("Serial port not specified in configuration file.");
}
let serial_port = config.interface.serial_port.as_ref().unwrap();
let serial = serialport::new(serial_port, 115200)
.open()
.expect("opening serial port failed");
let mut transport = PacketTransportSerialCobs::new(serial, CobsDecoderOwned::new(1024));
if cli.ping {
let tc = create_stm32f3_tc(&embedded_types::stm32f3::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 = stm32f3::Request::ChangeBlinkFrequency(Duration::from_millis(freq_ms as u64));
let tc = create_stm32f3_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::info!("Received packet: {:?}", reader);
})
.unwrap();
}
}
fn create_stm32f3_tc(request: &stm32f3::Request) -> CcsdsPacketCreatorOwned {
let req_raw = postcard::to_allocvec(&request).unwrap();
let sp_header = SpHeader::new_from_apid(embedded_types::stm32f3::PUS_APID);
CcsdsPacketCreatorOwned::new_tc_with_checksum(sp_header, &req_raw).unwrap()
}
@@ -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,43 +0,0 @@
use std::{fs::File, io::Read as _, net::SocketAddr, path::Path, time::SystemTime};
#[derive(Debug, serde::Deserialize)]
pub struct Config {
pub interface: Interface,
}
#[derive(Debug, serde::Deserialize)]
pub struct Interface {
pub serial_port: Option<String>,
pub udp_addr: Option<SocketAddr>,
}
impl Config {
pub fn new_from_file() -> Self {
let mut config_file =
File::open(Path::new("config.toml")).expect("opening config.toml file failed");
let mut toml_str = String::new();
config_file
.read_to_string(&mut toml_str)
.expect("reading config.toml file failed");
let config: Config = toml::from_str(&toml_str).expect("parsing config.toml file failed");
config
}
}
pub fn setup_logger() -> Result<(), fern::InitError> {
fern::Dispatch::new()
.format(|out, message, record| {
out.finish(format_args!(
"[{} {} {}] {}",
humantime::format_rfc3339_seconds(SystemTime::now()),
record.level(),
record.target(),
message
))
})
.level(log::LevelFilter::Info)
.chain(std::io::stdout())
.chain(fern::log_file("output.log")?)
.apply()?;
Ok(())
}
@@ -1,37 +0,0 @@
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
# uncomment ONE of these three option to make `cargo run` start a GDB session
# which option to pick depends on your system
# You can also replace openocd.gdb by jlink.gdb when using a J-Link.
# runner = "arm-none-eabi-gdb -q -x openocd.gdb"
# runner = "gdb-multiarch -q -x openocd.gdb"
# runner = "gdb -q -x openocd.gdb"
runner = "probe-rs run --chip STM32F303VCTx"
rustflags = [
"-C", "linker=flip-link",
# LLD (shipped with the Rust toolchain) is used as the default linker
"-C", "link-arg=-Tlink.x",
"-C", "link-arg=-Tdefmt.x",
# This is needed if your flash or ram addresses are not aligned to 0x10000 in memory.x
# See https://github.com/rust-embedded/cortex-m-quickstart/pull/95
"-C", "link-arg=--nmagic",
# if you run into problems with LLD switch to the GNU linker by commenting out
# this line
# "-C", "linker=arm-none-eabi-ld",
# if you need to link to pre-compiled C libraries provided by a C toolchain
# use GCC as the linker by commenting out both lines above and then
# uncommenting the three lines below
# "-C", "linker=arm-none-eabi-gcc",
# "-C", "link-arg=-Wl,-Tlink.x",
# "-C", "link-arg=-nostartfiles",
]
[build]
# comment out the following line if you intend to run unit tests on host machine
target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU)
[env]
DEFMT_LOG = "info"
@@ -1,64 +0,0 @@
[package]
name = "satrs-stm32f3-disco-rtic"
version = "0.1.0"
edition = "2021"
default-run = "satrs-stm32f3-disco-rtic"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
embedded-types = { path = "../types", features = ["defmt"] }
cortex-m = { version = "0.7", features = ["critical-section-single-core"] }
cortex-m-rt = "0.7"
defmt = "1"
defmt-rtt = { version = "1" }
panic-probe = { version = "1", features = ["print-defmt"] }
embedded-hal = "1"
cortex-m-semihosting = "0.5.0"
embassy-stm32 = { version = "0.6", features = ["defmt", "stm32f303vc", "memory-x", "unstable-pac", "time-driver-any"] }
embassy-time = { version = "0.5", features = ["defmt", "generic-queue-16", "defmt-timestamp-uptime-ms"]}
enumset = "1"
heapless = "0.9"
embassy-sync = "0.8"
spacepackets = { version = "0.18", default-features = false, features = ["defmt", "serde"] }
static_cell = "2"
cobs = { version = "0.5", default-features = false, features = ["defmt"] }
postcard = { version = "1" }
arbitrary-int = "2"
thiserror = { version = "2", default-features = false }
serde = { version = "1", default-features = false, features = ["derive"] }
rtic = { version = "2", features = ["thumbv7-backend"] }
rtic-sync = { version = "1" }
[dev-dependencies]
defmt-test = "0.5"
# cargo test
[profile.test]
codegen-units = 1
debug = 2
debug-assertions = true # <-
incremental = false
opt-level = "s" # <-
overflow-checks = true # <-
# cargo build/run --release
[profile.release]
codegen-units = 1
debug = 2
debug-assertions = false # <-
incremental = false
lto = 'fat'
opt-level = "s" # <-
overflow-checks = false # <-
# cargo test --release
[profile.bench]
codegen-units = 1
debug = 2
debug-assertions = false # <-
incremental = false
lto = 'fat'
opt-level = "s" # <-
overflow-checks = false # <-
@@ -1,114 +0,0 @@
sat-rs example for the STM32F3-Discovery board
=======
This example application shows how the [sat-rs library](https://egit.irs.uni-stuttgart.de/rust/sat-rs)
can be used on an embedded target.
It also shows how a relatively simple OBSW could be built when no standard runtime is available.
It uses [RTIC](https://rtic.rs/2/book/en/) as the concurrency framework and the
[defmt](https://defmt.ferrous-systems.com/) framework for logging.
The STM32F3-Discovery device was picked because it is a cheap Cortex-M4 based device which is also
used by the [Rust Embedded Book](https://docs.rust-embedded.org/book/intro/hardware.html) and the
[Rust Discovery](https://docs.rust-embedded.org/discovery/f3discovery/) book as an introduction
to embedded Rust.
## Pre-Requisites
Make sure the following tools are installed:
1. [`probe-rs`](https://probe.rs/): Application used to flash and debug the MCU.
2. Optional and recommended: [VS Code](https://code.visualstudio.com/) with
[probe-rs plugin](https://marketplace.visualstudio.com/items?itemName=probe-rs.probe-rs-debugger)
for debugging.
## Preparing Rust and the repository
Building an application requires the `thumbv7em-none-eabihf` cross-compiler toolchain.
If you have not installed it yet, you can do so with
```sh
rustup target add thumbv7em-none-eabihf
```
A default `.cargo` config file is provided for this project, but needs to be copied to have
the correct name. This is so that the config file can be updated or edited for custom needs
without being tracked by git.
```sh
cp def_config.toml config.toml
```
The configuration file will also set the target so it does not always have to be specified with
the `--target` argument.
## Building
After that, assuming that you have a `.cargo/config.toml` setting the correct build target,
you can simply build the application with
```sh
cargo build
```
## Flashing from the command line
You can flash the application from the command line using `probe-rs`:
```sh
probe-rs run --chip STM32F303VCTx
```
## Debugging with VS Code
The STM32F3-Discovery comes with an on-board ST-Link so all that is required to flash and debug
the board is a Mini-USB cable. The code in this repository was debugged using [`probe-rs`](https://probe.rs/docs/tools/debuggerA)
and the VS Code [`probe-rs` plugin](https://marketplace.visualstudio.com/items?itemName=probe-rs.probe-rs-debugger).
Make sure to install this plugin first.
Sample configuration files are provided inside the `vscode` folder.
Use `cp vscode .vscode -r` to use them for your project.
Some sample configuration files for VS Code were provided as well. You can simply use `Run` and `Debug`
to automatically rebuild and flash your application.
The `tasks.json` and `launch.json` files are generic and you can use them immediately by opening
the folder in VS code or adding it to a workspace.
## Commanding with Python
When the SW is running on the Discovery board, you can command the MCU via a serial interface,
using COBS encoded PUS packets.
It is recommended to use a virtual environment to do this. To set up one in the command line,
you can use `python3 -m venv venv` on Unix systems or `py -m venv venv` on Windows systems.
After doing this, you can check the [venv tutorial](https://docs.python.org/3/tutorial/venv.html)
on how to activate the environment and then use the following command to install the required
dependency:
```sh
pip install -r requirements.txt
```
The packets are exchanged using a dedicated serial interface. You can use any generic USB-to-UART
converter device with the TX pin connected to the PA3 pin and the RX pin connected to the PA2 pin.
A default configuration file for the python application is provided and can be used by running
```sh
cp def_tmtc_conf.json tmtc_conf.json
```
After that, you can for example send a ping to the MCU using the following command
```sh
./main.py -p /ping
```
You can configure the blinky frequency using
```sh
./main.py -p /change_blink_freq
```
All these commands will package a PUS telecommand which will be sent to the MCU using the COBS
format as the packet framing format.
@@ -1,56 +0,0 @@
#![no_main]
#![no_std]
use panic_probe as _;
use rtic::app;
#[app(device = embassy_stm32)]
mod app {
use embassy_time::Timer;
use satrs_stm32f3_disco_rtic::{Direction, LedPinSet, Leds};
#[shared]
struct Shared {}
#[local]
struct Local {
leds: Leds,
current_dir: Direction,
}
#[init]
fn init(_cx: init::Context) -> (Shared, Local) {
let p = embassy_stm32::init(Default::default());
defmt::info!("Starting sat-rs demo application for the STM32F3-Discovery using RTICv2");
let led_pin_set = LedPinSet {
pin_n: p.PE8,
pin_ne: p.PE9,
pin_e: p.PE10,
pin_se: p.PE11,
pin_s: p.PE12,
pin_sw: p.PE13,
pin_w: p.PE14,
pin_nw: p.PE15,
};
let leds = Leds::new(led_pin_set);
blinky::spawn().expect("failed to spawn blinky task");
(
Shared {},
Local {
leds,
current_dir: Direction::North,
},
)
}
#[task(local = [leds, current_dir])]
async fn blinky(cx: blinky::Context) {
loop {
cx.local.leds.blink_next(cx.local.current_dir);
Timer::after_millis(200).await;
}
}
}
@@ -1,145 +0,0 @@
#![no_main]
#![no_std]
use defmt_rtt as _;
use panic_probe as _;
use arbitrary_int::u11;
use embassy_stm32::gpio::Output;
pub const APID: u11 = u11::new(0x02);
#[derive(defmt::Format, serde::Serialize, serde::Deserialize, PartialEq, Eq, Clone, Copy)]
pub enum Direction {
North,
NorthEast,
East,
SouthEast,
South,
SouthWest,
West,
NorthWest,
}
impl Direction {
pub fn switch_to_next(&mut self) -> (Self, Self) {
let curr = *self;
*self = match self {
Direction::North => Direction::NorthEast,
Direction::NorthEast => Direction::East,
Direction::East => Direction::SouthEast,
Direction::SouthEast => Direction::South,
Direction::South => Direction::SouthWest,
Direction::SouthWest => Direction::West,
Direction::West => Direction::NorthWest,
Direction::NorthWest => Direction::North,
};
(curr, *self)
}
}
pub struct Leds {
pub north: Output<'static>,
pub north_east: Output<'static>,
pub east: Output<'static>,
pub south_east: Output<'static>,
pub south: Output<'static>,
pub south_west: Output<'static>,
pub west: Output<'static>,
pub north_west: Output<'static>,
}
impl Leds {
pub fn blink_next(&mut self, current_dir: &mut Direction) {
let (prev, curr) = current_dir.switch_to_next();
self.set_dir_low(prev);
self.set_dir_high(curr);
}
pub fn set_dir(&mut self, dir: Direction, level: embassy_stm32::gpio::Level) {
match dir {
Direction::North => self.north.set_level(level),
Direction::NorthEast => self.north_east.set_level(level),
Direction::East => self.east.set_level(level),
Direction::SouthEast => self.south_east.set_level(level),
Direction::South => self.south.set_level(level),
Direction::SouthWest => self.south_west.set_level(level),
Direction::West => self.west.set_level(level),
Direction::NorthWest => self.north_west.set_level(level),
}
}
pub fn set_dir_low(&mut self, dir: Direction) {
self.set_dir(dir, embassy_stm32::gpio::Level::Low);
}
pub fn set_dir_high(&mut self, dir: Direction) {
self.set_dir(dir, embassy_stm32::gpio::Level::High);
}
}
pub struct LedPinSet {
pub pin_n: embassy_stm32::Peri<'static, embassy_stm32::peripherals::PE8>,
pub pin_ne: embassy_stm32::Peri<'static, embassy_stm32::peripherals::PE9>,
pub pin_e: embassy_stm32::Peri<'static, embassy_stm32::peripherals::PE10>,
pub pin_se: embassy_stm32::Peri<'static, embassy_stm32::peripherals::PE11>,
pub pin_s: embassy_stm32::Peri<'static, embassy_stm32::peripherals::PE12>,
pub pin_sw: embassy_stm32::Peri<'static, embassy_stm32::peripherals::PE13>,
pub pin_w: embassy_stm32::Peri<'static, embassy_stm32::peripherals::PE14>,
pub pin_nw: embassy_stm32::Peri<'static, embassy_stm32::peripherals::PE15>,
}
impl Leds {
pub fn new(pin_set: LedPinSet) -> Self {
let led_n = Output::new(
pin_set.pin_n,
embassy_stm32::gpio::Level::Low,
embassy_stm32::gpio::Speed::Medium,
);
let led_ne = Output::new(
pin_set.pin_ne,
embassy_stm32::gpio::Level::Low,
embassy_stm32::gpio::Speed::Medium,
);
let led_e = Output::new(
pin_set.pin_e,
embassy_stm32::gpio::Level::Low,
embassy_stm32::gpio::Speed::Medium,
);
let led_se = Output::new(
pin_set.pin_se,
embassy_stm32::gpio::Level::Low,
embassy_stm32::gpio::Speed::Medium,
);
let led_s = Output::new(
pin_set.pin_s,
embassy_stm32::gpio::Level::Low,
embassy_stm32::gpio::Speed::Medium,
);
let led_sw = Output::new(
pin_set.pin_sw,
embassy_stm32::gpio::Level::Low,
embassy_stm32::gpio::Speed::Medium,
);
let led_w = Output::new(
pin_set.pin_w,
embassy_stm32::gpio::Level::Low,
embassy_stm32::gpio::Speed::Medium,
);
let led_nw = Output::new(
pin_set.pin_nw,
embassy_stm32::gpio::Level::Low,
embassy_stm32::gpio::Speed::Medium,
);
Self {
north: led_n,
north_east: led_ne,
east: led_e,
south_east: led_se,
south: led_s,
south_west: led_sw,
west: led_w,
north_west: led_nw,
}
}
}
@@ -1,336 +0,0 @@
#![no_std]
#![no_main]
use arbitrary_int::u14;
use cortex_m_semihosting::debug::{self, EXIT_FAILURE, EXIT_SUCCESS};
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embedded_types::{create_tm_packet, stm32f3, tm_size, TmHeader};
use spacepackets::{CcsdsPacketCreationError, CcsdsPacketIdAndPsc, SpHeader};
use defmt_rtt as _; // global logger
use panic_probe as _;
use rtic::app;
const UART_BAUD: u32 = 115200;
const DEFAULT_BLINK_FREQ_MS: u32 = 1000;
const MAX_TC_LEN: usize = 128;
const MAX_TM_LEN: usize = 128;
// This is the predictable maximum overhead of the COBS encoding scheme.
// It is simply the maximum packet lenght dividied by 254 rounded up.
const COBS_TM_OVERHEAD: usize = cobs::max_encoding_overhead(MAX_TM_LEN);
const TM_BUF_LEN: usize = MAX_TC_LEN + COBS_TM_OVERHEAD;
const TC_DMA_BUF_LEN: usize = 512;
type TmPacket = heapless::Vec<u8, MAX_TM_LEN>;
static TM_QUEUE: embassy_sync::channel::Channel<CriticalSectionRawMutex, TmPacket, 16> =
embassy_sync::channel::Channel::new();
#[derive(Debug, defmt::Format, thiserror::Error)]
pub enum TmSendError {
#[error("packet creation error: {0}")]
PacketCreation(#[from] CcsdsPacketCreationError),
#[error("queue error")]
Queue,
}
#[derive(Debug, defmt::Format)]
pub struct RequestWithTcId {
pub request: stm32f3::Request,
pub tc_id: CcsdsPacketIdAndPsc,
}
#[app(device = embassy_stm32)]
mod app {
use core::time::Duration;
use super::*;
use arbitrary_int::u14;
use embassy_time::Timer;
use embedded_types::stm32f3::{Request, Response};
use rtic::Mutex;
use rtic_sync::{
channel::{Receiver, Sender},
make_channel,
};
use satrs_stm32f3_disco_rtic::LedPinSet;
use spacepackets::CcsdsPacketReader;
embassy_stm32::bind_interrupts!(struct Irqs {
USART2 => embassy_stm32::usart::InterruptHandler<embassy_stm32::peripherals::USART2>;
DMA1_CHANNEL6 => embassy_stm32::dma::InterruptHandler<embassy_stm32::peripherals::DMA1_CH6>;
DMA1_CHANNEL7 => embassy_stm32::dma::InterruptHandler<embassy_stm32::peripherals::DMA1_CH7>;
});
#[shared]
struct Shared {
blink_freq: Duration,
}
#[local]
struct Local {
leds: satrs_stm32f3_disco_rtic::Leds,
current_dir: satrs_stm32f3_disco_rtic::Direction,
seq_count: u14,
tx: embassy_stm32::usart::UartTx<'static, embassy_stm32::mode::Async>,
rx: embassy_stm32::usart::RingBufferedUartRx<'static>,
}
#[init]
fn init(_cx: init::Context) -> (Shared, Local) {
static DMA_BUF: static_cell::ConstStaticCell<[u8; TC_DMA_BUF_LEN]> =
static_cell::ConstStaticCell::new([0; TC_DMA_BUF_LEN]);
let p = embassy_stm32::init(Default::default());
let (req_sender, req_receiver) = make_channel!(RequestWithTcId, 16);
defmt::info!("sat-rs demo application for the STM32F3-Discovery with RTICv2");
let led_pin_set = LedPinSet {
pin_n: p.PE8,
pin_ne: p.PE9,
pin_e: p.PE10,
pin_se: p.PE11,
pin_s: p.PE12,
pin_sw: p.PE13,
pin_w: p.PE14,
pin_nw: p.PE15,
};
let leds = satrs_stm32f3_disco_rtic::Leds::new(led_pin_set);
let mut config = embassy_stm32::usart::Config::default();
config.baudrate = UART_BAUD;
let uart = embassy_stm32::usart::Uart::new(
p.USART2, p.PA3, p.PA2, p.DMA1_CH7, p.DMA1_CH6, Irqs, config,
)
.unwrap();
let (tx, rx) = uart.split();
defmt::info!("Spawning tasks");
blinky::spawn().unwrap();
serial_tx_handler::spawn().unwrap();
serial_rx_handler::spawn(req_sender).unwrap();
req_handler::spawn(req_receiver).unwrap();
(
Shared {
blink_freq: Duration::from_millis(DEFAULT_BLINK_FREQ_MS as u64),
},
Local {
leds,
tx,
seq_count: u14::new(0),
rx: rx.into_ring_buffered(DMA_BUF.take()),
current_dir: satrs_stm32f3_disco_rtic::Direction::North,
},
)
}
#[task(local = [leds, current_dir], shared=[blink_freq])]
async fn blinky(mut cx: blinky::Context) {
loop {
cx.local.leds.blink_next(cx.local.current_dir);
let current_blink_freq = cx.shared.blink_freq.lock(|current| *current);
Timer::after_millis(current_blink_freq.as_millis() as u64).await;
}
}
#[task(
local = [
tx,
encoded_buf: [u8; TM_BUF_LEN] = [0; TM_BUF_LEN]
],
shared = [],
)]
async fn serial_tx_handler(cx: serial_tx_handler::Context) {
loop {
loop {
let vec = TM_QUEUE.receive().await;
let encoded_len =
cobs::encode_including_sentinels(&vec[0..vec.len()], cx.local.encoded_buf);
defmt::debug!("sending {} bytes over UART", encoded_len);
cx.local
.tx
.write(&cx.local.encoded_buf[0..encoded_len])
.await
.unwrap();
}
}
}
#[task(
local = [
rx,
read_buf: [u8; 128] = [0; 128],
decode_buf: [u8; MAX_TC_LEN] = [0; MAX_TC_LEN],
],
shared = [blink_freq]
)]
async fn serial_rx_handler(
cx: serial_rx_handler::Context,
mut sender: Sender<'static, RequestWithTcId, 16>,
) {
let mut decoder = cobs::CobsDecoder::new(cx.local.decode_buf);
loop {
match cx.local.rx.read(cx.local.read_buf).await {
Ok(bytes) => {
defmt::debug!("received {} bytes over UART", bytes);
for byte in cx.local.read_buf[0..bytes].iter() {
match decoder.feed(*byte) {
Ok(None) => (),
Ok(Some(packet_size)) => {
match CcsdsPacketReader::new_with_checksum(
&decoder.dest()[0..packet_size],
) {
Ok(packet) => {
let tc_packet_id =
CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet);
if let Ok(request) =
postcard::from_bytes::<Request>(packet.packet_data())
{
sender
.send(RequestWithTcId {
request,
tc_id: tc_packet_id,
})
.await
.unwrap();
}
}
Err(e) => {
defmt::error!("error unpacking ccsds packet: {}", e);
}
}
}
Err(e) => {
defmt::error!("cobs decoding error: {}", e);
}
}
}
}
Err(e) => {
defmt::error!("uart read error: {}", e);
}
}
}
}
#[task(shared = [blink_freq], local = [seq_count])]
async fn req_handler(
mut cx: req_handler::Context,
mut receiver: Receiver<'static, RequestWithTcId, 16>,
) {
loop {
match receiver.recv().await {
Ok(request_with_tc_id) => {
let tm_send_result = match request_with_tc_id.request {
Request::Ping => {
handle_ping_request(&mut cx, request_with_tc_id.tc_id).await
}
Request::ChangeBlinkFrequency(duration) => {
handle_change_blink_frequency_request(
&mut cx,
request_with_tc_id.tc_id,
duration,
)
.await
}
};
if let Err(e) = tm_send_result {
defmt::error!("error sending TM response: {}", e);
}
}
Err(_e) => defmt::error!("request receive error"),
}
}
}
async fn handle_ping_request(
cx: &mut req_handler::Context<'_>,
tc_packet_id: CcsdsPacketIdAndPsc,
) -> Result<(), TmSendError> {
defmt::info!("Received PUS ping telecommand, sending ping reply");
send_tm(tc_packet_id, Response::Ok, *cx.local.seq_count).await?;
*cx.local.seq_count = cx.local.seq_count.wrapping_add(u14::new(1));
Ok(())
}
async fn handle_change_blink_frequency_request(
cx: &mut req_handler::Context<'_>,
tc_packet_id: CcsdsPacketIdAndPsc,
duration: Duration,
) -> Result<(), TmSendError> {
defmt::info!(
"Received ChangeBlinkFrequency request, new frequency: {} ms",
duration.as_millis()
);
cx.shared
.blink_freq
.lock(|blink_freq| *blink_freq = duration);
send_tm(tc_packet_id, Response::Ok, *cx.local.seq_count).await?;
*cx.local.seq_count = cx.local.seq_count.wrapping_add(u14::new(1));
Ok(())
}
}
async fn send_tm(
tc_packet_id: CcsdsPacketIdAndPsc,
response: stm32f3::Response,
current_seq_count: u14,
) -> Result<(), TmSendError> {
let sp_header = SpHeader::new_for_unseg_tc(stm32f3::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 mut tm_packet = TmPacket::new();
let tm_size = tm_size(&tm_header, &response);
tm_packet.resize(tm_size, 0).expect("vec resize failed");
create_tm_packet(&mut tm_packet, sp_header, tm_header, response)?;
TM_QUEUE.send(tm_packet).await;
Ok(())
}
// 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]
fn panic() -> ! {
cortex_m::asm::udf()
}
/// Terminates the application and makes a semihosting-capable debug tool exit
/// with status code 0.
pub fn exit() -> ! {
loop {
debug::exit(EXIT_SUCCESS);
}
}
/// Hardfault handler.
///
/// Terminates the application and makes a semihosting-capable debug tool exit
/// with an error. This seems better than the default, which is to spin in a
/// loop.
#[cortex_m_rt::exception]
unsafe fn HardFault(_frame: &cortex_m_rt::ExceptionFrame) -> ! {
loop {
debug::exit(EXIT_FAILURE);
}
}
// defmt-test 0.3.0 has the limitation that this `#[tests]` attribute can only be used
// once within a crate. the module can be in any file but there can only be at most
// one `#[tests]` module in this library crate
#[cfg(test)]
#[defmt_test::tests]
mod unit_tests {
use defmt::assert;
#[test]
fn it_works() {
assert!(true)
}
}
@@ -1,2 +0,0 @@
/settings.json
/.cortex-debug.*
@@ -1,12 +0,0 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
// List of extensions which should be recommended for users of this workspace.
"recommendations": [
"rust-lang.rust",
"probe-rs.probe-rs-debugger"
],
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
"unwantedRecommendations": []
}
@@ -1,22 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"preLaunchTask": "${defaultBuildTask}",
"type": "probe-rs-debug",
"request": "launch",
"name": "probe-rs Debugging ",
"flashingConfig": {
"flashingEnabled": true
},
"chip": "STM32F303VCTx",
"coreConfigs": [
{
"programBinary": "${workspaceFolder}/target/thumbv7em-none-eabihf/debug/satrs-stm32f3-disco-rtic",
"rttEnabled": true,
"svdFile": "STM32F303.svd"
}
]
}
]
}
@@ -1,18 +0,0 @@
#
# Cortex-Debug extension calls this function during initialization. You can copy this
# file, modify it and specifyy it as one of the config files supplied in launch.json
# preferably at the beginning.
#
# Note that this file simply defines a function for use later when it is time to configure
# for SWO.
#
set USE_SWO 0
proc CDSWOConfigure { CDCPUFreqHz CDSWOFreqHz CDSWOOutput } {
# Alternative option: Pipe ITM output into itm.txt file
# tpiu config internal itm.txt uart off $CDCPUFreqHz
# Default option so SWO display of VS code works. Please note that this might not be required
# anymore starting at openocd v0.12.0
tpiu config internal $CDSWOOutput uart off $CDCPUFreqHz $CDSWOFreqHz
itm port 0 on
}
@@ -1,20 +0,0 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "cargo build",
"type": "shell",
"command": "~/.cargo/bin/cargo", // note: full path to the cargo
"args": [
"build"
],
"group": {
"kind": "build",
"isDefault": true
}
},
]
}
@@ -1,118 +0,0 @@
sat-rs example for the STM32H73ZI-Nucleo board
=======
This example application shows how the [sat-rs library](https://egit.irs.uni-stuttgart.de/rust/sat-rs)
can be used on an embedded target.
It also shows how a relatively simple OBSW could be built when no standard runtime is available.
It uses [RTIC](https://rtic.rs/2/book/en/) as the concurrency framework and the
[defmt](https://defmt.ferrous-systems.com/) framework for logging.
The STM32H743ZIT device was picked because it is one of the more powerful Cortex-M based devices
available for STM with which also has a little bit more RAM available and also allows commanding
via TCP/IP.
## Pre-Requisites
Make sure the following tools are installed:
1. [`probe-rs`](https://probe.rs/): Application used to flash and debug the MCU.
2. Optional and recommended: [VS Code](https://code.visualstudio.com/) with
[probe-rs plugin](https://marketplace.visualstudio.com/items?itemName=probe-rs.probe-rs-debugger)
for debugging.
## Preparing Rust and the repository
Building an application requires the `thumbv7em-none-eabihf` cross-compiler toolchain.
If you have not installed it yet, you can do so with
```sh
rustup target add thumbv7em-none-eabihf
```
A default `.cargo` config file is provided for this project, but needs to be copied to have
the correct name. This is so that the config file can be updated or edited for custom needs
without being tracked by git.
```sh
cp def_config.toml config.toml
```
The configuration file will also set the target so it does not always have to be specified with
the `--target` argument.
## Building
After that, assuming that you have a `.cargo/config.toml` setting the correct build target,
you can simply build the application with
```sh
cargo build
```
## Flashing from the command line
You can flash the application from the command line using `probe-rs`:
```sh
probe-rs run --chip STM32H743ZITx
```
## Debugging with VS Code
The STM32F3-Discovery comes with an on-board ST-Link so all that is required to flash and debug
the board is a Mini-USB cable. The code in this repository was debugged using [`probe-rs`](https://probe.rs/docs/tools/debuggerA)
and the VS Code [`probe-rs` plugin](https://marketplace.visualstudio.com/items?itemName=probe-rs.probe-rs-debugger).
Make sure to install this plugin first.
Sample configuration files are provided inside the `vscode` folder.
Use `cp vscode .vscode -r` to use them for your project.
Some sample configuration files for VS Code were provided as well. You can simply use `Run` and `Debug`
to automatically rebuild and flash your application.
The `tasks.json` and `launch.json` files are generic and you can use them immediately by opening
the folder in VS code or adding it to a workspace.
## Commanding with Python
When the SW is running on the Discovery board, you can command the MCU via a serial interface,
using COBS encoded PUS packets.
It is recommended to use a virtual environment to do this. To set up one in the command line,
you can use `python3 -m venv venv` on Unix systems or `py -m venv venv` on Windows systems.
After doing this, you can check the [venv tutorial](https://docs.python.org/3/tutorial/venv.html)
on how to activate the environment and then use the following command to install the required
dependency:
```sh
pip install -r requirements.txt
```
The packets are exchanged using a dedicated serial interface. You can use any generic USB-to-UART
converter device with the TX pin connected to the PA3 pin and the RX pin connected to the PA2 pin.
A default configuration file for the python application is provided and can be used by running
```sh
cp def_tmtc_conf.json tmtc_conf.json
```
After that, you can for example send a ping to the MCU using the following command
```sh
./main.py -p /ping
```
You can configure the blinky frequency using
```sh
./main.py -p /change_blink_freq
```
All these commands will package a PUS telecommand which will be sent to the MCU using the COBS
format as the packet framing format.
## Resources
- [STM32H743ZI Ethernet link checker example](https://github.com/stm32-rs/stm32h7xx-hal/blob/master/examples/ethernet-nucleo-h743zi2.rs)
- [smoltcp DHCP client](https://github.com/smoltcp-rs/smoltcp/blob/main/examples/dhcp_client.rs)
@@ -1,119 +0,0 @@
/* Taken from https://github.com/stm32-rs/stm32h7xx-hal/pull/299, adapted slightly to work with */
/* flip-link */
MEMORY
{
/* This file is intended for parts in the STM32H743/743v/753/753v families (RM0433), */
/* with the exception of the STM32H742/742v parts which have a different RAM layout. */
/* - FLASH and RAM are mandatory memory sections. */
/* - The sum of all non-FLASH sections must add to 1060K total device RAM. */
/* - The FLASH section size must match your device, see table below. */
/* FLASH */
/* Flash is divided in two independent banks (except 750xB). */
/* Select the appropriate FLASH size for your device. */
/* - STM32H750xB 128K (only FLASH1) */
/* - STM32H750xB 1M (512K + 512K) */
/* - STM32H743xI/753xI 2M ( 1M + 1M) */
FLASH1 : ORIGIN = 0x08000000, LENGTH = 1M
FLASH2 : ORIGIN = 0x08100000, LENGTH = 1M
/* Data TCM */
/* - Two contiguous 64KB RAMs. */
/* - Used for interrupt handlers, stacks and general RAM. */
/* - Zero wait-states. */
/* - The DTCM is taken as the origin of the base ram. (See below.) */
/* This is also where the interrupt table and such will live, */
/* which is required for deterministic performance. */
/* Need a region called RAM */
/* DTCM : ORIGIN = 0x20000000, LENGTH = 128K */
RAM : ORIGIN = 0x20000000, LENGTH = 128K
/* Instruction TCM */
/* - Used for latency-critical interrupt handlers etc. */
/* - Zero wait-states. */
ITCM : ORIGIN = 0x00000000, LENGTH = 64K
/* AXI SRAM */
/* - AXISRAM is in D1 and accessible by all system masters except BDMA. */
/* - Suitable for application data not stored in DTCM. */
/* - Zero wait-states. */
AXISRAM : ORIGIN = 0x24000000, LENGTH = 512K
/* AHB SRAM */
/* - SRAM1-3 are in D2 and accessible by all system masters except BDMA. */
/* Suitable for use as DMA buffers. */
/* - SRAM4 is in D3 and additionally accessible by the BDMA. Used for BDMA */
/* buffers, for storing application data in lower-power modes. */
/* - Zero wait-states. */
SRAM1 : ORIGIN = 0x30000000, LENGTH = 128K
SRAM2 : ORIGIN = 0x30020000, LENGTH = 128K
SRAM3 : ORIGIN = 0x30040000, LENGTH = 32K
SRAM4 : ORIGIN = 0x38000000, LENGTH = 64K
/* Backup SRAM */
BSRAM : ORIGIN = 0x38800000, LENGTH = 4K
}
/*
/* Assign the memory regions defined above for use. */
/*
/* Provide the mandatory FLASH and RAM definitions for cortex-m-rt's linker script. */
/* These do not work with flip-link */
REGION_ALIAS(FLASH, FLASH1);
/* REGION_ALIAS(RAM, DTCM); */
/* The location of the stack can be overridden using the `_stack_start` symbol. */
/* - Set the stack location at the end of RAM, using all remaining space. */
_stack_start = ORIGIN(RAM) + LENGTH(RAM);
/* The location of the .text section can be overridden using the */
/* `_stext` symbol. By default it will place after .vector_table. */
/* _stext = ORIGIN(FLASH) + 0x40c; */
/* Define sections for placing symbols into the extra memory regions above. */
/* This makes them accessible from code. */
/* - ITCM, DTCM and AXISRAM connect to a 64-bit wide bus -> align to 8 bytes. */
/* - All other memories connect to a 32-bit wide bus -> align to 4 bytes. */
SECTIONS {
.flash2 (NOLOAD) : ALIGN(4) {
*(.flash2 .flash2.*);
. = ALIGN(4);
} > FLASH2
.itcm (NOLOAD) : ALIGN(8) {
*(.itcm .itcm.*);
. = ALIGN(8);
} > ITCM
.axisram (NOLOAD) : ALIGN(8) {
*(.axisram .axisram.*);
. = ALIGN(8);
} > AXISRAM
.sram1 (NOLOAD) : ALIGN(8) {
*(.sram1 .sram1.*);
. = ALIGN(4);
} > SRAM1
.sram2 (NOLOAD) : ALIGN(8) {
*(.sram2 .sram2.*);
. = ALIGN(4);
} > SRAM2
.sram3 (NOLOAD) : ALIGN(4) {
*(.sram3 .sram3.*);
. = ALIGN(4);
} > SRAM3
.sram4 (NOLOAD) : ALIGN(4) {
*(.sram4 .sram4.*);
. = ALIGN(4);
} > SRAM4
.bsram (NOLOAD) : ALIGN(4) {
*(.bsram .bsram.*);
. = ALIGN(4);
} > BSRAM
};
@@ -1,386 +0,0 @@
#![no_main]
#![no_std]
extern crate alloc;
use rtic::app;
// global logger + panicking-behavior + memory layout
use embassy_stm32::bind_interrupts;
use satrs_stm32h7_nucleo_rtic as _;
use core::mem::MaybeUninit;
use embedded_alloc::LlffHeap as Heap;
const DEFAULT_BLINK_FREQ_MS: u32 = 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;
#[app(device = embassy_stm32, peripherals = false)]
mod app {
use super::*;
use arbitrary_int::u14;
use embassy_net::udp::UdpSocket;
use embassy_net::StackResources;
use embassy_stm32::eth;
use embassy_stm32::gpio;
use embassy_stm32::peripherals;
use embassy_stm32::rng;
use embassy_sync::blocking_mutex::raw::NoopRawMutex;
use embassy_time::Duration;
use embassy_time::Timer;
use embassy_time::WithTimeout as _;
use embedded_types::create_tm_packet;
use embedded_types::stm32h7;
use embedded_types::tm_size;
use embedded_types::TmHeader;
use spacepackets::CcsdsPacketCreationError;
use spacepackets::CcsdsPacketIdAndPsc;
use spacepackets::CcsdsPacketReader;
use spacepackets::SpHeader;
use static_cell::StaticCell;
bind_interrupts!(struct Irqs {
ETH => eth::InterruptHandler;
RNG => rng::InterruptHandler<peripherals::RNG>;
});
type Device = eth::Ethernet<
'static,
peripherals::ETH,
eth::GenericPhy<eth::Sma<'static, peripherals::ETH_SMA>>,
>;
struct BlinkyLeds {
led1: gpio::Output<'static>,
led2: 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>,
tc_rx: embassy_sync::channel::Receiver<
'static,
NoopRawMutex,
alloc::vec::Vec<u8>,
TC_QUEUE_DEPTH,
>,
tc_tx: embassy_sync::channel::Sender<
'static,
NoopRawMutex,
alloc::vec::Vec<u8>,
TC_QUEUE_DEPTH,
>,
tm_rx: embassy_sync::channel::Receiver<
'static,
NoopRawMutex,
alloc::vec::Vec<u8>,
TM_QUEUE_DEPTH,
>,
tm_tx: embassy_sync::channel::Sender<
'static,
NoopRawMutex,
alloc::vec::Vec<u8>,
TM_QUEUE_DEPTH,
>,
}
#[shared]
struct Shared {
sequence_count: u14,
blink_freq: embassy_time::Duration,
}
#[init]
fn init(_cx: init::Context) -> (Shared, Local) {
defmt::println!("Starting sat-rs demo application for the STM32H743ZIT");
let mut config = embassy_stm32::Config::default();
{
use embassy_stm32::rcc::*;
config.rcc.hsi = Some(HSIPrescaler::Div1);
config.rcc.csi = true;
config.rcc.hsi48 = Some(Default::default()); // needed for RNG
config.rcc.pll1 = Some(Pll {
source: PllSource::Hsi,
prediv: PllPreDiv::Div4,
mul: PllMul::Mul50,
fracn: None,
divp: Some(PllDiv::Div2),
divq: None,
divr: None,
});
config.rcc.sys = Sysclk::Pll1P; // 400 Mhz
config.rcc.ahb_pre = AHBPrescaler::Div2; // 200 Mhz
config.rcc.apb1_pre = APBPrescaler::Div2; // 100 Mhz
config.rcc.apb2_pre = APBPrescaler::Div2; // 100 Mhz
config.rcc.apb3_pre = APBPrescaler::Div2; // 100 Mhz
config.rcc.apb4_pre = APBPrescaler::Div2; // 100 Mhz
config.rcc.voltage_scale = VoltageScale::Scale1;
}
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.PB7, 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 };
static PACKETS: StaticCell<eth::PacketQueue<4, 4>> = StaticCell::new();
// warning: Not all STM32H7 devices have the exact same pins here
// for STM32H747XIH, replace p.PB13 for PG12
let device = eth::Ethernet::new(
PACKETS.init(eth::PacketQueue::<4, 4>::new()),
periphs.ETH,
Irqs,
periphs.PA1, // ref_clk
periphs.PA7, // CRS_DV: Carrier Sense
periphs.PC4, // RX_D0: Received Bit 0
periphs.PC5, // RX_D1: Received Bit 1
periphs.PG13, // TX_D0: Transmit Bit 0
periphs.PB13, // TX_D1: Transmit Bit 1
periphs.PG11, // TX_EN: Transmit Enable
MAC_ADDRESS,
periphs.ETH_SMA,
periphs.PA2, // mdio
periphs.PC1, // mdc
);
let config = embassy_net::Config::dhcpv4(embassy_net::DhcpConfig::default());
// Generate random seed.
let mut rng = rng::Rng::new(periphs.RNG, Irqs);
let mut seed = [0; 8];
rng.fill_bytes(&mut seed);
let seed = u64::from_le_bytes(seed);
// Init network stack
static RESOURCES: StaticCell<StackResources<3>> = StaticCell::new();
let (stack, runner) =
embassy_net::new(device, config, RESOURCES.init(StackResources::new()), seed);
// Set up global allocator. Use AXISRAM for the heap.
#[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());
let tc_channel = TC_CHANNEL.take();
let tc_sender = tc_channel.sender();
let tc_receiver = tc_channel.receiver();
static TM_CHANNEL: static_cell::ConstStaticCell<
embassy_sync::channel::Channel<NoopRawMutex, alloc::vec::Vec<u8>, TM_QUEUE_DEPTH>,
> = static_cell::ConstStaticCell::new(embassy_sync::channel::Channel::new());
let tm_channel = TM_CHANNEL.take();
let tm_sender = tm_channel.sender();
let tm_receiver = tm_channel.receiver();
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");
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),
},
Local {
link_led,
leds,
net_runner: runner,
net_stack: stack,
tc_tx: tc_sender,
tc_rx: tc_receiver,
tm_tx: tm_sender,
tm_rx: tm_receiver,
},
)
}
#[task(local = [leds], shared=[blink_freq])]
async fn blinky(mut cx: blinky::Context) {
let leds = cx.local.leds;
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;
}
}
#[task(local=[net_runner])]
async fn net_lib_task(cx: net_lib_task::Context) {
cx.local.net_runner.run().await;
}
#[task(local = [net_stack, link_led, tc_tx, tm_rx])]
async fn net_app_task(cx: net_app_task::Context) {
pub const MTU: usize = 1500;
// Ensure those are in the data section by making them static.
static RX_UDP_META: static_cell::ConstStaticCell<[embassy_net::udp::PacketMetadata; 8]> =
static_cell::ConstStaticCell::new([embassy_net::udp::PacketMetadata::EMPTY; 8]);
static TX_UDP_META: static_cell::ConstStaticCell<[embassy_net::udp::PacketMetadata; 8]> =
static_cell::ConstStaticCell::new([embassy_net::udp::PacketMetadata::EMPTY; 8]);
static TX_UDP_BUFS: static_cell::ConstStaticCell<[u8; MTU]> =
static_cell::ConstStaticCell::new([0; MTU]);
static RX_UDP_BUFS: static_cell::ConstStaticCell<[u8; MTU]> =
static_cell::ConstStaticCell::new([0; MTU]);
let rx_udp_meta = RX_UDP_META.take();
let rx_udp_bufs = RX_UDP_BUFS.take();
let tx_udp_meta = TX_UDP_META.take();
let tx_udp_bufs = TX_UDP_BUFS.take();
let mut rx_buffer = [0; MTU];
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
cx.local.net_stack.wait_config_up().await;
let config = cx.local.net_stack.config_v4();
defmt::info!("Network task initialized, config: {}", config);
let mut udp = UdpSocket::new(
cx.local.net_stack.clone(),
rx_udp_meta,
rx_udp_bufs,
tx_udp_meta,
tx_udp_bufs,
);
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
.recv_from(&mut rx_buffer)
.with_timeout(Duration::from_millis(200))
.await
{
Ok(result) => match result {
Ok((data, meta)) => {
remote_endpoint = Some(meta.endpoint);
defmt::debug!("UDP RX {}, Meta: {}", data, meta);
cx.local.tc_tx.send(rx_buffer[0..data].to_vec()).await;
}
Err(e) => {
defmt::warn!("udp receive error: {}", e);
Timer::after_millis(100).await;
}
},
Err(_e) => (),
}
if let Some(endpoint) = remote_endpoint {
while let Ok(packet) = cx.local.tm_rx.try_receive() {
match udp.send_to(&packet, endpoint).await {
Ok(_) => {
defmt::debug!("UDP TX: {} bytes to: {}", packet.len(), endpoint)
}
Err(e) => defmt::warn!("udp send error: {}", e),
}
}
}
}
}
}
#[task(local = [tc_rx, tm_tx], shared=[sequence_count, blink_freq])]
async fn tc_handler(mut cx: tc_handler::Context) {
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);
}
}
}
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<
'static,
NoopRawMutex,
alloc::vec::Vec<u8>,
TM_QUEUE_DEPTH,
>,
) -> Result<(), CcsdsPacketCreationError> {
let sp_header = SpHeader::new_for_unseg_tc(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(())
}
}
-11
View File
@@ -1,11 +0,0 @@
[package]
name = "embedded-types"
version = "0.1.0"
edition = "2024"
[dependencies]
serde = { version = "1", default-features = false }
defmt = { version = "1", optional = true }
spacepackets = { version = "0.18", default-features = false, features = ["defmt", "serde"] }
postcard = { version = "1", features = ["defmt"] }
arbitrary-int = "2"
-84
View File
@@ -1,84 +0,0 @@
#![no_std]
use spacepackets::{
CcsdsPacketCreationError, CcsdsPacketCreatorWithReservedData, CcsdsPacketIdAndPsc,
SpacePacketHeader, ccsds_packet_len_for_user_data_len_with_checksum,
};
pub mod stm32f3 {
use arbitrary_int::u11;
use core::time::Duration;
pub const PUS_APID: u11 = u11::new(0x02);
#[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,
}
}
/// 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 {
pub tc_packet_id: Option<CcsdsPacketIdAndPsc>,
pub uptime_millis: u64,
}
pub fn tm_size<Response: serde::Serialize>(tm_header: &TmHeader, response: &Response) -> usize {
ccsds_packet_len_for_user_data_len_with_checksum(
postcard::experimental::serialized_size(tm_header).unwrap()
+ postcard::experimental::serialized_size(response).unwrap(),
)
.unwrap()
}
pub fn create_tm_packet<Response: serde::Serialize>(
buf: &mut [u8],
sp_header: SpacePacketHeader,
tm_header: TmHeader,
response: Response,
) -> Result<usize, CcsdsPacketCreationError> {
let packet_data_size = postcard::experimental::serialized_size(&tm_header).unwrap()
+ postcard::experimental::serialized_size(&response).unwrap();
let mut creator =
CcsdsPacketCreatorWithReservedData::new_tm_with_checksum(sp_header, packet_data_size, buf)?;
let current_index = postcard::to_slice(&tm_header, creator.packet_data_mut())
.unwrap()
.len();
postcard::to_slice(&response, &mut creator.packet_data_mut()[current_index..]).unwrap();
Ok(creator.finish())
}
#[cfg(test)]
mod tests {}
+1
View File
@@ -0,0 +1 @@
/config.toml
@@ -8,15 +8,15 @@ clap = { version = "4", features = ["derive"] }
log = "0.4"
fern = "0.7"
humantime = "2"
serde = { version = "1" }
serde_json = "1"
serde = { version = "1", features = ["derive"] }
toml = "1"
satrs = { path = "../../satrs" }
satrs-example = { path = ".." }
satrs-minisim = { path = "../minisim" }
example-std = { path = "../example-std" }
minisim-types = { path = "../minisim-types" }
types = { path = "../types" }
spacepackets = { version = "0.18", default-features = false }
bitbybit = "2"
arbitrary-int = "2"
ctrlc = { version = "3.5" }
postcard = { version = "1" }
postcard = { version = "1", features = ["alloc"] }
anyhow = "1"
+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"
@@ -1,8 +1,8 @@
use anyhow::{Context as _, bail};
use arbitrary_int::u11;
use clap::Parser as _;
use satrs_example::config::{OBSW_SERVER_ADDR, SERVER_PORT};
use satrs_minisim::{
use example_std::config::{OBSW_SERVER_ADDR, SERVER_PORT};
use minisim_types::{
SimCtrlReply, SimCtrlRequest, SimReply, SimRequest, SimRequestWithTime, acs::mgm,
udp::SIM_CTRL_PORT,
};
@@ -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)]
@@ -189,6 +220,30 @@ struct MgtArgs {
torque: Option<types::acs::mgt::Dipole>,
#[arg(long, default_value_t = 1000)]
torque_duration_ms: u64,
/// Override the device's FDIR health state, for example to clear a `Faulty` state.
#[arg(long, value_enum)]
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)]
@@ -271,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};
@@ -295,6 +384,13 @@ fn handle_mgt_command(client: &UdpSocket, addr: SocketAddr, args: MgtArgs) -> an
};
send_mgt_request(client, addr, request);
}
if let Some(health) = args.health {
send_mgt_request(
client,
addr,
Request::Health(types::HealthRequest::SetHealth(health.into())),
);
}
Ok(())
}
@@ -373,9 +469,9 @@ fn handle_mgm_command(
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
TcHeader::new(target_id, types::MessageType::Health),
types::acs::mgm::request::Request::Health(
types::acs::mgm::request::HealthRequest::SetHealth(health.into()),
),
types::acs::mgm::request::Request::Health(types::HealthRequest::SetHealth(
health.into(),
)),
);
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
log::info!(
@@ -446,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)))?;
@@ -574,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),
}
}
@@ -611,10 +713,10 @@ fn inject_mgm_failure(target_id: types::ComponentId, fault: mgm::SpiFault) -> an
let mut reply_buf = [0u8; 4096];
let ping = SimRequestWithTime::new_with_epoch_time(SimCtrlRequest::Ping);
sim_socket.send_to(&serde_json::to_vec(&ping)?, sim_addr)?;
sim_socket.send_to(&postcard::to_allocvec(&ping)?, sim_addr)?;
match sim_socket.recv(&mut reply_buf) {
Ok(len) => {
let reply: SimReply = serde_json::from_slice(&reply_buf[..len])?;
let reply: SimReply = postcard::from_bytes(&reply_buf[..len])?;
if reply != SimReply::SimCtrl(SimCtrlReply::Pong) {
bail!("unexpected reply while checking minisim connectivity: {reply:?}");
}
@@ -639,7 +741,7 @@ fn inject_mgm_failure(target_id: types::ComponentId, fault: mgm::SpiFault) -> an
id,
request: mgm::Request::SetSpiFault(fault),
});
sim_socket.send_to(&serde_json::to_vec(&request)?, sim_addr)?;
sim_socket.send_to(&postcard::to_allocvec(&request)?, sim_addr)?;
log::info!("injected SPI fault {fault:?} into minisim {target_id:?}");
Ok(())
}
@@ -753,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!(),
@@ -778,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();
File renamed without changes.
File renamed without changes.
@@ -1,9 +1,8 @@
[package]
name = "satrs-example"
name = "example-std"
version = "0.1.1"
edition = "2024"
default-run = "satrs-example"
homepage = "https://egit.irs.uni-stuttgart.de/rust/sat-rs"
default-run = "example-std"
repository = "https://egit.irs.uni-stuttgart.de/rust/sat-rs"
[dependencies]
@@ -22,15 +21,14 @@ derive-new = "0.7"
cfg-if = "1"
arbitrary-int = "2"
bitbybit = "2"
postcard = "1"
postcard = { version = "1", features = ["alloc"] }
ctrlc = "3"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
satrs = { path = "../satrs", features = ["test_util"] }
types = { path = "./types" }
satrs-minisim = { path = "./minisim" }
satrs-mib = { path = "../satrs-mib" }
satrs = { path = "../../satrs", features = ["test_util"] }
types = { path = "../types" }
minisim-types = { path = "../minisim-types" }
satrs-mib = { path = "../../satrs-mib" }
[features]
# default = ["heap_tmtc"]
File renamed without changes.
@@ -40,7 +40,7 @@ Use `cargo run -p client -- --help` to list all available commands.
## Adding the mini simulator application
This example application features a few device handlers. The
[`satrs-minisim`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example/minisim)
[`minisim`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/minisim)
can be used to simulate the physical devices managed by these device handlers.
The example application will attempt communication with the mini simulator on UDP port 7303.
@@ -51,7 +51,7 @@ return constant values or behave like ideal devices.
In summary, you can use the following command command to run the mini-simulator first:
```sh
cargo run -p satrs-minisim
cargo run -p minisim
```
and then start the example using `cargo run -p satrs-example`.
and then start the example using `cargo run -p example-std`.
File renamed without changes.
@@ -1,10 +1,10 @@
use satrs::fdir::{FaultCounterStd, FaultResponse, RecoveryEvent, RecoveryFdir};
use example_std::{HkHelperSingleSet, TimestampHelper, TmtcQueues};
use minisim_types::acs::mgm as sim_mgm;
use minisim_types::acs::mgm::{FIELD_LSB_PER_GAUSS_4_SENS, GAUSS_TO_MICROTESLA_FACTOR};
use minisim_types::{SimReply, SimRequest, SimRequestWithTime};
use satrs::fdir::{FaultCounterStd, RecoveryEvent};
use satrs::health::HealthTableMapSync;
use satrs::spacepackets::CcsdsPacketIdAndPsc;
use satrs_example::{HkHelperSingleSet, TimestampHelper, TmtcQueues};
use satrs_minisim::acs::mgm as sim_mgm;
use satrs_minisim::acs::mgm::{FIELD_LSB_PER_GAUSS_4_SENS, GAUSS_TO_MICROTESLA_FACTOR};
use satrs_minisim::{SimReply, SimRequest, SimRequestWithTime};
use std::sync::mpsc;
use std::sync::{Arc, Mutex};
use std::time::Duration;
@@ -15,6 +15,7 @@ use types::pcdu::SwitchId;
use types::{ComponentId, DeviceMode, HkRequestType, acs::mgm};
use crate::ccsds::pack_ccsds_tm_packet_for_now;
use crate::device_fdir::{DeviceFdir, FdirEvent};
use crate::device_mode::{ModeTransitionEvent, SwitchAndModeHelper};
use crate::eps::PowerSwitchHelper;
@@ -34,13 +35,6 @@ pub const Z_LOWBYTE_IDX: usize = 13;
pub const SPI_FAULT_THRESHOLD: u32 = 2;
pub const SPI_FAULT_DECREMENT_AFTER: Duration = Duration::from_secs(30);
// FDIR configuration for power cycle recoveries. The component is marked faulty if it would be
// recovered more than RECOVERY_THRESHOLD times before the counter is decremented again.
pub const RECOVERY_THRESHOLD: u32 = 2;
pub const RECOVERY_DECREMENT_AFTER: Duration = Duration::from_secs(60);
/// Time the device stays unpowered during a power cycle, so it can fully discharge.
pub const RECOVERY_OFF_DURATION: Duration = Duration::from_millis(500);
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum MgmId {
_0,
@@ -48,7 +42,7 @@ pub enum MgmId {
}
impl MgmId {
pub const fn str(&self) -> &str {
pub const fn str(&self) -> &'static str {
match self {
MgmId::_0 => "MGM 0",
MgmId::_1 => "MGM 1",
@@ -176,11 +170,13 @@ pub struct ModeLeafHelper {
///
/// This device handler includes several components beyond the scope of reading sensor values:
///
/// - FDIR handling on communication issues.
/// - FDIR helper for power cycling the device on communication issues.
/// - Event generation for certain events like communication issues.
/// - HK helper for periodic data generation.
/// - Mode leaf helper to allow integration into a full ACS mode tree
/// - The [SpiCommunication] structure models different communication interfaces to the
/// physical device.
/// - The device manages and commands its own power switch using the [SwitchAndModeHelper].
/// - The device FDIR is integrated directly into the device handler using the [DeviceFdir]
/// helper. It power cycles the device on communication issues and generates events for them.
/// - Periodic HK is generated using the [HkHelperSingleSet] helper.
/// - The device is a mode leaf in the ACS tree and has a [ModeLeafHelper] for this.
pub struct MgmHandlerLis3Mdl {
id: MgmId,
tmtc_queues: TmtcQueues,
@@ -191,9 +187,7 @@ pub struct MgmHandlerLis3Mdl {
hk_helper: HkHelperSingleSet,
switch_and_mode_helper: SwitchAndModeHelper<DeviceMode>,
mode_leaf_helper: ModeLeafHelper,
spi_fault_counter: FaultCounterStd,
fdir: RecoveryFdir<HealthTableMapSync>,
recovery_off_duration: Duration,
fdir: DeviceFdir,
event_tx: mpsc::SyncSender<(ComponentId, mgm::Event)>,
}
@@ -225,14 +219,12 @@ impl MgmHandlerLis3Mdl {
stamp_helper: TimestampHelper::default(),
hk_helper: HkHelperSingleSet::new(false, Duration::from_millis(200)),
mode_leaf_helper,
spi_fault_counter: FaultCounterStd::new(SPI_FAULT_THRESHOLD, SPI_FAULT_DECREMENT_AFTER),
fdir: RecoveryFdir::new(
id.component_id().into(),
fdir: DeviceFdir::new(
id.str(),
id.component_id(),
health_table,
RECOVERY_THRESHOLD,
RECOVERY_DECREMENT_AFTER,
FaultCounterStd::new(SPI_FAULT_THRESHOLD, SPI_FAULT_DECREMENT_AFTER),
),
recovery_off_duration: RECOVERY_OFF_DURATION,
event_tx,
}
}
@@ -253,8 +245,9 @@ impl MgmHandlerLis3Mdl {
// Handle assembly related messages.
self.handle_mode_leaf_handling();
self.fdir.periodic_operation();
self.check_needs_recovery();
self.fdir
.periodic_operation(&mut self.switch_and_mode_helper);
self.handle_fdir_events();
// Handle mode transitions first. This also takes care of recoveries required by FDIR.
if let Some(event) = self.switch_and_mode_helper.handle_mode_transition() {
@@ -266,9 +259,14 @@ impl MgmHandlerLis3Mdl {
self.handle_mode_transition_failure(tc_commander)
}
// The mode did not change for other components, so there is nothing to report.
ModeTransitionEvent::PowerCycleDone => self.handle_recovery_done(),
ModeTransitionEvent::PowerCycleDone => {
self.fdir.handle_power_cycle_done();
self.handle_fdir_events();
}
ModeTransitionEvent::PowerCycleFailed { restore_mode } => {
self.handle_recovery_failure(restore_mode)
self.fdir
.handle_power_cycle_failed(&mut self.switch_and_mode_helper, restore_mode);
self.handle_fdir_events();
}
}
}
@@ -319,7 +317,7 @@ impl MgmHandlerLis3Mdl {
},
mgm::request::Request::Health(health_request) => {
match health_request {
mgm::request::HealthRequest::SetHealth(health_state) => {
types::HealthRequest::SetHealth(health_state) => {
log::info!(
"{}: setting health to {:?} via ground command",
self.id.str(),
@@ -453,7 +451,7 @@ impl MgmHandlerLis3Mdl {
return;
}
// Successfull readout, so we can decrement the counter.
self.spi_fault_counter.try_decrement();
self.fdir.register_success();
// Simple scaling to retrieve the float value, assuming the best sensor resolution.
let mut mgm_guard = self.shared_mgm_set.lock().unwrap();
mgm_guard.x = x_raw as f32 * GAUSS_TO_MICROTESLA_FACTOR as f32 * FIELD_LSB_PER_GAUSS_4_SENS;
@@ -463,122 +461,39 @@ impl MgmHandlerLis3Mdl {
drop(mgm_guard);
}
/// Registers one SPI fault with the FDIR fault counter, invalidating the current
/// sensor set. If the failure threshold is exceeded, the device is power cycled. If it was
/// power cycled too often, the component is marked faulty and commanded off instead.
/// Registers one SPI fault with the FDIR, invalidating the current sensor set.
fn register_spi_fault(&mut self) {
log::warn!("{}: stuck-bus SPI fault", self.id.str());
self.shared_mgm_set.lock().unwrap().valid = false;
if !self.spi_fault_counter.increment_and_check() {
return;
}
match self.fdir.handle_fault() {
FaultResponse::Ignored => {
log::info!(
"{}: SPI fault threshold exceeded, but component is already faulty, \
recovering or externally controlled",
self.id.str()
);
}
FaultResponse::Recover => {
log::warn!(
"{}: SPI fault threshold exceeded, power cycling device",
self.id.str()
);
self.send_event(mgm::Event::SpiFaultThresholdExceeded);
self.check_needs_recovery();
}
FaultResponse::SetFaulty => {
log::error!(
"{}: SPI fault threshold exceeded after too many recoveries, marking \
component faulty",
self.id.str()
);
self.send_event(mgm::Event::SpiFaultThresholdExceeded);
self.send_event(mgm::Event::Recovery(RecoveryEvent::ThresholdExceeded));
self.switch_off_faulty_device();
}
self.fdir.register_fault(&mut self.switch_and_mode_helper);
self.handle_fdir_events();
}
fn handle_fdir_events(&mut self) {
while let Some(event) = self.fdir.next_event() {
let event = match event {
FdirEvent::FaultThresholdExceeded => mgm::Event::SpiFaultThresholdExceeded,
FdirEvent::Recovery(recovery_event) => {
// The device is power cycled or switched off.
if matches!(
recovery_event,
RecoveryEvent::Started | RecoveryEvent::ThresholdExceeded
) {
self.shared_mgm_set.lock().unwrap().valid = false;
}
mgm::Event::Recovery(recovery_event)
}
};
self.send_event(event);
}
}
fn switch_off_faulty_device(&mut self) {
// Do not restart an already pending Off transition, which would reset the transition
// state machine before it can finish.
if self.switch_and_mode_helper.target() != Some(DeviceMode::Off) {
log::warn!("{}: commanding device off due to fault", self.id.str());
self.start_transition(DeviceMode::Off, None);
}
}
/// Starts a power cycle if the health is [satrs::health::HealthState::NeedsRecovery]. The health is set
/// either by the FDIR or by ground.
fn check_needs_recovery(&mut self) {
if self.switch_and_mode_helper.power_cycle_active()
|| self.switch_and_mode_helper.target().is_some()
|| !self.fdir.needs_recovery()
{
return;
}
if self.mode() == DeviceMode::Off {
// Nothing to power cycle, the next switch-on is a fresh start anyway.
log::info!("{}: device is off, no recovery required", self.id.str());
self.fdir.recovery_done();
return;
}
self.start_recovery(self.mode());
}
fn start_recovery(&mut self, restore_mode: DeviceMode) {
log::warn!("{}: starting power cycle recovery", self.id.str());
self.shared_mgm_set.lock().unwrap().valid = false;
self.switch_and_mode_helper
.start_power_cycle(restore_mode, self.recovery_off_duration);
self.send_event(mgm::Event::Recovery(RecoveryEvent::Started));
}
fn handle_recovery_done(&mut self) {
log::info!("{}: power cycle recovery done", self.id.str());
// Faults registered while the device was switched off do not count anymore.
self.spi_fault_counter.clear();
self.fdir.recovery_done();
self.send_event(mgm::Event::Recovery(RecoveryEvent::Done));
}
/// A failed power cycle costs a recovery attempt like any other fault.
fn handle_recovery_failure(&mut self, restore_mode: DeviceMode) {
self.send_event(mgm::Event::Recovery(RecoveryEvent::Failed));
match self.fdir.recovery_failed() {
FaultResponse::Recover => {
log::warn!("{}: power cycle recovery failed, retrying", self.id.str());
self.start_recovery(restore_mode);
}
FaultResponse::SetFaulty => {
log::error!(
"{}: power cycle recovery failed too often, marking component faulty",
self.id.str()
);
self.send_event(mgm::Event::Recovery(RecoveryEvent::ThresholdExceeded));
self.switch_off_faulty_device();
}
// Ground changed the health during the recovery and is in charge now.
FaultResponse::Ignored => (),
}
}
/// Mode commands from ground or the parent abort a running recovery.
fn handle_mode_command(
&mut self,
target_mode: DeviceMode,
tc_commander: Option<CcsdsPacketIdAndPsc>,
) {
if self.switch_and_mode_helper.power_cycle_active() {
log::warn!(
"{}: mode command aborts power cycle recovery",
self.id.str()
);
// Otherwise, the recovery would restart right away.
self.fdir.recovery_done();
}
self.fdir.handle_mode_command(&self.switch_and_mode_helper);
self.start_transition(target_mode, tc_commander);
}
@@ -652,9 +567,9 @@ mod tests {
};
use arbitrary_int::u11;
use minisim_types::acs::mgm as sim_mgm;
use satrs::health::{HealthState, HealthTableProvider};
use satrs::spacepackets::SpacePacketHeader;
use satrs_minisim::acs::mgm as sim_mgm;
use types::{
Apid, ComponentId, TcHeader,
acs::mgm::request::HkRequest,
@@ -662,6 +577,7 @@ mod tests {
pcdu::{SwitchRequest, SwitchState, SwitchStateBinary},
};
use crate::device_fdir::RECOVERY_THRESHOLD;
use crate::eps::pcdu::{SharedSwitchSet, SwitchMap, SwitchSet};
use super::*;
@@ -734,7 +650,7 @@ mod tests {
health_table.clone(),
event_tx,
);
handler.recovery_off_duration = Duration::ZERO;
handler.fdir.recovery_off_duration = Duration::ZERO;
Self {
assembly_mode_request_tx,
mode_report_rx,
@@ -1106,7 +1022,7 @@ mod tests {
testbench.switch_to_normal();
testbench.drain_events();
testbench.drain_switch_requests();
testbench.mode_report_rx.try_iter().for_each(drop);
while testbench.mode_report_rx.try_recv().is_ok() {}
testbench.exceed_spi_fault_threshold();
testbench.test_spi_interface().next_mgm_data = sim_mgm::RawValues::default();
let call_count = testbench.test_spi_interface().call_count;
@@ -1132,7 +1048,7 @@ mod tests {
mgm::Event::Recovery(RecoveryEvent::Done),
]
));
assert_eq!(testbench.handler.spi_fault_counter.fault_count(), 0);
assert_eq!(testbench.handler.fdir.fault_count(), 0);
assert!(testbench.handler.shared_mgm_set.lock().unwrap().valid);
}
@@ -1189,7 +1105,7 @@ mod tests {
.tc_tx
.send(create_request_tc(
MgmSelect::_0,
mgm::request::Request::Health(mgm::request::HealthRequest::SetHealth(
mgm::request::Request::Health(types::HealthRequest::SetHealth(
HealthState::NeedsRecovery,
)),
))
@@ -1234,7 +1150,7 @@ mod tests {
let mut testbench = MgmTestbench::new();
testbench.switch_to_normal();
testbench.drain_events();
testbench.mode_report_rx.try_iter().for_each(drop);
while testbench.mode_report_rx.try_recv().is_ok() {}
testbench.exceed_spi_fault_threshold();
// The switch never turns on again. Every failed power cycle costs a recovery attempt.
@@ -1279,7 +1195,7 @@ mod tests {
let mut testbench = MgmTestbench::new();
testbench.switch_to_normal();
testbench.drain_events();
testbench.mode_report_rx.try_iter().for_each(drop);
while testbench.mode_report_rx.try_recv().is_ok() {}
testbench.exceed_spi_fault_threshold();
testbench.test_spi_interface().next_mgm_data = sim_mgm::RawValues::default();
@@ -1324,7 +1240,7 @@ mod tests {
.tc_tx
.send(create_request_tc(
MgmSelect::_0,
mgm::request::Request::Health(mgm::request::HealthRequest::SetHealth(
mgm::request::Request::Health(types::HealthRequest::SetHealth(
HealthState::ExternalControl,
)),
))
@@ -1343,10 +1259,10 @@ mod tests {
testbench.handler.periodic_operation();
testbench.set_switch_state(SwitchState::Off);
// Keep the device off until the parent asked for its mode.
testbench.handler.recovery_off_duration = Duration::from_secs(60);
testbench.handler.fdir.recovery_off_duration = Duration::from_secs(60);
testbench.handler.periodic_operation();
assert_eq!(testbench.handler.mode(), DeviceMode::Off);
testbench.mode_report_rx.try_iter().for_each(drop);
while testbench.mode_report_rx.try_recv().is_ok() {}
testbench
.assembly_mode_request_tx
@@ -1,7 +1,7 @@
use std::{sync::mpsc, time::Duration};
use example_std::{ModeHelper, TmtcQueues};
use satrs::spacepackets::CcsdsPacketIdAndPsc;
use satrs_example::{ModeHelper, TmtcQueues};
use types::{
ComponentId, DeviceMode,
acs::mgm_assembly::{Mode, request, response},
@@ -2,62 +2,67 @@ use std::collections::VecDeque;
use std::sync::mpsc;
use std::time::{Duration, Instant};
use example_std::{HkHelperSingleSet, TmtcQueues};
use minisim_types::acs::mgt as sim_mgt;
use minisim_types::{SimReply, SimRequest, SimRequestWithTime};
use satrs::fdir::{FaultCounterStd, RecoveryEvent};
use satrs::health::HealthTableMapSync;
use satrs::spacepackets::CcsdsPacketIdAndPsc;
use satrs_example::{HkHelperSingleSet, TmtcQueues};
use satrs_minisim::acs::mgt as sim_mgt;
use satrs_minisim::{SimReply, SimRequestWithTime};
use types::acs::mgt::{
self, HkSet,
request::{ModeRequest, Request},
response::{ModeResponse, Response},
};
use types::pcdu::SwitchId;
use types::{ComponentId, DeviceMode, HkRequestType};
use types::{ComponentId, DeviceMode, HealthRequest, HkRequestType};
use crate::ccsds::pack_ccsds_tm_packet_for_now;
use crate::device_fdir::{DeviceFdir, FdirEvent};
use crate::device_mode::{ModeTransitionEvent, SwitchAndModeHelper};
use crate::eps::PowerSwitchHelper;
// The handler blocks while waiting for a reply, so this must be well below the cycle time
// of the ACS thread.
pub const REPLY_TIMEOUT: Duration = Duration::from_millis(50);
pub const REPLY_FAULT_THRESHOLD: u32 = 3;
pub const REPLY_FAULT_DECREMENT_AFTER: Duration = Duration::from_secs(30);
/// Interface for ideal device which never fails.
#[derive(Default)]
pub struct DummyInterface {
dipole: sim_mgt::Dipole,
dipole: mgt::Dipole,
torque_end: Option<Instant>,
hk_requested: bool,
}
impl DummyInterface {
fn send(&mut self, request: sim_mgt::Request) {
match request {
fn transfer(&mut self, frame: &[u8]) -> Option<Vec<u8>> {
let reply = match sim_mgt::Request::from_frame(frame).ok()? {
sim_mgt::Request::ApplyTorque { duration, dipole } => {
self.dipole = dipole;
self.torque_end = Some(Instant::now() + duration);
sim_mgt::Reply::Ack
}
sim_mgt::Request::RequestHk => self.hk_requested = true,
}
}
fn try_recv_hk(&mut self) -> Option<sim_mgt::HkSet> {
if !std::mem::take(&mut self.hk_requested) {
return None;
}
let torquing = self.torque_end.is_some_and(|end| Instant::now() < end);
Some(sim_mgt::HkSet {
dipole: if torquing {
self.dipole
} else {
sim_mgt::Dipole::default()
},
torquing,
})
sim_mgt::Request::RequestHk => {
let torquing = self.torque_end.is_some_and(|end| Instant::now() < end);
sim_mgt::Reply::Hk(sim_mgt::HkSet {
dipole: if torquing {
self.dipole
} else {
mgt::Dipole::default()
},
torquing,
})
}
};
Some(reply.to_frame())
}
}
/// Records all requests and returns injected HK replies.
/// Records all sent frames and returns injected reply frames.
#[derive(Default)]
pub struct TestInterface {
pub sent_requests: Vec<sim_mgt::Request>,
pub hk_replies: VecDeque<sim_mgt::HkSet>,
pub sent_frames: Vec<Vec<u8>>,
pub replies: VecDeque<Vec<u8>>,
}
pub struct SimInterface {
@@ -66,20 +71,21 @@ pub struct SimInterface {
}
impl SimInterface {
fn send(&mut self, request: sim_mgt::Request) {
fn transfer(&mut self, frame: &[u8]) -> Option<Vec<u8>> {
// Replies which arrived after a previous timeout must not be mistaken for this reply.
while self.sim_reply_rx.try_recv().is_ok() {}
if let Err(e) = self
.sim_request_tx
.send(SimRequestWithTime::new_with_epoch_time(request))
.send(SimRequestWithTime::new_with_epoch_time(SimRequest::Mgt(
frame.to_vec(),
)))
{
log::error!("failed to send MGT SIM request: {e}");
return None;
}
}
fn try_recv_hk(&mut self) -> Option<sim_mgt::HkSet> {
let sim_reply = self.sim_reply_rx.try_recv().ok()?;
match sim_reply {
SimReply::Mgt(sim_mgt::Reply::Hk(hk)) => Some(hk),
_ => {
match self.sim_reply_rx.recv_timeout(REPLY_TIMEOUT).ok()? {
SimReply::Mgt(frame) => Some(frame),
sim_reply => {
log::warn!("unexpected MGT SIM reply: {sim_reply:?}");
None
}
@@ -87,6 +93,7 @@ impl SimInterface {
}
}
/// Frame based transport to the device. The handler implements the protocol on top of it.
pub enum MgtCommunication {
Dummy(DummyInterface),
Sim(SimInterface),
@@ -95,19 +102,16 @@ pub enum MgtCommunication {
}
impl MgtCommunication {
fn send(&mut self, request: sim_mgt::Request) {
/// Sends a request frame and blocks until the reply frame arrives. Returns [None] if there
/// was no reply in time.
fn transfer(&mut self, frame: &[u8]) -> Option<Vec<u8>> {
match self {
MgtCommunication::Dummy(dummy) => dummy.send(request),
MgtCommunication::Sim(sim) => sim.send(request),
MgtCommunication::Test(test) => test.sent_requests.push(request),
}
}
fn try_recv_hk(&mut self) -> Option<sim_mgt::HkSet> {
match self {
MgtCommunication::Dummy(dummy) => dummy.try_recv_hk(),
MgtCommunication::Sim(sim) => sim.try_recv_hk(),
MgtCommunication::Test(test) => test.hk_replies.pop_front(),
MgtCommunication::Dummy(dummy) => dummy.transfer(frame),
MgtCommunication::Sim(sim) => sim.transfer(frame),
MgtCommunication::Test(test) => {
test.sent_frames.push(frame.to_vec());
test.replies.pop_front()
}
}
}
}
@@ -122,8 +126,19 @@ pub struct ModeLeafHelper {
/// Magnetorquer (MGT) device handler.
///
/// The device is powered through the PCDU and only accepts torque commands in normal mode.
/// In normal mode, the device HK is polled every cycle. The replies arrive asynchronously and
/// are cached as the HK set of the handler.
/// In normal mode, the device HK is polled every cycle and cached as the HK set of the handler.
/// Every request is answered with exactly one reply. A missing, invalid or unexpected reply is
/// a fault which is handled by the FDIR.
///
/// This device handler includes several components beyond the scope of commanding the device:
///
/// - The [MgtCommunication] structure models different communication interfaces to the
/// physical device.
/// - The device manages and commands its own power switch using the [SwitchAndModeHelper].
/// - The device FDIR is integrated directly into the device handler using the [DeviceFdir]
/// helper.
/// - Periodic HK is generated using the [HkHelperSingleSet] helper.
/// - The device is a mode leaf in the ACS tree and has a [ModeLeafHelper] for this.
pub struct MgtHandler {
tmtc_queues: TmtcQueues,
pub com: MgtCommunication,
@@ -131,6 +146,7 @@ pub struct MgtHandler {
hk_helper: HkHelperSingleSet,
switch_and_mode_helper: SwitchAndModeHelper<DeviceMode>,
mode_leaf_helper: ModeLeafHelper,
fdir: DeviceFdir,
event_tx: mpsc::SyncSender<mgt::Event>,
}
@@ -141,6 +157,7 @@ impl MgtHandler {
com: MgtCommunication,
mode_leaf_helper: ModeLeafHelper,
mode_timeout: Duration,
health_table: HealthTableMapSync,
event_tx: mpsc::SyncSender<mgt::Event>,
) -> Self {
Self {
@@ -155,6 +172,12 @@ impl MgtHandler {
SwitchId::Mgt,
),
mode_leaf_helper,
fdir: DeviceFdir::new(
"MGT",
ComponentId::AcsMgt,
health_table,
FaultCounterStd::new(REPLY_FAULT_THRESHOLD, REPLY_FAULT_DECREMENT_AFTER),
),
event_tx,
}
}
@@ -168,6 +191,10 @@ impl MgtHandler {
self.handle_telecommands();
self.handle_mode_leaf_handling();
self.fdir
.periodic_operation(&mut self.switch_and_mode_helper);
self.handle_fdir_events();
if let Some(event) = self.switch_and_mode_helper.handle_mode_transition() {
match event {
ModeTransitionEvent::Reached(tc_commander) => {
@@ -176,25 +203,20 @@ impl MgtHandler {
ModeTransitionEvent::Failed(tc_commander) => {
self.handle_mode_transition_failure(tc_commander)
}
// No power cycles are started without FDIR.
ModeTransitionEvent::PowerCycleDone
| ModeTransitionEvent::PowerCycleFailed { .. } => (),
ModeTransitionEvent::PowerCycleDone => {
self.fdir.handle_power_cycle_done();
self.handle_fdir_events();
}
ModeTransitionEvent::PowerCycleFailed { restore_mode } => {
self.fdir
.handle_power_cycle_failed(&mut self.switch_and_mode_helper, restore_mode);
self.handle_fdir_events();
}
}
}
if self.ready_for_commanding() {
self.com.send(sim_mgt::Request::RequestHk);
}
while let Some(hk) = self.com.try_recv_hk() {
self.hk_set = HkSet {
valid: true,
dipole: types::acs::mgt::Dipole {
x: hk.dipole.x,
y: hk.dipole.y,
z: hk.dipole.z,
},
torquing: hk.torquing,
};
self.poll_hk();
}
if self.hk_helper.needs_generation() {
@@ -202,6 +224,54 @@ impl MgtHandler {
}
}
fn poll_hk(&mut self) {
match self.transfer(sim_mgt::Request::RequestHk) {
Some(sim_mgt::Reply::Hk(hk)) => {
self.fdir.register_success();
self.hk_set = HkSet {
valid: true,
dipole: hk.dipole,
torquing: hk.torquing,
};
}
reply => self.register_reply_fault(reply),
}
}
/// Returns [None] if there was no reply in time or the reply frame was invalid.
fn transfer(&mut self, request: sim_mgt::Request) -> Option<sim_mgt::Reply> {
let frame = self.com.transfer(&request.to_frame())?;
sim_mgt::Reply::from_frame(&frame)
.inspect_err(|e| log::warn!("MGT: invalid reply frame {frame:02x?}: {e}"))
.ok()
}
fn register_reply_fault(&mut self, reply: Option<sim_mgt::Reply>) {
log::warn!("MGT: missing or unexpected reply {reply:?}");
self.hk_set.valid = false;
self.fdir.register_fault(&mut self.switch_and_mode_helper);
self.handle_fdir_events();
}
fn handle_fdir_events(&mut self) {
while let Some(event) = self.fdir.next_event() {
let event = match event {
FdirEvent::FaultThresholdExceeded => mgt::Event::ReplyFaultThresholdExceeded,
FdirEvent::Recovery(recovery_event) => {
// The device is power cycled or switched off.
if matches!(
recovery_event,
RecoveryEvent::Started | RecoveryEvent::ThresholdExceeded
) {
self.hk_set = HkSet::default();
}
mgt::Event::Recovery(recovery_event)
}
};
self.send_event(event);
}
}
fn ready_for_commanding(&self) -> bool {
self.mode() == DeviceMode::Normal && self.switch_and_mode_helper.target().is_none()
}
@@ -227,11 +297,20 @@ impl MgtHandler {
Request::Mode(ModeRequest::SetMode(mode)) => {
self.start_transition(mode, Some(tc_id))
}
Request::Mode(ModeRequest::ReadMode) => self
.send_telemetry(Some(tc_id), Response::Mode(ModeResponse::Mode(self.mode()))),
Request::Mode(ModeRequest::ReadMode) => self.send_telemetry(
Some(tc_id),
Response::Mode(ModeResponse::Mode(
self.switch_and_mode_helper.reported_mode(),
)),
),
Request::ApplyTorque { dipole, duration } => {
self.handle_torque_command(tc_id, dipole, duration)
}
Request::Health(HealthRequest::SetHealth(health)) => {
log::info!("MGT: setting health to {health:?} via ground command");
self.fdir.set_health(health);
self.send_telemetry(Some(tc_id), Response::Ok);
}
}
}
}
@@ -263,7 +342,7 @@ impl MgtHandler {
fn handle_torque_command(
&mut self,
tc_id: CcsdsPacketIdAndPsc,
dipole: types::acs::mgt::Dipole,
dipole: mgt::Dipole,
duration: Duration,
) {
if !self.ready_for_commanding() {
@@ -271,15 +350,16 @@ impl MgtHandler {
self.send_telemetry(Some(tc_id), Response::NotInNormalMode);
return;
}
self.com.send(sim_mgt::Request::ApplyTorque {
duration,
dipole: sim_mgt::Dipole {
x: dipole.x,
y: dipole.y,
z: dipole.z,
},
});
self.send_telemetry(Some(tc_id), Response::Ok);
match self.transfer(sim_mgt::Request::ApplyTorque { duration, dipole }) {
Some(sim_mgt::Reply::Ack) => {
self.fdir.register_success();
self.send_telemetry(Some(tc_id), Response::Ok);
}
reply => {
self.register_reply_fault(reply);
self.send_telemetry(Some(tc_id), Response::ReplyTimeout);
}
}
}
fn start_transition(
@@ -288,6 +368,7 @@ impl MgtHandler {
tc_commander: Option<CcsdsPacketIdAndPsc>,
) {
log::info!("MGT: transitioning to mode {:?}", target_mode);
self.fdir.handle_mode_command(&self.switch_and_mode_helper);
if target_mode == DeviceMode::Off {
self.hk_set = HkSet::default();
}
@@ -317,7 +398,9 @@ impl MgtHandler {
fn report_mode_to_parent(&self) {
self.mode_leaf_helper
.report_tx
.send(ModeResponse::Mode(self.mode()))
.send(ModeResponse::Mode(
self.switch_and_mode_helper.reported_mode(),
))
.unwrap();
}
@@ -344,6 +427,7 @@ mod tests {
use std::sync::Mutex;
use arbitrary_int::u11;
use satrs::health::{HealthState, HealthTableProvider};
use satrs::spacepackets::SpacePacketHeader;
use types::{
Apid, Message as _, TcHeader,
@@ -351,10 +435,24 @@ mod tests {
pcdu::{SwitchRequest, SwitchState, SwitchStateBinary},
};
use crate::device_fdir::RECOVERY_THRESHOLD;
use crate::eps::pcdu::{SharedSwitchSet, SwitchMap, SwitchSet};
use super::*;
impl TestInterface {
fn sent_requests(&self) -> Vec<sim_mgt::Request> {
self.sent_frames
.iter()
.map(|frame| sim_mgt::Request::from_frame(frame).unwrap())
.collect()
}
fn push_reply(&mut self, reply: sim_mgt::Reply) {
self.replies.push_back(reply.to_frame());
}
}
struct MgtTestbench {
parent_request_tx: mpsc::SyncSender<ModeRequest>,
parent_report_rx: mpsc::Receiver<ModeResponse>,
@@ -363,6 +461,7 @@ mod tests {
tc_tx: mpsc::SyncSender<CcsdsTcPacketOwned>,
tm_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
event_rx: mpsc::Receiver<mgt::Event>,
health_table: HealthTableMapSync,
handler: MgtHandler,
}
@@ -373,11 +472,12 @@ mod tests {
let (tc_tx, tc_rx) = mpsc::sync_channel(10);
let (tm_tx, tm_rx) = mpsc::sync_channel(10);
let (switch_tx, switch_rx) = mpsc::sync_channel(10);
let (event_tx, event_rx) = mpsc::sync_channel(10);
let (event_tx, event_rx) = mpsc::sync_channel(20);
let mut switch_map = SwitchMap::new();
switch_map.insert(SwitchId::Mgt, SwitchState::Off);
let shared_switch_set = SharedSwitchSet::new(Mutex::new(SwitchSet::new(switch_map)));
let handler = MgtHandler::new(
let health_table = HealthTableMapSync::default();
let mut handler = MgtHandler::new(
TmtcQueues { tc_rx, tm_tx },
PowerSwitchHelper::new(switch_tx, shared_switch_set.clone()),
MgtCommunication::Test(TestInterface::default()),
@@ -386,8 +486,10 @@ mod tests {
report_tx,
},
Duration::from_millis(100),
health_table.clone(),
event_tx,
);
handler.fdir.recovery_off_duration = Duration::ZERO;
Self {
parent_request_tx,
parent_report_rx,
@@ -396,6 +498,7 @@ mod tests {
tc_tx,
tm_rx,
event_rx,
health_table,
handler,
}
}
@@ -420,10 +523,7 @@ mod tests {
fn switch_to_normal(&mut self) {
self.send_tc(Request::Mode(ModeRequest::SetMode(DeviceMode::Normal)));
self.handler.periodic_operation();
self.shared_switch_set
.lock()
.unwrap()
.set_switch_state(SwitchId::Mgt, SwitchState::On);
self.set_switch_state(SwitchState::On);
self.handler.periodic_operation();
assert_eq!(self.handler.mode(), DeviceMode::Normal);
assert_eq!(self.next_response(), Response::Ok);
@@ -435,6 +535,42 @@ mod tests {
_ => panic!("unexpected MGT interface"),
}
}
fn set_switch_state(&self, state: SwitchState) {
self.shared_switch_set
.lock()
.unwrap()
.set_switch_state(SwitchId::Mgt, state);
}
fn health(&self) -> Option<HealthState> {
self.health_table.health(ComponentId::AcsMgt.into())
}
fn drain_events(&self) -> Vec<mgt::Event> {
self.event_rx.try_iter().collect()
}
/// No replies are injected, so every HK poll is a fault. The poll in the cycle which
/// reached normal mode already registered the first fault.
fn exceed_reply_fault_threshold(&mut self) {
for _ in 0..REPLY_FAULT_THRESHOLD {
self.handler.periodic_operation();
}
}
/// Drives a started power cycle recovery to completion, completing both power switch
/// handshakes.
fn complete_power_cycle(&mut self) {
self.handler.periodic_operation();
self.set_switch_state(SwitchState::Off);
self.handler.periodic_operation();
assert_eq!(self.handler.mode(), DeviceMode::Off);
self.handler.periodic_operation();
self.set_switch_state(SwitchState::On);
self.handler.periodic_operation();
assert_eq!(self.handler.mode(), DeviceMode::Normal);
}
}
#[test]
@@ -442,7 +578,7 @@ mod tests {
let mut testbench = MgtTestbench::new();
testbench.handler.periodic_operation();
assert_eq!(testbench.handler.mode(), DeviceMode::Off);
assert!(testbench.test_interface().sent_requests.is_empty());
assert!(testbench.test_interface().sent_frames.is_empty());
}
#[test]
@@ -455,11 +591,7 @@ mod tests {
assert_eq!(switch_request.target_state, SwitchStateBinary::On);
assert_eq!(testbench.handler.mode(), DeviceMode::Off);
testbench
.shared_switch_set
.lock()
.unwrap()
.set_switch_state(SwitchId::Mgt, SwitchState::On);
testbench.set_switch_state(SwitchState::On);
testbench.handler.periodic_operation();
assert_eq!(testbench.handler.mode(), DeviceMode::Normal);
assert_eq!(testbench.next_response(), Response::Ok);
@@ -481,11 +613,7 @@ mod tests {
.send(ModeRequest::SetMode(DeviceMode::Normal))
.unwrap();
testbench.handler.periodic_operation();
testbench
.shared_switch_set
.lock()
.unwrap()
.set_switch_state(SwitchId::Mgt, SwitchState::On);
testbench.set_switch_state(SwitchState::On);
testbench.handler.periodic_operation();
assert_eq!(
testbench.parent_report_rx.try_recv(),
@@ -504,14 +632,15 @@ mod tests {
});
testbench.handler.periodic_operation();
assert_eq!(testbench.next_response(), Response::NotInNormalMode);
assert!(testbench.test_interface().sent_requests.is_empty());
assert!(testbench.test_interface().sent_frames.is_empty());
}
#[test]
fn test_torque_command_forwarded_in_normal_mode() {
let mut testbench = MgtTestbench::new();
testbench.switch_to_normal();
testbench.test_interface().sent_requests.clear();
testbench.test_interface().sent_frames.clear();
testbench.test_interface().push_reply(sim_mgt::Reply::Ack);
testbench.send_tc(Request::ApplyTorque {
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
@@ -520,14 +649,26 @@ mod tests {
testbench.handler.periodic_operation();
assert_eq!(testbench.next_response(), Response::Ok);
assert_eq!(
testbench.test_interface().sent_requests.first(),
testbench.test_interface().sent_requests().first(),
Some(&sim_mgt::Request::ApplyTorque {
duration: Duration::from_millis(100),
dipole: sim_mgt::Dipole { x: 1, y: 2, z: 3 },
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
})
);
}
#[test]
fn test_torque_command_without_ack() {
let mut testbench = MgtTestbench::new();
testbench.switch_to_normal();
testbench.send_tc(Request::ApplyTorque {
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
duration: Duration::from_millis(100),
});
testbench.handler.periodic_operation();
assert_eq!(testbench.next_response(), Response::ReplyTimeout);
}
#[test]
fn test_hk_polling_updates_hk_set() {
let mut testbench = MgtTestbench::new();
@@ -535,17 +676,16 @@ mod tests {
assert!(
testbench
.test_interface()
.sent_requests
.sent_requests()
.contains(&sim_mgt::Request::RequestHk)
);
testbench
.test_interface()
.hk_replies
.push_back(sim_mgt::HkSet {
dipole: sim_mgt::Dipole { x: 1, y: 2, z: 3 },
.push_reply(sim_mgt::Reply::Hk(sim_mgt::HkSet {
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
torquing: true,
});
}));
testbench.handler.periodic_operation();
testbench.send_tc(Request::Hk(HkRequestType::OneShot));
testbench.handler.periodic_operation();
@@ -559,17 +699,45 @@ mod tests {
);
}
#[test]
fn test_switch_off() {
let mut testbench = MgtTestbench::new();
testbench.switch_to_normal();
testbench.drain_events();
while testbench.parent_report_rx.try_recv().is_ok() {}
testbench.send_tc(Request::Mode(ModeRequest::SetMode(DeviceMode::Off)));
testbench.handler.periodic_operation();
let switch_request = testbench
.switch_rx
.try_iter()
.last()
.expect("no switch request");
assert_eq!(switch_request.target_state, SwitchStateBinary::Off);
testbench.set_switch_state(SwitchState::Off);
testbench.handler.periodic_operation();
assert_eq!(testbench.handler.mode(), DeviceMode::Off);
assert_eq!(testbench.next_response(), Response::Ok);
assert!(matches!(
testbench.drain_events().as_slice(),
[mgt::Event::ModeChanged(DeviceMode::Off)]
));
assert_eq!(
testbench.parent_report_rx.try_recv(),
Ok(ModeResponse::Mode(DeviceMode::Off))
);
}
#[test]
fn test_hk_set_invalid_after_switch_off() {
let mut testbench = MgtTestbench::new();
testbench.switch_to_normal();
testbench
.test_interface()
.hk_replies
.push_back(sim_mgt::HkSet {
dipole: sim_mgt::Dipole::default(),
.push_reply(sim_mgt::Reply::Hk(sim_mgt::HkSet {
dipole: mgt::Dipole::default(),
torquing: false,
});
}));
testbench.handler.periodic_operation();
testbench.send_tc(Request::Mode(ModeRequest::SetMode(DeviceMode::Off)));
@@ -593,4 +761,116 @@ mod tests {
testbench.handler.periodic_operation();
assert!(testbench.tm_rx.try_recv().is_err());
}
#[test]
fn test_valid_reply_no_fault() {
let mut testbench = MgtTestbench::new();
testbench
.test_interface()
.push_reply(sim_mgt::Reply::Hk(sim_mgt::HkSet {
dipole: mgt::Dipole::default(),
torquing: false,
}));
testbench.switch_to_normal();
assert_eq!(testbench.handler.fdir.fault_count(), 0);
assert!(testbench.handler.hk_set.valid);
}
#[test]
fn test_missing_reply_registers_fault() {
let mut testbench = MgtTestbench::new();
testbench.switch_to_normal();
assert_eq!(testbench.handler.fdir.fault_count(), 1);
assert!(!testbench.handler.hk_set.valid);
assert!(matches!(
testbench.drain_events().as_slice(),
[mgt::Event::ModeChanged(DeviceMode::Normal)]
));
}
#[test]
fn test_unexpected_reply_registers_fault() {
let mut testbench = MgtTestbench::new();
testbench.test_interface().push_reply(sim_mgt::Reply::Ack);
testbench.switch_to_normal();
assert_eq!(testbench.handler.fdir.fault_count(), 1);
assert!(matches!(
testbench.drain_events().as_slice(),
[mgt::Event::ModeChanged(DeviceMode::Normal)]
));
}
#[test]
fn test_invalid_reply_registers_fault() {
let mut testbench = MgtTestbench::new();
testbench.test_interface().replies.push_back(vec![0xff]);
testbench.switch_to_normal();
assert_eq!(testbench.handler.fdir.fault_count(), 1);
assert!(matches!(
testbench.drain_events().as_slice(),
[mgt::Event::ModeChanged(DeviceMode::Normal)]
));
}
#[test]
fn test_reply_fault_threshold_starts_recovery() {
let mut testbench = MgtTestbench::new();
testbench.switch_to_normal();
testbench.drain_events();
while testbench.parent_report_rx.try_recv().is_ok() {}
testbench.exceed_reply_fault_threshold();
assert_eq!(testbench.health(), Some(HealthState::NeedsRecovery));
assert!(matches!(
testbench.drain_events().as_slice(),
[
mgt::Event::ReplyFaultThresholdExceeded,
mgt::Event::Recovery(RecoveryEvent::Started)
]
));
testbench.complete_power_cycle();
assert_eq!(testbench.health(), Some(HealthState::Healthy));
assert!(matches!(
testbench.drain_events().as_slice(),
[mgt::Event::Recovery(RecoveryEvent::Done)]
));
// The power cycle is hidden from the parent.
assert!(testbench.parent_report_rx.try_recv().is_err());
}
#[test]
fn test_unresponsive_device_marked_faulty() {
let mut testbench = MgtTestbench::new();
testbench.switch_to_normal();
for _ in 0..RECOVERY_THRESHOLD {
testbench.exceed_reply_fault_threshold();
assert_eq!(testbench.health(), Some(HealthState::NeedsRecovery));
testbench.complete_power_cycle();
}
testbench.drain_events();
testbench.exceed_reply_fault_threshold();
assert_eq!(testbench.health(), Some(HealthState::Faulty));
assert!(matches!(
testbench.drain_events().as_slice(),
[
mgt::Event::ReplyFaultThresholdExceeded,
mgt::Event::Recovery(RecoveryEvent::ThresholdExceeded)
]
));
assert_eq!(
testbench.handler.switch_and_mode_helper.target(),
Some(DeviceMode::Off)
);
}
#[test]
fn test_set_health() {
let mut testbench = MgtTestbench::new();
testbench.send_tc(Request::Health(HealthRequest::SetHealth(
HealthState::Faulty,
)));
testbench.handler.periodic_operation();
assert_eq!(testbench.next_response(), Response::Ok);
assert_eq!(testbench.health(), Some(HealthState::Faulty));
}
}
File renamed without changes.
@@ -4,6 +4,7 @@ use std::{
time::Duration,
};
use example_std::{ModeHelper, TmtcQueues};
use satrs::{
mode_tree::{
ModeStoreProvider, ModeStoreVec, SequenceModeTables, SequenceTableEntry,
@@ -15,7 +16,6 @@ use satrs::{
SubsystemCommandingHelper, SubsystemHelperResult,
},
};
use satrs_example::{ModeHelper, TmtcQueues};
use types::{
ComponentId,
acs::subsystem::{Mode, response},
File renamed without changes.
File renamed without changes.
File renamed without changes.
+200
View File
@@ -0,0 +1,200 @@
use std::collections::VecDeque;
use std::time::Duration;
use satrs::fdir::{FaultCounterStd, FaultResponse, RecoveryEvent, RecoveryFdir};
use satrs::health::{HealthState, HealthTableMapSync};
use types::{ComponentId, DeviceMode};
use crate::device_mode::SwitchAndModeHelper;
// The component is marked faulty if it would be recovered more than RECOVERY_THRESHOLD times
// before the counter is decremented again.
pub const RECOVERY_THRESHOLD: u32 = 2;
pub const RECOVERY_DECREMENT_AFTER: Duration = Duration::from_secs(60);
/// Time the device stays unpowered during a power cycle, so it can fully discharge.
pub const RECOVERY_OFF_DURATION: Duration = Duration::from_millis(500);
/// Generic FDIR events. The device handler maps them to its own event type.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum FdirEvent {
FaultThresholdExceeded,
Recovery(RecoveryEvent),
}
/// Fault counting and power cycle recovery for device handlers which own the power switch of
/// their device.
///
/// The handler detects faults itself and reports them with [Self::register_fault]. This helper
/// then decides whether the device is power cycled, or marked faulty and switched off, and drives
/// the [SwitchAndModeHelper] of the handler accordingly. The handler retrieves the resulting
/// events with [Self::next_event].
pub struct DeviceFdir {
name: &'static str,
fault_counter: FaultCounterStd,
recovery: RecoveryFdir<HealthTableMapSync>,
pub recovery_off_duration: Duration,
events: VecDeque<FdirEvent>,
}
impl DeviceFdir {
pub fn new(
name: &'static str,
component_id: ComponentId,
health_table: HealthTableMapSync,
fault_counter: FaultCounterStd,
) -> Self {
Self {
name,
fault_counter,
recovery: RecoveryFdir::new(
component_id.into(),
health_table,
RECOVERY_THRESHOLD,
RECOVERY_DECREMENT_AFTER,
),
recovery_off_duration: RECOVERY_OFF_DURATION,
events: VecDeque::new(),
}
}
#[cfg(test)]
pub fn fault_count(&self) -> u32 {
self.fault_counter.fault_count()
}
pub fn set_health(&mut self, health: HealthState) {
self.recovery.set_health(health);
}
pub fn next_event(&mut self) -> Option<FdirEvent> {
self.events.pop_front()
}
/// Should be called once per cycle, before the mode transition is handled. Starts a power
/// cycle if the health was set to [HealthState::NeedsRecovery] by the FDIR or by ground.
pub fn periodic_operation(&mut self, modes: &mut SwitchAndModeHelper<DeviceMode>) {
self.recovery.periodic_operation();
self.check_needs_recovery(modes);
}
pub fn register_success(&mut self) {
self.fault_counter.try_decrement();
}
/// If the fault threshold is exceeded, the device is power cycled. If it was power cycled
/// too often, the component is marked faulty and commanded off instead.
pub fn register_fault(&mut self, modes: &mut SwitchAndModeHelper<DeviceMode>) {
if !self.fault_counter.increment_and_check() {
return;
}
match self.recovery.handle_fault() {
FaultResponse::Ignored => {
log::info!(
"{}: fault threshold exceeded, but component is already faulty, \
recovering or externally controlled",
self.name
);
}
FaultResponse::Recover => {
log::warn!(
"{}: fault threshold exceeded, power cycling device",
self.name
);
self.events.push_back(FdirEvent::FaultThresholdExceeded);
self.check_needs_recovery(modes);
}
FaultResponse::SetFaulty => {
log::error!(
"{}: fault threshold exceeded after too many recoveries, marking \
component faulty",
self.name
);
self.events.push_back(FdirEvent::FaultThresholdExceeded);
self.events
.push_back(FdirEvent::Recovery(RecoveryEvent::ThresholdExceeded));
self.switch_off_faulty_device(modes);
}
}
}
/// Mode commands from ground or the parent abort a running recovery. Must be called before
/// the commanded transition is started.
pub fn handle_mode_command(&mut self, modes: &SwitchAndModeHelper<DeviceMode>) {
if modes.power_cycle_active() {
log::warn!("{}: mode command aborts power cycle recovery", self.name);
// Otherwise, the recovery would restart right away.
self.recovery.recovery_done();
}
}
pub fn handle_power_cycle_done(&mut self) {
log::info!("{}: power cycle recovery done", self.name);
// Faults registered while the device was switched off do not count anymore.
self.fault_counter.clear();
self.recovery.recovery_done();
self.events
.push_back(FdirEvent::Recovery(RecoveryEvent::Done));
}
/// A failed power cycle costs a recovery attempt like any other fault.
pub fn handle_power_cycle_failed(
&mut self,
modes: &mut SwitchAndModeHelper<DeviceMode>,
restore_mode: DeviceMode,
) {
self.events
.push_back(FdirEvent::Recovery(RecoveryEvent::Failed));
match self.recovery.recovery_failed() {
FaultResponse::Recover => {
log::warn!("{}: power cycle recovery failed, retrying", self.name);
self.start_recovery(modes, restore_mode);
}
FaultResponse::SetFaulty => {
log::error!(
"{}: power cycle recovery failed too often, marking component faulty",
self.name
);
self.events
.push_back(FdirEvent::Recovery(RecoveryEvent::ThresholdExceeded));
self.switch_off_faulty_device(modes);
}
// Ground changed the health during the recovery and is in charge now.
FaultResponse::Ignored => (),
}
}
fn check_needs_recovery(&mut self, modes: &mut SwitchAndModeHelper<DeviceMode>) {
if modes.power_cycle_active() || modes.target().is_some() || !self.recovery.needs_recovery()
{
return;
}
if modes.mode() == DeviceMode::Off {
// Nothing to power cycle, the next switch-on is a fresh start anyway.
log::info!("{}: device is off, no recovery required", self.name);
self.recovery.recovery_done();
return;
}
let restore_mode = modes.mode();
self.start_recovery(modes, restore_mode);
}
fn start_recovery(
&mut self,
modes: &mut SwitchAndModeHelper<DeviceMode>,
restore_mode: DeviceMode,
) {
log::warn!("{}: starting power cycle recovery", self.name);
modes.start_power_cycle(restore_mode, self.recovery_off_duration);
self.events
.push_back(FdirEvent::Recovery(RecoveryEvent::Started));
}
fn switch_off_faulty_device(&mut self, modes: &mut SwitchAndModeHelper<DeviceMode>) {
// Do not restart an already pending Off transition, which would reset the transition
// state machine before it can finish.
if modes.target() != Some(DeviceMode::Off) {
log::warn!("{}: commanding device off due to fault", self.name);
modes.start_transition(DeviceMode::Off, None);
}
}
}
@@ -68,7 +68,7 @@ pub enum ModeTransitionEvent<Mode> {
/// a parent) are not this helper's concern: [`Self::handle_mode_transition`] just reports when a
/// transition finishes (or fails) and leaves what to do about it to the caller.
pub struct SwitchAndModeHelper<Mode: PowerSwitchedMode> {
mode_helper: satrs_example::ModeHelper<Mode, SwitchTransitionState>,
mode_helper: example_std::ModeHelper<Mode, SwitchTransitionState>,
switch_helper: PowerSwitchHelper,
switch_id: SwitchId,
power_cycle_state: PowerCycleState<Mode>,
@@ -83,7 +83,7 @@ impl<Mode: PowerSwitchedMode> SwitchAndModeHelper<Mode> {
switch_id: SwitchId,
) -> Self {
Self {
mode_helper: satrs_example::ModeHelper::new(init_mode, timeout),
mode_helper: example_std::ModeHelper::new(init_mode, timeout),
switch_helper,
switch_id,
power_cycle_state: PowerCycleState::Idle,
File renamed without changes.
@@ -5,13 +5,13 @@ use std::{
};
use derive_new::new;
use num_enum::{IntoPrimitive, TryFromPrimitive};
use satrs::spacepackets::CcsdsPacketIdAndPsc;
use satrs_example::TimestampHelper;
use satrs_minisim::{
use example_std::TimestampHelper;
use minisim_types::{
SimReply, SimRequestWithTime,
eps::{PcduReply, PcduRequest},
};
use num_enum::{IntoPrimitive, TryFromPrimitive};
use satrs::spacepackets::CcsdsPacketIdAndPsc;
use serde::{Deserialize, Serialize};
use strum::IntoEnumIterator as _;
use types::{
@@ -134,7 +134,7 @@ impl SerialInterface for SerialInterfaceToSim {
type Error = ();
fn send(&self, data: &[u8]) -> Result<(), Self::Error> {
let request: PcduRequest = serde_json::from_slice(data).expect("expected a PCDU request");
let request: PcduRequest = postcard::from_bytes(data).expect("expected a PCDU request");
self.sim_request_tx
.send(SimRequestWithTime::new_with_epoch_time(request))
.expect("failed to send request to simulation");
@@ -148,8 +148,8 @@ impl SerialInterface for SerialInterfaceToSim {
loop {
match self.sim_reply_rx.try_recv() {
Ok(reply) => {
let reply = serde_json::to_string(&reply).unwrap();
f(reply.as_bytes());
let reply = postcard::to_allocvec(&reply).unwrap();
f(&reply);
}
Err(e) => match e {
mpsc::TryRecvError::Empty => break,
@@ -175,18 +175,13 @@ impl SerialInterface for SerialInterfaceDummy {
type Error = ();
fn send(&self, data: &[u8]) -> Result<(), Self::Error> {
let pcdu_req: PcduRequest = serde_json::from_slice(data).unwrap();
let pcdu_req: PcduRequest = postcard::from_bytes(data).unwrap();
let switch_map_mut = &mut self.switch_map.borrow_mut().0;
match pcdu_req {
PcduRequest::SwitchDevice { switch, state } => {
match switch_map_mut.entry(switch) {
std::collections::hash_map::Entry::Occupied(mut val) => {
*val.get_mut() = state;
}
std::collections::hash_map::Entry::Vacant(vacant) => {
vacant.insert(state);
}
};
switch_map_mut
.insert(switch, state)
.expect("switch map capacity exceeded");
}
PcduRequest::RequestSwitchInfo => {
let mut reply_deque_mut = self.reply_deque.borrow_mut();
@@ -206,8 +201,8 @@ impl SerialInterface for SerialInterfaceDummy {
return Ok(());
}
loop {
let reply = self.get_next_reply_as_string();
f(reply.as_bytes());
let reply = self.next_reply_serialized();
f(&reply);
if self.reply_queue_empty() {
break;
}
@@ -217,10 +212,10 @@ impl SerialInterface for SerialInterfaceDummy {
}
impl SerialInterfaceDummy {
fn get_next_reply_as_string(&self) -> String {
fn next_reply_serialized(&self) -> Vec<u8> {
let mut reply_deque_mut = self.reply_deque.borrow_mut();
let next_reply = reply_deque_mut.pop_front().unwrap();
serde_json::to_string(&next_reply).unwrap()
postcard::to_allocvec(&next_reply).unwrap()
}
fn reply_queue_empty(&self) -> bool {
@@ -437,8 +432,8 @@ impl<ComInterface: SerialInterface> PcduHandler<ComInterface> {
pub fn handle_periodic_commands(&self) {
let pcdu_req = PcduRequest::RequestSwitchInfo;
let pcdu_req_ser = serde_json::to_string(&pcdu_req).unwrap();
if let Err(_e) = self.com_interface.send(pcdu_req_ser.as_bytes()) {
let pcdu_req_ser = postcard::to_allocvec(&pcdu_req).unwrap();
if let Err(_e) = self.com_interface.send(&pcdu_req_ser) {
log::warn!("polling PCDU switch info failed");
if let Err(e) = self.event_tx.send(pcdu::Event::SerialCommError) {
log::warn!("failed to send comm error event: {}", e);
@@ -484,9 +479,9 @@ impl<ComInterface: SerialInterface> PcduHandler<ComInterface> {
switch: switch_id,
state,
};
let pcdu_req_ser = serde_json::to_string(&pcdu_req).unwrap();
let pcdu_req_ser = postcard::to_allocvec(&pcdu_req).unwrap();
self.com_interface
.send(pcdu_req_ser.as_bytes())
.send(&pcdu_req_ser)
.expect("failed to send switch request to PCDU");
}
@@ -509,7 +504,7 @@ impl<ComInterface: SerialInterface> PcduHandler<ComInterface> {
pub fn poll_and_handle_replies(&mut self) {
if let Err(e) = self.com_interface.try_recv_replies(|reply| {
let sim_reply: SimReply = serde_json::from_slice(reply).expect("invalid reply format");
let sim_reply: SimReply = postcard::from_bytes(reply).expect("invalid reply format");
let SimReply::Pcdu(pcdu_reply) = sim_reply else {
log::warn!("unexpected PCDU SIM reply: {sim_reply:?}");
return;
@@ -559,7 +554,7 @@ mod tests {
pub struct SerialInterfaceTest {
pub inner: SerialInterfaceDummy,
pub send_queue: RefCell<VecDeque<Vec<u8>>>,
pub reply_queue: RefCell<VecDeque<String>>,
pub reply_queue: RefCell<VecDeque<Vec<u8>>>,
/// Makes the next `send` call fail, to exercise comm-error handling.
pub fail_next_send: RefCell<bool>,
}
@@ -584,9 +579,9 @@ mod tests {
return Ok(());
}
loop {
let reply = self.inner.get_next_reply_as_string();
let reply = self.inner.next_reply_serialized();
self.reply_queue.borrow_mut().push_back(reply.clone());
f(reply.as_bytes());
f(&reply);
if self.inner.reply_queue_empty() {
break;
}
@@ -642,7 +637,7 @@ mod tests {
assert_eq!(send_queue_mut.len(), expected_queue_len);
let packet_sent = send_queue_mut.pop_front().unwrap();
drop(send_queue_mut);
let pcdu_req: PcduRequest = serde_json::from_slice(&packet_sent).unwrap();
let pcdu_req: PcduRequest = postcard::from_bytes(&packet_sent).unwrap();
assert_eq!(pcdu_req, PcduRequest::RequestSwitchInfo);
}
@@ -657,7 +652,7 @@ mod tests {
assert_eq!(send_queue_mut.len(), expected_queue_len);
let packet_sent = send_queue_mut.pop_front().unwrap();
drop(send_queue_mut);
let pcdu_req: PcduRequest = serde_json::from_slice(&packet_sent).unwrap();
let pcdu_req: PcduRequest = postcard::from_bytes(&packet_sent).unwrap();
assert_eq!(
pcdu_req,
PcduRequest::SwitchDevice {
@@ -676,7 +671,7 @@ mod tests {
let mut reply_received_mut = self.handler.com_interface.reply_queue.borrow_mut();
assert_eq!(reply_received_mut.len(), expected_queue_len);
let reply_received = reply_received_mut.pop_front().unwrap();
let sim_reply: SimReply = serde_json::from_str(&reply_received).unwrap();
let sim_reply: SimReply = postcard::from_bytes(&reply_received).unwrap();
assert_eq!(
sim_reply,
SimReply::Pcdu(PcduReply::SwitchInfo(expected_map))
File renamed without changes.
File renamed without changes.
@@ -5,9 +5,9 @@ use std::{
time::Duration,
};
use minisim_types::{ComponentId, SimReply, SimRequestWithTime, udp::SIM_CTRL_PORT};
use minisim_types::{SimCtrlReply, SimCtrlRequest};
use satrs::HandlingStatus;
use satrs_minisim::{ComponentId, SimReply, SimRequestWithTime, udp::SIM_CTRL_PORT};
use satrs_minisim::{SimCtrlReply, SimCtrlRequest};
struct SimReplyMap(pub HashMap<ComponentId, mpsc::Sender<SimReply>>);
@@ -35,10 +35,10 @@ pub enum SimClientCreationError {
Io(#[from] std::io::Error),
#[error("timeout when trying to connect to sim UDP server")]
Timeout,
#[error("invalid ping reply when trying connection to UDP sim server")]
InvalidReplyJsonError(#[from] serde_json::Error),
#[error("invalid ping reply when trying connection to UDP sim server: {0}")]
InvalidReply(#[from] postcard::Error),
#[error("invalid sim reply, not pong reply as expected: {0:?}")]
ReplyIsNotPong(SimReply),
ReplyIsNotPong(Box<SimReply>),
}
pub struct SimClientUdp {
@@ -74,14 +74,14 @@ impl SimClientUdp {
reply_buf: &mut [u8],
) -> Result<(), SimClientCreationError> {
let sim_req = SimRequestWithTime::new_with_epoch_time(SimCtrlRequest::Ping);
let sim_req_json = serde_json::to_string(&sim_req).expect("failed to serialize SimRequest");
udp_client.send_to(sim_req_json.as_bytes(), simulator_addr)?;
let sim_req_raw = postcard::to_allocvec(&sim_req).expect("failed to serialize SimRequest");
udp_client.send_to(&sim_req_raw, simulator_addr)?;
match udp_client.recv(reply_buf) {
Ok(reply_len) => {
let sim_reply: SimReply = serde_json::from_slice(&reply_buf[0..reply_len])?;
let sim_reply: SimReply = postcard::from_bytes(&reply_buf[0..reply_len])?;
match sim_reply {
SimReply::SimCtrl(SimCtrlReply::Pong) => Ok(()),
_ => Err(SimClientCreationError::ReplyIsNotPong(sim_reply)),
_ => Err(SimClientCreationError::ReplyIsNotPong(Box::new(sim_reply))),
}
}
Err(e) => {
@@ -102,12 +102,9 @@ impl SimClientUdp {
loop {
match self.sim_request_rx.try_recv() {
Ok(request) => {
let request_json =
serde_json::to_string(&request).expect("failed to serialize SimRequest");
if let Err(e) = self
.udp_client
.send_to(request_json.as_bytes(), self.simulator_addr)
{
let request_raw =
postcard::to_allocvec(&request).expect("failed to serialize SimRequest");
if let Err(e) = self.udp_client.send_to(&request_raw, self.simulator_addr) {
log::error!("error sending data to UDP SIM server: {e}");
break;
} else {
@@ -129,8 +126,8 @@ impl SimClientUdp {
match self.udp_client.recv(&mut self.reply_buf) {
Ok(recvd_bytes) => {
no_data_from_udp_server_received = false;
let sim_reply_result: serde_json::Result<SimReply> =
serde_json::from_slice(&self.reply_buf[0..recvd_bytes]);
let sim_reply_result: postcard::Result<SimReply> =
postcard::from_bytes(&self.reply_buf[0..recvd_bytes]);
match sim_reply_result {
Ok(sim_reply) => {
if let Some(sender) = self.reply_map.0.get(&sim_reply.component()) {
@@ -176,7 +173,6 @@ impl SimClientUdp {
#[cfg(test)]
pub mod tests {
use std::{
collections::HashMap,
net::{SocketAddr, UdpSocket},
sync::{
Arc,
@@ -186,11 +182,13 @@ pub mod tests {
time::Duration,
};
use satrs_minisim::{
use minisim_types::{
ComponentId, SimCtrlReply, SimCtrlRequest, SimReply, SimRequest, SimRequestWithTime,
eps::{PcduReply, PcduRequest},
};
use types::pcdu::SwitchMapBinary;
use super::SimClientUdp;
struct UdpSimTestServer {
@@ -233,10 +231,10 @@ pub mod tests {
loop {
match self.reply_rx.try_recv() {
Ok(sim_reply) => {
let sim_reply_json = serde_json::to_string(&sim_reply)
let sim_reply_raw = postcard::to_allocvec(&sim_reply)
.expect("failed to serialize SimReply");
self.udp_server
.send_to(sim_reply_json.as_bytes(), last_sender)
.send_to(&sim_reply_raw, last_sender)
.expect("failed to send reply to client from UDP server");
no_sim_replies_handled = false;
}
@@ -254,17 +252,17 @@ pub mod tests {
match self.udp_server.recv_from(&mut self.recv_buf) {
Ok((read_bytes, from)) => {
let sim_request: SimRequestWithTime =
serde_json::from_slice(&self.recv_buf[0..read_bytes])
postcard::from_bytes(&self.recv_buf[0..read_bytes])
.expect("failed to deserialize SimRequest");
// For a ping, we perform the reply handling here directly
if sim_request.request == SimRequest::SimCtrl(SimCtrlRequest::Ping) {
no_data_received = false;
self.last_sender = Some(from);
let sim_reply = SimReply::from(SimCtrlReply::Pong);
let sim_reply_json = serde_json::to_string(&sim_reply)
let sim_reply_raw = postcard::to_allocvec(&sim_reply)
.expect("failed to serialize SimReply");
self.udp_server
.send_to(sim_reply_json.as_bytes(), from)
.send_to(&sim_reply_raw, from)
.expect("failed to send reply to client from UDP server");
}
// Forward each SIM request for testing purposes.
@@ -369,7 +367,7 @@ pub mod tests {
);
// We inject the reply ourselves.
let pcdu_reply = PcduReply::SwitchInfo(HashMap::new());
let pcdu_reply = PcduReply::SwitchInfo(SwitchMapBinary::default());
server_sim_reply_tx
.send(SimReply::from(pcdu_reply.clone()))
.expect("sending PCDU reply failed");
File renamed without changes.
@@ -112,6 +112,7 @@ mod tests {
use arbitrary_int::traits::Integer as _;
use arbitrary_int::u14;
use example_std::config::OBSW_SERVER_ADDR;
use satrs::spacepackets::ecss::{CreatorConfig, MessageTypeId};
use satrs::{
ComponentId,
@@ -120,7 +121,6 @@ mod tests {
ecss::{WritablePusPacket, tc::PusTcCreator},
},
};
use satrs_example::config::OBSW_SERVER_ADDR;
use types::Apid;
use crate::tmtc::sender::MockSender;
@@ -15,7 +15,7 @@ use satrs::{
},
spacepackets::time::cds::CdsTime,
};
use satrs_example::ids::generic_pus::PUS_EVENT_MANAGEMENT;
use example_std::ids::generic_pus::PUS_EVENT_MANAGEMENT;
use crate::update_time;
File renamed without changes.
@@ -16,9 +16,9 @@ use satrs::pus::{
use satrs::request::{GenericMessage, UniqueApidTargetId};
use satrs::spacepackets::ecss::tc::PusTcReader;
use satrs::spacepackets::ecss::{EcssEnumU16, PusPacket, PusServiceId};
use satrs_example::config::tmtc_err;
use satrs_example::ids;
use satrs_example::ids::generic_pus::PUS_ACTION;
use example_std::config::tmtc_err;
use example_std::ids;
use example_std::ids::generic_pus::PUS_ACTION;
use std::sync::mpsc;
use std::time::Duration;
@@ -10,7 +10,7 @@ use satrs::pus::{
PartialPusHandlingError, PusServiceHelper,
};
use satrs::spacepackets::ecss::PusServiceId;
use satrs_example::ids::generic_pus::PUS_EVENT_MANAGEMENT;
use example_std::ids::generic_pus::PUS_EVENT_MANAGEMENT;
use super::{DirectPusService, HandlingStatus};
@@ -13,8 +13,8 @@ use satrs::request::{GenericMessage, UniqueApidTargetId};
use satrs::res_code::ResultU16;
use satrs::spacepackets::ecss::tc::PusTcReader;
use satrs::spacepackets::ecss::{hk, PusPacket, PusServiceId};
use satrs_example::config::{hk_err, tmtc_err};
use satrs_example::ids::generic_pus::PUS_HK;
use example_std::config::{hk_err, tmtc_err};
use example_std::ids::generic_pus::PUS_HK;
use std::sync::mpsc;
use std::time::Duration;
@@ -321,7 +321,7 @@ mod tests {
SpHeader,
},
};
use satrs_example::config::tmtc_err;
use example_std::config::tmtc_err;
use crate::pus::{
hk::HkReplyVariant,
@@ -18,9 +18,9 @@ use satrs::spacepackets::ecss::tc::PusTcReader;
use satrs::spacepackets::ecss::{PusPacket, PusServiceId};
use satrs::tmtc::{PacketAsVec, PacketInPool};
use satrs::ComponentId;
use satrs_example::config::{tmtc_err, CustomPusServiceId};
use satrs_example::ids::generic_pus::PUS_ROUTING;
use satrs_example::TimestampHelper;
use example_std::config::{tmtc_err, CustomPusServiceId};
use example_std::ids::generic_pus::PUS_ROUTING;
use example_std::TimestampHelper;
use std::fmt::Debug;
use std::sync::mpsc;
@@ -3,7 +3,7 @@ use arbitrary_int::u14;
use derive_new::new;
use satrs::mode_tree::{ModeNode, ModeParent};
use satrs::spacepackets::ecss::{CreatorConfig, MessageTypeId};
use satrs_example::ids;
use example_std::ids;
use std::sync::mpsc;
use std::time::Duration;
@@ -37,7 +37,7 @@ use satrs::{
},
ComponentId,
};
use satrs_example::config::{mode_err, tmtc_err, CustomPusServiceId};
use example_std::config::{mode_err, tmtc_err, CustomPusServiceId};
use super::{
create_verification_reporter, generic_pus_request_timeout_handler, HandlingStatus,
@@ -316,7 +316,7 @@ mod tests {
SpHeader,
},
};
use satrs_example::config::tmtc_err;
use example_std::config::tmtc_err;
use crate::pus::{
mode::ModeReplyHandler,
@@ -15,7 +15,7 @@ use satrs::pus::{
use satrs::spacepackets::ecss::PusServiceId;
use satrs::tmtc::{PacketAsVec, PacketInPool, PacketSenderWithSharedPool};
use satrs::ComponentId;
use satrs_example::ids::sched::PUS_SCHED;
use example_std::ids::sched::PUS_SCHED;
use super::{DirectPusService, HandlingStatus};
@@ -11,8 +11,8 @@ use satrs::pus::{
};
use satrs::spacepackets::ecss::tc::PusTcReader;
use satrs::spacepackets::ecss::{PusPacket, PusServiceId};
use satrs_example::config::{tmtc_err, TEST_EVENT};
use satrs_example::ids::generic_pus::PUS_TEST;
use example_std::config::{tmtc_err, TEST_EVENT};
use example_std::ids::generic_pus::PUS_TEST;
use std::sync::mpsc;
use super::{DirectPusService, HandlingStatus};
@@ -14,7 +14,7 @@ use satrs::request::{GenericMessage, MessageMetadata, UniqueApidTargetId};
use satrs::spacepackets::ecss::tc::PusTcReader;
use satrs::spacepackets::ecss::PusPacket;
use satrs::ComponentId;
use satrs_example::config::tmtc_err;
use example_std::config::tmtc_err;
/*
#[derive(Clone, Debug)]
File renamed without changes.
File renamed without changes.
@@ -13,6 +13,13 @@ use eps::{
PowerSwitchHelper,
pcdu::{PcduHandler, SerialInterfaceDummy, SerialInterfaceToSim, SerialSimInterfaceWrapper},
};
use example_std::{
TmtcQueues,
config::{
OBSW_SERVER_ADDR, PACKET_ID_VALIDATOR, SERVER_PORT,
tasks::{FREQ_MS_AOCS, FREQ_MS_CONTROLLER, FREQ_MS_UDP_TMTC, SIM_CLIENT_IDLE_DELAY_MS},
},
};
use interface::{
sim_client_udp::create_sim_client,
tcp::{SyncTcpTmSource, TcpTask},
@@ -25,13 +32,6 @@ use satrs::{
hal::std::{tcp_server::ServerConfig, udp_server::UdpTcServer},
spacepackets::time::cds::CdsTime,
};
use satrs_example::{
TmtcQueues,
config::{
OBSW_SERVER_ADDR, PACKET_ID_VALIDATOR, SERVER_PORT,
tasks::{FREQ_MS_AOCS, FREQ_MS_CONTROLLER, FREQ_MS_UDP_TMTC, SIM_CLIENT_IDLE_DELAY_MS},
},
};
use tmtc::sender::TmTcSender;
use tmtc::{tc_source::TcSourceTask, tm_sink::TmSink};
use types::{ComponentId, DeviceMode};
@@ -48,6 +48,7 @@ use crate::{
mod acs;
mod ccsds;
mod controller;
mod device_fdir;
mod device_mode;
mod eps;
mod event_manager;
@@ -181,8 +182,8 @@ fn main() {
let (mgm_0_spi_interface, mgm_1_spi_interface) = if let Some(sim_client) =
opt_sim_client.as_mut()
{
sim_client.add_reply_recipient(satrs_minisim::ComponentId::Mgm0Lis3Mdl, mgm_0_sim_reply_tx);
sim_client.add_reply_recipient(satrs_minisim::ComponentId::Mgm1Lis3Mdl, mgm_1_sim_reply_tx);
sim_client.add_reply_recipient(minisim_types::ComponentId::Mgm0Lis3Mdl, mgm_0_sim_reply_tx);
sim_client.add_reply_recipient(minisim_types::ComponentId::Mgm1Lis3Mdl, mgm_1_sim_reply_tx);
(
mgm::SpiCommunication::Sim(mgm::SpiSimInterface {
id: mgm::MgmId::_0,
@@ -258,7 +259,7 @@ fn main() {
});
let mgt_com = if let Some(sim_client) = opt_sim_client.as_mut() {
sim_client.add_reply_recipient(satrs_minisim::ComponentId::Mgt, mgt_sim_reply_tx);
sim_client.add_reply_recipient(minisim_types::ComponentId::Mgt, mgt_sim_reply_tx);
mgt::MgtCommunication::Sim(mgt::SimInterface {
sim_request_tx: sim_request_tx.clone(),
sim_reply_rx: mgt_sim_reply_rx,
@@ -278,6 +279,7 @@ fn main() {
report_tx: mgt_report_tx,
},
Duration::from_millis(1000),
health_table.clone(),
mgt_event_tx,
);
@@ -299,7 +301,7 @@ fn main() {
);
let pcdu_serial_interface = if let Some(sim_client) = opt_sim_client.as_mut() {
sim_client.add_reply_recipient(satrs_minisim::ComponentId::Pcdu, pcdu_sim_reply_tx);
sim_client.add_reply_recipient(minisim_types::ComponentId::Pcdu, pcdu_sim_reply_tx);
SerialSimInterfaceWrapper::Sim(SerialInterfaceToSim::new(
sim_request_tx.clone(),
pcdu_sim_reply_rx,
File renamed without changes.
File renamed without changes.
File renamed without changes.
+14
View File
@@ -0,0 +1,14 @@
[package]
name = "minisim-types"
version = "0.1.0"
edition = "2024"
[dependencies]
num_enum = { version = "0.7", default-features = false }
serde = { version = "1", default-features = false, features = ["derive"] }
tai-time = { version = "1", default-features = false, features = ["serde"] }
thiserror = { version = "2", default-features = false }
types = { path = "../types" }
[dev-dependencies]
postcard = { version = "1", features = ["alloc"] }
+472
View File
@@ -0,0 +1,472 @@
#![no_std]
extern crate alloc;
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
use tai_time::MonotonicTime;
use crate::{
acs::{mgm, mgt},
eps::{PcduReply, PcduRequest},
};
/// Used by clients to route replies to the component handling them.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
pub enum ComponentId {
SimCtrl,
Mgm0Lis3Mdl,
Mgm1Lis3Mdl,
Mgt,
Pcdu,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum SimRequest {
SimCtrl(SimCtrlRequest),
Mgm {
id: mgm::Id,
request: mgm::Request,
},
/// Raw frame of the MGT serial protocol.
Mgt(Vec<u8>),
Pcdu(PcduRequest),
}
impl From<SimCtrlRequest> for SimRequest {
fn from(request: SimCtrlRequest) -> Self {
Self::SimCtrl(request)
}
}
impl From<mgt::Request> for SimRequest {
fn from(request: mgt::Request) -> Self {
Self::Mgt(request.to_frame())
}
}
impl From<PcduRequest> for SimRequest {
fn from(request: PcduRequest) -> Self {
Self::Pcdu(request)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SimRequestWithTime {
pub request: SimRequest,
pub timestamp: MonotonicTime,
}
impl SimRequestWithTime {
pub fn new(request: impl Into<SimRequest>, timestamp: MonotonicTime) -> Self {
Self {
request: request.into(),
timestamp,
}
}
pub fn new_with_epoch_time(request: impl Into<SimRequest>) -> Self {
Self::new(request, MonotonicTime::EPOCH)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum SimReply {
SimCtrl(SimCtrlReply),
Mgm {
id: mgm::Id,
reply: mgm::Reply,
},
/// Raw frame of the MGT serial protocol.
Mgt(Vec<u8>),
Pcdu(PcduReply),
}
impl SimReply {
pub fn component(&self) -> ComponentId {
match self {
SimReply::SimCtrl(_) => ComponentId::SimCtrl,
SimReply::Mgm { id, .. } => id.sim_component(),
SimReply::Mgt(_) => ComponentId::Mgt,
SimReply::Pcdu(_) => ComponentId::Pcdu,
}
}
}
impl From<SimCtrlReply> for SimReply {
fn from(reply: SimCtrlReply) -> Self {
Self::SimCtrl(reply)
}
}
impl From<mgt::Reply> for SimReply {
fn from(reply: mgt::Reply) -> Self {
Self::Mgt(reply.to_frame())
}
}
impl From<PcduReply> for SimReply {
fn from(reply: PcduReply) -> Self {
Self::Pcdu(reply)
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SimCtrlRequest {
Ping,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SimCtrlReply {
Pong,
}
pub mod eps {
use super::*;
use types::pcdu::{SwitchId, SwitchMapBinary, SwitchStateBinary};
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PcduRequest {
SwitchDevice {
switch: SwitchId,
state: SwitchStateBinary,
},
RequestSwitchInfo,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum PcduReply {
SwitchInfo(SwitchMapBinary),
}
}
pub mod acs {
/// MGM module strongly based on the LIS3MDL device.
pub mod mgm {
use serde::{Deserialize, Serialize};
use types::pcdu::SwitchStateBinary;
use crate::ComponentId;
/// Fault mode injected on the simulated SPI bus, independent of the switch state.
///
/// Models the classic symptom of a stuck SPI bus: an undriven MISO line commonly reads
/// back as all-1s, a shorted/grounded one as all-0s.
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SpiFaultMode {
#[default]
None,
AllZeros,
AllOnes,
}
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SpiFault {
pub mode: SpiFaultMode,
/// The fault is cleared when the device is switched off, so a power cycle recovers
/// from it.
pub cleared_by_power_cycle: bool,
}
// Normally, small magnetometers generate their output as a signed 16 bit raw format or something
// similar which needs to be converted to a signed float value with physical units. We will
// simplify this now and generate the signed float values directly. The unit is micro tesla.
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub struct SensorValuesMicroTesla {
pub x: f32,
pub y: f32,
pub z: f32,
}
pub const MGT_GEN_MAGNETIC_FIELD: SensorValuesMicroTesla = SensorValuesMicroTesla {
x: 30.0,
y: -30.0,
z: 30.0,
};
pub const ALL_ONES_SENSOR_VAL: i16 = 0xffff_u16 as i16;
pub const ALL_ZEROS_SENSOR_VAL: i16 = 0;
// Field data register scaling
pub const GAUSS_TO_MICROTESLA_FACTOR: u32 = 100;
pub const FIELD_LSB_PER_GAUSS_4_SENS: f32 = 1.0 / 6842.0;
#[derive(Default, Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub struct RawValues {
pub x: i16,
pub y: i16,
pub z: i16,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Request {
RequestSensorData,
/// Force the raw register reply into a stuck-bus pattern, regardless of switch state.
/// Used to test FDIR handling of SPI bus faults.
SetSpiFault(SpiFault),
}
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub struct Reply {
pub switch_state: SwitchStateBinary,
pub sensor_values: SensorValuesMicroTesla,
// Raw sensor values which are transmitted by the LIS3 device in little-endian
// order.
pub raw: RawValues,
}
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub enum Id {
Mgm0,
Mgm1,
}
impl Id {
pub const fn sim_component(&self) -> ComponentId {
match self {
Id::Mgm0 => ComponentId::Mgm0Lis3Mdl,
Id::Mgm1 => ComponentId::Mgm1Lis3Mdl,
}
}
}
impl RawValues {
pub const fn splat(value: i16) -> Self {
Self {
x: value,
y: value,
z: value,
}
}
}
}
/// Simple serial protocol of the magnetorquer.
///
/// The first byte of each frame is the packet ID. The high bit of the ID is set for replies.
/// All fields are big endian. Every command is answered with exactly one reply, but only
/// if the device is powered. The device drops invalid frames.
///
/// A data link layer is deliberately skipped for simplicity. A real serial link would need
/// framing and error detection, for example COBS encoding and a CRC. Here, the transport
/// always delivers complete and intact frames.
pub mod mgt {
use alloc::{vec, vec::Vec};
use core::time::Duration;
use num_enum::{IntoPrimitive, TryFromPrimitive};
pub use types::acs::mgt::Dipole;
#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
#[repr(u8)]
pub enum RequestId {
RequestHk = 0x01,
/// Payload: dipole (3 x i16), duration in milliseconds (u32).
ApplyTorque = 0x02,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
#[repr(u8)]
pub enum ReplyId {
/// Payload: dipole (3 x i16), torquing flag (u8).
Hk = 0x81,
/// Reply to [RequestId::ApplyTorque].
Ack = 0x82,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, thiserror::Error)]
pub enum FrameError {
#[error("empty frame")]
Empty,
#[error("unknown packet ID {0:#04x}")]
UnknownPacketId(u8),
#[error("invalid length {len} for packet ID {id:#04x}")]
InvalidLength { id: u8, len: usize },
}
fn split_packet_id(frame: &[u8]) -> Result<(u8, &[u8]), FrameError> {
let (&id, payload) = frame.split_first().ok_or(FrameError::Empty)?;
Ok((id, payload))
}
fn payload_array<const N: usize>(id: u8, payload: &[u8]) -> Result<&[u8; N], FrameError> {
payload.try_into().map_err(|_| FrameError::InvalidLength {
id,
len: payload.len() + 1,
})
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Request {
/// The duration has millisecond resolution on the wire.
ApplyTorque {
duration: Duration,
dipole: Dipole,
},
RequestHk,
}
impl Request {
pub fn to_frame(&self) -> Vec<u8> {
match self {
Request::RequestHk => vec![RequestId::RequestHk.into()],
Request::ApplyTorque { duration, dipole } => {
let mut frame = vec![RequestId::ApplyTorque.into()];
frame.extend_from_slice(&dipole.to_be_bytes());
let duration_ms = u32::try_from(duration.as_millis()).unwrap_or(u32::MAX);
frame.extend_from_slice(&duration_ms.to_be_bytes());
frame
}
}
}
pub fn from_frame(frame: &[u8]) -> Result<Self, FrameError> {
let (id, payload) = split_packet_id(frame)?;
match RequestId::try_from(id).map_err(|_| FrameError::UnknownPacketId(id))? {
RequestId::RequestHk => {
payload_array::<0>(id, payload)?;
Ok(Request::RequestHk)
}
RequestId::ApplyTorque => {
let payload: &[u8; Dipole::LEN + 4] = payload_array(id, payload)?;
let [dipole @ .., d0, d1, d2, d3] = *payload;
Ok(Request::ApplyTorque {
duration: Duration::from_millis(
u32::from_be_bytes([d0, d1, d2, d3]).into(),
),
dipole: Dipole::from_be_bytes(&dipole),
})
}
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct HkSet {
pub dipole: Dipole,
pub torquing: bool,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Reply {
Hk(HkSet),
Ack,
}
impl Reply {
pub fn to_frame(&self) -> Vec<u8> {
match self {
Reply::Hk(hk) => {
let mut frame = vec![ReplyId::Hk.into()];
frame.extend_from_slice(&hk.dipole.to_be_bytes());
frame.push(hk.torquing as u8);
frame
}
Reply::Ack => vec![ReplyId::Ack.into()],
}
}
pub fn from_frame(frame: &[u8]) -> Result<Self, FrameError> {
let (id, payload) = split_packet_id(frame)?;
match ReplyId::try_from(id).map_err(|_| FrameError::UnknownPacketId(id))? {
ReplyId::Hk => {
let payload: &[u8; Dipole::LEN + 1] = payload_array(id, payload)?;
let [dipole @ .., torquing] = *payload;
Ok(Reply::Hk(HkSet {
dipole: Dipole::from_be_bytes(&dipole),
torquing: torquing != 0,
}))
}
ReplyId::Ack => {
payload_array::<0>(id, payload)?;
Ok(Reply::Ack)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_apply_torque_frame() {
let request = Request::ApplyTorque {
duration: Duration::from_millis(0x0102_0304),
dipole: Dipole {
x: -2,
y: 0x0506,
z: 0x0708,
},
};
let frame = request.to_frame();
assert_eq!(
frame,
[
0x02, 0xff, 0xfe, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04
]
);
assert_eq!(Request::from_frame(&frame), Ok(request));
}
#[test]
fn test_request_hk_frame() {
assert_eq!(Request::RequestHk.to_frame(), [0x01]);
assert_eq!(Request::from_frame(&[0x01]), Ok(Request::RequestHk));
}
#[test]
fn test_reply_frames() {
let hk = Reply::Hk(HkSet {
dipole: Dipole { x: 1, y: 2, z: 3 },
torquing: true,
});
let frame = hk.to_frame();
assert_eq!(frame, [0x81, 0, 1, 0, 2, 0, 3, 1]);
assert_eq!(Reply::from_frame(&frame), Ok(hk));
assert_eq!(Reply::Ack.to_frame(), [0x82]);
assert_eq!(Reply::from_frame(&[0x82]), Ok(Reply::Ack));
}
#[test]
fn test_invalid_frames() {
assert_eq!(Request::from_frame(&[]), Err(FrameError::Empty));
assert_eq!(
Request::from_frame(&[0x81]),
Err(FrameError::UnknownPacketId(0x81))
);
assert_eq!(
Request::from_frame(&[0x01, 0x00]),
Err(FrameError::InvalidLength { id: 0x01, len: 2 })
);
assert_eq!(
Reply::from_frame(&[0x81, 0, 1]),
Err(FrameError::InvalidLength { id: 0x81, len: 3 })
);
}
}
}
}
pub mod udp {
pub const SIM_CTRL_PORT: u16 = 7303;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_request_serde_roundtrip() {
let sim_request = SimRequestWithTime::new_with_epoch_time(SimCtrlRequest::Ping);
let bytes = postcard::to_allocvec(&sim_request).unwrap();
let deserialized: SimRequestWithTime = postcard::from_bytes(&bytes).unwrap();
assert_eq!(deserialized, sim_request);
}
#[test]
fn test_reply_serde_roundtrip() {
let sim_reply = SimReply::from(SimCtrlReply::Pong);
assert_eq!(sim_reply.component(), ComponentId::SimCtrl);
let bytes = postcard::to_allocvec(&sim_reply).unwrap();
let deserialized: SimReply = postcard::from_bytes(&bytes).unwrap();
assert_eq!(deserialized, sim_reply);
}
}
@@ -1,24 +1,24 @@
[package]
name = "satrs-minisim"
name = "minisim"
version = "0.1.0"
edition = "2021"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
postcard = { version = "1", features = ["alloc"] }
log = "0.4"
thiserror = "2"
fern = "0.7"
strum = { version = "0.28", features = ["derive"] }
num_enum = "0.7"
humantime = "2"
tai-time = { version = "0.3", features = ["serde"] }
nexosim = "1"
satrs = { path = "../../satrs" }
types = { path = "../types" }
minisim-types = { path = "../minisim-types" }
[dev-dependencies]
delegate = "0.13"
@@ -5,7 +5,7 @@ This crate contains a mini-simulator based on the open-source discrete-event sim
[nexosim](https://github.com/asynchronics/nexosim).
Right now, this crate is primarily used together with the
[`satrs-example` application](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example)
[`example-std` application](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/example-std)
to simulate the devices connected to the example application.
You can simply run this application using
@@ -17,7 +17,7 @@ cargo run
or
```sh
cargo run -p satrs-minisim
cargo run -p minisim
```
in the workspace. The mini simulator uses the UDP port 7303 to exchange simulation requests and
@@ -1,10 +1,10 @@
use std::f32::consts::PI;
use minisim_types::{SimReply, acs::mgm};
use nexosim::{
model::{Context, Model},
ports::Output,
};
use satrs_minisim::{acs::mgm, SimReply};
use serde::{Deserialize, Serialize};
use types::pcdu::SwitchStateBinary;
@@ -59,7 +59,7 @@ impl MgmModel {
pub async fn send_sensor_values(&mut self, _: (), cx: &Context<Self>) {
let reply = SimReply::Mgm {
id: self.id,
reply: mgm::Reply::new(
reply: create_reply(
self.switch_state,
self.calculate_current_mgm_tuple(current_millis(cx.time())),
self.spi_fault.mode,
@@ -98,11 +98,46 @@ impl MgmModel {
}
}
/// Builds the reply of the simulated LIS3MDL, including the raw register values.
fn create_reply(
switch_state: SwitchStateBinary,
sensor_values: mgm::SensorValuesMicroTesla,
fault_mode: mgm::SpiFaultMode,
) -> mgm::Reply {
// An injected fault always wins. A switched off device reads back like an undriven bus.
let raw = match (fault_mode, switch_state) {
(mgm::SpiFaultMode::AllZeros, _) => mgm::RawValues::splat(mgm::ALL_ZEROS_SENSOR_VAL),
(mgm::SpiFaultMode::AllOnes, _) | (mgm::SpiFaultMode::None, SwitchStateBinary::Off) => {
mgm::RawValues::splat(mgm::ALL_ONES_SENSOR_VAL)
}
(mgm::SpiFaultMode::None, SwitchStateBinary::On) => {
raw_values_from_microtesla(sensor_values)
}
};
mgm::Reply {
switch_state,
sensor_values,
raw,
}
}
fn raw_values_from_microtesla(values: mgm::SensorValuesMicroTesla) -> mgm::RawValues {
let to_raw = |microtesla: f32| {
(microtesla / (mgm::GAUSS_TO_MICROTESLA_FACTOR as f32 * mgm::FIELD_LSB_PER_GAUSS_4_SENS))
.round() as i16
};
mgm::RawValues {
x: to_raw(values.x),
y: to_raw(values.y),
z: to_raw(values.z),
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use satrs_minisim::{acs::mgm, SimReply, SimRequest};
use minisim_types::{SimReply, SimRequest, acs::mgm};
use types::pcdu::{SwitchId, SwitchStateBinary};
use crate::{
@@ -1,15 +1,18 @@
use nexosim::{
model::{schedulable, Context, Model},
ports::Output,
};
use satrs_minisim::{
acs::{mgm, mgt},
use minisim_types::{
SimReply,
acs::{mgm, mgt},
};
use nexosim::{
model::{Context, Model, schedulable},
ports::Output,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use types::pcdu::SwitchStateBinary;
/// Time the device needs to answer a command.
const REPLY_DELAY: Duration = Duration::from_millis(15);
/// Simple magnetorquer simulation model.
#[derive(Serialize, Deserialize)]
pub struct MgtModel {
@@ -39,6 +42,9 @@ impl MgtModel {
duration_and_dipole: (Duration, mgt::Dipole),
cx: &Context<Self>,
) {
if self.switch_state != SwitchStateBinary::On {
return;
}
self.torque_dipole = duration_and_dipole.1;
self.torquing = true;
if cx
@@ -48,6 +54,7 @@ impl MgtModel {
log::warn!("torque clearing can only be set for a future time.");
}
self.generate_magnetic_field(()).await;
self.schedule_reply(mgt::Reply::Ack, cx);
}
#[nexosim(schedulable)]
@@ -69,22 +76,22 @@ impl MgtModel {
if self.switch_state != SwitchStateBinary::On {
return;
}
cx.schedule_event(
Duration::from_millis(15),
schedulable!(Self::send_housekeeping_data),
(),
)
.expect("requesting housekeeping data failed")
// The HK is sampled when the command is processed, not when the reply is sent.
let hk = mgt::HkSet {
dipole: self.torque_dipole,
torquing: self.torquing,
};
self.schedule_reply(mgt::Reply::Hk(hk), cx);
}
fn schedule_reply(&self, reply: mgt::Reply, cx: &Context<Self>) {
cx.schedule_event(REPLY_DELAY, schedulable!(Self::send_reply), reply)
.expect("scheduling MGT reply failed")
}
#[nexosim(schedulable)]
async fn send_housekeeping_data(&mut self) {
self.reply
.send(SimReply::from(mgt::Reply::Hk(mgt::HkSet {
dipole: self.torque_dipole,
torquing: self.torquing,
})))
.await;
async fn send_reply(&mut self, reply: mgt::Reply) {
self.reply.send(SimReply::from(reply)).await;
}
fn calc_magnetic_field(&self, _: mgt::Dipole) -> mgm::SensorValuesMicroTesla {
@@ -109,19 +116,26 @@ impl MgtModel {
mod tests {
use std::time::Duration;
use satrs_minisim::{
use minisim_types::{
SimReply, SimRequest, SimRequestWithTime,
acs::{mgm, mgt},
eps::PcduRequest,
SimReply, SimRequest, SimRequestWithTime,
};
use types::pcdu::{SwitchId, SwitchStateBinary};
use crate::{eps::tests::switch_device_on, test_helpers::SimTestbench};
fn decode_reply(sim_reply: SimReply) -> mgt::Reply {
let SimReply::Mgt(frame) = sim_reply else {
panic!("unexpected reply {sim_reply:?}");
};
mgt::Reply::from_frame(&frame).expect("invalid MGT reply frame")
}
fn request_hk(sim_testbench: &mut SimTestbench) -> Option<mgt::HkSet> {
let sim_reply = sim_testbench.request_reply(mgt::Request::RequestHk)?;
let SimReply::Mgt(mgt::Reply::Hk(hk)) = sim_reply else {
panic!("unexpected reply {sim_reply:?}");
let mgt::Reply::Hk(hk) = decode_reply(sim_reply) else {
panic!("unexpected MGT reply");
};
Some(hk)
}
@@ -162,7 +176,11 @@ mod tests {
.send_request(request)
.expect("sending MGT request failed");
sim_testbench.handle_sim_requests_time_agnostic();
sim_testbench.step_until(Duration::from_millis(5)).unwrap();
sim_testbench.step_until(Duration::from_millis(20)).unwrap();
let ack = sim_testbench
.try_receive_next_reply()
.expect("no torque command ack");
assert_eq!(decode_reply(ack), mgt::Reply::Ack);
assert_eq!(
request_hk(&mut sim_testbench),
@@ -183,6 +201,27 @@ mod tests {
);
}
#[test]
fn test_torque_command_not_acked_when_off() {
let mut sim_testbench = SimTestbench::new();
let reply = sim_testbench.request_reply(mgt::Request::ApplyTorque {
duration: Duration::from_millis(100),
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
});
assert!(reply.is_none());
}
#[test]
fn test_invalid_frame_is_dropped() {
let mut sim_testbench = SimTestbench::new();
switch_device_on(&mut sim_testbench, SwitchId::Mgt);
assert!(
sim_testbench
.request_reply(SimRequest::Mgt(vec![0x01, 0x00]))
.is_none()
);
}
/// Processes the request without stepping, so scheduled events like the torque clearing do
/// not fire.
fn process_without_step(sim_testbench: &mut SimTestbench, request: impl Into<SimRequest>) {
@@ -200,13 +239,15 @@ mod tests {
request: mgm::Request::RequestSensorData,
},
);
let sim_reply = sim_testbench
.try_receive_next_reply()
.expect("no MGM reply received");
let SimReply::Mgm { reply, .. } = sim_reply else {
panic!("unexpected reply {sim_reply:?}");
};
reply.sensor_values
// Skips pending MGT replies, for example torque command acks.
loop {
let sim_reply = sim_testbench
.try_receive_next_reply()
.expect("no MGM reply received");
if let SimReply::Mgm { reply, .. } = sim_reply {
return reply.sensor_values;
}
}
}
fn start_torquing(sim_testbench: &mut SimTestbench, duration: Duration) {
File renamed without changes.
@@ -3,15 +3,15 @@ use std::{
time::{Duration, SystemTime},
};
use nexosim::{
ports::{event_queue, EventQueueReader, EventSinkReader, EventSource, SinkState},
simulation::{EventId, ExecutionError, Mailbox, SimInit, Simulation},
time::{Clock, Deadline, MonotonicTime, SystemClock},
};
use satrs_minisim::{
use minisim_types::{
SimCtrlReply, SimCtrlRequest, SimReply, SimRequest, SimRequestWithTime,
acs::{mgm, mgt},
eps::PcduRequest,
SimCtrlReply, SimCtrlRequest, SimReply, SimRequest, SimRequestWithTime,
};
use nexosim::{
ports::{EventQueueReader, EventSinkReader, EventSource, SinkState, event_queue},
simulation::{EventId, ExecutionError, Mailbox, SimInit, Simulation},
time::{Clock, Deadline, MonotonicTime, SystemClock},
};
use types::pcdu::{SwitchId, SwitchStateBinary};
@@ -268,7 +268,14 @@ impl SimController {
}
}
fn handle_mgt_request(&mut self, mgt_request: mgt::Request) {
fn handle_mgt_request(&mut self, frame: Vec<u8>) {
let mgt_request = match mgt::Request::from_frame(&frame) {
Ok(request) => request,
Err(e) => {
log::warn!("dropping invalid MGT frame {frame:02x?}: {e}");
return;
}
};
if MGT_REQ_WIRETAPPING {
log::info!("received MGT request: {mgt_request:?}");
}
@@ -1,10 +1,10 @@
use std::time::Duration;
use minisim_types::{SimReply, eps::PcduReply};
use nexosim::{
model::{schedulable, Context, Model},
model::{Context, Model, schedulable},
ports::Output,
};
use satrs_minisim::{eps::PcduReply, SimReply};
use serde::{Deserialize, Serialize};
use types::pcdu::{SwitchId, SwitchMapBinary, SwitchMapBinaryWrapper, SwitchStateBinary};
@@ -76,7 +76,7 @@ pub(crate) mod tests {
use super::*;
use std::time::Duration;
use satrs_minisim::{eps::PcduRequest, SimRequestWithTime};
use minisim_types::{SimRequestWithTime, eps::PcduRequest};
use types::pcdu::SwitchMapBinary;
use crate::test_helpers::SimTestbench;
@@ -1,6 +1,6 @@
use controller::{SimController, ThreadingModel};
use minisim_types::udp::SIM_CTRL_PORT;
use nexosim::time::MonotonicTime;
use satrs_minisim::udp::SIM_CTRL_PORT;
use std::sync::mpsc;
use std::thread;
use udp::SimUdpServer;
@@ -1,11 +1,11 @@
use delegate::delegate;
use std::sync::mpsc;
use minisim_types::{SimReply, SimRequest, SimRequestWithTime};
use nexosim::{
simulation::ExecutionError,
time::{Deadline, MonotonicTime},
};
use satrs_minisim::{SimReply, SimRequest, SimRequestWithTime};
use crate::controller::{SimController, ThreadingModel};
File renamed without changes.
@@ -2,11 +2,11 @@ use std::{
collections::VecDeque,
io::ErrorKind,
net::{SocketAddr, UdpSocket},
sync::{atomic::AtomicBool, mpsc, Arc},
sync::{Arc, atomic::AtomicBool, mpsc},
time::Duration,
};
use satrs_minisim::{SimReply, SimRequestWithTime};
use minisim_types::{SimReply, SimRequestWithTime};
// A UDP server which handles all TC received by a client application.
pub struct SimUdpServer {
@@ -53,10 +53,10 @@ impl SimUdpServer {
pub fn run(&mut self) {
loop {
if let Some(stop_signal) = &self.stop_signal {
if stop_signal.load(std::sync::atomic::Ordering::Relaxed) {
break;
}
if let Some(stop_signal) = &self.stop_signal
&& stop_signal.load(std::sync::atomic::Ordering::Relaxed)
{
break;
}
let processed_requests = self.process_requests();
let processed_replies = self.process_replies();
@@ -89,7 +89,7 @@ impl SimUdpServer {
self.sender_addr = Some(src);
let sim_req = serde_json::from_slice::<SimRequestWithTime>(&self.req_buf[..bytes_read]);
let sim_req = postcard::from_bytes::<SimRequestWithTime>(&self.req_buf[..bytes_read]);
if let Err(e) = sim_req {
log::warn!("received UDP request with invalid format: {}", e);
return processed_requests;
@@ -130,9 +130,7 @@ impl SimUdpServer {
let next_reply_to_send = self.reply_queue.pop_front().unwrap();
self.socket
.send_to(
serde_json::to_string(&next_reply_to_send)
.unwrap()
.as_bytes(),
&postcard::to_allocvec(&next_reply_to_send).unwrap(),
self.sender_addr.unwrap(),
)
.expect("sending reply failed");
@@ -148,15 +146,16 @@ mod tests {
io::ErrorKind,
net::{SocketAddr, UdpSocket},
sync::{
Arc,
atomic::{AtomicBool, Ordering},
mpsc, Arc,
mpsc,
},
time::Duration,
};
use satrs_minisim::{
eps::{PcduReply, PcduRequest},
use minisim_types::{
SimCtrlReply, SimCtrlRequest, SimReply, SimRequestWithTime,
eps::{PcduReply, PcduRequest},
};
use crate::eps::tests::get_all_off_switch_map;
@@ -171,8 +170,8 @@ mod tests {
pub enum ReceptionError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Serde JSON error: {0}")]
SerdeJson(#[from] serde_json::Error),
#[error("postcard error: {0}")]
Postcard(#[from] postcard::Error),
}
pub struct SimUdpTestClient {
@@ -203,7 +202,8 @@ mod tests {
pub fn send_request(&self, sim_request: &SimRequestWithTime) -> std::io::Result<usize> {
self.socket.send(
&serde_json::to_vec(sim_request).expect("conversion of request to vector failed"),
&postcard::to_allocvec(sim_request)
.expect("conversion of request to vector failed"),
)
}
@@ -213,7 +213,7 @@ mod tests {
pub fn recv_sim_reply(&mut self) -> Result<SimReply, ReceptionError> {
let read_len = self.recv_raw()?;
Ok(serde_json::from_slice(&self.reply_buf[0..read_len])?)
Ok(postcard::from_bytes(&self.reply_buf[0..read_len])?)
}
}
struct UdpTestbench {
@@ -302,8 +302,8 @@ mod tests {
panic!("unexpected request server error: {e}");
}
}
ReceptionError::SerdeJson(json_error) => {
panic!("unexpected JSON error: {json_error}");
ReceptionError::Postcard(postcard_error) => {
panic!("unexpected postcard error: {postcard_error}");
}
},
}
@@ -0,0 +1,29 @@
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
runner = "probe-rs run --chip STM32H753ZITx"
# runner = ["probe-rs", "run", "--chip", "$CHIP", "--log-format", "{L} {s}"]
rustflags = [
"-C", "linker=flip-link",
"-C", "link-arg=-Tlink.x",
"-C", "link-arg=-Tdefmt.x",
# This is needed if your flash or ram addresses are not aligned to 0x10000 in memory.x
# See https://github.com/rust-embedded/cortex-m-quickstart/pull/95
"-C", "link-arg=--nmagic",
# Can be useful for debugging.
# "-Clink-args=-Map=app.map"
]
[build]
# (`thumbv6m-*` is compatible with all ARM Cortex-M chips but using the right
# target improves performance)
# target = "thumbv6m-none-eabi" # Cortex-M0 and Cortex-M0+
# target = "thumbv7m-none-eabi" # Cortex-M3
# target = "thumbv7em-none-eabi" # Cortex-M4 and Cortex-M7 (no FPU)
target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU)
[alias]
rb = "run --bin"
rrb = "run --release --bin"
[env]
DEFMT_LOG = "info"
@@ -1,4 +1,4 @@
/target
/itm.txt
/.cargo/config.toml
/.vscode
/app.map
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,76 @@
[package]
name = "stm32h7-nucleo-embassy"
edition = "2024"
version = "0.1.0"
default-run = "stm32h7-nucleo-embassy"
[lib]
harness = false
# needed for each integration test
[[test]]
name = "integration"
harness = false
[dependencies]
types = { path = "../types" }
cortex-m = { version = "0.7", features = ["critical-section-single-core"] }
arbitrary-int = "2"
cortex-m-rt = "0.7"
defmt = "1"
defmt-rtt = "1"
panic-probe = { version = "1", features = ["print-defmt"] }
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"] }
embassy-time = { version = "0.5", features = ["defmt-timestamp-uptime-ms"] }
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"
# cargo build/run
[profile.dev]
codegen-units = 1
debug = 2
debug-assertions = true # <-
incremental = false
opt-level = 's' # <-
overflow-checks = true # <-
# cargo test
[profile.test]
codegen-units = 1
debug = 2
debug-assertions = true # <-
incremental = false
opt-level = 3 # <-
overflow-checks = true # <-
# cargo build/run --release
[profile.release]
codegen-units = 1
debug = 2
debug-assertions = false # <-
incremental = false
lto = 'fat'
opt-level = 3 # <-
overflow-checks = false # <-
# cargo test --release
[profile.bench]
codegen-units = 1
debug = 2
debug-assertions = false # <-
incremental = false
lto = 'fat'
opt-level = 3 # <-
overflow-checks = false # <-
+116
View File
@@ -0,0 +1,116 @@
sat-rs embassy example for the STM32H753ZI-Nucleo board
=======
This example application shows how the [sat-rs library](https://egit.irs.uni-stuttgart.de/rust/sat-rs)
can be used on an embedded target.
It also shows how a relatively simple OBSW could be built when no standard runtime is available.
It uses the [embassy](https://embassy.dev/) executor as the concurrency framework and the
[defmt](https://defmt.ferrous-systems.com/) framework for logging.
This is the embassy variant of the
[`stm32h7-nucleo-rtic`](../stm32h7-nucleo-rtic) example. Both provide the same functionality.
The STM32H753ZIT device was picked because it is one of the more powerful Cortex-M based STM32
devices. It has more RAM available and allows commanding via Ethernet. The example is written for
the NUCLEO-H753ZI board, which uses the MB1364 Nucleo-144 board layout.
## Pre-Requisites
Make sure the following tools are installed:
1. [`probe-rs`](https://probe.rs/): Application used to flash and debug the MCU.
2. Optional and recommended: [VS Code](https://code.visualstudio.com/) with
[probe-rs plugin](https://marketplace.visualstudio.com/items?itemName=probe-rs.probe-rs-debugger)
for debugging.
## Preparing Rust and the repository
Building an application requires the `thumbv7em-none-eabihf` cross-compiler toolchain.
If you have not installed it yet, you can do so with
```sh
rustup target add thumbv7em-none-eabihf
```
A default `.cargo` config file is provided as `.cargo/config.toml.template`. The build script
copies it to `.cargo/config.toml` if that file does not exist yet. The copy is not tracked by git,
so you can change settings like the runner for your setup.
Cargo reads the configuration before the build script runs, so the very first build on a fresh
checkout does not use it yet and might fail. Simply run the build again, or copy the file
manually beforehand:
```sh
cp .cargo/config.toml.template .cargo/config.toml
```
The configuration file also sets the target so it does not always have to be specified with
the `--target` argument.
## Building
After that, assuming that you have a `.cargo/config.toml` setting the correct build target,
you can simply build the application with
```sh
cargo build
```
## Flashing from the command line
You can flash the application from the command line using `probe-rs`:
```sh
probe-rs run --chip STM32H753ZITx
```
## Debugging with VS Code
The Nucleo board comes with an on-board ST-Link so all that is required to flash and debug
the board is a USB cable. The code in this repository was debugged using [`probe-rs`](https://probe.rs/docs/tools/debuggerA)
and the VS Code [`probe-rs` plugin](https://marketplace.visualstudio.com/items?itemName=probe-rs.probe-rs-debugger).
Make sure to install this plugin first.
## Commanding the board
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 board uses the same TMTC protocol as the
[`example-std`](../example-std) application, which is defined inside the [`types`](../types)
crate.
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"
```
For example, you can then send a ping to the MCU using
```sh
cargo run -p client -- --ping
```
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 -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.
## Resources
- [STM32H743ZI Ethernet link checker example](https://github.com/stm32-rs/stm32h7xx-hal/blob/master/examples/ethernet-nucleo-h743zi2.rs)
- [smoltcp DHCP client](https://github.com/smoltcp-rs/smoltcp/blob/main/examples/dhcp_client.rs)
@@ -3,7 +3,7 @@ use std::{env, fs};
fn main() {
let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
let cargo_dir = manifest_dir.parent().unwrap().join(".cargo");
let cargo_dir = manifest_dir.join(".cargo");
let config = cargo_dir.join("config.toml");
let config_template = cargo_dir.join("config.toml.template");
@@ -0,0 +1,43 @@
//! Blinks an LED
//!
//! This assumes that LD1 (green) is connected to PB0, LD2 (yellow) to PE1 and LD3 (red) to
//! PB14. This assumption is true for the MB1364 Nucleo-144 board, for example the
//! NUCLEO-H753ZI.
#![no_std]
#![no_main]
use embassy_executor::Spawner;
use embassy_stm32::gpio;
use stm32h7_nucleo_embassy as _;
#[embassy_executor::main]
async fn main(spawner: Spawner) {
let p = embassy_stm32::init(Default::default());
defmt::info!("Hello World!");
let ld1 = gpio::Output::new(p.PB0, gpio::Level::High, gpio::Speed::Low);
let ld2 = gpio::Output::new(p.PE1, gpio::Level::High, gpio::Speed::Low);
let ld3 = gpio::Output::new(p.PB14, gpio::Level::High, gpio::Speed::Low);
spawner.spawn(blink(ld1, ld2, ld3).expect("spawning blink task failed"));
}
#[embassy_executor::task]
async fn blink(
mut ld1: gpio::Output<'static>,
mut ld2: gpio::Output<'static>,
mut ld3: gpio::Output<'static>,
) {
loop {
defmt::info!("high");
ld1.set_high();
ld2.set_high();
ld3.set_high();
embassy_time::Timer::after_millis(500).await;
defmt::info!("low");
ld1.set_low();
ld2.set_low();
ld3.set_low();
embassy_time::Timer::after_millis(500).await;
}
}
@@ -2,7 +2,7 @@
#![no_std]
// global logger + panicking-behavior + memory layout
use satrs_stm32h7_nucleo_rtic as _;
use stm32h7_nucleo_embassy as _;
#[cortex_m_rt::entry]
fn main() -> ! {
@@ -0,0 +1,51 @@
#![no_main]
#![no_std]
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]
fn panic() -> ! {
cortex_m::asm::udf()
}
/// Hardfault handler.
///
/// Terminates the application and makes a semihosting-capable debug tool exit
/// with an error. This seems better than the default, which is to spin in a
/// loop.
#[cortex_m_rt::exception]
unsafe fn HardFault(_frame: &cortex_m_rt::ExceptionFrame) -> ! {
panic!("unexpected hard fault");
}
#[cfg(test)]
#[defmt_test::tests]
mod unit_tests {
use defmt::assert;
#[test]
fn it_works() {
assert!(true)
}
}
+393
View File
@@ -0,0 +1,393 @@
#![no_main]
#![no_std]
extern crate alloc;
use alloc::vec::Vec;
use arbitrary_int::u14;
use defmt::Debug2Format;
use embassy_executor::Spawner;
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::{CriticalSectionRawMutex, NoopRawMutex};
use embassy_sync::channel::{Channel, Receiver, Sender};
use embassy_sync::signal::Signal;
use embassy_time::{Duration, Timer};
use spacepackets::{CcsdsPacketIdAndPsc, CcsdsPacketReader, SpHeader};
use static_cell::{ConstStaticCell, StaticCell};
use types::ccsds::{CcsdsCreationError, CcsdsTmPacketOwned};
use types::{Apid, ComponentId, Message, TcHeader, TmHeader, control, led, tmtc};
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;
/// 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();
bind_interrupts!(struct Irqs {
ETH => eth::InterruptHandler;
HASH_RNG => rng::InterruptHandler<peripherals::RNG>;
});
type Device = eth::Ethernet<
'static,
peripherals::ETH,
eth::GenericPhy<eth::Sma<'static, peripherals::ETH_SMA>>,
>;
type TcSender = Sender<'static, NoopRawMutex, Vec<u8>, TC_QUEUE_DEPTH>;
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 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");
// Safety: Called once, before the first allocation.
unsafe { stm32h7_nucleo_embassy::init_heap() };
let mut config = embassy_stm32::Config::default();
{
use embassy_stm32::rcc::*;
config.rcc.hsi = Some(HSIPrescaler::DIV1);
config.rcc.csi = true;
config.rcc.hsi48 = Some(Default::default()); // needed for RNG
config.rcc.pll1 = Some(Pll {
source: PllSource::HSI,
prediv: PllPreDiv::DIV4,
mul: PllMul::MUL50,
divp: Some(PllDiv::DIV2),
divq: None,
divr: None,
});
config.rcc.sys = Sysclk::PLL1_P; // 400 Mhz
config.rcc.ahb_pre = AHBPrescaler::DIV2; // 200 Mhz
config.rcc.apb1_pre = APBPrescaler::DIV2; // 100 Mhz
config.rcc.apb2_pre = APBPrescaler::DIV2; // 100 Mhz
config.rcc.apb3_pre = APBPrescaler::DIV2; // 100 Mhz
config.rcc.apb4_pre = APBPrescaler::DIV2; // 100 Mhz
config.rcc.voltage_scale = VoltageScale::Scale1;
}
let periphs = embassy_stm32::init(config);
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
// for STM32H747XIH, replace p.PB13 for PG12
let device = eth::Ethernet::new(
PACKETS.init(eth::PacketQueue::<4, 4>::new()),
periphs.ETH,
Irqs,
periphs.PA1, // ref_clk
periphs.PA7, // CRS_DV: Carrier Sense
periphs.PC4, // RX_D0: Received Bit 0
periphs.PC5, // RX_D1: Received Bit 1
periphs.PG13, // TX_D0: Transmit Bit 0
periphs.PB13, // TX_D1: Transmit Bit 1
periphs.PG11, // TX_EN: Transmit Enable
MAC_ADDRESS,
periphs.ETH_SMA,
periphs.PA2, // mdio
periphs.PC1, // mdc
);
let net_config = embassy_net::Config::dhcpv4(embassy_net::DhcpConfig::default());
// Generate random seed.
let mut rng = rng::Rng::new(periphs.RNG, Irqs);
let mut seed = [0; 8];
rng.fill_bytes(&mut seed);
let seed = u64::from_le_bytes(seed);
static RESOURCES: StaticCell<StackResources<3>> = StaticCell::new();
let (stack, runner) = embassy_net::new(
device,
net_config,
RESOURCES.init(StackResources::new()),
seed,
);
static TC_CHANNEL: ConstStaticCell<Channel<NoopRawMutex, Vec<u8>, TC_QUEUE_DEPTH>> =
ConstStaticCell::new(Channel::new());
let tc_channel = TC_CHANNEL.take();
static TM_CHANNEL: ConstStaticCell<Channel<NoopRawMutex, Vec<u8>, TM_QUEUE_DEPTH>> =
ConstStaticCell::new(Channel::new());
let tm_channel = TM_CHANNEL.take();
spawner.spawn(net_stack_task(runner).expect("spawning net stack task failed"));
spawner.spawn(
udp_task(stack, tc_channel.sender(), tm_channel.receiver())
.expect("spawning UDP 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(),
Telemetry {
tx: tm_channel.sender(),
sequence_count: u14::new(0),
},
)
.expect("spawning TC handler task failed"),
);
}
#[embassy_executor::task]
async fn heartbeat(mut led: gpio::Output<'static>) {
loop {
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,
};
}
}
#[embassy_executor::task]
async fn net_stack_task(mut runner: embassy_net::Runner<'static, Device>) -> ! {
runner.run().await
}
#[embassy_executor::task]
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];
let mut rx_udp_buf = [0; MTU];
let mut tx_udp_buf = [0; MTU];
let mut rx_buffer = [0; MTU];
loop {
stack.wait_link_up().await;
defmt::info!("Network link is up");
// Ensure DHCP configuration is up before trying connect
stack.wait_config_up().await;
defmt::info!("Network task initialized, config: {}", stack.config_v4());
let mut udp = UdpSocket::new(
stack,
&mut rx_udp_meta,
&mut rx_udp_buf,
&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);
let mut remote_endpoint = None;
loop {
match select3(
udp.recv_from(&mut rx_buffer),
tm_rx.receive(),
stack.wait_link_down(),
)
.await
{
Either3::First(Ok((len, meta))) => {
remote_endpoint = Some(meta.endpoint);
defmt::debug!("UDP RX {}, Meta: {}", len, meta);
tc_tx.send(rx_buffer[0..len].to_vec()).await;
}
Either3::First(Err(e)) => {
defmt::warn!("udp receive error: {}", e);
Timer::after_millis(100).await;
}
// TM is only generated as a response to a TC, so the endpoint is usually known.
Either3::Second(packet) => match remote_endpoint {
Some(endpoint) => match udp.send_to(&packet, endpoint).await {
Ok(_) => defmt::debug!("UDP TX: {} bytes to: {}", packet.len(), endpoint),
Err(e) => defmt::warn!("udp send error: {}", e),
},
None => defmt::warn!("dropping TM, no remote endpoint known"),
},
Either3::Third(()) => {
defmt::warn!("Network link is down");
break;
}
}
}
}
}
#[embassy_executor::task]
async fn tc_handler(tc_rx: TcReceiver, mut telemetry: Telemetry) {
loop {
let tc = tc_rx.receive().await;
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_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(&mut telemetry, tmtc::Event::InvalidTcHeader).await;
continue;
};
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;
}
}
}
}
/// 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)),
}
}
}
@@ -1,7 +1,7 @@
#![no_std]
#![no_main]
use satrs_stm32h7_nucleo_rtic as _; // memory layout + panic handler
use stm32h7_nucleo_embassy as _; // memory layout + panic handler
// See https://crates.io/crates/defmt-test/0.3.0 for more documentation (e.g. about the 'state'
// feature)
Loaded 100 of 149 files, more files were not shown because too many files have changed in this diff. Show more