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