Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1fafe149c1 | |||
| bcc1cc850f | |||
| 2cab8d7fed | |||
| b6612ed861 | |||
| 9d15dbcf09 | |||
| b1253eaad4 | |||
| ae4d26b8bd | |||
| cfcfabb5e3 | |||
| 70f747ad86 | |||
| f44aac6ea2 | |||
| df517af85b | |||
| ae9edf5888 | |||
| 512384026c | |||
| 42c7a3b9ee | |||
| 1e15e3d501 | |||
| d7e6732888 | |||
| c27569a526 | |||
| b2bc87641c | |||
| c8245772bb |
@@ -11,6 +11,9 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
- name: Install libudev-dev on Ubuntu
|
||||||
|
if: ${{ matrix.os == 'ubuntu-latest' }}
|
||||||
|
run: sudo apt update && sudo apt install -y libudev-dev
|
||||||
- run: cargo check
|
- run: cargo check
|
||||||
# Check example with static pool configuration
|
# Check example with static pool configuration
|
||||||
- run: cargo check -p satrs-example --no-default-features
|
- run: cargo check -p satrs-example --no-default-features
|
||||||
@@ -23,6 +26,7 @@ jobs:
|
|||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
- name: Install nextest
|
- name: Install nextest
|
||||||
uses: taiki-e/install-action@nextest
|
uses: taiki-e/install-action@nextest
|
||||||
|
- run: sudo apt update && sudo apt install -y libudev-dev
|
||||||
- run: cargo nextest run --all-features
|
- run: cargo nextest run --all-features
|
||||||
- run: cargo test --doc --all-features
|
- run: cargo test --doc --all-features
|
||||||
|
|
||||||
@@ -57,7 +61,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: dtolnay/rust-toolchain@nightly
|
- uses: dtolnay/rust-toolchain@nightly
|
||||||
- run: RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc -p satrs --all-features
|
- run: RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc -p satrs --all-features --no-deps
|
||||||
|
|
||||||
clippy:
|
clippy:
|
||||||
name: Clippy
|
name: Clippy
|
||||||
@@ -67,4 +71,5 @@ jobs:
|
|||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
components: clippy
|
components: clippy
|
||||||
|
- run: sudo apt update && sudo apt install -y libudev-dev
|
||||||
- run: cargo clippy -- -D warnings
|
- run: cargo clippy -- -D warnings
|
||||||
|
|||||||
+4
-1
@@ -4,9 +4,12 @@ members = [
|
|||||||
"satrs",
|
"satrs",
|
||||||
"satrs-mib",
|
"satrs-mib",
|
||||||
"satrs-example",
|
"satrs-example",
|
||||||
"satrs-minisim",
|
"satrs-example/models",
|
||||||
|
"satrs-example/client",
|
||||||
|
"satrs-example/minisim",
|
||||||
"satrs-shared",
|
"satrs-shared",
|
||||||
"embedded-examples/embedded-client",
|
"embedded-examples/embedded-client",
|
||||||
|
"embedded-examples/models",
|
||||||
]
|
]
|
||||||
|
|
||||||
exclude = [
|
exclude = [
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ toml = "0.9"
|
|||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
satrs-stm32f3-disco-rtic = { path = "../stm32f3-disco-rtic" }
|
satrs-stm32f3-disco-rtic = { path = "../stm32f3-disco-rtic" }
|
||||||
spacepackets = { version = "0.17" }
|
spacepackets = { version = "0.17" }
|
||||||
|
embedded-models = { path = "../models" }
|
||||||
tmtc-utils = { git = "https://egit.irs.uni-stuttgart.de/rust/tmtc-utils.git", version = "0.1" }
|
tmtc-utils = { git = "https://egit.irs.uni-stuttgart.de/rust/tmtc-utils.git", version = "0.1" }
|
||||||
postcard = { version = "1", features = ["alloc"] }
|
postcard = { version = "1", features = ["alloc"] }
|
||||||
cobs = "0.5"
|
cobs = "0.5"
|
||||||
|
|||||||
+8
-49
@@ -1,13 +1,9 @@
|
|||||||
use std::{
|
use std::time::Duration;
|
||||||
fs::File,
|
|
||||||
io::Read,
|
|
||||||
path::Path,
|
|
||||||
time::{Duration, SystemTime},
|
|
||||||
};
|
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use cobs::CobsDecoderOwned;
|
use cobs::CobsDecoderOwned;
|
||||||
use satrs_stm32f3_disco_rtic::Request;
|
use embedded_client::setup_logger;
|
||||||
|
use embedded_models::stm32f3;
|
||||||
use spacepackets::{CcsdsPacketCreatorOwned, CcsdsPacketReader, SpHeader};
|
use spacepackets::{CcsdsPacketCreatorOwned, CcsdsPacketReader, SpHeader};
|
||||||
use tmtc_utils::transport::serial::PacketTransportSerialCobs;
|
use tmtc_utils::transport::serial::PacketTransportSerialCobs;
|
||||||
|
|
||||||
@@ -21,47 +17,11 @@ struct Cli {
|
|||||||
set_led_frequency: Option<u32>,
|
set_led_frequency: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
|
||||||
struct Config {
|
|
||||||
interface: Interface,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
|
||||||
struct Interface {
|
|
||||||
serial_port: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
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::Debug)
|
|
||||||
.chain(std::io::stdout())
|
|
||||||
.chain(fern::log_file("output.log")?)
|
|
||||||
.apply()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
setup_logger().expect("failed to initialize logger");
|
setup_logger().expect("failed to initialize logger");
|
||||||
println!("sat-rs embedded examples TMTC client");
|
println!("-- STM32F3 TMTC client --");
|
||||||
|
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
let mut config_file =
|
let config = embedded_client::Config::new_from_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");
|
|
||||||
println!("Connecting to serial port {}", config.interface.serial_port);
|
|
||||||
|
|
||||||
let serial = serialport::new(config.interface.serial_port, 115200)
|
let serial = serialport::new(config.interface.serial_port, 115200)
|
||||||
.open()
|
.open()
|
||||||
@@ -69,8 +29,7 @@ fn main() {
|
|||||||
let mut transport = PacketTransportSerialCobs::new(serial, CobsDecoderOwned::new(1024));
|
let mut transport = PacketTransportSerialCobs::new(serial, CobsDecoderOwned::new(1024));
|
||||||
|
|
||||||
if cli.ping {
|
if cli.ping {
|
||||||
let request = Request::Ping;
|
let tc = create_stm32f3_tc(&embedded_models::stm32f3::Request::Ping);
|
||||||
let tc = create_stm32f3_tc(&request);
|
|
||||||
log::info!(
|
log::info!(
|
||||||
"Sending ping request with TC ID: {:#010x}",
|
"Sending ping request with TC ID: {:#010x}",
|
||||||
tc.ccsds_packet_id_and_psc().raw()
|
tc.ccsds_packet_id_and_psc().raw()
|
||||||
@@ -79,7 +38,7 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(freq_ms) = cli.set_led_frequency {
|
if let Some(freq_ms) = cli.set_led_frequency {
|
||||||
let request = Request::ChangeBlinkFrequency(Duration::from_millis(freq_ms as u64));
|
let request = stm32f3::Request::ChangeBlinkFrequency(Duration::from_millis(freq_ms as u64));
|
||||||
let tc = create_stm32f3_tc(&request);
|
let tc = create_stm32f3_tc(&request);
|
||||||
log::info!(
|
log::info!(
|
||||||
"Sending change blink frequency request {:?} with TC ID: {:#010x}",
|
"Sending change blink frequency request {:?} with TC ID: {:#010x}",
|
||||||
@@ -100,7 +59,7 @@ fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_stm32f3_tc(request: &Request) -> CcsdsPacketCreatorOwned {
|
fn create_stm32f3_tc(request: &stm32f3::Request) -> CcsdsPacketCreatorOwned {
|
||||||
let req_raw = postcard::to_allocvec(&request).unwrap();
|
let req_raw = postcard::to_allocvec(&request).unwrap();
|
||||||
let sp_header = SpHeader::new_from_apid(satrs_stm32f3_disco_rtic::APID);
|
let sp_header = SpHeader::new_from_apid(satrs_stm32f3_disco_rtic::APID);
|
||||||
CcsdsPacketCreatorOwned::new_tc_with_checksum(sp_header, &req_raw).unwrap()
|
CcsdsPacketCreatorOwned::new_tc_with_checksum(sp_header, &req_raw).unwrap()
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
use std::{fs::File, io::Read, path::Path, time::Duration};
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
use cobs::CobsDecoderOwned;
|
||||||
|
use embedded_client::setup_logger;
|
||||||
|
use embedded_models::{stm32f3, stm32h7};
|
||||||
|
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() {
|
||||||
|
setup_logger().expect("failed to initialize logger");
|
||||||
|
println!("-- STM32H7 TMTC client --");
|
||||||
|
let cli = Cli::parse();
|
||||||
|
let config = embedded_client::Config::new_from_file();
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
if cli.ping {
|
||||||
|
let tc = create_stm32f3_tc(&embedded_models::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_stm32h7_tc(request: &stm32h7::Request) -> CcsdsPacketCreatorOwned {
|
||||||
|
let req_raw = postcard::to_allocvec(&request).unwrap();
|
||||||
|
let sp_header = SpHeader::new_from_apid(satrs_stm32f3_disco_rtic::APID);
|
||||||
|
CcsdsPacketCreatorOwned::new_tc_with_checksum(sp_header, &req_raw).unwrap()
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
use std::{fs::File, io::Read as _, path::Path, time::SystemTime};
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
pub struct Config {
|
||||||
|
pub interface: Interface,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
pub struct Interface {
|
||||||
|
pub serial_port: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
println!("Connecting to serial port {}", config.interface.serial_port);
|
||||||
|
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::Debug)
|
||||||
|
.chain(std::io::stdout())
|
||||||
|
.chain(fern::log_file("output.log")?)
|
||||||
|
.apply()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "embedded-models"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "1", default-features = false }
|
||||||
|
defmt = { version = "1", optional = true }
|
||||||
|
spacepackets = { version = "0.17", default-features = false, features = ["defmt", "serde"] }
|
||||||
|
postcard = { version = "1", features = ["defmt"] }
|
||||||
|
arbitrary-int = "2"
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
#![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 {}
|
||||||
+15
-1
@@ -592,6 +592,17 @@ dependencies = [
|
|||||||
"embedded-io",
|
"embedded-io",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "embedded-models"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"arbitrary-int 2.0.0",
|
||||||
|
"defmt 1.0.1",
|
||||||
|
"postcard",
|
||||||
|
"serde",
|
||||||
|
"spacepackets",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "embedded-storage"
|
name = "embedded-storage"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@@ -942,6 +953,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24"
|
checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cobs 0.3.0",
|
"cobs 0.3.0",
|
||||||
|
"defmt 1.0.1",
|
||||||
"heapless 0.7.17",
|
"heapless 0.7.17",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
@@ -1139,6 +1151,7 @@ dependencies = [
|
|||||||
"defmt-test",
|
"defmt-test",
|
||||||
"embassy-stm32",
|
"embassy-stm32",
|
||||||
"embedded-hal 1.0.0",
|
"embedded-hal 1.0.0",
|
||||||
|
"embedded-models",
|
||||||
"enumset",
|
"enumset",
|
||||||
"heapless 0.9.1",
|
"heapless 0.9.1",
|
||||||
"panic-probe",
|
"panic-probe",
|
||||||
@@ -1245,7 +1258,8 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "spacepackets"
|
name = "spacepackets"
|
||||||
version = "0.17.0"
|
version = "0.17.0"
|
||||||
source = "git+https://egit.irs.uni-stuttgart.de/rust/spacepackets.git#2bc61677105765e69cc96bb1ff9960557c00fa8e"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94979a990b4333f43667cba9fe9e72b9c4e9ada82e09fbe8fb250844358590cc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arbitrary-int 2.0.0",
|
"arbitrary-int 2.0.0",
|
||||||
"bitbybit",
|
"bitbybit",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ default-run = "satrs-stm32f3-disco-rtic"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
embedded-models = { path = "../models", features = ["defmt"] }
|
||||||
cortex-m = { version = "0.7", features = ["critical-section-single-core"] }
|
cortex-m = { version = "0.7", features = ["critical-section-single-core"] }
|
||||||
cortex-m-rt = "0.7"
|
cortex-m-rt = "0.7"
|
||||||
defmt = "1"
|
defmt = "1"
|
||||||
@@ -29,11 +30,6 @@ rtic = { version = "2", features = ["thumbv7-backend"] }
|
|||||||
rtic-sync = { version = "1" }
|
rtic-sync = { version = "1" }
|
||||||
rtic-monotonics = { version = "2", features = ["cortex-m-systick"] }
|
rtic-monotonics = { version = "2", features = ["cortex-m-systick"] }
|
||||||
|
|
||||||
#[dependencies.satrs]
|
|
||||||
# path = "../../satrs"
|
|
||||||
#default-features = false
|
|
||||||
# features = ["defmt"]
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
defmt-test = "0.4"
|
defmt-test = "0.4"
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
#![no_main]
|
#![no_main]
|
||||||
#![no_std]
|
#![no_std]
|
||||||
|
|
||||||
|
use defmt_rtt as _;
|
||||||
|
|
||||||
use arbitrary_int::u11;
|
use arbitrary_int::u11;
|
||||||
use core::time::Duration;
|
|
||||||
use embassy_stm32::gpio::Output;
|
use embassy_stm32::gpio::Output;
|
||||||
use spacepackets::{
|
|
||||||
ccsds_packet_len_for_user_data_len_with_checksum, CcsdsPacketCreationError,
|
|
||||||
CcsdsPacketCreatorWithReservedData, CcsdsPacketIdAndPsc, SpacePacketHeader,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const APID: u11 = u11::new(0x02);
|
pub const APID: u11 = u11::new(0x02);
|
||||||
|
|
||||||
@@ -40,49 +37,6 @@ impl Direction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, defmt::Format, serde::Serialize, serde::Deserialize)]
|
|
||||||
pub enum Request {
|
|
||||||
Ping,
|
|
||||||
ChangeBlinkFrequency(Duration),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, defmt::Format, serde::Serialize, serde::Deserialize)]
|
|
||||||
pub struct TmHeader {
|
|
||||||
pub tc_packet_id: Option<CcsdsPacketIdAndPsc>,
|
|
||||||
pub uptime_millis: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, defmt::Format, serde::Serialize, serde::Deserialize)]
|
|
||||||
pub enum Response {
|
|
||||||
CommandDone,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tm_size(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(
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Leds {
|
pub struct Leds {
|
||||||
pub north: Output<'static>,
|
pub north: Output<'static>,
|
||||||
pub north_east: Output<'static>,
|
pub north_east: Output<'static>,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
#![no_std]
|
#![no_std]
|
||||||
#![no_main]
|
#![no_main]
|
||||||
use arbitrary_int::{u11, u14};
|
use arbitrary_int::u14;
|
||||||
use cortex_m_semihosting::debug::{self, EXIT_FAILURE, EXIT_SUCCESS};
|
use cortex_m_semihosting::debug::{self, EXIT_FAILURE, EXIT_SUCCESS};
|
||||||
use satrs_stm32f3_disco_rtic::{create_tm_packet, tm_size, CcsdsPacketId, Request, Response};
|
use embedded_models::{create_tm_packet, stm32f3, tm_size, TmHeader};
|
||||||
use spacepackets::{CcsdsPacketCreationError, SpHeader};
|
use spacepackets::{CcsdsPacketCreationError, CcsdsPacketIdAndPsc, SpHeader};
|
||||||
|
|
||||||
use defmt_rtt as _; // global logger
|
use defmt_rtt as _; // global logger
|
||||||
|
|
||||||
@@ -23,8 +23,6 @@ const TX_HANDLER_FREQ_MS: u32 = 20;
|
|||||||
const MAX_TC_LEN: usize = 128;
|
const MAX_TC_LEN: usize = 128;
|
||||||
const MAX_TM_LEN: usize = 128;
|
const MAX_TM_LEN: usize = 128;
|
||||||
|
|
||||||
pub const PUS_APID: u11 = u11::new(0x02);
|
|
||||||
|
|
||||||
// This is the predictable maximum overhead of the COBS encoding scheme.
|
// This is the predictable maximum overhead of the COBS encoding scheme.
|
||||||
// It is simply the maximum packet lenght dividied by 254 rounded up.
|
// 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 COBS_TM_OVERHEAD: usize = cobs::max_encoding_overhead(MAX_TM_LEN);
|
||||||
@@ -47,8 +45,8 @@ pub enum TmSendError {
|
|||||||
|
|
||||||
#[derive(Debug, defmt::Format)]
|
#[derive(Debug, defmt::Format)]
|
||||||
pub struct RequestWithTcId {
|
pub struct RequestWithTcId {
|
||||||
pub request: Request,
|
pub request: stm32f3::Request,
|
||||||
pub tc_id: CcsdsPacketId,
|
pub tc_id: CcsdsPacketIdAndPsc,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[app(device = embassy_stm32)]
|
#[app(device = embassy_stm32)]
|
||||||
@@ -57,12 +55,13 @@ mod app {
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use arbitrary_int::u14;
|
use arbitrary_int::u14;
|
||||||
|
use embedded_models::stm32f3::{Request, Response};
|
||||||
use rtic::Mutex;
|
use rtic::Mutex;
|
||||||
use rtic_sync::{
|
use rtic_sync::{
|
||||||
channel::{Receiver, Sender},
|
channel::{Receiver, Sender},
|
||||||
make_channel,
|
make_channel,
|
||||||
};
|
};
|
||||||
use satrs_stm32f3_disco_rtic::{CcsdsPacketId, LedPinSet, Request, Response};
|
use satrs_stm32f3_disco_rtic::LedPinSet;
|
||||||
use spacepackets::CcsdsPacketReader;
|
use spacepackets::CcsdsPacketReader;
|
||||||
|
|
||||||
systick_monotonic!(Mono, 1000);
|
systick_monotonic!(Mono, 1000);
|
||||||
@@ -200,7 +199,7 @@ mod app {
|
|||||||
Ok(packet) => {
|
Ok(packet) => {
|
||||||
let packet_id = packet.packet_id();
|
let packet_id = packet.packet_id();
|
||||||
let psc = packet.psc();
|
let psc = packet.psc();
|
||||||
let tc_packet_id = CcsdsPacketId { packet_id, psc };
|
let tc_packet_id = CcsdsPacketIdAndPsc { packet_id, psc };
|
||||||
if let Ok(request) =
|
if let Ok(request) =
|
||||||
postcard::from_bytes::<Request>(packet.packet_data())
|
postcard::from_bytes::<Request>(packet.packet_data())
|
||||||
{
|
{
|
||||||
@@ -260,17 +259,17 @@ mod app {
|
|||||||
|
|
||||||
fn handle_ping_request(
|
fn handle_ping_request(
|
||||||
cx: &mut req_handler::Context,
|
cx: &mut req_handler::Context,
|
||||||
tc_packet_id: CcsdsPacketId,
|
tc_packet_id: CcsdsPacketIdAndPsc,
|
||||||
) -> Result<(), TmSendError> {
|
) -> Result<(), TmSendError> {
|
||||||
defmt::info!("Received PUS ping telecommand, sending ping reply");
|
defmt::info!("Received PUS ping telecommand, sending ping reply");
|
||||||
send_tm(tc_packet_id, Response::CommandDone, *cx.local.seq_count)?;
|
send_tm(tc_packet_id, Response::Ok, *cx.local.seq_count)?;
|
||||||
*cx.local.seq_count = cx.local.seq_count.wrapping_add(u14::new(1));
|
*cx.local.seq_count = cx.local.seq_count.wrapping_add(u14::new(1));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_change_blink_frequency_request(
|
fn handle_change_blink_frequency_request(
|
||||||
cx: &mut req_handler::Context,
|
cx: &mut req_handler::Context,
|
||||||
tc_packet_id: CcsdsPacketId,
|
tc_packet_id: CcsdsPacketIdAndPsc,
|
||||||
duration: Duration,
|
duration: Duration,
|
||||||
) -> Result<(), TmSendError> {
|
) -> Result<(), TmSendError> {
|
||||||
defmt::info!(
|
defmt::info!(
|
||||||
@@ -280,21 +279,21 @@ mod app {
|
|||||||
cx.shared
|
cx.shared
|
||||||
.blink_freq
|
.blink_freq
|
||||||
.lock(|blink_freq| *blink_freq = duration);
|
.lock(|blink_freq| *blink_freq = duration);
|
||||||
send_tm(tc_packet_id, Response::CommandDone, *cx.local.seq_count)?;
|
send_tm(tc_packet_id, Response::Ok, *cx.local.seq_count)?;
|
||||||
*cx.local.seq_count = cx.local.seq_count.wrapping_add(u14::new(1));
|
*cx.local.seq_count = cx.local.seq_count.wrapping_add(u14::new(1));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_tm(
|
fn send_tm(
|
||||||
tc_packet_id: CcsdsPacketId,
|
tc_packet_id: CcsdsPacketIdAndPsc,
|
||||||
response: Response,
|
response: stm32f3::Response,
|
||||||
current_seq_count: u14,
|
current_seq_count: u14,
|
||||||
) -> Result<(), TmSendError> {
|
) -> Result<(), TmSendError> {
|
||||||
let sp_header = SpHeader::new_for_unseg_tc(PUS_APID, current_seq_count, 0);
|
let sp_header = SpHeader::new_for_unseg_tc(stm32f3::PUS_APID, current_seq_count, 0);
|
||||||
let tm_header = satrs_stm32f3_disco_rtic::TmHeader {
|
let tm_header = TmHeader {
|
||||||
tc_packet_id: Some(tc_packet_id),
|
tc_packet_id: Some(tc_packet_id),
|
||||||
uptime_millis: Mono::now().duration_since_epoch().to_millis(),
|
uptime_millis: Mono::now().duration_since_epoch().to_millis() as u64,
|
||||||
};
|
};
|
||||||
let mut tm_packet = TmPacket::new();
|
let mut tm_packet = TmPacket::new();
|
||||||
let tm_size = tm_size(&tm_header, &response);
|
let tm_size = tm_size(&tm_header, &response);
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
|
|
||||||
runner = "probe-rs run --chip STM32H743ZITx"
|
|
||||||
# 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"
|
|
||||||
+644
-593
File diff suppressed because it is too large
Load Diff
@@ -14,38 +14,28 @@ name = "integration"
|
|||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
embedded-models = { path = "../models", features = ["defmt"] }
|
||||||
|
|
||||||
cortex-m = { version = "0.7", features = ["critical-section-single-core"] }
|
cortex-m = { version = "0.7", features = ["critical-section-single-core"] }
|
||||||
|
arbitrary-int = "2"
|
||||||
cortex-m-rt = "0.7"
|
cortex-m-rt = "0.7"
|
||||||
defmt = "1"
|
defmt = "1"
|
||||||
defmt-brtt = { version = "0.1", default-features = false, features = ["rtt"] }
|
defmt-rtt = "1"
|
||||||
panic-probe = { version = "1", features = ["print-defmt"] }
|
panic-probe = { version = "1", features = ["print-defmt"] }
|
||||||
cortex-m-semihosting = "0.5.0"
|
embedded-alloc = "0.7"
|
||||||
# TODO: Replace with embassy-hal.
|
static_cell = "2"
|
||||||
stm32h7xx-hal = { version="0.16", features= ["stm32h743v", "ethernet"] }
|
rtic = { version = "2", features = ["thumbv7-backend"] }
|
||||||
embedded-alloc = "0.6"
|
spacepackets = { version = "0.17", default-features = false, features = ["defmt"] }
|
||||||
rtic-sync = { version = "1", features = ["defmt-03"] }
|
postcard = "1"
|
||||||
|
|
||||||
[dependencies.smoltcp]
|
embassy-stm32 = { git = "https://github.com/embassy-rs/embassy.git", rev = "dd8e4c14e53f088bae27c5d841ab7a4fa338a52c", version = "0.6", features = ["stm32h743zi", "memory-x", "defmt", "time-driver-any"]}
|
||||||
version = "0.12"
|
|
||||||
default-features = false
|
|
||||||
features = ["medium-ethernet", "proto-ipv4", "socket-raw", "socket-dhcpv4", "socket-udp", "defmt"]
|
|
||||||
|
|
||||||
[dependencies.rtic]
|
embassy-time = { git = "https://github.com/embassy-rs/embassy.git", rev = "dd8e4c14e53f088bae27c5d841ab7a4fa338a52c", version = "0.5", features = ["defmt-timestamp-uptime-ms", "generic-queue-16"] }
|
||||||
version = "2"
|
embassy-net = { git = "https://github.com/embassy-rs/embassy.git", rev = "dd8e4c14e53f088bae27c5d841ab7a4fa338a52c", version = "0.9", features = ["medium-ethernet", "proto-ipv4", "tcp", "udp", "auto-icmp-echo-reply", "dhcpv4", "defmt"] }
|
||||||
features = ["thumbv7-backend"]
|
embassy-sync = { git = "https://github.com/embassy-rs/embassy.git", rev = "dd8e4c14e53f088bae27c5d841ab7a4fa338a52c" }
|
||||||
|
|
||||||
[dependencies.rtic-monotonics]
|
|
||||||
version = "2"
|
|
||||||
features = ["cortex-m-systick"]
|
|
||||||
|
|
||||||
[dependencies.satrs]
|
|
||||||
path = "../../satrs"
|
|
||||||
# version = "0.2"
|
|
||||||
default-features = false
|
|
||||||
features = ["defmt", "heapless"]
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
defmt-test = "0.4"
|
defmt-test = "0.5"
|
||||||
|
|
||||||
# cargo build/run
|
# cargo build/run
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,14 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
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 config = cargo_dir.join("config.toml");
|
||||||
|
let config_template = cargo_dir.join("config.toml.template");
|
||||||
|
|
||||||
|
if !config.exists() && config_template.exists() {
|
||||||
|
fs::create_dir_all(&cargo_dir).unwrap();
|
||||||
|
fs::copy(&config_template, &config).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
/venv
|
|
||||||
/.tmtc-history.txt
|
|
||||||
/log
|
|
||||||
/.idea/*
|
|
||||||
!/.idea/runConfigurations
|
|
||||||
|
|
||||||
/seqcnt.txt
|
|
||||||
/tmtc_conf.json
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"com_if": "udp",
|
|
||||||
"tcpip_udp_port": 7301
|
|
||||||
}
|
|
||||||
@@ -1,305 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Example client for the sat-rs example application"""
|
|
||||||
import struct
|
|
||||||
import logging
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
from typing import Any, Optional, cast
|
|
||||||
from prompt_toolkit.history import FileHistory, History
|
|
||||||
from spacepackets.ecss.tm import CdsShortTimestamp
|
|
||||||
|
|
||||||
import tmtccmd
|
|
||||||
from spacepackets.ecss import PusTelemetry, PusTelecommand, PusTm, PusVerificator
|
|
||||||
from spacepackets.ecss.pus_17_test import Service17Tm
|
|
||||||
from spacepackets.ecss.pus_1_verification import UnpackParams, Service1Tm
|
|
||||||
|
|
||||||
from tmtccmd import TcHandlerBase, ProcedureParamsWrapper
|
|
||||||
from tmtccmd.core.base import BackendRequest
|
|
||||||
from tmtccmd.core.ccsds_backend import QueueWrapper
|
|
||||||
from tmtccmd.logging import add_colorlog_console_logger
|
|
||||||
from tmtccmd.pus import VerificationWrapper
|
|
||||||
from tmtccmd.tmtc import CcsdsTmHandler, SpecificApidHandlerBase
|
|
||||||
from tmtccmd.com import ComInterface
|
|
||||||
from tmtccmd.config import (
|
|
||||||
CmdTreeNode,
|
|
||||||
default_json_path,
|
|
||||||
SetupParams,
|
|
||||||
HookBase,
|
|
||||||
params_to_procedure_conversion,
|
|
||||||
)
|
|
||||||
from tmtccmd.config.com import SerialCfgWrapper
|
|
||||||
from tmtccmd.config import PreArgsParsingWrapper, SetupWrapper
|
|
||||||
from tmtccmd.logging.pus import (
|
|
||||||
RegularTmtcLogWrapper,
|
|
||||||
RawTmtcTimedLogWrapper,
|
|
||||||
TimedLogWhen,
|
|
||||||
)
|
|
||||||
from tmtccmd.tmtc import (
|
|
||||||
TcQueueEntryType,
|
|
||||||
ProcedureWrapper,
|
|
||||||
TcProcedureType,
|
|
||||||
FeedWrapper,
|
|
||||||
SendCbParams,
|
|
||||||
DefaultPusQueueHelper,
|
|
||||||
)
|
|
||||||
from tmtccmd.pus.s5_fsfw_event import Service5Tm
|
|
||||||
from spacepackets.seqcount import FileSeqCountProvider, PusFileSeqCountProvider
|
|
||||||
from tmtccmd.util.obj_id import ObjectIdDictT
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger()
|
|
||||||
|
|
||||||
EXAMPLE_PUS_APID = 0x02
|
|
||||||
|
|
||||||
|
|
||||||
class SatRsConfigHook(HookBase):
|
|
||||||
def __init__(self, json_cfg_path: str):
|
|
||||||
super().__init__(json_cfg_path)
|
|
||||||
|
|
||||||
def get_communication_interface(self, com_if_key: str) -> Optional[ComInterface]:
|
|
||||||
from tmtccmd.config.com import (
|
|
||||||
create_com_interface_default,
|
|
||||||
create_com_interface_cfg_default,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert self.cfg_path is not None
|
|
||||||
cfg = create_com_interface_cfg_default(
|
|
||||||
com_if_key=com_if_key,
|
|
||||||
json_cfg_path=self.cfg_path,
|
|
||||||
space_packet_ids=None,
|
|
||||||
)
|
|
||||||
if cfg is None:
|
|
||||||
raise ValueError(
|
|
||||||
f"No valid configuration could be retrieved for the COM IF with key {com_if_key}"
|
|
||||||
)
|
|
||||||
if cfg.com_if_key == "serial_cobs":
|
|
||||||
cfg = cast(SerialCfgWrapper, cfg)
|
|
||||||
cfg.serial_cfg.serial_timeout = 0.5
|
|
||||||
return create_com_interface_default(cfg)
|
|
||||||
|
|
||||||
def get_command_definitions(self) -> CmdTreeNode:
|
|
||||||
"""This function should return the root node of the command definition tree."""
|
|
||||||
return create_cmd_definition_tree()
|
|
||||||
|
|
||||||
def get_cmd_history(self) -> Optional[History]:
|
|
||||||
"""Optionlly return a history class for the past command paths which will be used
|
|
||||||
when prompting a command path from the user in CLI mode."""
|
|
||||||
return FileHistory(".tmtc-history.txt")
|
|
||||||
|
|
||||||
def get_object_ids(self) -> ObjectIdDictT:
|
|
||||||
from tmtccmd.config.objects import get_core_object_ids
|
|
||||||
|
|
||||||
return get_core_object_ids()
|
|
||||||
|
|
||||||
|
|
||||||
def create_cmd_definition_tree() -> CmdTreeNode:
|
|
||||||
root_node = CmdTreeNode.root_node()
|
|
||||||
root_node.add_child(CmdTreeNode("ping", "Send PUS ping TC"))
|
|
||||||
root_node.add_child(CmdTreeNode("change_blink_freq", "Change blink frequency"))
|
|
||||||
return root_node
|
|
||||||
|
|
||||||
|
|
||||||
class PusHandler(SpecificApidHandlerBase):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
file_logger: logging.Logger,
|
|
||||||
verif_wrapper: VerificationWrapper,
|
|
||||||
raw_logger: RawTmtcTimedLogWrapper,
|
|
||||||
):
|
|
||||||
super().__init__(EXAMPLE_PUS_APID, None)
|
|
||||||
self.file_logger = file_logger
|
|
||||||
self.raw_logger = raw_logger
|
|
||||||
self.verif_wrapper = verif_wrapper
|
|
||||||
|
|
||||||
def handle_tm(self, packet: bytes, _user_args: Any):
|
|
||||||
try:
|
|
||||||
pus_tm = PusTm.unpack(
|
|
||||||
packet, timestamp_len=CdsShortTimestamp.TIMESTAMP_SIZE
|
|
||||||
)
|
|
||||||
except ValueError as e:
|
|
||||||
_LOGGER.warning("Could not generate PUS TM object from raw data")
|
|
||||||
_LOGGER.warning(f"Raw Packet: [{packet.hex(sep=',')}], REPR: {packet!r}")
|
|
||||||
raise e
|
|
||||||
service = pus_tm.service
|
|
||||||
tm_packet = None
|
|
||||||
if service == 1:
|
|
||||||
tm_packet = Service1Tm.unpack(
|
|
||||||
data=packet, params=UnpackParams(CdsShortTimestamp.TIMESTAMP_SIZE, 1, 2)
|
|
||||||
)
|
|
||||||
res = self.verif_wrapper.add_tm(tm_packet)
|
|
||||||
if res is None:
|
|
||||||
_LOGGER.info(
|
|
||||||
f"Received Verification TM[{tm_packet.service}, {tm_packet.subservice}] "
|
|
||||||
f"with Request ID {tm_packet.tc_req_id.as_u32():#08x}"
|
|
||||||
)
|
|
||||||
_LOGGER.warning(
|
|
||||||
f"No matching telecommand found for {tm_packet.tc_req_id}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.verif_wrapper.log_to_console(tm_packet, res)
|
|
||||||
self.verif_wrapper.log_to_file(tm_packet, res)
|
|
||||||
if service == 3:
|
|
||||||
_LOGGER.info("No handling for HK packets implemented")
|
|
||||||
_LOGGER.info(f"Raw packet: 0x[{packet.hex(sep=',')}]")
|
|
||||||
pus_tm = PusTelemetry.unpack(packet, CdsShortTimestamp.TIMESTAMP_SIZE)
|
|
||||||
if pus_tm.subservice == 25:
|
|
||||||
if len(pus_tm.source_data) < 8:
|
|
||||||
raise ValueError("No addressable ID in HK packet")
|
|
||||||
json_str = pus_tm.source_data[8:]
|
|
||||||
_LOGGER.info("received JSON string: " + json_str.decode("utf-8"))
|
|
||||||
if service == 5:
|
|
||||||
tm_packet = Service5Tm.unpack(packet, CdsShortTimestamp.TIMESTAMP_SIZE)
|
|
||||||
if service == 17:
|
|
||||||
tm_packet = Service17Tm.unpack(packet, CdsShortTimestamp.TIMESTAMP_SIZE)
|
|
||||||
if tm_packet.subservice == 2:
|
|
||||||
_LOGGER.info("Received Ping Reply TM[17,2]")
|
|
||||||
else:
|
|
||||||
_LOGGER.info(
|
|
||||||
f"Received Test Packet with unknown subservice {tm_packet.subservice}"
|
|
||||||
)
|
|
||||||
if tm_packet is None:
|
|
||||||
_LOGGER.info(
|
|
||||||
f"The service {service} is not implemented in Telemetry Factory"
|
|
||||||
)
|
|
||||||
tm_packet = PusTelemetry.unpack(packet, CdsShortTimestamp.TIMESTAMP_SIZE)
|
|
||||||
self.raw_logger.log_tm(pus_tm)
|
|
||||||
|
|
||||||
|
|
||||||
def make_addressable_id(target_id: int, unique_id: int) -> bytes:
|
|
||||||
byte_string = bytearray(struct.pack("!I", target_id))
|
|
||||||
byte_string.extend(struct.pack("!I", unique_id))
|
|
||||||
return byte_string
|
|
||||||
|
|
||||||
|
|
||||||
class TcHandler(TcHandlerBase):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
seq_count_provider: FileSeqCountProvider,
|
|
||||||
verif_wrapper: VerificationWrapper,
|
|
||||||
):
|
|
||||||
super(TcHandler, self).__init__()
|
|
||||||
self.seq_count_provider = seq_count_provider
|
|
||||||
self.verif_wrapper = verif_wrapper
|
|
||||||
self.queue_helper = DefaultPusQueueHelper(
|
|
||||||
queue_wrapper=QueueWrapper.empty(),
|
|
||||||
tc_sched_timestamp_len=7,
|
|
||||||
seq_cnt_provider=seq_count_provider,
|
|
||||||
pus_verificator=verif_wrapper.pus_verificator,
|
|
||||||
default_pus_apid=EXAMPLE_PUS_APID,
|
|
||||||
)
|
|
||||||
|
|
||||||
def send_cb(self, send_params: SendCbParams):
|
|
||||||
entry_helper = send_params.entry
|
|
||||||
if entry_helper.is_tc:
|
|
||||||
if entry_helper.entry_type == TcQueueEntryType.PUS_TC:
|
|
||||||
pus_tc_wrapper = entry_helper.to_pus_tc_entry()
|
|
||||||
pus_tc_wrapper.pus_tc.seq_count = (
|
|
||||||
self.seq_count_provider.get_and_increment()
|
|
||||||
)
|
|
||||||
self.verif_wrapper.add_tc(pus_tc_wrapper.pus_tc)
|
|
||||||
raw_tc = pus_tc_wrapper.pus_tc.pack()
|
|
||||||
_LOGGER.info(f"Sending {pus_tc_wrapper.pus_tc}")
|
|
||||||
send_params.com_if.send(raw_tc)
|
|
||||||
elif entry_helper.entry_type == TcQueueEntryType.LOG:
|
|
||||||
log_entry = entry_helper.to_log_entry()
|
|
||||||
_LOGGER.info(log_entry.log_str)
|
|
||||||
|
|
||||||
def queue_finished_cb(self, info: ProcedureWrapper):
|
|
||||||
if info.proc_type == TcProcedureType.TREE_COMMANDING:
|
|
||||||
def_proc = info.to_tree_commanding_procedure()
|
|
||||||
_LOGGER.info(f"Queue handling finished for command {def_proc.cmd_path}")
|
|
||||||
|
|
||||||
def feed_cb(self, info: ProcedureWrapper, wrapper: FeedWrapper):
|
|
||||||
q = self.queue_helper
|
|
||||||
q.queue_wrapper = wrapper.queue_wrapper
|
|
||||||
if info.proc_type == TcProcedureType.TREE_COMMANDING:
|
|
||||||
def_proc = info.to_tree_commanding_procedure()
|
|
||||||
cmd_path = def_proc.cmd_path
|
|
||||||
if cmd_path == "/ping":
|
|
||||||
q.add_log_cmd("Sending PUS ping telecommand")
|
|
||||||
q.add_pus_tc(PusTelecommand(service=17, subservice=1))
|
|
||||||
if cmd_path == "/change_blink_freq":
|
|
||||||
self.create_change_blink_freq_command(q)
|
|
||||||
|
|
||||||
def create_change_blink_freq_command(self, q: DefaultPusQueueHelper):
|
|
||||||
q.add_log_cmd("Changing blink frequency")
|
|
||||||
while True:
|
|
||||||
blink_freq = int(
|
|
||||||
input(
|
|
||||||
"Please specify new blink frequency in ms. Valid Range [2..10000]: "
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if blink_freq < 2 or blink_freq > 10000:
|
|
||||||
print(
|
|
||||||
"Invalid blink frequency. Please specify a value between 2 and 10000."
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
break
|
|
||||||
app_data = struct.pack("!I", blink_freq)
|
|
||||||
q.add_pus_tc(PusTelecommand(service=8, subservice=1, app_data=app_data))
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
add_colorlog_console_logger(_LOGGER)
|
|
||||||
tmtccmd.init_printout(False)
|
|
||||||
hook_obj = SatRsConfigHook(json_cfg_path=default_json_path())
|
|
||||||
parser_wrapper = PreArgsParsingWrapper()
|
|
||||||
parser_wrapper.create_default_parent_parser()
|
|
||||||
parser_wrapper.create_default_parser()
|
|
||||||
parser_wrapper.add_def_proc_args()
|
|
||||||
params = SetupParams()
|
|
||||||
post_args_wrapper = parser_wrapper.parse(hook_obj, params)
|
|
||||||
proc_wrapper = ProcedureParamsWrapper()
|
|
||||||
if post_args_wrapper.use_gui:
|
|
||||||
post_args_wrapper.set_params_without_prompts(proc_wrapper)
|
|
||||||
else:
|
|
||||||
post_args_wrapper.set_params_with_prompts(proc_wrapper)
|
|
||||||
params.apid = EXAMPLE_PUS_APID
|
|
||||||
setup_args = SetupWrapper(
|
|
||||||
hook_obj=hook_obj, setup_params=params, proc_param_wrapper=proc_wrapper
|
|
||||||
)
|
|
||||||
# Create console logger helper and file loggers
|
|
||||||
tmtc_logger = RegularTmtcLogWrapper()
|
|
||||||
file_logger = tmtc_logger.logger
|
|
||||||
raw_logger = RawTmtcTimedLogWrapper(when=TimedLogWhen.PER_HOUR, interval=1)
|
|
||||||
verificator = PusVerificator()
|
|
||||||
verification_wrapper = VerificationWrapper(verificator, _LOGGER, file_logger)
|
|
||||||
# Create primary TM handler and add it to the CCSDS Packet Handler
|
|
||||||
tm_handler = PusHandler(file_logger, verification_wrapper, raw_logger)
|
|
||||||
ccsds_handler = CcsdsTmHandler(generic_handler=None)
|
|
||||||
ccsds_handler.add_apid_handler(tm_handler)
|
|
||||||
|
|
||||||
# Create TC handler
|
|
||||||
seq_count_provider = PusFileSeqCountProvider()
|
|
||||||
tc_handler = TcHandler(seq_count_provider, verification_wrapper)
|
|
||||||
tmtccmd.setup(setup_args=setup_args)
|
|
||||||
init_proc = params_to_procedure_conversion(setup_args.proc_param_wrapper)
|
|
||||||
tmtc_backend = tmtccmd.create_default_tmtc_backend(
|
|
||||||
setup_wrapper=setup_args,
|
|
||||||
tm_handler=ccsds_handler,
|
|
||||||
tc_handler=tc_handler,
|
|
||||||
init_procedure=init_proc,
|
|
||||||
)
|
|
||||||
tmtccmd.start(tmtc_backend=tmtc_backend, hook_obj=hook_obj)
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
state = tmtc_backend.periodic_op(None)
|
|
||||||
if state.request == BackendRequest.TERMINATION_NO_ERROR:
|
|
||||||
sys.exit(0)
|
|
||||||
elif state.request == BackendRequest.DELAY_IDLE:
|
|
||||||
_LOGGER.info("TMTC Client in IDLE mode")
|
|
||||||
time.sleep(3.0)
|
|
||||||
elif state.request == BackendRequest.DELAY_LISTENER:
|
|
||||||
time.sleep(0.8)
|
|
||||||
elif state.request == BackendRequest.DELAY_CUSTOM:
|
|
||||||
if state.next_delay.total_seconds() <= 0.4:
|
|
||||||
time.sleep(state.next_delay.total_seconds())
|
|
||||||
else:
|
|
||||||
time.sleep(0.4)
|
|
||||||
elif state.request == BackendRequest.CALL_NEXT:
|
|
||||||
pass
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
tmtccmd == 8.0.1
|
|
||||||
# -e git+https://github.com/robamu-org/tmtccmd.git@main#egg=tmtccmd
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[toolchain]
|
||||||
|
targets = ["thumbv7em-none-eabihf"]
|
||||||
@@ -5,51 +5,53 @@
|
|||||||
|
|
||||||
#![no_std]
|
#![no_std]
|
||||||
#![no_main]
|
#![no_main]
|
||||||
|
use rtic::app;
|
||||||
use satrs_stm32h7_nucleo_rtic as _;
|
use satrs_stm32h7_nucleo_rtic as _;
|
||||||
|
|
||||||
use stm32h7xx_hal::{block, prelude::*, timer::Timer};
|
#[app(device = embassy_stm32, peripherals = false, dispatchers = [SPI1])]
|
||||||
|
mod app {
|
||||||
|
use embassy_stm32::gpio;
|
||||||
|
|
||||||
use cortex_m_rt::entry;
|
#[shared]
|
||||||
|
struct Shared {}
|
||||||
|
|
||||||
#[entry]
|
#[local]
|
||||||
fn main() -> ! {
|
struct Local {}
|
||||||
defmt::println!("starting stm32h7 blinky example");
|
|
||||||
|
|
||||||
// Get access to the device specific peripherals from the peripheral access crate
|
#[init]
|
||||||
let dp = stm32h7xx_hal::stm32::Peripherals::take().unwrap();
|
fn init(_cx: init::Context) -> (Shared, Local) {
|
||||||
|
let p = embassy_stm32::init(Default::default());
|
||||||
|
defmt::info!("Hello World!");
|
||||||
|
// Configure gpio B pin 0 as a push-pull output.
|
||||||
|
let ld1 = gpio::Output::new(p.PB0, gpio::Level::High, gpio::Speed::Low);
|
||||||
|
let ld2 = gpio::Output::new(p.PB7, gpio::Level::High, gpio::Speed::Low);
|
||||||
|
let ld3 = gpio::Output::new(p.PB14, gpio::Level::High, gpio::Speed::Low);
|
||||||
|
|
||||||
// Take ownership over the RCC devices and convert them into the corresponding HAL structs
|
// Schedule the blinking task
|
||||||
let rcc = dp.RCC.constrain();
|
blink::spawn(ld1, ld2, ld3).ok();
|
||||||
|
|
||||||
let pwr = dp.PWR.constrain();
|
(Shared {}, Local {})
|
||||||
let pwrcfg = pwr.freeze();
|
}
|
||||||
|
|
||||||
// Freeze the configuration of all the clocks in the system and
|
#[task()]
|
||||||
// retrieve the Core Clock Distribution and Reset (CCDR) object
|
async fn blink(
|
||||||
let rcc = rcc.use_hse(8.MHz()).bypass_hse();
|
_cx: blink::Context,
|
||||||
let ccdr = rcc.freeze(pwrcfg, &dp.SYSCFG);
|
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;
|
||||||
|
|
||||||
// Acquire the GPIOB peripheral
|
defmt::info!("low");
|
||||||
let gpiob = dp.GPIOB.split(ccdr.peripheral.GPIOB);
|
ld1.set_low();
|
||||||
|
ld2.set_low();
|
||||||
// Configure gpio B pin 0 as a push-pull output.
|
ld3.set_low();
|
||||||
let mut ld1 = gpiob.pb0.into_push_pull_output();
|
embassy_time::Timer::after_millis(500).await;
|
||||||
|
}
|
||||||
// Configure gpio B pin 7 as a push-pull output.
|
|
||||||
let mut ld2 = gpiob.pb7.into_push_pull_output();
|
|
||||||
|
|
||||||
// Configure gpio B pin 14 as a push-pull output.
|
|
||||||
let mut ld3 = gpiob.pb14.into_push_pull_output();
|
|
||||||
|
|
||||||
// Configure the timer to trigger an update every second
|
|
||||||
let mut timer = Timer::tim1(dp.TIM1, ccdr.peripheral.TIM1, &ccdr.clocks);
|
|
||||||
timer.start(1.Hz());
|
|
||||||
|
|
||||||
// Wait for the timer to trigger an update and change the state of the LED
|
|
||||||
loop {
|
|
||||||
ld1.toggle();
|
|
||||||
ld2.toggle();
|
|
||||||
ld3.toggle();
|
|
||||||
block!(timer.wait()).unwrap();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
#![no_main]
|
#![no_main]
|
||||||
#![no_std]
|
#![no_std]
|
||||||
|
|
||||||
use satrs_stm32h7_nucleo_rtic as _; // global logger + panicking-behavior + memory layout
|
// global logger + panicking-behavior + memory layout
|
||||||
|
use satrs_stm32h7_nucleo_rtic as _;
|
||||||
|
|
||||||
#[cortex_m_rt::entry]
|
#[cortex_m_rt::entry]
|
||||||
fn main() -> ! {
|
fn main() -> ! {
|
||||||
defmt::println!("Hello, world!");
|
loop {
|
||||||
|
defmt::println!("Hello, world!");
|
||||||
satrs_stm32h7_nucleo_rtic::exit()
|
cortex_m::asm::delay(100_000_000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
#![no_main]
|
#![no_main]
|
||||||
#![no_std]
|
#![no_std]
|
||||||
|
|
||||||
use cortex_m_semihosting::debug;
|
use defmt_rtt as _;
|
||||||
|
use embassy_stm32 as _;
|
||||||
use defmt_brtt as _; // global logger
|
|
||||||
|
|
||||||
// TODO(5) adjust HAL import
|
|
||||||
use stm32h7xx_hal as _; // memory layout
|
|
||||||
|
|
||||||
use panic_probe as _;
|
use panic_probe as _;
|
||||||
|
|
||||||
// same panicking *behavior* as `panic-probe` but doesn't print a panic message
|
// same panicking *behavior* as `panic-probe` but doesn't print a panic message
|
||||||
@@ -17,14 +12,6 @@ fn panic() -> ! {
|
|||||||
cortex_m::asm::udf()
|
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(debug::EXIT_SUCCESS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Hardfault handler.
|
/// Hardfault handler.
|
||||||
///
|
///
|
||||||
/// Terminates the application and makes a semihosting-capable debug tool exit
|
/// Terminates the application and makes a semihosting-capable debug tool exit
|
||||||
@@ -32,9 +19,7 @@ pub fn exit() -> ! {
|
|||||||
/// loop.
|
/// loop.
|
||||||
#[cortex_m_rt::exception]
|
#[cortex_m_rt::exception]
|
||||||
unsafe fn HardFault(_frame: &cortex_m_rt::ExceptionFrame) -> ! {
|
unsafe fn HardFault(_frame: &cortex_m_rt::ExceptionFrame) -> ! {
|
||||||
loop {
|
panic!("unexpected hard fault");
|
||||||
debug::exit(debug::EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// defmt-test 0.3.0 has the limitation that this `#[tests]` attribute can only be used
|
// defmt-test 0.3.0 has the limitation that this `#[tests]` attribute can only be used
|
||||||
|
|||||||
@@ -3,422 +3,215 @@
|
|||||||
extern crate alloc;
|
extern crate alloc;
|
||||||
|
|
||||||
use rtic::app;
|
use rtic::app;
|
||||||
use rtic_monotonics::systick::prelude::*;
|
|
||||||
use satrs::pool::{PoolAddr, PoolProvider, StaticHeaplessMemoryPool};
|
|
||||||
use satrs::static_subpool;
|
|
||||||
// global logger + panicking-behavior + memory layout
|
// global logger + panicking-behavior + memory layout
|
||||||
|
use embassy_stm32::bind_interrupts;
|
||||||
use satrs_stm32h7_nucleo_rtic as _;
|
use satrs_stm32h7_nucleo_rtic as _;
|
||||||
use smoltcp::socket::udp::UdpMetadata;
|
|
||||||
use smoltcp::socket::{dhcpv4, udp};
|
|
||||||
|
|
||||||
use core::mem::MaybeUninit;
|
use core::mem::MaybeUninit;
|
||||||
use embedded_alloc::LlffHeap as Heap;
|
use embedded_alloc::LlffHeap as Heap;
|
||||||
use smoltcp::iface::{Config, Interface, SocketHandle, SocketSet, SocketStorage};
|
|
||||||
use smoltcp::wire::{HardwareAddress, IpAddress, IpCidr};
|
|
||||||
use stm32h7xx_hal::ethernet;
|
|
||||||
|
|
||||||
const DEFAULT_BLINK_FREQ_MS: u32 = 1000;
|
const DEFAULT_BLINK_FREQ_MS: u32 = 1000;
|
||||||
const PORT: u16 = 7301;
|
const PORT: u16 = 7301;
|
||||||
|
|
||||||
const HEAP_SIZE: usize = 131_072;
|
const HEAP_SIZE: usize = 131_072;
|
||||||
|
|
||||||
const TC_SOURCE_CHANNEL_DEPTH: usize = 16;
|
|
||||||
pub type SharedPool = StaticHeaplessMemoryPool<3>;
|
|
||||||
pub type TcSourceChannel = rtic_sync::channel::Channel<PoolAddr, TC_SOURCE_CHANNEL_DEPTH>;
|
|
||||||
pub type TcSourceTx = rtic_sync::channel::Sender<'static, PoolAddr, TC_SOURCE_CHANNEL_DEPTH>;
|
|
||||||
pub type TcSourceRx = rtic_sync::channel::Receiver<'static, PoolAddr, TC_SOURCE_CHANNEL_DEPTH>;
|
|
||||||
|
|
||||||
#[global_allocator]
|
#[global_allocator]
|
||||||
static HEAP: Heap = Heap::empty();
|
static HEAP: Heap = Heap::empty();
|
||||||
|
|
||||||
systick_monotonic!(Mono, 1000);
|
|
||||||
|
|
||||||
// We place the memory pool buffers inside the larger AXISRAM.
|
|
||||||
pub const SUBPOOL_SMALL_NUM_BLOCKS: u16 = 32;
|
|
||||||
pub const SUBPOOL_SMALL_BLOCK_SIZE: usize = 32;
|
|
||||||
pub const SUBPOOL_MEDIUM_NUM_BLOCKS: u16 = 16;
|
|
||||||
pub const SUBPOOL_MEDIUM_BLOCK_SIZE: usize = 128;
|
|
||||||
pub const SUBPOOL_LARGE_NUM_BLOCKS: u16 = 8;
|
|
||||||
pub const SUBPOOL_LARGE_BLOCK_SIZE: usize = 2048;
|
|
||||||
|
|
||||||
// This data will be held by Net through a mutable reference
|
|
||||||
pub struct NetStorageStatic<'a> {
|
|
||||||
socket_storage: [SocketStorage<'a>; 8],
|
|
||||||
}
|
|
||||||
// MaybeUninit allows us write code that is correct even if STORE is not
|
|
||||||
// initialised by the runtime
|
|
||||||
static mut STORE: MaybeUninit<NetStorageStatic> = MaybeUninit::uninit();
|
|
||||||
|
|
||||||
static mut UDP_RX_META: [udp::PacketMetadata; 12] = [udp::PacketMetadata::EMPTY; 12];
|
|
||||||
static mut UDP_RX: [u8; 2048] = [0; 2048];
|
|
||||||
static mut UDP_TX_META: [udp::PacketMetadata; 12] = [udp::PacketMetadata::EMPTY; 12];
|
|
||||||
static mut UDP_TX: [u8; 2048] = [0; 2048];
|
|
||||||
|
|
||||||
/// Locally administered MAC address
|
/// Locally administered MAC address
|
||||||
const MAC_ADDRESS: [u8; 6] = [0x02, 0x00, 0x11, 0x22, 0x33, 0x44];
|
const MAC_ADDRESS: [u8; 6] = [0x02, 0x00, 0x11, 0x22, 0x33, 0x44];
|
||||||
|
|
||||||
pub struct Net {
|
const TC_QUEUE_DEPTH: usize = 32;
|
||||||
iface: Interface,
|
const TM_QUEUE_DEPTH: usize = 32;
|
||||||
ethdev: ethernet::EthernetDMA<4, 4>,
|
|
||||||
dhcp_handle: SocketHandle,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Net {
|
#[app(device = embassy_stm32, peripherals = false)]
|
||||||
pub fn new(
|
|
||||||
sockets: &mut SocketSet<'static>,
|
|
||||||
mut ethdev: ethernet::EthernetDMA<4, 4>,
|
|
||||||
ethernet_addr: HardwareAddress,
|
|
||||||
) -> Self {
|
|
||||||
let config = Config::new(ethernet_addr);
|
|
||||||
let mut iface = Interface::new(
|
|
||||||
config,
|
|
||||||
&mut ethdev,
|
|
||||||
smoltcp::time::Instant::from_millis(Mono::now().duration_since_epoch().to_millis()),
|
|
||||||
);
|
|
||||||
// Create sockets
|
|
||||||
let dhcp_socket = dhcpv4::Socket::new();
|
|
||||||
iface.update_ip_addrs(|addrs| {
|
|
||||||
let _ = addrs.push(IpCidr::new(IpAddress::v4(192, 168, 1, 99), 0));
|
|
||||||
});
|
|
||||||
|
|
||||||
let dhcp_handle = sockets.add(dhcp_socket);
|
|
||||||
Net {
|
|
||||||
iface,
|
|
||||||
ethdev,
|
|
||||||
dhcp_handle,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Polls on the ethernet interface. You should refer to the smoltcp
|
|
||||||
/// documentation for poll() to understand how to call poll efficiently
|
|
||||||
pub fn poll<'a>(&mut self, sockets: &'a mut SocketSet) -> bool {
|
|
||||||
let uptime = Mono::now().duration_since_epoch();
|
|
||||||
let timestamp = smoltcp::time::Instant::from_millis(uptime.to_millis());
|
|
||||||
|
|
||||||
self.iface.poll(timestamp, &mut self.ethdev, sockets)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn poll_dhcp<'a>(&mut self, sockets: &'a mut SocketSet) -> Option<dhcpv4::Event<'a>> {
|
|
||||||
let opt_event = sockets.get_mut::<dhcpv4::Socket>(self.dhcp_handle).poll();
|
|
||||||
if let Some(event) = &opt_event {
|
|
||||||
match event {
|
|
||||||
dhcpv4::Event::Deconfigured => {
|
|
||||||
defmt::info!("DHCP lost configuration");
|
|
||||||
self.iface.update_ip_addrs(|addrs| addrs.clear());
|
|
||||||
self.iface.routes_mut().remove_default_ipv4_route();
|
|
||||||
}
|
|
||||||
dhcpv4::Event::Configured(config) => {
|
|
||||||
defmt::info!("DHCP configuration acquired");
|
|
||||||
defmt::info!("IP address: {}", config.address);
|
|
||||||
self.iface.update_ip_addrs(|addrs| {
|
|
||||||
addrs.clear();
|
|
||||||
addrs.push(IpCidr::Ipv4(config.address)).unwrap();
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(router) = config.router {
|
|
||||||
defmt::debug!("Default gateway: {}", router);
|
|
||||||
self.iface
|
|
||||||
.routes_mut()
|
|
||||||
.add_default_ipv4_route(router)
|
|
||||||
.unwrap();
|
|
||||||
} else {
|
|
||||||
defmt::debug!("Default gateway: None");
|
|
||||||
self.iface.routes_mut().remove_default_ipv4_route();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
opt_event
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct UdpNet {
|
|
||||||
udp_handle: SocketHandle,
|
|
||||||
last_client: Option<UdpMetadata>,
|
|
||||||
tc_source_tx: TcSourceTx,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UdpNet {
|
|
||||||
pub fn new<'sockets>(sockets: &mut SocketSet<'sockets>, tc_source_tx: TcSourceTx) -> Self {
|
|
||||||
// SAFETY: The RX and TX buffers are passed here and not used anywhere else.
|
|
||||||
let udp_rx_buffer =
|
|
||||||
smoltcp::socket::udp::PacketBuffer::new(unsafe { &mut UDP_RX_META[..] }, unsafe {
|
|
||||||
&mut UDP_RX[..]
|
|
||||||
});
|
|
||||||
let udp_tx_buffer =
|
|
||||||
smoltcp::socket::udp::PacketBuffer::new(unsafe { &mut UDP_TX_META[..] }, unsafe {
|
|
||||||
&mut UDP_TX[..]
|
|
||||||
});
|
|
||||||
let udp_socket = smoltcp::socket::udp::Socket::new(udp_rx_buffer, udp_tx_buffer);
|
|
||||||
|
|
||||||
let udp_handle = sockets.add(udp_socket);
|
|
||||||
Self {
|
|
||||||
udp_handle,
|
|
||||||
last_client: None,
|
|
||||||
tc_source_tx,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn poll<'sockets>(
|
|
||||||
&mut self,
|
|
||||||
sockets: &'sockets mut SocketSet,
|
|
||||||
shared_pool: &mut SharedPool,
|
|
||||||
) {
|
|
||||||
let socket = sockets.get_mut::<udp::Socket>(self.udp_handle);
|
|
||||||
if !socket.is_open() {
|
|
||||||
if let Err(e) = socket.bind(PORT) {
|
|
||||||
defmt::warn!("binding UDP socket failed: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
loop {
|
|
||||||
match socket.recv() {
|
|
||||||
Ok((data, client)) => {
|
|
||||||
match shared_pool.add(data) {
|
|
||||||
Ok(store_addr) => {
|
|
||||||
if let Err(e) = self.tc_source_tx.try_send(store_addr) {
|
|
||||||
defmt::warn!("TC source channel is full: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
defmt::warn!("could not add UDP packet to shared pool: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.last_client = Some(client);
|
|
||||||
// TODO: Implement packet wiretapping.
|
|
||||||
}
|
|
||||||
Err(e) => match e {
|
|
||||||
udp::RecvError::Exhausted => {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
udp::RecvError::Truncated => {
|
|
||||||
defmt::warn!("UDP packet was truncacted");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[app(device = stm32h7xx_hal::stm32, peripherals = true)]
|
|
||||||
mod app {
|
mod app {
|
||||||
use core::ptr::addr_of_mut;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use rtic_monotonics::fugit::MillisDurationU32;
|
use arbitrary_int::u14;
|
||||||
use satrs::spacepackets::ecss::tc::PusTcReader;
|
use embassy_net::udp::UdpSocket;
|
||||||
use stm32h7xx_hal::ethernet::{EthernetMAC, PHY};
|
use embassy_net::StackResources;
|
||||||
use stm32h7xx_hal::gpio::{Output, Pin};
|
use embassy_stm32::eth;
|
||||||
use stm32h7xx_hal::prelude::*;
|
use embassy_stm32::gpio;
|
||||||
use stm32h7xx_hal::stm32::Interrupt;
|
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_models::create_tm_packet;
|
||||||
|
use embedded_models::stm32h7;
|
||||||
|
use embedded_models::tm_size;
|
||||||
|
use embedded_models::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 {
|
struct BlinkyLeds {
|
||||||
led1: Pin<'B', 7, Output>,
|
led1: gpio::Output<'static>,
|
||||||
led2: Pin<'B', 14, Output>,
|
led2: gpio::Output<'static>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[local]
|
#[local]
|
||||||
struct Local {
|
struct Local {
|
||||||
|
net_runner: embassy_net::Runner<'static, Device>,
|
||||||
|
net_stack: embassy_net::Stack<'static>,
|
||||||
leds: BlinkyLeds,
|
leds: BlinkyLeds,
|
||||||
link_led: Pin<'B', 0, Output>,
|
link_led: gpio::Output<'static>,
|
||||||
net: Net,
|
tc_rx: embassy_sync::channel::Receiver<
|
||||||
udp: UdpNet,
|
'static,
|
||||||
tc_source_rx: TcSourceRx,
|
NoopRawMutex,
|
||||||
phy: ethernet::phy::LAN8742A<EthernetMAC>,
|
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]
|
#[shared]
|
||||||
struct Shared {
|
struct Shared {
|
||||||
blink_freq: MillisDurationU32,
|
sequence_count: u14,
|
||||||
eth_link_up: bool,
|
blink_freq: embassy_time::Duration,
|
||||||
sockets: SocketSet<'static>,
|
|
||||||
shared_pool: SharedPool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[init]
|
#[init]
|
||||||
fn init(mut cx: init::Context) -> (Shared, Local) {
|
fn init(_cx: init::Context) -> (Shared, Local) {
|
||||||
defmt::println!("Starting sat-rs demo application for the STM32H743ZIT");
|
defmt::println!("Starting sat-rs demo application for the STM32H743ZIT");
|
||||||
|
|
||||||
let pwr = cx.device.PWR.constrain();
|
let mut config = embassy_stm32::Config::default();
|
||||||
let pwrcfg = pwr.freeze();
|
{
|
||||||
|
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 rcc = cx.device.RCC.constrain();
|
let link_led = gpio::Output::new(periphs.PB0, gpio::Level::Low, gpio::Speed::Medium);
|
||||||
// Try to keep the clock configuration similar to one used in STM examples:
|
let mut led1 = gpio::Output::new(periphs.PB7, gpio::Level::Low, gpio::Speed::Medium);
|
||||||
// https://github.com/STMicroelectronics/STM32CubeH7/blob/master/Projects/NUCLEO-H743ZI/Examples/GPIO/GPIO_EXTI/Src/main.c
|
let mut led2 = gpio::Output::new(periphs.PB14, gpio::Level::Low, gpio::Speed::Medium);
|
||||||
let ccdr = rcc
|
|
||||||
.sys_ck(400.MHz())
|
|
||||||
.hclk(200.MHz())
|
|
||||||
.use_hse(8.MHz())
|
|
||||||
.bypass_hse()
|
|
||||||
.pclk1(100.MHz())
|
|
||||||
.pclk2(100.MHz())
|
|
||||||
.pclk3(100.MHz())
|
|
||||||
.pclk4(100.MHz())
|
|
||||||
.freeze(pwrcfg, &cx.device.SYSCFG);
|
|
||||||
|
|
||||||
// Initialize the systick interrupt & obtain the token to prove that we did
|
|
||||||
Mono::start(cx.core.SYST, ccdr.clocks.sys_ck().to_Hz());
|
|
||||||
|
|
||||||
// Those are used in the smoltcp of the stm32h7xx-hal , I am not fully sure what they are
|
|
||||||
// good for.
|
|
||||||
cx.core.SCB.enable_icache();
|
|
||||||
cx.core.DWT.enable_cycle_counter();
|
|
||||||
|
|
||||||
let gpioa = cx.device.GPIOA.split(ccdr.peripheral.GPIOA);
|
|
||||||
let gpiob = cx.device.GPIOB.split(ccdr.peripheral.GPIOB);
|
|
||||||
let gpioc = cx.device.GPIOC.split(ccdr.peripheral.GPIOC);
|
|
||||||
let gpiog = cx.device.GPIOG.split(ccdr.peripheral.GPIOG);
|
|
||||||
|
|
||||||
let link_led = gpiob.pb0.into_push_pull_output();
|
|
||||||
let mut led1 = gpiob.pb7.into_push_pull_output();
|
|
||||||
let mut led2 = gpiob.pb14.into_push_pull_output();
|
|
||||||
|
|
||||||
// Criss-cross pattern looks cooler.
|
// Criss-cross pattern looks cooler.
|
||||||
led1.set_high();
|
led1.set_high();
|
||||||
led2.set_low();
|
led2.set_low();
|
||||||
let leds = BlinkyLeds { led1, led2 };
|
let leds = BlinkyLeds { led1, led2 };
|
||||||
|
|
||||||
let rmii_ref_clk = gpioa.pa1.into_alternate::<11>();
|
static PACKETS: StaticCell<eth::PacketQueue<4, 4>> = StaticCell::new();
|
||||||
let rmii_mdio = gpioa.pa2.into_alternate::<11>();
|
// warning: Not all STM32H7 devices have the exact same pins here
|
||||||
let rmii_mdc = gpioc.pc1.into_alternate::<11>();
|
// for STM32H747XIH, replace p.PB13 for PG12
|
||||||
let rmii_crs_dv = gpioa.pa7.into_alternate::<11>();
|
let device = eth::Ethernet::new(
|
||||||
let rmii_rxd0 = gpioc.pc4.into_alternate::<11>();
|
PACKETS.init(eth::PacketQueue::<4, 4>::new()),
|
||||||
let rmii_rxd1 = gpioc.pc5.into_alternate::<11>();
|
periphs.ETH,
|
||||||
let rmii_tx_en = gpiog.pg11.into_alternate::<11>();
|
Irqs,
|
||||||
let rmii_txd0 = gpiog.pg13.into_alternate::<11>();
|
periphs.PA1, // ref_clk
|
||||||
let rmii_txd1 = gpiob.pb13.into_alternate::<11>();
|
periphs.PA7, // CRS_DV: Carrier Sense
|
||||||
|
periphs.PC4, // RX_D0: Received Bit 0
|
||||||
let mac_addr = smoltcp::wire::EthernetAddress::from_bytes(&MAC_ADDRESS);
|
periphs.PC5, // RX_D1: Received Bit 1
|
||||||
|
periphs.PG13, // TX_D0: Transmit Bit 0
|
||||||
/// Ethernet descriptor rings are a global singleton
|
periphs.PB13, // TX_D1: Transmit Bit 1
|
||||||
#[link_section = ".sram3.eth"]
|
periphs.PG11, // TX_EN: Transmit Enable
|
||||||
static mut DES_RING: MaybeUninit<ethernet::DesRing<4, 4>> = MaybeUninit::uninit();
|
MAC_ADDRESS,
|
||||||
|
periphs.ETH_SMA,
|
||||||
let (eth_dma, eth_mac) = ethernet::new(
|
periphs.PA2, // mdio
|
||||||
cx.device.ETHERNET_MAC,
|
periphs.PC1, // mdc
|
||||||
cx.device.ETHERNET_MTL,
|
|
||||||
cx.device.ETHERNET_DMA,
|
|
||||||
(
|
|
||||||
rmii_ref_clk,
|
|
||||||
rmii_mdio,
|
|
||||||
rmii_mdc,
|
|
||||||
rmii_crs_dv,
|
|
||||||
rmii_rxd0,
|
|
||||||
rmii_rxd1,
|
|
||||||
rmii_tx_en,
|
|
||||||
rmii_txd0,
|
|
||||||
rmii_txd1,
|
|
||||||
),
|
|
||||||
// SAFETY: We do not move the returned DMA struct across thread boundaries, so this
|
|
||||||
// should be safe according to the docs.
|
|
||||||
unsafe { DES_RING.assume_init_mut() },
|
|
||||||
mac_addr,
|
|
||||||
ccdr.peripheral.ETH1MAC,
|
|
||||||
&ccdr.clocks,
|
|
||||||
);
|
|
||||||
// Initialise ethernet PHY...
|
|
||||||
let mut lan8742a = ethernet::phy::LAN8742A::new(eth_mac.set_phy_addr(0));
|
|
||||||
lan8742a.phy_reset();
|
|
||||||
lan8742a.phy_init();
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
ethernet::enable_interrupt();
|
|
||||||
cx.core.NVIC.set_priority(Interrupt::ETH, 196); // Mid prio
|
|
||||||
cortex_m::peripheral::NVIC::unmask(Interrupt::ETH);
|
|
||||||
}
|
|
||||||
|
|
||||||
// unsafe: mutable reference to static storage, we only do this once
|
|
||||||
let store = unsafe {
|
|
||||||
let store_ptr = STORE.as_mut_ptr();
|
|
||||||
|
|
||||||
// Initialise the socket_storage field. Using `write` instead of
|
|
||||||
// assignment via `=` to not call `drop` on the old, uninitialised
|
|
||||||
// value
|
|
||||||
addr_of_mut!((*store_ptr).socket_storage).write([SocketStorage::EMPTY; 8]);
|
|
||||||
|
|
||||||
// Now that all fields are initialised we can safely use
|
|
||||||
// assume_init_mut to return a mutable reference to STORE
|
|
||||||
STORE.assume_init_mut()
|
|
||||||
};
|
|
||||||
|
|
||||||
let (tc_source_tx, tc_source_rx) =
|
|
||||||
rtic_sync::make_channel!(PoolAddr, TC_SOURCE_CHANNEL_DEPTH);
|
|
||||||
|
|
||||||
let mut sockets = SocketSet::new(&mut store.socket_storage[..]);
|
|
||||||
let net = Net::new(&mut sockets, eth_dma, mac_addr.into());
|
|
||||||
let udp = UdpNet::new(&mut sockets, tc_source_tx);
|
|
||||||
|
|
||||||
let mut shared_pool: SharedPool = StaticHeaplessMemoryPool::new(true);
|
|
||||||
static_subpool!(
|
|
||||||
SUBPOOL_SMALL,
|
|
||||||
SUBPOOL_SMALL_SIZES,
|
|
||||||
SUBPOOL_SMALL_NUM_BLOCKS as usize,
|
|
||||||
SUBPOOL_SMALL_BLOCK_SIZE,
|
|
||||||
link_section = ".axisram"
|
|
||||||
);
|
|
||||||
static_subpool!(
|
|
||||||
SUBPOOL_MEDIUM,
|
|
||||||
SUBPOOL_MEDIUM_SIZES,
|
|
||||||
SUBPOOL_MEDIUM_NUM_BLOCKS as usize,
|
|
||||||
SUBPOOL_MEDIUM_BLOCK_SIZE,
|
|
||||||
link_section = ".axisram"
|
|
||||||
);
|
|
||||||
static_subpool!(
|
|
||||||
SUBPOOL_LARGE,
|
|
||||||
SUBPOOL_LARGE_SIZES,
|
|
||||||
SUBPOOL_LARGE_NUM_BLOCKS as usize,
|
|
||||||
SUBPOOL_LARGE_BLOCK_SIZE,
|
|
||||||
link_section = ".axisram"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
shared_pool
|
let config = embassy_net::Config::dhcpv4(embassy_net::DhcpConfig::default());
|
||||||
.grow(
|
|
||||||
SUBPOOL_SMALL.get_mut().unwrap(),
|
// Generate random seed.
|
||||||
SUBPOOL_SMALL_SIZES.get_mut().unwrap(),
|
let mut rng = rng::Rng::new(periphs.RNG, Irqs);
|
||||||
SUBPOOL_SMALL_NUM_BLOCKS,
|
let mut seed = [0; 8];
|
||||||
true,
|
rng.fill_bytes(&mut seed);
|
||||||
)
|
let seed = u64::from_le_bytes(seed);
|
||||||
.expect("growing heapless memory pool failed");
|
|
||||||
shared_pool
|
// Init network stack
|
||||||
.grow(
|
static RESOURCES: StaticCell<StackResources<3>> = StaticCell::new();
|
||||||
SUBPOOL_MEDIUM.get_mut().unwrap(),
|
let (stack, runner) =
|
||||||
SUBPOOL_MEDIUM_SIZES.get_mut().unwrap(),
|
embassy_net::new(device, config, RESOURCES.init(StackResources::new()), seed);
|
||||||
SUBPOOL_MEDIUM_NUM_BLOCKS,
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
.expect("growing heapless memory pool failed");
|
|
||||||
shared_pool
|
|
||||||
.grow(
|
|
||||||
SUBPOOL_LARGE.get_mut().unwrap(),
|
|
||||||
SUBPOOL_LARGE_SIZES.get_mut().unwrap(),
|
|
||||||
SUBPOOL_LARGE_NUM_BLOCKS,
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
.expect("growing heapless memory pool failed");
|
|
||||||
|
|
||||||
// Set up global allocator. Use AXISRAM for the heap.
|
// Set up global allocator. Use AXISRAM for the heap.
|
||||||
#[link_section = ".axisram"]
|
#[link_section = ".axisram"]
|
||||||
static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE];
|
static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE];
|
||||||
unsafe { HEAP.init(&raw mut HEAP_MEM as usize, HEAP_SIZE) }
|
unsafe { HEAP.init(&raw mut HEAP_MEM as usize, HEAP_SIZE) }
|
||||||
|
|
||||||
eth_link_check::spawn().expect("eth link check failed");
|
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");
|
blinky::spawn().expect("spawning blink task failed");
|
||||||
udp_task::spawn().expect("spawning UDP task failed");
|
tc_handler::spawn().expect("spawning TC handler task failed");
|
||||||
tc_source_task::spawn().expect("spawning TC source task failed");
|
|
||||||
|
|
||||||
(
|
(
|
||||||
Shared {
|
Shared {
|
||||||
blink_freq: MillisDurationU32::from_ticks(DEFAULT_BLINK_FREQ_MS),
|
blink_freq: Duration::from_millis(DEFAULT_BLINK_FREQ_MS as u64),
|
||||||
eth_link_up: false,
|
sequence_count: u14::new(0),
|
||||||
sockets,
|
|
||||||
shared_pool,
|
|
||||||
},
|
},
|
||||||
Local {
|
Local {
|
||||||
link_led,
|
link_led,
|
||||||
leds,
|
leds,
|
||||||
net,
|
net_runner: runner,
|
||||||
udp,
|
net_stack: stack,
|
||||||
tc_source_rx,
|
tc_tx: tc_sender,
|
||||||
phy: lan8742a,
|
tc_rx: tc_receiver,
|
||||||
|
tm_tx: tm_sender,
|
||||||
|
tm_rx: tm_receiver,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -430,94 +223,159 @@ mod app {
|
|||||||
leds.led1.toggle();
|
leds.led1.toggle();
|
||||||
leds.led2.toggle();
|
leds.led2.toggle();
|
||||||
let current_blink_freq = cx.shared.blink_freq.lock(|current| *current);
|
let current_blink_freq = cx.shared.blink_freq.lock(|current| *current);
|
||||||
Mono::delay(current_blink_freq).await;
|
Timer::after_millis(current_blink_freq.as_millis()).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This task checks for the network link.
|
#[task(local=[net_runner])]
|
||||||
#[task(local=[link_led, phy], shared=[eth_link_up])]
|
async fn net_lib_task(cx: net_lib_task::Context) {
|
||||||
async fn eth_link_check(mut cx: eth_link_check::Context) {
|
cx.local.net_runner.run().await;
|
||||||
let phy = cx.local.phy;
|
}
|
||||||
let link_led = cx.local.link_led;
|
|
||||||
|
#[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 {
|
loop {
|
||||||
let link_was_up = cx.shared.eth_link_up.lock(|link_up| *link_up);
|
cx.local.net_stack.wait_link_up().await;
|
||||||
if phy.poll_link() {
|
cx.local.link_led.set_high();
|
||||||
if !link_was_up {
|
defmt::info!("Network link is up");
|
||||||
link_led.set_high();
|
|
||||||
cx.shared.eth_link_up.lock(|link_up| *link_up = true);
|
// Ensure DHCP configuration is up before trying connect
|
||||||
defmt::info!("Ethernet link up");
|
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;
|
||||||
}
|
}
|
||||||
} else if link_was_up {
|
match udp
|
||||||
link_led.set_low();
|
.recv_from(&mut rx_buffer)
|
||||||
cx.shared.eth_link_up.lock(|link_up| *link_up = false);
|
.with_timeout(Duration::from_millis(200))
|
||||||
defmt::info!("Ethernet link down");
|
.await
|
||||||
}
|
{
|
||||||
Mono::delay(100.millis()).await;
|
Ok(result) => match result {
|
||||||
}
|
Ok((data, meta)) => {
|
||||||
}
|
remote_endpoint = Some(meta.endpoint);
|
||||||
|
defmt::debug!("UDP RX {}, Meta: {}", data, meta);
|
||||||
#[task(binds=ETH, local=[net], shared=[sockets])]
|
cx.local.tc_tx.send(rx_buffer[0..data].to_vec()).await;
|
||||||
fn eth_isr(mut cx: eth_isr::Context) {
|
|
||||||
// SAFETY: We do not write the register mentioned inside the docs anywhere else.
|
|
||||||
unsafe {
|
|
||||||
ethernet::interrupt_handler();
|
|
||||||
}
|
|
||||||
// Check and process ETH frames and DHCP. UDP is checked in a different task.
|
|
||||||
cx.shared.sockets.lock(|sockets| {
|
|
||||||
cx.local.net.poll(sockets);
|
|
||||||
cx.local.net.poll_dhcp(sockets);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This task routes UDP packets.
|
|
||||||
#[task(local=[udp], shared=[sockets, shared_pool])]
|
|
||||||
async fn udp_task(mut cx: udp_task::Context) {
|
|
||||||
loop {
|
|
||||||
cx.shared.sockets.lock(|sockets| {
|
|
||||||
cx.shared.shared_pool.lock(|pool| {
|
|
||||||
cx.local.udp.poll(sockets, pool);
|
|
||||||
})
|
|
||||||
});
|
|
||||||
Mono::delay(40.millis()).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This task handles all the incoming telecommands.
|
|
||||||
#[task(local=[read_buf: [u8; 1024] = [0; 1024], tc_source_rx], shared=[shared_pool])]
|
|
||||||
async fn tc_source_task(mut cx: tc_source_task::Context) {
|
|
||||||
loop {
|
|
||||||
let recv_result = cx.local.tc_source_rx.recv().await;
|
|
||||||
match recv_result {
|
|
||||||
Ok(pool_addr) => {
|
|
||||||
cx.shared.shared_pool.lock(|pool| {
|
|
||||||
match pool.read(&pool_addr, cx.local.read_buf.as_mut()) {
|
|
||||||
Ok(packet_len) => {
|
|
||||||
defmt::info!("received {} bytes in the TC source task", packet_len);
|
|
||||||
match PusTcReader::new(&cx.local.read_buf[0..packet_len]) {
|
|
||||||
Ok((packet, _tc_len)) => {
|
|
||||||
// TODO: Handle packet here or dispatch to dedicated PUS
|
|
||||||
// handler? Dispatching could simplify some things and make
|
|
||||||
// the software more scalable..
|
|
||||||
defmt::info!("received PUS packet: {}", packet);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
defmt::info!("invalid TC format, not a PUS packet: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Err(e) = pool.delete(pool_addr) {
|
|
||||||
defmt::warn!("deleting TC data failed: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
defmt::warn!("TC packet read failed: {}", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
Err(e) => {
|
||||||
|
defmt::warn!("udp receive error: {}", e);
|
||||||
|
Timer::after_millis(100).await;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(_e) => (),
|
||||||
}
|
}
|
||||||
Err(e) => {
|
if let Some(endpoint) = remote_endpoint {
|
||||||
defmt::warn!("TC source reception error: {}", e);
|
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 => {
|
||||||
|
stm32h7::Response::Ok
|
||||||
|
}
|
||||||
|
stm32h7::Request::ChangeBlinkFrequency(duration) => {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#![no_std]
|
#![no_std]
|
||||||
#![no_main]
|
#![no_main]
|
||||||
|
|
||||||
use stm32h7_testapp as _; // memory layout + panic handler
|
use satrs_stm32h7_nucleo_rtic as _; // memory layout + panic handler
|
||||||
|
|
||||||
// See https://crates.io/crates/defmt-test/0.3.0 for more documentation (e.g. about the 'state'
|
// See https://crates.io/crates/defmt-test/0.3.0 for more documentation (e.g. about the 'state'
|
||||||
// feature)
|
// feature)
|
||||||
|
|||||||
@@ -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": "STM32H743ZITx",
|
|
||||||
"coreConfigs": [
|
|
||||||
{
|
|
||||||
"programBinary": "${workspaceFolder}/target/thumbv7em-none-eabihf/debug/satrs-stm32h7-nucleo-rtic",
|
|
||||||
"rttEnabled": true,
|
|
||||||
"svdFile": "STM32H743.svd"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -11,15 +11,23 @@ test:
|
|||||||
cargo nextest run --all-features
|
cargo nextest run --all-features
|
||||||
cargo test --doc --all-features
|
cargo test --doc --all-features
|
||||||
|
|
||||||
embedded:
|
embedded: embedded-stm32h7 embedded-stm32f3
|
||||||
cargo check -p satrs --target=thumbv7em-none-eabihf --no-default-features
|
cargo check -p satrs --target=thumbv7em-none-eabihf --no-default-features
|
||||||
|
|
||||||
fmt:
|
[working-directory:"embedded-examples/stm32h7-nucleo-rtic"]
|
||||||
cargo fmt --all
|
embedded-stm32h7:
|
||||||
|
cargo build --target=thumbv7em-none-eabihf --release
|
||||||
|
|
||||||
|
[working-directory:"embedded-examples/stm32f3-disco-rtic"]
|
||||||
|
embedded-stm32f3:
|
||||||
|
cargo build --target=thumbv7em-none-eabihf --release
|
||||||
|
|
||||||
check-fmt:
|
check-fmt:
|
||||||
cargo fmt --all -- --check
|
cargo fmt --all -- --check
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
cargo fmt --all
|
||||||
|
|
||||||
clippy:
|
clippy:
|
||||||
cargo clippy -- -D warnings
|
cargo clippy -- -D warnings
|
||||||
|
|
||||||
|
|||||||
+11
-15
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "satrs-example"
|
name = "satrs-example"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
authors = ["Robin Mueller <muellerr@irs.uni-stuttgart.de>"]
|
authors = ["Robin Mueller <muellerr@irs.uni-stuttgart.de>"]
|
||||||
default-run = "satrs-example"
|
default-run = "satrs-example"
|
||||||
homepage = "https://egit.irs.uni-stuttgart.de/rust/sat-rs"
|
homepage = "https://egit.irs.uni-stuttgart.de/rust/sat-rs"
|
||||||
@@ -18,28 +18,24 @@ csv = "1"
|
|||||||
num_enum = "0.7"
|
num_enum = "0.7"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
lazy_static = "1"
|
lazy_static = "1"
|
||||||
strum = { version = "0.27", features = ["derive"] }
|
strum = { version = "0.28", features = ["derive"] }
|
||||||
derive-new = "0.7"
|
derive-new = "0.7"
|
||||||
cfg-if = "1"
|
cfg-if = "1"
|
||||||
arbitrary-int = "2"
|
arbitrary-int = "2"
|
||||||
bitbybit = "1.4"
|
bitbybit = "2"
|
||||||
|
postcard = "1"
|
||||||
|
ctrlc = "3"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|
||||||
[dependencies.satrs]
|
satrs = { path = "../satrs", features = ["test_util"] }
|
||||||
path = "../satrs"
|
models = { path = "./models" }
|
||||||
features = ["test_util"]
|
satrs-minisim = { path = "./minisim" }
|
||||||
|
satrs-mib = { path = "../satrs-mib" }
|
||||||
[dependencies.satrs-minisim]
|
|
||||||
path = "../satrs-minisim"
|
|
||||||
|
|
||||||
[dependencies.satrs-mib]
|
|
||||||
version = "0.1.1"
|
|
||||||
path = "../satrs-mib"
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["heap_tmtc"]
|
# default = ["heap_tmtc"]
|
||||||
heap_tmtc = []
|
# heap_tmtc = []
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[package]
|
||||||
|
name = "client"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
log = "0.4"
|
||||||
|
fern = "0.7"
|
||||||
|
humantime = "2"
|
||||||
|
serde = { version = "1" }
|
||||||
|
satrs-example = { path = ".." }
|
||||||
|
models = { path = "../models" }
|
||||||
|
spacepackets = { version = "0.17", git = "https://egit.irs.uni-stuttgart.de/rust/spacepackets.git", default-features = false }
|
||||||
|
bitbybit = "2"
|
||||||
|
arbitrary-int = "2"
|
||||||
|
ctrlc = { version = "3.5" }
|
||||||
|
postcard = { version = "1" }
|
||||||
|
anyhow = "1"
|
||||||
@@ -0,0 +1,334 @@
|
|||||||
|
use anyhow::bail;
|
||||||
|
use arbitrary_int::u11;
|
||||||
|
use clap::Parser as _;
|
||||||
|
use models::{Apid, MessageType, TcHeader, mgm::request::HkRequest};
|
||||||
|
use satrs_example::config::{OBSW_SERVER_ADDR, SERVER_PORT};
|
||||||
|
use spacepackets::{CcsdsPacketIdAndPsc, SpacePacketHeader};
|
||||||
|
use std::{
|
||||||
|
net::{IpAddr, SocketAddr, UdpSocket},
|
||||||
|
sync::{
|
||||||
|
Arc,
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
},
|
||||||
|
time::{Duration, SystemTime},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(clap::Parser)]
|
||||||
|
pub struct Cli {
|
||||||
|
#[arg(short, long)]
|
||||||
|
ping: bool,
|
||||||
|
#[arg(short, long)]
|
||||||
|
test_event: bool,
|
||||||
|
|
||||||
|
#[command(subcommand)]
|
||||||
|
commands: Option<Commands>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(clap::Subcommand)]
|
||||||
|
enum Commands {
|
||||||
|
Mgm0(MgmArgs),
|
||||||
|
Mgm1(MgmArgs),
|
||||||
|
MgmAssy(MgmAssemblyArgs),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Commands {
|
||||||
|
#[inline]
|
||||||
|
pub fn target_id(&self) -> models::ComponentId {
|
||||||
|
match self {
|
||||||
|
Commands::Mgm0(_mgm_args) => models::ComponentId::AcsMgm0,
|
||||||
|
Commands::Mgm1(_mgm_args) => models::ComponentId::AcsMgm1,
|
||||||
|
Commands::MgmAssy(_mgm_assembly_args) => models::ComponentId::AcsMgmAssembly,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::Parser)]
|
||||||
|
struct MgmArgs {
|
||||||
|
#[arg(short, long)]
|
||||||
|
ping: bool,
|
||||||
|
#[arg(long)]
|
||||||
|
request_hk: bool,
|
||||||
|
#[arg(short, long)]
|
||||||
|
mode: Option<DeviceModeSelect>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::Parser)]
|
||||||
|
struct MgmAssemblyArgs {
|
||||||
|
#[arg(short, long)]
|
||||||
|
ping: bool,
|
||||||
|
#[arg(short, long)]
|
||||||
|
mode: Option<AssemblyModeSelect>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
|
||||||
|
pub enum DeviceModeSelect {
|
||||||
|
Off,
|
||||||
|
Normal,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
|
||||||
|
pub enum AssemblyModeSelect {
|
||||||
|
NoModeKeeping,
|
||||||
|
Off,
|
||||||
|
Normal,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_logger(level: log::LevelFilter) -> 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(level)
|
||||||
|
.chain(std::io::stdout())
|
||||||
|
.chain(fern::log_file("output.log")?)
|
||||||
|
.apply()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> anyhow::Result<()> {
|
||||||
|
setup_logger(log::LevelFilter::Debug).unwrap();
|
||||||
|
let kill_signal = Arc::new(AtomicBool::new(false));
|
||||||
|
let ctrl_kill_signal = kill_signal.clone();
|
||||||
|
ctrlc::set_handler(move || ctrl_kill_signal.store(true, Ordering::Relaxed)).unwrap();
|
||||||
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
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");
|
||||||
|
client.set_nonblocking(true)?;
|
||||||
|
client.set_read_timeout(Some(Duration::from_millis(200)))?;
|
||||||
|
|
||||||
|
if cli.ping {
|
||||||
|
let request = models::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||||
|
SpacePacketHeader::new_from_apid(u11::new(Apid::Tmtc as u16)),
|
||||||
|
TcHeader::new(models::ComponentId::Controller, models::MessageType::Ping),
|
||||||
|
models::control::request::Request::Ping,
|
||||||
|
);
|
||||||
|
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
|
||||||
|
log::info!("sending ping request with TC ID {:#010x}", sent_tc_id.raw());
|
||||||
|
let request_packet = request.to_vec();
|
||||||
|
client.send_to(&request_packet, addr).unwrap();
|
||||||
|
}
|
||||||
|
if cli.test_event {
|
||||||
|
let request = models::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||||
|
SpacePacketHeader::new_from_apid(u11::new(Apid::Tmtc as u16)),
|
||||||
|
TcHeader::new(models::ComponentId::Controller, models::MessageType::Event),
|
||||||
|
models::control::request::Request::TestEvent,
|
||||||
|
);
|
||||||
|
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
|
||||||
|
log::info!(
|
||||||
|
"sending event request with TC ID {:#010x}",
|
||||||
|
sent_tc_id.raw()
|
||||||
|
);
|
||||||
|
let request_packet = request.to_vec();
|
||||||
|
client.send_to(&request_packet, addr).unwrap();
|
||||||
|
}
|
||||||
|
if let Some(cmd) = cli.commands {
|
||||||
|
let target_id = cmd.target_id();
|
||||||
|
match cmd {
|
||||||
|
Commands::Mgm0(args) | Commands::Mgm1(args) => {
|
||||||
|
if args.ping {
|
||||||
|
let request = models::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||||
|
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||||
|
TcHeader::new(cmd.target_id(), models::MessageType::Ping),
|
||||||
|
models::mgm::request::Request::Ping,
|
||||||
|
);
|
||||||
|
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
|
||||||
|
log::info!(
|
||||||
|
"sending {:?} ping request with TC ID {:#010x}",
|
||||||
|
target_id,
|
||||||
|
sent_tc_id.raw()
|
||||||
|
);
|
||||||
|
let request_packet = request.to_vec();
|
||||||
|
client.send_to(&request_packet, addr).unwrap();
|
||||||
|
}
|
||||||
|
if args.request_hk {
|
||||||
|
let request = models::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||||
|
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||||
|
TcHeader::new(target_id, models::MessageType::Hk),
|
||||||
|
models::mgm::request::Request::Hk(HkRequest {
|
||||||
|
id: models::mgm::request::HkId::Sensor,
|
||||||
|
req_type: models::HkRequestType::OneShot,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
|
||||||
|
log::info!(
|
||||||
|
"sending {:?} HK request with TC ID {:#010x}",
|
||||||
|
target_id,
|
||||||
|
sent_tc_id.raw()
|
||||||
|
);
|
||||||
|
let request_packet = request.to_vec();
|
||||||
|
client.send_to(&request_packet, addr).unwrap();
|
||||||
|
}
|
||||||
|
if let Some(mode) = args.mode {
|
||||||
|
let dev_mode = match mode {
|
||||||
|
DeviceModeSelect::Off => models::DeviceMode::Off,
|
||||||
|
DeviceModeSelect::Normal => models::DeviceMode::Normal,
|
||||||
|
};
|
||||||
|
|
||||||
|
let request = models::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||||
|
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||||
|
TcHeader::new(target_id, models::MessageType::Mode),
|
||||||
|
models::mgm::request::Request::Mode(
|
||||||
|
models::mgm::request::ModeRequest::SetMode(dev_mode),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
|
||||||
|
log::info!(
|
||||||
|
"sending {:?} HK request with TC ID {:#010x}",
|
||||||
|
target_id,
|
||||||
|
sent_tc_id.raw()
|
||||||
|
);
|
||||||
|
let request_packet = request.to_vec();
|
||||||
|
client.send_to(&request_packet, addr).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Commands::MgmAssy(mgm_assembly_args) => {
|
||||||
|
if mgm_assembly_args.ping {
|
||||||
|
let request = models::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||||
|
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||||
|
TcHeader::new(cmd.target_id(), models::MessageType::Ping),
|
||||||
|
models::mgm::request::Request::Ping,
|
||||||
|
);
|
||||||
|
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
|
||||||
|
log::info!(
|
||||||
|
"sending {:?} ping request with TC ID {:#010x}",
|
||||||
|
target_id,
|
||||||
|
sent_tc_id.raw()
|
||||||
|
);
|
||||||
|
let request_packet = request.to_vec();
|
||||||
|
client.send_to(&request_packet, addr).unwrap();
|
||||||
|
}
|
||||||
|
if let Some(mode) = mgm_assembly_args.mode {
|
||||||
|
let assembly_mode = match mode {
|
||||||
|
AssemblyModeSelect::NoModeKeeping => {
|
||||||
|
models::mgm_assembly::AssemblyMode::NoModeKeeping
|
||||||
|
}
|
||||||
|
AssemblyModeSelect::Off => {
|
||||||
|
models::mgm_assembly::AssemblyMode::Device(models::DeviceMode::Off)
|
||||||
|
}
|
||||||
|
AssemblyModeSelect::Normal => {
|
||||||
|
models::mgm_assembly::AssemblyMode::Device(models::DeviceMode::Normal)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let request = models::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||||
|
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||||
|
TcHeader::new(target_id, models::MessageType::Mode),
|
||||||
|
models::mgm_assembly::request::Request::Mode(
|
||||||
|
models::mgm_assembly::request::ModeRequest::SetMode(assembly_mode),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
|
||||||
|
log::info!(
|
||||||
|
"sending {:?} HK request with TC ID {:#010x}",
|
||||||
|
target_id,
|
||||||
|
sent_tc_id.raw()
|
||||||
|
);
|
||||||
|
let request_packet = request.to_vec();
|
||||||
|
client.send_to(&request_packet, addr).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut recv_buf: Box<[u8; 2048]> = Box::new([0; 2048]);
|
||||||
|
log::info!("entering listening loop");
|
||||||
|
loop {
|
||||||
|
if kill_signal.load(std::sync::atomic::Ordering::Relaxed) {
|
||||||
|
log::info!("received kill signal, exiting");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
match client.recv(recv_buf.as_mut_slice()) {
|
||||||
|
Ok(received_bytes) => handle_raw_tm_packet(&recv_buf.as_slice()[0..received_bytes])?,
|
||||||
|
Err(e) => {
|
||||||
|
if e.kind() == std::io::ErrorKind::WouldBlock
|
||||||
|
|| e.kind() == std::io::ErrorKind::TimedOut
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
log::warn!("UDP reception error: {}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_raw_tm_packet(data: &[u8]) -> anyhow::Result<()> {
|
||||||
|
match spacepackets::CcsdsPacketReader::new_with_checksum(data) {
|
||||||
|
Ok(packet) => {
|
||||||
|
let tm_header_result =
|
||||||
|
postcard::take_from_bytes::<models::TmHeader>(packet.user_data());
|
||||||
|
if let Err(e) = tm_header_result {
|
||||||
|
bail!("Failed to deserialize TM header: {}", e);
|
||||||
|
}
|
||||||
|
let (tm_header, remainder) = tm_header_result.unwrap();
|
||||||
|
if let Some(tc_id) = tm_header.tc_id {
|
||||||
|
log::info!(
|
||||||
|
"Received TM with APID {} and from sender {:?} for TC ID {:#010x}",
|
||||||
|
packet.apid(),
|
||||||
|
tm_header.sender_id,
|
||||||
|
tc_id.raw()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log::info!(
|
||||||
|
"Received unsolicited TM with APID {} and from sender {:?}",
|
||||||
|
packet.apid(),
|
||||||
|
tm_header.sender_id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if tm_header.message_type == MessageType::Event {
|
||||||
|
let response = postcard::from_bytes::<models::Event>(remainder);
|
||||||
|
log::info!(
|
||||||
|
"Received event from {:?}: {:?}",
|
||||||
|
tm_header.sender_id,
|
||||||
|
response.unwrap()
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
match tm_header.sender_id {
|
||||||
|
models::ComponentId::EpsPcdu => {
|
||||||
|
let response =
|
||||||
|
postcard::from_bytes::<models::pcdu::response::Response>(remainder);
|
||||||
|
log::info!("Received response from PCDU: {:?}", response.unwrap());
|
||||||
|
}
|
||||||
|
models::ComponentId::Controller => {
|
||||||
|
let response =
|
||||||
|
postcard::from_bytes::<models::control::response::Response>(remainder);
|
||||||
|
log::info!("Received response from controller: {:?}", response.unwrap());
|
||||||
|
}
|
||||||
|
models::ComponentId::AcsSubsystem => todo!(),
|
||||||
|
models::ComponentId::AcsMgmAssembly => {
|
||||||
|
let response =
|
||||||
|
postcard::from_bytes::<models::mgm_assembly::response::Response>(remainder);
|
||||||
|
log::info!(
|
||||||
|
"Received response from MGM Assembly: {:?}",
|
||||||
|
response.unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
models::ComponentId::AcsMgm0 => {
|
||||||
|
let response =
|
||||||
|
postcard::from_bytes::<models::mgm::response::Response>(remainder);
|
||||||
|
log::info!("Received response from MGM0: {:?}", response.unwrap());
|
||||||
|
}
|
||||||
|
models::ComponentId::AcsMgm1 => {
|
||||||
|
let response =
|
||||||
|
postcard::from_bytes::<models::mgm::response::Response>(remainder);
|
||||||
|
log::info!("Received response from MGM1: {:?}", response.unwrap());
|
||||||
|
}
|
||||||
|
models::ComponentId::EpsSubsystem => todo!(),
|
||||||
|
models::ComponentId::UdpServer => todo!(),
|
||||||
|
models::ComponentId::TcpServer => todo!(),
|
||||||
|
models::ComponentId::Ground => todo!(),
|
||||||
|
models::ComponentId::EventManager => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => todo!(),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -11,16 +11,14 @@ serde_json = "1"
|
|||||||
log = "0.4"
|
log = "0.4"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
fern = "0.7"
|
fern = "0.7"
|
||||||
strum = { version = "0.27", features = ["derive"] }
|
strum = { version = "0.28", features = ["derive"] }
|
||||||
num_enum = "0.7"
|
num_enum = "0.7"
|
||||||
humantime = "2"
|
humantime = "2"
|
||||||
tai-time = { version = "0.3", features = ["serde"] }
|
tai-time = { version = "0.3", features = ["serde"] }
|
||||||
|
nexosim = { version = "0.3.1" }
|
||||||
|
|
||||||
[dependencies.nexosim]
|
satrs = { path = "../../satrs" }
|
||||||
version = "0.3.1"
|
models = { path = "../models" }
|
||||||
|
|
||||||
[dependencies.satrs]
|
|
||||||
path = "../satrs"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
delegate = "0.13"
|
delegate = "0.13"
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
use std::{f32::consts::PI, sync::mpsc, time::Duration};
|
use std::{f32::consts::PI, sync::mpsc, time::Duration};
|
||||||
|
|
||||||
|
use models::pcdu::SwitchStateBinary;
|
||||||
use nexosim::{
|
use nexosim::{
|
||||||
model::{Context, Model},
|
model::{Context, Model},
|
||||||
ports::Output,
|
ports::Output,
|
||||||
};
|
};
|
||||||
use satrs::power::SwitchStateBinary;
|
|
||||||
use satrs_minisim::{
|
use satrs_minisim::{
|
||||||
acs::{
|
acs::{
|
||||||
lis3mdl::MgmLis3MdlReply, MgmReplyCommon, MgmReplyProvider, MgmSensorValuesMicroTesla,
|
lis3mdl::MgmLis3MdlReply, MgmReplyCommon, MgmReplyProvider, MgmSensorValuesMicroTesla,
|
||||||
@@ -179,13 +179,12 @@ impl Model for MagnetorquerModel {}
|
|||||||
pub mod tests {
|
pub mod tests {
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use satrs::power::SwitchStateBinary;
|
use models::pcdu::{SwitchId, SwitchStateBinary};
|
||||||
use satrs_minisim::{
|
use satrs_minisim::{
|
||||||
acs::{
|
acs::{
|
||||||
lis3mdl::{self, MgmLis3MdlReply},
|
lis3mdl::{self, MgmLis3MdlReply},
|
||||||
MgmRequestLis3Mdl, MgtDipole, MgtHkSet, MgtReply, MgtRequest,
|
MgmRequestLis3Mdl, MgtDipole, MgtHkSet, MgtReply, MgtRequest,
|
||||||
},
|
},
|
||||||
eps::PcduSwitch,
|
|
||||||
SerializableSimMsgPayload, SimComponent, SimMessageProvider, SimRequest,
|
SerializableSimMsgPayload, SimComponent, SimMessageProvider, SimRequest,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -215,7 +214,7 @@ pub mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_basic_mgm_request_switched_on() {
|
fn test_basic_mgm_request_switched_on() {
|
||||||
let mut sim_testbench = SimTestbench::new();
|
let mut sim_testbench = SimTestbench::new();
|
||||||
switch_device_on(&mut sim_testbench, PcduSwitch::Mgm);
|
switch_device_on(&mut sim_testbench, SwitchId::Mgm0);
|
||||||
|
|
||||||
let mut request = SimRequest::new_with_epoch_time(MgmRequestLis3Mdl::RequestSensorData);
|
let mut request = SimRequest::new_with_epoch_time(MgmRequestLis3Mdl::RequestSensorData);
|
||||||
sim_testbench
|
sim_testbench
|
||||||
@@ -279,7 +278,7 @@ pub mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_basic_mgt_request_is_on() {
|
fn test_basic_mgt_request_is_on() {
|
||||||
let mut sim_testbench = SimTestbench::new();
|
let mut sim_testbench = SimTestbench::new();
|
||||||
switch_device_on(&mut sim_testbench, PcduSwitch::Mgt);
|
switch_device_on(&mut sim_testbench, SwitchId::Mgt);
|
||||||
let request = SimRequest::new_with_epoch_time(MgtRequest::RequestHk);
|
let request = SimRequest::new_with_epoch_time(MgtRequest::RequestHk);
|
||||||
|
|
||||||
sim_testbench
|
sim_testbench
|
||||||
@@ -324,7 +323,7 @@ pub mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_basic_mgt_request_is_on_and_torquing() {
|
fn test_basic_mgt_request_is_on_and_torquing() {
|
||||||
let mut sim_testbench = SimTestbench::new();
|
let mut sim_testbench = SimTestbench::new();
|
||||||
switch_device_on(&mut sim_testbench, PcduSwitch::Mgt);
|
switch_device_on(&mut sim_testbench, SwitchId::Mgt);
|
||||||
let commanded_dipole = MgtDipole {
|
let commanded_dipole = MgtDipole {
|
||||||
x: -200,
|
x: -200,
|
||||||
y: 200,
|
y: 200,
|
||||||
@@ -1,14 +1,11 @@
|
|||||||
use std::{sync::mpsc, time::Duration};
|
use std::{sync::mpsc, time::Duration};
|
||||||
|
|
||||||
|
use models::pcdu::{SwitchId, SwitchMapBinaryWrapper, SwitchStateBinary};
|
||||||
use nexosim::{
|
use nexosim::{
|
||||||
model::{Context, Model},
|
model::{Context, Model},
|
||||||
ports::Output,
|
ports::Output,
|
||||||
};
|
};
|
||||||
use satrs::power::SwitchStateBinary;
|
use satrs_minisim::{eps::PcduReply, SimReply};
|
||||||
use satrs_minisim::{
|
|
||||||
eps::{PcduReply, PcduSwitch, SwitchMapBinaryWrapper},
|
|
||||||
SimReply,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const SWITCH_INFO_DELAY_MS: u64 = 10;
|
pub const SWITCH_INFO_DELAY_MS: u64 = 10;
|
||||||
|
|
||||||
@@ -45,10 +42,12 @@ impl PcduModel {
|
|||||||
self.reply_sender.send(reply).unwrap();
|
self.reply_sender.send(reply).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn switch_device(
|
pub async fn switch_device(&mut self, switch_and_target_state: (SwitchId, SwitchStateBinary)) {
|
||||||
&mut self,
|
log::info!(
|
||||||
switch_and_target_state: (PcduSwitch, SwitchStateBinary),
|
"switching {:?} to {:?}",
|
||||||
) {
|
switch_and_target_state.0,
|
||||||
|
switch_and_target_state.1
|
||||||
|
);
|
||||||
let val = self
|
let val = self
|
||||||
.switcher_map
|
.switcher_map
|
||||||
.0
|
.0
|
||||||
@@ -56,12 +55,13 @@ impl PcduModel {
|
|||||||
.unwrap_or_else(|| panic!("switch {:?} not found", switch_and_target_state.0));
|
.unwrap_or_else(|| panic!("switch {:?} not found", switch_and_target_state.0));
|
||||||
*val = switch_and_target_state.1;
|
*val = switch_and_target_state.1;
|
||||||
match switch_and_target_state.0 {
|
match switch_and_target_state.0 {
|
||||||
PcduSwitch::Mgm => {
|
SwitchId::Mgm0 => {
|
||||||
self.mgm_0_switch.send(switch_and_target_state.1).await;
|
self.mgm_0_switch.send(switch_and_target_state.1).await;
|
||||||
}
|
}
|
||||||
PcduSwitch::Mgt => {
|
SwitchId::Mgt => {
|
||||||
self.mgt_switch.send(switch_and_target_state.1).await;
|
self.mgt_switch.send(switch_and_target_state.1).await;
|
||||||
}
|
}
|
||||||
|
SwitchId::Mgm1 => todo!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,16 +73,16 @@ pub(crate) mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use models::pcdu::SwitchMapBinary;
|
||||||
use satrs_minisim::{
|
use satrs_minisim::{
|
||||||
eps::{PcduRequest, SwitchMapBinary},
|
eps::PcduRequest, SerializableSimMsgPayload, SimComponent, SimMessageProvider, SimRequest,
|
||||||
SerializableSimMsgPayload, SimComponent, SimMessageProvider, SimRequest,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::test_helpers::SimTestbench;
|
use crate::test_helpers::SimTestbench;
|
||||||
|
|
||||||
fn switch_device(
|
fn switch_device(
|
||||||
sim_testbench: &mut SimTestbench,
|
sim_testbench: &mut SimTestbench,
|
||||||
switch: PcduSwitch,
|
switch: SwitchId,
|
||||||
target: SwitchStateBinary,
|
target: SwitchStateBinary,
|
||||||
) {
|
) {
|
||||||
let request = SimRequest::new_with_epoch_time(PcduRequest::SwitchDevice {
|
let request = SimRequest::new_with_epoch_time(PcduRequest::SwitchDevice {
|
||||||
@@ -97,10 +97,10 @@ pub(crate) mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub(crate) fn switch_device_off(sim_testbench: &mut SimTestbench, switch: PcduSwitch) {
|
pub(crate) fn switch_device_off(sim_testbench: &mut SimTestbench, switch: SwitchId) {
|
||||||
switch_device(sim_testbench, switch, SwitchStateBinary::Off);
|
switch_device(sim_testbench, switch, SwitchStateBinary::Off);
|
||||||
}
|
}
|
||||||
pub(crate) fn switch_device_on(sim_testbench: &mut SimTestbench, switch: PcduSwitch) {
|
pub(crate) fn switch_device_on(sim_testbench: &mut SimTestbench, switch: SwitchId) {
|
||||||
switch_device(sim_testbench, switch, SwitchStateBinary::On);
|
switch_device(sim_testbench, switch, SwitchStateBinary::On);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +128,7 @@ pub(crate) mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_pcdu_switching_single_switch(switch: PcduSwitch, target: SwitchStateBinary) {
|
fn test_pcdu_switching_single_switch(switch: SwitchId, target: SwitchStateBinary) {
|
||||||
let mut sim_testbench = SimTestbench::new();
|
let mut sim_testbench = SimTestbench::new();
|
||||||
switch_device(&mut sim_testbench, switch, target);
|
switch_device(&mut sim_testbench, switch, target);
|
||||||
let mut switcher_map = get_all_off_switch_map();
|
let mut switcher_map = get_all_off_switch_map();
|
||||||
@@ -165,17 +165,17 @@ pub(crate) mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_pcdu_switching_mgm_on() {
|
fn test_pcdu_switching_mgm_on() {
|
||||||
test_pcdu_switching_single_switch(PcduSwitch::Mgm, SwitchStateBinary::On);
|
test_pcdu_switching_single_switch(SwitchId::Mgm0, SwitchStateBinary::On);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_pcdu_switching_mgt_on() {
|
fn test_pcdu_switching_mgt_on() {
|
||||||
test_pcdu_switching_single_switch(PcduSwitch::Mgt, SwitchStateBinary::On);
|
test_pcdu_switching_single_switch(SwitchId::Mgt, SwitchStateBinary::On);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_pcdu_switching_mgt_off() {
|
fn test_pcdu_switching_mgt_off() {
|
||||||
test_pcdu_switching_single_switch(PcduSwitch::Mgt, SwitchStateBinary::On);
|
test_pcdu_switching_single_switch(SwitchId::Mgt, SwitchStateBinary::On);
|
||||||
test_pcdu_switching_single_switch(PcduSwitch::Mgt, SwitchStateBinary::Off);
|
test_pcdu_switching_single_switch(SwitchId::Mgt, SwitchStateBinary::Off);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
use nexosim::time::MonotonicTime;
|
use nexosim::time::MonotonicTime;
|
||||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
|
||||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
|
||||||
@@ -162,73 +161,7 @@ impl From<SimRequestError> for SimCtrlReply {
|
|||||||
|
|
||||||
pub mod eps {
|
pub mod eps {
|
||||||
use super::*;
|
use super::*;
|
||||||
use satrs::power::{SwitchState, SwitchStateBinary};
|
use models::pcdu::{SwitchId, SwitchMapBinary, SwitchStateBinary};
|
||||||
use std::collections::HashMap;
|
|
||||||
use strum::{EnumIter, IntoEnumIterator};
|
|
||||||
|
|
||||||
pub type SwitchMap = HashMap<PcduSwitch, SwitchState>;
|
|
||||||
pub type SwitchMapBinary = HashMap<PcduSwitch, SwitchStateBinary>;
|
|
||||||
|
|
||||||
pub struct SwitchMapWrapper(pub SwitchMap);
|
|
||||||
pub struct SwitchMapBinaryWrapper(pub SwitchMapBinary);
|
|
||||||
|
|
||||||
#[derive(
|
|
||||||
Debug,
|
|
||||||
Copy,
|
|
||||||
Clone,
|
|
||||||
PartialEq,
|
|
||||||
Eq,
|
|
||||||
Serialize,
|
|
||||||
Deserialize,
|
|
||||||
Hash,
|
|
||||||
EnumIter,
|
|
||||||
IntoPrimitive,
|
|
||||||
TryFromPrimitive,
|
|
||||||
)]
|
|
||||||
#[repr(u16)]
|
|
||||||
pub enum PcduSwitch {
|
|
||||||
Mgm = 0,
|
|
||||||
Mgt = 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for SwitchMapBinaryWrapper {
|
|
||||||
fn default() -> Self {
|
|
||||||
let mut switch_map = SwitchMapBinary::default();
|
|
||||||
for entry in PcduSwitch::iter() {
|
|
||||||
switch_map.insert(entry, SwitchStateBinary::Off);
|
|
||||||
}
|
|
||||||
Self(switch_map)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for SwitchMapWrapper {
|
|
||||||
fn default() -> Self {
|
|
||||||
let mut switch_map = SwitchMap::default();
|
|
||||||
for entry in PcduSwitch::iter() {
|
|
||||||
switch_map.insert(entry, SwitchState::Unknown);
|
|
||||||
}
|
|
||||||
Self(switch_map)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SwitchMapWrapper {
|
|
||||||
pub fn new_with_init_switches_off() -> Self {
|
|
||||||
let mut switch_map = SwitchMap::default();
|
|
||||||
for entry in PcduSwitch::iter() {
|
|
||||||
switch_map.insert(entry, SwitchState::Off);
|
|
||||||
}
|
|
||||||
Self(switch_map)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_binary_switch_map_ref(switch_map: &SwitchMapBinary) -> Self {
|
|
||||||
Self(
|
|
||||||
switch_map
|
|
||||||
.iter()
|
|
||||||
.map(|(key, value)| (*key, SwitchState::from(*value)))
|
|
||||||
.collect(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
@@ -240,7 +173,7 @@ pub mod eps {
|
|||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub enum PcduRequest {
|
pub enum PcduRequest {
|
||||||
SwitchDevice {
|
SwitchDevice {
|
||||||
switch: PcduSwitch,
|
switch: SwitchId,
|
||||||
state: SwitchStateBinary,
|
state: SwitchStateBinary,
|
||||||
},
|
},
|
||||||
RequestSwitchInfo,
|
RequestSwitchInfo,
|
||||||
@@ -264,7 +197,7 @@ pub mod eps {
|
|||||||
pub mod acs {
|
pub mod acs {
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use satrs::power::SwitchStateBinary;
|
use models::pcdu::SwitchStateBinary;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
@@ -91,11 +91,8 @@ impl SimUdpServer {
|
|||||||
self.sender_addr = Some(src);
|
self.sender_addr = Some(src);
|
||||||
|
|
||||||
let sim_req = SimRequest::from_raw_data(&self.req_buf[..bytes_read]);
|
let sim_req = SimRequest::from_raw_data(&self.req_buf[..bytes_read]);
|
||||||
if sim_req.is_err() {
|
if let Err(e) = sim_req {
|
||||||
log::warn!(
|
log::warn!("received UDP request with invalid format: {}", e);
|
||||||
"received UDP request with invalid format: {}",
|
|
||||||
sim_req.unwrap_err()
|
|
||||||
);
|
|
||||||
return processed_requests;
|
return processed_requests;
|
||||||
}
|
}
|
||||||
self.request_sender.send(sim_req.unwrap()).unwrap();
|
self.request_sender.send(sim_req.unwrap()).unwrap();
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "models"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
spacepackets = { version = "0.17", git = "https://egit.irs.uni-stuttgart.de/rust/spacepackets.git", default-features = false }
|
||||||
|
satrs = { path = "../../satrs" }
|
||||||
|
num_enum = { version = "0.7" }
|
||||||
|
strum = { version = "0.28", features = ["derive"] }
|
||||||
|
postcard = { version = "1" }
|
||||||
|
thiserror = { version = "2" }
|
||||||
|
bitbybit = "2"
|
||||||
|
arbitrary-int = "2"
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
use crate::TmHeader;
|
||||||
|
use serde::Serialize;
|
||||||
|
use spacepackets::{
|
||||||
|
CcsdsPacketCreationError, CcsdsPacketCreatorWithReservedData, SpHeader, SpacePacketHeader,
|
||||||
|
ccsds_packet_len_for_user_data_len_with_checksum,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::TcHeader;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct CcsdsTcPacketOwned {
|
||||||
|
pub sp_header: SpacePacketHeader,
|
||||||
|
pub tc_header: TcHeader,
|
||||||
|
pub payload: alloc::vec::Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CcsdsTcPacketOwned {
|
||||||
|
pub fn new_with_request<R: serde::Serialize>(
|
||||||
|
sp_header: SpacePacketHeader,
|
||||||
|
tc_header: TcHeader,
|
||||||
|
request: R,
|
||||||
|
) -> Self {
|
||||||
|
let request_serialized = postcard::to_allocvec(&request).unwrap();
|
||||||
|
Self::new(sp_header, tc_header, request_serialized)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(
|
||||||
|
sp_header: SpacePacketHeader,
|
||||||
|
tc_header: TcHeader,
|
||||||
|
payload: alloc::vec::Vec<u8>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
sp_header,
|
||||||
|
tc_header,
|
||||||
|
payload,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_to_bytes(&self, buf: &mut [u8]) -> Result<usize, CcsdsCreationError> {
|
||||||
|
let response_len =
|
||||||
|
postcard::experimental::serialized_size(&self.tc_header)? + self.payload.len();
|
||||||
|
let mut ccsds_tc = CcsdsPacketCreatorWithReservedData::new_tc_with_checksum(
|
||||||
|
self.sp_header,
|
||||||
|
response_len,
|
||||||
|
buf,
|
||||||
|
)?;
|
||||||
|
let user_data = ccsds_tc.packet_data_mut();
|
||||||
|
let ser_len = postcard::to_slice(&self.tc_header, user_data)?.len();
|
||||||
|
user_data[ser_len..ser_len + self.payload.len()].copy_from_slice(&self.payload);
|
||||||
|
let ccsds_packet_len = ccsds_tc.finish();
|
||||||
|
Ok(ccsds_packet_len)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len_written(&self) -> usize {
|
||||||
|
ccsds_packet_len_for_user_data_len_with_checksum(
|
||||||
|
postcard::experimental::serialized_size(&self.tc_header).unwrap() as usize
|
||||||
|
+ postcard::experimental::serialized_size(&self.payload).unwrap() as usize,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
buf.truncate(len);
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum CcsdsCreationError {
|
||||||
|
#[error("CCSDS packet creation error: {0}")]
|
||||||
|
CcsdsPacketCreation(#[from] CcsdsPacketCreationError),
|
||||||
|
#[error("postcard error: {0}")]
|
||||||
|
Postcard(#[from] postcard::Error),
|
||||||
|
#[error("timestamp generation error")]
|
||||||
|
Time,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unserialized owned TM packet which can be cloned and sent around.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct CcsdsTmPacketOwned {
|
||||||
|
pub sp_header: SpacePacketHeader,
|
||||||
|
pub tm_header: TmHeader,
|
||||||
|
pub payload: alloc::vec::Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CcsdsTmPacketOwned {
|
||||||
|
pub fn new_with_serde_payload(
|
||||||
|
sp_header: SpHeader,
|
||||||
|
tm_header: &TmHeader,
|
||||||
|
payload: &impl Serialize,
|
||||||
|
) -> Result<Self, postcard::Error> {
|
||||||
|
Ok(CcsdsTmPacketOwned {
|
||||||
|
sp_header,
|
||||||
|
tm_header: *tm_header,
|
||||||
|
payload: postcard::to_allocvec(&payload)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_to_bytes(&self, buf: &mut [u8]) -> Result<usize, CcsdsCreationError> {
|
||||||
|
let response_len =
|
||||||
|
postcard::experimental::serialized_size(&self.tm_header)? + self.payload.len();
|
||||||
|
let mut ccsds_tm = CcsdsPacketCreatorWithReservedData::new_tm_with_checksum(
|
||||||
|
self.sp_header,
|
||||||
|
response_len,
|
||||||
|
buf,
|
||||||
|
)?;
|
||||||
|
let user_data = ccsds_tm.packet_data_mut();
|
||||||
|
let ser_len = postcard::to_slice(&self.tm_header, user_data)?.len();
|
||||||
|
user_data[ser_len..ser_len + self.payload.len()].copy_from_slice(&self.payload);
|
||||||
|
let ccsds_packet_len = ccsds_tm.finish();
|
||||||
|
Ok(ccsds_packet_len)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len_written(&self) -> usize {
|
||||||
|
ccsds_packet_len_for_user_data_len_with_checksum(
|
||||||
|
postcard::experimental::serialized_size(&self.tm_header).unwrap() as usize
|
||||||
|
+ postcard::experimental::serialized_size(&self.payload).unwrap() as usize,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
buf.truncate(len);
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
use crate::Message;
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)]
|
||||||
|
pub enum Event {
|
||||||
|
TestEvent,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Message for Event {
|
||||||
|
fn message_type(&self) -> crate::MessageType {
|
||||||
|
crate::MessageType::Event
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod request {
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)]
|
||||||
|
pub enum Request {
|
||||||
|
Ping,
|
||||||
|
TestEvent,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod response {
|
||||||
|
use crate::Message;
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)]
|
||||||
|
pub enum Response {
|
||||||
|
Ok,
|
||||||
|
Event(super::Event),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Message for Response {
|
||||||
|
fn message_type(&self) -> crate::MessageType {
|
||||||
|
match self {
|
||||||
|
Response::Ok => crate::MessageType::Verification,
|
||||||
|
Response::Event(_event) => crate::MessageType::Event,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
extern crate alloc;
|
||||||
|
use core::str::FromStr;
|
||||||
|
|
||||||
|
use spacepackets::{
|
||||||
|
CcsdsPacketIdAndPsc,
|
||||||
|
time::cds::{CdsTime, MIN_CDS_FIELD_LEN},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod ccsds;
|
||||||
|
pub mod control;
|
||||||
|
pub mod mgm;
|
||||||
|
pub mod mgm_assembly;
|
||||||
|
pub mod pcdu;
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Debug,
|
||||||
|
Copy,
|
||||||
|
Clone,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
Hash,
|
||||||
|
serde::Serialize,
|
||||||
|
serde::Deserialize,
|
||||||
|
num_enum::TryFromPrimitive,
|
||||||
|
num_enum::IntoPrimitive,
|
||||||
|
)]
|
||||||
|
#[repr(u64)]
|
||||||
|
pub enum ComponentId {
|
||||||
|
Controller,
|
||||||
|
|
||||||
|
AcsSubsystem,
|
||||||
|
AcsMgmAssembly,
|
||||||
|
AcsMgm0,
|
||||||
|
AcsMgm1,
|
||||||
|
|
||||||
|
EpsSubsystem,
|
||||||
|
EpsPcdu,
|
||||||
|
|
||||||
|
UdpServer,
|
||||||
|
TcpServer,
|
||||||
|
EventManager,
|
||||||
|
|
||||||
|
Ground,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, strum::EnumIter)]
|
||||||
|
#[bitbybit::bitenum(u11)]
|
||||||
|
pub enum Apid {
|
||||||
|
Tmtc = 1,
|
||||||
|
Cfdp = 2,
|
||||||
|
|
||||||
|
Acs = 3,
|
||||||
|
Eps = 6,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum Event {
|
||||||
|
ControllerEvent(control::Event),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Message for Event {
|
||||||
|
fn message_type(&self) -> MessageType {
|
||||||
|
MessageType::Event
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub struct TmHeader {
|
||||||
|
pub sender_id: ComponentId,
|
||||||
|
pub target_id: ComponentId,
|
||||||
|
pub message_type: MessageType,
|
||||||
|
/// Telemetry can either be sent unsolicited, or as a response to telecommands.
|
||||||
|
pub tc_id: Option<CcsdsPacketIdAndPsc>,
|
||||||
|
/// Raw CDS short timestamp.
|
||||||
|
pub timestamp: Option<[u8; 7]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TmHeader {
|
||||||
|
pub fn new(
|
||||||
|
sender_id: ComponentId,
|
||||||
|
target_id: ComponentId,
|
||||||
|
message_type: MessageType,
|
||||||
|
tc_id: Option<CcsdsPacketIdAndPsc>,
|
||||||
|
cds_timestamp: &CdsTime,
|
||||||
|
) -> Self {
|
||||||
|
// Can not fail, CDS short always requires 7 bytes.
|
||||||
|
let mut stamp_buf: [u8; MIN_CDS_FIELD_LEN] = [0; MIN_CDS_FIELD_LEN];
|
||||||
|
cds_timestamp.write_to_bytes(&mut stamp_buf).unwrap();
|
||||||
|
Self {
|
||||||
|
sender_id,
|
||||||
|
target_id,
|
||||||
|
tc_id,
|
||||||
|
message_type,
|
||||||
|
timestamp: Some(stamp_buf),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn new_for_unsolicited_tm(
|
||||||
|
sender_id: ComponentId,
|
||||||
|
target_id: ComponentId,
|
||||||
|
message_type: MessageType,
|
||||||
|
cds_timestamp: &CdsTime,
|
||||||
|
) -> Self {
|
||||||
|
Self::new(sender_id, target_id, message_type, None, cds_timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_for_tc_response(
|
||||||
|
sender_id: ComponentId,
|
||||||
|
target_id: ComponentId,
|
||||||
|
message_type: MessageType,
|
||||||
|
tc_id: CcsdsPacketIdAndPsc,
|
||||||
|
cds_timestamp: &CdsTime,
|
||||||
|
) -> Self {
|
||||||
|
Self::new(
|
||||||
|
sender_id,
|
||||||
|
target_id,
|
||||||
|
message_type,
|
||||||
|
Some(tc_id),
|
||||||
|
cds_timestamp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_bytes_postcard(data: &[u8]) -> Result<(Self, &[u8]), postcard::Error> {
|
||||||
|
postcard::take_from_bytes::<TmHeader>(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub struct TcHeader {
|
||||||
|
pub target_id: ComponentId,
|
||||||
|
pub request_type: MessageType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TcHeader {
|
||||||
|
pub fn new(target_id: ComponentId, request_type: MessageType) -> Self {
|
||||||
|
Self {
|
||||||
|
target_id,
|
||||||
|
request_type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum MessageType {
|
||||||
|
Ping,
|
||||||
|
Mode,
|
||||||
|
Hk,
|
||||||
|
Action,
|
||||||
|
Event,
|
||||||
|
Verification,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Message {
|
||||||
|
fn message_type(&self) -> MessageType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generic device mode which covers the requirements of most devices.
|
||||||
|
///
|
||||||
|
/// The states are related both to the physical and the logical state of the device. Some
|
||||||
|
/// device handlers control the power supply of their own device and an off state might also
|
||||||
|
/// mean that the device is physically off.
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Copy, Clone)]
|
||||||
|
pub enum DeviceMode {
|
||||||
|
Off = 0,
|
||||||
|
On = 1,
|
||||||
|
/// Normal operation mode where periodic polling might be done as well.
|
||||||
|
Normal = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for DeviceMode {
|
||||||
|
type Err = ();
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s.to_lowercase().as_str() {
|
||||||
|
"off" => Ok(DeviceMode::Off),
|
||||||
|
"on" => Ok(DeviceMode::On),
|
||||||
|
"normal" => Ok(DeviceMode::Normal),
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum HkRequestType {
|
||||||
|
OneShot,
|
||||||
|
/// Enable periodic HK generation with a specified frequency.
|
||||||
|
EnablePeriodic(core::time::Duration),
|
||||||
|
DisablePeriodic,
|
||||||
|
/// Modify periodic HK generation interval.
|
||||||
|
ModifyInterval(core::time::Duration),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
pub mod request {
|
||||||
|
use crate::{DeviceMode, HkRequestType, Message};
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ModeRequest {
|
||||||
|
SetMode(DeviceMode),
|
||||||
|
ReadMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum HkId {
|
||||||
|
Sensor,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct HkRequest {
|
||||||
|
pub id: HkId,
|
||||||
|
pub req_type: HkRequestType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)]
|
||||||
|
pub enum Request {
|
||||||
|
Ping,
|
||||||
|
Hk(HkRequest),
|
||||||
|
Mode(ModeRequest),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Request {
|
||||||
|
fn message_type(&self) -> crate::MessageType {
|
||||||
|
match self {
|
||||||
|
Request::Ping => crate::MessageType::Verification,
|
||||||
|
Request::Hk(_hk_request) => crate::MessageType::Hk,
|
||||||
|
Request::Mode(_mode) => crate::MessageType::Mode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Message for Request {
|
||||||
|
fn message_type(&self) -> crate::MessageType {
|
||||||
|
self.message_type()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Copy, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct MgmData {
|
||||||
|
pub valid: bool,
|
||||||
|
pub x: f32,
|
||||||
|
pub y: f32,
|
||||||
|
pub z: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod response {
|
||||||
|
use crate::{DeviceMode, Message, mgm::MgmData};
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)]
|
||||||
|
pub enum HkResponse {
|
||||||
|
MgmData(MgmData),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy)]
|
||||||
|
pub enum ModeResponse {
|
||||||
|
/// New mode has been set.
|
||||||
|
Mode(DeviceMode),
|
||||||
|
/// Setting a mode timed out.
|
||||||
|
SetModeTimeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)]
|
||||||
|
pub enum Response {
|
||||||
|
Ok,
|
||||||
|
Hk(HkResponse),
|
||||||
|
Mode(ModeResponse),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Response {
|
||||||
|
fn message_type(&self) -> crate::MessageType {
|
||||||
|
match self {
|
||||||
|
Response::Ok => crate::MessageType::Verification,
|
||||||
|
Response::Hk(_hk_response) => crate::MessageType::Hk,
|
||||||
|
Response::Mode(_mode_failure) => crate::MessageType::Mode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Message for Response {
|
||||||
|
fn message_type(&self) -> crate::MessageType {
|
||||||
|
self.message_type()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
use core::str::FromStr;
|
||||||
|
|
||||||
|
use crate::DeviceMode;
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum AssemblyMode {
|
||||||
|
/// The assembly mode ressembles the modes of the devices it controls. It also tries to keep
|
||||||
|
/// the children in the correct mode by re-commanding them into the correct mode.
|
||||||
|
Device(DeviceMode),
|
||||||
|
/// Mode keeping disabled.
|
||||||
|
NoModeKeeping,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for AssemblyMode {
|
||||||
|
type Err = ();
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s.to_lowercase().as_str() {
|
||||||
|
"off" => Ok(AssemblyMode::Device(DeviceMode::Off)),
|
||||||
|
"on" => Ok(AssemblyMode::Device(DeviceMode::On)),
|
||||||
|
"normal" => Ok(AssemblyMode::Device(DeviceMode::Normal)),
|
||||||
|
"no_mode_keeping" => Ok(AssemblyMode::NoModeKeeping),
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod request {
|
||||||
|
use crate::{HkRequestType, Message, mgm_assembly::AssemblyMode};
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum HkId {
|
||||||
|
Sensor,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum ModeRequest {
|
||||||
|
SetMode(AssemblyMode),
|
||||||
|
ReadMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct HkRequest {
|
||||||
|
pub id: HkId,
|
||||||
|
pub req_type: HkRequestType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)]
|
||||||
|
pub enum Request {
|
||||||
|
Ping,
|
||||||
|
Mode(ModeRequest),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Request {
|
||||||
|
fn message_type(&self) -> crate::MessageType {
|
||||||
|
match self {
|
||||||
|
Request::Ping => crate::MessageType::Verification,
|
||||||
|
Request::Mode(_mode) => crate::MessageType::Mode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Message for Request {
|
||||||
|
fn message_type(&self) -> crate::MessageType {
|
||||||
|
self.message_type()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod response {
|
||||||
|
use crate::{DeviceMode, Message};
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum ModeCommandFailure {
|
||||||
|
Timeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum ModeReport {
|
||||||
|
/// Mode of the assembly.
|
||||||
|
Mode(super::AssemblyMode),
|
||||||
|
/// Timeout failure setting the children modes.
|
||||||
|
SetModeTimeout([Option<DeviceMode>; 2]),
|
||||||
|
/// Children are in wrong mode after commanding.
|
||||||
|
WrongMode([Option<DeviceMode>; 2]),
|
||||||
|
/// An assembly tried modekeeping but can not keep its mode.
|
||||||
|
CanNotKeepMode([Option<DeviceMode>; 2]),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum Response {
|
||||||
|
Ok,
|
||||||
|
Mode(ModeReport),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Response {
|
||||||
|
fn message_type(&self) -> crate::MessageType {
|
||||||
|
match self {
|
||||||
|
Response::Ok => crate::MessageType::Verification,
|
||||||
|
Response::Mode(_mode_report) => crate::MessageType::Mode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Message for Response {
|
||||||
|
fn message_type(&self) -> crate::MessageType {
|
||||||
|
self.message_type()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use strum::IntoEnumIterator as _;
|
||||||
|
|
||||||
|
#[bitbybit::bitfield(u16, debug, default = 0x0)]
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct SwitchesBitfield {
|
||||||
|
#[bit(2, rw)]
|
||||||
|
magnetorquer: bool,
|
||||||
|
#[bit(1, rw)]
|
||||||
|
mgm1: bool,
|
||||||
|
#[bit(0, rw)]
|
||||||
|
mgm0: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Debug,
|
||||||
|
Copy,
|
||||||
|
Clone,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
serde::Serialize,
|
||||||
|
serde::Deserialize,
|
||||||
|
Hash,
|
||||||
|
strum::EnumIter,
|
||||||
|
num_enum::IntoPrimitive,
|
||||||
|
num_enum::TryFromPrimitive,
|
||||||
|
)]
|
||||||
|
#[repr(u16)]
|
||||||
|
pub enum SwitchId {
|
||||||
|
Mgm0 = 0,
|
||||||
|
Mgm1 = 1,
|
||||||
|
Mgt = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq, Copy, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum SwitchState {
|
||||||
|
Off = 0,
|
||||||
|
On = 1,
|
||||||
|
Unknown = 2,
|
||||||
|
Faulty = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SwitchStateBinary> for SwitchState {
|
||||||
|
fn from(value: SwitchStateBinary) -> Self {
|
||||||
|
match value {
|
||||||
|
SwitchStateBinary::Off => SwitchState::Off,
|
||||||
|
SwitchStateBinary::On => SwitchState::On,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq, Copy, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum SwitchStateBinary {
|
||||||
|
Off = 0,
|
||||||
|
On = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type SwitchMapBinary = HashMap<SwitchId, SwitchStateBinary>;
|
||||||
|
|
||||||
|
pub struct SwitchMapBinaryWrapper(pub SwitchMapBinary);
|
||||||
|
|
||||||
|
impl Default for SwitchMapBinaryWrapper {
|
||||||
|
fn default() -> Self {
|
||||||
|
let mut switch_map = SwitchMapBinary::default();
|
||||||
|
for entry in SwitchId::iter() {
|
||||||
|
switch_map.insert(entry, SwitchStateBinary::Off);
|
||||||
|
}
|
||||||
|
Self(switch_map)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SwitchRequest {
|
||||||
|
pub switch_id: SwitchId,
|
||||||
|
pub target_state: SwitchStateBinary,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SwitchRequest {
|
||||||
|
pub fn new(switch_id: SwitchId, target_state: SwitchStateBinary) -> Self {
|
||||||
|
Self {
|
||||||
|
switch_id,
|
||||||
|
target_state,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn switch_id(&self) -> SwitchId {
|
||||||
|
self.switch_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn target_state(&self) -> SwitchStateBinary {
|
||||||
|
self.target_state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod request {
|
||||||
|
use crate::{DeviceMode, Message};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)]
|
||||||
|
pub enum Request {
|
||||||
|
Mode(DeviceMode),
|
||||||
|
Ping,
|
||||||
|
GetSwitches,
|
||||||
|
EnableSwitches(SwitchesBitfield),
|
||||||
|
DisableSwitches(SwitchesBitfield),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Request {
|
||||||
|
pub fn message_type(&self) -> crate::MessageType {
|
||||||
|
match self {
|
||||||
|
Request::Mode(_mode) => crate::MessageType::Mode,
|
||||||
|
Request::Ping => crate::MessageType::Verification,
|
||||||
|
Request::GetSwitches => crate::MessageType::Action,
|
||||||
|
Request::EnableSwitches(_switches) | Request::DisableSwitches(_switches) => {
|
||||||
|
crate::MessageType::Action
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Message for Request {
|
||||||
|
fn message_type(&self) -> crate::MessageType {
|
||||||
|
self.message_type()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod response {
|
||||||
|
use super::*;
|
||||||
|
use crate::Message;
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)]
|
||||||
|
pub enum Response {
|
||||||
|
Ok,
|
||||||
|
Switches(SwitchesBitfield),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Message for Response {
|
||||||
|
fn message_type(&self) -> crate::MessageType {
|
||||||
|
match self {
|
||||||
|
Response::Ok => crate::MessageType::Verification,
|
||||||
|
Response::Switches(_switches) => crate::MessageType::Action,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
// TODO: Write the assembly
|
|
||||||
+526
-478
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,634 @@
|
|||||||
|
use std::{sync::mpsc, time::Duration};
|
||||||
|
|
||||||
|
use models::{
|
||||||
|
ComponentId, DeviceMode,
|
||||||
|
mgm_assembly::{AssemblyMode, request, response},
|
||||||
|
};
|
||||||
|
use satrs::spacepackets::CcsdsPacketIdAndPsc;
|
||||||
|
use satrs_example::{ModeHelper, TmtcQueues};
|
||||||
|
|
||||||
|
use crate::ccsds::pack_ccsds_tm_packet_for_now;
|
||||||
|
|
||||||
|
pub struct ParentQueueHelper {
|
||||||
|
pub request_rx: mpsc::Receiver<request::ModeRequest>,
|
||||||
|
pub report_tx: mpsc::SyncSender<response::ModeReport>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper component for communication with a parent component, which is usually as assembly.
|
||||||
|
pub struct ChildrenQueueHelper {
|
||||||
|
pub request_tx_queues: [mpsc::SyncSender<models::mgm::request::ModeRequest>; 2],
|
||||||
|
pub report_rx_queues: [mpsc::Receiver<models::mgm::response::ModeResponse>; 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum TransitionState {
|
||||||
|
#[default]
|
||||||
|
Idle,
|
||||||
|
AwaitingReplies,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Copy, Clone)]
|
||||||
|
pub struct MgmInfo {
|
||||||
|
reply_received: bool,
|
||||||
|
mode: Option<DeviceMode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MGM assembly component.
|
||||||
|
pub struct Assembly {
|
||||||
|
mode_helper: ModeHelper<AssemblyMode, TransitionState>,
|
||||||
|
/// This boolean is used for the distinction between transitions commanded by the parent
|
||||||
|
/// or by ground, and transitions which were commanded autonomously as part of children
|
||||||
|
/// mode keeping.
|
||||||
|
mode_keeping_transition: bool,
|
||||||
|
tmtc_queues: TmtcQueues,
|
||||||
|
mgm_modes: [MgmInfo; 2],
|
||||||
|
parent_queues: ParentQueueHelper,
|
||||||
|
pub(crate) children_queues: ChildrenQueueHelper,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Assembly {
|
||||||
|
pub const ID: ComponentId = ComponentId::AcsMgmAssembly;
|
||||||
|
|
||||||
|
pub fn new(
|
||||||
|
parent_queues: ParentQueueHelper,
|
||||||
|
children_queues: ChildrenQueueHelper,
|
||||||
|
tmtc_queues: TmtcQueues,
|
||||||
|
mode_timeout: Duration,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
mode_helper: ModeHelper::new(AssemblyMode::NoModeKeeping, mode_timeout),
|
||||||
|
mode_keeping_transition: false,
|
||||||
|
tmtc_queues,
|
||||||
|
mgm_modes: [MgmInfo::default(); 2],
|
||||||
|
parent_queues,
|
||||||
|
children_queues,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn periodic_operation(&mut self) {
|
||||||
|
self.handle_telecommands();
|
||||||
|
self.handle_parent_mode_queue();
|
||||||
|
self.handle_children_mode_queues();
|
||||||
|
|
||||||
|
if self.mode_helper.transition_active() {
|
||||||
|
self.handle_mode_transition();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_telecommands(&mut self) {
|
||||||
|
loop {
|
||||||
|
match self.tmtc_queues.tc_rx.try_recv() {
|
||||||
|
Ok(packet) => {
|
||||||
|
let tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
|
||||||
|
match postcard::from_bytes::<models::mgm_assembly::request::Request>(
|
||||||
|
&packet.payload,
|
||||||
|
) {
|
||||||
|
Ok(request) => match request {
|
||||||
|
models::mgm_assembly::request::Request::Ping => {
|
||||||
|
self.send_telemetry(Some(tc_id), response::Response::Ok)
|
||||||
|
}
|
||||||
|
models::mgm_assembly::request::Request::Mode(request) => {
|
||||||
|
match request {
|
||||||
|
request::ModeRequest::SetMode(assembly_mode) => {
|
||||||
|
self.start_transition(false, assembly_mode, Some(tc_id))
|
||||||
|
}
|
||||||
|
request::ModeRequest::ReadMode => self.send_telemetry(
|
||||||
|
Some(tc_id),
|
||||||
|
response::Response::Mode(response::ModeReport::Mode(
|
||||||
|
self.mode(),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("failed to deserialize request: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => match e {
|
||||||
|
mpsc::TryRecvError::Empty => break,
|
||||||
|
mpsc::TryRecvError::Disconnected => log::warn!("packet sender disconnected"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_telemetry(
|
||||||
|
&self,
|
||||||
|
tc_id: Option<CcsdsPacketIdAndPsc>,
|
||||||
|
response: models::mgm_assembly::response::Response,
|
||||||
|
) {
|
||||||
|
match pack_ccsds_tm_packet_for_now(Self::ID, tc_id, &response) {
|
||||||
|
Ok(packet) => {
|
||||||
|
if let Err(e) = self.tmtc_queues.tm_tx.send(packet) {
|
||||||
|
log::warn!("failed to send TM packet: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("failed to pack TM packet: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_parent_mode_queue(&mut self) {
|
||||||
|
loop {
|
||||||
|
match self.parent_queues.request_rx.try_recv() {
|
||||||
|
Ok(request) => match request {
|
||||||
|
request::ModeRequest::SetMode(assembly_mode) => match assembly_mode {
|
||||||
|
AssemblyMode::Device(_device_mode) => {
|
||||||
|
self.start_transition(false, assembly_mode, None);
|
||||||
|
}
|
||||||
|
AssemblyMode::NoModeKeeping => {
|
||||||
|
self.mode_helper.current = AssemblyMode::NoModeKeeping;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
request::ModeRequest::ReadMode => self
|
||||||
|
.parent_queues
|
||||||
|
.report_tx
|
||||||
|
.send(response::ModeReport::Mode(self.mode_helper.current))
|
||||||
|
.unwrap(),
|
||||||
|
},
|
||||||
|
Err(e) => match e {
|
||||||
|
mpsc::TryRecvError::Empty => break,
|
||||||
|
mpsc::TryRecvError::Disconnected => {
|
||||||
|
log::warn!("packet sender disconnected")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_children_mode_queues(&mut self) {
|
||||||
|
let mut mode_report_received = false;
|
||||||
|
for (idx, rx) in self.children_queues.report_rx_queues.iter_mut().enumerate() {
|
||||||
|
loop {
|
||||||
|
match rx.try_recv() {
|
||||||
|
Ok(report) => match report {
|
||||||
|
models::mgm::response::ModeResponse::Mode(device_mode) => {
|
||||||
|
self.mgm_modes[idx].mode = Some(device_mode);
|
||||||
|
self.mgm_modes[idx].reply_received = true;
|
||||||
|
mode_report_received = true;
|
||||||
|
}
|
||||||
|
models::mgm::response::ModeResponse::SetModeTimeout => {
|
||||||
|
// Ignore, handle this with our own timeout.
|
||||||
|
log::warn!("MGM {} mode timeout", idx);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => match e {
|
||||||
|
mpsc::TryRecvError::Empty => break,
|
||||||
|
mpsc::TryRecvError::Disconnected => {
|
||||||
|
log::warn!("packet sender disconnected")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !mode_report_received {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transition is active, check for completion.
|
||||||
|
if self.mode_helper.transition_active()
|
||||||
|
&& self.mgm_modes.iter().all(|i| i.reply_received)
|
||||||
|
&& let AssemblyMode::Device(device_mode) = self.mode_helper.target.unwrap()
|
||||||
|
{
|
||||||
|
// If at least one child reached the correct mode, we are done.
|
||||||
|
if self.mgm_modes.iter().any(|i| i.mode == Some(device_mode)) {
|
||||||
|
self.handle_mode_reached(true);
|
||||||
|
} else {
|
||||||
|
let report = if self.mode_keeping_transition {
|
||||||
|
response::ModeReport::CanNotKeepMode(self.mgm_modes.map(|info| info.mode))
|
||||||
|
} else {
|
||||||
|
response::ModeReport::WrongMode(self.mgm_modes.map(|info| info.mode))
|
||||||
|
};
|
||||||
|
self.handle_mode_transition_failure(report);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mode keeping active: Check children modes.
|
||||||
|
if let AssemblyMode::Device(device_mode) = self.mode_helper.current
|
||||||
|
&& self
|
||||||
|
.mgm_modes
|
||||||
|
.iter()
|
||||||
|
.all(|info| info.mode != Some(device_mode))
|
||||||
|
{
|
||||||
|
// Children lost mode. Try to command them back to the correct
|
||||||
|
// mode.
|
||||||
|
self.start_transition(true, self.mode_helper.current, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_mode_transition(&mut self) {
|
||||||
|
if self.mode_helper.target.is_none() {
|
||||||
|
self.handle_mode_reached(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let target = self.mode_helper.target.unwrap();
|
||||||
|
let device_mode = match target {
|
||||||
|
AssemblyMode::Device(device_mode) => device_mode,
|
||||||
|
AssemblyMode::NoModeKeeping => {
|
||||||
|
self.handle_mode_reached(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if self.mode_helper.transition_state == TransitionState::Idle {
|
||||||
|
self.command_children(device_mode);
|
||||||
|
self.mode_helper.transition_state = TransitionState::AwaitingReplies;
|
||||||
|
}
|
||||||
|
if self.mode_helper.transition_state == TransitionState::AwaitingReplies
|
||||||
|
&& self.mode_helper.timed_out()
|
||||||
|
{
|
||||||
|
let report = if self.mode_keeping_transition {
|
||||||
|
response::ModeReport::CanNotKeepMode(self.mgm_modes.map(|info| info.mode))
|
||||||
|
} else {
|
||||||
|
response::ModeReport::SetModeTimeout(self.mgm_modes.map(|info| info.mode))
|
||||||
|
};
|
||||||
|
self.handle_mode_transition_failure(report);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_mode_reached(&mut self, success: bool) {
|
||||||
|
let tc_commander = self.mode_helper.finish(success);
|
||||||
|
self.announce_mode();
|
||||||
|
if tc_commander.is_some() {
|
||||||
|
self.send_telemetry(tc_commander, response::Response::Ok);
|
||||||
|
}
|
||||||
|
self.parent_queues
|
||||||
|
.report_tx
|
||||||
|
.send(response::ModeReport::Mode(self.mode_helper.current))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_mode_transition_failure(&mut self, report: response::ModeReport) {
|
||||||
|
if self.mode_helper.tc_commander.is_some() {
|
||||||
|
self.send_telemetry(
|
||||||
|
self.mode_helper.tc_commander,
|
||||||
|
response::Response::Mode(response::ModeReport::SetModeTimeout(
|
||||||
|
self.mgm_modes.map(|info| info.mode),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.parent_queues.report_tx.send(report).unwrap();
|
||||||
|
self.mode_helper.finish(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn command_children(&self, mode: DeviceMode) {
|
||||||
|
for tx in &self.children_queues.request_tx_queues {
|
||||||
|
tx.send(models::mgm::request::ModeRequest::SetMode(mode))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_transition(
|
||||||
|
&mut self,
|
||||||
|
mode_keeping: bool,
|
||||||
|
target: AssemblyMode,
|
||||||
|
tc_id: Option<CcsdsPacketIdAndPsc>,
|
||||||
|
) {
|
||||||
|
self.mode_keeping_transition = mode_keeping;
|
||||||
|
self.mode_helper.tc_commander = tc_id;
|
||||||
|
self.mgm_modes
|
||||||
|
.iter_mut()
|
||||||
|
.for_each(|m| m.reply_received = false);
|
||||||
|
self.mode_helper.start(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn announce_mode(&self) {
|
||||||
|
// TODO: Event?
|
||||||
|
log::info!(
|
||||||
|
"{:?} announcing mode: {:?}",
|
||||||
|
Self::ID,
|
||||||
|
self.mode_helper.current
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn mode(&self) -> AssemblyMode {
|
||||||
|
self.mode_helper.current
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[cfg(test)]
|
||||||
|
fn mode_transition_active(&self) -> bool {
|
||||||
|
self.mode_helper.transition_active()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::sync::mpsc::TryRecvError;
|
||||||
|
|
||||||
|
use arbitrary_int::u11;
|
||||||
|
use models::{
|
||||||
|
Apid, Message, MessageType, TcHeader,
|
||||||
|
ccsds::{CcsdsTcPacketOwned, CcsdsTmPacketOwned},
|
||||||
|
mgm_assembly,
|
||||||
|
};
|
||||||
|
use satrs::spacepackets::SpacePacketHeader;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub struct Testbench {
|
||||||
|
subsystem_req_tx: mpsc::SyncSender<request::ModeRequest>,
|
||||||
|
subsystem_report_rx: mpsc::Receiver<response::ModeReport>,
|
||||||
|
mgm_request_rx: [mpsc::Receiver<models::mgm::request::ModeRequest>; 2],
|
||||||
|
mgm_report_tx: [mpsc::SyncSender<models::mgm::response::ModeResponse>; 2],
|
||||||
|
tc_tx: mpsc::SyncSender<CcsdsTcPacketOwned>,
|
||||||
|
tm_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
|
||||||
|
assembly: Assembly,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Testbench {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let (subsystem_req_tx, subsystem_req_rx) = mpsc::sync_channel(5);
|
||||||
|
let (subsystem_report_tx, subsystem_report_rx) = mpsc::sync_channel(5);
|
||||||
|
|
||||||
|
let (mgm_0_mode_request_tx, mgm_0_mode_request_rx) = mpsc::sync_channel(5);
|
||||||
|
let (mgm_1_mode_request_tx, mgm_1_mode_request_rx) = mpsc::sync_channel(5);
|
||||||
|
let (mgm_0_mode_report_tx, mgm_0_mode_report_rx) = mpsc::sync_channel(5);
|
||||||
|
let (mgm_1_mode_report_tx, mgm_1_mode_report_rx) = mpsc::sync_channel(5);
|
||||||
|
|
||||||
|
let (tc_tx, tc_rx) = mpsc::sync_channel(5);
|
||||||
|
let (tm_tx, tm_rx) = mpsc::sync_channel(5);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
subsystem_req_tx,
|
||||||
|
subsystem_report_rx,
|
||||||
|
mgm_request_rx: [mgm_0_mode_request_rx, mgm_1_mode_request_rx],
|
||||||
|
mgm_report_tx: [mgm_0_mode_report_tx, mgm_1_mode_report_tx],
|
||||||
|
tc_tx,
|
||||||
|
tm_rx,
|
||||||
|
assembly: Assembly::new(
|
||||||
|
ParentQueueHelper {
|
||||||
|
request_rx: subsystem_req_rx,
|
||||||
|
report_tx: subsystem_report_tx,
|
||||||
|
},
|
||||||
|
ChildrenQueueHelper {
|
||||||
|
request_tx_queues: [mgm_0_mode_request_tx, mgm_1_mode_request_tx],
|
||||||
|
report_rx_queues: [mgm_0_mode_report_rx, mgm_1_mode_report_rx],
|
||||||
|
},
|
||||||
|
TmtcQueues { tc_rx, tm_tx },
|
||||||
|
Duration::from_millis(20),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assert_all_queues_empty(&self) {
|
||||||
|
assert!(
|
||||||
|
matches!(self.tm_rx.try_recv().unwrap_err(), TryRecvError::Empty),
|
||||||
|
"TM queue not empty"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
matches!(
|
||||||
|
self.subsystem_report_rx.try_recv().unwrap_err(),
|
||||||
|
TryRecvError::Empty
|
||||||
|
),
|
||||||
|
"subsystem report queue not empty"
|
||||||
|
);
|
||||||
|
for rx in self.mgm_request_rx.iter() {
|
||||||
|
assert!(
|
||||||
|
matches!(rx.try_recv().unwrap_err(), TryRecvError::Empty),
|
||||||
|
"mgm request queue not empty"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_request_tc(
|
||||||
|
request: models::mgm_assembly::request::Request,
|
||||||
|
) -> models::ccsds::CcsdsTcPacketOwned {
|
||||||
|
models::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||||
|
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||||
|
TcHeader::new(Assembly::ID, request.message_type()),
|
||||||
|
request,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn basic_test() {
|
||||||
|
let mut tb = Testbench::new();
|
||||||
|
tb.assert_all_queues_empty();
|
||||||
|
tb.assembly.periodic_operation();
|
||||||
|
tb.assert_all_queues_empty();
|
||||||
|
assert_eq!(tb.assembly.mode(), AssemblyMode::NoModeKeeping);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tc_commanded_transition() {
|
||||||
|
let mut tb = Testbench::new();
|
||||||
|
tb.tc_tx
|
||||||
|
.send(create_request_tc(mgm_assembly::request::Request::Mode(
|
||||||
|
request::ModeRequest::SetMode(AssemblyMode::Device(DeviceMode::Normal)),
|
||||||
|
)))
|
||||||
|
.unwrap();
|
||||||
|
tb.assembly.periodic_operation();
|
||||||
|
assert!(tb.assembly.mode_transition_active());
|
||||||
|
|
||||||
|
for rx in tb.mgm_request_rx.iter() {
|
||||||
|
let request = rx.try_recv().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
request,
|
||||||
|
models::mgm::request::ModeRequest::SetMode(DeviceMode::Normal)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm the mode is set.
|
||||||
|
for tx in tb.mgm_report_tx.iter() {
|
||||||
|
tx.send(models::mgm::response::ModeResponse::Mode(
|
||||||
|
DeviceMode::Normal,
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
tb.assembly.periodic_operation();
|
||||||
|
assert!(!tb.assembly.mode_transition_active());
|
||||||
|
assert_eq!(tb.assembly.mode(), AssemblyMode::Device(DeviceMode::Normal));
|
||||||
|
|
||||||
|
let response = tb.tm_rx.try_recv().unwrap();
|
||||||
|
assert_eq!(response.tm_header.sender_id, Assembly::ID);
|
||||||
|
assert_eq!(response.tm_header.message_type, MessageType::Verification);
|
||||||
|
let response: response::Response = postcard::from_bytes(&response.payload).unwrap();
|
||||||
|
assert_eq!(response, response::Response::Ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parent_commanded_transition() {
|
||||||
|
let mut tb = Testbench::new();
|
||||||
|
tb.subsystem_req_tx
|
||||||
|
.send(request::ModeRequest::SetMode(AssemblyMode::Device(
|
||||||
|
DeviceMode::Normal,
|
||||||
|
)))
|
||||||
|
.unwrap();
|
||||||
|
tb.assembly.periodic_operation();
|
||||||
|
assert!(tb.assembly.mode_transition_active());
|
||||||
|
|
||||||
|
for rx in tb.mgm_request_rx.iter() {
|
||||||
|
let request = rx.try_recv().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
request,
|
||||||
|
models::mgm::request::ModeRequest::SetMode(DeviceMode::Normal)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm the mode is set.
|
||||||
|
for tx in tb.mgm_report_tx.iter() {
|
||||||
|
tx.send(models::mgm::response::ModeResponse::Mode(
|
||||||
|
DeviceMode::Normal,
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
tb.assembly.periodic_operation();
|
||||||
|
assert!(!tb.assembly.mode_transition_active());
|
||||||
|
assert_eq!(tb.assembly.mode(), AssemblyMode::Device(DeviceMode::Normal));
|
||||||
|
|
||||||
|
let report = tb.subsystem_report_rx.try_recv().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
report,
|
||||||
|
response::ModeReport::Mode(AssemblyMode::Device(DeviceMode::Normal))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_one_mgm_is_sufficient() {
|
||||||
|
let mut tb = Testbench::new();
|
||||||
|
tb.subsystem_req_tx
|
||||||
|
.send(request::ModeRequest::SetMode(AssemblyMode::Device(
|
||||||
|
DeviceMode::Normal,
|
||||||
|
)))
|
||||||
|
.unwrap();
|
||||||
|
tb.assembly.periodic_operation();
|
||||||
|
assert!(tb.assembly.mode_transition_active());
|
||||||
|
|
||||||
|
for rx in tb.mgm_request_rx.iter() {
|
||||||
|
let request = rx.try_recv().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
request,
|
||||||
|
models::mgm::request::ModeRequest::SetMode(DeviceMode::Normal)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// One device is sufficient.
|
||||||
|
tb.mgm_report_tx[0]
|
||||||
|
.send(models::mgm::response::ModeResponse::Mode(
|
||||||
|
DeviceMode::Normal,
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
tb.mgm_report_tx[1]
|
||||||
|
.send(models::mgm::response::ModeResponse::Mode(DeviceMode::Off))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
tb.assembly.periodic_operation();
|
||||||
|
assert!(!tb.assembly.mode_transition_active());
|
||||||
|
assert_eq!(tb.assembly.mode(), AssemblyMode::Device(DeviceMode::Normal));
|
||||||
|
|
||||||
|
let report = tb.subsystem_report_rx.try_recv().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
report,
|
||||||
|
response::ModeReport::Mode(AssemblyMode::Device(DeviceMode::Normal))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mode_commanding_fails() {
|
||||||
|
let mut tb = Testbench::new();
|
||||||
|
tb.subsystem_req_tx
|
||||||
|
.send(request::ModeRequest::SetMode(AssemblyMode::Device(
|
||||||
|
DeviceMode::Normal,
|
||||||
|
)))
|
||||||
|
.unwrap();
|
||||||
|
tb.assembly.periodic_operation();
|
||||||
|
assert!(tb.assembly.mode_transition_active());
|
||||||
|
|
||||||
|
for rx in tb.mgm_request_rx.iter() {
|
||||||
|
let request = rx.try_recv().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
request,
|
||||||
|
models::mgm::request::ModeRequest::SetMode(DeviceMode::Normal)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm the mode is set.
|
||||||
|
for tx in tb.mgm_report_tx.iter() {
|
||||||
|
tx.send(models::mgm::response::ModeResponse::Mode(DeviceMode::Off))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
tb.assembly.periodic_operation();
|
||||||
|
assert!(!tb.assembly.mode_transition_active());
|
||||||
|
assert_eq!(tb.assembly.mode(), AssemblyMode::NoModeKeeping);
|
||||||
|
|
||||||
|
let report = tb.subsystem_report_rx.try_recv().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
report,
|
||||||
|
response::ModeReport::WrongMode([Some(DeviceMode::Off), Some(DeviceMode::Off)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mode_keeping_fails() {
|
||||||
|
let mut tb = Testbench::new();
|
||||||
|
tb.subsystem_req_tx
|
||||||
|
.send(request::ModeRequest::SetMode(AssemblyMode::Device(
|
||||||
|
DeviceMode::Normal,
|
||||||
|
)))
|
||||||
|
.unwrap();
|
||||||
|
tb.assembly.periodic_operation();
|
||||||
|
assert!(tb.assembly.mode_transition_active());
|
||||||
|
|
||||||
|
for rx in tb.mgm_request_rx.iter() {
|
||||||
|
let request = rx.try_recv().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
request,
|
||||||
|
models::mgm::request::ModeRequest::SetMode(DeviceMode::Normal)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm the mode is set.
|
||||||
|
for tx in tb.mgm_report_tx.iter() {
|
||||||
|
tx.send(models::mgm::response::ModeResponse::Mode(
|
||||||
|
DeviceMode::Normal,
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
tb.assembly.periodic_operation();
|
||||||
|
assert!(!tb.assembly.mode_transition_active());
|
||||||
|
assert_eq!(tb.assembly.mode(), AssemblyMode::Device(DeviceMode::Normal));
|
||||||
|
|
||||||
|
let report = tb.subsystem_report_rx.try_recv().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
report,
|
||||||
|
response::ModeReport::Mode(AssemblyMode::Device(DeviceMode::Normal))
|
||||||
|
);
|
||||||
|
|
||||||
|
for tx in tb.mgm_report_tx.iter() {
|
||||||
|
tx.send(models::mgm::response::ModeResponse::Mode(DeviceMode::Off))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
// This should start mode keeping.
|
||||||
|
tb.assembly.periodic_operation();
|
||||||
|
assert!(tb.assembly.mode_transition_active());
|
||||||
|
|
||||||
|
for rx in tb.mgm_request_rx.iter() {
|
||||||
|
let request = rx.try_recv().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
request,
|
||||||
|
models::mgm::request::ModeRequest::SetMode(DeviceMode::Normal)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let the mode keeping fail.
|
||||||
|
for tx in tb.mgm_report_tx.iter() {
|
||||||
|
tx.send(models::mgm::response::ModeResponse::Mode(DeviceMode::Off))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
tb.assembly.periodic_operation();
|
||||||
|
let report = tb.subsystem_report_rx.try_recv().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
report,
|
||||||
|
response::ModeReport::CanNotKeepMode([Some(DeviceMode::Off), Some(DeviceMode::Off)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
pub mod assembly;
|
|
||||||
pub mod ctrl;
|
pub mod ctrl;
|
||||||
|
|
||||||
pub mod mgm;
|
pub mod mgm;
|
||||||
|
pub mod mgm_assembly;
|
||||||
|
|
||||||
pub mod subsystem;
|
pub mod subsystem;
|
||||||
|
|||||||
@@ -1,87 +0,0 @@
|
|||||||
use arbitrary_int::u11;
|
|
||||||
use satrs::pus::verification::RequestId;
|
|
||||||
use satrs::spacepackets::ecss::tc::PusTcCreator;
|
|
||||||
use satrs::spacepackets::ecss::tm::PusTmReader;
|
|
||||||
use satrs::spacepackets::ecss::{CreatorConfig, MessageTypeId};
|
|
||||||
use satrs::spacepackets::SpHeader;
|
|
||||||
use satrs_example::config::{OBSW_SERVER_ADDR, SERVER_PORT};
|
|
||||||
use std::net::{IpAddr, SocketAddr, UdpSocket};
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let mut buf = [0; 32];
|
|
||||||
let addr = SocketAddr::new(IpAddr::V4(OBSW_SERVER_ADDR), SERVER_PORT);
|
|
||||||
let pus_tc = PusTcCreator::new_simple(
|
|
||||||
SpHeader::new_from_apid(u11::new(0x02)),
|
|
||||||
MessageTypeId::new(17, 1),
|
|
||||||
&[],
|
|
||||||
CreatorConfig::default(),
|
|
||||||
);
|
|
||||||
let client = UdpSocket::bind("127.0.0.1:7302").expect("Connecting to UDP server failed");
|
|
||||||
let tc_req_id = RequestId::new(&pus_tc);
|
|
||||||
println!("Packing and sending PUS ping command TC[17,1] with request ID {tc_req_id}");
|
|
||||||
let size = pus_tc
|
|
||||||
.write_to_bytes(&mut buf)
|
|
||||||
.expect("Creating PUS TC failed");
|
|
||||||
client
|
|
||||||
.send_to(&buf[0..size], addr)
|
|
||||||
.unwrap_or_else(|_| panic!("Sending to {addr:?} failed"));
|
|
||||||
client
|
|
||||||
.set_read_timeout(Some(Duration::from_secs(2)))
|
|
||||||
.expect("Setting read timeout failed");
|
|
||||||
loop {
|
|
||||||
let res = client.recv(&mut buf);
|
|
||||||
match res {
|
|
||||||
Ok(_len) => {
|
|
||||||
let pus_tm = PusTmReader::new(&buf, 7).expect("Parsing PUS TM failed");
|
|
||||||
if pus_tm.service_type_id() == 17 && pus_tm.message_subtype_id() == 2 {
|
|
||||||
println!("Received PUS Ping Reply TM[17,2]")
|
|
||||||
} else if pus_tm.service_type_id() == 1 {
|
|
||||||
if pus_tm.source_data().is_empty() {
|
|
||||||
println!("Invalid verification TM, no source data");
|
|
||||||
}
|
|
||||||
let src_data = pus_tm.source_data();
|
|
||||||
if src_data.len() < 4 {
|
|
||||||
println!("Invalid verification TM source data, less than 4 bytes")
|
|
||||||
}
|
|
||||||
let req_id = RequestId::from_bytes(src_data).unwrap();
|
|
||||||
let subtype_id = pus_tm.message_subtype_id();
|
|
||||||
if subtype_id == 1 {
|
|
||||||
println!("Received TM[1,1] acceptance success for request ID {req_id}")
|
|
||||||
} else if subtype_id == 2 {
|
|
||||||
println!("Received TM[1,2] acceptance failure for request ID {req_id}")
|
|
||||||
} else if subtype_id == 3 {
|
|
||||||
println!("Received TM[1,3] start success for request ID {req_id}")
|
|
||||||
} else if subtype_id == 4 {
|
|
||||||
println!("Received TM[1,2] start failure for request ID {req_id}")
|
|
||||||
} else if subtype_id == 5 {
|
|
||||||
println!("Received TM[1,5] step success for request ID {req_id}")
|
|
||||||
} else if subtype_id == 6 {
|
|
||||||
println!("Received TM[1,6] step failure for request ID {req_id}")
|
|
||||||
} else if subtype_id == 7 {
|
|
||||||
println!("Received TM[1,7] completion success for request ID {req_id}")
|
|
||||||
} else if subtype_id == 8 {
|
|
||||||
println!("Received TM[1,8] completion failure for request ID {req_id}");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
println!(
|
|
||||||
"Received TM[{}, {}] with {} bytes",
|
|
||||||
pus_tm.service_type_id(),
|
|
||||||
pus_tm.message_subtype_id(),
|
|
||||||
size
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(ref e)
|
|
||||||
if e.kind() == std::io::ErrorKind::WouldBlock
|
|
||||||
|| e.kind() == std::io::ErrorKind::TimedOut =>
|
|
||||||
{
|
|
||||||
println!("No reply received for 2 seconds");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
println!("UDP receive error {:?}", res.unwrap_err());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
#![allow(dead_code)]
|
|
||||||
|
|
||||||
use crossbeam_channel::{bounded, Receiver, Sender};
|
|
||||||
use std::sync::atomic::{AtomicU16, Ordering};
|
|
||||||
use std::thread;
|
|
||||||
use zerocopy::{FromBytes, Immutable, IntoBytes, NetworkEndian, Unaligned, U16};
|
|
||||||
|
|
||||||
trait FieldDataProvider: Send {
|
|
||||||
fn get_data(&self) -> &[u8];
|
|
||||||
}
|
|
||||||
|
|
||||||
struct FixedFieldDataWrapper {
|
|
||||||
data: [u8; 8],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FixedFieldDataWrapper {
|
|
||||||
pub fn from_two_u32(p0: u32, p1: u32) -> Self {
|
|
||||||
let mut data = [0; 8];
|
|
||||||
data[0..4].copy_from_slice(p0.to_be_bytes().as_slice());
|
|
||||||
data[4..8].copy_from_slice(p1.to_be_bytes().as_slice());
|
|
||||||
Self { data }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FieldDataProvider for FixedFieldDataWrapper {
|
|
||||||
fn get_data(&self) -> &[u8] {
|
|
||||||
self.data.as_slice()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type FieldDataTraitObj = Box<dyn FieldDataProvider>;
|
|
||||||
|
|
||||||
struct ExampleMgmSet {
|
|
||||||
mgm_vec: [f32; 3],
|
|
||||||
temperature: u16,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(FromBytes, IntoBytes, Immutable, Unaligned)]
|
|
||||||
#[repr(C)]
|
|
||||||
struct ExampleMgmSetZc {
|
|
||||||
mgm_vec: [u8; 12],
|
|
||||||
temperatur: U16<NetworkEndian>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let (s0, r0): (Sender<FieldDataTraitObj>, Receiver<FieldDataTraitObj>) = bounded(5);
|
|
||||||
let data_wrapper = FixedFieldDataWrapper::from_two_u32(2, 3);
|
|
||||||
s0.send(Box::new(data_wrapper)).unwrap();
|
|
||||||
let jh0 = thread::spawn(move || {
|
|
||||||
let data = r0.recv().unwrap();
|
|
||||||
let raw = data.get_data();
|
|
||||||
println!("Received data {raw:?}");
|
|
||||||
});
|
|
||||||
let jh1 = thread::spawn(|| {});
|
|
||||||
jh0.join().unwrap();
|
|
||||||
jh1.join().unwrap();
|
|
||||||
//let mut max_val: u16 = u16::MAX;
|
|
||||||
//max_val += 1;
|
|
||||||
//println!("Max val: {}", max_val);
|
|
||||||
let atomic_u16: AtomicU16 = AtomicU16::new(u16::MAX);
|
|
||||||
atomic_u16.fetch_add(1, Ordering::SeqCst);
|
|
||||||
println!(
|
|
||||||
"atomic after overflow: {}",
|
|
||||||
atomic_u16.load(Ordering::SeqCst)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
use arbitrary_int::u11;
|
||||||
|
use models::{Apid, ComponentId, Message, TmHeader, ccsds::CcsdsTmPacketOwned};
|
||||||
|
use satrs::spacepackets::{
|
||||||
|
CcsdsPacketIdAndPsc, SpHeader,
|
||||||
|
time::{StdTimestampError, cds::CdsTime},
|
||||||
|
};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum CcsdsTmCreationError {
|
||||||
|
#[error("postcard error: {0}")]
|
||||||
|
Postcard(#[from] postcard::Error),
|
||||||
|
#[error("timestamp error: {0}")]
|
||||||
|
Time(#[from] StdTimestampError),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pack_ccsds_tm_packet_for_now(
|
||||||
|
sender_id: ComponentId,
|
||||||
|
tc_id: Option<CcsdsPacketIdAndPsc>,
|
||||||
|
payload: &(impl Serialize + Message),
|
||||||
|
) -> Result<CcsdsTmPacketOwned, CcsdsTmCreationError> {
|
||||||
|
let now = CdsTime::now_with_u16_days()?;
|
||||||
|
let sp_header = SpHeader::new_from_apid(u11::new(Apid::Tmtc as u16));
|
||||||
|
let tm_header = TmHeader::new(
|
||||||
|
sender_id,
|
||||||
|
ComponentId::Ground,
|
||||||
|
payload.message_type(),
|
||||||
|
tc_id,
|
||||||
|
&now,
|
||||||
|
);
|
||||||
|
Ok(CcsdsTmPacketOwned::new_with_serde_payload(
|
||||||
|
sp_header, &tm_header, payload,
|
||||||
|
)?)
|
||||||
|
}
|
||||||
@@ -7,13 +7,10 @@ use satrs::{
|
|||||||
use satrs_mib::res_code::ResultU16Info;
|
use satrs_mib::res_code::ResultU16Info;
|
||||||
use satrs_mib::resultcode;
|
use satrs_mib::resultcode;
|
||||||
use std::{collections::HashSet, net::Ipv4Addr};
|
use std::{collections::HashSet, net::Ipv4Addr};
|
||||||
use strum::IntoEnumIterator;
|
use strum::IntoEnumIterator as _;
|
||||||
|
|
||||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||||
use satrs::{
|
use satrs::pool::{StaticMemoryPool, StaticPoolConfig};
|
||||||
events_legacy::{EventU32TypedSev, SeverityInfo},
|
|
||||||
pool::{StaticMemoryPool, StaticPoolConfig},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, TryFromPrimitive, IntoPrimitive)]
|
#[derive(Copy, Clone, PartialEq, Eq, Debug, TryFromPrimitive, IntoPrimitive)]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
@@ -39,19 +36,17 @@ pub enum GroupId {
|
|||||||
pub const OBSW_SERVER_ADDR: Ipv4Addr = Ipv4Addr::UNSPECIFIED;
|
pub const OBSW_SERVER_ADDR: Ipv4Addr = Ipv4Addr::UNSPECIFIED;
|
||||||
pub const SERVER_PORT: u16 = 7301;
|
pub const SERVER_PORT: u16 = 7301;
|
||||||
|
|
||||||
pub const TEST_EVENT: EventU32TypedSev<SeverityInfo> = EventU32TypedSev::<SeverityInfo>::new(0, 0);
|
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref PACKET_ID_VALIDATOR: HashSet<PacketId> = {
|
pub static ref PACKET_ID_VALIDATOR: HashSet<PacketId> = {
|
||||||
let mut set = HashSet::new();
|
let mut set = HashSet::new();
|
||||||
for id in crate::ids::Apid::iter() {
|
for id in models::Apid::iter() {
|
||||||
set.insert(PacketId::new(PacketType::Tc, true, u11::new(id as u16)));
|
set.insert(PacketId::new(PacketType::Tc, true, u11::new(id as u16)));
|
||||||
}
|
}
|
||||||
set
|
set
|
||||||
};
|
};
|
||||||
pub static ref APID_VALIDATOR: HashSet<u16> = {
|
pub static ref APID_VALIDATOR: HashSet<u16> = {
|
||||||
let mut set = HashSet::new();
|
let mut set = HashSet::new();
|
||||||
for id in crate::ids::Apid::iter() {
|
for id in models::Apid::iter() {
|
||||||
set.insert(id as u16);
|
set.insert(id as u16);
|
||||||
}
|
}
|
||||||
set
|
set
|
||||||
@@ -174,7 +169,7 @@ pub mod pool {
|
|||||||
|
|
||||||
pub mod tasks {
|
pub mod tasks {
|
||||||
pub const FREQ_MS_UDP_TMTC: u64 = 200;
|
pub const FREQ_MS_UDP_TMTC: u64 = 200;
|
||||||
pub const FREQ_MS_AOCS: u64 = 500;
|
pub const FREQ_MS_AOCS: u64 = 200;
|
||||||
pub const FREQ_MS_PUS_STACK: u64 = 200;
|
pub const FREQ_MS_CONTROLLER: u64 = 200;
|
||||||
pub const SIM_CLIENT_IDLE_DELAY_MS: u64 = 5;
|
pub const SIM_CLIENT_IDLE_DELAY_MS: u64 = 5;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
use models::{
|
||||||
|
ComponentId,
|
||||||
|
ccsds::{CcsdsTcPacketOwned, CcsdsTmPacketOwned},
|
||||||
|
control,
|
||||||
|
};
|
||||||
|
use satrs::spacepackets::CcsdsPacketIdAndPsc;
|
||||||
|
|
||||||
|
use crate::ccsds::pack_ccsds_tm_packet_for_now;
|
||||||
|
|
||||||
|
pub struct Controller {
|
||||||
|
pub tc_rx: std::sync::mpsc::Receiver<CcsdsTcPacketOwned>,
|
||||||
|
pub tm_tx: std::sync::mpsc::SyncSender<CcsdsTmPacketOwned>,
|
||||||
|
pub event_ctrl_tx: std::sync::mpsc::SyncSender<control::Event>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Controller {
|
||||||
|
pub fn new(
|
||||||
|
tc_rx: std::sync::mpsc::Receiver<CcsdsTcPacketOwned>,
|
||||||
|
tm_tx: std::sync::mpsc::SyncSender<CcsdsTmPacketOwned>,
|
||||||
|
event_ctrl_tx: std::sync::mpsc::SyncSender<control::Event>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
tc_rx,
|
||||||
|
tm_tx,
|
||||||
|
event_ctrl_tx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn periodic_operation(&mut self) {
|
||||||
|
self.handle_telecommands();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_telecommands(&mut self) {
|
||||||
|
loop {
|
||||||
|
match self.tc_rx.try_recv() {
|
||||||
|
Ok(packet) => {
|
||||||
|
let tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
|
||||||
|
match postcard::from_bytes::<control::request::Request>(&packet.payload) {
|
||||||
|
Ok(request) => {
|
||||||
|
log::info!(
|
||||||
|
"received request {:?} with TC ID {:#010x}",
|
||||||
|
request,
|
||||||
|
tc_id.raw()
|
||||||
|
);
|
||||||
|
match request {
|
||||||
|
control::request::Request::Ping => self
|
||||||
|
.send_telemetry(Some(tc_id), control::response::Response::Ok),
|
||||||
|
control::request::Request::TestEvent => {
|
||||||
|
self.event_ctrl_tx.send(control::Event::TestEvent).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("failed to deserialize request: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => match e {
|
||||||
|
std::sync::mpsc::TryRecvError::Empty => break,
|
||||||
|
std::sync::mpsc::TryRecvError::Disconnected => {
|
||||||
|
log::warn!("packet sender disconnected")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_telemetry(
|
||||||
|
&self,
|
||||||
|
tc_id: Option<CcsdsPacketIdAndPsc>,
|
||||||
|
response: control::response::Response,
|
||||||
|
) {
|
||||||
|
match pack_ccsds_tm_packet_for_now(ComponentId::Controller, tc_id, &response) {
|
||||||
|
Ok(packet) => {
|
||||||
|
if let Err(e) = self.tm_tx.send(packet) {
|
||||||
|
log::warn!("failed to send TM packet: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("failed to pack TM packet: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
use derive_new::new;
|
use derive_new::new;
|
||||||
|
use models::pcdu::{SwitchId, SwitchRequest, SwitchState, SwitchStateBinary};
|
||||||
use std::{cell::RefCell, collections::VecDeque, sync::mpsc, time::Duration};
|
use std::{cell::RefCell, collections::VecDeque, sync::mpsc, time::Duration};
|
||||||
|
|
||||||
use satrs::{
|
use satrs::{
|
||||||
power::{
|
|
||||||
PowerSwitchInfo, PowerSwitcherCommandSender, SwitchRequest, SwitchState, SwitchStateBinary,
|
|
||||||
},
|
|
||||||
queue::GenericSendError,
|
queue::GenericSendError,
|
||||||
request::{GenericMessage, MessageMetadata},
|
request::{GenericMessage, MessageMetadata},
|
||||||
};
|
};
|
||||||
use satrs_minisim::eps::{PcduSwitch, SwitchMapWrapper};
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::eps::pcdu::SwitchMapWrapper;
|
||||||
|
|
||||||
use self::pcdu::SharedSwitchSet;
|
use self::pcdu::SharedSwitchSet;
|
||||||
|
|
||||||
pub mod pcdu;
|
pub mod pcdu;
|
||||||
@@ -22,6 +21,7 @@ pub struct PowerSwitchHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error, Copy, Clone, PartialEq, Eq)]
|
#[derive(Debug, Error, Copy, Clone, PartialEq, Eq)]
|
||||||
|
#[allow(dead_code)]
|
||||||
pub enum SwitchCommandingError {
|
pub enum SwitchCommandingError {
|
||||||
#[error("send error: {0}")]
|
#[error("send error: {0}")]
|
||||||
Send(#[from] GenericSendError),
|
Send(#[from] GenericSendError),
|
||||||
@@ -31,18 +31,72 @@ pub enum SwitchCommandingError {
|
|||||||
pub enum SwitchInfoError {
|
pub enum SwitchInfoError {
|
||||||
/// This is a configuration error which should not occur.
|
/// This is a configuration error which should not occur.
|
||||||
#[error("switch ID not in map")]
|
#[error("switch ID not in map")]
|
||||||
SwitchIdNotInMap(PcduSwitch),
|
SwitchIdNotInMap(SwitchId),
|
||||||
#[error("switch set invalid")]
|
#[error("switch set invalid")]
|
||||||
SwitchSetInvalid,
|
SwitchSetInvalid,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PowerSwitchInfo<PcduSwitch> for PowerSwitchHelper {
|
impl PowerSwitchHelper {
|
||||||
|
pub fn send_switch_on_cmd(
|
||||||
|
&self,
|
||||||
|
requestor_info: satrs::request::MessageMetadata,
|
||||||
|
switch_id: SwitchId,
|
||||||
|
) -> Result<(), GenericSendError> {
|
||||||
|
self.switcher_tx.send(GenericMessage::new(
|
||||||
|
requestor_info,
|
||||||
|
SwitchRequest::new(switch_id, SwitchStateBinary::On),
|
||||||
|
))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn send_switch_off_cmd(
|
||||||
|
&self,
|
||||||
|
requestor_info: satrs::request::MessageMetadata,
|
||||||
|
switch_id: SwitchId,
|
||||||
|
) -> Result<(), GenericSendError> {
|
||||||
|
self.switcher_tx.send(GenericMessage::new(
|
||||||
|
requestor_info,
|
||||||
|
SwitchRequest::new(switch_id, SwitchStateBinary::Off),
|
||||||
|
))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn switch_state(&self, switch_id: SwitchId) -> Result<SwitchState, SwitchInfoError> {
|
||||||
|
let switch_set = self
|
||||||
|
.shared_switch_set
|
||||||
|
.lock()
|
||||||
|
.expect("failed to lock switch set");
|
||||||
|
if !switch_set.valid {
|
||||||
|
return Err(SwitchInfoError::SwitchSetInvalid);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(state) = switch_set.switch_map.get(&switch_id) {
|
||||||
|
return Ok(*state);
|
||||||
|
}
|
||||||
|
Err(SwitchInfoError::SwitchIdNotInMap(switch_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn switch_delay_ms(&self) -> Duration {
|
||||||
|
// Here, we could set device specific switch delays theoretically. Set it to this value
|
||||||
|
// for now.
|
||||||
|
Duration::from_millis(1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_switch_on(&self, switch_id: SwitchId) -> bool {
|
||||||
|
if let Ok(state) = self.switch_state(switch_id) {
|
||||||
|
state == SwitchState::On
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
impl PowerSwitchInfo<SwitchId> for PowerSwitchHelper {
|
||||||
type Error = SwitchInfoError;
|
type Error = SwitchInfoError;
|
||||||
|
|
||||||
fn switch_state(
|
fn switch_state(&self, switch_id: SwitchId) -> Result<SwitchState, Self::Error> {
|
||||||
&self,
|
|
||||||
switch_id: PcduSwitch,
|
|
||||||
) -> Result<satrs::power::SwitchState, Self::Error> {
|
|
||||||
let switch_set = self
|
let switch_set = self
|
||||||
.shared_switch_set
|
.shared_switch_set
|
||||||
.lock()
|
.lock()
|
||||||
@@ -63,43 +117,51 @@ impl PowerSwitchInfo<PcduSwitch> for PowerSwitchHelper {
|
|||||||
Duration::from_millis(1000)
|
Duration::from_millis(1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
impl PowerSwitcherCommandSender<PcduSwitch> for PowerSwitchHelper {
|
/*
|
||||||
|
impl PowerSwitcherCommandSender<SwitchId> for PowerSwitchHelper {
|
||||||
type Error = SwitchCommandingError;
|
type Error = SwitchCommandingError;
|
||||||
|
|
||||||
fn send_switch_on_cmd(
|
fn send_switch_on_cmd(
|
||||||
&self,
|
&self,
|
||||||
requestor_info: satrs::request::MessageMetadata,
|
requestor_info: satrs::request::MessageMetadata,
|
||||||
switch_id: PcduSwitch,
|
switch_id: SwitchId,
|
||||||
) -> Result<(), Self::Error> {
|
) -> Result<(), Self::Error> {
|
||||||
self.switcher_tx
|
self.switcher_tx.send(GenericMessage::new(
|
||||||
.send_switch_on_cmd(requestor_info, switch_id)?;
|
requestor_info,
|
||||||
|
SwitchRequest::new(switch_id, SwitchStateBinary::On),
|
||||||
|
));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_switch_off_cmd(
|
fn send_switch_off_cmd(
|
||||||
&self,
|
&self,
|
||||||
requestor_info: satrs::request::MessageMetadata,
|
requestor_info: satrs::request::MessageMetadata,
|
||||||
switch_id: PcduSwitch,
|
switch_id: SwitchId,
|
||||||
) -> Result<(), Self::Error> {
|
) -> Result<(), Self::Error> {
|
||||||
self.switcher_tx
|
self.switcher_tx.send(GenericMessage::new(
|
||||||
.send_switch_off_cmd(requestor_info, switch_id)?;
|
requestor_info,
|
||||||
|
SwitchRequest::new(switch_id, SwitchStateBinary::Off),
|
||||||
|
));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[derive(new)]
|
#[derive(new)]
|
||||||
pub struct SwitchRequestInfo {
|
pub struct SwitchRequestInfo {
|
||||||
pub requestor_info: MessageMetadata,
|
pub requestor_info: MessageMetadata,
|
||||||
pub switch_id: PcduSwitch,
|
pub switch_id: SwitchId,
|
||||||
pub target_state: satrs::power::SwitchStateBinary,
|
pub target_state: SwitchStateBinary,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test switch helper which can be used for unittests.
|
// Test switch helper which can be used for unittests.
|
||||||
|
#[allow(dead_code)]
|
||||||
pub struct TestSwitchHelper {
|
pub struct TestSwitchHelper {
|
||||||
pub switch_requests: RefCell<VecDeque<SwitchRequestInfo>>,
|
pub switch_requests: RefCell<VecDeque<SwitchRequestInfo>>,
|
||||||
pub switch_info_requests: RefCell<VecDeque<PcduSwitch>>,
|
pub switch_info_requests: RefCell<VecDeque<SwitchId>>,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub switch_delay_request_count: u32,
|
pub switch_delay_request_count: u32,
|
||||||
pub next_switch_delay: Duration,
|
pub next_switch_delay: Duration,
|
||||||
@@ -120,13 +182,11 @@ impl Default for TestSwitchHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PowerSwitchInfo<PcduSwitch> for TestSwitchHelper {
|
/*
|
||||||
|
impl PowerSwitchInfo<SwitchId> for TestSwitchHelper {
|
||||||
type Error = SwitchInfoError;
|
type Error = SwitchInfoError;
|
||||||
|
|
||||||
fn switch_state(
|
fn switch_state(&self, switch_id: SwitchId) -> Result<satrs::power::SwitchState, Self::Error> {
|
||||||
&self,
|
|
||||||
switch_id: PcduSwitch,
|
|
||||||
) -> Result<satrs::power::SwitchState, Self::Error> {
|
|
||||||
let mut switch_info_requests_mut = self.switch_info_requests.borrow_mut();
|
let mut switch_info_requests_mut = self.switch_info_requests.borrow_mut();
|
||||||
switch_info_requests_mut.push_back(switch_id);
|
switch_info_requests_mut.push_back(switch_id);
|
||||||
if !self.switch_map_valid {
|
if !self.switch_map_valid {
|
||||||
@@ -144,13 +204,13 @@ impl PowerSwitchInfo<PcduSwitch> for TestSwitchHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PowerSwitcherCommandSender<PcduSwitch> for TestSwitchHelper {
|
impl PowerSwitcherCommandSender<SwitchId> for TestSwitchHelper {
|
||||||
type Error = SwitchCommandingError;
|
type Error = SwitchCommandingError;
|
||||||
|
|
||||||
fn send_switch_on_cmd(
|
fn send_switch_on_cmd(
|
||||||
&self,
|
&self,
|
||||||
requestor_info: MessageMetadata,
|
requestor_info: MessageMetadata,
|
||||||
switch_id: PcduSwitch,
|
switch_id: SwitchId,
|
||||||
) -> Result<(), Self::Error> {
|
) -> Result<(), Self::Error> {
|
||||||
let mut switch_requests_mut = self.switch_requests.borrow_mut();
|
let mut switch_requests_mut = self.switch_requests.borrow_mut();
|
||||||
switch_requests_mut.push_back(SwitchRequestInfo {
|
switch_requests_mut.push_back(SwitchRequestInfo {
|
||||||
@@ -170,7 +230,7 @@ impl PowerSwitcherCommandSender<PcduSwitch> for TestSwitchHelper {
|
|||||||
fn send_switch_off_cmd(
|
fn send_switch_off_cmd(
|
||||||
&self,
|
&self,
|
||||||
requestor_info: MessageMetadata,
|
requestor_info: MessageMetadata,
|
||||||
switch_id: PcduSwitch,
|
switch_id: SwitchId,
|
||||||
) -> Result<(), Self::Error> {
|
) -> Result<(), Self::Error> {
|
||||||
let mut switch_requests_mut = self.switch_requests.borrow_mut();
|
let mut switch_requests_mut = self.switch_requests.borrow_mut();
|
||||||
switch_requests_mut.push_back(SwitchRequestInfo {
|
switch_requests_mut.push_back(SwitchRequestInfo {
|
||||||
@@ -187,11 +247,12 @@ impl PowerSwitcherCommandSender<PcduSwitch> for TestSwitchHelper {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
impl TestSwitchHelper {
|
impl TestSwitchHelper {
|
||||||
// Helper function which can be used to force a switch to another state for test purposes.
|
// Helper function which can be used to force a switch to another state for test purposes.
|
||||||
pub fn set_switch_state(&mut self, switch: PcduSwitch, state: SwitchState) {
|
pub fn set_switch_state(&mut self, switch: SwitchId, state: SwitchState) {
|
||||||
self.switch_map.get_mut().0.insert(switch, state);
|
self.switch_map.get_mut().0.insert(switch, state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+299
-291
@@ -1,43 +1,109 @@
|
|||||||
use std::{
|
use std::{
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
collections::VecDeque,
|
collections::{HashMap, VecDeque},
|
||||||
sync::{mpsc, Arc, Mutex},
|
sync::{Arc, Mutex, mpsc},
|
||||||
};
|
};
|
||||||
|
|
||||||
use derive_new::new;
|
use derive_new::new;
|
||||||
|
use models::{
|
||||||
|
ComponentId, DeviceMode,
|
||||||
|
ccsds::{CcsdsTcPacketOwned, CcsdsTmPacketOwned},
|
||||||
|
pcdu::{
|
||||||
|
self, SwitchId, SwitchMapBinary, SwitchMapBinaryWrapper, SwitchRequest, SwitchState,
|
||||||
|
SwitchStateBinary, SwitchesBitfield,
|
||||||
|
},
|
||||||
|
};
|
||||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||||
use satrs::{
|
use satrs::{request::GenericMessage, spacepackets::CcsdsPacketIdAndPsc};
|
||||||
hk::{HkRequest, HkRequestVariant},
|
use satrs_example::TimestampHelper;
|
||||||
mode::{
|
|
||||||
ModeAndSubmode, ModeError, ModeProvider, ModeReply, ModeRequestHandler,
|
|
||||||
ModeRequestHandlerMpscBounded,
|
|
||||||
},
|
|
||||||
mode_tree::{ModeChild, ModeNode},
|
|
||||||
power::SwitchRequest,
|
|
||||||
pus::{EcssTmSender, PusTmVariant},
|
|
||||||
queue::GenericSendError,
|
|
||||||
request::{GenericMessage, MessageMetadata, UniqueApidTargetId},
|
|
||||||
spacepackets::ByteConversionError,
|
|
||||||
};
|
|
||||||
use satrs_example::{
|
|
||||||
config::components::NO_SENDER,
|
|
||||||
ids::{eps::PCDU, generic_pus::PUS_MODE},
|
|
||||||
DeviceMode, TimestampHelper,
|
|
||||||
};
|
|
||||||
use satrs_minisim::{
|
use satrs_minisim::{
|
||||||
eps::{
|
|
||||||
PcduReply, PcduRequest, PcduSwitch, SwitchMap, SwitchMapBinaryWrapper, SwitchMapWrapper,
|
|
||||||
},
|
|
||||||
SerializableSimMsgPayload, SimReply, SimRequest,
|
SerializableSimMsgPayload, SimReply, SimRequest,
|
||||||
|
eps::{PcduReply, PcduRequest},
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use strum::IntoEnumIterator as _;
|
||||||
|
|
||||||
use crate::{
|
use crate::ccsds::pack_ccsds_tm_packet_for_now;
|
||||||
hk::PusHkHelper,
|
|
||||||
pus::hk::{HkReply, HkReplyVariant},
|
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
requests::CompositeRequest,
|
pub struct SwitchSet {
|
||||||
tmtc::sender::TmTcSender,
|
pub valid: bool,
|
||||||
};
|
pub switch_map: SwitchMap,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SwitchSet {
|
||||||
|
pub fn new(switch_map: SwitchMap) -> Self {
|
||||||
|
Self {
|
||||||
|
valid: true,
|
||||||
|
switch_map,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_with_init_switches_unknown() -> Self {
|
||||||
|
let wrapper = SwitchMapWrapper::default();
|
||||||
|
Self::new(wrapper.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_bitfield(&self) -> Option<SwitchesBitfield> {
|
||||||
|
for entry in SwitchId::iter() {
|
||||||
|
if !self.switch_map.contains_key(&entry) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(
|
||||||
|
SwitchesBitfield::builder()
|
||||||
|
.with_magnetorquer(*self.switch_map.get(&SwitchId::Mgt).unwrap() == SwitchState::On)
|
||||||
|
.with_mgm1(*self.switch_map.get(&SwitchId::Mgm1).unwrap() == SwitchState::On)
|
||||||
|
.with_mgm0(*self.switch_map.get(&SwitchId::Mgm0).unwrap() == SwitchState::On)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn set_switch_state(&mut self, switch_id: SwitchId, state: SwitchState) -> bool {
|
||||||
|
if !self.switch_map.contains_key(&switch_id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*self.switch_map.get_mut(&switch_id).unwrap() = state;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type SwitchMap = HashMap<SwitchId, SwitchState>;
|
||||||
|
|
||||||
|
pub struct SwitchMapWrapper(pub SwitchMap);
|
||||||
|
|
||||||
|
impl Default for SwitchMapWrapper {
|
||||||
|
fn default() -> Self {
|
||||||
|
let mut switch_map = SwitchMap::default();
|
||||||
|
for entry in SwitchId::iter() {
|
||||||
|
switch_map.insert(entry, SwitchState::Unknown);
|
||||||
|
}
|
||||||
|
Self(switch_map)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SwitchMapWrapper {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn new_with_init_switches_off() -> Self {
|
||||||
|
let mut switch_map = SwitchMap::default();
|
||||||
|
for entry in SwitchId::iter() {
|
||||||
|
switch_map.insert(entry, SwitchState::Off);
|
||||||
|
}
|
||||||
|
Self(switch_map)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_binary_switch_map_ref(switch_map: &SwitchMapBinary) -> Self {
|
||||||
|
Self(
|
||||||
|
switch_map
|
||||||
|
.iter()
|
||||||
|
.map(|(key, value)| (*key, SwitchState::from(*value)))
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type SharedSwitchSet = Arc<Mutex<SwitchSet>>;
|
||||||
|
|
||||||
pub trait SerialInterface {
|
pub trait SerialInterface {
|
||||||
type Error: core::fmt::Debug;
|
type Error: core::fmt::Debug;
|
||||||
@@ -194,49 +260,50 @@ pub enum OpCode {
|
|||||||
PollAndRecvReplies = 1,
|
PollAndRecvReplies = 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
|
|
||||||
pub struct SwitchSet {
|
|
||||||
pub valid: bool,
|
|
||||||
pub switch_map: SwitchMap,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type SharedSwitchSet = Arc<Mutex<SwitchSet>>;
|
|
||||||
|
|
||||||
/// Example PCDU device handler.
|
/// Example PCDU device handler.
|
||||||
#[derive(new)]
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub struct PcduHandler<ComInterface: SerialInterface> {
|
pub struct PcduHandler<ComInterface: SerialInterface> {
|
||||||
id: UniqueApidTargetId,
|
|
||||||
dev_str: &'static str,
|
dev_str: &'static str,
|
||||||
mode_node: ModeRequestHandlerMpscBounded,
|
|
||||||
composite_request_rx: mpsc::Receiver<GenericMessage<CompositeRequest>>,
|
|
||||||
hk_reply_tx: mpsc::SyncSender<GenericMessage<HkReply>>,
|
|
||||||
switch_request_rx: mpsc::Receiver<GenericMessage<SwitchRequest>>,
|
switch_request_rx: mpsc::Receiver<GenericMessage<SwitchRequest>>,
|
||||||
tm_sender: TmTcSender,
|
tc_rx: std::sync::mpsc::Receiver<CcsdsTcPacketOwned>,
|
||||||
|
tm_tx: mpsc::SyncSender<CcsdsTmPacketOwned>,
|
||||||
pub com_interface: ComInterface,
|
pub com_interface: ComInterface,
|
||||||
shared_switch_map: Arc<Mutex<SwitchSet>>,
|
shared_switch_map: Arc<Mutex<SwitchSet>>,
|
||||||
#[new(value = "PusHkHelper::new(id)")]
|
mode: DeviceMode,
|
||||||
hk_helper: PusHkHelper,
|
|
||||||
#[new(value = "ModeAndSubmode::new(satrs_example::DeviceMode::Off as u32, 0)")]
|
|
||||||
mode_and_submode: ModeAndSubmode,
|
|
||||||
#[new(default)]
|
|
||||||
stamp_helper: TimestampHelper,
|
stamp_helper: TimestampHelper,
|
||||||
#[new(value = "[0; 256]")]
|
|
||||||
tm_buf: [u8; 256],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<ComInterface: SerialInterface> PcduHandler<ComInterface> {
|
impl<ComInterface: SerialInterface> PcduHandler<ComInterface> {
|
||||||
|
pub fn new(
|
||||||
|
tc_rx: std::sync::mpsc::Receiver<CcsdsTcPacketOwned>,
|
||||||
|
tm_tx: std::sync::mpsc::SyncSender<CcsdsTmPacketOwned>,
|
||||||
|
switch_request_rx: mpsc::Receiver<GenericMessage<SwitchRequest>>,
|
||||||
|
com_interface: ComInterface,
|
||||||
|
shared_switch_map: Arc<Mutex<SwitchSet>>,
|
||||||
|
init_mode: DeviceMode,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
dev_str: "PCDU",
|
||||||
|
tc_rx,
|
||||||
|
switch_request_rx,
|
||||||
|
tm_tx,
|
||||||
|
com_interface,
|
||||||
|
shared_switch_map,
|
||||||
|
stamp_helper: TimestampHelper::default(),
|
||||||
|
// Start in normal mode by default. Assume that the PCDU itself is on by default.
|
||||||
|
mode: init_mode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn periodic_operation(&mut self, op_code: OpCode) {
|
pub fn periodic_operation(&mut self, op_code: OpCode) {
|
||||||
match op_code {
|
match op_code {
|
||||||
OpCode::RegularOp => {
|
OpCode::RegularOp => {
|
||||||
self.stamp_helper.update_from_now();
|
self.stamp_helper.update_from_now();
|
||||||
// Handle requests.
|
// Handle requests.
|
||||||
self.handle_composite_requests();
|
self.handle_telecommands();
|
||||||
self.handle_mode_requests();
|
|
||||||
self.handle_switch_requests();
|
self.handle_switch_requests();
|
||||||
// Poll the switch states and/or telemetry regularly here.
|
// Poll the switch states and/or telemetry regularly here.
|
||||||
if self.mode() == DeviceMode::Normal as u32 || self.mode() == DeviceMode::On as u32
|
if self.mode() == DeviceMode::Normal || self.mode() == DeviceMode::On {
|
||||||
{
|
|
||||||
self.handle_periodic_commands();
|
self.handle_periodic_commands();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -246,75 +313,122 @@ impl<ComInterface: SerialInterface> PcduHandler<ComInterface> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_composite_requests(&mut self) {
|
#[inline]
|
||||||
loop {
|
pub fn mode(&self) -> DeviceMode {
|
||||||
match self.composite_request_rx.try_recv() {
|
self.mode
|
||||||
Ok(ref msg) => match &msg.message {
|
}
|
||||||
CompositeRequest::Hk(hk_request) => {
|
|
||||||
self.handle_hk_request(&msg.requestor_info, hk_request)
|
|
||||||
}
|
|
||||||
// TODO: This object does not have actions (yet).. Still send back completion failure
|
|
||||||
// reply.
|
|
||||||
CompositeRequest::Action(_action_req) => {}
|
|
||||||
},
|
|
||||||
|
|
||||||
Err(e) => {
|
pub fn handle_telecommands(&mut self) {
|
||||||
if e != mpsc::TryRecvError::Empty {
|
loop {
|
||||||
log::warn!(
|
match self.tc_rx.try_recv() {
|
||||||
"{}: failed to receive composite request: {:?}",
|
Ok(packet) => {
|
||||||
self.dev_str,
|
let tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
|
||||||
e
|
match postcard::from_bytes::<pcdu::request::Request>(&packet.payload) {
|
||||||
);
|
Ok(request) => {
|
||||||
} else {
|
log::info!(
|
||||||
break;
|
"received request {:?} with TC ID {:#010x}",
|
||||||
|
request,
|
||||||
|
tc_id.raw()
|
||||||
|
);
|
||||||
|
match request {
|
||||||
|
pcdu::request::Request::Ping => {
|
||||||
|
self.send_tm(Some(tc_id), pcdu::response::Response::Ok)
|
||||||
|
}
|
||||||
|
pcdu::request::Request::GetSwitches => self.send_tm(
|
||||||
|
Some(tc_id),
|
||||||
|
pcdu::response::Response::Switches(
|
||||||
|
self.shared_switch_map
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.as_bitfield()
|
||||||
|
.expect("could not build switches response"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pcdu::request::Request::EnableSwitches(switches) => {
|
||||||
|
self.handle_switches_bitfield_request(
|
||||||
|
switches,
|
||||||
|
SwitchStateBinary::On,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
pcdu::request::Request::DisableSwitches(switches) => {
|
||||||
|
self.handle_switches_bitfield_request(
|
||||||
|
switches,
|
||||||
|
SwitchStateBinary::Off,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
pcdu::request::Request::Mode(device_mode) => {
|
||||||
|
self.switch_mode(tc_id, device_mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("failed to deserialize request: {}", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Err(e) => match e {
|
||||||
|
std::sync::mpsc::TryRecvError::Empty => break,
|
||||||
|
std::sync::mpsc::TryRecvError::Disconnected => {
|
||||||
|
log::warn!("packet sender disconnected")
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_hk_request(&mut self, requestor_info: &MessageMetadata, hk_request: &HkRequest) {
|
pub fn handle_switches_bitfield_request(
|
||||||
match hk_request.variant {
|
&mut self,
|
||||||
HkRequestVariant::OneShot => {
|
switches: SwitchesBitfield,
|
||||||
if hk_request.unique_id == SetId::SwitcherSet as u32 {
|
state: SwitchStateBinary,
|
||||||
if let Ok(hk_tm) = self.hk_helper.generate_hk_report_packet(
|
) {
|
||||||
self.stamp_helper.stamp(),
|
if switches.mgm0() {
|
||||||
SetId::SwitcherSet as u32,
|
self.handle_device_switching(SwitchId::Mgm0, state);
|
||||||
&mut |hk_buf| {
|
}
|
||||||
// Send TM down as JSON.
|
if switches.mgm1() {
|
||||||
let switch_map_snapshot = self
|
self.handle_device_switching(SwitchId::Mgm1, state);
|
||||||
.shared_switch_map
|
}
|
||||||
.lock()
|
if switches.magnetorquer() {
|
||||||
.expect("failed to lock switch map")
|
self.handle_device_switching(SwitchId::Mgt, state);
|
||||||
.clone();
|
}
|
||||||
let switch_map_json = serde_json::to_string(&switch_map_snapshot)
|
}
|
||||||
.expect("failed to serialize switch map");
|
|
||||||
if switch_map_json.len() > hk_buf.len() {
|
pub fn send_tm(&self, tc_id: Option<CcsdsPacketIdAndPsc>, response: pcdu::response::Response) {
|
||||||
log::error!("switch map JSON too large for HK buffer");
|
match pack_ccsds_tm_packet_for_now(ComponentId::EpsPcdu, tc_id, &response) {
|
||||||
return Err(ByteConversionError::ToSliceTooSmall {
|
Ok(packet) => {
|
||||||
found: hk_buf.len(),
|
if let Err(e) = self.tm_tx.send(packet) {
|
||||||
expected: switch_map_json.len(),
|
log::warn!("failed to send TM packet: {}", e);
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(switch_map_json.len())
|
|
||||||
},
|
|
||||||
&mut self.tm_buf,
|
|
||||||
) {
|
|
||||||
self.tm_sender
|
|
||||||
.send_tm(self.id.id(), PusTmVariant::Direct(hk_tm))
|
|
||||||
.expect("failed to send HK TM");
|
|
||||||
self.hk_reply_tx
|
|
||||||
.send(GenericMessage::new(
|
|
||||||
*requestor_info,
|
|
||||||
HkReply::new(hk_request.unique_id, HkReplyVariant::Ack),
|
|
||||||
))
|
|
||||||
.expect("failed to send HK reply");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HkRequestVariant::EnablePeriodic => todo!(),
|
Err(e) => {
|
||||||
HkRequestVariant::DisablePeriodic => todo!(),
|
log::warn!("failed to pack TM packet: {}", e);
|
||||||
HkRequestVariant::ModifyCollectionInterval(_) => todo!(),
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn switch_mode(&mut self, requestor: CcsdsPacketIdAndPsc, mode: DeviceMode) {
|
||||||
|
log::info!("{}: transitioning to mode {:?}", self.dev_str, mode);
|
||||||
|
self.mode = mode;
|
||||||
|
if self.mode() == DeviceMode::Off {
|
||||||
|
self.shared_switch_map.lock().unwrap().valid = false;
|
||||||
|
}
|
||||||
|
log::info!("{} announcing mode: {:?}", self.dev_str, self.mode);
|
||||||
|
self.send_telemetry(Some(requestor), pcdu::response::Response::Ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_telemetry(
|
||||||
|
&self,
|
||||||
|
tc_id: Option<CcsdsPacketIdAndPsc>,
|
||||||
|
response: pcdu::response::Response,
|
||||||
|
) {
|
||||||
|
match pack_ccsds_tm_packet_for_now(ComponentId::EpsPcdu, tc_id, &response) {
|
||||||
|
Ok(packet) => {
|
||||||
|
if let Err(e) = self.tm_tx.send(packet) {
|
||||||
|
log::warn!("failed to send TM packet: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("failed to pack TM packet: {}", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,6 +440,7 @@ impl<ComInterface: SerialInterface> PcduHandler<ComInterface> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
pub fn handle_mode_requests(&mut self) {
|
pub fn handle_mode_requests(&mut self) {
|
||||||
loop {
|
loop {
|
||||||
// TODO: Only allow one set mode request per cycle?
|
// TODO: Only allow one set mode request per cycle?
|
||||||
@@ -356,23 +471,28 @@ impl<ComInterface: SerialInterface> PcduHandler<ComInterface> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
pub fn handle_device_switching(&mut self, switch_id: SwitchId, state: SwitchStateBinary) {
|
||||||
|
let pcdu_req = PcduRequest::SwitchDevice {
|
||||||
|
switch: switch_id,
|
||||||
|
state,
|
||||||
|
};
|
||||||
|
let pcdu_req_ser = serde_json::to_string(&pcdu_req).unwrap();
|
||||||
|
self.com_interface
|
||||||
|
.send(pcdu_req_ser.as_bytes())
|
||||||
|
.expect("failed to send switch request to PCDU");
|
||||||
|
}
|
||||||
|
|
||||||
pub fn handle_switch_requests(&mut self) {
|
pub fn handle_switch_requests(&mut self) {
|
||||||
loop {
|
loop {
|
||||||
match self.switch_request_rx.try_recv() {
|
match self.switch_request_rx.try_recv() {
|
||||||
Ok(switch_req) => match PcduSwitch::try_from(switch_req.message.switch_id()) {
|
Ok(switch_req) => {
|
||||||
Ok(pcdu_switch) => {
|
self.handle_device_switching(
|
||||||
let pcdu_req = PcduRequest::SwitchDevice {
|
switch_req.message.switch_id(),
|
||||||
switch: pcdu_switch,
|
switch_req.message.target_state(),
|
||||||
state: switch_req.message.target_state(),
|
);
|
||||||
};
|
}
|
||||||
let pcdu_req_ser = serde_json::to_string(&pcdu_req).unwrap();
|
|
||||||
self.com_interface
|
|
||||||
.send(pcdu_req_ser.as_bytes())
|
|
||||||
.expect("failed to send switch request to PCDU");
|
|
||||||
}
|
|
||||||
Err(e) => todo!("failed to convert switch ID {:?} to typed PCDU switch", e),
|
|
||||||
},
|
|
||||||
Err(e) => match e {
|
Err(e) => match e {
|
||||||
mpsc::TryRecvError::Empty => break,
|
mpsc::TryRecvError::Empty => break,
|
||||||
mpsc::TryRecvError::Disconnected => {
|
mpsc::TryRecvError::Disconnected => {
|
||||||
@@ -406,115 +526,33 @@ impl<ComInterface: SerialInterface> PcduHandler<ComInterface> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<ComInterface: SerialInterface> ModeProvider for PcduHandler<ComInterface> {
|
|
||||||
fn mode_and_submode(&self) -> ModeAndSubmode {
|
|
||||||
self.mode_and_submode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<ComInterface: SerialInterface> ModeRequestHandler for PcduHandler<ComInterface> {
|
|
||||||
type Error = ModeError;
|
|
||||||
fn start_transition(
|
|
||||||
&mut self,
|
|
||||||
requestor: MessageMetadata,
|
|
||||||
mode_and_submode: ModeAndSubmode,
|
|
||||||
_forced: bool,
|
|
||||||
) -> Result<(), satrs::mode::ModeError> {
|
|
||||||
log::info!(
|
|
||||||
"{}: transitioning to mode {:?}",
|
|
||||||
self.dev_str,
|
|
||||||
mode_and_submode
|
|
||||||
);
|
|
||||||
self.mode_and_submode = mode_and_submode;
|
|
||||||
if mode_and_submode.mode() == DeviceMode::Off as u32 {
|
|
||||||
self.shared_switch_map.lock().unwrap().valid = false;
|
|
||||||
}
|
|
||||||
self.handle_mode_reached(Some(requestor))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn announce_mode(&self, _requestor_info: Option<MessageMetadata>, _recursive: bool) {
|
|
||||||
log::info!(
|
|
||||||
"{} announcing mode: {:?}",
|
|
||||||
self.dev_str,
|
|
||||||
self.mode_and_submode
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_mode_reached(
|
|
||||||
&mut self,
|
|
||||||
requestor: Option<MessageMetadata>,
|
|
||||||
) -> Result<(), Self::Error> {
|
|
||||||
self.announce_mode(requestor, false);
|
|
||||||
if let Some(requestor) = requestor {
|
|
||||||
if requestor.sender_id() == NO_SENDER {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
if requestor.sender_id() != PUS_MODE.id() {
|
|
||||||
log::warn!(
|
|
||||||
"can not send back mode reply to sender {}",
|
|
||||||
requestor.sender_id()
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
self.send_mode_reply(requestor, ModeReply::ModeReply(self.mode_and_submode()))?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send_mode_reply(
|
|
||||||
&self,
|
|
||||||
requestor: MessageMetadata,
|
|
||||||
reply: ModeReply,
|
|
||||||
) -> Result<(), Self::Error> {
|
|
||||||
if requestor.sender_id() != PUS_MODE.id() {
|
|
||||||
log::warn!(
|
|
||||||
"can not send back mode reply to sender {}",
|
|
||||||
requestor.sender_id()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
self.mode_node
|
|
||||||
.send_mode_reply(requestor, reply)
|
|
||||||
.map_err(|_| GenericSendError::RxDisconnected)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_mode_info(
|
|
||||||
&mut self,
|
|
||||||
_requestor_info: MessageMetadata,
|
|
||||||
_info: ModeAndSubmode,
|
|
||||||
) -> Result<(), Self::Error> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<ComInterface: SerialInterface> ModeNode for PcduHandler<ComInterface> {
|
|
||||||
fn id(&self) -> satrs::ComponentId {
|
|
||||||
PCDU.into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<ComInterface: SerialInterface> ModeChild for PcduHandler<ComInterface> {
|
|
||||||
type Sender = mpsc::SyncSender<GenericMessage<ModeReply>>;
|
|
||||||
|
|
||||||
fn add_mode_parent(&mut self, id: satrs::ComponentId, reply_sender: Self::Sender) {
|
|
||||||
self.mode_node.add_message_target(id, reply_sender);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
|
|
||||||
use arbitrary_int::u21;
|
use arbitrary_int::u11;
|
||||||
use satrs::{
|
use models::{
|
||||||
mode::ModeRequest, power::SwitchStateBinary, request::GenericMessage, tmtc::PacketAsVec,
|
Apid, TcHeader,
|
||||||
|
pcdu::{SwitchMapBinary, SwitchStateBinary},
|
||||||
|
};
|
||||||
|
use satrs::{
|
||||||
|
mode::{ModeReply, ModeRequest},
|
||||||
|
request::{GenericMessage, MessageMetadata},
|
||||||
|
spacepackets::SpacePacketHeader,
|
||||||
};
|
};
|
||||||
use satrs_example::ids::{self, Apid};
|
|
||||||
use satrs_minisim::eps::SwitchMapBinary;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
pub fn create_request_tc(
|
||||||
|
request: models::pcdu::request::Request,
|
||||||
|
) -> models::ccsds::CcsdsTcPacketOwned {
|
||||||
|
models::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||||
|
SpacePacketHeader::new_from_apid(u11::new(Apid::Eps as u16)),
|
||||||
|
TcHeader::new(ComponentId::EpsPcdu, request.message_type()),
|
||||||
|
request,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct SerialInterfaceTest {
|
pub struct SerialInterfaceTest {
|
||||||
pub inner: SerialInterfaceDummy,
|
pub inner: SerialInterfaceDummy,
|
||||||
@@ -550,47 +588,37 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub struct PcduTestbench {
|
pub struct PcduTestbench {
|
||||||
pub mode_request_tx: mpsc::SyncSender<GenericMessage<ModeRequest>>,
|
pub mode_request_tx: mpsc::SyncSender<GenericMessage<ModeRequest>>,
|
||||||
pub mode_reply_rx_to_pus: mpsc::Receiver<GenericMessage<ModeReply>>,
|
|
||||||
pub mode_reply_rx_to_parent: mpsc::Receiver<GenericMessage<ModeReply>>,
|
pub mode_reply_rx_to_parent: mpsc::Receiver<GenericMessage<ModeReply>>,
|
||||||
pub composite_request_tx: mpsc::Sender<GenericMessage<CompositeRequest>>,
|
pub tc_tx: mpsc::SyncSender<CcsdsTcPacketOwned>,
|
||||||
pub hk_reply_rx: mpsc::Receiver<GenericMessage<HkReply>>,
|
pub tm_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
|
||||||
pub tm_rx: mpsc::Receiver<PacketAsVec>,
|
|
||||||
pub switch_request_tx: mpsc::Sender<GenericMessage<SwitchRequest>>,
|
pub switch_request_tx: mpsc::Sender<GenericMessage<SwitchRequest>>,
|
||||||
pub handler: PcduHandler<SerialInterfaceTest>,
|
pub handler: PcduHandler<SerialInterfaceTest>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PcduTestbench {
|
impl PcduTestbench {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let (mode_request_tx, mode_request_rx) = mpsc::sync_channel(5);
|
let (mode_request_tx, _mode_request_rx) = mpsc::sync_channel(5);
|
||||||
let (mode_reply_tx_to_pus, mode_reply_rx_to_pus) = mpsc::sync_channel(5);
|
let (_mode_reply_tx_to_parent, mode_reply_rx_to_parent) = mpsc::sync_channel(5);
|
||||||
let (mode_reply_tx_to_parent, mode_reply_rx_to_parent) = mpsc::sync_channel(5);
|
let (tc_tx, tc_rx) = mpsc::sync_channel(5);
|
||||||
let mode_node = ModeRequestHandlerMpscBounded::new(PCDU.into(), mode_request_rx);
|
let (tm_tx, tm_rx) = mpsc::sync_channel(5);
|
||||||
let (composite_request_tx, composite_request_rx) = mpsc::channel();
|
|
||||||
let (hk_reply_tx, hk_reply_rx) = mpsc::sync_channel(10);
|
|
||||||
let (tm_tx, tm_rx) = mpsc::sync_channel::<PacketAsVec>(5);
|
|
||||||
let (switch_request_tx, switch_reqest_rx) = mpsc::channel();
|
let (switch_request_tx, switch_reqest_rx) = mpsc::channel();
|
||||||
let shared_switch_map = Arc::new(Mutex::new(SwitchSet::default()));
|
let shared_switch_map =
|
||||||
let mut handler = PcduHandler::new(
|
Arc::new(Mutex::new(SwitchSet::new_with_init_switches_unknown()));
|
||||||
UniqueApidTargetId::new(Apid::Eps.raw_value(), u21::new(0)),
|
let handler = PcduHandler::new(
|
||||||
"TEST_PCDU",
|
tc_rx,
|
||||||
mode_node,
|
tm_tx.clone(),
|
||||||
composite_request_rx,
|
|
||||||
hk_reply_tx,
|
|
||||||
switch_reqest_rx,
|
switch_reqest_rx,
|
||||||
TmTcSender::Heap(tm_tx.clone()),
|
|
||||||
SerialInterfaceTest::default(),
|
SerialInterfaceTest::default(),
|
||||||
shared_switch_map,
|
shared_switch_map,
|
||||||
|
DeviceMode::Off,
|
||||||
);
|
);
|
||||||
handler.add_mode_parent(ids::eps::SUBSYSTEM.into(), mode_reply_tx_to_parent);
|
|
||||||
handler.add_mode_parent(PUS_MODE.into(), mode_reply_tx_to_pus);
|
|
||||||
Self {
|
Self {
|
||||||
mode_request_tx,
|
mode_request_tx,
|
||||||
mode_reply_rx_to_pus,
|
|
||||||
mode_reply_rx_to_parent,
|
mode_reply_rx_to_parent,
|
||||||
composite_request_tx,
|
tc_tx,
|
||||||
hk_reply_rx,
|
|
||||||
tm_rx,
|
tm_rx,
|
||||||
switch_request_tx,
|
switch_request_tx,
|
||||||
handler,
|
handler,
|
||||||
@@ -610,7 +638,7 @@ mod tests {
|
|||||||
pub fn verify_switch_req_was_sent(
|
pub fn verify_switch_req_was_sent(
|
||||||
&self,
|
&self,
|
||||||
expected_queue_len: usize,
|
expected_queue_len: usize,
|
||||||
switch_id: PcduSwitch,
|
switch_id: SwitchId,
|
||||||
target_state: SwitchStateBinary,
|
target_state: SwitchStateBinary,
|
||||||
) {
|
) {
|
||||||
// Check that there is now communication happening.
|
// Check that there is now communication happening.
|
||||||
@@ -651,11 +679,7 @@ mod tests {
|
|||||||
testbench.handler.com_interface.reply_queue.borrow().len(),
|
testbench.handler.com_interface.reply_queue.borrow().len(),
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(testbench.handler.mode(), DeviceMode::Off);
|
||||||
testbench.handler.mode_and_submode().mode(),
|
|
||||||
DeviceMode::Off as u32
|
|
||||||
);
|
|
||||||
assert_eq!(testbench.handler.mode_and_submode().submode(), 0_u16);
|
|
||||||
testbench.handler.periodic_operation(OpCode::RegularOp);
|
testbench.handler.periodic_operation(OpCode::RegularOp);
|
||||||
testbench
|
testbench
|
||||||
.handler
|
.handler
|
||||||
@@ -666,39 +690,27 @@ mod tests {
|
|||||||
testbench.handler.com_interface.reply_queue.borrow().len(),
|
testbench.handler.com_interface.reply_queue.borrow().len(),
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(testbench.handler.mode(), DeviceMode::Off);
|
||||||
testbench.handler.mode_and_submode().mode(),
|
|
||||||
DeviceMode::Off as u32
|
|
||||||
);
|
|
||||||
assert_eq!(testbench.handler.mode_and_submode().submode(), 0_u16);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_normal_mode() {
|
fn test_normal_mode() {
|
||||||
let mut testbench = PcduTestbench::new();
|
let mut testbench = PcduTestbench::new();
|
||||||
testbench
|
testbench
|
||||||
.mode_request_tx
|
.tc_tx
|
||||||
.send(GenericMessage::new(
|
.send(create_request_tc(pcdu::request::Request::Mode(
|
||||||
MessageMetadata::new(0, PUS_MODE.id()),
|
DeviceMode::Normal,
|
||||||
ModeRequest::SetMode {
|
)))
|
||||||
mode_and_submode: ModeAndSubmode::new(DeviceMode::Normal as u32, 0),
|
.unwrap();
|
||||||
forced: false,
|
|
||||||
},
|
|
||||||
))
|
|
||||||
.expect("failed to send mode request");
|
|
||||||
let switch_map_shared = testbench.handler.shared_switch_map.lock().unwrap();
|
let switch_map_shared = testbench.handler.shared_switch_map.lock().unwrap();
|
||||||
assert!(!switch_map_shared.valid);
|
assert!(switch_map_shared.valid);
|
||||||
drop(switch_map_shared);
|
drop(switch_map_shared);
|
||||||
testbench.handler.periodic_operation(OpCode::RegularOp);
|
testbench.handler.periodic_operation(OpCode::RegularOp);
|
||||||
testbench
|
testbench
|
||||||
.handler
|
.handler
|
||||||
.periodic_operation(OpCode::PollAndRecvReplies);
|
.periodic_operation(OpCode::PollAndRecvReplies);
|
||||||
// Check correctness of mode.
|
// Check correctness of mode.
|
||||||
assert_eq!(
|
assert_eq!(testbench.handler.mode(), DeviceMode::Normal);
|
||||||
testbench.handler.mode_and_submode().mode(),
|
|
||||||
DeviceMode::Normal as u32
|
|
||||||
);
|
|
||||||
assert_eq!(testbench.handler.mode_and_submode().submode(), 0);
|
|
||||||
|
|
||||||
testbench.verify_switch_info_req_was_sent(1);
|
testbench.verify_switch_info_req_was_sent(1);
|
||||||
testbench.verify_switch_reply_received(1, SwitchMapBinaryWrapper::default().0);
|
testbench.verify_switch_reply_received(1, SwitchMapBinaryWrapper::default().0);
|
||||||
@@ -712,20 +724,16 @@ mod tests {
|
|||||||
fn test_switch_request_handling() {
|
fn test_switch_request_handling() {
|
||||||
let mut testbench = PcduTestbench::new();
|
let mut testbench = PcduTestbench::new();
|
||||||
testbench
|
testbench
|
||||||
.mode_request_tx
|
.tc_tx
|
||||||
.send(GenericMessage::new(
|
.send(create_request_tc(pcdu::request::Request::Mode(
|
||||||
MessageMetadata::new(0, PUS_MODE.id()),
|
DeviceMode::Normal,
|
||||||
ModeRequest::SetMode {
|
)))
|
||||||
mode_and_submode: ModeAndSubmode::new(DeviceMode::Normal as u32, 0),
|
.unwrap();
|
||||||
forced: false,
|
|
||||||
},
|
|
||||||
))
|
|
||||||
.expect("failed to send mode request");
|
|
||||||
testbench
|
testbench
|
||||||
.switch_request_tx
|
.switch_request_tx
|
||||||
.send(GenericMessage::new(
|
.send(GenericMessage::new(
|
||||||
MessageMetadata::new(0, ids::acs::MGM0.id()),
|
MessageMetadata::new(0, ComponentId::AcsMgm0 as u32),
|
||||||
SwitchRequest::new(0, SwitchStateBinary::On),
|
SwitchRequest::new(SwitchId::Mgm0, SwitchStateBinary::On),
|
||||||
))
|
))
|
||||||
.expect("failed to send switch request");
|
.expect("failed to send switch request");
|
||||||
testbench.handler.periodic_operation(OpCode::RegularOp);
|
testbench.handler.periodic_operation(OpCode::RegularOp);
|
||||||
@@ -733,11 +741,11 @@ mod tests {
|
|||||||
.handler
|
.handler
|
||||||
.periodic_operation(OpCode::PollAndRecvReplies);
|
.periodic_operation(OpCode::PollAndRecvReplies);
|
||||||
|
|
||||||
testbench.verify_switch_req_was_sent(2, PcduSwitch::Mgm, SwitchStateBinary::On);
|
testbench.verify_switch_req_was_sent(2, SwitchId::Mgm0, SwitchStateBinary::On);
|
||||||
testbench.verify_switch_info_req_was_sent(1);
|
testbench.verify_switch_info_req_was_sent(1);
|
||||||
let mut switch_map = SwitchMapBinaryWrapper::default().0;
|
let mut switch_map = SwitchMapBinaryWrapper::default().0;
|
||||||
*switch_map
|
*switch_map
|
||||||
.get_mut(&PcduSwitch::Mgm)
|
.get_mut(&SwitchId::Mgm0)
|
||||||
.expect("switch state setting failed") = SwitchStateBinary::On;
|
.expect("switch state setting failed") = SwitchStateBinary::On;
|
||||||
testbench.verify_switch_reply_received(1, switch_map);
|
testbench.verify_switch_reply_received(1, switch_map);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
use models::{ComponentId, Event, Message, ccsds::CcsdsTmPacketOwned, control};
|
||||||
|
|
||||||
|
use crate::ccsds::pack_ccsds_tm_packet_for_now;
|
||||||
|
|
||||||
|
// TODO: We should add the capability to enable/disable the TM generation of individual events and
|
||||||
|
// event groups as well.
|
||||||
|
pub struct EventManager {
|
||||||
|
pub ctrl_rx: std::sync::mpsc::Receiver<control::Event>,
|
||||||
|
pub tm_tx: std::sync::mpsc::SyncSender<CcsdsTmPacketOwned>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventManager {
|
||||||
|
pub fn periodic_operation(&mut self) {
|
||||||
|
if let Ok(event) = self.ctrl_rx.try_recv() {
|
||||||
|
self.event_to_tm(ComponentId::Controller, &Event::ControllerEvent(event));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn event_to_tm(
|
||||||
|
&mut self,
|
||||||
|
sender_id: ComponentId,
|
||||||
|
event: &(impl serde::Serialize + Message),
|
||||||
|
) {
|
||||||
|
match pack_ccsds_tm_packet_for_now(sender_id, None, event) {
|
||||||
|
Ok(packet) => {
|
||||||
|
if let Err(e) = self.tm_tx.send(packet) {
|
||||||
|
log::warn!("error sending event TM packet: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("error packing event TM packet: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
//! This is an auto-generated configuration module.
|
|
||||||
use satrs::request::UniqueApidTargetId;
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, strum::EnumIter)]
|
|
||||||
#[bitbybit::bitenum(u11)]
|
|
||||||
pub enum Apid {
|
|
||||||
Sched = 1,
|
|
||||||
GenericPus = 2,
|
|
||||||
Acs = 3,
|
|
||||||
Cfdp = 4,
|
|
||||||
Tmtc = 5,
|
|
||||||
Eps = 6,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod acs {
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
|
||||||
#[bitbybit::bitenum(u21, exhaustive = false)]
|
|
||||||
pub enum Id {
|
|
||||||
Subsystem = 1,
|
|
||||||
Assembly = 2,
|
|
||||||
Mgm0 = 3,
|
|
||||||
Mgm1 = 4,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const SUBSYSTEM: super::UniqueApidTargetId =
|
|
||||||
super::UniqueApidTargetId::new(super::Apid::Acs.raw_value(), Id::Subsystem.raw_value());
|
|
||||||
pub const ASSEMBLY: super::UniqueApidTargetId =
|
|
||||||
super::UniqueApidTargetId::new(super::Apid::Acs.raw_value(), Id::Assembly.raw_value());
|
|
||||||
pub const MGM0: super::UniqueApidTargetId =
|
|
||||||
super::UniqueApidTargetId::new(super::Apid::Acs.raw_value(), Id::Mgm0.raw_value());
|
|
||||||
pub const MGM1: super::UniqueApidTargetId =
|
|
||||||
super::UniqueApidTargetId::new(super::Apid::Acs.raw_value(), Id::Mgm1.raw_value());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod eps {
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
|
||||||
#[bitbybit::bitenum(u21, exhaustive = false)]
|
|
||||||
pub enum Id {
|
|
||||||
Pcdu = 0,
|
|
||||||
Subsystem = 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const PCDU: super::UniqueApidTargetId =
|
|
||||||
super::UniqueApidTargetId::new(super::Apid::Eps.raw_value(), Id::Pcdu.raw_value());
|
|
||||||
pub const SUBSYSTEM: super::UniqueApidTargetId =
|
|
||||||
super::UniqueApidTargetId::new(super::Apid::Eps.raw_value(), Id::Subsystem.raw_value());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod generic_pus {
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
|
||||||
#[bitbybit::bitenum(u21, exhaustive = false)]
|
|
||||||
pub enum Id {
|
|
||||||
PusEventManagement = 0,
|
|
||||||
PusRouting = 1,
|
|
||||||
PusTest = 2,
|
|
||||||
PusAction = 3,
|
|
||||||
PusMode = 4,
|
|
||||||
PusHk = 5,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const PUS_EVENT_MANAGEMENT: super::UniqueApidTargetId = super::UniqueApidTargetId::new(
|
|
||||||
super::Apid::GenericPus.raw_value(),
|
|
||||||
Id::PusEventManagement.raw_value(),
|
|
||||||
);
|
|
||||||
pub const PUS_ROUTING: super::UniqueApidTargetId = super::UniqueApidTargetId::new(
|
|
||||||
super::Apid::GenericPus.raw_value(),
|
|
||||||
Id::PusRouting.raw_value(),
|
|
||||||
);
|
|
||||||
pub const PUS_TEST: super::UniqueApidTargetId = super::UniqueApidTargetId::new(
|
|
||||||
super::Apid::GenericPus.raw_value(),
|
|
||||||
Id::PusTest.raw_value(),
|
|
||||||
);
|
|
||||||
pub const PUS_ACTION: super::UniqueApidTargetId = super::UniqueApidTargetId::new(
|
|
||||||
super::Apid::GenericPus.raw_value(),
|
|
||||||
Id::PusAction.raw_value(),
|
|
||||||
);
|
|
||||||
pub const PUS_MODE: super::UniqueApidTargetId = super::UniqueApidTargetId::new(
|
|
||||||
super::Apid::GenericPus.raw_value(),
|
|
||||||
Id::PusMode.raw_value(),
|
|
||||||
);
|
|
||||||
pub const PUS_HK: super::UniqueApidTargetId =
|
|
||||||
super::UniqueApidTargetId::new(super::Apid::GenericPus.raw_value(), Id::PusHk.raw_value());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod sched {
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
|
||||||
#[bitbybit::bitenum(u21, exhaustive = false)]
|
|
||||||
pub enum Id {
|
|
||||||
PusSched = 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const PUS_SCHED: super::UniqueApidTargetId =
|
|
||||||
super::UniqueApidTargetId::new(super::Apid::Sched.raw_value(), Id::PusSched.raw_value());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod tmtc {
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
|
||||||
#[bitbybit::bitenum(u21, exhaustive = false)]
|
|
||||||
pub enum Id {
|
|
||||||
UdpServer = 0,
|
|
||||||
TcpServer = 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const UDP_SERVER: super::UniqueApidTargetId =
|
|
||||||
super::UniqueApidTargetId::new(super::Apid::Tmtc.raw_value(), Id::UdpServer.raw_value());
|
|
||||||
pub const TCP_SERVER: super::UniqueApidTargetId =
|
|
||||||
super::UniqueApidTargetId::new(super::Apid::Tmtc.raw_value(), Id::TcpServer.raw_value());
|
|
||||||
}
|
|
||||||
@@ -7,8 +7,8 @@ use std::{
|
|||||||
|
|
||||||
use satrs::pus::HandlingStatus;
|
use satrs::pus::HandlingStatus;
|
||||||
use satrs_minisim::{
|
use satrs_minisim::{
|
||||||
udp::SIM_CTRL_PORT, SerializableSimMsgPayload, SimComponent, SimMessageProvider, SimReply,
|
SerializableSimMsgPayload, SimComponent, SimMessageProvider, SimReply, SimRequest,
|
||||||
SimRequest,
|
udp::SIM_CTRL_PORT,
|
||||||
};
|
};
|
||||||
use satrs_minisim::{SimCtrlReply, SimCtrlRequest};
|
use satrs_minisim::{SimCtrlReply, SimCtrlRequest};
|
||||||
|
|
||||||
@@ -187,16 +187,17 @@ pub mod tests {
|
|||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
net::{SocketAddr, UdpSocket},
|
net::{SocketAddr, UdpSocket},
|
||||||
sync::{
|
sync::{
|
||||||
|
Arc,
|
||||||
atomic::{AtomicBool, Ordering},
|
atomic::{AtomicBool, Ordering},
|
||||||
mpsc, Arc,
|
mpsc,
|
||||||
},
|
},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use satrs_minisim::{
|
use satrs_minisim::{
|
||||||
eps::{PcduReply, PcduRequest},
|
|
||||||
SerializableSimMsgPayload, SimComponent, SimCtrlReply, SimCtrlRequest, SimMessageProvider,
|
SerializableSimMsgPayload, SimComponent, SimCtrlReply, SimCtrlRequest, SimMessageProvider,
|
||||||
SimReply, SimRequest,
|
SimReply, SimRequest,
|
||||||
|
eps::{PcduReply, PcduRequest},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::SimClientUdp;
|
use super::SimClientUdp;
|
||||||
|
|||||||
@@ -129,15 +129,13 @@ impl TcpTask {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn periodic_operation(&mut self) {
|
pub fn periodic_operation(&mut self) {
|
||||||
loop {
|
let result = self
|
||||||
let result = self
|
.0
|
||||||
.0
|
.handle_all_connections(Some(Duration::from_millis(400)));
|
||||||
.handle_all_connections(Some(Duration::from_millis(400)));
|
match result {
|
||||||
match result {
|
Ok(_conn_result) => (),
|
||||||
Ok(_conn_result) => (),
|
Err(e) => {
|
||||||
Err(e) => {
|
warn!("TCP server error: {e:?}");
|
||||||
warn!("TCP server error: {e:?}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,66 +1,29 @@
|
|||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
|
use std::collections::VecDeque;
|
||||||
use std::net::{SocketAddr, UdpSocket};
|
use std::net::{SocketAddr, UdpSocket};
|
||||||
use std::sync::mpsc;
|
use std::sync::{Arc, Mutex, mpsc};
|
||||||
|
|
||||||
use log::{info, warn};
|
use log::warn;
|
||||||
|
use models::ccsds::CcsdsTmPacketOwned;
|
||||||
use satrs::hal::std::udp_server::{ReceiveResult, UdpTcServer};
|
use satrs::hal::std::udp_server::{ReceiveResult, UdpTcServer};
|
||||||
use satrs::pus::HandlingStatus;
|
use satrs::pus::HandlingStatus;
|
||||||
use satrs::queue::GenericSendError;
|
use satrs::queue::GenericSendError;
|
||||||
use satrs::tmtc::PacketAsVec;
|
|
||||||
|
|
||||||
use satrs::pool::{PoolProviderWithGuards, SharedStaticMemoryPool};
|
|
||||||
use satrs::tmtc::PacketInPool;
|
|
||||||
|
|
||||||
use crate::tmtc::sender::TmTcSender;
|
use crate::tmtc::sender::TmTcSender;
|
||||||
|
|
||||||
pub trait UdpTmHandler {
|
pub trait UdpTmHandlerProvider {
|
||||||
fn send_tm_to_udp_client(&mut self, socket: &UdpSocket, recv_addr: &SocketAddr);
|
fn send_tm_to_udp_client(&mut self, socket: &UdpSocket, recv_addr: &SocketAddr);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct StaticUdpTmHandler {
|
pub struct UdpTmHandlerWithChannel {
|
||||||
pub tm_rx: mpsc::Receiver<PacketInPool>,
|
pub tm_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
|
||||||
pub tm_store: SharedStaticMemoryPool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UdpTmHandler for StaticUdpTmHandler {
|
impl UdpTmHandlerProvider for UdpTmHandlerWithChannel {
|
||||||
fn send_tm_to_udp_client(&mut self, socket: &UdpSocket, &recv_addr: &SocketAddr) {
|
|
||||||
while let Ok(pus_tm_in_pool) = self.tm_rx.try_recv() {
|
|
||||||
let store_lock = self.tm_store.write();
|
|
||||||
if store_lock.is_err() {
|
|
||||||
warn!("Locking TM store failed");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let mut store_lock = store_lock.unwrap();
|
|
||||||
let pg = store_lock.read_with_guard(pus_tm_in_pool.store_addr);
|
|
||||||
let read_res = pg.read_as_vec();
|
|
||||||
if read_res.is_err() {
|
|
||||||
warn!("Error reading TM pool data");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let buf = read_res.unwrap();
|
|
||||||
let result = socket.send_to(&buf, recv_addr);
|
|
||||||
if let Err(e) = result {
|
|
||||||
warn!("Sending TM with UDP socket failed: {e}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct DynamicUdpTmHandler {
|
|
||||||
pub tm_rx: mpsc::Receiver<PacketAsVec>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UdpTmHandler for DynamicUdpTmHandler {
|
|
||||||
fn send_tm_to_udp_client(&mut self, socket: &UdpSocket, recv_addr: &SocketAddr) {
|
fn send_tm_to_udp_client(&mut self, socket: &UdpSocket, recv_addr: &SocketAddr) {
|
||||||
while let Ok(tm) = self.tm_rx.try_recv() {
|
while let Ok(tm) = self.tm_rx.try_recv() {
|
||||||
if tm.packet.len() > 9 {
|
log::debug!("sending TM from sender {:?}", tm.tm_header.sender_id);
|
||||||
let service = tm.packet[7];
|
let result = socket.send_to(&tm.to_vec(), recv_addr);
|
||||||
let subservice = tm.packet[8];
|
|
||||||
info!("Sending PUS TM[{service},{subservice}]")
|
|
||||||
} else {
|
|
||||||
info!("Sending PUS TM");
|
|
||||||
}
|
|
||||||
let result = socket.send_to(&tm.packet, recv_addr);
|
|
||||||
if let Err(e) = result {
|
if let Err(e) = result {
|
||||||
warn!("Sending TM with UDP socket failed: {e}")
|
warn!("Sending TM with UDP socket failed: {e}")
|
||||||
}
|
}
|
||||||
@@ -68,12 +31,49 @@ impl UdpTmHandler for DynamicUdpTmHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct UdpTmtcServer<TmHandler: UdpTmHandler> {
|
#[derive(Default, Debug, Clone)]
|
||||||
pub udp_tc_server: UdpTcServer<TmTcSender, GenericSendError>,
|
pub struct TestTmHandler {
|
||||||
pub tm_handler: TmHandler,
|
addrs_to_send_to: Arc<Mutex<VecDeque<SocketAddr>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<TmHandler: UdpTmHandler> UdpTmtcServer<TmHandler> {
|
impl UdpTmHandlerProvider for TestTmHandler {
|
||||||
|
fn send_tm_to_udp_client(&mut self, _socket: &UdpSocket, recv_addr: &SocketAddr) {
|
||||||
|
self.addrs_to_send_to.lock().unwrap().push_back(*recv_addr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum UdpTmHandler {
|
||||||
|
Normal(UdpTmHandlerWithChannel),
|
||||||
|
Test(TestTmHandler),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<UdpTmHandlerWithChannel> for UdpTmHandler {
|
||||||
|
fn from(handler: UdpTmHandlerWithChannel) -> Self {
|
||||||
|
UdpTmHandler::Normal(handler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TestTmHandler> for UdpTmHandler {
|
||||||
|
fn from(handler: TestTmHandler) -> Self {
|
||||||
|
UdpTmHandler::Test(handler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UdpTmHandlerProvider for UdpTmHandler {
|
||||||
|
fn send_tm_to_udp_client(&mut self, socket: &UdpSocket, recv_addr: &SocketAddr) {
|
||||||
|
match self {
|
||||||
|
UdpTmHandler::Normal(handler) => handler.send_tm_to_udp_client(socket, recv_addr),
|
||||||
|
UdpTmHandler::Test(handler) => handler.send_tm_to_udp_client(socket, recv_addr),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct UdpTmtcServer {
|
||||||
|
pub udp_tc_server: UdpTcServer<TmTcSender, GenericSendError>,
|
||||||
|
pub tm_handler: UdpTmHandler,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UdpTmtcServer {
|
||||||
pub fn periodic_operation(&mut self) {
|
pub fn periodic_operation(&mut self) {
|
||||||
loop {
|
loop {
|
||||||
if self.poll_tc_server() == HandlingStatus::Empty {
|
if self.poll_tc_server() == HandlingStatus::Empty {
|
||||||
@@ -107,43 +107,28 @@ impl<TmHandler: UdpTmHandler> UdpTmtcServer<TmHandler> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use std::net::IpAddr;
|
||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
use std::{
|
|
||||||
collections::VecDeque,
|
|
||||||
net::IpAddr,
|
|
||||||
sync::{Arc, Mutex},
|
|
||||||
};
|
|
||||||
|
|
||||||
use arbitrary_int::traits::Integer as _;
|
use arbitrary_int::traits::Integer as _;
|
||||||
use arbitrary_int::u14;
|
use arbitrary_int::u14;
|
||||||
|
use models::Apid;
|
||||||
use satrs::spacepackets::ecss::{CreatorConfig, MessageTypeId};
|
use satrs::spacepackets::ecss::{CreatorConfig, MessageTypeId};
|
||||||
use satrs::{
|
use satrs::{
|
||||||
spacepackets::{
|
|
||||||
ecss::{tc::PusTcCreator, WritablePusPacket},
|
|
||||||
SpHeader,
|
|
||||||
},
|
|
||||||
ComponentId,
|
ComponentId,
|
||||||
|
spacepackets::{
|
||||||
|
SpHeader,
|
||||||
|
ecss::{WritablePusPacket, tc::PusTcCreator},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use satrs_example::config::OBSW_SERVER_ADDR;
|
use satrs_example::config::OBSW_SERVER_ADDR;
|
||||||
use satrs_example::ids;
|
|
||||||
|
|
||||||
use crate::tmtc::sender::{MockSender, TmTcSender};
|
use crate::tmtc::sender::MockSender;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
const UDP_SERVER_ID: ComponentId = 0x05;
|
const UDP_SERVER_ID: ComponentId = 0x05;
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone)]
|
|
||||||
pub struct TestTmHandler {
|
|
||||||
addrs_to_send_to: Arc<Mutex<VecDeque<SocketAddr>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UdpTmHandler for TestTmHandler {
|
|
||||||
fn send_tm_to_udp_client(&mut self, _socket: &UdpSocket, recv_addr: &SocketAddr) {
|
|
||||||
self.addrs_to_send_to.lock().unwrap().push_back(*recv_addr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_basic() {
|
fn test_basic() {
|
||||||
let sock_addr = SocketAddr::new(IpAddr::V4(OBSW_SERVER_ADDR), 0);
|
let sock_addr = SocketAddr::new(IpAddr::V4(OBSW_SERVER_ADDR), 0);
|
||||||
@@ -154,7 +139,7 @@ mod tests {
|
|||||||
let tm_handler_calls = tm_handler.addrs_to_send_to.clone();
|
let tm_handler_calls = tm_handler.addrs_to_send_to.clone();
|
||||||
let mut udp_dyn_server = UdpTmtcServer {
|
let mut udp_dyn_server = UdpTmtcServer {
|
||||||
udp_tc_server,
|
udp_tc_server,
|
||||||
tm_handler,
|
tm_handler: tm_handler.into(),
|
||||||
};
|
};
|
||||||
udp_dyn_server.periodic_operation();
|
udp_dyn_server.periodic_operation();
|
||||||
let queue = udp_dyn_server
|
let queue = udp_dyn_server
|
||||||
@@ -179,9 +164,9 @@ mod tests {
|
|||||||
let tm_handler_calls = tm_handler.addrs_to_send_to.clone();
|
let tm_handler_calls = tm_handler.addrs_to_send_to.clone();
|
||||||
let mut udp_dyn_server = UdpTmtcServer {
|
let mut udp_dyn_server = UdpTmtcServer {
|
||||||
udp_tc_server,
|
udp_tc_server,
|
||||||
tm_handler,
|
tm_handler: tm_handler.into(),
|
||||||
};
|
};
|
||||||
let sph = SpHeader::new_for_unseg_tc(ids::Apid::GenericPus.raw_value(), u14::ZERO, 0);
|
let sph = SpHeader::new_for_unseg_tc(Apid::Tmtc.raw_value(), u14::ZERO, 0);
|
||||||
let ping_tc = PusTcCreator::new_simple(
|
let ping_tc = PusTcCreator::new_simple(
|
||||||
sph,
|
sph,
|
||||||
MessageTypeId::new(17, 1),
|
MessageTypeId::new(17, 1),
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
use std::sync::mpsc::{self};
|
|
||||||
|
|
||||||
use crate::pus::create_verification_reporter;
|
|
||||||
use arbitrary_int::traits::Integer as _;
|
use arbitrary_int::traits::Integer as _;
|
||||||
use arbitrary_int::u11;
|
use arbitrary_int::u11;
|
||||||
use satrs::event_man_legacy::{EventMessageU32, EventRoutingError};
|
use satrs::event_man_legacy::{EventMessageU32, EventRoutingError};
|
||||||
@@ -34,6 +31,7 @@ impl EventTmHook for EventApidSetter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
/// The PUS event handler subscribes for all events and converts them into ECSS PUS 5 event
|
/// The PUS event handler subscribes for all events and converts them into ECSS PUS 5 event
|
||||||
/// packets. It also handles the verification completion of PUS event service requests.
|
/// packets. It also handles the verification completion of PUS event service requests.
|
||||||
pub struct PusEventHandler<TmSender: EcssTmSender> {
|
pub struct PusEventHandler<TmSender: EcssTmSender> {
|
||||||
@@ -292,3 +290,4 @@ mod tests {
|
|||||||
// TODO: Add test.
|
// TODO: Add test.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
@@ -39,6 +39,7 @@ pub fn create_verification_reporter(owner_id: ComponentId, apid: Apid) -> Verifi
|
|||||||
VerificationReporter::new(owner_id, &verif_cfg)
|
VerificationReporter::new(owner_id, &verif_cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
/// Simple router structure which forwards PUS telecommands to dedicated handlers.
|
/// Simple router structure which forwards PUS telecommands to dedicated handlers.
|
||||||
pub struct PusTcMpscRouter {
|
pub struct PusTcMpscRouter {
|
||||||
pub test_tc_sender: mpsc::SyncSender<EcssTcAndToken>,
|
pub test_tc_sender: mpsc::SyncSender<EcssTcAndToken>,
|
||||||
@@ -187,6 +188,7 @@ impl PusTcDistributor {
|
|||||||
Ok(HandlingStatus::HandledOne)
|
Ok(HandlingStatus::HandledOne)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
pub trait TargetedPusService {
|
pub trait TargetedPusService {
|
||||||
const SERVICE_ID: u8;
|
const SERVICE_ID: u8;
|
||||||
@@ -5,7 +5,7 @@ use crate::pus::create_verification_reporter;
|
|||||||
use crate::tmtc::sender::TmTcSender;
|
use crate::tmtc::sender::TmTcSender;
|
||||||
use log::info;
|
use log::info;
|
||||||
use satrs::pool::{PoolProvider, StaticMemoryPool};
|
use satrs::pool::{PoolProvider, StaticMemoryPool};
|
||||||
use satrs::pus::scheduler::{PusScheduler, TcInfo};
|
use satrs::pus::scheduler::{PusSchedulerAlloc, TcInfo};
|
||||||
use satrs::pus::scheduler_srv::PusSchedServiceHandler;
|
use satrs::pus::scheduler_srv::PusSchedServiceHandler;
|
||||||
use satrs::pus::verification::VerificationReporter;
|
use satrs::pus::verification::VerificationReporter;
|
||||||
use satrs::pus::{
|
use satrs::pus::{
|
||||||
@@ -86,7 +86,7 @@ pub struct SchedulingServiceWrapper {
|
|||||||
TmTcSender,
|
TmTcSender,
|
||||||
EcssTcCacher,
|
EcssTcCacher,
|
||||||
VerificationReporter,
|
VerificationReporter,
|
||||||
PusScheduler,
|
PusSchedulerAlloc,
|
||||||
>,
|
>,
|
||||||
pub sched_tc_pool: StaticMemoryPool,
|
pub sched_tc_pool: StaticMemoryPool,
|
||||||
pub releaser_buf: [u8; 4096],
|
pub releaser_buf: [u8; 4096],
|
||||||
@@ -179,7 +179,7 @@ pub fn create_scheduler_service(
|
|||||||
pus_sched_rx: mpsc::Receiver<EcssTcAndToken>,
|
pus_sched_rx: mpsc::Receiver<EcssTcAndToken>,
|
||||||
sched_tc_pool: StaticMemoryPool,
|
sched_tc_pool: StaticMemoryPool,
|
||||||
) -> SchedulingServiceWrapper {
|
) -> SchedulingServiceWrapper {
|
||||||
let scheduler = PusScheduler::new_with_current_init_time(Duration::from_secs(5))
|
let scheduler = PusSchedulerAlloc::new_with_current_init_time(Duration::from_secs(5))
|
||||||
.expect("Creating PUS Scheduler failed");
|
.expect("Creating PUS Scheduler failed");
|
||||||
let pus_11_handler = PusSchedServiceHandler::new(
|
let pus_11_handler = PusSchedServiceHandler::new(
|
||||||
PusServiceHelper::new(
|
PusServiceHelper::new(
|
||||||
@@ -15,8 +15,8 @@ use satrs::spacepackets::ecss::tc::PusTcReader;
|
|||||||
use satrs::spacepackets::ecss::PusPacket;
|
use satrs::spacepackets::ecss::PusPacket;
|
||||||
use satrs::ComponentId;
|
use satrs::ComponentId;
|
||||||
use satrs_example::config::tmtc_err;
|
use satrs_example::config::tmtc_err;
|
||||||
use satrs_example::ids;
|
|
||||||
|
|
||||||
|
/*
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum CompositeRequest {
|
pub enum CompositeRequest {
|
||||||
@@ -153,3 +153,4 @@ impl PusRequestRouter<ModeRequest> for GenericRequestRouter {
|
|||||||
Err(GenericRoutingError::UnknownTargetId(target_id))
|
Err(GenericRoutingError::UnknownTargetId(target_id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
+124
-7
@@ -1,13 +1,28 @@
|
|||||||
use satrs::spacepackets::time::cds::CdsTime;
|
extern crate alloc;
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
sync::mpsc,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub use models::ComponentId;
|
||||||
|
use models::ccsds::{CcsdsTcPacketOwned, CcsdsTmPacketOwned};
|
||||||
|
use satrs::spacepackets::{CcsdsPacketIdAndPsc, time::cds::CdsTime};
|
||||||
|
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod ids;
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
/// Simple type modelling packet stored in the heap. This structure is intended to
|
||||||
pub enum DeviceMode {
|
/// be used when sending a packet via a message queue, so it also contains the sender ID.
|
||||||
Off = 0,
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
On = 1,
|
pub struct PacketAsVec {
|
||||||
Normal = 2,
|
pub sender_id: ComponentId,
|
||||||
|
pub packet: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PacketAsVec {
|
||||||
|
pub fn new(sender_id: ComponentId, packet: Vec<u8>) -> Self {
|
||||||
|
Self { sender_id, packet }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TimestampHelper {
|
pub struct TimestampHelper {
|
||||||
@@ -38,3 +53,105 @@ impl Default for TimestampHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Helper structure for periodic HK generation of a single set.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct HkHelperSingleSet {
|
||||||
|
pub enabled: bool,
|
||||||
|
pub frequency: Duration,
|
||||||
|
pub last_generated: Option<Instant>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HkHelperSingleSet {
|
||||||
|
#[inline]
|
||||||
|
pub const fn new(enabled: bool, init_frequency: Duration) -> Self {
|
||||||
|
Self {
|
||||||
|
enabled,
|
||||||
|
frequency: init_frequency,
|
||||||
|
last_generated: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub const fn enabled(&self) -> bool {
|
||||||
|
self.enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether a new HK packet needs to be generated.
|
||||||
|
pub fn needs_generation(&mut self) -> bool {
|
||||||
|
if !self.enabled {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if self.last_generated.is_none() {
|
||||||
|
self.last_generated = Some(Instant::now());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let last_generated = self.last_generated.unwrap();
|
||||||
|
if Instant::now() - last_generated >= self.frequency {
|
||||||
|
self.last_generated = Some(Instant::now());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TmtcQueues {
|
||||||
|
pub tc_rx: mpsc::Receiver<CcsdsTcPacketOwned>,
|
||||||
|
pub tm_tx: mpsc::SyncSender<CcsdsTmPacketOwned>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ModeHelper<Mode, TransitionState> {
|
||||||
|
pub current: Mode,
|
||||||
|
pub target: Option<Mode>,
|
||||||
|
pub tc_commander: Option<CcsdsPacketIdAndPsc>,
|
||||||
|
pub transition_start: Option<Instant>,
|
||||||
|
pub timeout: Duration,
|
||||||
|
pub transition_state: TransitionState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Mode: Copy + Clone, TransitionState: Default> ModeHelper<Mode, TransitionState> {
|
||||||
|
pub fn new(init_mode: Mode, timeout: Duration) -> Self {
|
||||||
|
Self {
|
||||||
|
current: init_mode,
|
||||||
|
target: Default::default(),
|
||||||
|
tc_commander: Default::default(),
|
||||||
|
transition_start: None,
|
||||||
|
timeout,
|
||||||
|
transition_state: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start(&mut self, target: Mode) {
|
||||||
|
self.target = Some(target);
|
||||||
|
self.transition_start = Some(Instant::now());
|
||||||
|
self.transition_state = TransitionState::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn transition_active(&self) -> bool {
|
||||||
|
self.target.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn timed_out(&self) -> bool {
|
||||||
|
if self.target.is_none() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if let Some(transition_start) = self.transition_start {
|
||||||
|
return Instant::now() - transition_start >= self.timeout;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn finish(&mut self, success: bool) -> Option<CcsdsPacketIdAndPsc> {
|
||||||
|
self.target?;
|
||||||
|
if success {
|
||||||
|
self.current = self.target.take().unwrap();
|
||||||
|
} else {
|
||||||
|
self.target = None;
|
||||||
|
}
|
||||||
|
self.transition_state = Default::default();
|
||||||
|
self.transition_start = None;
|
||||||
|
self.tc_commander.take()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
|
use std::str::FromStr as _;
|
||||||
|
|
||||||
pub fn setup_logger() -> Result<(), fern::InitError> {
|
pub fn setup_logger() -> Result<(), fern::InitError> {
|
||||||
|
// Read the RUST_LOG environment variable and parse it.
|
||||||
|
// If the variable is not set or is invalid, default to Debug.
|
||||||
|
let mut log_level = log::LevelFilter::Info;
|
||||||
|
if let Ok(log_level_str) = std::env::var("RUST_LOG") {
|
||||||
|
log_level = log::LevelFilter::from_str(&log_level_str).unwrap_or(log::LevelFilter::Info);
|
||||||
|
}
|
||||||
|
|
||||||
fern::Dispatch::new()
|
fern::Dispatch::new()
|
||||||
.format(|out, message, record| {
|
.format(|out, message, record| {
|
||||||
out.finish(format_args!(
|
out.finish(format_args!(
|
||||||
@@ -9,7 +18,7 @@ pub fn setup_logger() -> Result<(), fern::InitError> {
|
|||||||
message
|
message
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
.level(log::LevelFilter::Debug)
|
.level(log_level)
|
||||||
.chain(std::io::stdout())
|
.chain(std::io::stdout())
|
||||||
.chain(fern::log_file("output.log")?)
|
.chain(fern::log_file("output.log")?)
|
||||||
.apply()?;
|
.apply()?;
|
||||||
|
|||||||
+185
-306
@@ -1,16 +1,18 @@
|
|||||||
use std::{
|
use std::{
|
||||||
net::{IpAddr, SocketAddr},
|
net::{IpAddr, SocketAddr},
|
||||||
sync::{mpsc, Arc, Mutex},
|
sync::{
|
||||||
|
Arc, Mutex,
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
mpsc,
|
||||||
|
},
|
||||||
thread,
|
thread,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use acs::mgm::{MgmHandlerLis3Mdl, SpiDummyInterface, SpiSimInterface, SpiSimInterfaceWrapper};
|
|
||||||
use eps::{
|
use eps::{
|
||||||
pcdu::{PcduHandler, SerialInterfaceDummy, SerialInterfaceToSim, SerialSimInterfaceWrapper},
|
|
||||||
PowerSwitchHelper,
|
PowerSwitchHelper,
|
||||||
|
pcdu::{PcduHandler, SerialInterfaceDummy, SerialInterfaceToSim, SerialSimInterfaceWrapper},
|
||||||
};
|
};
|
||||||
use events::EventHandler;
|
|
||||||
use interface::{
|
use interface::{
|
||||||
sim_client_udp::create_sim_client,
|
sim_client_udp::create_sim_client,
|
||||||
tcp::{SyncTcpTmSource, TcpTask},
|
tcp::{SyncTcpTmSource, TcpTask},
|
||||||
@@ -18,256 +20,117 @@ use interface::{
|
|||||||
};
|
};
|
||||||
use log::info;
|
use log::info;
|
||||||
use logger::setup_logger;
|
use logger::setup_logger;
|
||||||
use pus::{
|
use models::{ComponentId, DeviceMode};
|
||||||
action::create_action_service,
|
|
||||||
event::create_event_service,
|
|
||||||
hk::create_hk_service,
|
|
||||||
mode::create_mode_service,
|
|
||||||
scheduler::{create_scheduler_service, TcReleaser},
|
|
||||||
stack::PusStack,
|
|
||||||
test::create_test_service,
|
|
||||||
PusTcDistributor, PusTcMpscRouter,
|
|
||||||
};
|
|
||||||
use requests::GenericRequestRouter;
|
|
||||||
use satrs::{
|
use satrs::{
|
||||||
hal::std::{tcp_server::ServerConfig, udp_server::UdpTcServer},
|
hal::std::{tcp_server::ServerConfig, udp_server::UdpTcServer},
|
||||||
mode::{Mode, ModeAndSubmode, ModeRequest, ModeRequestHandlerMpscBounded},
|
mode::{Mode, ModeAndSubmode, ModeRequest},
|
||||||
mode_tree::connect_mode_nodes,
|
pus::HandlingStatus,
|
||||||
pus::{event_man::EventRequestWithToken, EcssTcCacher, HandlingStatus},
|
|
||||||
request::{GenericMessage, MessageMetadata},
|
request::{GenericMessage, MessageMetadata},
|
||||||
spacepackets::time::cds::CdsTime,
|
spacepackets::time::cds::CdsTime,
|
||||||
};
|
};
|
||||||
use satrs_example::{
|
use satrs_example::{
|
||||||
|
TmtcQueues,
|
||||||
config::{
|
config::{
|
||||||
components::NO_SENDER,
|
|
||||||
pool::create_sched_tc_pool,
|
|
||||||
tasks::{FREQ_MS_AOCS, FREQ_MS_PUS_STACK, FREQ_MS_UDP_TMTC, SIM_CLIENT_IDLE_DELAY_MS},
|
|
||||||
OBSW_SERVER_ADDR, PACKET_ID_VALIDATOR, SERVER_PORT,
|
OBSW_SERVER_ADDR, PACKET_ID_VALIDATOR, SERVER_PORT,
|
||||||
|
components::NO_SENDER,
|
||||||
|
tasks::{FREQ_MS_AOCS, FREQ_MS_CONTROLLER, FREQ_MS_UDP_TMTC, SIM_CLIENT_IDLE_DELAY_MS},
|
||||||
},
|
},
|
||||||
ids::{
|
|
||||||
acs::*,
|
|
||||||
eps::*,
|
|
||||||
tmtc::{TCP_SERVER, UDP_SERVER},
|
|
||||||
},
|
|
||||||
DeviceMode,
|
|
||||||
};
|
};
|
||||||
use tmtc::sender::TmTcSender;
|
use tmtc::sender::TmTcSender;
|
||||||
use tmtc::{tc_source::TcSourceTask, tm_sink::TmSink};
|
use tmtc::{tc_source::TcSourceTask, tm_sink::TmSink};
|
||||||
|
|
||||||
cfg_if::cfg_if! {
|
use crate::{
|
||||||
if #[cfg(feature = "heap_tmtc")] {
|
acs::{mgm, mgm_assembly},
|
||||||
use interface::udp::DynamicUdpTmHandler;
|
control::Controller,
|
||||||
use satrs::pus::EcssTcVecCacher;
|
eps::pcdu::SwitchSet,
|
||||||
use tmtc::{tc_source::TcSourceTaskDynamic, tm_sink::TmSinkDynamic};
|
event_manager::EventManager,
|
||||||
} else {
|
interface::udp::UdpTmHandlerWithChannel,
|
||||||
use std::sync::RwLock;
|
tmtc::tc_source::CcsdsDistributor,
|
||||||
use interface::udp::StaticUdpTmHandler;
|
};
|
||||||
use satrs::pus::EcssTcInSharedPoolCacher;
|
|
||||||
use satrs::tmtc::{PacketSenderWithSharedPool, SharedPacketPool};
|
|
||||||
use satrs_example::config::pool::create_static_pools;
|
|
||||||
use tmtc::{
|
|
||||||
tc_source::TcSourceTaskStatic,
|
|
||||||
tm_sink::TmSinkStatic,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod acs;
|
mod acs;
|
||||||
|
mod ccsds;
|
||||||
|
mod control;
|
||||||
mod eps;
|
mod eps;
|
||||||
mod events;
|
mod event_manager;
|
||||||
mod hk;
|
|
||||||
mod interface;
|
mod interface;
|
||||||
mod logger;
|
mod logger;
|
||||||
mod pus;
|
|
||||||
mod requests;
|
|
||||||
mod spi;
|
|
||||||
mod tmtc;
|
mod tmtc;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
setup_logger().expect("setting up logging with fern failed");
|
static KILL_SIGNAL: AtomicBool = AtomicBool::new(false);
|
||||||
println!("Running OBSW example");
|
|
||||||
|
|
||||||
cfg_if::cfg_if! {
|
setup_logger().expect("setting up logging with fern failed");
|
||||||
if #[cfg(not(feature = "heap_tmtc"))] {
|
println!("Runng OBSW example");
|
||||||
let (tm_pool, tc_pool) = create_static_pools();
|
ctrlc::set_handler(move || {
|
||||||
let shared_tm_pool = Arc::new(RwLock::new(tm_pool));
|
log::info!("Received Ctrl-C, shutting down");
|
||||||
let shared_tc_pool = Arc::new(RwLock::new(tc_pool));
|
KILL_SIGNAL.store(true, Ordering::Relaxed);
|
||||||
let shared_tm_pool_wrapper = SharedPacketPool::new(&shared_tm_pool);
|
})
|
||||||
let shared_tc_pool_wrapper = SharedPacketPool::new(&shared_tc_pool);
|
.expect("Error setting Ctrl-C handler");
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let (tc_source_tx, tc_source_rx) = mpsc::sync_channel(50);
|
let (tc_source_tx, tc_source_rx) = mpsc::sync_channel(50);
|
||||||
let (tm_sink_tx, tm_sink_rx) = mpsc::sync_channel(50);
|
let (tm_sink_tx, tm_sink_rx) = mpsc::sync_channel(50);
|
||||||
let (tm_server_tx, tm_server_rx) = mpsc::sync_channel(50);
|
let (tm_server_tx, tm_server_rx) = mpsc::sync_channel(50);
|
||||||
|
|
||||||
cfg_if::cfg_if! {
|
|
||||||
if #[cfg(not(feature = "heap_tmtc"))] {
|
|
||||||
let tm_sender = TmTcSender::Static(
|
|
||||||
PacketSenderWithSharedPool::new(tm_sink_tx.clone(), shared_tm_pool_wrapper.clone())
|
|
||||||
);
|
|
||||||
} else if #[cfg(feature = "heap_tmtc")] {
|
|
||||||
let tm_sender = TmTcSender::Heap(tm_sink_tx.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let (sim_request_tx, sim_request_rx) = mpsc::channel();
|
let (sim_request_tx, sim_request_rx) = mpsc::channel();
|
||||||
let (mgm_0_sim_reply_tx, mgm_0_sim_reply_rx) = mpsc::channel();
|
let (mgm_0_sim_reply_tx, mgm_0_sim_reply_rx) = mpsc::channel();
|
||||||
let (mgm_1_sim_reply_tx, mgm_1_sim_reply_rx) = mpsc::channel();
|
let (mgm_1_sim_reply_tx, mgm_1_sim_reply_rx) = mpsc::channel();
|
||||||
let (pcdu_sim_reply_tx, pcdu_sim_reply_rx) = mpsc::channel();
|
let (pcdu_sim_reply_tx, pcdu_sim_reply_rx) = mpsc::channel();
|
||||||
let mut opt_sim_client = create_sim_client(sim_request_rx);
|
let mut opt_sim_client = create_sim_client(sim_request_rx);
|
||||||
|
|
||||||
let (mgm_0_handler_composite_tx, mgm_0_handler_composite_rx) = mpsc::sync_channel(10);
|
let (mgm_0_handler_tc_tx, mgm_0_handler_tc_rx) = mpsc::sync_channel(10);
|
||||||
let (mgm_1_handler_composite_tx, mgm_1_handler_composite_rx) = mpsc::sync_channel(10);
|
let (mgm_1_handler_tc_tx, mgm_1_handler_tc_rx) = mpsc::sync_channel(10);
|
||||||
let (pcdu_handler_composite_tx, pcdu_handler_composite_rx) = mpsc::sync_channel(30);
|
let (mgm_assembly_tc_tx, mgm_assembly_tc_rx) = mpsc::sync_channel(10);
|
||||||
let (mgm_0_handler_mode_tx, mgm_0_handler_mode_rx) = mpsc::sync_channel(5);
|
let (pcdu_handler_tc_tx, pcdu_handler_tc_rx) = mpsc::sync_channel(30);
|
||||||
let (mgm_1_handler_mode_tx, mgm_1_handler_mode_rx) = mpsc::sync_channel(5);
|
let (controller_tc_tx, controller_tc_rx) = mpsc::sync_channel(10);
|
||||||
let (pcdu_handler_mode_tx, pcdu_handler_mode_rx) = mpsc::sync_channel(5);
|
|
||||||
|
|
||||||
// Some request are targetable. This map is used to retrieve sender handles based on a target ID.
|
// These message handles need to go into the MGM assembly and ACS subsystem.
|
||||||
let mut request_map = GenericRequestRouter::default();
|
let (_mgm_assembly_request_tx, mgm_assembly_request_rx) = mpsc::sync_channel(5);
|
||||||
request_map
|
let (mgm_assembly_report_tx, _mgm_assembly_report_rx) = mpsc::sync_channel(5);
|
||||||
.composite_router_map
|
|
||||||
.insert(MGM0.id(), mgm_0_handler_composite_tx);
|
|
||||||
request_map
|
|
||||||
.composite_router_map
|
|
||||||
.insert(MGM1.id(), mgm_1_handler_composite_tx);
|
|
||||||
request_map
|
|
||||||
.composite_router_map
|
|
||||||
.insert(PCDU.id(), pcdu_handler_composite_tx);
|
|
||||||
|
|
||||||
// This helper structure is used by all telecommand providers which need to send telecommands
|
// These message handles need to go into the MGM assembly and MGM devices.
|
||||||
// to the TC source.
|
let (mgm_0_mode_request_tx, mgm_0_mode_request_rx) = mpsc::sync_channel(5);
|
||||||
cfg_if::cfg_if! {
|
let (mgm_1_mode_request_tx, mgm_1_mode_request_rx) = mpsc::sync_channel(5);
|
||||||
if #[cfg(not(feature = "heap_tmtc"))] {
|
let (mgm_0_mode_report_tx, mgm_0_mode_report_rx) = mpsc::sync_channel(5);
|
||||||
let tc_sender_with_shared_pool =
|
let (mgm_1_mode_report_tx, mgm_1_mode_report_rx) = mpsc::sync_channel(5);
|
||||||
PacketSenderWithSharedPool::new(tc_source_tx, shared_tc_pool_wrapper.clone());
|
|
||||||
let tc_in_mem_converter =
|
|
||||||
EcssTcCacher::Static(EcssTcInSharedPoolCacher::new(shared_tc_pool, 4096));
|
|
||||||
} else if #[cfg(feature = "heap_tmtc")] {
|
|
||||||
let tc_in_mem_converter = EcssTcCacher::Heap(EcssTcVecCacher::default());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create event handling components
|
let (pcdu_handler_mode_tx, _pcdu_handler_mode_rx) = mpsc::sync_channel(5);
|
||||||
// These sender handles are used to send event requests, for example to enable or disable
|
|
||||||
// certain events.
|
|
||||||
let (event_tx, event_rx) = mpsc::sync_channel(100);
|
|
||||||
let (event_request_tx, event_request_rx) = mpsc::channel::<EventRequestWithToken>();
|
|
||||||
|
|
||||||
// The event task is the core handler to perform the event routing and TM handling as specified
|
let (event_ctrl_tx, event_ctrl_rx) = mpsc::sync_channel(10);
|
||||||
// in the sat-rs documentation.
|
let mut event_manager = EventManager {
|
||||||
let mut event_handler = EventHandler::new(tm_sink_tx.clone(), event_rx, event_request_rx);
|
ctrl_rx: event_ctrl_rx,
|
||||||
|
tm_tx: tm_sink_tx.clone(),
|
||||||
let (pus_test_tx, pus_test_rx) = mpsc::sync_channel(20);
|
|
||||||
let (pus_event_tx, pus_event_rx) = mpsc::sync_channel(10);
|
|
||||||
let (pus_sched_tx, pus_sched_rx) = mpsc::sync_channel(50);
|
|
||||||
let (pus_hk_tx, pus_hk_rx) = mpsc::sync_channel(50);
|
|
||||||
let (pus_action_tx, pus_action_rx) = mpsc::sync_channel(50);
|
|
||||||
let (pus_mode_tx, pus_mode_rx) = mpsc::sync_channel(50);
|
|
||||||
|
|
||||||
let (_pus_action_reply_tx, pus_action_reply_rx) = mpsc::channel();
|
|
||||||
let (pus_hk_reply_tx, pus_hk_reply_rx) = mpsc::sync_channel(50);
|
|
||||||
let (pus_mode_reply_tx, pus_mode_reply_rx) = mpsc::sync_channel(30);
|
|
||||||
|
|
||||||
cfg_if::cfg_if! {
|
|
||||||
if #[cfg(not(feature = "heap_tmtc"))] {
|
|
||||||
let tc_releaser = TcReleaser::Static(tc_sender_with_shared_pool.clone());
|
|
||||||
} else if #[cfg(feature = "heap_tmtc")] {
|
|
||||||
let tc_releaser = TcReleaser::Heap(tc_source_tx.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let pus_router = PusTcMpscRouter {
|
|
||||||
test_tc_sender: pus_test_tx,
|
|
||||||
event_tc_sender: pus_event_tx,
|
|
||||||
sched_tc_sender: pus_sched_tx,
|
|
||||||
hk_tc_sender: pus_hk_tx,
|
|
||||||
action_tc_sender: pus_action_tx,
|
|
||||||
mode_tc_sender: pus_mode_tx,
|
|
||||||
};
|
};
|
||||||
let pus_test_service = create_test_service(
|
|
||||||
tm_sender.clone(),
|
|
||||||
tc_in_mem_converter.clone(),
|
|
||||||
event_tx.clone(),
|
|
||||||
pus_test_rx,
|
|
||||||
);
|
|
||||||
let pus_scheduler_service = create_scheduler_service(
|
|
||||||
tm_sender.clone(),
|
|
||||||
tc_in_mem_converter.clone(),
|
|
||||||
tc_releaser,
|
|
||||||
pus_sched_rx,
|
|
||||||
create_sched_tc_pool(),
|
|
||||||
);
|
|
||||||
let pus_event_service = create_event_service(
|
|
||||||
tm_sender.clone(),
|
|
||||||
tc_in_mem_converter.clone(),
|
|
||||||
pus_event_rx,
|
|
||||||
event_request_tx,
|
|
||||||
);
|
|
||||||
let pus_action_service = create_action_service(
|
|
||||||
tm_sender.clone(),
|
|
||||||
tc_in_mem_converter.clone(),
|
|
||||||
pus_action_rx,
|
|
||||||
request_map.clone(),
|
|
||||||
pus_action_reply_rx,
|
|
||||||
);
|
|
||||||
let pus_hk_service = create_hk_service(
|
|
||||||
tm_sender.clone(),
|
|
||||||
tc_in_mem_converter.clone(),
|
|
||||||
pus_hk_rx,
|
|
||||||
request_map.clone(),
|
|
||||||
pus_hk_reply_rx,
|
|
||||||
);
|
|
||||||
let pus_mode_service = create_mode_service(
|
|
||||||
tm_sender.clone(),
|
|
||||||
tc_in_mem_converter.clone(),
|
|
||||||
pus_mode_rx,
|
|
||||||
request_map,
|
|
||||||
pus_mode_reply_rx,
|
|
||||||
);
|
|
||||||
let mut pus_stack = PusStack::new(
|
|
||||||
pus_test_service,
|
|
||||||
pus_hk_service,
|
|
||||||
pus_event_service,
|
|
||||||
pus_action_service,
|
|
||||||
pus_scheduler_service,
|
|
||||||
pus_mode_service,
|
|
||||||
);
|
|
||||||
|
|
||||||
cfg_if::cfg_if! {
|
let mut controller = Controller::new(controller_tc_rx, tm_sink_tx.clone(), event_ctrl_tx);
|
||||||
if #[cfg(not(feature = "heap_tmtc"))] {
|
|
||||||
let mut tmtc_task = TcSourceTask::Static(TcSourceTaskStatic::new(
|
let ccsds_distributor = CcsdsDistributor::default();
|
||||||
shared_tc_pool_wrapper.clone(),
|
let mut tc_source = TcSourceTask::new(tc_source_rx, ccsds_distributor);
|
||||||
tc_source_rx,
|
tc_source.add_target(ComponentId::EpsPcdu, pcdu_handler_tc_tx);
|
||||||
PusTcDistributor::new(tm_sender.clone(), pus_router),
|
tc_source.add_target(ComponentId::Controller, controller_tc_tx);
|
||||||
));
|
tc_source.add_target(ComponentId::AcsMgm0, mgm_0_handler_tc_tx);
|
||||||
let tc_sender = TmTcSender::Static(tc_sender_with_shared_pool);
|
tc_source.add_target(ComponentId::AcsMgm1, mgm_1_handler_tc_tx);
|
||||||
let udp_tm_handler = StaticUdpTmHandler {
|
tc_source.add_target(ComponentId::AcsMgmAssembly, mgm_assembly_tc_tx);
|
||||||
tm_rx: tm_server_rx,
|
|
||||||
tm_store: shared_tm_pool.clone(),
|
let tc_sender = TmTcSender::Normal(tc_source_tx.clone());
|
||||||
};
|
let udp_tm_handler = UdpTmHandlerWithChannel {
|
||||||
} else if #[cfg(feature = "heap_tmtc")] {
|
tm_rx: tm_server_rx,
|
||||||
let mut tmtc_task = TcSourceTask::Heap(TcSourceTaskDynamic::new(
|
};
|
||||||
tc_source_rx,
|
|
||||||
PusTcDistributor::new(tm_sender.clone(), pus_router),
|
|
||||||
));
|
|
||||||
let tc_sender = TmTcSender::Heap(tc_source_tx.clone());
|
|
||||||
let udp_tm_handler = DynamicUdpTmHandler {
|
|
||||||
tm_rx: tm_server_rx,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let sock_addr = SocketAddr::new(IpAddr::V4(OBSW_SERVER_ADDR), SERVER_PORT);
|
let sock_addr = SocketAddr::new(IpAddr::V4(OBSW_SERVER_ADDR), SERVER_PORT);
|
||||||
let udp_tc_server = UdpTcServer::new(UDP_SERVER.id(), sock_addr, 2048, tc_sender.clone())
|
let udp_tc_server = UdpTcServer::new(
|
||||||
.expect("creating UDP TMTC server failed");
|
ComponentId::UdpServer as u32,
|
||||||
|
sock_addr,
|
||||||
|
2048,
|
||||||
|
tc_sender.clone(),
|
||||||
|
)
|
||||||
|
.expect("creating UDP TMTC server failed");
|
||||||
let mut udp_tmtc_server = UdpTmtcServer {
|
let mut udp_tmtc_server = UdpTmtcServer {
|
||||||
udp_tc_server,
|
udp_tc_server,
|
||||||
tm_handler: udp_tm_handler,
|
tm_handler: udp_tm_handler.into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let tcp_server_cfg = ServerConfig::new(
|
let tcp_server_cfg = ServerConfig::new(
|
||||||
TCP_SERVER.id(),
|
ComponentId::TcpServer as u32,
|
||||||
sock_addr,
|
sock_addr,
|
||||||
Duration::from_millis(400),
|
Duration::from_millis(400),
|
||||||
4096,
|
4096,
|
||||||
@@ -282,31 +145,14 @@ fn main() {
|
|||||||
)
|
)
|
||||||
.expect("tcp server creation failed");
|
.expect("tcp server creation failed");
|
||||||
|
|
||||||
cfg_if::cfg_if! {
|
let mut tm_sink = TmSink::new(sync_tm_tcp_source, tm_sink_rx, tm_server_tx);
|
||||||
if #[cfg(not(feature = "heap_tmtc"))] {
|
|
||||||
let mut tm_sink = TmSink::Static(TmSinkStatic::new(
|
|
||||||
shared_tm_pool_wrapper,
|
|
||||||
sync_tm_tcp_source,
|
|
||||||
tm_sink_rx,
|
|
||||||
tm_server_tx,
|
|
||||||
));
|
|
||||||
} else if #[cfg(feature = "heap_tmtc")] {
|
|
||||||
let mut tm_sink = TmSink::Heap(TmSinkDynamic::new(
|
|
||||||
sync_tm_tcp_source,
|
|
||||||
tm_sink_rx,
|
|
||||||
tm_server_tx,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let shared_switch_set = Arc::new(Mutex::default());
|
let shared_switch_set = Arc::new(Mutex::new(SwitchSet::new_with_init_switches_unknown()));
|
||||||
let (switch_request_tx, switch_request_rx) = mpsc::sync_channel(20);
|
let (switch_request_tx, switch_request_rx) = mpsc::sync_channel(20);
|
||||||
let switch_helper = PowerSwitchHelper::new(switch_request_tx, shared_switch_set.clone());
|
let switch_helper = PowerSwitchHelper::new(switch_request_tx, shared_switch_set.clone());
|
||||||
|
|
||||||
let shared_mgm_0_set = Arc::default();
|
let shared_mgm_0_set = Arc::default();
|
||||||
let shared_mgm_1_set = Arc::default();
|
let shared_mgm_1_set = Arc::default();
|
||||||
let mgm_0_mode_node = ModeRequestHandlerMpscBounded::new(MGM0.into(), mgm_0_handler_mode_rx);
|
|
||||||
let mgm_1_mode_node = ModeRequestHandlerMpscBounded::new(MGM1.into(), mgm_1_handler_mode_rx);
|
|
||||||
let (mgm_0_spi_interface, mgm_1_spi_interface) =
|
let (mgm_0_spi_interface, mgm_1_spi_interface) =
|
||||||
if let Some(sim_client) = opt_sim_client.as_mut() {
|
if let Some(sim_client) = opt_sim_client.as_mut() {
|
||||||
sim_client
|
sim_client
|
||||||
@@ -314,55 +160,65 @@ fn main() {
|
|||||||
sim_client
|
sim_client
|
||||||
.add_reply_recipient(satrs_minisim::SimComponent::Mgm1Lis3Mdl, mgm_1_sim_reply_tx);
|
.add_reply_recipient(satrs_minisim::SimComponent::Mgm1Lis3Mdl, mgm_1_sim_reply_tx);
|
||||||
(
|
(
|
||||||
SpiSimInterfaceWrapper::Sim(SpiSimInterface {
|
mgm::SpiCommunication::Sim(mgm::SpiSimInterface {
|
||||||
sim_request_tx: sim_request_tx.clone(),
|
sim_request_tx: sim_request_tx.clone(),
|
||||||
sim_reply_rx: mgm_0_sim_reply_rx,
|
sim_reply_rx: mgm_0_sim_reply_rx,
|
||||||
}),
|
}),
|
||||||
SpiSimInterfaceWrapper::Sim(SpiSimInterface {
|
mgm::SpiCommunication::Sim(mgm::SpiSimInterface {
|
||||||
sim_request_tx: sim_request_tx.clone(),
|
sim_request_tx: sim_request_tx.clone(),
|
||||||
sim_reply_rx: mgm_1_sim_reply_rx,
|
sim_reply_rx: mgm_1_sim_reply_rx,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
(
|
(
|
||||||
SpiSimInterfaceWrapper::Dummy(SpiDummyInterface::default()),
|
mgm::SpiCommunication::Dummy(mgm::SpiDummyInterface::default()),
|
||||||
SpiSimInterfaceWrapper::Dummy(SpiDummyInterface::default()),
|
mgm::SpiCommunication::Dummy(mgm::SpiDummyInterface::default()),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
let mut mgm_0_handler = MgmHandlerLis3Mdl::new(
|
let mut mgm_0_handler = mgm::MgmHandlerLis3Mdl::new(
|
||||||
MGM0,
|
mgm::MgmId::_0,
|
||||||
"MGM_0",
|
TmtcQueues {
|
||||||
mgm_0_mode_node,
|
tc_rx: mgm_0_handler_tc_rx,
|
||||||
mgm_0_handler_composite_rx,
|
tm_tx: tm_sink_tx.clone(),
|
||||||
pus_hk_reply_tx.clone(),
|
},
|
||||||
switch_helper.clone(),
|
switch_helper.clone(),
|
||||||
tm_sender.clone(),
|
|
||||||
mgm_0_spi_interface,
|
mgm_0_spi_interface,
|
||||||
shared_mgm_0_set,
|
shared_mgm_0_set,
|
||||||
|
mgm::ModeLeafHelper {
|
||||||
|
request_rx: mgm_0_mode_request_rx,
|
||||||
|
report_tx: mgm_0_mode_report_tx,
|
||||||
|
},
|
||||||
|
Duration::from_millis(1000),
|
||||||
);
|
);
|
||||||
let mut mgm_1_handler = MgmHandlerLis3Mdl::new(
|
let mut mgm_1_handler = mgm::MgmHandlerLis3Mdl::new(
|
||||||
MGM1,
|
mgm::MgmId::_1,
|
||||||
"MGM_1",
|
TmtcQueues {
|
||||||
mgm_1_mode_node,
|
tc_rx: mgm_1_handler_tc_rx,
|
||||||
mgm_1_handler_composite_rx,
|
tm_tx: tm_sink_tx.clone(),
|
||||||
pus_hk_reply_tx.clone(),
|
},
|
||||||
switch_helper.clone(),
|
switch_helper.clone(),
|
||||||
tm_sender.clone(),
|
|
||||||
mgm_1_spi_interface,
|
mgm_1_spi_interface,
|
||||||
shared_mgm_1_set,
|
shared_mgm_1_set,
|
||||||
|
mgm::ModeLeafHelper {
|
||||||
|
request_rx: mgm_1_mode_request_rx,
|
||||||
|
report_tx: mgm_1_mode_report_tx,
|
||||||
|
},
|
||||||
|
Duration::from_millis(1000),
|
||||||
);
|
);
|
||||||
// Connect PUS service to device handlers.
|
let mut mgm_assembly = mgm_assembly::Assembly::new(
|
||||||
connect_mode_nodes(
|
mgm_assembly::ParentQueueHelper {
|
||||||
&mut pus_stack.mode_srv,
|
request_rx: mgm_assembly_request_rx,
|
||||||
mgm_0_handler_mode_tx,
|
report_tx: mgm_assembly_report_tx,
|
||||||
&mut mgm_0_handler,
|
},
|
||||||
pus_mode_reply_tx.clone(),
|
mgm_assembly::ChildrenQueueHelper {
|
||||||
);
|
request_tx_queues: [mgm_0_mode_request_tx, mgm_1_mode_request_tx],
|
||||||
connect_mode_nodes(
|
report_rx_queues: [mgm_0_mode_report_rx, mgm_1_mode_report_rx],
|
||||||
&mut pus_stack.mode_srv,
|
},
|
||||||
mgm_1_handler_mode_tx,
|
TmtcQueues {
|
||||||
&mut mgm_1_handler,
|
tc_rx: mgm_assembly_tc_rx,
|
||||||
pus_mode_reply_tx.clone(),
|
tm_tx: tm_sink_tx.clone(),
|
||||||
|
},
|
||||||
|
Duration::from_millis(2000),
|
||||||
);
|
);
|
||||||
|
|
||||||
let pcdu_serial_interface = if let Some(sim_client) = opt_sim_client.as_mut() {
|
let pcdu_serial_interface = if let Some(sim_client) = opt_sim_client.as_mut() {
|
||||||
@@ -374,23 +230,13 @@ fn main() {
|
|||||||
} else {
|
} else {
|
||||||
SerialSimInterfaceWrapper::Dummy(SerialInterfaceDummy::default())
|
SerialSimInterfaceWrapper::Dummy(SerialInterfaceDummy::default())
|
||||||
};
|
};
|
||||||
let pcdu_mode_node = ModeRequestHandlerMpscBounded::new(PCDU.into(), pcdu_handler_mode_rx);
|
|
||||||
let mut pcdu_handler = PcduHandler::new(
|
let mut pcdu_handler = PcduHandler::new(
|
||||||
PCDU,
|
pcdu_handler_tc_rx,
|
||||||
"PCDU",
|
tm_sink_tx.clone(),
|
||||||
pcdu_mode_node,
|
|
||||||
pcdu_handler_composite_rx,
|
|
||||||
pus_hk_reply_tx,
|
|
||||||
switch_request_rx,
|
switch_request_rx,
|
||||||
tm_sender.clone(),
|
|
||||||
pcdu_serial_interface,
|
pcdu_serial_interface,
|
||||||
shared_switch_set,
|
shared_switch_set,
|
||||||
);
|
DeviceMode::Normal,
|
||||||
connect_mode_nodes(
|
|
||||||
&mut pus_stack.mode_srv,
|
|
||||||
pcdu_handler_mode_tx.clone(),
|
|
||||||
&mut pcdu_handler,
|
|
||||||
pus_mode_reply_tx,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// The PCDU is a critical component which should be in normal mode immediately.
|
// The PCDU is a critical component which should be in normal mode immediately.
|
||||||
@@ -406,12 +252,15 @@ fn main() {
|
|||||||
|
|
||||||
info!("Starting TMTC and UDP task");
|
info!("Starting TMTC and UDP task");
|
||||||
let jh_udp_tmtc = thread::Builder::new()
|
let jh_udp_tmtc = thread::Builder::new()
|
||||||
.name("SATRS tmtc-udp".to_string())
|
.name("TMTC & UDP".to_string())
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
info!("Running UDP server on port {SERVER_PORT}");
|
info!("Running UDP server on port {SERVER_PORT}");
|
||||||
loop {
|
loop {
|
||||||
|
if KILL_SIGNAL.load(Ordering::Relaxed) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
udp_tmtc_server.periodic_operation();
|
udp_tmtc_server.periodic_operation();
|
||||||
tmtc_task.periodic_operation();
|
tc_source.periodic_operation();
|
||||||
thread::sleep(Duration::from_millis(FREQ_MS_UDP_TMTC));
|
thread::sleep(Duration::from_millis(FREQ_MS_UDP_TMTC));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -419,10 +268,13 @@ fn main() {
|
|||||||
|
|
||||||
info!("Starting TCP task");
|
info!("Starting TCP task");
|
||||||
let jh_tcp = thread::Builder::new()
|
let jh_tcp = thread::Builder::new()
|
||||||
.name("sat-rs tcp".to_string())
|
.name("TCP".to_string())
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
info!("Running TCP server on port {SERVER_PORT}");
|
info!("Running TCP server on port {SERVER_PORT}");
|
||||||
loop {
|
loop {
|
||||||
|
if KILL_SIGNAL.load(Ordering::Relaxed) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
tcp_server.periodic_operation();
|
tcp_server.periodic_operation();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -430,9 +282,14 @@ fn main() {
|
|||||||
|
|
||||||
info!("Starting TM funnel task");
|
info!("Starting TM funnel task");
|
||||||
let jh_tm_funnel = thread::Builder::new()
|
let jh_tm_funnel = thread::Builder::new()
|
||||||
.name("tm sink".to_string())
|
.name("TM SINK".to_string())
|
||||||
.spawn(move || loop {
|
.spawn(move || {
|
||||||
tm_sink.operation();
|
loop {
|
||||||
|
if KILL_SIGNAL.load(Ordering::Relaxed) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
tm_sink.operation();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -441,10 +298,15 @@ fn main() {
|
|||||||
info!("Starting UDP sim client task");
|
info!("Starting UDP sim client task");
|
||||||
opt_jh_sim_client = Some(
|
opt_jh_sim_client = Some(
|
||||||
thread::Builder::new()
|
thread::Builder::new()
|
||||||
.name("sat-rs sim adapter".to_string())
|
.name("SIM ADAPTER".to_string())
|
||||||
.spawn(move || loop {
|
.spawn(move || {
|
||||||
if sim_client.operation() == HandlingStatus::Empty {
|
loop {
|
||||||
std::thread::sleep(Duration::from_millis(SIM_CLIENT_IDLE_DELAY_MS));
|
if KILL_SIGNAL.load(Ordering::Relaxed) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if sim_client.operation() == HandlingStatus::Empty {
|
||||||
|
std::thread::sleep(Duration::from_millis(SIM_CLIENT_IDLE_DELAY_MS));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
@@ -453,38 +315,55 @@ fn main() {
|
|||||||
|
|
||||||
info!("Starting AOCS thread");
|
info!("Starting AOCS thread");
|
||||||
let jh_aocs = thread::Builder::new()
|
let jh_aocs = thread::Builder::new()
|
||||||
.name("sat-rs aocs".to_string())
|
.name("AOCS".to_string())
|
||||||
.spawn(move || loop {
|
.spawn(move || {
|
||||||
mgm_0_handler.periodic_operation();
|
loop {
|
||||||
mgm_1_handler.periodic_operation();
|
if KILL_SIGNAL.load(Ordering::Relaxed) {
|
||||||
thread::sleep(Duration::from_millis(FREQ_MS_AOCS));
|
break;
|
||||||
|
}
|
||||||
|
mgm_0_handler.periodic_operation();
|
||||||
|
mgm_1_handler.periodic_operation();
|
||||||
|
mgm_assembly.periodic_operation();
|
||||||
|
thread::sleep(Duration::from_millis(FREQ_MS_AOCS));
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
info!("Starting EPS thread");
|
info!("Starting EPS thread");
|
||||||
let jh_eps = thread::Builder::new()
|
let jh_eps = thread::Builder::new()
|
||||||
.name("sat-rs eps".to_string())
|
.name("EPS".to_string())
|
||||||
.spawn(move || loop {
|
.spawn(move || {
|
||||||
// TODO: We should introduce something like a fixed timeslot helper to allow a more
|
loop {
|
||||||
// declarative API. It would also be very useful for the AOCS task.
|
if KILL_SIGNAL.load(Ordering::Relaxed) {
|
||||||
//
|
break;
|
||||||
// TODO: The fixed timeslot handler exists.. use it.
|
}
|
||||||
pcdu_handler.periodic_operation(crate::eps::pcdu::OpCode::RegularOp);
|
// TODO: We should introduce something like a fixed timeslot helper to allow a more
|
||||||
thread::sleep(Duration::from_millis(50));
|
// declarative API. It would also be very useful for the AOCS task.
|
||||||
pcdu_handler.periodic_operation(crate::eps::pcdu::OpCode::PollAndRecvReplies);
|
//
|
||||||
thread::sleep(Duration::from_millis(50));
|
// TODO: The fixed timeslot handler exists.. use it.
|
||||||
pcdu_handler.periodic_operation(crate::eps::pcdu::OpCode::PollAndRecvReplies);
|
// TODO: Why not just use sync code in the PCDU handler, and fully delay there?
|
||||||
thread::sleep(Duration::from_millis(300));
|
pcdu_handler.periodic_operation(crate::eps::pcdu::OpCode::RegularOp);
|
||||||
|
thread::sleep(Duration::from_millis(50));
|
||||||
|
pcdu_handler.periodic_operation(crate::eps::pcdu::OpCode::PollAndRecvReplies);
|
||||||
|
thread::sleep(Duration::from_millis(50));
|
||||||
|
pcdu_handler.periodic_operation(crate::eps::pcdu::OpCode::PollAndRecvReplies);
|
||||||
|
thread::sleep(Duration::from_millis(300));
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
info!("Starting PUS handler thread");
|
info!("Starting controller thread");
|
||||||
let jh_pus_handler = thread::Builder::new()
|
let jh_controller_thread = thread::Builder::new()
|
||||||
.name("sat-rs pus".to_string())
|
.name("CTRL".to_string())
|
||||||
.spawn(move || loop {
|
.spawn(move || {
|
||||||
event_handler.periodic_operation();
|
loop {
|
||||||
pus_stack.periodic_operation();
|
if KILL_SIGNAL.load(Ordering::Relaxed) {
|
||||||
thread::sleep(Duration::from_millis(FREQ_MS_PUS_STACK));
|
break;
|
||||||
|
}
|
||||||
|
controller.periodic_operation();
|
||||||
|
event_manager.periodic_operation();
|
||||||
|
thread::sleep(Duration::from_millis(FREQ_MS_CONTROLLER));
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -504,7 +383,7 @@ fn main() {
|
|||||||
}
|
}
|
||||||
jh_aocs.join().expect("Joining AOCS thread failed");
|
jh_aocs.join().expect("Joining AOCS thread failed");
|
||||||
jh_eps.join().expect("Joining EPS thread failed");
|
jh_eps.join().expect("Joining EPS thread failed");
|
||||||
jh_pus_handler
|
jh_controller_thread
|
||||||
.join()
|
.join()
|
||||||
.expect("Joining PUS handler thread failed");
|
.expect("Joining PUS handler thread failed");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
use core::fmt::Debug;
|
|
||||||
|
|
||||||
pub trait SpiInterface {
|
|
||||||
type Error: Debug;
|
|
||||||
fn transfer(&mut self, tx: &[u8], rx: &mut [u8]) -> Result<(), Self::Error>;
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
use std::{cell::RefCell, collections::VecDeque, sync::mpsc};
|
use std::{cell::RefCell, collections::VecDeque, sync::mpsc};
|
||||||
|
|
||||||
use satrs::{
|
use satrs::{
|
||||||
pus::EcssTmSender,
|
|
||||||
queue::GenericSendError,
|
|
||||||
spacepackets::ecss::WritablePusPacket,
|
|
||||||
tmtc::{PacketAsVec, PacketHandler, PacketSenderWithSharedPool},
|
|
||||||
ComponentId,
|
ComponentId,
|
||||||
|
queue::GenericSendError,
|
||||||
|
tmtc::{PacketAsVec, PacketHandler},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone)]
|
#[derive(Default, Debug, Clone)]
|
||||||
@@ -14,8 +12,7 @@ pub struct MockSender(pub RefCell<VecDeque<PacketAsVec>>);
|
|||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum TmTcSender {
|
pub enum TmTcSender {
|
||||||
Static(PacketSenderWithSharedPool),
|
Normal(mpsc::SyncSender<PacketAsVec>),
|
||||||
Heap(mpsc::SyncSender<PacketAsVec>),
|
|
||||||
Mock(MockSender),
|
Mock(MockSender),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,37 +26,13 @@ impl TmTcSender {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EcssTmSender for TmTcSender {
|
|
||||||
fn send_tm(
|
|
||||||
&self,
|
|
||||||
sender_id: satrs::ComponentId,
|
|
||||||
tm: satrs::pus::PusTmVariant,
|
|
||||||
) -> Result<(), satrs::pus::EcssTmtcError> {
|
|
||||||
match self {
|
|
||||||
TmTcSender::Static(sync_sender) => sync_sender.send_tm(sender_id, tm),
|
|
||||||
TmTcSender::Heap(sync_sender) => match tm {
|
|
||||||
satrs::pus::PusTmVariant::InStore(_) => panic!("can not send TM in store"),
|
|
||||||
satrs::pus::PusTmVariant::Direct(pus_tm_creator) => sync_sender
|
|
||||||
.send(PacketAsVec::new(sender_id, pus_tm_creator.to_vec()?))
|
|
||||||
.map_err(|_| GenericSendError::RxDisconnected.into()),
|
|
||||||
},
|
|
||||||
TmTcSender::Mock(_) => Ok(()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PacketHandler for TmTcSender {
|
impl PacketHandler for TmTcSender {
|
||||||
type Error = GenericSendError;
|
type Error = GenericSendError;
|
||||||
|
|
||||||
fn handle_packet(&self, sender_id: ComponentId, packet: &[u8]) -> Result<(), Self::Error> {
|
fn handle_packet(&self, sender_id: ComponentId, packet: &[u8]) -> Result<(), Self::Error> {
|
||||||
match self {
|
match self {
|
||||||
TmTcSender::Static(packet_sender_with_shared_pool) => {
|
TmTcSender::Normal(sync_sender) => {
|
||||||
if let Err(e) = packet_sender_with_shared_pool.handle_packet(sender_id, packet) {
|
if let Err(e) = sync_sender.send(PacketAsVec::new(sender_id, packet.to_vec())) {
|
||||||
log::error!("Error sending packet via Static TM/TC sender: {:?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
TmTcSender::Heap(sync_sender) => {
|
|
||||||
if let Err(e) = sync_sender.handle_packet(sender_id, packet) {
|
|
||||||
log::error!("Error sending packet via Heap TM/TC sender: {:?}", e);
|
log::error!("Error sending packet via Heap TM/TC sender: {:?}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,121 +1,97 @@
|
|||||||
|
use models::{ComponentId, TcHeader, ccsds::CcsdsTcPacketOwned};
|
||||||
use satrs::{
|
use satrs::{
|
||||||
pool::PoolProvider,
|
|
||||||
pus::HandlingStatus,
|
pus::HandlingStatus,
|
||||||
tmtc::{PacketAsVec, PacketInPool, SharedPacketPool},
|
spacepackets::{CcsdsPacketReader, ChecksumType},
|
||||||
|
tmtc::PacketAsVec,
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
sync::mpsc::{self, TryRecvError},
|
||||||
};
|
};
|
||||||
use std::sync::mpsc::{self, TryRecvError};
|
|
||||||
|
|
||||||
use crate::pus::PusTcDistributor;
|
pub type CcsdsDistributor = HashMap<ComponentId, std::sync::mpsc::SyncSender<CcsdsTcPacketOwned>>;
|
||||||
|
|
||||||
// TC source components where static pools are the backing memory of the received telecommands.
|
|
||||||
pub struct TcSourceTaskStatic {
|
|
||||||
shared_tc_pool: SharedPacketPool,
|
|
||||||
tc_receiver: mpsc::Receiver<PacketInPool>,
|
|
||||||
/// We allocate this buffer from the heap to avoid a clippy warning on large enum variant
|
|
||||||
/// differences.
|
|
||||||
tc_buf: Box<[u8; 4096]>,
|
|
||||||
pus_distributor: PusTcDistributor,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
impl TcSourceTaskStatic {
|
|
||||||
pub fn new(
|
|
||||||
shared_tc_pool: SharedPacketPool,
|
|
||||||
tc_receiver: mpsc::Receiver<PacketInPool>,
|
|
||||||
pus_receiver: PusTcDistributor,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
shared_tc_pool,
|
|
||||||
tc_receiver,
|
|
||||||
tc_buf: Box::new([0; 4096]),
|
|
||||||
pus_distributor: pus_receiver,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn periodic_operation(&mut self) {
|
|
||||||
self.poll_tc();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn poll_tc(&mut self) -> HandlingStatus {
|
|
||||||
// Right now, we only expect ECSS PUS packets.
|
|
||||||
// If packets like CFDP are expected, we might have to check the APID first.
|
|
||||||
match self.tc_receiver.try_recv() {
|
|
||||||
Ok(packet_in_pool) => {
|
|
||||||
let pool = self
|
|
||||||
.shared_tc_pool
|
|
||||||
.0
|
|
||||||
.read()
|
|
||||||
.expect("locking tc pool failed");
|
|
||||||
pool.read(&packet_in_pool.store_addr, self.tc_buf.as_mut_slice())
|
|
||||||
.expect("reading pool failed");
|
|
||||||
drop(pool);
|
|
||||||
self.pus_distributor
|
|
||||||
.handle_tc_packet_in_store(packet_in_pool, self.tc_buf.as_slice())
|
|
||||||
.ok();
|
|
||||||
HandlingStatus::HandledOne
|
|
||||||
}
|
|
||||||
Err(e) => match e {
|
|
||||||
TryRecvError::Empty => HandlingStatus::Empty,
|
|
||||||
TryRecvError::Disconnected => {
|
|
||||||
log::warn!("tmtc thread: sender disconnected");
|
|
||||||
HandlingStatus::Empty
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TC source components where the heap is the backing memory of the received telecommands.
|
// TC source components where the heap is the backing memory of the received telecommands.
|
||||||
pub struct TcSourceTaskDynamic {
|
pub struct TcSourceTask {
|
||||||
pub tc_receiver: mpsc::Receiver<PacketAsVec>,
|
pub tc_receiver: mpsc::Receiver<PacketAsVec>,
|
||||||
pus_distributor: PusTcDistributor,
|
ccsds_distributor: CcsdsDistributor,
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
impl TcSourceTaskDynamic {
|
|
||||||
pub fn new(tc_receiver: mpsc::Receiver<PacketAsVec>, pus_receiver: PusTcDistributor) -> Self {
|
|
||||||
Self {
|
|
||||||
tc_receiver,
|
|
||||||
pus_distributor: pus_receiver,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn periodic_operation(&mut self) {
|
|
||||||
self.poll_tc();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn poll_tc(&mut self) -> HandlingStatus {
|
|
||||||
// Right now, we only expect ECSS PUS packets.
|
|
||||||
// If packets like CFDP are expected, we might have to check the APID first.
|
|
||||||
match self.tc_receiver.try_recv() {
|
|
||||||
Ok(packet_as_vec) => {
|
|
||||||
self.pus_distributor
|
|
||||||
.handle_tc_packet_vec(packet_as_vec)
|
|
||||||
.ok();
|
|
||||||
HandlingStatus::HandledOne
|
|
||||||
}
|
|
||||||
Err(e) => match e {
|
|
||||||
TryRecvError::Empty => HandlingStatus::Empty,
|
|
||||||
TryRecvError::Disconnected => {
|
|
||||||
log::warn!("tmtc thread: sender disconnected");
|
|
||||||
HandlingStatus::Empty
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub enum TcSourceTask {
|
|
||||||
Static(TcSourceTaskStatic),
|
|
||||||
Heap(TcSourceTaskDynamic),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TcSourceTask {
|
impl TcSourceTask {
|
||||||
|
pub fn new(
|
||||||
|
tc_receiver: mpsc::Receiver<PacketAsVec>,
|
||||||
|
ccsds_distributor: CcsdsDistributor,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
tc_receiver,
|
||||||
|
ccsds_distributor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_target(
|
||||||
|
&mut self,
|
||||||
|
target_id: ComponentId,
|
||||||
|
sender: mpsc::SyncSender<CcsdsTcPacketOwned>,
|
||||||
|
) {
|
||||||
|
self.ccsds_distributor.insert(target_id, sender);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn periodic_operation(&mut self) {
|
pub fn periodic_operation(&mut self) {
|
||||||
match self {
|
loop {
|
||||||
TcSourceTask::Static(task) => task.periodic_operation(),
|
if self.poll_tc() == HandlingStatus::Empty {
|
||||||
TcSourceTask::Heap(task) => task.periodic_operation(),
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn poll_tc(&mut self) -> HandlingStatus {
|
||||||
|
match self.tc_receiver.try_recv() {
|
||||||
|
Ok(packet) => {
|
||||||
|
log::debug!("received raw packet: {:?}", packet);
|
||||||
|
let ccsds_tc_reader_result =
|
||||||
|
CcsdsPacketReader::new(&packet.packet, Some(ChecksumType::WithCrc16));
|
||||||
|
if ccsds_tc_reader_result.is_err() {
|
||||||
|
log::warn!(
|
||||||
|
"received invalid CCSDS TC packet: {:?}",
|
||||||
|
ccsds_tc_reader_result.err()
|
||||||
|
);
|
||||||
|
// TODO: Send a dedicated TM packet.
|
||||||
|
return HandlingStatus::HandledOne;
|
||||||
|
}
|
||||||
|
let ccsds_tc_reader = ccsds_tc_reader_result.unwrap();
|
||||||
|
let tc_header_result =
|
||||||
|
postcard::take_from_bytes::<TcHeader>(ccsds_tc_reader.user_data());
|
||||||
|
if tc_header_result.is_err() {
|
||||||
|
log::warn!(
|
||||||
|
"received CCSDS TC packet with invalid TC header: {:?}",
|
||||||
|
tc_header_result.err()
|
||||||
|
);
|
||||||
|
// TODO: Send a dedicated TM packet.
|
||||||
|
return HandlingStatus::HandledOne;
|
||||||
|
}
|
||||||
|
let (tc_header, payload) = tc_header_result.unwrap();
|
||||||
|
if let Some(sender) = self.ccsds_distributor.get(&tc_header.target_id) {
|
||||||
|
log::debug!("sending TC packet to target ID: {:?}", tc_header.target_id);
|
||||||
|
sender
|
||||||
|
.send(CcsdsTcPacketOwned {
|
||||||
|
sp_header: *ccsds_tc_reader.sp_header(),
|
||||||
|
tc_header,
|
||||||
|
payload: payload.to_vec(),
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
} else {
|
||||||
|
log::warn!("no TC handler for target ID {:?}", tc_header.target_id);
|
||||||
|
// TODO: Send a dedicated TM packet.
|
||||||
|
}
|
||||||
|
HandlingStatus::HandledOne
|
||||||
|
}
|
||||||
|
Err(e) => match e {
|
||||||
|
TryRecvError::Empty => HandlingStatus::Empty,
|
||||||
|
TryRecvError::Disconnected => {
|
||||||
|
log::warn!("tmtc thread: sender disconnected");
|
||||||
|
HandlingStatus::Empty
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,18 +4,8 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use arbitrary_int::{u11, u14};
|
use arbitrary_int::{u11, u14};
|
||||||
use log::info;
|
use models::ccsds::CcsdsTmPacketOwned;
|
||||||
use satrs::{
|
use satrs::spacepackets::seq_count::{SequenceCounter, SequenceCounterCcsdsSimple};
|
||||||
pool::PoolProvider,
|
|
||||||
spacepackets::{
|
|
||||||
ecss::{tm::PusTmZeroCopyWriter, PusPacket},
|
|
||||||
seq_count::SequenceCounter,
|
|
||||||
seq_count::SequenceCounterCcsdsSimple,
|
|
||||||
time::cds::MIN_CDS_FIELD_LEN,
|
|
||||||
CcsdsPacket,
|
|
||||||
},
|
|
||||||
tmtc::{PacketAsVec, PacketInPool, SharedPacketPool},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::interface::tcp::SyncTcpTmSource;
|
use crate::interface::tcp::SyncTcpTmSource;
|
||||||
|
|
||||||
@@ -34,150 +24,35 @@ impl CcsdsSeqCounterMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TmFunnelCommon {
|
pub struct TmSink {
|
||||||
seq_counter_map: CcsdsSeqCounterMap,
|
seq_counter_map: CcsdsSeqCounterMap,
|
||||||
msg_counter_map: HashMap<u8, u16>,
|
|
||||||
sync_tm_tcp_source: SyncTcpTmSource,
|
sync_tm_tcp_source: SyncTcpTmSource,
|
||||||
|
tm_funnel_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
|
||||||
|
tm_server_tx: mpsc::SyncSender<CcsdsTmPacketOwned>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TmFunnelCommon {
|
impl TmSink {
|
||||||
pub fn new(sync_tm_tcp_source: SyncTcpTmSource) -> Self {
|
pub fn new(
|
||||||
|
sync_tm_tcp_source: SyncTcpTmSource,
|
||||||
|
tm_funnel_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
|
||||||
|
tm_server_tx: mpsc::SyncSender<CcsdsTmPacketOwned>,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
seq_counter_map: Default::default(),
|
seq_counter_map: Default::default(),
|
||||||
msg_counter_map: Default::default(),
|
|
||||||
sync_tm_tcp_source,
|
sync_tm_tcp_source,
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Applies common packet processing operations for PUS TM packets. This includes setting
|
|
||||||
// a sequence counter
|
|
||||||
fn apply_packet_processing(&mut self, mut zero_copy_writer: PusTmZeroCopyWriter) {
|
|
||||||
// zero_copy_writer.set_apid(PUS_APID);
|
|
||||||
zero_copy_writer.set_seq_count(
|
|
||||||
self.seq_counter_map
|
|
||||||
.get_and_increment(zero_copy_writer.apid()),
|
|
||||||
);
|
|
||||||
let entry = self
|
|
||||||
.msg_counter_map
|
|
||||||
.entry(zero_copy_writer.service_type_id())
|
|
||||||
.or_insert(0);
|
|
||||||
zero_copy_writer.set_msg_count(*entry);
|
|
||||||
if *entry == u16::MAX {
|
|
||||||
*entry = 0;
|
|
||||||
} else {
|
|
||||||
*entry += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
Self::packet_printout(&zero_copy_writer);
|
|
||||||
// This operation has to come last!
|
|
||||||
zero_copy_writer.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn packet_printout(tm: &PusTmZeroCopyWriter) {
|
|
||||||
info!(
|
|
||||||
"Sending PUS TM[{},{}] with APID {}",
|
|
||||||
tm.service_type_id(),
|
|
||||||
tm.message_subtype_id(),
|
|
||||||
tm.apid()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct TmSinkStatic {
|
|
||||||
common: TmFunnelCommon,
|
|
||||||
shared_tm_store: SharedPacketPool,
|
|
||||||
tm_funnel_rx: mpsc::Receiver<PacketInPool>,
|
|
||||||
tm_server_tx: mpsc::SyncSender<PacketInPool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
impl TmSinkStatic {
|
|
||||||
pub fn new(
|
|
||||||
shared_tm_store: SharedPacketPool,
|
|
||||||
sync_tm_tcp_source: SyncTcpTmSource,
|
|
||||||
tm_funnel_rx: mpsc::Receiver<PacketInPool>,
|
|
||||||
tm_server_tx: mpsc::SyncSender<PacketInPool>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
common: TmFunnelCommon::new(sync_tm_tcp_source),
|
|
||||||
shared_tm_store,
|
|
||||||
tm_funnel_rx,
|
tm_funnel_rx,
|
||||||
tm_server_tx,
|
tm_server_tx,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn operation(&mut self) {
|
pub fn operation(&mut self) {
|
||||||
if let Ok(pus_tm_in_pool) = self.tm_funnel_rx.recv() {
|
if let Ok(mut tm) = self.tm_funnel_rx.try_recv() {
|
||||||
// Read the TM, set sequence counter and message counter, and finally update
|
tm.sp_header
|
||||||
// the CRC.
|
.set_seq_count(self.seq_counter_map.get_and_increment(tm.sp_header.apid()));
|
||||||
let shared_pool = self.shared_tm_store.0.clone();
|
self.sync_tm_tcp_source.add_tm(&tm.to_vec());
|
||||||
let mut pool_guard = shared_pool.write().expect("Locking TM pool failed");
|
|
||||||
let mut tm_copy = Vec::new();
|
|
||||||
pool_guard
|
|
||||||
.modify(&pus_tm_in_pool.store_addr, |buf| {
|
|
||||||
let zero_copy_writer = PusTmZeroCopyWriter::new(buf, MIN_CDS_FIELD_LEN, true)
|
|
||||||
.expect("Creating TM zero copy writer failed");
|
|
||||||
self.common.apply_packet_processing(zero_copy_writer);
|
|
||||||
tm_copy = buf.to_vec()
|
|
||||||
})
|
|
||||||
.expect("Reading TM from pool failed");
|
|
||||||
self.tm_server_tx
|
|
||||||
.send(pus_tm_in_pool)
|
|
||||||
.expect("Sending TM to server failed");
|
|
||||||
// We could also do this step in the update closure, but I'd rather avoid this, could
|
|
||||||
// lead to nested locking.
|
|
||||||
self.common.sync_tm_tcp_source.add_tm(&tm_copy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct TmSinkDynamic {
|
|
||||||
common: TmFunnelCommon,
|
|
||||||
tm_funnel_rx: mpsc::Receiver<PacketAsVec>,
|
|
||||||
tm_server_tx: mpsc::SyncSender<PacketAsVec>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
impl TmSinkDynamic {
|
|
||||||
pub fn new(
|
|
||||||
sync_tm_tcp_source: SyncTcpTmSource,
|
|
||||||
tm_funnel_rx: mpsc::Receiver<PacketAsVec>,
|
|
||||||
tm_server_tx: mpsc::SyncSender<PacketAsVec>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
common: TmFunnelCommon::new(sync_tm_tcp_source),
|
|
||||||
tm_funnel_rx,
|
|
||||||
tm_server_tx,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn operation(&mut self) {
|
|
||||||
if let Ok(mut tm) = self.tm_funnel_rx.recv() {
|
|
||||||
// Read the TM, set sequence counter and message counter, and finally update
|
|
||||||
// the CRC.
|
|
||||||
let zero_copy_writer =
|
|
||||||
PusTmZeroCopyWriter::new(&mut tm.packet, MIN_CDS_FIELD_LEN, true)
|
|
||||||
.expect("Creating TM zero copy writer failed");
|
|
||||||
self.common.apply_packet_processing(zero_copy_writer);
|
|
||||||
self.common.sync_tm_tcp_source.add_tm(&tm.packet);
|
|
||||||
self.tm_server_tx
|
self.tm_server_tx
|
||||||
.send(tm)
|
.send(tm)
|
||||||
.expect("Sending TM to server failed");
|
.expect("Sending TM to server failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub enum TmSink {
|
|
||||||
Static(TmSinkStatic),
|
|
||||||
Heap(TmSinkDynamic),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TmSink {
|
|
||||||
pub fn operation(&mut self) {
|
|
||||||
match self {
|
|
||||||
TmSink::Static(static_sink) => static_sink.operation(),
|
|
||||||
TmSink::Heap(dynamic_sink) => dynamic_sink.operation(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ license = "Apache-2.0"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
spacepackets = { version = "0.17", default-features = false }
|
# spacepackets = { version = "0.17", default-features = false }
|
||||||
|
spacepackets = { version = "0.17", git = "https://egit.irs.uni-stuttgart.de/rust/spacepackets.git", default-features = false }
|
||||||
serde = { version = "1", default-features = false, optional = true }
|
serde = { version = "1", default-features = false, optional = true }
|
||||||
defmt = {version = "1", optional = true }
|
defmt = {version = "1", optional = true }
|
||||||
|
|
||||||
|
|||||||
+2
-7
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "satrs"
|
name = "satrs"
|
||||||
version = "0.3.0-alpha.2"
|
version = "0.3.0-alpha.3"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.85.0"
|
rust-version = "1.85.0"
|
||||||
authors = ["Robin Mueller <muellerr@irs.uni-stuttgart.de>"]
|
authors = ["Robin Mueller <muellerr@irs.uni-stuttgart.de>"]
|
||||||
@@ -14,7 +14,7 @@ categories = ["aerospace", "aerospace::space-protocols", "no-std", "hardware-sup
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
satrs-shared = { version = "0.2", path = "../satrs-shared" }
|
satrs-shared = { version = "0.2", path = "../satrs-shared" }
|
||||||
spacepackets = { version = "0.17", default-features = false }
|
spacepackets = { version = "0.17", git = "https://egit.irs.uni-stuttgart.de/rust/spacepackets.git", default-features = false }
|
||||||
|
|
||||||
delegate = "0.13"
|
delegate = "0.13"
|
||||||
paste = "1"
|
paste = "1"
|
||||||
@@ -78,8 +78,3 @@ test_util = []
|
|||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
rustdoc-args = ["--generate-link-to-definition"]
|
rustdoc-args = ["--generate-link-to-definition"]
|
||||||
|
|
||||||
[[test]]
|
|
||||||
name = "event_test"
|
|
||||||
path = "tests/pus_events.rs"
|
|
||||||
required-features = ["test_util"]
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod scheduler;
|
||||||
@@ -0,0 +1,926 @@
|
|||||||
|
//! # CCSDS Telecommand Scheduler.
|
||||||
|
#![deny(missing_docs)]
|
||||||
|
use core::{hash::Hash, time::Duration};
|
||||||
|
|
||||||
|
#[cfg(feature = "alloc")]
|
||||||
|
pub use alloc_mod::*;
|
||||||
|
use spacepackets::{
|
||||||
|
CcsdsPacketIdAndPsc,
|
||||||
|
time::{TimestampError, UnixTime},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Generic CCSDS scheduling errors.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||||
|
pub enum ScheduleError {
|
||||||
|
/// The release time is within the time-margin added on top of the current time.
|
||||||
|
/// The first parameter is the current time, the second one the time margin, and the third one
|
||||||
|
/// the release time.
|
||||||
|
#[error("release time in margin")]
|
||||||
|
ReleaseTimeInTimeMargin {
|
||||||
|
/// Current time.
|
||||||
|
current_time: UnixTime,
|
||||||
|
/// Configured time margin.
|
||||||
|
time_margin: Duration,
|
||||||
|
/// Release time.
|
||||||
|
release_time: UnixTime,
|
||||||
|
},
|
||||||
|
/// Nested time-tagged commands are not allowed.
|
||||||
|
#[error("nested scheduled tc")]
|
||||||
|
NestedScheduledTc,
|
||||||
|
/// TC data is empty.
|
||||||
|
#[error("tc data empty")]
|
||||||
|
TcDataEmpty,
|
||||||
|
/// Scheduler is full, packet number limit reached.
|
||||||
|
#[error("scheduler is full, packet number limit reached")]
|
||||||
|
PacketLimitReached,
|
||||||
|
/// Scheduler is full, numver of bytes limit reached.
|
||||||
|
#[error("scheduler is full, number of bytes limit reached")]
|
||||||
|
ByteLimitReached,
|
||||||
|
/// Timestamp error.
|
||||||
|
#[error("timestamp error: {0}")]
|
||||||
|
TimestampError(#[from] TimestampError),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Packet ID used for identifying scheduled packets.
|
||||||
|
///
|
||||||
|
/// Right now, this ID can be determined from the packet without requiring external input
|
||||||
|
/// or custom data fields in the CCSDS space pacekt.
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
pub struct CcsdsSchedulePacketId {
|
||||||
|
/// Base ID.
|
||||||
|
pub base: CcsdsPacketIdAndPsc,
|
||||||
|
/// Optional checksum of the packet.
|
||||||
|
pub crc16: Option<u16>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CcsdsSchedulePacketId {
|
||||||
|
/// Create a new CCSDS scheduling packet ID.
|
||||||
|
pub const fn new(base: CcsdsPacketIdAndPsc, checksum: Option<u16>) -> Self {
|
||||||
|
Self {
|
||||||
|
base,
|
||||||
|
crc16: checksum,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hash for CcsdsSchedulePacketId {
|
||||||
|
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||||
|
self.base.hash(state);
|
||||||
|
self.crc16.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Modules requiring [alloc] support.
|
||||||
|
#[cfg(feature = "alloc")]
|
||||||
|
pub mod alloc_mod {
|
||||||
|
use core::time::Duration;
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
use std::time::SystemTimeError;
|
||||||
|
|
||||||
|
use alloc::collections::btree_map;
|
||||||
|
use spacepackets::{CcsdsPacketIdAndPsc, CcsdsPacketReader, time::UnixTime};
|
||||||
|
|
||||||
|
use crate::ccsds::scheduler::CcsdsSchedulePacketId;
|
||||||
|
|
||||||
|
/// The scheduler can be configured to have bounds for both the number of packets
|
||||||
|
/// and the total number of bytes used by scheduled packets.
|
||||||
|
///
|
||||||
|
/// This can be used to avoid memory exhaustion in systems with limited resources or under
|
||||||
|
/// heavy workloads.
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||||
|
pub struct Limits {
|
||||||
|
/// Maximum number of scheduled packets.
|
||||||
|
pub packets: Option<usize>,
|
||||||
|
/// Maximum total number of bytes used by scheduled packets.
|
||||||
|
pub bytes: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Limits {
|
||||||
|
/// Create new limits for the CCSDS scheduler.
|
||||||
|
pub const fn new(packets: Option<usize>, bytes: Option<usize>) -> Self {
|
||||||
|
Self { packets, bytes }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if no limits are set.
|
||||||
|
pub fn has_no_limits(&self) -> bool {
|
||||||
|
self.packets.is_none() && self.bytes.is_none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fill count of the scheduler.
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||||
|
pub struct FillCount {
|
||||||
|
/// Number of scheduled packets.
|
||||||
|
pub packets: usize,
|
||||||
|
/// Total number of bytes used by scheduled packets.
|
||||||
|
pub bytes: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simple CCSDS scheduler implementation.
|
||||||
|
///
|
||||||
|
/// Relies of [alloc] support but limits the number of scheduled packets.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CcsdsScheduler {
|
||||||
|
tc_map: alloc::collections::BTreeMap<
|
||||||
|
UnixTime,
|
||||||
|
alloc::vec::Vec<(CcsdsSchedulePacketId, alloc::vec::Vec<u8>)>,
|
||||||
|
>,
|
||||||
|
limits: Limits,
|
||||||
|
pub(crate) current_time: UnixTime,
|
||||||
|
time_margin: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CcsdsScheduler {
|
||||||
|
/// Create a new CCSDS scheduler.
|
||||||
|
pub fn new(current_time: UnixTime, limits: Limits, time_margin: Duration) -> Self {
|
||||||
|
Self {
|
||||||
|
tc_map: alloc::collections::BTreeMap::new(),
|
||||||
|
limits,
|
||||||
|
current_time,
|
||||||
|
time_margin,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Like [Self::new], but sets the `init_current_time` parameter to the current system time.
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
pub fn new_with_current_init_time(
|
||||||
|
limits: Limits,
|
||||||
|
time_margin: Duration,
|
||||||
|
) -> Result<Self, SystemTimeError> {
|
||||||
|
Ok(Self::new(UnixTime::now()?, limits, time_margin))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Current fill count: number of scheduled packets and total number of bytes.
|
||||||
|
///
|
||||||
|
/// The first returned value is the number of scheduled packets, the second one is the
|
||||||
|
/// byte count.
|
||||||
|
pub fn current_fill_count(&self) -> FillCount {
|
||||||
|
let mut fill_count = FillCount::default();
|
||||||
|
for value in self.tc_map.values() {
|
||||||
|
for (_, raw_scheduled_tc) in value {
|
||||||
|
fill_count.packets += 1;
|
||||||
|
fill_count.bytes += raw_scheduled_tc.len();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fill_count
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Current number of scheduled entries.
|
||||||
|
pub fn num_of_entries(&self) -> usize {
|
||||||
|
self.current_fill_count().packets
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the current time.
|
||||||
|
#[inline]
|
||||||
|
pub fn update_time(&mut self, current_time: UnixTime) {
|
||||||
|
self.current_time = current_time;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Current time.
|
||||||
|
#[inline]
|
||||||
|
pub fn current_time(&self) -> &UnixTime {
|
||||||
|
&self.current_time
|
||||||
|
}
|
||||||
|
|
||||||
|
fn common_check(
|
||||||
|
&mut self,
|
||||||
|
release_time: UnixTime,
|
||||||
|
packet_size: usize,
|
||||||
|
) -> Result<(), super::ScheduleError> {
|
||||||
|
if !self.limits.has_no_limits() {
|
||||||
|
let fill_count = self.current_fill_count();
|
||||||
|
if let Some(max_bytes) = self.limits.bytes {
|
||||||
|
if fill_count.bytes + packet_size > max_bytes {
|
||||||
|
return Err(super::ScheduleError::ByteLimitReached);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(max_packets) = self.limits.packets {
|
||||||
|
if fill_count.packets + 1 > max_packets {
|
||||||
|
return Err(super::ScheduleError::PacketLimitReached);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if release_time < self.current_time + self.time_margin {
|
||||||
|
return Err(super::ScheduleError::ReleaseTimeInTimeMargin {
|
||||||
|
current_time: self.current_time,
|
||||||
|
time_margin: self.time_margin,
|
||||||
|
release_time,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert a telecommand using an existing [CcsdsPacketReader].
|
||||||
|
pub fn insert_telecommand_with_reader(
|
||||||
|
&mut self,
|
||||||
|
reader: &CcsdsPacketReader,
|
||||||
|
release_time: UnixTime,
|
||||||
|
) -> Result<(), super::ScheduleError> {
|
||||||
|
self.common_check(release_time, reader.packet_len())?;
|
||||||
|
let base_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(reader);
|
||||||
|
let checksum = reader.checksum();
|
||||||
|
let packet_id_scheduling = CcsdsSchedulePacketId {
|
||||||
|
base: base_id,
|
||||||
|
crc16: checksum,
|
||||||
|
};
|
||||||
|
self.insert_telecommand(packet_id_scheduling, reader.raw_data(), release_time)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert a raw telecommand, assuming the user has already extracted the
|
||||||
|
/// [CcsdsSchedulePacketId]
|
||||||
|
pub fn insert_telecommand(
|
||||||
|
&mut self,
|
||||||
|
packet_id_scheduling: CcsdsSchedulePacketId,
|
||||||
|
raw_packet: &[u8],
|
||||||
|
release_time: UnixTime,
|
||||||
|
) -> Result<(), super::ScheduleError> {
|
||||||
|
self.common_check(release_time, raw_packet.len())?;
|
||||||
|
match self.tc_map.entry(release_time) {
|
||||||
|
btree_map::Entry::Vacant(e) => {
|
||||||
|
e.insert(alloc::vec![(packet_id_scheduling, raw_packet.to_vec())]);
|
||||||
|
}
|
||||||
|
btree_map::Entry::Occupied(mut v) => {
|
||||||
|
v.get_mut()
|
||||||
|
.push((packet_id_scheduling, raw_packet.to_vec()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Release all telecommands which should be released based on the current time.
|
||||||
|
pub fn release_telecommands<R: FnMut(&CcsdsSchedulePacketId, &[u8])>(
|
||||||
|
&mut self,
|
||||||
|
mut releaser: R,
|
||||||
|
) {
|
||||||
|
let tcs_to_release = self.telecommands_to_release();
|
||||||
|
for tc_group in tcs_to_release {
|
||||||
|
for (packet_id, raw_tc) in tc_group.1 {
|
||||||
|
releaser(packet_id, raw_tc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.tc_map.retain(|k, _| k > &self.current_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve all telecommands which should be released based on the current time.
|
||||||
|
pub fn telecommands_to_release(
|
||||||
|
&self,
|
||||||
|
) -> btree_map::Range<
|
||||||
|
'_,
|
||||||
|
UnixTime,
|
||||||
|
alloc::vec::Vec<(CcsdsSchedulePacketId, alloc::vec::Vec<u8>)>,
|
||||||
|
> {
|
||||||
|
self.tc_map.range(..=self.current_time)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete scheduled telecommand by their packet ID.
|
||||||
|
///
|
||||||
|
/// Returns whether any telecommand was deleted. This function might have to be called
|
||||||
|
/// multiple times if multiple identical CCSDS packet IDs are possible.
|
||||||
|
pub fn delete_by_id(&mut self, packet_id: &CcsdsSchedulePacketId) -> bool {
|
||||||
|
let mut was_removed = false;
|
||||||
|
self.tc_map.retain(|_, v| {
|
||||||
|
let len_before = v.len();
|
||||||
|
v.retain(|(stored_id, _)| stored_id != packet_id);
|
||||||
|
let has_remaining = !v.is_empty();
|
||||||
|
if v.len() < len_before {
|
||||||
|
was_removed = true;
|
||||||
|
}
|
||||||
|
has_remaining
|
||||||
|
});
|
||||||
|
was_removed
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete all telecommands scheduled in a time window.
|
||||||
|
///
|
||||||
|
/// The range includes the start time but excludes the end time. Returns whether any
|
||||||
|
/// telecommands were deleted.
|
||||||
|
pub fn delete_time_window(&mut self, start_time: UnixTime, end_time: UnixTime) -> bool {
|
||||||
|
let len_before = self.tc_map.len();
|
||||||
|
self.tc_map.retain(|k, _| k < &start_time || k >= &end_time);
|
||||||
|
self.tc_map.len() < len_before
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete all scheduled telecommands scheduled after or at a given time.
|
||||||
|
///
|
||||||
|
/// Returns whether any telecommands were deleted.
|
||||||
|
pub fn delete_starting_at(&mut self, start_time: UnixTime) -> bool {
|
||||||
|
let len_before = self.tc_map.len();
|
||||||
|
self.tc_map.retain(|k, _| k < &start_time);
|
||||||
|
self.tc_map.len() < len_before
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete all scheduled telecommands scheduled before but not equal to a given time.
|
||||||
|
///
|
||||||
|
/// Returns whether any telecommands were deleted.
|
||||||
|
pub fn delete_before(&mut self, end_time: UnixTime) -> bool {
|
||||||
|
let len_before = self.tc_map.len();
|
||||||
|
self.tc_map.retain(|k, _| k >= &end_time);
|
||||||
|
self.tc_map.len() < len_before
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Completely clear the scheduler.
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.tc_map.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use arbitrary_int::{traits::Integer, u11, u14};
|
||||||
|
use spacepackets::{
|
||||||
|
CcsdsPacketCreatorOwned, CcsdsPacketReader, ChecksumType, SpacePacketHeader,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn test_tc(app_data: &[u8], seq_count: u14) -> CcsdsPacketCreatorOwned {
|
||||||
|
CcsdsPacketCreatorOwned::new(
|
||||||
|
SpacePacketHeader::new_for_tc(
|
||||||
|
u11::new(0x1),
|
||||||
|
spacepackets::SequenceFlags::Unsegmented,
|
||||||
|
seq_count,
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
spacepackets::PacketType::Tc,
|
||||||
|
app_data,
|
||||||
|
Some(ChecksumType::WithCrc16),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_basic() {
|
||||||
|
let unix_time = UnixTime::new(0, 0);
|
||||||
|
let mut scheduler = CcsdsScheduler::new(
|
||||||
|
unix_time,
|
||||||
|
Limits::new(Some(100), Some(1024)),
|
||||||
|
Duration::from_millis(5000),
|
||||||
|
);
|
||||||
|
assert_eq!(scheduler.current_fill_count().packets, 0);
|
||||||
|
assert_eq!(scheduler.current_fill_count().bytes, 0);
|
||||||
|
assert_eq!(scheduler.num_of_entries(), 0);
|
||||||
|
assert_eq!(
|
||||||
|
scheduler
|
||||||
|
.telecommands_to_release()
|
||||||
|
.collect::<alloc::vec::Vec<_>>()
|
||||||
|
.len(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
assert_eq!(scheduler.current_time(), &unix_time);
|
||||||
|
scheduler.release_telecommands(|_, _| {
|
||||||
|
panic!("should not be called");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mutable_closure() {
|
||||||
|
let unix_time = UnixTime::new(0, 0);
|
||||||
|
let mut scheduler = CcsdsScheduler::new(
|
||||||
|
unix_time,
|
||||||
|
Limits::new(Some(100), Some(1024)),
|
||||||
|
Duration::from_millis(5000),
|
||||||
|
);
|
||||||
|
let mut some_flag = false;
|
||||||
|
// We should be able to manipulate the boolean inside the closure.
|
||||||
|
scheduler.release_telecommands(|_, _| {
|
||||||
|
some_flag = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_clear() {
|
||||||
|
let unix_time = UnixTime::new(0, 0);
|
||||||
|
let mut scheduler = CcsdsScheduler::new(
|
||||||
|
unix_time,
|
||||||
|
Limits::new(Some(100), Some(1024)),
|
||||||
|
Duration::from_millis(1000),
|
||||||
|
);
|
||||||
|
let test_tc = test_tc(&[1, 2, 3], u14::ZERO);
|
||||||
|
let test_tc_raw = test_tc.to_vec();
|
||||||
|
let reader = CcsdsPacketReader::new(&test_tc_raw, Some(ChecksumType::WithCrc16)).unwrap();
|
||||||
|
scheduler
|
||||||
|
.insert_telecommand_with_reader(&reader, UnixTime::new(2, 0))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(scheduler.current_fill_count().packets, 1);
|
||||||
|
assert_eq!(scheduler.current_fill_count().bytes, test_tc_raw.len());
|
||||||
|
assert_eq!(scheduler.num_of_entries(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
scheduler
|
||||||
|
.telecommands_to_release()
|
||||||
|
.collect::<alloc::vec::Vec<_>>()
|
||||||
|
.len(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
scheduler.clear();
|
||||||
|
assert_eq!(scheduler.current_fill_count().packets, 0);
|
||||||
|
assert_eq!(scheduler.current_fill_count().bytes, 0);
|
||||||
|
assert_eq!(scheduler.num_of_entries(), 0);
|
||||||
|
assert_eq!(
|
||||||
|
scheduler
|
||||||
|
.telecommands_to_release()
|
||||||
|
.collect::<alloc::vec::Vec<_>>()
|
||||||
|
.len(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn insert_and_release_one() {
|
||||||
|
let unix_time = UnixTime::new(0, 0);
|
||||||
|
let mut scheduler = CcsdsScheduler::new(
|
||||||
|
unix_time,
|
||||||
|
Limits::new(Some(100), Some(1024)),
|
||||||
|
Duration::from_millis(1000),
|
||||||
|
);
|
||||||
|
let test_tc_0 = test_tc(&[1, 2, 3], u14::ZERO);
|
||||||
|
let tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&test_tc_0);
|
||||||
|
let test_tc_raw = test_tc_0.to_vec();
|
||||||
|
let reader = CcsdsPacketReader::new(&test_tc_raw, Some(ChecksumType::WithCrc16)).unwrap();
|
||||||
|
let checksum = reader.checksum();
|
||||||
|
scheduler
|
||||||
|
.insert_telecommand_with_reader(&reader, UnixTime::new(2, 0))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(scheduler.current_fill_count().packets, 1);
|
||||||
|
assert_eq!(scheduler.current_fill_count().bytes, test_tc_raw.len());
|
||||||
|
assert_eq!(scheduler.num_of_entries(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
scheduler
|
||||||
|
.telecommands_to_release()
|
||||||
|
.collect::<alloc::vec::Vec<_>>()
|
||||||
|
.len(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
scheduler.release_telecommands(|_, _| {
|
||||||
|
panic!("should not be called");
|
||||||
|
});
|
||||||
|
scheduler.update_time(UnixTime::new(3, 0));
|
||||||
|
assert_eq!(
|
||||||
|
scheduler
|
||||||
|
.telecommands_to_release()
|
||||||
|
.collect::<alloc::vec::Vec<_>>()
|
||||||
|
.len(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
scheduler.release_telecommands(|tc_id_scheduled, tc_raw| {
|
||||||
|
assert_eq!(tc_id, tc_id_scheduled.base);
|
||||||
|
assert_eq!(checksum, tc_id_scheduled.crc16);
|
||||||
|
assert_eq!(tc_raw, test_tc_raw);
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
scheduler
|
||||||
|
.telecommands_to_release()
|
||||||
|
.collect::<alloc::vec::Vec<_>>()
|
||||||
|
.len(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn insert_and_release_multi_0() {
|
||||||
|
let unix_time = UnixTime::new(0, 0);
|
||||||
|
let mut scheduler = CcsdsScheduler::new(
|
||||||
|
unix_time,
|
||||||
|
Limits::new(Some(100), Some(1024)),
|
||||||
|
Duration::from_millis(1000),
|
||||||
|
);
|
||||||
|
let test_tc_0 = test_tc(&[42], u14::ZERO);
|
||||||
|
let test_tc_1 = test_tc(&[1, 2, 3], u14::new(1));
|
||||||
|
let tc_id_0 = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&test_tc_0);
|
||||||
|
let tc_id_1 = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&test_tc_1);
|
||||||
|
let test_tc_0_raw = test_tc_0.to_vec();
|
||||||
|
let test_tc_1_raw = test_tc_1.to_vec();
|
||||||
|
let reader_0 =
|
||||||
|
CcsdsPacketReader::new(&test_tc_0_raw, Some(ChecksumType::WithCrc16)).unwrap();
|
||||||
|
let reader_1 =
|
||||||
|
CcsdsPacketReader::new(&test_tc_1_raw, Some(ChecksumType::WithCrc16)).unwrap();
|
||||||
|
scheduler
|
||||||
|
.insert_telecommand_with_reader(&reader_0, UnixTime::new(2, 0))
|
||||||
|
.unwrap();
|
||||||
|
scheduler
|
||||||
|
.insert_telecommand(
|
||||||
|
CcsdsSchedulePacketId::new(tc_id_1, reader_1.checksum()),
|
||||||
|
&test_tc_1_raw,
|
||||||
|
UnixTime::new(5, 0),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(scheduler.current_fill_count().packets, 2);
|
||||||
|
assert_eq!(
|
||||||
|
scheduler.current_fill_count().bytes,
|
||||||
|
test_tc_0_raw.len() + test_tc_1_raw.len()
|
||||||
|
);
|
||||||
|
assert_eq!(scheduler.num_of_entries(), 2);
|
||||||
|
assert_eq!(
|
||||||
|
scheduler
|
||||||
|
.telecommands_to_release()
|
||||||
|
.collect::<alloc::vec::Vec<_>>()
|
||||||
|
.len(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
scheduler.release_telecommands(|_, _| {
|
||||||
|
panic!("should not be called");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Release first TC.
|
||||||
|
scheduler.update_time(UnixTime::new(3, 0));
|
||||||
|
assert_eq!(
|
||||||
|
scheduler
|
||||||
|
.telecommands_to_release()
|
||||||
|
.collect::<alloc::vec::Vec<_>>()
|
||||||
|
.len(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
scheduler.release_telecommands(|tc_id_scheduled, tc_raw| {
|
||||||
|
assert_eq!(tc_id_0, tc_id_scheduled.base);
|
||||||
|
assert_eq!(reader_0.checksum(), tc_id_scheduled.crc16);
|
||||||
|
assert_eq!(tc_raw, test_tc_0_raw);
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
scheduler
|
||||||
|
.telecommands_to_release()
|
||||||
|
.collect::<alloc::vec::Vec<_>>()
|
||||||
|
.len(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
assert_eq!(scheduler.current_fill_count().packets, 1);
|
||||||
|
assert_eq!(scheduler.current_fill_count().bytes, test_tc_1_raw.len());
|
||||||
|
assert_eq!(scheduler.num_of_entries(), 1);
|
||||||
|
|
||||||
|
// Release second TC.
|
||||||
|
scheduler.update_time(UnixTime::new(6, 0));
|
||||||
|
assert_eq!(
|
||||||
|
scheduler
|
||||||
|
.telecommands_to_release()
|
||||||
|
.collect::<alloc::vec::Vec<_>>()
|
||||||
|
.len(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
scheduler.release_telecommands(|tc_id_scheduled, tc_raw| {
|
||||||
|
assert_eq!(tc_id_1, tc_id_scheduled.base);
|
||||||
|
assert_eq!(reader_1.checksum(), tc_id_scheduled.crc16);
|
||||||
|
assert_eq!(tc_raw, test_tc_1_raw);
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
scheduler
|
||||||
|
.telecommands_to_release()
|
||||||
|
.collect::<alloc::vec::Vec<_>>()
|
||||||
|
.len(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
assert_eq!(scheduler.current_fill_count().packets, 0);
|
||||||
|
assert_eq!(scheduler.current_fill_count().bytes, 0);
|
||||||
|
assert_eq!(scheduler.num_of_entries(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn insert_and_release_multi_1() {
|
||||||
|
let unix_time = UnixTime::new(0, 0);
|
||||||
|
let mut scheduler = CcsdsScheduler::new(
|
||||||
|
unix_time,
|
||||||
|
Limits::new(Some(100), Some(1024)),
|
||||||
|
Duration::from_millis(1000),
|
||||||
|
);
|
||||||
|
let test_tc_0 = test_tc(&[42], u14::ZERO);
|
||||||
|
let test_tc_1 = test_tc(&[1, 2, 3], u14::new(1));
|
||||||
|
let tc_id_0 = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&test_tc_0);
|
||||||
|
let tc_id_1 = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&test_tc_1);
|
||||||
|
let test_tc_0_raw = test_tc_0.to_vec();
|
||||||
|
let test_tc_1_raw = test_tc_1.to_vec();
|
||||||
|
let reader_0 =
|
||||||
|
CcsdsPacketReader::new(&test_tc_0_raw, Some(ChecksumType::WithCrc16)).unwrap();
|
||||||
|
let reader_1 =
|
||||||
|
CcsdsPacketReader::new(&test_tc_1_raw, Some(ChecksumType::WithCrc16)).unwrap();
|
||||||
|
scheduler
|
||||||
|
.insert_telecommand_with_reader(&reader_0, UnixTime::new(2, 0))
|
||||||
|
.unwrap();
|
||||||
|
scheduler
|
||||||
|
.insert_telecommand(
|
||||||
|
CcsdsSchedulePacketId::new(tc_id_1, reader_1.checksum()),
|
||||||
|
&test_tc_1_raw,
|
||||||
|
UnixTime::new(5, 0),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(scheduler.current_fill_count().packets, 2);
|
||||||
|
assert_eq!(
|
||||||
|
scheduler.current_fill_count().bytes,
|
||||||
|
test_tc_0_raw.len() + test_tc_1_raw.len()
|
||||||
|
);
|
||||||
|
assert_eq!(scheduler.num_of_entries(), 2);
|
||||||
|
assert_eq!(
|
||||||
|
scheduler
|
||||||
|
.telecommands_to_release()
|
||||||
|
.collect::<alloc::vec::Vec<_>>()
|
||||||
|
.len(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
scheduler.release_telecommands(|_, _| {
|
||||||
|
panic!("should not be called");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Release first TC.
|
||||||
|
scheduler.update_time(UnixTime::new(6, 0));
|
||||||
|
assert_eq!(
|
||||||
|
scheduler
|
||||||
|
.telecommands_to_release()
|
||||||
|
.collect::<alloc::vec::Vec<_>>()
|
||||||
|
.len(),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
let mut index = 0;
|
||||||
|
scheduler.release_telecommands(|tc_id_scheduled, tc_raw| {
|
||||||
|
if index == 0 {
|
||||||
|
assert_eq!(tc_id_0, tc_id_scheduled.base);
|
||||||
|
assert_eq!(reader_0.checksum(), tc_id_scheduled.crc16);
|
||||||
|
assert_eq!(tc_raw, test_tc_0_raw);
|
||||||
|
} else {
|
||||||
|
assert_eq!(tc_id_1, tc_id_scheduled.base);
|
||||||
|
assert_eq!(reader_1.checksum(), tc_id_scheduled.crc16);
|
||||||
|
assert_eq!(tc_raw, test_tc_1_raw);
|
||||||
|
}
|
||||||
|
index += 1;
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
scheduler
|
||||||
|
.telecommands_to_release()
|
||||||
|
.collect::<alloc::vec::Vec<_>>()
|
||||||
|
.len(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
assert_eq!(scheduler.current_fill_count().packets, 0);
|
||||||
|
assert_eq!(scheduler.current_fill_count().bytes, 0);
|
||||||
|
assert_eq!(scheduler.num_of_entries(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_packet_limit_reached() {
|
||||||
|
let unix_time = UnixTime::new(0, 0);
|
||||||
|
let mut scheduler = CcsdsScheduler::new(
|
||||||
|
unix_time,
|
||||||
|
Limits::new(Some(3), None),
|
||||||
|
Duration::from_millis(1000),
|
||||||
|
);
|
||||||
|
let test_tc_0 = test_tc(&[42], u14::ZERO);
|
||||||
|
let test_tc_0_raw = test_tc_0.to_vec();
|
||||||
|
let reader = CcsdsPacketReader::new(&test_tc_0_raw, Some(ChecksumType::WithCrc16)).unwrap();
|
||||||
|
let tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&test_tc_0);
|
||||||
|
scheduler
|
||||||
|
.insert_telecommand_with_reader(&reader, UnixTime::new(2, 0))
|
||||||
|
.unwrap();
|
||||||
|
scheduler
|
||||||
|
.insert_telecommand_with_reader(&reader, UnixTime::new(2, 0))
|
||||||
|
.unwrap();
|
||||||
|
scheduler
|
||||||
|
.insert_telecommand_with_reader(&reader, UnixTime::new(2, 0))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(scheduler.current_fill_count().packets, 3);
|
||||||
|
assert_eq!(
|
||||||
|
scheduler.insert_telecommand_with_reader(&reader, UnixTime::new(2, 0)),
|
||||||
|
Err(ScheduleError::PacketLimitReached)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
scheduler.insert_telecommand(
|
||||||
|
CcsdsSchedulePacketId::new(tc_id, reader.checksum()),
|
||||||
|
&test_tc_0_raw,
|
||||||
|
UnixTime::new(2, 0)
|
||||||
|
),
|
||||||
|
Err(ScheduleError::PacketLimitReached)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_byte_limit_reached() {
|
||||||
|
let unix_time = UnixTime::new(0, 0);
|
||||||
|
let test_tc_0 = test_tc(&[42], u14::ZERO);
|
||||||
|
let mut scheduler = CcsdsScheduler::new(
|
||||||
|
unix_time,
|
||||||
|
Limits::new(None, Some(test_tc_0.len_written() * 3)),
|
||||||
|
Duration::from_millis(1000),
|
||||||
|
);
|
||||||
|
let test_tc_0_raw = test_tc_0.to_vec();
|
||||||
|
let reader = CcsdsPacketReader::new(&test_tc_0_raw, Some(ChecksumType::WithCrc16)).unwrap();
|
||||||
|
let tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&test_tc_0);
|
||||||
|
scheduler
|
||||||
|
.insert_telecommand_with_reader(&reader, UnixTime::new(2, 0))
|
||||||
|
.unwrap();
|
||||||
|
scheduler
|
||||||
|
.insert_telecommand_with_reader(&reader, UnixTime::new(2, 0))
|
||||||
|
.unwrap();
|
||||||
|
scheduler
|
||||||
|
.insert_telecommand_with_reader(&reader, UnixTime::new(2, 0))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(scheduler.current_fill_count().packets, 3);
|
||||||
|
assert_eq!(
|
||||||
|
scheduler.insert_telecommand_with_reader(&reader, UnixTime::new(2, 0)),
|
||||||
|
Err(ScheduleError::ByteLimitReached)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
scheduler.insert_telecommand(
|
||||||
|
CcsdsSchedulePacketId::new(tc_id, reader.checksum()),
|
||||||
|
&test_tc_0_raw,
|
||||||
|
UnixTime::new(2, 0)
|
||||||
|
),
|
||||||
|
Err(ScheduleError::ByteLimitReached)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deletion_by_id() {
|
||||||
|
let unix_time = UnixTime::new(0, 0);
|
||||||
|
let mut scheduler = CcsdsScheduler::new(
|
||||||
|
unix_time,
|
||||||
|
Limits::new(Some(100), Some(1024)),
|
||||||
|
Duration::from_millis(1000),
|
||||||
|
);
|
||||||
|
let test_tc = test_tc(&[1, 2, 3], u14::ZERO);
|
||||||
|
let tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&test_tc);
|
||||||
|
let test_tc_raw = test_tc.to_vec();
|
||||||
|
let reader = CcsdsPacketReader::new(&test_tc_raw, Some(ChecksumType::WithCrc16)).unwrap();
|
||||||
|
let checksum = reader.checksum();
|
||||||
|
let id = CcsdsSchedulePacketId::new(tc_id, checksum);
|
||||||
|
scheduler
|
||||||
|
.insert_telecommand_with_reader(&reader, UnixTime::new(2, 0))
|
||||||
|
.unwrap();
|
||||||
|
scheduler.delete_by_id(&id);
|
||||||
|
assert_eq!(scheduler.current_fill_count().packets, 0);
|
||||||
|
assert_eq!(scheduler.current_fill_count().bytes, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deletion_by_window_0() {
|
||||||
|
let unix_time = UnixTime::new(0, 0);
|
||||||
|
let mut scheduler = CcsdsScheduler::new(
|
||||||
|
unix_time,
|
||||||
|
Limits::new(Some(100), Some(1024)),
|
||||||
|
Duration::from_millis(1000),
|
||||||
|
);
|
||||||
|
let test_tc_0 = test_tc(&[42], u14::ZERO);
|
||||||
|
let test_tc_1 = test_tc(&[1, 2, 3], u14::new(1));
|
||||||
|
let test_tc_2 = test_tc(&[1, 2, 3], u14::new(2));
|
||||||
|
let test_tc_0_raw = test_tc_0.to_vec();
|
||||||
|
let test_tc_1_raw = test_tc_1.to_vec();
|
||||||
|
let test_tc_2_raw = test_tc_2.to_vec();
|
||||||
|
let reader_0 =
|
||||||
|
CcsdsPacketReader::new(&test_tc_0_raw, Some(ChecksumType::WithCrc16)).unwrap();
|
||||||
|
let reader_1 =
|
||||||
|
CcsdsPacketReader::new(&test_tc_1_raw, Some(ChecksumType::WithCrc16)).unwrap();
|
||||||
|
let reader_2 =
|
||||||
|
CcsdsPacketReader::new(&test_tc_2_raw, Some(ChecksumType::WithCrc16)).unwrap();
|
||||||
|
scheduler
|
||||||
|
.insert_telecommand_with_reader(&reader_0, UnixTime::new(2, 0))
|
||||||
|
.unwrap();
|
||||||
|
scheduler
|
||||||
|
.insert_telecommand_with_reader(&reader_1, UnixTime::new(5, 0))
|
||||||
|
.unwrap();
|
||||||
|
scheduler
|
||||||
|
.insert_telecommand_with_reader(&reader_2, UnixTime::new(7, 0))
|
||||||
|
.unwrap();
|
||||||
|
let deleted = scheduler.delete_time_window(UnixTime::new(3, 0), UnixTime::new(6, 0));
|
||||||
|
assert!(deleted);
|
||||||
|
assert_eq!(scheduler.current_fill_count().packets, 2);
|
||||||
|
assert_eq!(
|
||||||
|
scheduler.current_fill_count().bytes,
|
||||||
|
test_tc_0_raw.len() + test_tc_2_raw.len()
|
||||||
|
);
|
||||||
|
scheduler.update_time(UnixTime::new(10, 0));
|
||||||
|
let mut index = 0;
|
||||||
|
scheduler.release_telecommands(|_id, packet| {
|
||||||
|
if index == 0 {
|
||||||
|
assert_eq!(packet, test_tc_0_raw);
|
||||||
|
} else {
|
||||||
|
assert_eq!(packet, test_tc_2_raw);
|
||||||
|
}
|
||||||
|
index += 1;
|
||||||
|
});
|
||||||
|
assert_eq!(scheduler.current_fill_count().packets, 0);
|
||||||
|
assert_eq!(scheduler.current_fill_count().bytes, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deletion_by_window_1() {
|
||||||
|
let unix_time = UnixTime::new(0, 0);
|
||||||
|
let mut scheduler = CcsdsScheduler::new(
|
||||||
|
unix_time,
|
||||||
|
Limits::new(Some(100), Some(1024)),
|
||||||
|
Duration::from_millis(1000),
|
||||||
|
);
|
||||||
|
let test_tc_0 = test_tc(&[42], u14::ZERO);
|
||||||
|
let test_tc_1 = test_tc(&[1, 2, 3], u14::new(1));
|
||||||
|
let test_tc_2 = test_tc(&[1, 2, 3], u14::new(2));
|
||||||
|
let test_tc_0_raw = test_tc_0.to_vec();
|
||||||
|
let test_tc_1_raw = test_tc_1.to_vec();
|
||||||
|
let test_tc_2_raw = test_tc_2.to_vec();
|
||||||
|
let reader_0 =
|
||||||
|
CcsdsPacketReader::new(&test_tc_0_raw, Some(ChecksumType::WithCrc16)).unwrap();
|
||||||
|
let reader_1 =
|
||||||
|
CcsdsPacketReader::new(&test_tc_1_raw, Some(ChecksumType::WithCrc16)).unwrap();
|
||||||
|
let reader_2 =
|
||||||
|
CcsdsPacketReader::new(&test_tc_2_raw, Some(ChecksumType::WithCrc16)).unwrap();
|
||||||
|
scheduler
|
||||||
|
.insert_telecommand_with_reader(&reader_0, UnixTime::new(2, 0))
|
||||||
|
.unwrap();
|
||||||
|
scheduler
|
||||||
|
.insert_telecommand_with_reader(&reader_1, UnixTime::new(5, 0))
|
||||||
|
.unwrap();
|
||||||
|
scheduler
|
||||||
|
.insert_telecommand_with_reader(&reader_2, UnixTime::new(7, 0))
|
||||||
|
.unwrap();
|
||||||
|
// This only deletes the first 2 TCs.
|
||||||
|
let deleted = scheduler.delete_time_window(UnixTime::new(2, 0), UnixTime::new(7, 0));
|
||||||
|
assert!(deleted);
|
||||||
|
assert_eq!(scheduler.current_fill_count().packets, 1);
|
||||||
|
assert_eq!(scheduler.current_fill_count().bytes, test_tc_2_raw.len());
|
||||||
|
scheduler.update_time(UnixTime::new(10, 0));
|
||||||
|
scheduler.release_telecommands(|_id, packet| {
|
||||||
|
assert_eq!(packet, test_tc_2_raw);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deletion_from_start() {
|
||||||
|
let unix_time = UnixTime::new(0, 0);
|
||||||
|
let mut scheduler = CcsdsScheduler::new(
|
||||||
|
unix_time,
|
||||||
|
Limits::new(Some(100), Some(1024)),
|
||||||
|
Duration::from_millis(1000),
|
||||||
|
);
|
||||||
|
let test_tc_0 = test_tc(&[42], u14::ZERO);
|
||||||
|
let test_tc_1 = test_tc(&[1, 2, 3], u14::new(1));
|
||||||
|
let test_tc_2 = test_tc(&[1, 2, 3], u14::new(2));
|
||||||
|
let test_tc_0_raw = test_tc_0.to_vec();
|
||||||
|
let test_tc_1_raw = test_tc_1.to_vec();
|
||||||
|
let test_tc_2_raw = test_tc_2.to_vec();
|
||||||
|
let reader_0 =
|
||||||
|
CcsdsPacketReader::new(&test_tc_0_raw, Some(ChecksumType::WithCrc16)).unwrap();
|
||||||
|
let reader_1 =
|
||||||
|
CcsdsPacketReader::new(&test_tc_1_raw, Some(ChecksumType::WithCrc16)).unwrap();
|
||||||
|
let reader_2 =
|
||||||
|
CcsdsPacketReader::new(&test_tc_2_raw, Some(ChecksumType::WithCrc16)).unwrap();
|
||||||
|
scheduler
|
||||||
|
.insert_telecommand_with_reader(&reader_0, UnixTime::new(2, 0))
|
||||||
|
.unwrap();
|
||||||
|
scheduler
|
||||||
|
.insert_telecommand_with_reader(&reader_1, UnixTime::new(5, 0))
|
||||||
|
.unwrap();
|
||||||
|
scheduler
|
||||||
|
.insert_telecommand_with_reader(&reader_2, UnixTime::new(7, 0))
|
||||||
|
.unwrap();
|
||||||
|
// This only deletes the first 2 TCs.
|
||||||
|
let deleted = scheduler.delete_starting_at(UnixTime::new(5, 0));
|
||||||
|
assert!(deleted);
|
||||||
|
assert_eq!(scheduler.current_fill_count().packets, 1);
|
||||||
|
assert_eq!(scheduler.current_fill_count().bytes, test_tc_0_raw.len());
|
||||||
|
scheduler.update_time(UnixTime::new(10, 0));
|
||||||
|
scheduler.release_telecommands(|_id, packet| {
|
||||||
|
assert_eq!(packet, test_tc_0_raw);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deletion_until_end() {
|
||||||
|
let unix_time = UnixTime::new(0, 0);
|
||||||
|
let mut scheduler = CcsdsScheduler::new(
|
||||||
|
unix_time,
|
||||||
|
Limits::new(Some(100), Some(1024)),
|
||||||
|
Duration::from_millis(1000),
|
||||||
|
);
|
||||||
|
let test_tc_0 = test_tc(&[42], u14::ZERO);
|
||||||
|
let test_tc_1 = test_tc(&[1, 2, 3], u14::new(1));
|
||||||
|
let test_tc_2 = test_tc(&[1, 2, 3], u14::new(2));
|
||||||
|
let test_tc_0_raw = test_tc_0.to_vec();
|
||||||
|
let test_tc_1_raw = test_tc_1.to_vec();
|
||||||
|
let test_tc_2_raw = test_tc_2.to_vec();
|
||||||
|
let reader_0 =
|
||||||
|
CcsdsPacketReader::new(&test_tc_0_raw, Some(ChecksumType::WithCrc16)).unwrap();
|
||||||
|
let reader_1 =
|
||||||
|
CcsdsPacketReader::new(&test_tc_1_raw, Some(ChecksumType::WithCrc16)).unwrap();
|
||||||
|
let reader_2 =
|
||||||
|
CcsdsPacketReader::new(&test_tc_2_raw, Some(ChecksumType::WithCrc16)).unwrap();
|
||||||
|
scheduler
|
||||||
|
.insert_telecommand_with_reader(&reader_0, UnixTime::new(2, 0))
|
||||||
|
.unwrap();
|
||||||
|
scheduler
|
||||||
|
.insert_telecommand_with_reader(&reader_1, UnixTime::new(5, 0))
|
||||||
|
.unwrap();
|
||||||
|
scheduler
|
||||||
|
.insert_telecommand_with_reader(&reader_2, UnixTime::new(7, 0))
|
||||||
|
.unwrap();
|
||||||
|
// This only deletes the first 2 TCs.
|
||||||
|
let deleted = scheduler.delete_before(UnixTime::new(7, 0));
|
||||||
|
assert!(deleted);
|
||||||
|
assert_eq!(scheduler.current_fill_count().packets, 1);
|
||||||
|
assert_eq!(scheduler.current_fill_count().bytes, test_tc_2_raw.len());
|
||||||
|
scheduler.update_time(UnixTime::new(10, 0));
|
||||||
|
scheduler.release_telecommands(|_id, packet| {
|
||||||
|
assert_eq!(packet, test_tc_2_raw);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,448 +0,0 @@
|
|||||||
use crate::{
|
|
||||||
ComponentId,
|
|
||||||
mode::{ModeAndSubmode, ModeReply, ModeRequest, ModeRequestSender},
|
|
||||||
mode_tree::{ModeStoreProvider, ModeStoreVec},
|
|
||||||
queue::{GenericSendError, GenericTargetedMessagingError},
|
|
||||||
request::{GenericMessage, RequestId},
|
|
||||||
};
|
|
||||||
use core::fmt::Debug;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub struct ActiveModeCommandContext {
|
|
||||||
pub target_mode: ModeAndSubmode,
|
|
||||||
pub active_request_id: RequestId,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default, PartialEq, Eq)]
|
|
||||||
pub enum DevManagerHelperResult {
|
|
||||||
#[default]
|
|
||||||
Idle,
|
|
||||||
Busy,
|
|
||||||
ModeCommandingDone(ActiveModeCommandContext),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum DevManagerHelperError {
|
|
||||||
ChildNotInStore,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait DevManagerUserHook: Debug {
|
|
||||||
fn send_mode_cmd_to_child(
|
|
||||||
&self,
|
|
||||||
request_id: RequestId,
|
|
||||||
target_id: ComponentId,
|
|
||||||
mode: ModeAndSubmode,
|
|
||||||
forced: bool,
|
|
||||||
children_mode_store: &mut ModeStoreVec,
|
|
||||||
mode_req_sender: &impl ModeRequestSender,
|
|
||||||
) -> Result<(), GenericSendError>;
|
|
||||||
|
|
||||||
fn send_mode_cmds_to_children(
|
|
||||||
&self,
|
|
||||||
request_id: RequestId,
|
|
||||||
commanded_parent_mode: ModeAndSubmode,
|
|
||||||
forced: bool,
|
|
||||||
children_mode_store: &mut ModeStoreVec,
|
|
||||||
mode_req_sender: &impl ModeRequestSender,
|
|
||||||
) -> Result<(), GenericSendError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct TransparentDevManagerHook {}
|
|
||||||
|
|
||||||
impl DevManagerUserHook for TransparentDevManagerHook {
|
|
||||||
fn send_mode_cmds_to_children(
|
|
||||||
&self,
|
|
||||||
request_id: RequestId,
|
|
||||||
commanded_parent_mode: ModeAndSubmode,
|
|
||||||
forced: bool,
|
|
||||||
children_mode_store: &mut ModeStoreVec,
|
|
||||||
mode_req_sender: &impl ModeRequestSender,
|
|
||||||
) -> Result<(), GenericSendError> {
|
|
||||||
for child in children_mode_store {
|
|
||||||
mode_req_sender.send_mode_request(
|
|
||||||
request_id,
|
|
||||||
child.id(),
|
|
||||||
ModeRequest::SetMode {
|
|
||||||
mode_and_submode: commanded_parent_mode,
|
|
||||||
forced,
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
child.awaiting_reply = true;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send_mode_cmd_to_child(
|
|
||||||
&self,
|
|
||||||
request_id: RequestId,
|
|
||||||
target_id: ComponentId,
|
|
||||||
mode: ModeAndSubmode,
|
|
||||||
forced: bool,
|
|
||||||
children_mode_store: &mut ModeStoreVec,
|
|
||||||
mode_req_sender: &impl ModeRequestSender,
|
|
||||||
) -> Result<(), GenericSendError> {
|
|
||||||
let mut_val = children_mode_store
|
|
||||||
.get_mut(target_id)
|
|
||||||
.ok_or(GenericSendError::TargetDoesNotExist(target_id))?;
|
|
||||||
mut_val.awaiting_reply = true;
|
|
||||||
mode_req_sender.send_mode_request(
|
|
||||||
request_id,
|
|
||||||
target_id,
|
|
||||||
ModeRequest::SetMode {
|
|
||||||
mode_and_submode: mode,
|
|
||||||
forced,
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum DevManagerCommandingState {
|
|
||||||
#[default]
|
|
||||||
Idle,
|
|
||||||
AwaitingReplies(ActiveModeCommandContext),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DevManagerCommandingState {
|
|
||||||
fn new_active_cmd(mode_and_submode: ModeAndSubmode, active_request_id: RequestId) -> Self {
|
|
||||||
DevManagerCommandingState::AwaitingReplies(ActiveModeCommandContext {
|
|
||||||
target_mode: mode_and_submode,
|
|
||||||
active_request_id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A generic helper for manager components which manage child components in a mode tree.
|
|
||||||
///
|
|
||||||
/// Mode commands are usually forwarded to all children components transparently.
|
|
||||||
/// For example, this could be used in an Assembly component which manages multiple redundant
|
|
||||||
/// child components. It can also be used inside a manager component which only manages one device.
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct DevManagerCommandingHelper<UserHook: DevManagerUserHook> {
|
|
||||||
/// The IDs, modes and reply awaition status of all children are tracked in this data
|
|
||||||
/// structure.
|
|
||||||
pub children_mode_store: ModeStoreVec,
|
|
||||||
pub user_hook: UserHook,
|
|
||||||
pub state: DevManagerCommandingState,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<UserHook: DevManagerUserHook> DevManagerCommandingHelper<UserHook> {
|
|
||||||
pub fn new(user_hook: UserHook) -> Self {
|
|
||||||
Self {
|
|
||||||
children_mode_store: Default::default(),
|
|
||||||
user_hook,
|
|
||||||
state: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn send_mode_cmd_to_one_child(
|
|
||||||
&mut self,
|
|
||||||
request_id: RequestId,
|
|
||||||
target_id: ComponentId,
|
|
||||||
mode_and_submode: ModeAndSubmode,
|
|
||||||
forced: bool,
|
|
||||||
mode_req_sender: &impl ModeRequestSender,
|
|
||||||
) -> Result<(), GenericSendError> {
|
|
||||||
self.state = DevManagerCommandingState::new_active_cmd(mode_and_submode, request_id);
|
|
||||||
self.user_hook.send_mode_cmd_to_child(
|
|
||||||
request_id,
|
|
||||||
target_id,
|
|
||||||
mode_and_submode,
|
|
||||||
forced,
|
|
||||||
&mut self.children_mode_store,
|
|
||||||
mode_req_sender,
|
|
||||||
)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn send_mode_cmd_to_all_children(
|
|
||||||
&mut self,
|
|
||||||
request_id: RequestId,
|
|
||||||
mode_and_submode: ModeAndSubmode,
|
|
||||||
forced: bool,
|
|
||||||
mode_req_sender: &impl ModeRequestSender,
|
|
||||||
) -> Result<(), GenericSendError> {
|
|
||||||
self.state = DevManagerCommandingState::new_active_cmd(mode_and_submode, request_id);
|
|
||||||
self.user_hook.send_mode_cmds_to_children(
|
|
||||||
request_id,
|
|
||||||
mode_and_submode,
|
|
||||||
forced,
|
|
||||||
&mut self.children_mode_store,
|
|
||||||
mode_req_sender,
|
|
||||||
)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn target_mode(&self) -> Option<ModeAndSubmode> {
|
|
||||||
match self.state {
|
|
||||||
DevManagerCommandingState::Idle => None,
|
|
||||||
DevManagerCommandingState::AwaitingReplies(context) => Some(context.target_mode),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn state(&self) -> DevManagerCommandingState {
|
|
||||||
self.state
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn send_announce_mode_cmd_to_children(
|
|
||||||
&self,
|
|
||||||
request_id: RequestId,
|
|
||||||
mode_req_sender: &impl ModeRequestSender,
|
|
||||||
recursive: bool,
|
|
||||||
) -> Result<(), GenericTargetedMessagingError> {
|
|
||||||
let mut request = ModeRequest::AnnounceMode;
|
|
||||||
if recursive {
|
|
||||||
request = ModeRequest::AnnounceModeRecursive;
|
|
||||||
}
|
|
||||||
for child in self.children_mode_store.0.iter() {
|
|
||||||
mode_req_sender.send_mode_request(request_id, child.id(), request)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_mode_child(&mut self, target_id: ComponentId, mode: ModeAndSubmode) {
|
|
||||||
self.children_mode_store.add_component(target_id, mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper method which counts the number of children which have a certain mode.
|
|
||||||
pub fn count_number_of_children_with_mode(&self, mode_and_submode: ModeAndSubmode) -> usize {
|
|
||||||
let mut children_in_target_mode = 0;
|
|
||||||
for child in &self.children_mode_store {
|
|
||||||
if child.mode_and_submode() == mode_and_submode {
|
|
||||||
children_in_target_mode += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
children_in_target_mode
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_mode_reply(
|
|
||||||
&mut self,
|
|
||||||
mode_reply: &GenericMessage<ModeReply>,
|
|
||||||
) -> Result<DevManagerHelperResult, DevManagerHelperError> {
|
|
||||||
let context = match self.state {
|
|
||||||
DevManagerCommandingState::Idle => return Ok(DevManagerHelperResult::Idle),
|
|
||||||
DevManagerCommandingState::AwaitingReplies(active_mode_command_context) => {
|
|
||||||
Some(active_mode_command_context)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if !self
|
|
||||||
.children_mode_store
|
|
||||||
.has_component(mode_reply.sender_id())
|
|
||||||
{
|
|
||||||
return Err(DevManagerHelperError::ChildNotInStore);
|
|
||||||
}
|
|
||||||
let mut generic_mode_reply_handler = |mode_and_submode: Option<ModeAndSubmode>| {
|
|
||||||
// Tying the reply awaition to the request ID ensures that something like replies
|
|
||||||
// belonging to older requests do not interfere with the completion handling of
|
|
||||||
// the mode commanding. This is important for forced mode commands.
|
|
||||||
let mut handle_awaition = false;
|
|
||||||
if let DevManagerCommandingState::AwaitingReplies { .. } = self.state {
|
|
||||||
handle_awaition = true;
|
|
||||||
}
|
|
||||||
let still_awating_replies = self.children_mode_store.mode_reply_handler(
|
|
||||||
mode_reply.sender_id(),
|
|
||||||
mode_and_submode,
|
|
||||||
handle_awaition,
|
|
||||||
);
|
|
||||||
// It is okay to unwrap: If awaition should be handled, the returned value should
|
|
||||||
// always be some valid value.
|
|
||||||
if handle_awaition && !still_awating_replies.unwrap() {
|
|
||||||
self.state = DevManagerCommandingState::Idle;
|
|
||||||
return Ok(DevManagerHelperResult::ModeCommandingDone(context.unwrap()));
|
|
||||||
}
|
|
||||||
Ok(DevManagerHelperResult::Busy)
|
|
||||||
};
|
|
||||||
match mode_reply.message {
|
|
||||||
ModeReply::ModeInfo(mode_and_submode) | ModeReply::ModeReply(mode_and_submode) => {
|
|
||||||
generic_mode_reply_handler(Some(mode_and_submode))
|
|
||||||
}
|
|
||||||
ModeReply::CantReachMode(_result_u16) => generic_mode_reply_handler(None),
|
|
||||||
ModeReply::WrongMode {
|
|
||||||
expected: _,
|
|
||||||
reached,
|
|
||||||
} => generic_mode_reply_handler(Some(reached)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use crate::{
|
|
||||||
mode::{UNKNOWN_MODE, tests::ModeReqSenderMock},
|
|
||||||
request::MessageMetadata,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
pub enum ExampleId {
|
|
||||||
Id1 = 1,
|
|
||||||
Id2 = 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum ExampleMode {
|
|
||||||
Mode1 = 1,
|
|
||||||
Mode2 = 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_basic() {
|
|
||||||
let assy_helper = DevManagerCommandingHelper::new(TransparentDevManagerHook::default());
|
|
||||||
assert_eq!(assy_helper.state(), DevManagerCommandingState::Idle);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_mode_announce() {
|
|
||||||
let mut assy_helper = DevManagerCommandingHelper::new(TransparentDevManagerHook::default());
|
|
||||||
let mode_req_sender = ModeReqSenderMock::default();
|
|
||||||
assy_helper.add_mode_child(ExampleId::Id1 as ComponentId, UNKNOWN_MODE);
|
|
||||||
assy_helper.add_mode_child(ExampleId::Id2 as ComponentId, UNKNOWN_MODE);
|
|
||||||
assy_helper
|
|
||||||
.send_announce_mode_cmd_to_children(1, &mode_req_sender, false)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(mode_req_sender.requests.borrow().len(), 2);
|
|
||||||
let mut req = mode_req_sender.requests.borrow_mut().pop_front().unwrap();
|
|
||||||
assert_eq!(req.target_id, ExampleId::Id1 as ComponentId);
|
|
||||||
assert_eq!(req.request_id, 1);
|
|
||||||
assert_eq!(req.request, ModeRequest::AnnounceMode);
|
|
||||||
req = mode_req_sender.requests.borrow_mut().pop_front().unwrap();
|
|
||||||
assert_eq!(req.target_id, ExampleId::Id2 as ComponentId);
|
|
||||||
assert_eq!(req.request_id, 1);
|
|
||||||
assert_eq!(req.request, ModeRequest::AnnounceMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_mode_announce_recursive() {
|
|
||||||
let mut assy_helper = DevManagerCommandingHelper::new(TransparentDevManagerHook::default());
|
|
||||||
let mode_req_sender = ModeReqSenderMock::default();
|
|
||||||
assy_helper.add_mode_child(ExampleId::Id1 as ComponentId, UNKNOWN_MODE);
|
|
||||||
assy_helper.add_mode_child(ExampleId::Id2 as ComponentId, UNKNOWN_MODE);
|
|
||||||
assy_helper
|
|
||||||
.send_announce_mode_cmd_to_children(1, &mode_req_sender, true)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(mode_req_sender.requests.borrow().len(), 2);
|
|
||||||
let mut req = mode_req_sender.requests.borrow_mut().pop_front().unwrap();
|
|
||||||
assert_eq!(req.target_id, ExampleId::Id1 as ComponentId);
|
|
||||||
assert_eq!(req.request_id, 1);
|
|
||||||
assert_eq!(req.request, ModeRequest::AnnounceModeRecursive);
|
|
||||||
req = mode_req_sender.requests.borrow_mut().pop_front().unwrap();
|
|
||||||
assert_eq!(req.target_id, ExampleId::Id2 as ComponentId);
|
|
||||||
assert_eq!(req.request_id, 1);
|
|
||||||
assert_eq!(req.request, ModeRequest::AnnounceModeRecursive);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_mode_commanding_one_child() {
|
|
||||||
let mut dev_mgmt_helper =
|
|
||||||
DevManagerCommandingHelper::new(TransparentDevManagerHook::default());
|
|
||||||
let mode_req_sender = ModeReqSenderMock::default();
|
|
||||||
dev_mgmt_helper.add_mode_child(ExampleId::Id1 as ComponentId, UNKNOWN_MODE);
|
|
||||||
let expected_mode = ModeAndSubmode::new(ExampleMode::Mode1 as u32, 0);
|
|
||||||
dev_mgmt_helper
|
|
||||||
.send_mode_cmd_to_one_child(
|
|
||||||
1,
|
|
||||||
ExampleId::Id1 as ComponentId,
|
|
||||||
expected_mode,
|
|
||||||
false,
|
|
||||||
&mode_req_sender,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(mode_req_sender.requests.borrow().len(), 1);
|
|
||||||
let req = mode_req_sender.requests.borrow_mut().pop_front().unwrap();
|
|
||||||
assert_eq!(req.target_id, ExampleId::Id1 as ComponentId);
|
|
||||||
assert_eq!(req.request_id, 1);
|
|
||||||
assert_eq!(
|
|
||||||
req.request,
|
|
||||||
ModeRequest::SetMode {
|
|
||||||
mode_and_submode: expected_mode,
|
|
||||||
forced: false
|
|
||||||
}
|
|
||||||
);
|
|
||||||
matches!(
|
|
||||||
dev_mgmt_helper.state(),
|
|
||||||
DevManagerCommandingState::AwaitingReplies { .. }
|
|
||||||
);
|
|
||||||
if let DevManagerCommandingState::AwaitingReplies(ctx) = dev_mgmt_helper.state() {
|
|
||||||
assert_eq!(ctx.target_mode, expected_mode);
|
|
||||||
assert_eq!(ctx.active_request_id, 1);
|
|
||||||
}
|
|
||||||
let reply = GenericMessage::new(
|
|
||||||
MessageMetadata::new(1, ExampleId::Id1 as ComponentId),
|
|
||||||
ModeReply::ModeReply(expected_mode),
|
|
||||||
);
|
|
||||||
if let DevManagerHelperResult::ModeCommandingDone(ActiveModeCommandContext {
|
|
||||||
target_mode,
|
|
||||||
active_request_id,
|
|
||||||
}) = dev_mgmt_helper.handle_mode_reply(&reply).unwrap()
|
|
||||||
{
|
|
||||||
assert_eq!(target_mode, expected_mode);
|
|
||||||
assert_eq!(active_request_id, 1);
|
|
||||||
}
|
|
||||||
matches!(dev_mgmt_helper.state(), DevManagerCommandingState::Idle);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_mode_commanding_multi_child() {
|
|
||||||
let mut dev_mgmt_helper =
|
|
||||||
DevManagerCommandingHelper::new(TransparentDevManagerHook::default());
|
|
||||||
let mode_req_sender = ModeReqSenderMock::default();
|
|
||||||
dev_mgmt_helper.add_mode_child(ExampleId::Id1 as ComponentId, UNKNOWN_MODE);
|
|
||||||
dev_mgmt_helper.add_mode_child(ExampleId::Id2 as ComponentId, UNKNOWN_MODE);
|
|
||||||
let expected_mode = ModeAndSubmode::new(ExampleMode::Mode2 as u32, 0);
|
|
||||||
dev_mgmt_helper
|
|
||||||
.send_mode_cmd_to_all_children(1, expected_mode, false, &mode_req_sender)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(mode_req_sender.requests.borrow().len(), 2);
|
|
||||||
let req = mode_req_sender.requests.borrow_mut().pop_front().unwrap();
|
|
||||||
assert_eq!(req.target_id, ExampleId::Id1 as ComponentId);
|
|
||||||
assert_eq!(req.request_id, 1);
|
|
||||||
assert_eq!(
|
|
||||||
req.request,
|
|
||||||
ModeRequest::SetMode {
|
|
||||||
mode_and_submode: expected_mode,
|
|
||||||
forced: false
|
|
||||||
}
|
|
||||||
);
|
|
||||||
let req = mode_req_sender.requests.borrow_mut().pop_front().unwrap();
|
|
||||||
assert_eq!(req.target_id, ExampleId::Id2 as ComponentId);
|
|
||||||
assert_eq!(req.request_id, 1);
|
|
||||||
assert_eq!(
|
|
||||||
req.request,
|
|
||||||
ModeRequest::SetMode {
|
|
||||||
mode_and_submode: expected_mode,
|
|
||||||
forced: false
|
|
||||||
}
|
|
||||||
);
|
|
||||||
matches!(
|
|
||||||
dev_mgmt_helper.state(),
|
|
||||||
DevManagerCommandingState::AwaitingReplies { .. }
|
|
||||||
);
|
|
||||||
if let DevManagerCommandingState::AwaitingReplies(ctx) = dev_mgmt_helper.state() {
|
|
||||||
assert_eq!(ctx.target_mode, expected_mode);
|
|
||||||
assert_eq!(ctx.active_request_id, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
let reply = GenericMessage::new(
|
|
||||||
MessageMetadata::new(1, ExampleId::Id1 as ComponentId),
|
|
||||||
ModeReply::ModeReply(expected_mode),
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
dev_mgmt_helper.handle_mode_reply(&reply).unwrap(),
|
|
||||||
DevManagerHelperResult::Busy
|
|
||||||
);
|
|
||||||
let reply = GenericMessage::new(
|
|
||||||
MessageMetadata::new(1, ExampleId::Id2 as ComponentId),
|
|
||||||
ModeReply::ModeReply(expected_mode),
|
|
||||||
);
|
|
||||||
if let DevManagerHelperResult::ModeCommandingDone(ActiveModeCommandContext {
|
|
||||||
target_mode,
|
|
||||||
active_request_id,
|
|
||||||
}) = dev_mgmt_helper.handle_mode_reply(&reply).unwrap()
|
|
||||||
{
|
|
||||||
assert_eq!(target_mode, expected_mode);
|
|
||||||
assert_eq!(active_request_id, 1);
|
|
||||||
}
|
|
||||||
matches!(dev_mgmt_helper.state(), DevManagerCommandingState::Idle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
use crate::ComponentId;
|
|
||||||
|
|
||||||
pub type CollectionIntervalFactor = u32;
|
|
||||||
/// Unique Identifier for a certain housekeeping dataset.
|
|
||||||
pub type UniqueId = u32;
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
|
||||||
pub struct HkRequest {
|
|
||||||
pub unique_id: UniqueId,
|
|
||||||
pub variant: HkRequestVariant,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HkRequest {
|
|
||||||
pub fn new(unique_id: UniqueId, variant: HkRequestVariant) -> Self {
|
|
||||||
Self { unique_id, variant }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
|
||||||
pub enum HkRequestVariant {
|
|
||||||
OneShot,
|
|
||||||
EnablePeriodic,
|
|
||||||
DisablePeriodic,
|
|
||||||
ModifyCollectionInterval(CollectionIntervalFactor),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
|
||||||
pub struct TargetedHkRequest {
|
|
||||||
pub target_id: ComponentId,
|
|
||||||
pub hk_request: HkRequestVariant,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TargetedHkRequest {
|
|
||||||
pub fn new(target_id: ComponentId, hk_request: HkRequestVariant) -> Self {
|
|
||||||
Self {
|
|
||||||
target_id,
|
|
||||||
hk_request,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
//! show how the event management modules can be integrated into a more complex software.
|
//! show how the event management modules can be integrated into a more complex software.
|
||||||
use crate::{
|
use crate::{
|
||||||
ComponentId,
|
ComponentId,
|
||||||
events::{Event, EventId, GroupId},
|
legacy::events::{Event, EventId, GroupId},
|
||||||
queue::GenericSendError,
|
queue::GenericSendError,
|
||||||
};
|
};
|
||||||
use core::marker::PhantomData;
|
use core::marker::PhantomData;
|
||||||
@@ -96,6 +96,7 @@ impl<EventInstance: Event> EventMessage<EventInstance> {
|
|||||||
pub trait EventSender<EventInstance: Event> {
|
pub trait EventSender<EventInstance: Event> {
|
||||||
type Error;
|
type Error;
|
||||||
|
|
||||||
|
/// Destination component ID.
|
||||||
fn target_id(&self) -> ComponentId;
|
fn target_id(&self) -> ComponentId;
|
||||||
|
|
||||||
fn send(&self, message: EventMessage<EventInstance>) -> Result<(), Self::Error>;
|
fn send(&self, message: EventMessage<EventInstance>) -> Result<(), Self::Error>;
|
||||||
@@ -317,7 +318,7 @@ pub mod alloc_mod {
|
|||||||
use alloc::vec::Vec;
|
use alloc::vec::Vec;
|
||||||
use hashbrown::HashMap;
|
use hashbrown::HashMap;
|
||||||
|
|
||||||
use crate::events::EventErasedAlloc;
|
use crate::legacy::events::EventErasedAlloc;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
@@ -460,7 +461,7 @@ pub mod alloc_mod {
|
|||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
pub mod std_mod {
|
pub mod std_mod {
|
||||||
use crate::{
|
use crate::{
|
||||||
events::{EventErasedAlloc, EventErasedHeapless},
|
legacy::events::{EventErasedAlloc, EventErasedHeapless},
|
||||||
queue::GenericReceiveError,
|
queue::GenericReceiveError,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -577,7 +578,7 @@ mod tests {
|
|||||||
use arbitrary_int::u14;
|
use arbitrary_int::u14;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::events::{EventErasedAlloc, Severity};
|
use crate::legacy::events::{EventErasedAlloc, Severity};
|
||||||
use crate::pus::test_util::{TEST_COMPONENT_ID_0, TEST_COMPONENT_ID_1};
|
use crate::pus::test_util::{TEST_COMPONENT_ID_0, TEST_COMPONENT_ID_1};
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
//! The [PUS event](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example/src/pus/event.rs)
|
//! The [PUS event](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example/src/pus/event.rs)
|
||||||
//! module and the generic [events module](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example/src/events.rs)
|
//! module and the generic [events module](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example/src/events.rs)
|
||||||
//! show how the event management modules can be integrated into a more complex software.
|
//! show how the event management modules can be integrated into a more complex software.
|
||||||
use crate::events_legacy::{EventU16, EventU32, GenericEvent, LargestEventRaw, LargestGroupIdRaw};
|
use crate::legacy::events_legacy::{EventU16, EventU32, GenericEvent, LargestEventRaw, LargestGroupIdRaw};
|
||||||
use crate::params::Params;
|
use crate::params::Params;
|
||||||
use crate::queue::GenericSendError;
|
use crate::queue::GenericSendError;
|
||||||
use core::fmt::Debug;
|
use core::fmt::Debug;
|
||||||
@@ -588,8 +588,8 @@ pub mod std_mod {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::event_man_legacy::EventManager;
|
use crate::legacy::event_man_legacy::EventManager;
|
||||||
use crate::events_legacy::{EventU32, GenericEvent, Severity};
|
use crate::legacy::events_legacy::{EventU32, GenericEvent, Severity};
|
||||||
use crate::params::{ParamsHeapless, ParamsRaw};
|
use crate::params::{ParamsHeapless, ParamsRaw};
|
||||||
use crate::pus::test_util::{TEST_COMPONENT_ID_0, TEST_COMPONENT_ID_1};
|
use crate::pus::test_util::{TEST_COMPONENT_ID_0, TEST_COMPONENT_ID_1};
|
||||||
use std::format;
|
use std::format;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
pub mod event_man;
|
||||||
|
pub mod events;
|
||||||
|
|
||||||
|
pub mod pus;
|
||||||
@@ -267,217 +267,4 @@ mod alloc_mod {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {}
|
||||||
use super::*;
|
|
||||||
use crate::ComponentId;
|
|
||||||
use crate::events_legacy::{EventU32, Severity};
|
|
||||||
use crate::pus::test_util::TEST_COMPONENT_ID_0;
|
|
||||||
use crate::pus::tests::CommonTmInfo;
|
|
||||||
use crate::pus::{ChannelWithId, EcssTmSender, EcssTmtcError, PusTmVariant};
|
|
||||||
use spacepackets::ByteConversionError;
|
|
||||||
use spacepackets::ecss::PusError;
|
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::collections::VecDeque;
|
|
||||||
use std::vec::Vec;
|
|
||||||
|
|
||||||
const EXAMPLE_APID: u11 = u11::new(0xee);
|
|
||||||
const EXAMPLE_GROUP_ID: u16 = 2;
|
|
||||||
const EXAMPLE_EVENT_ID_0: u16 = 1;
|
|
||||||
#[allow(dead_code)]
|
|
||||||
const EXAMPLE_EVENT_ID_1: u16 = 2;
|
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
|
||||||
struct TmInfo {
|
|
||||||
pub sender_id: ComponentId,
|
|
||||||
pub common: CommonTmInfo,
|
|
||||||
pub event: EventU32,
|
|
||||||
pub aux_data: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
|
||||||
struct TestSender {
|
|
||||||
pub service_queue: RefCell<VecDeque<TmInfo>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ChannelWithId for TestSender {
|
|
||||||
fn id(&self) -> ComponentId {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EcssTmSender for TestSender {
|
|
||||||
fn send_tm(&self, sender_id: ComponentId, tm: PusTmVariant) -> Result<(), EcssTmtcError> {
|
|
||||||
match tm {
|
|
||||||
PusTmVariant::InStore(_) => {
|
|
||||||
panic!("TestSender: unexpected call with address");
|
|
||||||
}
|
|
||||||
PusTmVariant::Direct(tm) => {
|
|
||||||
assert!(!tm.source_data().is_empty());
|
|
||||||
let src_data = tm.source_data();
|
|
||||||
assert!(src_data.len() >= 4);
|
|
||||||
let event =
|
|
||||||
EventU32::from(u32::from_be_bytes(src_data[0..4].try_into().unwrap()));
|
|
||||||
let mut aux_data = Vec::new();
|
|
||||||
if src_data.len() > 4 {
|
|
||||||
aux_data.extend_from_slice(&src_data[4..]);
|
|
||||||
}
|
|
||||||
self.service_queue.borrow_mut().push_back(TmInfo {
|
|
||||||
sender_id,
|
|
||||||
common: CommonTmInfo::new_from_tm(&tm),
|
|
||||||
event,
|
|
||||||
aux_data,
|
|
||||||
});
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn severity_to_subservice(severity: Severity) -> MessageSubtypeId {
|
|
||||||
match severity {
|
|
||||||
Severity::Info => MessageSubtypeId::TmInfoReport,
|
|
||||||
Severity::Low => MessageSubtypeId::TmLowSeverityReport,
|
|
||||||
Severity::Medium => MessageSubtypeId::TmMediumSeverityReport,
|
|
||||||
Severity::High => MessageSubtypeId::TmHighSeverityReport,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn report_basic_event(
|
|
||||||
reporter: &mut EventReporter,
|
|
||||||
sender: &mut TestSender,
|
|
||||||
time_stamp: &[u8],
|
|
||||||
event: EventU32,
|
|
||||||
severity: Severity,
|
|
||||||
aux_data: Option<&[u8]>,
|
|
||||||
) {
|
|
||||||
match severity {
|
|
||||||
Severity::Info => {
|
|
||||||
reporter
|
|
||||||
.event_info(sender, time_stamp, event, aux_data)
|
|
||||||
.expect("Error reporting info event");
|
|
||||||
}
|
|
||||||
Severity::Low => {
|
|
||||||
reporter
|
|
||||||
.event_low_severity(sender, time_stamp, event, aux_data)
|
|
||||||
.expect("Error reporting low event");
|
|
||||||
}
|
|
||||||
Severity::Medium => {
|
|
||||||
reporter
|
|
||||||
.event_medium_severity(sender, time_stamp, event, aux_data)
|
|
||||||
.expect("Error reporting medium event");
|
|
||||||
}
|
|
||||||
Severity::High => {
|
|
||||||
reporter
|
|
||||||
.event_high_severity(sender, time_stamp, event, aux_data)
|
|
||||||
.expect("Error reporting high event");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn basic_event_test(
|
|
||||||
max_event_aux_data_buf: usize,
|
|
||||||
severity: Severity,
|
|
||||||
error_data: Option<&[u8]>,
|
|
||||||
) {
|
|
||||||
let mut sender = TestSender::default();
|
|
||||||
let mut reporter = EventReporter::new(
|
|
||||||
TEST_COMPONENT_ID_0.id(),
|
|
||||||
EXAMPLE_APID,
|
|
||||||
0,
|
|
||||||
max_event_aux_data_buf,
|
|
||||||
);
|
|
||||||
let time_stamp_empty: [u8; 7] = [0; 7];
|
|
||||||
let mut error_copy = Vec::new();
|
|
||||||
if let Some(err_data) = error_data {
|
|
||||||
error_copy.extend_from_slice(err_data);
|
|
||||||
}
|
|
||||||
let event = EventU32::new_checked(severity, EXAMPLE_GROUP_ID, EXAMPLE_EVENT_ID_0)
|
|
||||||
.expect("Error creating example event");
|
|
||||||
report_basic_event(
|
|
||||||
&mut reporter,
|
|
||||||
&mut sender,
|
|
||||||
&time_stamp_empty,
|
|
||||||
event,
|
|
||||||
severity,
|
|
||||||
error_data,
|
|
||||||
);
|
|
||||||
let mut service_queue = sender.service_queue.borrow_mut();
|
|
||||||
assert_eq!(service_queue.len(), 1);
|
|
||||||
let tm_info = service_queue.pop_front().unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
tm_info.common.subservice,
|
|
||||||
severity_to_subservice(severity) as u8
|
|
||||||
);
|
|
||||||
assert_eq!(tm_info.common.dest_id, 0);
|
|
||||||
assert_eq!(tm_info.common.timestamp, time_stamp_empty);
|
|
||||||
assert_eq!(tm_info.common.msg_counter, 0);
|
|
||||||
assert_eq!(tm_info.common.apid, EXAMPLE_APID);
|
|
||||||
assert_eq!(tm_info.event, event);
|
|
||||||
assert_eq!(tm_info.sender_id, TEST_COMPONENT_ID_0.id());
|
|
||||||
assert_eq!(tm_info.aux_data, error_copy);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn basic_info_event_generation() {
|
|
||||||
basic_event_test(4, Severity::Info, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn basic_low_severity_event() {
|
|
||||||
basic_event_test(4, Severity::Low, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn basic_medium_severity_event() {
|
|
||||||
basic_event_test(4, Severity::Medium, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn basic_high_severity_event() {
|
|
||||||
basic_event_test(4, Severity::High, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn event_with_info_string() {
|
|
||||||
let info_string = "Test Information";
|
|
||||||
basic_event_test(32, Severity::Info, Some(info_string.as_bytes()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn low_severity_with_raw_err_data() {
|
|
||||||
let raw_err_param: i32 = -1;
|
|
||||||
let raw_err = raw_err_param.to_be_bytes();
|
|
||||||
basic_event_test(8, Severity::Low, Some(&raw_err))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn check_buf_too_small(
|
|
||||||
reporter: &mut EventReporter,
|
|
||||||
sender: &mut TestSender,
|
|
||||||
expected_found_len: usize,
|
|
||||||
) {
|
|
||||||
let time_stamp_empty: [u8; 7] = [0; 7];
|
|
||||||
let event = EventU32::new_checked(Severity::Info, EXAMPLE_GROUP_ID, EXAMPLE_EVENT_ID_0)
|
|
||||||
.expect("Error creating example event");
|
|
||||||
let err = reporter.event_info(sender, &time_stamp_empty, event, None);
|
|
||||||
assert!(err.is_err());
|
|
||||||
let err = err.unwrap_err();
|
|
||||||
if let EcssTmtcError::Pus(PusError::ByteConversion(
|
|
||||||
ByteConversionError::ToSliceTooSmall { found, expected },
|
|
||||||
)) = err
|
|
||||||
{
|
|
||||||
assert_eq!(expected, 4);
|
|
||||||
assert_eq!(found, expected_found_len);
|
|
||||||
} else {
|
|
||||||
panic!("Unexpected error {:?}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn insufficient_buffer() {
|
|
||||||
let mut sender = TestSender::default();
|
|
||||||
for i in 0..3 {
|
|
||||||
let mut reporter = EventReporter::new(0, EXAMPLE_APID, 0, i);
|
|
||||||
check_buf_too_small(&mut reporter, &mut sender, i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user