first step in unifying the examples: one example folder
One example folder for all examples, including std and embedded examples
This commit is contained in:
1 parent
bb8decfc74
commit
7cc2ff3ff9
130 files changed
+399
-368
No files matched your search
@@ -0,0 +1,21 @@
|
||||
[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 = { path = "../../satrs" }
|
||||
example-std = { path = "../example-std" }
|
||||
minisim-types = { path = "../minisim-types" }
|
||||
types = { path = "../types" }
|
||||
spacepackets = { version = "0.18", default-features = false }
|
||||
bitbybit = "2"
|
||||
arbitrary-int = "2"
|
||||
ctrlc = { version = "3.5" }
|
||||
postcard = { version = "1", features = ["alloc"] }
|
||||
anyhow = "1"
|
||||
@@ -0,0 +1,799 @@
|
||||
use anyhow::{Context as _, bail};
|
||||
use arbitrary_int::u11;
|
||||
use clap::Parser as _;
|
||||
use example_std::config::{OBSW_SERVER_ADDR, SERVER_PORT};
|
||||
use minisim_types::{
|
||||
SimCtrlReply, SimCtrlRequest, SimReply, SimRequest, SimRequestWithTime, acs::mgm,
|
||||
udp::SIM_CTRL_PORT,
|
||||
};
|
||||
use spacepackets::{CcsdsPacketIdAndPsc, SpacePacketHeader};
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket},
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
use types::{Apid, Message as _, MessageType, TcHeader, acs::mgm::request::HkRequest};
|
||||
|
||||
#[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),
|
||||
Mgt(MgtArgs),
|
||||
AcsSubsystem(SubsystemArgs),
|
||||
EventManager(EventManagerArgs),
|
||||
}
|
||||
|
||||
#[derive(clap::Parser)]
|
||||
struct EventManagerArgs {
|
||||
#[command(subcommand)]
|
||||
action: EventFilterAction,
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand)]
|
||||
enum EventFilterAction {
|
||||
/// Enable event TM generation.
|
||||
Enable(EventFilterArgs),
|
||||
/// Disable event TM generation.
|
||||
Disable(EventFilterArgs),
|
||||
}
|
||||
|
||||
#[derive(clap::Args)]
|
||||
struct EventFilterArgs {
|
||||
#[arg(value_enum)]
|
||||
component: EventSenderSelect,
|
||||
/// Raw event ID. Without it, the filter applies to all events of the component.
|
||||
#[arg(short, long)]
|
||||
event_id: Option<u16>,
|
||||
}
|
||||
|
||||
/// Components which emit events.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
|
||||
enum EventSenderSelect {
|
||||
Controller,
|
||||
Mgm0,
|
||||
Mgm1,
|
||||
MgmAssy,
|
||||
Mgt,
|
||||
Pcdu,
|
||||
UdpServer,
|
||||
TcpServer,
|
||||
Ground,
|
||||
}
|
||||
|
||||
impl From<EventSenderSelect> for types::ComponentId {
|
||||
fn from(sender: EventSenderSelect) -> Self {
|
||||
match sender {
|
||||
EventSenderSelect::Controller => types::ComponentId::Controller,
|
||||
EventSenderSelect::Mgm0 => types::ComponentId::AcsMgm0,
|
||||
EventSenderSelect::Mgm1 => types::ComponentId::AcsMgm1,
|
||||
EventSenderSelect::Mgt => types::ComponentId::AcsMgt,
|
||||
EventSenderSelect::MgmAssy => types::ComponentId::AcsMgmAssembly,
|
||||
EventSenderSelect::Pcdu => types::ComponentId::EpsPcdu,
|
||||
EventSenderSelect::UdpServer => types::ComponentId::UdpServer,
|
||||
EventSenderSelect::TcpServer => types::ComponentId::TcpServer,
|
||||
EventSenderSelect::Ground => types::ComponentId::Ground,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
|
||||
enum FaultMode {
|
||||
None,
|
||||
/// SPI communication is all zeroes, modelling an unconnected sensor.
|
||||
AllZeros,
|
||||
/// SPI communication is all ones, modelling a broken sensor.
|
||||
AllOnes,
|
||||
}
|
||||
|
||||
impl From<FaultMode> for mgm::SpiFaultMode {
|
||||
fn from(mode: FaultMode) -> Self {
|
||||
match mode {
|
||||
FaultMode::None => mgm::SpiFaultMode::None,
|
||||
FaultMode::AllZeros => mgm::SpiFaultMode::AllZeros,
|
||||
FaultMode::AllOnes => mgm::SpiFaultMode::AllOnes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
|
||||
enum FaultKind {
|
||||
/// Cleared when the device is switched off, so a power cycle recovers from it.
|
||||
Transient,
|
||||
/// Survives power cycles.
|
||||
#[default]
|
||||
Permanent,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
|
||||
enum HkSelect {
|
||||
OneShot,
|
||||
EnablePeriodic,
|
||||
DisablePeriodic,
|
||||
ModifyInterval,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
|
||||
enum HealthStateSelect {
|
||||
Healthy,
|
||||
Faulty,
|
||||
PermanentFaulty,
|
||||
ExternalControl,
|
||||
NeedsRecovery,
|
||||
}
|
||||
|
||||
impl From<HealthStateSelect> for satrs::health::HealthState {
|
||||
fn from(state: HealthStateSelect) -> Self {
|
||||
match state {
|
||||
HealthStateSelect::Healthy => satrs::health::HealthState::Healthy,
|
||||
HealthStateSelect::Faulty => satrs::health::HealthState::Faulty,
|
||||
HealthStateSelect::PermanentFaulty => satrs::health::HealthState::PermanentFaulty,
|
||||
HealthStateSelect::ExternalControl => satrs::health::HealthState::ExternalControl,
|
||||
HealthStateSelect::NeedsRecovery => satrs::health::HealthState::NeedsRecovery,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::Parser)]
|
||||
struct MgmArgs {
|
||||
#[arg(short, long)]
|
||||
ping: bool,
|
||||
/// Housekeeping request for the sensor data set.
|
||||
#[arg(long, value_enum)]
|
||||
hk: Option<HkSelect>,
|
||||
/// Periodic HK interval. Required for `modify-interval`, optional for `enable-periodic`.
|
||||
#[arg(long)]
|
||||
hk_interval_ms: Option<u64>,
|
||||
#[arg(short, long)]
|
||||
mode: Option<DeviceModeSelect>,
|
||||
/// Inject (or clear) an SPI bus failure on the simulated device, bypassing the OBSW.
|
||||
#[arg(long, value_enum)]
|
||||
fault: Option<FaultMode>,
|
||||
/// Whether a power cycle clears the injected SPI fault.
|
||||
#[arg(long, value_enum, default_value_t)]
|
||||
fault_kind: FaultKind,
|
||||
/// Override the device's FDIR health state, for example to clear a `Faulty` state set by
|
||||
/// the handler after the underlying issue has been fixed or worked around.
|
||||
#[arg(long, value_enum)]
|
||||
health: Option<HealthStateSelect>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::Parser)]
|
||||
struct MgtArgs {
|
||||
#[arg(short, long)]
|
||||
ping: bool,
|
||||
/// Housekeeping request for the status data set.
|
||||
#[arg(long, value_enum)]
|
||||
hk: Option<HkSelect>,
|
||||
/// Periodic HK interval. Required for `modify-interval`, optional for `enable-periodic`.
|
||||
#[arg(long)]
|
||||
hk_interval_ms: Option<u64>,
|
||||
#[arg(short, long)]
|
||||
mode: Option<DeviceModeSelect>,
|
||||
/// Apply a dipole, given as `x,y,z`. Only accepted in normal mode.
|
||||
#[arg(long, value_name = "X,Y,Z", value_parser = parse_dipole, allow_hyphen_values = true)]
|
||||
torque: Option<types::acs::mgt::Dipole>,
|
||||
#[arg(long, default_value_t = 1000)]
|
||||
torque_duration_ms: u64,
|
||||
/// Override the device's FDIR health state, for example to clear a `Faulty` state.
|
||||
#[arg(long, value_enum)]
|
||||
health: Option<HealthStateSelect>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::Parser)]
|
||||
struct MgmAssemblyArgs {
|
||||
#[arg(short, long)]
|
||||
ping: bool,
|
||||
#[arg(short, long)]
|
||||
mode: Option<AssemblyModeSelect>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::Parser)]
|
||||
struct SubsystemArgs {
|
||||
#[arg(short, long)]
|
||||
ping: bool,
|
||||
#[arg(short, long)]
|
||||
mode: Option<SubsystemModeSelect>,
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
|
||||
pub enum SubsystemModeSelect {
|
||||
Off,
|
||||
Safe,
|
||||
}
|
||||
|
||||
fn hk_request_type(
|
||||
hk: HkSelect,
|
||||
hk_interval_ms: Option<u64>,
|
||||
) -> anyhow::Result<types::HkRequestType> {
|
||||
let opt_interval = hk_interval_ms.map(Duration::from_millis);
|
||||
Ok(match hk {
|
||||
HkSelect::OneShot => types::HkRequestType::OneShot,
|
||||
HkSelect::EnablePeriodic => types::HkRequestType::EnablePeriodic(opt_interval),
|
||||
HkSelect::DisablePeriodic => types::HkRequestType::DisablePeriodic,
|
||||
HkSelect::ModifyInterval => types::HkRequestType::ModifyInterval(
|
||||
opt_interval.context("--hk-interval-ms is required for modify-interval")?,
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_dipole(value: &str) -> Result<types::acs::mgt::Dipole, String> {
|
||||
let axes: Vec<i16> = value
|
||||
.split(',')
|
||||
.map(|axis| axis.trim().parse::<i16>().map_err(|e| e.to_string()))
|
||||
.collect::<Result<_, _>>()?;
|
||||
let [x, y, z] = axes[..] else {
|
||||
return Err(format!("expected 3 values, got {}", axes.len()));
|
||||
};
|
||||
Ok(types::acs::mgt::Dipole { x, y, z })
|
||||
}
|
||||
|
||||
fn send_mgt_request(
|
||||
client: &UdpSocket,
|
||||
addr: SocketAddr,
|
||||
request: types::acs::mgt::request::Request,
|
||||
) {
|
||||
let packet = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||
TcHeader::new(types::ComponentId::AcsMgt, request.message_type()),
|
||||
request,
|
||||
);
|
||||
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
|
||||
log::info!(
|
||||
"sending MGT request {:?} with TC ID {:#010x}",
|
||||
request,
|
||||
sent_tc_id.raw()
|
||||
);
|
||||
client.send_to(&packet.to_vec(), addr).unwrap();
|
||||
}
|
||||
|
||||
fn handle_mgt_command(client: &UdpSocket, addr: SocketAddr, args: MgtArgs) -> anyhow::Result<()> {
|
||||
use types::acs::mgt::request::{ModeRequest, Request};
|
||||
|
||||
if args.ping {
|
||||
send_mgt_request(client, addr, Request::Ping);
|
||||
}
|
||||
if let Some(hk) = args.hk {
|
||||
let req_type = hk_request_type(hk, args.hk_interval_ms)?;
|
||||
send_mgt_request(client, addr, Request::Hk(req_type));
|
||||
}
|
||||
if let Some(mode) = args.mode {
|
||||
let mode = match mode {
|
||||
DeviceModeSelect::Off => types::DeviceMode::Off,
|
||||
DeviceModeSelect::Normal => types::DeviceMode::Normal,
|
||||
};
|
||||
send_mgt_request(client, addr, Request::Mode(ModeRequest::SetMode(mode)));
|
||||
}
|
||||
if let Some(dipole) = args.torque {
|
||||
let request = Request::ApplyTorque {
|
||||
dipole,
|
||||
duration: Duration::from_millis(args.torque_duration_ms),
|
||||
};
|
||||
send_mgt_request(client, addr, request);
|
||||
}
|
||||
if let Some(health) = args.health {
|
||||
send_mgt_request(
|
||||
client,
|
||||
addr,
|
||||
Request::Health(types::HealthRequest::SetHealth(health.into())),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_mgm_command(
|
||||
client: &UdpSocket,
|
||||
addr: SocketAddr,
|
||||
target_id: types::ComponentId,
|
||||
args: MgmArgs,
|
||||
) -> anyhow::Result<()> {
|
||||
if let Some(mode) = args.fault {
|
||||
inject_mgm_failure(
|
||||
target_id,
|
||||
mgm::SpiFault {
|
||||
mode: mode.into(),
|
||||
cleared_by_power_cycle: args.fault_kind == FaultKind::Transient,
|
||||
},
|
||||
)?;
|
||||
}
|
||||
if args.ping {
|
||||
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||
TcHeader::new(target_id, types::MessageType::Ping),
|
||||
types::acs::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(hk) = args.hk {
|
||||
let req_type = hk_request_type(hk, args.hk_interval_ms)?;
|
||||
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||
TcHeader::new(target_id, types::MessageType::Hk),
|
||||
types::acs::mgm::request::Request::Hk(HkRequest {
|
||||
id: types::acs::mgm::request::HkId::Sensor,
|
||||
req_type,
|
||||
}),
|
||||
);
|
||||
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 => types::DeviceMode::Off,
|
||||
DeviceModeSelect::Normal => types::DeviceMode::Normal,
|
||||
};
|
||||
|
||||
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||
TcHeader::new(target_id, types::MessageType::Mode),
|
||||
types::acs::mgm::request::Request::Mode(
|
||||
types::acs::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();
|
||||
}
|
||||
if let Some(health) = args.health {
|
||||
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||
TcHeader::new(target_id, types::MessageType::Health),
|
||||
types::acs::mgm::request::Request::Health(types::HealthRequest::SetHealth(
|
||||
health.into(),
|
||||
)),
|
||||
);
|
||||
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
|
||||
log::info!(
|
||||
"sending {:?} set-health request with TC ID {:#010x}",
|
||||
target_id,
|
||||
sent_tc_id.raw()
|
||||
);
|
||||
let request_packet = request.to_vec();
|
||||
client.send_to(&request_packet, addr).unwrap();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_event_manager_command(client: &UdpSocket, addr: SocketAddr, args: EventManagerArgs) {
|
||||
use types::event_manager::request::Request;
|
||||
|
||||
let request = match args.action {
|
||||
EventFilterAction::Enable(filter) => match filter.event_id {
|
||||
Some(event_id) => Request::EnableEvent {
|
||||
sender_id: filter.component.into(),
|
||||
event_id,
|
||||
},
|
||||
None => Request::EnableComponent(filter.component.into()),
|
||||
},
|
||||
EventFilterAction::Disable(filter) => match filter.event_id {
|
||||
Some(event_id) => Request::DisableEvent {
|
||||
sender_id: filter.component.into(),
|
||||
event_id,
|
||||
},
|
||||
None => Request::DisableComponent(filter.component.into()),
|
||||
},
|
||||
};
|
||||
let request_packet = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Tmtc as u16)),
|
||||
TcHeader::new(types::ComponentId::EventManager, MessageType::Event),
|
||||
request,
|
||||
);
|
||||
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request_packet.sp_header);
|
||||
log::info!(
|
||||
"sending event manager request {:?} with TC ID {:#010x}",
|
||||
request,
|
||||
sent_tc_id.raw()
|
||||
);
|
||||
client.send_to(&request_packet.to_vec(), addr).unwrap();
|
||||
}
|
||||
|
||||
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 = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Tmtc as u16)),
|
||||
TcHeader::new(types::ComponentId::Controller, types::MessageType::Ping),
|
||||
types::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 = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Tmtc as u16)),
|
||||
TcHeader::new(types::ComponentId::Controller, types::MessageType::Event),
|
||||
types::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 {
|
||||
match cmd {
|
||||
Commands::Mgm0(args) => {
|
||||
handle_mgm_command(&client, addr, types::ComponentId::AcsMgm0, args)?
|
||||
}
|
||||
Commands::Mgm1(args) => {
|
||||
handle_mgm_command(&client, addr, types::ComponentId::AcsMgm1, args)?
|
||||
}
|
||||
Commands::Mgt(args) => handle_mgt_command(&client, addr, args)?,
|
||||
Commands::MgmAssy(mgm_assembly_args) => {
|
||||
let target_id = types::ComponentId::AcsMgmAssembly;
|
||||
if mgm_assembly_args.ping {
|
||||
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||
TcHeader::new(target_id, types::MessageType::Ping),
|
||||
types::acs::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 => {
|
||||
types::acs::mgm_assembly::Mode::NoModeKeeping
|
||||
}
|
||||
AssemblyModeSelect::Off => {
|
||||
types::acs::mgm_assembly::Mode::Device(types::DeviceMode::Off)
|
||||
}
|
||||
AssemblyModeSelect::Normal => {
|
||||
types::acs::mgm_assembly::Mode::Device(types::DeviceMode::Normal)
|
||||
}
|
||||
};
|
||||
|
||||
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||
TcHeader::new(target_id, types::MessageType::Mode),
|
||||
types::acs::mgm_assembly::request::Request::Mode(
|
||||
types::acs::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();
|
||||
}
|
||||
}
|
||||
Commands::AcsSubsystem(subsystem_args) => {
|
||||
let target_id = types::ComponentId::AcsSubsystem;
|
||||
if subsystem_args.ping {
|
||||
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||
TcHeader::new(target_id, types::MessageType::Ping),
|
||||
types::acs::subsystem::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) = subsystem_args.mode {
|
||||
let subsystem_mode = match mode {
|
||||
SubsystemModeSelect::Off => types::acs::subsystem::Mode::Off,
|
||||
SubsystemModeSelect::Safe => types::acs::subsystem::Mode::Safe,
|
||||
};
|
||||
|
||||
let request = types::ccsds::CcsdsTcPacketOwned::new_with_request(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||
TcHeader::new(target_id, types::MessageType::Mode),
|
||||
types::acs::subsystem::request::Request::Mode(
|
||||
types::acs::subsystem::request::ModeRequest::SetMode(subsystem_mode),
|
||||
),
|
||||
);
|
||||
let sent_tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&request.sp_header);
|
||||
log::info!(
|
||||
"sending {:?} mode 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::EventManager(args) => handle_event_manager_command(&client, addr, args),
|
||||
}
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
/// Injects the given SPI fault directly into minisim's MGM model, bypassing the OBSW.
|
||||
///
|
||||
/// Confirms the simulator is actually reachable first (same ping/pong check the OBSW's own
|
||||
/// internal sim client does, see `SimClientUdp::attempt_connection`), since a fire-and-forget
|
||||
/// UDP send would otherwise silently do nothing if minisim is not running.
|
||||
fn inject_mgm_failure(target_id: types::ComponentId, fault: mgm::SpiFault) -> anyhow::Result<()> {
|
||||
let sim_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), SIM_CTRL_PORT);
|
||||
let sim_socket = UdpSocket::bind("127.0.0.1:0")?;
|
||||
sim_socket.set_read_timeout(Some(Duration::from_millis(200)))?;
|
||||
|
||||
let mut reply_buf = [0u8; 4096];
|
||||
let ping = SimRequestWithTime::new_with_epoch_time(SimCtrlRequest::Ping);
|
||||
sim_socket.send_to(&postcard::to_allocvec(&ping)?, sim_addr)?;
|
||||
match sim_socket.recv(&mut reply_buf) {
|
||||
Ok(len) => {
|
||||
let reply: SimReply = postcard::from_bytes(&reply_buf[..len])?;
|
||||
if reply != SimReply::SimCtrl(SimCtrlReply::Pong) {
|
||||
bail!("unexpected reply while checking minisim connectivity: {reply:?}");
|
||||
}
|
||||
}
|
||||
Err(e)
|
||||
if matches!(
|
||||
e.kind(),
|
||||
std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
|
||||
) =>
|
||||
{
|
||||
bail!("minisim not reachable at {sim_addr} (ping timed out) - is it running?");
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
|
||||
let id = match target_id {
|
||||
types::ComponentId::AcsMgm0 => mgm::Id::Mgm0,
|
||||
types::ComponentId::AcsMgm1 => mgm::Id::Mgm1,
|
||||
_ => bail!("SPI fault injection is not supported for {target_id:?}"),
|
||||
};
|
||||
let request = SimRequestWithTime::new_with_epoch_time(SimRequest::Mgm {
|
||||
id,
|
||||
request: mgm::Request::SetSpiFault(fault),
|
||||
});
|
||||
sim_socket.send_to(&postcard::to_allocvec(&request)?, sim_addr)?;
|
||||
log::info!("injected SPI fault {fault:?} into minisim {target_id:?}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Each component has its own event type, so the sender ID determines how to decode the event.
|
||||
fn handle_event(sender_id: types::ComponentId, data: &[u8]) {
|
||||
fn log_event<E: serde::de::DeserializeOwned + core::fmt::Debug>(
|
||||
sender_id: types::ComponentId,
|
||||
data: &[u8],
|
||||
) {
|
||||
match postcard::from_bytes::<E>(data) {
|
||||
Ok(event) => log::info!("Received event from {:?}: {:?}", sender_id, event),
|
||||
Err(e) => log::warn!("Failed to deserialize event from {:?}: {}", sender_id, e),
|
||||
}
|
||||
}
|
||||
match sender_id {
|
||||
types::ComponentId::Controller => log_event::<types::Event>(sender_id, data),
|
||||
types::ComponentId::AcsMgm0 | types::ComponentId::AcsMgm1 => {
|
||||
log_event::<types::acs::mgm::Event>(sender_id, data)
|
||||
}
|
||||
types::ComponentId::AcsMgmAssembly => {
|
||||
log_event::<types::acs::mgm_assembly::Event>(sender_id, data)
|
||||
}
|
||||
types::ComponentId::AcsMgt => log_event::<types::acs::mgt::Event>(sender_id, data),
|
||||
types::ComponentId::EpsPcdu => log_event::<types::pcdu::Event>(sender_id, data),
|
||||
// TC source events are sent with the ID of the packet source.
|
||||
types::ComponentId::UdpServer
|
||||
| types::ComponentId::TcpServer
|
||||
| types::ComponentId::Ground => log_event::<types::tmtc::Event>(sender_id, data),
|
||||
_ => log::warn!(
|
||||
"Received event from {:?} with unknown event type",
|
||||
sender_id
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
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::<types::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()
|
||||
);
|
||||
}
|
||||
if tm_header.message_type == MessageType::Event {
|
||||
handle_event(tm_header.sender_id, remainder);
|
||||
return Ok(());
|
||||
}
|
||||
match tm_header.sender_id {
|
||||
types::ComponentId::EpsPcdu => {
|
||||
let response =
|
||||
postcard::from_bytes::<types::pcdu::response::Response>(remainder);
|
||||
log::info!("Received response from PCDU: {:?}", response.unwrap());
|
||||
}
|
||||
types::ComponentId::Controller => {
|
||||
let response =
|
||||
postcard::from_bytes::<types::control::response::Response>(remainder);
|
||||
log::info!("Received response from controller: {:?}", response.unwrap());
|
||||
}
|
||||
types::ComponentId::AcsMgmAssembly => {
|
||||
let response = postcard::from_bytes::<
|
||||
types::acs::mgm_assembly::response::Response,
|
||||
>(remainder);
|
||||
log::info!(
|
||||
"Received response from MGM Assembly: {:?}",
|
||||
response.unwrap()
|
||||
);
|
||||
}
|
||||
types::ComponentId::AcsMgm0 => {
|
||||
let response =
|
||||
postcard::from_bytes::<types::acs::mgm::response::Response>(remainder);
|
||||
log::info!("Received response from MGM0: {:?}", response.unwrap());
|
||||
}
|
||||
types::ComponentId::AcsMgm1 => {
|
||||
let response =
|
||||
postcard::from_bytes::<types::acs::mgm::response::Response>(remainder);
|
||||
log::info!("Received response from MGM1: {:?}", response.unwrap());
|
||||
}
|
||||
types::ComponentId::AcsSubsystem => {
|
||||
let response = postcard::from_bytes::<types::acs::subsystem::response::Response>(
|
||||
remainder,
|
||||
);
|
||||
log::info!(
|
||||
"Received response from ACS subsystem: {:?}",
|
||||
response.unwrap()
|
||||
);
|
||||
}
|
||||
types::ComponentId::EpsSubsystem => todo!(),
|
||||
types::ComponentId::UdpServer => todo!(),
|
||||
types::ComponentId::TcpServer => todo!(),
|
||||
types::ComponentId::Ground => todo!(),
|
||||
types::ComponentId::EventManager => {
|
||||
let response =
|
||||
postcard::from_bytes::<types::event_manager::response::Response>(remainder);
|
||||
log::info!(
|
||||
"Received response from event manager: {:?}",
|
||||
response.unwrap()
|
||||
);
|
||||
}
|
||||
types::ComponentId::AcsController => todo!(),
|
||||
types::ComponentId::AcsMgt => {
|
||||
let response =
|
||||
postcard::from_bytes::<types::acs::mgt::response::Response>(remainder);
|
||||
log::info!("Received response from MGT: {:?}", response.unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => todo!(),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_dipole() {
|
||||
assert_eq!(
|
||||
parse_dipole("-200, 200,1000"),
|
||||
Ok(types::acs::mgt::Dipole {
|
||||
x: -200,
|
||||
y: 200,
|
||||
z: 1000
|
||||
})
|
||||
);
|
||||
assert!(parse_dipole("1,2").is_err());
|
||||
assert!(parse_dipole("1,2,3,4").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_negative_torque_argument() {
|
||||
let cli = Cli::try_parse_from(["client", "mgt", "--torque", "-200,200,1000"]).unwrap();
|
||||
let Some(Commands::Mgt(args)) = cli.commands else {
|
||||
panic!("expected mgt subcommand");
|
||||
};
|
||||
assert_eq!(args.torque.map(|dipole| dipole.x), Some(-200));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "embedded-client"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
serialport = "4"
|
||||
toml = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
spacepackets = { version = "0.18" }
|
||||
embedded-types = { path = "../embedded-types" }
|
||||
tmtc-utils = { path = "../../tmtc-utils" }
|
||||
postcard = { version = "1", features = ["alloc"] }
|
||||
anyhow = "1"
|
||||
cobs = "0.5"
|
||||
fern = "0.7"
|
||||
humantime = "2"
|
||||
log = "0.4"
|
||||
@@ -0,0 +1,3 @@
|
||||
[interface]
|
||||
serial_port = "/dev/ttyUSB0"
|
||||
# udp_addr = "192.168.XXX.XX:7301"
|
||||
@@ -0,0 +1,71 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::bail;
|
||||
use clap::Parser;
|
||||
use cobs::CobsDecoderOwned;
|
||||
use embedded_client::setup_logger;
|
||||
use embedded_types::stm32f3;
|
||||
use spacepackets::{CcsdsPacketCreatorOwned, CcsdsPacketReader, SpHeader};
|
||||
use tmtc_utils::transport::serial::PacketTransportSerialCobs;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct Cli {
|
||||
#[arg(short, long)]
|
||||
ping: bool,
|
||||
|
||||
/// Set frequency in milliseconds.
|
||||
#[arg(short, long)]
|
||||
set_led_frequency: Option<u32>,
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
setup_logger().expect("failed to initialize logger");
|
||||
println!("-- STM32F3 TMTC client --");
|
||||
let cli = Cli::parse();
|
||||
let config = embedded_client::Config::new_from_file();
|
||||
|
||||
if config.interface.serial_port.is_none() {
|
||||
bail!("Serial port not specified in configuration file.");
|
||||
}
|
||||
let serial_port = config.interface.serial_port.as_ref().unwrap();
|
||||
let serial = serialport::new(serial_port, 115200)
|
||||
.open()
|
||||
.expect("opening serial port failed");
|
||||
let mut transport = PacketTransportSerialCobs::new(serial, CobsDecoderOwned::new(1024));
|
||||
|
||||
if cli.ping {
|
||||
let tc = create_stm32f3_tc(&embedded_types::stm32f3::Request::Ping);
|
||||
log::info!(
|
||||
"Sending ping request with TC ID: {:#010x}",
|
||||
tc.ccsds_packet_id_and_psc().raw()
|
||||
);
|
||||
transport.send(&tc.to_vec()).unwrap();
|
||||
}
|
||||
|
||||
if let Some(freq_ms) = cli.set_led_frequency {
|
||||
let request = stm32f3::Request::ChangeBlinkFrequency(Duration::from_millis(freq_ms as u64));
|
||||
let tc = create_stm32f3_tc(&request);
|
||||
log::info!(
|
||||
"Sending change blink frequency request {:?} with TC ID: {:#010x}",
|
||||
request,
|
||||
tc.ccsds_packet_id_and_psc().raw()
|
||||
);
|
||||
transport.send(&tc.to_vec()).unwrap();
|
||||
}
|
||||
|
||||
log::info!("Waiting for response...");
|
||||
loop {
|
||||
transport
|
||||
.receive(|packet: &[u8]| {
|
||||
let reader = CcsdsPacketReader::new_with_checksum(packet);
|
||||
log::info!("Received packet: {:?}", reader);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn create_stm32f3_tc(request: &stm32f3::Request) -> CcsdsPacketCreatorOwned {
|
||||
let req_raw = postcard::to_allocvec(&request).unwrap();
|
||||
let sp_header = SpHeader::new_from_apid(embedded_types::stm32f3::PUS_APID);
|
||||
CcsdsPacketCreatorOwned::new_tc_with_checksum(sp_header, &req_raw).unwrap()
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
use std::{net::UdpSocket, time::Duration};
|
||||
|
||||
use anyhow::{Context as _, bail};
|
||||
use clap::Parser;
|
||||
use embedded_client::setup_logger;
|
||||
use embedded_types::{TmHeader, stm32h7};
|
||||
use spacepackets::{CcsdsPacketCreatorOwned, CcsdsPacketReader, SpHeader};
|
||||
use tmtc_utils::transport::udp::PacketTransportUdp;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct Cli {
|
||||
#[arg(short, long)]
|
||||
ping: bool,
|
||||
|
||||
/// Set frequency in milliseconds.
|
||||
#[arg(short, long)]
|
||||
set_led_frequency: Option<u32>,
|
||||
|
||||
/// UDP address to bind to.
|
||||
#[arg(short, long)]
|
||||
udp_addr: Option<std::net::SocketAddr>,
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
setup_logger().expect("failed to initialize logger");
|
||||
println!("-- STM32H7 TMTC client --");
|
||||
let cli = Cli::parse();
|
||||
let config = embedded_client::Config::new_from_file();
|
||||
let mut udp_addr = cli.udp_addr;
|
||||
if udp_addr.is_none() {
|
||||
udp_addr = config.interface.udp_addr;
|
||||
}
|
||||
if udp_addr.is_none() {
|
||||
bail!("UDP address not specified in config.toml or via command line");
|
||||
}
|
||||
let udp_addr = udp_addr.unwrap();
|
||||
log::info!("binding to UDP address: {}", udp_addr);
|
||||
let local_socket = UdpSocket::bind("0.0.0.0:0").expect("failed to bind UDP socket");
|
||||
let mut transport = PacketTransportUdp::new(local_socket, udp_addr)
|
||||
.with_context(|| "crateing UDP transport failed")?;
|
||||
|
||||
if cli.ping {
|
||||
let tc = create_stm32h7_tc(&embedded_types::stm32h7::Request::Ping);
|
||||
log::info!(
|
||||
"Sending ping request with TC ID: {:#010x}",
|
||||
tc.ccsds_packet_id_and_psc().raw()
|
||||
);
|
||||
transport.send(&tc.to_vec()).unwrap();
|
||||
}
|
||||
|
||||
if let Some(freq_ms) = cli.set_led_frequency {
|
||||
let request = stm32h7::Request::ChangeBlinkFrequency(Duration::from_millis(freq_ms as u64));
|
||||
let tc = create_stm32h7_tc(&request);
|
||||
log::info!(
|
||||
"Sending change blink frequency request {:?} with TC ID: {:#010x}",
|
||||
request,
|
||||
tc.ccsds_packet_id_and_psc().raw()
|
||||
);
|
||||
transport.send(&tc.to_vec()).unwrap();
|
||||
}
|
||||
|
||||
log::info!("Waiting for response...");
|
||||
loop {
|
||||
transport
|
||||
.receive(|packet: &[u8]| {
|
||||
let reader = CcsdsPacketReader::new_with_checksum(packet);
|
||||
log::debug!("Received packet: {:?}", reader);
|
||||
if let Ok(reader) = reader {
|
||||
let packet_data = reader.packet_data();
|
||||
let tm_header = postcard::take_from_bytes::<TmHeader>(packet_data);
|
||||
if let Ok((tm_header, remainder)) = tm_header {
|
||||
let response = postcard::from_bytes::<stm32h7::Response>(remainder);
|
||||
if let Ok(response) = response {
|
||||
log::info!(
|
||||
"Received TM with header: {:?} and response: {:?}",
|
||||
tm_header,
|
||||
response
|
||||
);
|
||||
} else {
|
||||
log::error!("Failed to deserialize response: {:?}", response.err());
|
||||
}
|
||||
} else {
|
||||
log::error!("Failed to deserialize TM header: {:?}", tm_header.err());
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn create_stm32h7_tc(request: &stm32h7::Request) -> CcsdsPacketCreatorOwned {
|
||||
let req_raw = postcard::to_allocvec(&request).unwrap();
|
||||
let sp_header = SpHeader::new_from_apid(embedded_types::stm32h7::PUS_APID);
|
||||
CcsdsPacketCreatorOwned::new_tc_with_checksum(sp_header, &req_raw).unwrap()
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
use std::{fs::File, io::Read as _, net::SocketAddr, path::Path, time::SystemTime};
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct Config {
|
||||
pub interface: Interface,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct Interface {
|
||||
pub serial_port: Option<String>,
|
||||
pub udp_addr: Option<SocketAddr>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn new_from_file() -> Self {
|
||||
let mut config_file =
|
||||
File::open(Path::new("config.toml")).expect("opening config.toml file failed");
|
||||
let mut toml_str = String::new();
|
||||
config_file
|
||||
.read_to_string(&mut toml_str)
|
||||
.expect("reading config.toml file failed");
|
||||
let config: Config = toml::from_str(&toml_str).expect("parsing config.toml file failed");
|
||||
config
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setup_logger() -> Result<(), fern::InitError> {
|
||||
fern::Dispatch::new()
|
||||
.format(|out, message, record| {
|
||||
out.finish(format_args!(
|
||||
"[{} {} {}] {}",
|
||||
humantime::format_rfc3339_seconds(SystemTime::now()),
|
||||
record.level(),
|
||||
record.target(),
|
||||
message
|
||||
))
|
||||
})
|
||||
.level(log::LevelFilter::Info)
|
||||
.chain(std::io::stdout())
|
||||
.chain(fern::log_file("output.log")?)
|
||||
.apply()?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "embedded-types"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", default-features = false }
|
||||
defmt = { version = "1", optional = true }
|
||||
spacepackets = { version = "0.18", default-features = false, features = ["defmt", "serde"] }
|
||||
postcard = { version = "1", features = ["defmt"] }
|
||||
arbitrary-int = "2"
|
||||
@@ -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 {}
|
||||
@@ -0,0 +1 @@
|
||||
/output.log
|
||||
@@ -0,0 +1,19 @@
|
||||
Change Log
|
||||
=======
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
# [unreleased]
|
||||
|
||||
# [v0.1.1] 2024-02-21
|
||||
|
||||
satrs v0.2.0-rc.0
|
||||
satrs-mib v0.1.1
|
||||
|
||||
# [v0.1.0] 2024-02-13
|
||||
|
||||
satrs v0.1.1
|
||||
satrs-mib v0.1.0
|
||||
@@ -0,0 +1,38 @@
|
||||
[package]
|
||||
name = "example-std"
|
||||
version = "0.1.1"
|
||||
edition = "2024"
|
||||
default-run = "example-std"
|
||||
repository = "https://egit.irs.uni-stuttgart.de/rust/sat-rs"
|
||||
|
||||
[dependencies]
|
||||
fern = "0.7"
|
||||
chrono = "0.4"
|
||||
log = "0.4"
|
||||
crossbeam-channel = "0.5"
|
||||
delegate = "0.13"
|
||||
zerocopy = "0.8"
|
||||
csv = "1"
|
||||
num_enum = "0.7"
|
||||
thiserror = "2"
|
||||
lazy_static = "1"
|
||||
strum = { version = "0.28", features = ["derive"] }
|
||||
derive-new = "0.7"
|
||||
cfg-if = "1"
|
||||
arbitrary-int = "2"
|
||||
bitbybit = "2"
|
||||
postcard = { version = "1", features = ["alloc"] }
|
||||
ctrlc = "3"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
satrs = { path = "../../satrs", features = ["test_util"] }
|
||||
types = { path = "../types" }
|
||||
minisim-types = { path = "../minisim-types" }
|
||||
satrs-mib = { path = "../../satrs-mib" }
|
||||
|
||||
[features]
|
||||
# default = ["heap_tmtc"]
|
||||
# heap_tmtc = []
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = "0.11"
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1 @@
|
||||
This software contains code developed at the University of Stuttgart's Institute of Space Systems.
|
||||
@@ -0,0 +1,57 @@
|
||||
sat-rs example
|
||||
======
|
||||
|
||||
This crate contains an example application which simulates an on-board software.
|
||||
It uses various components provided by the sat-rs framework to do this. As such, it shows how
|
||||
a more complex real on-board software could be built from these components. It is recommended to
|
||||
read the dedicated
|
||||
[example chapters](https://documentation.irs.uni-stuttgart.de/projects/sat-rs/book/example.html) inside
|
||||
the sat-rs book.
|
||||
|
||||
The application opens a UDP and a TCP server on port 7301 to receive telecommands.
|
||||
|
||||
You can run the application using `cargo run`.
|
||||
|
||||
# Features
|
||||
|
||||
The example has the `heap_tmtc` feature which is enabled by default. With this feature enabled,
|
||||
TMTC packets are exchanged using the heap as the backing memory instead of pre-allocated static
|
||||
stores.
|
||||
|
||||
You can run the application without this feature using
|
||||
|
||||
```sh
|
||||
cargo run --no-default-features
|
||||
```
|
||||
|
||||
# Interacting with the sat-rs example
|
||||
|
||||
The `client` crate is a command line client which sends telecommands to the example application
|
||||
and prints the received telemetry. For example, you can ping the application or switch MGM 0
|
||||
to normal mode like this:
|
||||
|
||||
```sh
|
||||
cargo run -p client -- --ping
|
||||
cargo run -p client -- mgm0 -m normal
|
||||
```
|
||||
|
||||
Use `cargo run -p client -- --help` to list all available commands.
|
||||
|
||||
## Adding the mini simulator application
|
||||
|
||||
This example application features a few device handlers. The
|
||||
[`minisim`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/minisim)
|
||||
can be used to simulate the physical devices managed by these device handlers.
|
||||
|
||||
The example application will attempt communication with the mini simulator on UDP port 7303.
|
||||
If this works, the device handlers will use communication interfaces dedicated to the communication
|
||||
with the mini simulator. Otherwise, they will be replaced by dummy interfaces which either
|
||||
return constant values or behave like ideal devices.
|
||||
|
||||
In summary, you can use the following command command to run the mini-simulator first:
|
||||
|
||||
```sh
|
||||
cargo run -p minisim
|
||||
```
|
||||
|
||||
and then start the example using `cargo run -p example-std`.
|
||||
@@ -0,0 +1,118 @@
|
||||
use std::sync::mpsc::{Receiver, SyncSender, TryRecvError};
|
||||
|
||||
use types::acs::ctrl::{Mode, request::ModeRequest, response::ModeReport};
|
||||
|
||||
/// Helper component for communication with a parent component, which is usually an assembly
|
||||
/// or a subsystem.
|
||||
pub struct ModeLeafHelper {
|
||||
pub request_rx: Receiver<ModeRequest>,
|
||||
pub report_tx: SyncSender<ModeReport>,
|
||||
}
|
||||
|
||||
/// Dummy ACS controller. It has no actual control law and reaches any commanded mode
|
||||
/// immediately, which is sufficient to exercise the mode commanding of its parent subsystem.
|
||||
pub struct Controller {
|
||||
mode: Mode,
|
||||
mode_leaf_helper: ModeLeafHelper,
|
||||
}
|
||||
|
||||
impl Controller {
|
||||
pub fn new(mode_leaf_helper: ModeLeafHelper) -> Self {
|
||||
Self {
|
||||
mode: Mode::Passive,
|
||||
mode_leaf_helper,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[inline]
|
||||
pub fn mode(&self) -> Mode {
|
||||
self.mode
|
||||
}
|
||||
|
||||
pub fn periodic_operation(&mut self) {
|
||||
self.handle_mode_leaf_handling();
|
||||
}
|
||||
|
||||
fn handle_mode_leaf_handling(&mut self) {
|
||||
loop {
|
||||
match self.mode_leaf_helper.request_rx.try_recv() {
|
||||
Ok(request) => match request {
|
||||
ModeRequest::SetMode(mode) => {
|
||||
log::info!("ACS controller: transitioning to mode {:?}", mode);
|
||||
self.mode = mode;
|
||||
self.report_mode();
|
||||
}
|
||||
ModeRequest::ReadMode => self.report_mode(),
|
||||
},
|
||||
Err(e) => match e {
|
||||
TryRecvError::Empty => break,
|
||||
TryRecvError::Disconnected => log::warn!("packet sender disconnected"),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn report_mode(&self) {
|
||||
self.mode_leaf_helper
|
||||
.report_tx
|
||||
.send(ModeReport::Mode(self.mode))
|
||||
.expect("failed to send mode report to parent");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::mpsc;
|
||||
|
||||
use super::*;
|
||||
|
||||
struct ControllerTestbench {
|
||||
request_tx: SyncSender<ModeRequest>,
|
||||
report_rx: Receiver<ModeReport>,
|
||||
controller: Controller,
|
||||
}
|
||||
|
||||
impl ControllerTestbench {
|
||||
fn new() -> Self {
|
||||
let (request_tx, request_rx) = mpsc::sync_channel(5);
|
||||
let (report_tx, report_rx) = mpsc::sync_channel(5);
|
||||
Self {
|
||||
request_tx,
|
||||
report_rx,
|
||||
controller: Controller::new(ModeLeafHelper {
|
||||
request_rx,
|
||||
report_tx,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_initial_mode() {
|
||||
let testbench = ControllerTestbench::new();
|
||||
assert_eq!(testbench.controller.mode(), Mode::Passive);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_mode() {
|
||||
let mut testbench = ControllerTestbench::new();
|
||||
testbench
|
||||
.request_tx
|
||||
.send(ModeRequest::SetMode(Mode::Safe))
|
||||
.unwrap();
|
||||
testbench.controller.periodic_operation();
|
||||
assert_eq!(testbench.controller.mode(), Mode::Safe);
|
||||
let report = testbench.report_rx.try_recv().expect("no mode report sent");
|
||||
assert_eq!(report, ModeReport::Mode(Mode::Safe));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_mode() {
|
||||
let mut testbench = ControllerTestbench::new();
|
||||
testbench.request_tx.send(ModeRequest::ReadMode).unwrap();
|
||||
testbench.controller.periodic_operation();
|
||||
let report = testbench.report_rx.try_recv().expect("no mode report sent");
|
||||
assert_eq!(report, ModeReport::Mode(Mode::Passive));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,662 @@
|
||||
use std::{sync::mpsc, time::Duration};
|
||||
|
||||
use example_std::{ModeHelper, TmtcQueues};
|
||||
use satrs::spacepackets::CcsdsPacketIdAndPsc;
|
||||
use types::{
|
||||
ComponentId, DeviceMode,
|
||||
acs::mgm_assembly::{Mode, request, response},
|
||||
};
|
||||
|
||||
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::ModeResponse>,
|
||||
}
|
||||
|
||||
/// Helper component for communication with a parent component, which is usually as assembly.
|
||||
pub struct ChildrenQueueHelper {
|
||||
pub request_tx_queues: [mpsc::SyncSender<types::acs::mgm::request::ModeRequest>; 2],
|
||||
pub report_rx_queues: [mpsc::Receiver<types::acs::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<Mode, 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,
|
||||
event_tx: mpsc::SyncSender<types::acs::mgm_assembly::Event>,
|
||||
}
|
||||
|
||||
impl Assembly {
|
||||
pub const ID: ComponentId = ComponentId::AcsMgmAssembly;
|
||||
|
||||
pub fn new(
|
||||
parent_queues: ParentQueueHelper,
|
||||
children_queues: ChildrenQueueHelper,
|
||||
tmtc_queues: TmtcQueues,
|
||||
mode_timeout: Duration,
|
||||
event_tx: mpsc::SyncSender<types::acs::mgm_assembly::Event>,
|
||||
) -> Self {
|
||||
Self {
|
||||
mode_helper: ModeHelper::new(Mode::NoModeKeeping, mode_timeout),
|
||||
mode_keeping_transition: false,
|
||||
tmtc_queues,
|
||||
mgm_modes: [MgmInfo::default(); 2],
|
||||
parent_queues,
|
||||
children_queues,
|
||||
event_tx,
|
||||
}
|
||||
}
|
||||
|
||||
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::<types::acs::mgm_assembly::request::Request>(
|
||||
&packet.payload,
|
||||
) {
|
||||
Ok(request) => match request {
|
||||
types::acs::mgm_assembly::request::Request::Ping => {
|
||||
self.send_telemetry(Some(tc_id), response::Response::Ok)
|
||||
}
|
||||
types::acs::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::ModeResponse::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: types::acs::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 {
|
||||
Mode::Device(_device_mode) => {
|
||||
self.start_transition(false, assembly_mode, None);
|
||||
}
|
||||
Mode::NoModeKeeping => {
|
||||
self.mode_helper.current = Mode::NoModeKeeping;
|
||||
}
|
||||
},
|
||||
request::ModeRequest::ReadMode => self
|
||||
.parent_queues
|
||||
.report_tx
|
||||
.send(response::ModeResponse::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 {
|
||||
types::acs::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;
|
||||
}
|
||||
types::acs::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 Mode::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::ModeResponse::CanNotKeepMode(self.mgm_modes.map(|info| info.mode))
|
||||
} else {
|
||||
response::ModeResponse::WrongMode(self.mgm_modes.map(|info| info.mode))
|
||||
};
|
||||
self.handle_mode_transition_failure(report);
|
||||
}
|
||||
}
|
||||
|
||||
// Mode keeping active: Check children modes.
|
||||
if let Mode::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 {
|
||||
Mode::Device(device_mode) => device_mode,
|
||||
Mode::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::ModeResponse::CanNotKeepMode(self.mgm_modes.map(|info| info.mode))
|
||||
} else {
|
||||
response::ModeResponse::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::ModeResponse::Mode(self.mode_helper.current))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn handle_mode_transition_failure(&mut self, report: response::ModeResponse) {
|
||||
if self.mode_helper.tc_commander.is_some() {
|
||||
self.send_telemetry(
|
||||
self.mode_helper.tc_commander,
|
||||
response::Response::Mode(response::ModeResponse::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(types::acs::mgm::request::ModeRequest::SetMode(mode))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_transition(
|
||||
&mut self,
|
||||
mode_keeping: bool,
|
||||
target: Mode,
|
||||
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) {
|
||||
log::info!(
|
||||
"{:?} announcing mode: {:?}",
|
||||
Self::ID,
|
||||
self.mode_helper.current
|
||||
);
|
||||
if let Err(e) = self
|
||||
.event_tx
|
||||
.send(types::acs::mgm_assembly::Event::ModeChanged(
|
||||
self.mode_helper.current,
|
||||
))
|
||||
{
|
||||
log::warn!("{:?}: failed to send mode changed event: {}", Self::ID, e);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn mode(&self) -> Mode {
|
||||
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 satrs::spacepackets::SpacePacketHeader;
|
||||
use types::{
|
||||
Apid, Message, MessageType, TcHeader,
|
||||
acs::mgm_assembly,
|
||||
ccsds::{CcsdsTcPacketOwned, CcsdsTmPacketOwned},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
pub struct Testbench {
|
||||
subsystem_req_tx: mpsc::SyncSender<request::ModeRequest>,
|
||||
subsystem_report_rx: mpsc::Receiver<response::ModeResponse>,
|
||||
mgm_request_rx: [mpsc::Receiver<types::acs::mgm::request::ModeRequest>; 2],
|
||||
mgm_report_tx: [mpsc::SyncSender<types::acs::mgm::response::ModeResponse>; 2],
|
||||
tc_tx: mpsc::SyncSender<CcsdsTcPacketOwned>,
|
||||
tm_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
|
||||
event_rx: mpsc::Receiver<mgm_assembly::Event>,
|
||||
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);
|
||||
let (event_tx, event_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,
|
||||
event_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),
|
||||
event_tx,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
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: types::acs::mgm_assembly::request::Request,
|
||||
) -> types::ccsds::CcsdsTcPacketOwned {
|
||||
types::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(), Mode::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(Mode::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,
|
||||
types::acs::mgm::request::ModeRequest::SetMode(DeviceMode::Normal)
|
||||
);
|
||||
}
|
||||
|
||||
// Confirm the mode is set.
|
||||
for tx in tb.mgm_report_tx.iter() {
|
||||
tx.send(types::acs::mgm::response::ModeResponse::Mode(
|
||||
DeviceMode::Normal,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
tb.assembly.periodic_operation();
|
||||
assert!(!tb.assembly.mode_transition_active());
|
||||
assert_eq!(tb.assembly.mode(), Mode::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);
|
||||
|
||||
let event = tb.event_rx.try_recv().expect("expected mode changed event");
|
||||
assert!(matches!(
|
||||
event,
|
||||
mgm_assembly::Event::ModeChanged(Mode::Device(DeviceMode::Normal))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parent_commanded_transition() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.subsystem_req_tx
|
||||
.send(request::ModeRequest::SetMode(Mode::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,
|
||||
types::acs::mgm::request::ModeRequest::SetMode(DeviceMode::Normal)
|
||||
);
|
||||
}
|
||||
|
||||
// Confirm the mode is set.
|
||||
for tx in tb.mgm_report_tx.iter() {
|
||||
tx.send(types::acs::mgm::response::ModeResponse::Mode(
|
||||
DeviceMode::Normal,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
tb.assembly.periodic_operation();
|
||||
assert!(!tb.assembly.mode_transition_active());
|
||||
assert_eq!(tb.assembly.mode(), Mode::Device(DeviceMode::Normal));
|
||||
|
||||
let report = tb.subsystem_report_rx.try_recv().unwrap();
|
||||
assert_eq!(
|
||||
report,
|
||||
response::ModeResponse::Mode(Mode::Device(DeviceMode::Normal))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_one_mgm_is_sufficient() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.subsystem_req_tx
|
||||
.send(request::ModeRequest::SetMode(Mode::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,
|
||||
types::acs::mgm::request::ModeRequest::SetMode(DeviceMode::Normal)
|
||||
);
|
||||
}
|
||||
|
||||
// One device is sufficient.
|
||||
tb.mgm_report_tx[0]
|
||||
.send(types::acs::mgm::response::ModeResponse::Mode(
|
||||
DeviceMode::Normal,
|
||||
))
|
||||
.unwrap();
|
||||
tb.mgm_report_tx[1]
|
||||
.send(types::acs::mgm::response::ModeResponse::Mode(
|
||||
DeviceMode::Off,
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
tb.assembly.periodic_operation();
|
||||
assert!(!tb.assembly.mode_transition_active());
|
||||
assert_eq!(tb.assembly.mode(), Mode::Device(DeviceMode::Normal));
|
||||
|
||||
let report = tb.subsystem_report_rx.try_recv().unwrap();
|
||||
assert_eq!(
|
||||
report,
|
||||
response::ModeResponse::Mode(Mode::Device(DeviceMode::Normal))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mode_commanding_fails() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.subsystem_req_tx
|
||||
.send(request::ModeRequest::SetMode(Mode::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,
|
||||
types::acs::mgm::request::ModeRequest::SetMode(DeviceMode::Normal)
|
||||
);
|
||||
}
|
||||
|
||||
// Confirm the mode is set.
|
||||
for tx in tb.mgm_report_tx.iter() {
|
||||
tx.send(types::acs::mgm::response::ModeResponse::Mode(
|
||||
DeviceMode::Off,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
tb.assembly.periodic_operation();
|
||||
assert!(!tb.assembly.mode_transition_active());
|
||||
assert_eq!(tb.assembly.mode(), Mode::NoModeKeeping);
|
||||
|
||||
let report = tb.subsystem_report_rx.try_recv().unwrap();
|
||||
assert_eq!(
|
||||
report,
|
||||
response::ModeResponse::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(Mode::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,
|
||||
types::acs::mgm::request::ModeRequest::SetMode(DeviceMode::Normal)
|
||||
);
|
||||
}
|
||||
|
||||
// Confirm the mode is set.
|
||||
for tx in tb.mgm_report_tx.iter() {
|
||||
tx.send(types::acs::mgm::response::ModeResponse::Mode(
|
||||
DeviceMode::Normal,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
tb.assembly.periodic_operation();
|
||||
assert!(!tb.assembly.mode_transition_active());
|
||||
assert_eq!(tb.assembly.mode(), Mode::Device(DeviceMode::Normal));
|
||||
|
||||
let report = tb.subsystem_report_rx.try_recv().unwrap();
|
||||
assert_eq!(
|
||||
report,
|
||||
response::ModeResponse::Mode(Mode::Device(DeviceMode::Normal))
|
||||
);
|
||||
|
||||
for tx in tb.mgm_report_tx.iter() {
|
||||
tx.send(types::acs::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,
|
||||
types::acs::mgm::request::ModeRequest::SetMode(DeviceMode::Normal)
|
||||
);
|
||||
}
|
||||
|
||||
// Let the mode keeping fail.
|
||||
for tx in tb.mgm_report_tx.iter() {
|
||||
tx.send(types::acs::mgm::response::ModeResponse::Mode(
|
||||
DeviceMode::Off,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
tb.assembly.periodic_operation();
|
||||
let report = tb.subsystem_report_rx.try_recv().unwrap();
|
||||
assert_eq!(
|
||||
report,
|
||||
response::ModeResponse::CanNotKeepMode([Some(DeviceMode::Off), Some(DeviceMode::Off)])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,876 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::mpsc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use example_std::{HkHelperSingleSet, TmtcQueues};
|
||||
use minisim_types::acs::mgt as sim_mgt;
|
||||
use minisim_types::{SimReply, SimRequest, SimRequestWithTime};
|
||||
use satrs::fdir::{FaultCounterStd, RecoveryEvent};
|
||||
use satrs::health::HealthTableMapSync;
|
||||
use satrs::spacepackets::CcsdsPacketIdAndPsc;
|
||||
use types::acs::mgt::{
|
||||
self, HkSet,
|
||||
request::{ModeRequest, Request},
|
||||
response::{ModeResponse, Response},
|
||||
};
|
||||
use types::pcdu::SwitchId;
|
||||
use types::{ComponentId, DeviceMode, HealthRequest, HkRequestType};
|
||||
|
||||
use crate::ccsds::pack_ccsds_tm_packet_for_now;
|
||||
use crate::device_fdir::{DeviceFdir, FdirEvent};
|
||||
use crate::device_mode::{ModeTransitionEvent, SwitchAndModeHelper};
|
||||
use crate::eps::PowerSwitchHelper;
|
||||
|
||||
// The handler blocks while waiting for a reply, so this must be well below the cycle time
|
||||
// of the ACS thread.
|
||||
pub const REPLY_TIMEOUT: Duration = Duration::from_millis(50);
|
||||
pub const REPLY_FAULT_THRESHOLD: u32 = 3;
|
||||
pub const REPLY_FAULT_DECREMENT_AFTER: Duration = Duration::from_secs(30);
|
||||
|
||||
/// Interface for ideal device which never fails.
|
||||
#[derive(Default)]
|
||||
pub struct DummyInterface {
|
||||
dipole: mgt::Dipole,
|
||||
torque_end: Option<Instant>,
|
||||
}
|
||||
|
||||
impl DummyInterface {
|
||||
fn transfer(&mut self, frame: &[u8]) -> Option<Vec<u8>> {
|
||||
let reply = match sim_mgt::Request::from_frame(frame).ok()? {
|
||||
sim_mgt::Request::ApplyTorque { duration, dipole } => {
|
||||
self.dipole = dipole;
|
||||
self.torque_end = Some(Instant::now() + duration);
|
||||
sim_mgt::Reply::Ack
|
||||
}
|
||||
sim_mgt::Request::RequestHk => {
|
||||
let torquing = self.torque_end.is_some_and(|end| Instant::now() < end);
|
||||
sim_mgt::Reply::Hk(sim_mgt::HkSet {
|
||||
dipole: if torquing {
|
||||
self.dipole
|
||||
} else {
|
||||
mgt::Dipole::default()
|
||||
},
|
||||
torquing,
|
||||
})
|
||||
}
|
||||
};
|
||||
Some(reply.to_frame())
|
||||
}
|
||||
}
|
||||
|
||||
/// Records all sent frames and returns injected reply frames.
|
||||
#[derive(Default)]
|
||||
pub struct TestInterface {
|
||||
pub sent_frames: Vec<Vec<u8>>,
|
||||
pub replies: VecDeque<Vec<u8>>,
|
||||
}
|
||||
|
||||
pub struct SimInterface {
|
||||
pub sim_request_tx: mpsc::Sender<SimRequestWithTime>,
|
||||
pub sim_reply_rx: mpsc::Receiver<SimReply>,
|
||||
}
|
||||
|
||||
impl SimInterface {
|
||||
fn transfer(&mut self, frame: &[u8]) -> Option<Vec<u8>> {
|
||||
// Replies which arrived after a previous timeout must not be mistaken for this reply.
|
||||
while self.sim_reply_rx.try_recv().is_ok() {}
|
||||
if let Err(e) = self
|
||||
.sim_request_tx
|
||||
.send(SimRequestWithTime::new_with_epoch_time(SimRequest::Mgt(
|
||||
frame.to_vec(),
|
||||
)))
|
||||
{
|
||||
log::error!("failed to send MGT SIM request: {e}");
|
||||
return None;
|
||||
}
|
||||
match self.sim_reply_rx.recv_timeout(REPLY_TIMEOUT).ok()? {
|
||||
SimReply::Mgt(frame) => Some(frame),
|
||||
sim_reply => {
|
||||
log::warn!("unexpected MGT SIM reply: {sim_reply:?}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Frame based transport to the device. The handler implements the protocol on top of it.
|
||||
pub enum MgtCommunication {
|
||||
Dummy(DummyInterface),
|
||||
Sim(SimInterface),
|
||||
#[allow(dead_code)]
|
||||
Test(TestInterface),
|
||||
}
|
||||
|
||||
impl MgtCommunication {
|
||||
/// Sends a request frame and blocks until the reply frame arrives. Returns [None] if there
|
||||
/// was no reply in time.
|
||||
fn transfer(&mut self, frame: &[u8]) -> Option<Vec<u8>> {
|
||||
match self {
|
||||
MgtCommunication::Dummy(dummy) => dummy.transfer(frame),
|
||||
MgtCommunication::Sim(sim) => sim.transfer(frame),
|
||||
MgtCommunication::Test(test) => {
|
||||
test.sent_frames.push(frame.to_vec());
|
||||
test.replies.pop_front()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper component for communication with a parent component, which is usually an assembly
|
||||
/// or a subsystem.
|
||||
pub struct ModeLeafHelper {
|
||||
pub request_rx: mpsc::Receiver<ModeRequest>,
|
||||
pub report_tx: mpsc::SyncSender<ModeResponse>,
|
||||
}
|
||||
|
||||
/// Magnetorquer (MGT) device handler.
|
||||
///
|
||||
/// The device is powered through the PCDU and only accepts torque commands in normal mode.
|
||||
/// In normal mode, the device HK is polled every cycle and cached as the HK set of the handler.
|
||||
/// Every request is answered with exactly one reply. A missing, invalid or unexpected reply is
|
||||
/// a fault which is handled by the FDIR.
|
||||
///
|
||||
/// This device handler includes several components beyond the scope of commanding the device:
|
||||
///
|
||||
/// - The [MgtCommunication] structure models different communication interfaces to the
|
||||
/// physical device.
|
||||
/// - The device manages and commands its own power switch using the [SwitchAndModeHelper].
|
||||
/// - The device FDIR is integrated directly into the device handler using the [DeviceFdir]
|
||||
/// helper.
|
||||
/// - Periodic HK is generated using the [HkHelperSingleSet] helper.
|
||||
/// - The device is a mode leaf in the ACS tree and has a [ModeLeafHelper] for this.
|
||||
pub struct MgtHandler {
|
||||
tmtc_queues: TmtcQueues,
|
||||
pub com: MgtCommunication,
|
||||
hk_set: HkSet,
|
||||
hk_helper: HkHelperSingleSet,
|
||||
switch_and_mode_helper: SwitchAndModeHelper<DeviceMode>,
|
||||
mode_leaf_helper: ModeLeafHelper,
|
||||
fdir: DeviceFdir,
|
||||
event_tx: mpsc::SyncSender<mgt::Event>,
|
||||
}
|
||||
|
||||
impl MgtHandler {
|
||||
pub fn new(
|
||||
tmtc_queues: TmtcQueues,
|
||||
switch_helper: PowerSwitchHelper,
|
||||
com: MgtCommunication,
|
||||
mode_leaf_helper: ModeLeafHelper,
|
||||
mode_timeout: Duration,
|
||||
health_table: HealthTableMapSync,
|
||||
event_tx: mpsc::SyncSender<mgt::Event>,
|
||||
) -> Self {
|
||||
Self {
|
||||
tmtc_queues,
|
||||
com,
|
||||
hk_set: HkSet::default(),
|
||||
hk_helper: HkHelperSingleSet::new(false, Duration::from_millis(200)),
|
||||
switch_and_mode_helper: SwitchAndModeHelper::new(
|
||||
DeviceMode::Off,
|
||||
mode_timeout,
|
||||
switch_helper,
|
||||
SwitchId::Mgt,
|
||||
),
|
||||
mode_leaf_helper,
|
||||
fdir: DeviceFdir::new(
|
||||
"MGT",
|
||||
ComponentId::AcsMgt,
|
||||
health_table,
|
||||
FaultCounterStd::new(REPLY_FAULT_THRESHOLD, REPLY_FAULT_DECREMENT_AFTER),
|
||||
),
|
||||
event_tx,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn mode(&self) -> DeviceMode {
|
||||
self.switch_and_mode_helper.mode()
|
||||
}
|
||||
|
||||
pub fn periodic_operation(&mut self) {
|
||||
self.handle_telecommands();
|
||||
self.handle_mode_leaf_handling();
|
||||
|
||||
self.fdir
|
||||
.periodic_operation(&mut self.switch_and_mode_helper);
|
||||
self.handle_fdir_events();
|
||||
|
||||
if let Some(event) = self.switch_and_mode_helper.handle_mode_transition() {
|
||||
match event {
|
||||
ModeTransitionEvent::Reached(tc_commander) => {
|
||||
self.handle_mode_reached(tc_commander)
|
||||
}
|
||||
ModeTransitionEvent::Failed(tc_commander) => {
|
||||
self.handle_mode_transition_failure(tc_commander)
|
||||
}
|
||||
ModeTransitionEvent::PowerCycleDone => {
|
||||
self.fdir.handle_power_cycle_done();
|
||||
self.handle_fdir_events();
|
||||
}
|
||||
ModeTransitionEvent::PowerCycleFailed { restore_mode } => {
|
||||
self.fdir
|
||||
.handle_power_cycle_failed(&mut self.switch_and_mode_helper, restore_mode);
|
||||
self.handle_fdir_events();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.ready_for_commanding() {
|
||||
self.poll_hk();
|
||||
}
|
||||
|
||||
if self.hk_helper.needs_generation() {
|
||||
self.send_telemetry(None, Response::Hk(self.hk_set));
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_hk(&mut self) {
|
||||
match self.transfer(sim_mgt::Request::RequestHk) {
|
||||
Some(sim_mgt::Reply::Hk(hk)) => {
|
||||
self.fdir.register_success();
|
||||
self.hk_set = HkSet {
|
||||
valid: true,
|
||||
dipole: hk.dipole,
|
||||
torquing: hk.torquing,
|
||||
};
|
||||
}
|
||||
reply => self.register_reply_fault(reply),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns [None] if there was no reply in time or the reply frame was invalid.
|
||||
fn transfer(&mut self, request: sim_mgt::Request) -> Option<sim_mgt::Reply> {
|
||||
let frame = self.com.transfer(&request.to_frame())?;
|
||||
sim_mgt::Reply::from_frame(&frame)
|
||||
.inspect_err(|e| log::warn!("MGT: invalid reply frame {frame:02x?}: {e}"))
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn register_reply_fault(&mut self, reply: Option<sim_mgt::Reply>) {
|
||||
log::warn!("MGT: missing or unexpected reply {reply:?}");
|
||||
self.hk_set.valid = false;
|
||||
self.fdir.register_fault(&mut self.switch_and_mode_helper);
|
||||
self.handle_fdir_events();
|
||||
}
|
||||
|
||||
fn handle_fdir_events(&mut self) {
|
||||
while let Some(event) = self.fdir.next_event() {
|
||||
let event = match event {
|
||||
FdirEvent::FaultThresholdExceeded => mgt::Event::ReplyFaultThresholdExceeded,
|
||||
FdirEvent::Recovery(recovery_event) => {
|
||||
// The device is power cycled or switched off.
|
||||
if matches!(
|
||||
recovery_event,
|
||||
RecoveryEvent::Started | RecoveryEvent::ThresholdExceeded
|
||||
) {
|
||||
self.hk_set = HkSet::default();
|
||||
}
|
||||
mgt::Event::Recovery(recovery_event)
|
||||
}
|
||||
};
|
||||
self.send_event(event);
|
||||
}
|
||||
}
|
||||
|
||||
fn ready_for_commanding(&self) -> bool {
|
||||
self.mode() == DeviceMode::Normal && self.switch_and_mode_helper.target().is_none()
|
||||
}
|
||||
|
||||
fn handle_telecommands(&mut self) {
|
||||
while let Ok(packet) = self.tmtc_queues.tc_rx.try_recv() {
|
||||
let tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
|
||||
let request = match postcard::from_bytes::<Request>(&packet.payload) {
|
||||
Ok(request) => request,
|
||||
Err(e) => {
|
||||
log::warn!("MGT: failed to deserialize request: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
log::info!(
|
||||
"MGT: received request {:?} with TC ID {:#010x}",
|
||||
request,
|
||||
tc_id.raw()
|
||||
);
|
||||
match request {
|
||||
Request::Ping => self.send_telemetry(Some(tc_id), Response::Ok),
|
||||
Request::Hk(hk_request) => self.handle_hk_request(tc_id, hk_request),
|
||||
Request::Mode(ModeRequest::SetMode(mode)) => {
|
||||
self.start_transition(mode, Some(tc_id))
|
||||
}
|
||||
Request::Mode(ModeRequest::ReadMode) => self.send_telemetry(
|
||||
Some(tc_id),
|
||||
Response::Mode(ModeResponse::Mode(
|
||||
self.switch_and_mode_helper.reported_mode(),
|
||||
)),
|
||||
),
|
||||
Request::ApplyTorque { dipole, duration } => {
|
||||
self.handle_torque_command(tc_id, dipole, duration)
|
||||
}
|
||||
Request::Health(HealthRequest::SetHealth(health)) => {
|
||||
log::info!("MGT: setting health to {health:?} via ground command");
|
||||
self.fdir.set_health(health);
|
||||
self.send_telemetry(Some(tc_id), Response::Ok);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_mode_leaf_handling(&mut self) {
|
||||
while let Ok(request) = self.mode_leaf_helper.request_rx.try_recv() {
|
||||
match request {
|
||||
ModeRequest::SetMode(mode) => self.start_transition(mode, None),
|
||||
ModeRequest::ReadMode => self.report_mode_to_parent(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_hk_request(&mut self, tc_id: CcsdsPacketIdAndPsc, hk_request: HkRequestType) {
|
||||
match hk_request {
|
||||
HkRequestType::OneShot => self.send_telemetry(Some(tc_id), Response::Hk(self.hk_set)),
|
||||
HkRequestType::EnablePeriodic(opt_interval) => {
|
||||
self.hk_helper.enabled = true;
|
||||
if let Some(interval) = opt_interval {
|
||||
self.hk_helper.frequency = interval;
|
||||
}
|
||||
}
|
||||
HkRequestType::DisablePeriodic => self.hk_helper.enabled = false,
|
||||
HkRequestType::ModifyInterval(interval) => self.hk_helper.frequency = interval,
|
||||
_ => log::warn!("MGT: unhandled HK request"),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_torque_command(
|
||||
&mut self,
|
||||
tc_id: CcsdsPacketIdAndPsc,
|
||||
dipole: mgt::Dipole,
|
||||
duration: Duration,
|
||||
) {
|
||||
if !self.ready_for_commanding() {
|
||||
log::warn!("MGT: rejecting torque command, device not in normal mode");
|
||||
self.send_telemetry(Some(tc_id), Response::NotInNormalMode);
|
||||
return;
|
||||
}
|
||||
match self.transfer(sim_mgt::Request::ApplyTorque { duration, dipole }) {
|
||||
Some(sim_mgt::Reply::Ack) => {
|
||||
self.fdir.register_success();
|
||||
self.send_telemetry(Some(tc_id), Response::Ok);
|
||||
}
|
||||
reply => {
|
||||
self.register_reply_fault(reply);
|
||||
self.send_telemetry(Some(tc_id), Response::ReplyTimeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn start_transition(
|
||||
&mut self,
|
||||
target_mode: DeviceMode,
|
||||
tc_commander: Option<CcsdsPacketIdAndPsc>,
|
||||
) {
|
||||
log::info!("MGT: transitioning to mode {:?}", target_mode);
|
||||
self.fdir.handle_mode_command(&self.switch_and_mode_helper);
|
||||
if target_mode == DeviceMode::Off {
|
||||
self.hk_set = HkSet::default();
|
||||
}
|
||||
self.switch_and_mode_helper
|
||||
.start_transition(target_mode, tc_commander);
|
||||
}
|
||||
|
||||
fn handle_mode_reached(&mut self, tc_commander: Option<CcsdsPacketIdAndPsc>) {
|
||||
log::info!("MGT: mode {:?} reached", self.mode());
|
||||
self.send_event(mgt::Event::ModeChanged(self.mode()));
|
||||
if tc_commander.is_some() {
|
||||
self.send_telemetry(tc_commander, Response::Ok);
|
||||
}
|
||||
self.report_mode_to_parent();
|
||||
}
|
||||
|
||||
fn handle_mode_transition_failure(&mut self, tc_commander: Option<CcsdsPacketIdAndPsc>) {
|
||||
if tc_commander.is_some() {
|
||||
self.send_telemetry(tc_commander, Response::Mode(ModeResponse::SetModeTimeout));
|
||||
}
|
||||
self.mode_leaf_helper
|
||||
.report_tx
|
||||
.send(ModeResponse::SetModeTimeout)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn report_mode_to_parent(&self) {
|
||||
self.mode_leaf_helper
|
||||
.report_tx
|
||||
.send(ModeResponse::Mode(
|
||||
self.switch_and_mode_helper.reported_mode(),
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn send_event(&self, event: mgt::Event) {
|
||||
if let Err(e) = self.event_tx.send(event) {
|
||||
log::warn!("MGT: failed to send event {:?}: {}", event, e);
|
||||
}
|
||||
}
|
||||
|
||||
fn send_telemetry(&self, tc_id: Option<CcsdsPacketIdAndPsc>, response: Response) {
|
||||
match pack_ccsds_tm_packet_for_now(ComponentId::AcsMgt, tc_id, &response) {
|
||||
Ok(packet) => {
|
||||
if let Err(e) = self.tmtc_queues.tm_tx.send(packet) {
|
||||
log::warn!("MGT: failed to send TM packet: {}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => log::warn!("MGT: failed to pack TM packet: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Mutex;
|
||||
|
||||
use arbitrary_int::u11;
|
||||
use satrs::health::{HealthState, HealthTableProvider};
|
||||
use satrs::spacepackets::SpacePacketHeader;
|
||||
use types::{
|
||||
Apid, Message as _, TcHeader,
|
||||
ccsds::{CcsdsTcPacketOwned, CcsdsTmPacketOwned},
|
||||
pcdu::{SwitchRequest, SwitchState, SwitchStateBinary},
|
||||
};
|
||||
|
||||
use crate::device_fdir::RECOVERY_THRESHOLD;
|
||||
use crate::eps::pcdu::{SharedSwitchSet, SwitchMap, SwitchSet};
|
||||
|
||||
use super::*;
|
||||
|
||||
impl TestInterface {
|
||||
fn sent_requests(&self) -> Vec<sim_mgt::Request> {
|
||||
self.sent_frames
|
||||
.iter()
|
||||
.map(|frame| sim_mgt::Request::from_frame(frame).unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn push_reply(&mut self, reply: sim_mgt::Reply) {
|
||||
self.replies.push_back(reply.to_frame());
|
||||
}
|
||||
}
|
||||
|
||||
struct MgtTestbench {
|
||||
parent_request_tx: mpsc::SyncSender<ModeRequest>,
|
||||
parent_report_rx: mpsc::Receiver<ModeResponse>,
|
||||
shared_switch_set: SharedSwitchSet,
|
||||
switch_rx: mpsc::Receiver<SwitchRequest>,
|
||||
tc_tx: mpsc::SyncSender<CcsdsTcPacketOwned>,
|
||||
tm_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
|
||||
event_rx: mpsc::Receiver<mgt::Event>,
|
||||
health_table: HealthTableMapSync,
|
||||
handler: MgtHandler,
|
||||
}
|
||||
|
||||
impl MgtTestbench {
|
||||
fn new() -> Self {
|
||||
let (parent_request_tx, request_rx) = mpsc::sync_channel(5);
|
||||
let (report_tx, parent_report_rx) = mpsc::sync_channel(5);
|
||||
let (tc_tx, tc_rx) = mpsc::sync_channel(10);
|
||||
let (tm_tx, tm_rx) = mpsc::sync_channel(10);
|
||||
let (switch_tx, switch_rx) = mpsc::sync_channel(10);
|
||||
let (event_tx, event_rx) = mpsc::sync_channel(20);
|
||||
let mut switch_map = SwitchMap::new();
|
||||
switch_map.insert(SwitchId::Mgt, SwitchState::Off);
|
||||
let shared_switch_set = SharedSwitchSet::new(Mutex::new(SwitchSet::new(switch_map)));
|
||||
let health_table = HealthTableMapSync::default();
|
||||
let mut handler = MgtHandler::new(
|
||||
TmtcQueues { tc_rx, tm_tx },
|
||||
PowerSwitchHelper::new(switch_tx, shared_switch_set.clone()),
|
||||
MgtCommunication::Test(TestInterface::default()),
|
||||
ModeLeafHelper {
|
||||
request_rx,
|
||||
report_tx,
|
||||
},
|
||||
Duration::from_millis(100),
|
||||
health_table.clone(),
|
||||
event_tx,
|
||||
);
|
||||
handler.fdir.recovery_off_duration = Duration::ZERO;
|
||||
Self {
|
||||
parent_request_tx,
|
||||
parent_report_rx,
|
||||
shared_switch_set,
|
||||
switch_rx,
|
||||
tc_tx,
|
||||
tm_rx,
|
||||
event_rx,
|
||||
health_table,
|
||||
handler,
|
||||
}
|
||||
}
|
||||
|
||||
fn send_tc(&self, request: Request) {
|
||||
self.tc_tx
|
||||
.send(CcsdsTcPacketOwned::new_with_request(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||
TcHeader::new(ComponentId::AcsMgt, request.message_type()),
|
||||
request,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn next_response(&self) -> Response {
|
||||
let tm = self.tm_rx.try_recv().expect("no TM generated");
|
||||
assert_eq!(tm.tm_header.sender_id, ComponentId::AcsMgt);
|
||||
postcard::from_bytes(&tm.payload).expect("invalid MGT response")
|
||||
}
|
||||
|
||||
/// Completes the power switch handshake for a commanded switch-on.
|
||||
fn switch_to_normal(&mut self) {
|
||||
self.send_tc(Request::Mode(ModeRequest::SetMode(DeviceMode::Normal)));
|
||||
self.handler.periodic_operation();
|
||||
self.set_switch_state(SwitchState::On);
|
||||
self.handler.periodic_operation();
|
||||
assert_eq!(self.handler.mode(), DeviceMode::Normal);
|
||||
assert_eq!(self.next_response(), Response::Ok);
|
||||
}
|
||||
|
||||
fn test_interface(&mut self) -> &mut TestInterface {
|
||||
match &mut self.handler.com {
|
||||
MgtCommunication::Test(test) => test,
|
||||
_ => panic!("unexpected MGT interface"),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_switch_state(&self, state: SwitchState) {
|
||||
self.shared_switch_set
|
||||
.lock()
|
||||
.unwrap()
|
||||
.set_switch_state(SwitchId::Mgt, state);
|
||||
}
|
||||
|
||||
fn health(&self) -> Option<HealthState> {
|
||||
self.health_table.health(ComponentId::AcsMgt.into())
|
||||
}
|
||||
|
||||
fn drain_events(&self) -> Vec<mgt::Event> {
|
||||
self.event_rx.try_iter().collect()
|
||||
}
|
||||
|
||||
/// No replies are injected, so every HK poll is a fault. The poll in the cycle which
|
||||
/// reached normal mode already registered the first fault.
|
||||
fn exceed_reply_fault_threshold(&mut self) {
|
||||
for _ in 0..REPLY_FAULT_THRESHOLD {
|
||||
self.handler.periodic_operation();
|
||||
}
|
||||
}
|
||||
|
||||
/// Drives a started power cycle recovery to completion, completing both power switch
|
||||
/// handshakes.
|
||||
fn complete_power_cycle(&mut self) {
|
||||
self.handler.periodic_operation();
|
||||
self.set_switch_state(SwitchState::Off);
|
||||
self.handler.periodic_operation();
|
||||
assert_eq!(self.handler.mode(), DeviceMode::Off);
|
||||
self.handler.periodic_operation();
|
||||
self.set_switch_state(SwitchState::On);
|
||||
self.handler.periodic_operation();
|
||||
assert_eq!(self.handler.mode(), DeviceMode::Normal);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_initial_state_no_polling() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench.handler.periodic_operation();
|
||||
assert_eq!(testbench.handler.mode(), DeviceMode::Off);
|
||||
assert!(testbench.test_interface().sent_frames.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_switch_to_normal() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench.send_tc(Request::Mode(ModeRequest::SetMode(DeviceMode::Normal)));
|
||||
testbench.handler.periodic_operation();
|
||||
let switch_request = testbench.switch_rx.try_recv().expect("no switch request");
|
||||
assert_eq!(switch_request.switch_id, SwitchId::Mgt);
|
||||
assert_eq!(switch_request.target_state, SwitchStateBinary::On);
|
||||
assert_eq!(testbench.handler.mode(), DeviceMode::Off);
|
||||
|
||||
testbench.set_switch_state(SwitchState::On);
|
||||
testbench.handler.periodic_operation();
|
||||
assert_eq!(testbench.handler.mode(), DeviceMode::Normal);
|
||||
assert_eq!(testbench.next_response(), Response::Ok);
|
||||
assert!(matches!(
|
||||
testbench.event_rx.try_recv(),
|
||||
Ok(mgt::Event::ModeChanged(DeviceMode::Normal))
|
||||
));
|
||||
assert_eq!(
|
||||
testbench.parent_report_rx.try_recv(),
|
||||
Ok(ModeResponse::Mode(DeviceMode::Normal))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mode_command_from_parent() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench
|
||||
.parent_request_tx
|
||||
.send(ModeRequest::SetMode(DeviceMode::Normal))
|
||||
.unwrap();
|
||||
testbench.handler.periodic_operation();
|
||||
testbench.set_switch_state(SwitchState::On);
|
||||
testbench.handler.periodic_operation();
|
||||
assert_eq!(
|
||||
testbench.parent_report_rx.try_recv(),
|
||||
Ok(ModeResponse::Mode(DeviceMode::Normal))
|
||||
);
|
||||
// Commanded by the parent, so no TC response is expected.
|
||||
assert!(testbench.tm_rx.try_recv().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_torque_command_rejected_when_off() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench.send_tc(Request::ApplyTorque {
|
||||
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
|
||||
duration: Duration::from_millis(100),
|
||||
});
|
||||
testbench.handler.periodic_operation();
|
||||
assert_eq!(testbench.next_response(), Response::NotInNormalMode);
|
||||
assert!(testbench.test_interface().sent_frames.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_torque_command_forwarded_in_normal_mode() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench.switch_to_normal();
|
||||
testbench.test_interface().sent_frames.clear();
|
||||
testbench.test_interface().push_reply(sim_mgt::Reply::Ack);
|
||||
|
||||
testbench.send_tc(Request::ApplyTorque {
|
||||
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
|
||||
duration: Duration::from_millis(100),
|
||||
});
|
||||
testbench.handler.periodic_operation();
|
||||
assert_eq!(testbench.next_response(), Response::Ok);
|
||||
assert_eq!(
|
||||
testbench.test_interface().sent_requests().first(),
|
||||
Some(&sim_mgt::Request::ApplyTorque {
|
||||
duration: Duration::from_millis(100),
|
||||
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_torque_command_without_ack() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench.switch_to_normal();
|
||||
testbench.send_tc(Request::ApplyTorque {
|
||||
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
|
||||
duration: Duration::from_millis(100),
|
||||
});
|
||||
testbench.handler.periodic_operation();
|
||||
assert_eq!(testbench.next_response(), Response::ReplyTimeout);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hk_polling_updates_hk_set() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench.switch_to_normal();
|
||||
assert!(
|
||||
testbench
|
||||
.test_interface()
|
||||
.sent_requests()
|
||||
.contains(&sim_mgt::Request::RequestHk)
|
||||
);
|
||||
|
||||
testbench
|
||||
.test_interface()
|
||||
.push_reply(sim_mgt::Reply::Hk(sim_mgt::HkSet {
|
||||
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
|
||||
torquing: true,
|
||||
}));
|
||||
testbench.handler.periodic_operation();
|
||||
testbench.send_tc(Request::Hk(HkRequestType::OneShot));
|
||||
testbench.handler.periodic_operation();
|
||||
assert_eq!(
|
||||
testbench.next_response(),
|
||||
Response::Hk(HkSet {
|
||||
valid: true,
|
||||
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
|
||||
torquing: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_switch_off() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench.switch_to_normal();
|
||||
testbench.drain_events();
|
||||
while testbench.parent_report_rx.try_recv().is_ok() {}
|
||||
|
||||
testbench.send_tc(Request::Mode(ModeRequest::SetMode(DeviceMode::Off)));
|
||||
testbench.handler.periodic_operation();
|
||||
let switch_request = testbench
|
||||
.switch_rx
|
||||
.try_iter()
|
||||
.last()
|
||||
.expect("no switch request");
|
||||
assert_eq!(switch_request.target_state, SwitchStateBinary::Off);
|
||||
testbench.set_switch_state(SwitchState::Off);
|
||||
testbench.handler.periodic_operation();
|
||||
assert_eq!(testbench.handler.mode(), DeviceMode::Off);
|
||||
assert_eq!(testbench.next_response(), Response::Ok);
|
||||
assert!(matches!(
|
||||
testbench.drain_events().as_slice(),
|
||||
[mgt::Event::ModeChanged(DeviceMode::Off)]
|
||||
));
|
||||
assert_eq!(
|
||||
testbench.parent_report_rx.try_recv(),
|
||||
Ok(ModeResponse::Mode(DeviceMode::Off))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hk_set_invalid_after_switch_off() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench.switch_to_normal();
|
||||
testbench
|
||||
.test_interface()
|
||||
.push_reply(sim_mgt::Reply::Hk(sim_mgt::HkSet {
|
||||
dipole: mgt::Dipole::default(),
|
||||
torquing: false,
|
||||
}));
|
||||
testbench.handler.periodic_operation();
|
||||
|
||||
testbench.send_tc(Request::Mode(ModeRequest::SetMode(DeviceMode::Off)));
|
||||
testbench.send_tc(Request::Hk(HkRequestType::OneShot));
|
||||
testbench.handler.periodic_operation();
|
||||
assert_eq!(testbench.next_response(), Response::Hk(HkSet::default()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_periodic_hk() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench.send_tc(Request::Hk(HkRequestType::EnablePeriodic(Some(
|
||||
Duration::ZERO,
|
||||
))));
|
||||
testbench.handler.periodic_operation();
|
||||
assert!(matches!(testbench.next_response(), Response::Hk(_)));
|
||||
let tm = testbench.tm_rx.try_recv();
|
||||
assert!(tm.is_err(), "only one HK packet per cycle expected");
|
||||
|
||||
testbench.send_tc(Request::Hk(HkRequestType::DisablePeriodic));
|
||||
testbench.handler.periodic_operation();
|
||||
assert!(testbench.tm_rx.try_recv().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_reply_no_fault() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench
|
||||
.test_interface()
|
||||
.push_reply(sim_mgt::Reply::Hk(sim_mgt::HkSet {
|
||||
dipole: mgt::Dipole::default(),
|
||||
torquing: false,
|
||||
}));
|
||||
testbench.switch_to_normal();
|
||||
assert_eq!(testbench.handler.fdir.fault_count(), 0);
|
||||
assert!(testbench.handler.hk_set.valid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_reply_registers_fault() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench.switch_to_normal();
|
||||
assert_eq!(testbench.handler.fdir.fault_count(), 1);
|
||||
assert!(!testbench.handler.hk_set.valid);
|
||||
assert!(matches!(
|
||||
testbench.drain_events().as_slice(),
|
||||
[mgt::Event::ModeChanged(DeviceMode::Normal)]
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unexpected_reply_registers_fault() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench.test_interface().push_reply(sim_mgt::Reply::Ack);
|
||||
testbench.switch_to_normal();
|
||||
assert_eq!(testbench.handler.fdir.fault_count(), 1);
|
||||
assert!(matches!(
|
||||
testbench.drain_events().as_slice(),
|
||||
[mgt::Event::ModeChanged(DeviceMode::Normal)]
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_reply_registers_fault() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench.test_interface().replies.push_back(vec![0xff]);
|
||||
testbench.switch_to_normal();
|
||||
assert_eq!(testbench.handler.fdir.fault_count(), 1);
|
||||
assert!(matches!(
|
||||
testbench.drain_events().as_slice(),
|
||||
[mgt::Event::ModeChanged(DeviceMode::Normal)]
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reply_fault_threshold_starts_recovery() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench.switch_to_normal();
|
||||
testbench.drain_events();
|
||||
while testbench.parent_report_rx.try_recv().is_ok() {}
|
||||
testbench.exceed_reply_fault_threshold();
|
||||
assert_eq!(testbench.health(), Some(HealthState::NeedsRecovery));
|
||||
assert!(matches!(
|
||||
testbench.drain_events().as_slice(),
|
||||
[
|
||||
mgt::Event::ReplyFaultThresholdExceeded,
|
||||
mgt::Event::Recovery(RecoveryEvent::Started)
|
||||
]
|
||||
));
|
||||
|
||||
testbench.complete_power_cycle();
|
||||
assert_eq!(testbench.health(), Some(HealthState::Healthy));
|
||||
assert!(matches!(
|
||||
testbench.drain_events().as_slice(),
|
||||
[mgt::Event::Recovery(RecoveryEvent::Done)]
|
||||
));
|
||||
// The power cycle is hidden from the parent.
|
||||
assert!(testbench.parent_report_rx.try_recv().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unresponsive_device_marked_faulty() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench.switch_to_normal();
|
||||
for _ in 0..RECOVERY_THRESHOLD {
|
||||
testbench.exceed_reply_fault_threshold();
|
||||
assert_eq!(testbench.health(), Some(HealthState::NeedsRecovery));
|
||||
testbench.complete_power_cycle();
|
||||
}
|
||||
testbench.drain_events();
|
||||
testbench.exceed_reply_fault_threshold();
|
||||
assert_eq!(testbench.health(), Some(HealthState::Faulty));
|
||||
assert!(matches!(
|
||||
testbench.drain_events().as_slice(),
|
||||
[
|
||||
mgt::Event::ReplyFaultThresholdExceeded,
|
||||
mgt::Event::Recovery(RecoveryEvent::ThresholdExceeded)
|
||||
]
|
||||
));
|
||||
assert_eq!(
|
||||
testbench.handler.switch_and_mode_helper.target(),
|
||||
Some(DeviceMode::Off)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_health() {
|
||||
let mut testbench = MgtTestbench::new();
|
||||
testbench.send_tc(Request::Health(HealthRequest::SetHealth(
|
||||
HealthState::Faulty,
|
||||
)));
|
||||
testbench.handler.periodic_operation();
|
||||
assert_eq!(testbench.next_response(), Response::Ok);
|
||||
assert_eq!(testbench.health(), Some(HealthState::Faulty));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod ctrl;
|
||||
pub mod mgm;
|
||||
pub mod mgm_assembly;
|
||||
pub mod mgt;
|
||||
pub mod subsystem;
|
||||
@@ -0,0 +1,406 @@
|
||||
#![allow(dead_code)]
|
||||
use std::{
|
||||
sync::mpsc::{self, Receiver, SyncSender},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use example_std::{ModeHelper, TmtcQueues};
|
||||
use satrs::{
|
||||
mode_tree::{
|
||||
ModeStoreProvider, ModeStoreVec, SequenceModeTables, SequenceTableEntry,
|
||||
SequenceTableMapTable, SequenceTablesMapValue, TargetModeTables,
|
||||
},
|
||||
spacepackets::CcsdsPacketIdAndPsc,
|
||||
subsystem::{
|
||||
ModeCommandingResult, ModeRaw, ModeResponse, ModeSetRequest, ModeTreeHelperError,
|
||||
SubsystemCommandingHelper, SubsystemHelperResult,
|
||||
},
|
||||
};
|
||||
use types::{
|
||||
ComponentId,
|
||||
acs::subsystem::{Mode, response},
|
||||
};
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum TransitionState {
|
||||
#[default]
|
||||
Idle,
|
||||
AwaitingReplies,
|
||||
}
|
||||
|
||||
fn build_sequence_tables() -> SequenceModeTables {
|
||||
let mut seq_tables = SequenceModeTables::default();
|
||||
|
||||
let mut off_table = SequenceTablesMapValue::new("OFF");
|
||||
let mut off_step_0 = SequenceTableMapTable::new("OFF_STEP_0");
|
||||
off_step_0.add_entry(SequenceTableEntry::new(
|
||||
"OFF_CTRL_PASSIVE",
|
||||
ComponentId::AcsController as satrs::ComponentId,
|
||||
types::acs::ctrl::Mode::Passive.into(),
|
||||
true,
|
||||
));
|
||||
off_table.add_sequence_table(off_step_0);
|
||||
let mut off_step_1 = SequenceTableMapTable::new("OFF_STEP_1");
|
||||
off_step_1.add_entry(SequenceTableEntry::new(
|
||||
"OFF_MGM_ASSY_OFF",
|
||||
ComponentId::AcsMgmAssembly as satrs::ComponentId,
|
||||
types::acs::mgm_assembly::Mode::Device(types::DeviceMode::Off).into(),
|
||||
false,
|
||||
));
|
||||
off_step_1.add_entry(SequenceTableEntry::new(
|
||||
"OFF_MGT_OFF",
|
||||
ComponentId::AcsMgt as satrs::ComponentId,
|
||||
types::DeviceMode::Off.into(),
|
||||
false,
|
||||
));
|
||||
off_table.add_sequence_table(off_step_1);
|
||||
seq_tables.0.insert(Mode::Off as ModeRaw, off_table);
|
||||
|
||||
let mut safe_table = SequenceTablesMapValue::new("SAFE");
|
||||
let mut safe_step_0 = SequenceTableMapTable::new("SAFE_STEP_0");
|
||||
safe_step_0.add_entry(SequenceTableEntry::new(
|
||||
"SAFE_MGM_ASSY_NORMAL",
|
||||
ComponentId::AcsMgmAssembly as satrs::ComponentId,
|
||||
types::acs::mgm_assembly::Mode::Device(types::DeviceMode::Normal).into(),
|
||||
false,
|
||||
));
|
||||
safe_step_0.add_entry(SequenceTableEntry::new(
|
||||
"SAFE_MGT_NORMAL",
|
||||
ComponentId::AcsMgt as satrs::ComponentId,
|
||||
types::DeviceMode::Normal.into(),
|
||||
false,
|
||||
));
|
||||
safe_table.add_sequence_table(safe_step_0);
|
||||
let mut safe_step_1 = SequenceTableMapTable::new("SAFE_STEP_1");
|
||||
safe_step_1.add_entry(SequenceTableEntry::new(
|
||||
"SAFE_CTRL_SAFE",
|
||||
ComponentId::AcsController as satrs::ComponentId,
|
||||
types::acs::ctrl::Mode::Safe.into(),
|
||||
false,
|
||||
));
|
||||
safe_table.add_sequence_table(safe_step_1);
|
||||
seq_tables.0.insert(Mode::Safe as ModeRaw, safe_table);
|
||||
|
||||
seq_tables
|
||||
}
|
||||
|
||||
fn ctrl_response_to_mode_response(
|
||||
response: types::acs::ctrl::response::ModeReport,
|
||||
) -> ModeResponse {
|
||||
let sender_id = ComponentId::AcsController as satrs::ComponentId;
|
||||
match response {
|
||||
types::acs::ctrl::response::ModeReport::Mode(mode) => ModeResponse {
|
||||
request_id: 0,
|
||||
sender_id,
|
||||
reported_mode: mode.into(),
|
||||
success: true,
|
||||
},
|
||||
types::acs::ctrl::response::ModeReport::WrongMode(_) => ModeResponse {
|
||||
request_id: 0,
|
||||
sender_id,
|
||||
reported_mode: 0,
|
||||
success: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn mgm_assy_response_to_mode_response(
|
||||
response: types::acs::mgm_assembly::response::ModeResponse,
|
||||
) -> ModeResponse {
|
||||
let sender_id = ComponentId::AcsMgmAssembly as satrs::ComponentId;
|
||||
match response {
|
||||
types::acs::mgm_assembly::response::ModeResponse::Mode(mode) => ModeResponse {
|
||||
request_id: 0,
|
||||
sender_id,
|
||||
reported_mode: mode.into(),
|
||||
success: true,
|
||||
},
|
||||
types::acs::mgm_assembly::response::ModeResponse::SetModeTimeout(_)
|
||||
| types::acs::mgm_assembly::response::ModeResponse::WrongMode(_)
|
||||
| types::acs::mgm_assembly::response::ModeResponse::CanNotKeepMode(_) => ModeResponse {
|
||||
request_id: 0,
|
||||
sender_id,
|
||||
reported_mode: 0,
|
||||
success: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn mgt_response_to_mode_response(
|
||||
response: types::acs::mgt::response::ModeResponse,
|
||||
) -> ModeResponse {
|
||||
let sender_id = ComponentId::AcsMgt as satrs::ComponentId;
|
||||
match response {
|
||||
types::acs::mgt::response::ModeResponse::Mode(mode) => ModeResponse {
|
||||
request_id: 0,
|
||||
sender_id,
|
||||
reported_mode: mode.into(),
|
||||
success: true,
|
||||
},
|
||||
types::acs::mgt::response::ModeResponse::SetModeTimeout => ModeResponse {
|
||||
request_id: 0,
|
||||
sender_id,
|
||||
reported_mode: 0,
|
||||
success: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ModeRequestSenders {
|
||||
pub mode_request_ctrl: SyncSender<types::acs::ctrl::request::ModeRequest>,
|
||||
pub mode_request_mgm_assy: SyncSender<types::acs::mgm_assembly::request::ModeRequest>,
|
||||
pub mode_request_mgt: SyncSender<types::acs::mgt::request::ModeRequest>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ModeReportReceivers {
|
||||
pub mode_response_ctrl: Receiver<types::acs::ctrl::response::ModeReport>,
|
||||
pub mode_response_mgm_assy: Receiver<types::acs::mgm_assembly::response::ModeResponse>,
|
||||
pub mode_response_mgt: Receiver<types::acs::mgt::response::ModeResponse>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Subsystem {
|
||||
mode_helper: ModeHelper<types::acs::subsystem::Mode, TransitionState>,
|
||||
mode_request_senders: ModeRequestSenders,
|
||||
mode_report_receivers: ModeReportReceivers,
|
||||
tmtc_queues: TmtcQueues,
|
||||
subsystem_helper: SubsystemCommandingHelper,
|
||||
}
|
||||
|
||||
impl Subsystem {
|
||||
pub const ID: ComponentId = ComponentId::AcsSubsystem;
|
||||
|
||||
pub fn new(
|
||||
mode_request_senders: ModeRequestSenders,
|
||||
mode_report_receivers: ModeReportReceivers,
|
||||
tmtc_queues: TmtcQueues,
|
||||
) -> Self {
|
||||
let mut mode_store_vec = ModeStoreVec::default();
|
||||
mode_store_vec
|
||||
.add_component(
|
||||
ComponentId::AcsMgmAssembly as satrs::ComponentId,
|
||||
types::acs::mgm_assembly::Mode::NoModeKeeping.into(),
|
||||
)
|
||||
.unwrap();
|
||||
mode_store_vec
|
||||
.add_component(
|
||||
ComponentId::AcsController as satrs::ComponentId,
|
||||
types::acs::ctrl::Mode::Passive.into(),
|
||||
)
|
||||
.unwrap();
|
||||
mode_store_vec
|
||||
.add_component(
|
||||
ComponentId::AcsMgt as satrs::ComponentId,
|
||||
types::DeviceMode::Off.into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let target_tables = TargetModeTables::default();
|
||||
|
||||
let sequence_tables = build_sequence_tables();
|
||||
|
||||
Self {
|
||||
mode_helper: ModeHelper::new(
|
||||
types::acs::subsystem::Mode::Off,
|
||||
Duration::from_millis(2000),
|
||||
),
|
||||
mode_request_senders,
|
||||
mode_report_receivers,
|
||||
tmtc_queues,
|
||||
subsystem_helper: SubsystemCommandingHelper::new(
|
||||
mode_store_vec,
|
||||
target_tables,
|
||||
sequence_tables,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn periodic_operation(&mut self) {
|
||||
self.handle_telecommands();
|
||||
let mut mode_requests = Vec::new();
|
||||
|
||||
while let Ok(response) = self.mode_report_receivers.mode_response_ctrl.try_recv() {
|
||||
let result = self
|
||||
.subsystem_helper
|
||||
.state_machine(Some(ctrl_response_to_mode_response(response)), |request| {
|
||||
mode_requests.push(request)
|
||||
});
|
||||
self.handle_state_machine_result(result);
|
||||
}
|
||||
while let Ok(response) = self.mode_report_receivers.mode_response_mgm_assy.try_recv() {
|
||||
let result = self.subsystem_helper.state_machine(
|
||||
Some(mgm_assy_response_to_mode_response(response)),
|
||||
|request| mode_requests.push(request),
|
||||
);
|
||||
self.handle_state_machine_result(result);
|
||||
}
|
||||
while let Ok(response) = self.mode_report_receivers.mode_response_mgt.try_recv() {
|
||||
let result = self
|
||||
.subsystem_helper
|
||||
.state_machine(Some(mgt_response_to_mode_response(response)), |request| {
|
||||
mode_requests.push(request)
|
||||
});
|
||||
self.handle_state_machine_result(result);
|
||||
}
|
||||
|
||||
let result = self
|
||||
.subsystem_helper
|
||||
.state_machine(None, |request| mode_requests.push(request));
|
||||
self.handle_state_machine_result(result);
|
||||
|
||||
for request in mode_requests {
|
||||
self.handle_mode_set_request(request);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_state_machine_result(
|
||||
&mut self,
|
||||
result: Result<SubsystemHelperResult, ModeTreeHelperError>,
|
||||
) {
|
||||
match result {
|
||||
Ok(result) => match result {
|
||||
SubsystemHelperResult::Idle => (),
|
||||
SubsystemHelperResult::TargetKeeping => (),
|
||||
SubsystemHelperResult::ModeCommanding(mode_commanding_result) => {
|
||||
match mode_commanding_result {
|
||||
ModeCommandingResult::Done => {
|
||||
let target_mode_typed =
|
||||
self.subsystem_helper.seq_exec_helper.target_mode();
|
||||
let target_mode = target_mode_typed.map(Mode::try_from);
|
||||
log::info!(
|
||||
"ACS SS: mode commanding for target mode {:?} done",
|
||||
target_mode
|
||||
);
|
||||
self.finish_mode_transition();
|
||||
}
|
||||
ModeCommandingResult::StepDone => log::info!("mode commanding step done"),
|
||||
ModeCommandingResult::AwaitingSuccessCheck => {
|
||||
log::info!("ACS SS: mode commanding awaiting success check")
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("mode tree helper error: {}", e);
|
||||
self.mode_helper.finish(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn finish_mode_transition(&mut self) {
|
||||
let tc_commander = self.mode_helper.finish(true);
|
||||
if let Some(requestor) = tc_commander {
|
||||
self.send_telemetry(
|
||||
Some(requestor),
|
||||
response::Response::Mode(response::ModeResponse::Mode(self.mode_helper.current)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_mode_set_request(&mut self, request: ModeSetRequest) {
|
||||
let target_id = ComponentId::try_from(request.target_id);
|
||||
if let Ok(target_id) = target_id {
|
||||
match target_id {
|
||||
ComponentId::AcsMgmAssembly => self
|
||||
.mode_request_senders
|
||||
.mode_request_mgm_assy
|
||||
.send(types::acs::mgm_assembly::request::ModeRequest::SetMode(
|
||||
types::acs::mgm_assembly::Mode::try_from(request.mode).unwrap(),
|
||||
))
|
||||
.unwrap(),
|
||||
ComponentId::AcsController => self
|
||||
.mode_request_senders
|
||||
.mode_request_ctrl
|
||||
.send(types::acs::ctrl::request::ModeRequest::SetMode(
|
||||
types::acs::ctrl::Mode::try_from(request.mode).unwrap(),
|
||||
))
|
||||
.unwrap(),
|
||||
ComponentId::AcsMgt => self
|
||||
.mode_request_senders
|
||||
.mode_request_mgt
|
||||
.send(types::acs::mgt::request::ModeRequest::SetMode(
|
||||
types::DeviceMode::try_from(request.mode).unwrap(),
|
||||
))
|
||||
.unwrap(),
|
||||
_ => {
|
||||
log::error!("invalid target ID {:?} for mode command", target_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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::<types::acs::subsystem::request::Request>(
|
||||
&packet.payload,
|
||||
) {
|
||||
Ok(request) => match request {
|
||||
types::acs::subsystem::request::Request::Ping => {
|
||||
self.send_telemetry(Some(tc_id), response::Response::Ok)
|
||||
}
|
||||
types::acs::subsystem::request::Request::Mode(mode_request) => {
|
||||
self.handle_mode_request(Some(tc_id), mode_request);
|
||||
}
|
||||
},
|
||||
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 handle_mode_request(
|
||||
&mut self,
|
||||
tc_id: Option<CcsdsPacketIdAndPsc>,
|
||||
mode_request: types::acs::subsystem::request::ModeRequest,
|
||||
) {
|
||||
match mode_request {
|
||||
types::acs::subsystem::request::ModeRequest::SetMode(target_mode) => {
|
||||
self.mode_helper.start(target_mode);
|
||||
self.mode_helper.tc_commander = tc_id;
|
||||
if let Err(e) = self
|
||||
.subsystem_helper
|
||||
.start_command_sequence(target_mode as ModeRaw)
|
||||
{
|
||||
log::error!("error starting command sequence: {}", e);
|
||||
}
|
||||
}
|
||||
types::acs::subsystem::request::ModeRequest::ReadMode => {
|
||||
self.send_telemetry(
|
||||
None,
|
||||
response::Response::Mode(response::ModeResponse::Mode(
|
||||
self.mode_helper.current,
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_telemetry(
|
||||
&self,
|
||||
tc_id: Option<CcsdsPacketIdAndPsc>,
|
||||
response: types::acs::subsystem::response::Response,
|
||||
) {
|
||||
match crate::ccsds::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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
use arbitrary_int::u11;
|
||||
use satrs::spacepackets::{
|
||||
CcsdsPacketIdAndPsc, SpHeader,
|
||||
time::{StdTimestampError, cds::CdsTime},
|
||||
};
|
||||
use serde::Serialize;
|
||||
use types::{Apid, ComponentId, Message, TmHeader, ccsds::CcsdsTmPacketOwned};
|
||||
|
||||
#[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,
|
||||
)?)
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
use arbitrary_int::u11;
|
||||
use lazy_static::lazy_static;
|
||||
use satrs::{
|
||||
legacy::res_code::ResultU16,
|
||||
spacepackets::{PacketId, PacketType},
|
||||
};
|
||||
use satrs_mib::res_code::ResultU16Info;
|
||||
use satrs_mib::resultcode;
|
||||
use std::{collections::HashSet, net::Ipv4Addr};
|
||||
use strum::IntoEnumIterator as _;
|
||||
|
||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
use satrs::pool::{StaticMemoryPool, StaticPoolConfig};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, TryFromPrimitive, IntoPrimitive)]
|
||||
#[repr(u8)]
|
||||
pub enum CustomPusServiceId {
|
||||
Mode = 200,
|
||||
Health = 201,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
|
||||
pub enum RequestTargetId {
|
||||
AcsSubsystem = 1,
|
||||
}
|
||||
|
||||
pub const AOCS_APID: u16 = 1;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum GroupId {
|
||||
Tmtc = 0,
|
||||
Hk = 1,
|
||||
Mode = 2,
|
||||
}
|
||||
|
||||
pub const OBSW_SERVER_ADDR: Ipv4Addr = Ipv4Addr::UNSPECIFIED;
|
||||
pub const SERVER_PORT: u16 = 7301;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref PACKET_ID_VALIDATOR: HashSet<PacketId> = {
|
||||
let mut set = HashSet::new();
|
||||
for id in types::Apid::iter() {
|
||||
set.insert(PacketId::new(PacketType::Tc, true, u11::new(id as u16)));
|
||||
}
|
||||
set
|
||||
};
|
||||
pub static ref APID_VALIDATOR: HashSet<u16> = {
|
||||
let mut set = HashSet::new();
|
||||
for id in types::Apid::iter() {
|
||||
set.insert(id as u16);
|
||||
}
|
||||
set
|
||||
};
|
||||
}
|
||||
|
||||
pub mod tmtc_err {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[resultcode]
|
||||
pub const INVALID_PUS_SERVICE: ResultU16 = ResultU16::new(GroupId::Tmtc as u8, 0);
|
||||
#[resultcode]
|
||||
pub const INVALID_PUS_SUBSERVICE: ResultU16 = ResultU16::new(GroupId::Tmtc as u8, 1);
|
||||
#[resultcode]
|
||||
pub const PUS_SERVICE_NOT_IMPLEMENTED: ResultU16 = ResultU16::new(GroupId::Tmtc as u8, 2);
|
||||
#[resultcode]
|
||||
pub const PUS_SUBSERVICE_NOT_IMPLEMENTED: ResultU16 = ResultU16::new(GroupId::Tmtc as u8, 3);
|
||||
#[resultcode]
|
||||
pub const UNKNOWN_TARGET_ID: ResultU16 = ResultU16::new(GroupId::Tmtc as u8, 4);
|
||||
#[resultcode]
|
||||
pub const ROUTING_ERROR: ResultU16 = ResultU16::new(GroupId::Tmtc as u8, 5);
|
||||
#[resultcode(info = "Request timeout for targeted PUS request. P1: Request ID. P2: Target ID")]
|
||||
pub const REQUEST_TIMEOUT: ResultU16 = ResultU16::new(GroupId::Tmtc as u8, 6);
|
||||
|
||||
#[resultcode(
|
||||
info = "Not enough data inside the TC application data field. Optionally includes: \
|
||||
8 bytes of failure data containing 2 failure parameters, \
|
||||
P1 (u32 big endian): Expected data length, P2: Found data length"
|
||||
)]
|
||||
pub const NOT_ENOUGH_APP_DATA: ResultU16 = ResultU16::new(GroupId::Tmtc as u8, 2);
|
||||
|
||||
pub const TMTC_RESULTS: &[ResultU16Info] = &[
|
||||
INVALID_PUS_SERVICE_EXT,
|
||||
INVALID_PUS_SUBSERVICE_EXT,
|
||||
PUS_SERVICE_NOT_IMPLEMENTED_EXT,
|
||||
UNKNOWN_TARGET_ID_EXT,
|
||||
ROUTING_ERROR_EXT,
|
||||
NOT_ENOUGH_APP_DATA_EXT,
|
||||
];
|
||||
}
|
||||
|
||||
pub mod hk_err {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[resultcode]
|
||||
pub const TARGET_ID_MISSING: ResultU16 = ResultU16::new(GroupId::Hk as u8, 0);
|
||||
#[resultcode]
|
||||
pub const UNIQUE_ID_MISSING: ResultU16 = ResultU16::new(GroupId::Hk as u8, 1);
|
||||
#[resultcode]
|
||||
pub const UNKNOWN_TARGET_ID: ResultU16 = ResultU16::new(GroupId::Hk as u8, 2);
|
||||
#[resultcode]
|
||||
pub const COLLECTION_INTERVAL_MISSING: ResultU16 = ResultU16::new(GroupId::Hk as u8, 3);
|
||||
|
||||
pub const HK_ERR_RESULTS: &[ResultU16Info] = &[
|
||||
TARGET_ID_MISSING_EXT,
|
||||
UNKNOWN_TARGET_ID_EXT,
|
||||
UNKNOWN_TARGET_ID_EXT,
|
||||
COLLECTION_INTERVAL_MISSING_EXT,
|
||||
];
|
||||
}
|
||||
|
||||
pub mod mode_err {
|
||||
use super::*;
|
||||
|
||||
#[resultcode]
|
||||
pub const WRONG_MODE: ResultU16 = ResultU16::new(GroupId::Mode as u8, 0);
|
||||
}
|
||||
|
||||
pub mod components {
|
||||
use satrs::ComponentId;
|
||||
|
||||
pub const NO_SENDER: ComponentId = ComponentId::MAX;
|
||||
}
|
||||
|
||||
pub mod pool {
|
||||
use super::*;
|
||||
pub fn create_static_pools() -> (StaticMemoryPool, StaticMemoryPool) {
|
||||
(
|
||||
StaticMemoryPool::new(StaticPoolConfig::new_from_subpool_cfg_tuples(
|
||||
vec![
|
||||
(30, 32),
|
||||
(15, 64),
|
||||
(15, 128),
|
||||
(15, 256),
|
||||
(15, 1024),
|
||||
(15, 2048),
|
||||
],
|
||||
true,
|
||||
)),
|
||||
StaticMemoryPool::new(StaticPoolConfig::new_from_subpool_cfg_tuples(
|
||||
vec![
|
||||
(30, 32),
|
||||
(15, 64),
|
||||
(15, 128),
|
||||
(15, 256),
|
||||
(15, 1024),
|
||||
(15, 2048),
|
||||
],
|
||||
true,
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn create_sched_tc_pool() -> StaticMemoryPool {
|
||||
StaticMemoryPool::new(StaticPoolConfig::new_from_subpool_cfg_tuples(
|
||||
vec![
|
||||
(30, 32),
|
||||
(15, 64),
|
||||
(15, 128),
|
||||
(15, 256),
|
||||
(15, 1024),
|
||||
(15, 2048),
|
||||
],
|
||||
true,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub mod tasks {
|
||||
pub const FREQ_MS_UDP_TMTC: u64 = 200;
|
||||
pub const FREQ_MS_AOCS: u64 = 200;
|
||||
pub const FREQ_MS_CONTROLLER: u64 = 200;
|
||||
pub const SIM_CLIENT_IDLE_DELAY_MS: u64 = 5;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
use satrs::spacepackets::CcsdsPacketIdAndPsc;
|
||||
use types::{
|
||||
ComponentId,
|
||||
ccsds::{CcsdsTcPacketOwned, CcsdsTmPacketOwned},
|
||||
control,
|
||||
};
|
||||
|
||||
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();
|
||||
self.send_telemetry(
|
||||
Some(tc_id),
|
||||
control::response::Response::Ok,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::time::Duration;
|
||||
|
||||
use satrs::fdir::{FaultCounterStd, FaultResponse, RecoveryEvent, RecoveryFdir};
|
||||
use satrs::health::{HealthState, HealthTableMapSync};
|
||||
use types::{ComponentId, DeviceMode};
|
||||
|
||||
use crate::device_mode::SwitchAndModeHelper;
|
||||
|
||||
// The component is marked faulty if it would be recovered more than RECOVERY_THRESHOLD times
|
||||
// before the counter is decremented again.
|
||||
pub const RECOVERY_THRESHOLD: u32 = 2;
|
||||
pub const RECOVERY_DECREMENT_AFTER: Duration = Duration::from_secs(60);
|
||||
/// Time the device stays unpowered during a power cycle, so it can fully discharge.
|
||||
pub const RECOVERY_OFF_DURATION: Duration = Duration::from_millis(500);
|
||||
|
||||
/// Generic FDIR events. The device handler maps them to its own event type.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum FdirEvent {
|
||||
FaultThresholdExceeded,
|
||||
Recovery(RecoveryEvent),
|
||||
}
|
||||
|
||||
/// Fault counting and power cycle recovery for device handlers which own the power switch of
|
||||
/// their device.
|
||||
///
|
||||
/// The handler detects faults itself and reports them with [Self::register_fault]. This helper
|
||||
/// then decides whether the device is power cycled, or marked faulty and switched off, and drives
|
||||
/// the [SwitchAndModeHelper] of the handler accordingly. The handler retrieves the resulting
|
||||
/// events with [Self::next_event].
|
||||
pub struct DeviceFdir {
|
||||
name: &'static str,
|
||||
fault_counter: FaultCounterStd,
|
||||
recovery: RecoveryFdir<HealthTableMapSync>,
|
||||
pub recovery_off_duration: Duration,
|
||||
events: VecDeque<FdirEvent>,
|
||||
}
|
||||
|
||||
impl DeviceFdir {
|
||||
pub fn new(
|
||||
name: &'static str,
|
||||
component_id: ComponentId,
|
||||
health_table: HealthTableMapSync,
|
||||
fault_counter: FaultCounterStd,
|
||||
) -> Self {
|
||||
Self {
|
||||
name,
|
||||
fault_counter,
|
||||
recovery: RecoveryFdir::new(
|
||||
component_id.into(),
|
||||
health_table,
|
||||
RECOVERY_THRESHOLD,
|
||||
RECOVERY_DECREMENT_AFTER,
|
||||
),
|
||||
recovery_off_duration: RECOVERY_OFF_DURATION,
|
||||
events: VecDeque::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn fault_count(&self) -> u32 {
|
||||
self.fault_counter.fault_count()
|
||||
}
|
||||
|
||||
pub fn set_health(&mut self, health: HealthState) {
|
||||
self.recovery.set_health(health);
|
||||
}
|
||||
|
||||
pub fn next_event(&mut self) -> Option<FdirEvent> {
|
||||
self.events.pop_front()
|
||||
}
|
||||
|
||||
/// Should be called once per cycle, before the mode transition is handled. Starts a power
|
||||
/// cycle if the health was set to [HealthState::NeedsRecovery] by the FDIR or by ground.
|
||||
pub fn periodic_operation(&mut self, modes: &mut SwitchAndModeHelper<DeviceMode>) {
|
||||
self.recovery.periodic_operation();
|
||||
self.check_needs_recovery(modes);
|
||||
}
|
||||
|
||||
pub fn register_success(&mut self) {
|
||||
self.fault_counter.try_decrement();
|
||||
}
|
||||
|
||||
/// If the fault threshold is exceeded, the device is power cycled. If it was power cycled
|
||||
/// too often, the component is marked faulty and commanded off instead.
|
||||
pub fn register_fault(&mut self, modes: &mut SwitchAndModeHelper<DeviceMode>) {
|
||||
if !self.fault_counter.increment_and_check() {
|
||||
return;
|
||||
}
|
||||
match self.recovery.handle_fault() {
|
||||
FaultResponse::Ignored => {
|
||||
log::info!(
|
||||
"{}: fault threshold exceeded, but component is already faulty, \
|
||||
recovering or externally controlled",
|
||||
self.name
|
||||
);
|
||||
}
|
||||
FaultResponse::Recover => {
|
||||
log::warn!(
|
||||
"{}: fault threshold exceeded, power cycling device",
|
||||
self.name
|
||||
);
|
||||
self.events.push_back(FdirEvent::FaultThresholdExceeded);
|
||||
self.check_needs_recovery(modes);
|
||||
}
|
||||
FaultResponse::SetFaulty => {
|
||||
log::error!(
|
||||
"{}: fault threshold exceeded after too many recoveries, marking \
|
||||
component faulty",
|
||||
self.name
|
||||
);
|
||||
self.events.push_back(FdirEvent::FaultThresholdExceeded);
|
||||
self.events
|
||||
.push_back(FdirEvent::Recovery(RecoveryEvent::ThresholdExceeded));
|
||||
self.switch_off_faulty_device(modes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Mode commands from ground or the parent abort a running recovery. Must be called before
|
||||
/// the commanded transition is started.
|
||||
pub fn handle_mode_command(&mut self, modes: &SwitchAndModeHelper<DeviceMode>) {
|
||||
if modes.power_cycle_active() {
|
||||
log::warn!("{}: mode command aborts power cycle recovery", self.name);
|
||||
// Otherwise, the recovery would restart right away.
|
||||
self.recovery.recovery_done();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_power_cycle_done(&mut self) {
|
||||
log::info!("{}: power cycle recovery done", self.name);
|
||||
// Faults registered while the device was switched off do not count anymore.
|
||||
self.fault_counter.clear();
|
||||
self.recovery.recovery_done();
|
||||
self.events
|
||||
.push_back(FdirEvent::Recovery(RecoveryEvent::Done));
|
||||
}
|
||||
|
||||
/// A failed power cycle costs a recovery attempt like any other fault.
|
||||
pub fn handle_power_cycle_failed(
|
||||
&mut self,
|
||||
modes: &mut SwitchAndModeHelper<DeviceMode>,
|
||||
restore_mode: DeviceMode,
|
||||
) {
|
||||
self.events
|
||||
.push_back(FdirEvent::Recovery(RecoveryEvent::Failed));
|
||||
match self.recovery.recovery_failed() {
|
||||
FaultResponse::Recover => {
|
||||
log::warn!("{}: power cycle recovery failed, retrying", self.name);
|
||||
self.start_recovery(modes, restore_mode);
|
||||
}
|
||||
FaultResponse::SetFaulty => {
|
||||
log::error!(
|
||||
"{}: power cycle recovery failed too often, marking component faulty",
|
||||
self.name
|
||||
);
|
||||
self.events
|
||||
.push_back(FdirEvent::Recovery(RecoveryEvent::ThresholdExceeded));
|
||||
self.switch_off_faulty_device(modes);
|
||||
}
|
||||
// Ground changed the health during the recovery and is in charge now.
|
||||
FaultResponse::Ignored => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn check_needs_recovery(&mut self, modes: &mut SwitchAndModeHelper<DeviceMode>) {
|
||||
if modes.power_cycle_active() || modes.target().is_some() || !self.recovery.needs_recovery()
|
||||
{
|
||||
return;
|
||||
}
|
||||
if modes.mode() == DeviceMode::Off {
|
||||
// Nothing to power cycle, the next switch-on is a fresh start anyway.
|
||||
log::info!("{}: device is off, no recovery required", self.name);
|
||||
self.recovery.recovery_done();
|
||||
return;
|
||||
}
|
||||
let restore_mode = modes.mode();
|
||||
self.start_recovery(modes, restore_mode);
|
||||
}
|
||||
|
||||
fn start_recovery(
|
||||
&mut self,
|
||||
modes: &mut SwitchAndModeHelper<DeviceMode>,
|
||||
restore_mode: DeviceMode,
|
||||
) {
|
||||
log::warn!("{}: starting power cycle recovery", self.name);
|
||||
modes.start_power_cycle(restore_mode, self.recovery_off_duration);
|
||||
self.events
|
||||
.push_back(FdirEvent::Recovery(RecoveryEvent::Started));
|
||||
}
|
||||
|
||||
fn switch_off_faulty_device(&mut self, modes: &mut SwitchAndModeHelper<DeviceMode>) {
|
||||
// Do not restart an already pending Off transition, which would reset the transition
|
||||
// state machine before it can finish.
|
||||
if modes.target() != Some(DeviceMode::Off) {
|
||||
log::warn!("{}: commanding device off due to fault", self.name);
|
||||
modes.start_transition(DeviceMode::Off, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,504 @@
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use types::pcdu::SwitchId;
|
||||
|
||||
use crate::eps::PowerSwitchHelper;
|
||||
|
||||
/// This is a helper trait required to make [SwitchAndModeHelper] generic.
|
||||
///
|
||||
/// It allows distinguish a powered-off state from one or more powered-on states, so
|
||||
/// [`SwitchAndModeHelper`] knows which way to drive the switch for a given target mode.
|
||||
pub trait PowerSwitchedMode: Copy + PartialEq {
|
||||
const OFF: Self;
|
||||
fn requires_power(&self) -> bool;
|
||||
}
|
||||
|
||||
impl PowerSwitchedMode for types::DeviceMode {
|
||||
const OFF: Self = types::DeviceMode::Off;
|
||||
|
||||
fn requires_power(&self) -> bool {
|
||||
*self != types::DeviceMode::Off
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, PartialEq, Eq)]
|
||||
enum SwitchTransitionState {
|
||||
#[default]
|
||||
Idle,
|
||||
PowerSwitching,
|
||||
Done,
|
||||
}
|
||||
|
||||
/// Outcome of a single power switch transition.
|
||||
enum SwitchOutcome {
|
||||
Reached(Option<satrs::spacepackets::CcsdsPacketIdAndPsc>),
|
||||
Failed(Option<satrs::spacepackets::CcsdsPacketIdAndPsc>),
|
||||
}
|
||||
|
||||
/// Dedicated states for power cycling a device.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum PowerCycleState<Mode> {
|
||||
Idle,
|
||||
SwitchingOff { restore_mode: Mode },
|
||||
WaitingOff { restore_mode: Mode, since: Instant },
|
||||
SwitchingOn { restore_mode: Mode },
|
||||
}
|
||||
|
||||
/// Outcome of a pending mode transition, once [`SwitchAndModeHelper::handle_mode_transition`]
|
||||
/// has driven it to completion. Carries back whichever TC commanded the transition, if any, so
|
||||
/// the caller can reply to it -- what that reply looks like is handler-specific, so this stays
|
||||
/// out of the helper.
|
||||
pub enum ModeTransitionEvent<Mode> {
|
||||
/// The target mode was reached.
|
||||
Reached(Option<satrs::spacepackets::CcsdsPacketIdAndPsc>),
|
||||
/// The target mode could not be reached.
|
||||
Failed(Option<satrs::spacepackets::CcsdsPacketIdAndPsc>),
|
||||
/// The power cycle completed and the mode before the power cycle was restored.
|
||||
PowerCycleDone,
|
||||
/// Power switching failed during the power cycle. The power cycle is not hidden anymore,
|
||||
/// so [SwitchAndModeHelper::reported_mode] returns the actual mode again. `restore_mode` is
|
||||
/// the mode the power cycle should have restored, which can be used to retry it.
|
||||
PowerCycleFailed { restore_mode: Mode },
|
||||
}
|
||||
|
||||
/// Drives the on/off power-switch commanding state machine (Idle -> PowerSwitching -> Done)
|
||||
/// shared by every device handler that owns a single power switch of its own.
|
||||
///
|
||||
/// Handler-specific reactions (sending telemetry, invalidating cached sensor data, reporting to
|
||||
/// a parent) are not this helper's concern: [`Self::handle_mode_transition`] just reports when a
|
||||
/// transition finishes (or fails) and leaves what to do about it to the caller.
|
||||
pub struct SwitchAndModeHelper<Mode: PowerSwitchedMode> {
|
||||
mode_helper: example_std::ModeHelper<Mode, SwitchTransitionState>,
|
||||
switch_helper: PowerSwitchHelper,
|
||||
switch_id: SwitchId,
|
||||
power_cycle_state: PowerCycleState<Mode>,
|
||||
power_cycle_off_duration: Duration,
|
||||
}
|
||||
|
||||
impl<Mode: PowerSwitchedMode> SwitchAndModeHelper<Mode> {
|
||||
pub fn new(
|
||||
init_mode: Mode,
|
||||
timeout: Duration,
|
||||
switch_helper: PowerSwitchHelper,
|
||||
switch_id: SwitchId,
|
||||
) -> Self {
|
||||
Self {
|
||||
mode_helper: example_std::ModeHelper::new(init_mode, timeout),
|
||||
switch_helper,
|
||||
switch_id,
|
||||
power_cycle_state: PowerCycleState::Idle,
|
||||
power_cycle_off_duration: Duration::ZERO,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn mode(&self) -> Mode {
|
||||
self.mode_helper.current
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn target(&self) -> Option<Mode> {
|
||||
self.mode_helper.target
|
||||
}
|
||||
|
||||
/// Mode which should be reported to other components. A power cycle is hidden from them,
|
||||
/// so this is the mode which is restored after the power cycle while one is active.
|
||||
pub fn reported_mode(&self) -> Mode {
|
||||
match self.power_cycle_state {
|
||||
PowerCycleState::SwitchingOff { restore_mode }
|
||||
| PowerCycleState::WaitingOff { restore_mode, .. }
|
||||
| PowerCycleState::SwitchingOn { restore_mode } => restore_mode,
|
||||
PowerCycleState::Idle => self.mode(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn power_cycle_active(&self) -> bool {
|
||||
self.power_cycle_state != PowerCycleState::Idle
|
||||
}
|
||||
|
||||
/// Starts a new transition, aborting a running power cycle.
|
||||
pub fn start_transition(
|
||||
&mut self,
|
||||
target_mode: Mode,
|
||||
tc_commander: Option<satrs::spacepackets::CcsdsPacketIdAndPsc>,
|
||||
) {
|
||||
self.power_cycle_state = PowerCycleState::Idle;
|
||||
self.start_transition_internal(target_mode, tc_commander);
|
||||
}
|
||||
|
||||
/// Switches the device off, keeps it off for `off_duration` and then switches it to
|
||||
/// `restore_mode`. Reaching the intermediate off mode does not generate an event.
|
||||
pub fn start_power_cycle(&mut self, restore_mode: Mode, off_duration: Duration) {
|
||||
self.power_cycle_state = PowerCycleState::SwitchingOff { restore_mode };
|
||||
self.power_cycle_off_duration = off_duration;
|
||||
self.start_transition_internal(Mode::OFF, None);
|
||||
}
|
||||
|
||||
fn start_transition_internal(
|
||||
&mut self,
|
||||
target_mode: Mode,
|
||||
tc_commander: Option<satrs::spacepackets::CcsdsPacketIdAndPsc>,
|
||||
) {
|
||||
self.mode_helper.tc_commander = tc_commander;
|
||||
self.mode_helper.start(target_mode);
|
||||
}
|
||||
|
||||
/// This is the main API that the periodic handler of a device handler should call.
|
||||
///
|
||||
/// It handles the switch commanding and returns relevant events.
|
||||
pub fn handle_mode_transition(&mut self) -> Option<ModeTransitionEvent<Mode>> {
|
||||
// The most probable case: Nothing to do.
|
||||
if self.target().is_none() && !self.power_cycle_active() {
|
||||
return None;
|
||||
}
|
||||
// Handle this as an extra step so the switch transition after this can proceed.
|
||||
self.handle_waiting_for_off_when_power_cycling();
|
||||
// Core logic: Command the switches, check whether target switch state was reached.
|
||||
// Note the ?: if a switch transition is on-going, we might do an early return.
|
||||
let outcome = self.handle_switch_transition()?;
|
||||
// Regular mode transition without power cycling.
|
||||
if self.power_cycle_state == PowerCycleState::Idle {
|
||||
return Some(match outcome {
|
||||
SwitchOutcome::Reached(tc_commander) => ModeTransitionEvent::Reached(tc_commander),
|
||||
SwitchOutcome::Failed(tc_commander) => ModeTransitionEvent::Failed(tc_commander),
|
||||
});
|
||||
}
|
||||
// Power cycling, where a bit more logic is required.
|
||||
// Handle the error case first.
|
||||
if let SwitchOutcome::Failed(_) = outcome {
|
||||
let restore_mode = self.reported_mode();
|
||||
self.power_cycle_state = PowerCycleState::Idle;
|
||||
return Some(ModeTransitionEvent::PowerCycleFailed { restore_mode });
|
||||
}
|
||||
// At this point: The switching was succesfull, so we only match on the
|
||||
// power cycle state.
|
||||
match self.power_cycle_state {
|
||||
// No switching going on for thse cases.
|
||||
PowerCycleState::Idle | PowerCycleState::WaitingOff { .. } => None,
|
||||
PowerCycleState::SwitchingOff { restore_mode } => {
|
||||
self.power_cycle_state = PowerCycleState::WaitingOff {
|
||||
restore_mode,
|
||||
since: Instant::now(),
|
||||
};
|
||||
None
|
||||
}
|
||||
PowerCycleState::SwitchingOn { .. } => {
|
||||
// Power is back and we are done.
|
||||
self.power_cycle_state = PowerCycleState::Idle;
|
||||
Some(ModeTransitionEvent::PowerCycleDone)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_waiting_for_off_when_power_cycling(&mut self) {
|
||||
if let PowerCycleState::WaitingOff {
|
||||
restore_mode,
|
||||
since,
|
||||
} = self.power_cycle_state
|
||||
&& since.elapsed() >= self.power_cycle_off_duration
|
||||
{
|
||||
self.power_cycle_state = PowerCycleState::SwitchingOn { restore_mode };
|
||||
self.start_transition_internal(restore_mode, None);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_switch_transition(&mut self) -> Option<SwitchOutcome> {
|
||||
let target_mode = self.mode_helper.target?;
|
||||
let switch_target_on = target_mode.requires_power();
|
||||
if self.mode_helper.transition_state == SwitchTransitionState::Idle {
|
||||
let result = if switch_target_on {
|
||||
self.switch_helper.send_switch_on_cmd(self.switch_id)
|
||||
} else {
|
||||
self.switch_helper.send_switch_off_cmd(self.switch_id)
|
||||
};
|
||||
if result.is_err() {
|
||||
// Could not send switch command.. still continue with transition.
|
||||
log::error!(
|
||||
"failed to send switch {} command",
|
||||
if switch_target_on { "on" } else { "off" }
|
||||
);
|
||||
}
|
||||
self.mode_helper.transition_state = SwitchTransitionState::PowerSwitching;
|
||||
}
|
||||
if self.mode_helper.transition_state == SwitchTransitionState::PowerSwitching {
|
||||
if self.switch_helper.is_switch_on(self.switch_id) == switch_target_on {
|
||||
log::info!("switch is {}", if switch_target_on { "on" } else { "off" });
|
||||
self.mode_helper.transition_state = SwitchTransitionState::Done;
|
||||
} else if self.mode_helper.timed_out() {
|
||||
return Some(SwitchOutcome::Failed(self.mode_helper.finish(false)));
|
||||
}
|
||||
}
|
||||
if self.mode_helper.transition_state == SwitchTransitionState::Done {
|
||||
return Some(SwitchOutcome::Reached(self.mode_helper.finish(true)));
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::{Arc, Mutex, mpsc};
|
||||
|
||||
use arbitrary_int::u11;
|
||||
use satrs::spacepackets::{CcsdsPacketIdAndPsc, SpacePacketHeader};
|
||||
use types::{
|
||||
DeviceMode,
|
||||
pcdu::{SwitchRequest, SwitchState, SwitchStateBinary},
|
||||
};
|
||||
|
||||
use crate::eps::pcdu::{SharedSwitchSet, SwitchMap, SwitchSet};
|
||||
|
||||
use super::*;
|
||||
|
||||
const TIMEOUT: Duration = Duration::from_millis(50);
|
||||
|
||||
struct Testbench {
|
||||
helper: SwitchAndModeHelper<DeviceMode>,
|
||||
switch_rx: mpsc::Receiver<SwitchRequest>,
|
||||
shared_switch_set: SharedSwitchSet,
|
||||
}
|
||||
|
||||
impl Testbench {
|
||||
fn new() -> Self {
|
||||
let (switch_tx, switch_rx) = mpsc::sync_channel(10);
|
||||
let mut switch_map = SwitchMap::new();
|
||||
switch_map.insert(SwitchId::Mgm0, SwitchState::Off);
|
||||
let shared_switch_set: SharedSwitchSet =
|
||||
Arc::new(Mutex::new(SwitchSet::new(switch_map)));
|
||||
Self {
|
||||
helper: SwitchAndModeHelper::new(
|
||||
DeviceMode::Off,
|
||||
TIMEOUT,
|
||||
PowerSwitchHelper::new(switch_tx, shared_switch_set.clone()),
|
||||
SwitchId::Mgm0,
|
||||
),
|
||||
switch_rx,
|
||||
shared_switch_set,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_switch_state(&self, state: SwitchState) {
|
||||
self.shared_switch_set
|
||||
.lock()
|
||||
.unwrap()
|
||||
.set_switch_state(SwitchId::Mgm0, state);
|
||||
}
|
||||
|
||||
fn switch_requests(&self) -> Vec<SwitchStateBinary> {
|
||||
self.switch_rx
|
||||
.try_iter()
|
||||
.map(|req| req.target_state)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Drives a transition to `Normal` to completion.
|
||||
fn switch_to_normal(&mut self) {
|
||||
self.helper.start_transition(DeviceMode::Normal, None);
|
||||
self.set_switch_state(SwitchState::On);
|
||||
assert!(matches!(
|
||||
self.helper.handle_mode_transition(),
|
||||
Some(ModeTransitionEvent::Reached(None))
|
||||
));
|
||||
self.switch_requests();
|
||||
}
|
||||
|
||||
/// Starts a power cycle from `Normal` and drives it until the device is off.
|
||||
fn power_cycle_until_off(&mut self, off_duration: Duration) {
|
||||
self.switch_to_normal();
|
||||
self.helper
|
||||
.start_power_cycle(DeviceMode::Normal, off_duration);
|
||||
assert!(self.helper.handle_mode_transition().is_none());
|
||||
assert_eq!(self.switch_requests(), [SwitchStateBinary::Off]);
|
||||
self.set_switch_state(SwitchState::Off);
|
||||
assert!(self.helper.handle_mode_transition().is_none());
|
||||
assert_eq!(self.helper.mode(), DeviceMode::Off);
|
||||
}
|
||||
}
|
||||
|
||||
fn tc_id() -> CcsdsPacketIdAndPsc {
|
||||
CcsdsPacketIdAndPsc::new_from_ccsds_packet(&SpacePacketHeader::new_from_apid(u11::new(1)))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_transition() {
|
||||
let mut tb = Testbench::new();
|
||||
assert_eq!(tb.helper.mode(), DeviceMode::Off);
|
||||
assert_eq!(tb.helper.target(), None);
|
||||
assert!(tb.helper.handle_mode_transition().is_none());
|
||||
assert!(tb.switch_requests().is_empty());
|
||||
assert!(!tb.helper.power_cycle_active());
|
||||
assert_eq!(tb.helper.reported_mode(), DeviceMode::Off);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_switch_on() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.helper
|
||||
.start_transition(DeviceMode::Normal, Some(tc_id()));
|
||||
assert!(tb.helper.handle_mode_transition().is_none());
|
||||
assert_eq!(tb.switch_requests(), [SwitchStateBinary::On]);
|
||||
assert_eq!(tb.helper.mode(), DeviceMode::Off);
|
||||
assert_eq!(tb.helper.target(), Some(DeviceMode::Normal));
|
||||
|
||||
tb.set_switch_state(SwitchState::On);
|
||||
match tb.helper.handle_mode_transition() {
|
||||
Some(ModeTransitionEvent::Reached(Some(id))) => assert_eq!(id, tc_id()),
|
||||
_ => panic!("expected mode reached event with TC commander"),
|
||||
}
|
||||
assert_eq!(tb.helper.mode(), DeviceMode::Normal);
|
||||
assert_eq!(tb.helper.target(), None);
|
||||
assert!(tb.switch_requests().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_switch_off() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.switch_to_normal();
|
||||
tb.helper.start_transition(DeviceMode::Off, None);
|
||||
assert!(tb.helper.handle_mode_transition().is_none());
|
||||
assert_eq!(tb.switch_requests(), [SwitchStateBinary::Off]);
|
||||
tb.set_switch_state(SwitchState::Off);
|
||||
assert!(matches!(
|
||||
tb.helper.handle_mode_transition(),
|
||||
Some(ModeTransitionEvent::Reached(None))
|
||||
));
|
||||
assert_eq!(tb.helper.mode(), DeviceMode::Off);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_switch_already_in_target_state() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.set_switch_state(SwitchState::On);
|
||||
tb.helper.start_transition(DeviceMode::On, None);
|
||||
assert!(matches!(
|
||||
tb.helper.handle_mode_transition(),
|
||||
Some(ModeTransitionEvent::Reached(None))
|
||||
));
|
||||
// The switch command is still sent.
|
||||
assert_eq!(tb.switch_requests(), [SwitchStateBinary::On]);
|
||||
assert_eq!(tb.helper.mode(), DeviceMode::On);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_switch_timeout() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.helper
|
||||
.start_transition(DeviceMode::Normal, Some(tc_id()));
|
||||
assert!(tb.helper.handle_mode_transition().is_none());
|
||||
std::thread::sleep(TIMEOUT);
|
||||
match tb.helper.handle_mode_transition() {
|
||||
Some(ModeTransitionEvent::Failed(Some(id))) => assert_eq!(id, tc_id()),
|
||||
_ => panic!("expected mode failed event with TC commander"),
|
||||
}
|
||||
assert_eq!(tb.helper.mode(), DeviceMode::Off);
|
||||
assert_eq!(tb.helper.target(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_power_cycle() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.power_cycle_until_off(Duration::ZERO);
|
||||
assert!(tb.helper.power_cycle_active());
|
||||
|
||||
// The off duration elapsed, so switching on starts right away.
|
||||
assert!(tb.helper.handle_mode_transition().is_none());
|
||||
assert_eq!(tb.switch_requests(), [SwitchStateBinary::On]);
|
||||
assert_eq!(tb.helper.target(), Some(DeviceMode::Normal));
|
||||
tb.set_switch_state(SwitchState::On);
|
||||
assert!(matches!(
|
||||
tb.helper.handle_mode_transition(),
|
||||
Some(ModeTransitionEvent::PowerCycleDone)
|
||||
));
|
||||
assert_eq!(tb.helper.mode(), DeviceMode::Normal);
|
||||
assert!(!tb.helper.power_cycle_active());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_power_cycle_reports_restored_mode() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.switch_to_normal();
|
||||
tb.helper
|
||||
.start_power_cycle(DeviceMode::Normal, Duration::from_secs(60));
|
||||
assert_eq!(tb.helper.reported_mode(), DeviceMode::Normal);
|
||||
tb.helper.handle_mode_transition();
|
||||
tb.set_switch_state(SwitchState::Off);
|
||||
tb.helper.handle_mode_transition();
|
||||
assert_eq!(tb.helper.mode(), DeviceMode::Off);
|
||||
assert_eq!(tb.helper.reported_mode(), DeviceMode::Normal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_power_cycle_reports_restored_mode_while_switching_on() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.power_cycle_until_off(Duration::ZERO);
|
||||
assert!(tb.helper.handle_mode_transition().is_none());
|
||||
assert_eq!(tb.helper.target(), Some(DeviceMode::Normal));
|
||||
assert_eq!(tb.helper.mode(), DeviceMode::Off);
|
||||
assert_eq!(tb.helper.reported_mode(), DeviceMode::Normal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_power_cycle_waits_off_duration() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.power_cycle_until_off(Duration::from_secs(60));
|
||||
for _ in 0..3 {
|
||||
assert!(tb.helper.handle_mode_transition().is_none());
|
||||
}
|
||||
assert!(tb.switch_requests().is_empty());
|
||||
assert_eq!(tb.helper.target(), None);
|
||||
assert!(tb.helper.power_cycle_active());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_power_cycle_switch_off_timeout() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.switch_to_normal();
|
||||
tb.helper
|
||||
.start_power_cycle(DeviceMode::Normal, Duration::ZERO);
|
||||
assert!(tb.helper.handle_mode_transition().is_none());
|
||||
std::thread::sleep(TIMEOUT);
|
||||
assert!(matches!(
|
||||
tb.helper.handle_mode_transition(),
|
||||
Some(ModeTransitionEvent::PowerCycleFailed {
|
||||
restore_mode: DeviceMode::Normal
|
||||
})
|
||||
));
|
||||
assert_eq!(tb.helper.mode(), DeviceMode::Normal);
|
||||
assert!(!tb.helper.power_cycle_active());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_power_cycle_switch_on_timeout() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.power_cycle_until_off(Duration::ZERO);
|
||||
assert!(tb.helper.handle_mode_transition().is_none());
|
||||
std::thread::sleep(TIMEOUT);
|
||||
assert!(matches!(
|
||||
tb.helper.handle_mode_transition(),
|
||||
Some(ModeTransitionEvent::PowerCycleFailed {
|
||||
restore_mode: DeviceMode::Normal
|
||||
})
|
||||
));
|
||||
assert_eq!(tb.helper.mode(), DeviceMode::Off);
|
||||
assert!(!tb.helper.power_cycle_active());
|
||||
// The failed power cycle is not hidden anymore.
|
||||
assert_eq!(tb.helper.reported_mode(), DeviceMode::Off);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transition_aborts_power_cycle() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.power_cycle_until_off(Duration::from_secs(60));
|
||||
tb.helper.start_transition(DeviceMode::On, Some(tc_id()));
|
||||
assert!(!tb.helper.power_cycle_active());
|
||||
assert_eq!(tb.helper.reported_mode(), DeviceMode::Off);
|
||||
tb.set_switch_state(SwitchState::On);
|
||||
// A regular transition event instead of a power cycle event.
|
||||
assert!(matches!(
|
||||
tb.helper.handle_mode_transition(),
|
||||
Some(ModeTransitionEvent::Reached(Some(_)))
|
||||
));
|
||||
assert_eq!(tb.helper.mode(), DeviceMode::On);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
use derive_new::new;
|
||||
use std::{cell::RefCell, collections::VecDeque, sync::mpsc, time::Duration};
|
||||
use types::pcdu::{SwitchId, SwitchRequest, SwitchState, SwitchStateBinary};
|
||||
|
||||
use satrs::{queue::GenericSendError, request::MessageMetadata};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::eps::pcdu::SwitchMapWrapper;
|
||||
|
||||
use self::pcdu::SharedSwitchSet;
|
||||
|
||||
pub mod pcdu;
|
||||
|
||||
#[derive(new, Clone)]
|
||||
pub struct PowerSwitchHelper {
|
||||
switcher_tx: mpsc::SyncSender<SwitchRequest>,
|
||||
shared_switch_set: SharedSwitchSet,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error, Copy, Clone, PartialEq, Eq)]
|
||||
#[allow(dead_code)]
|
||||
pub enum SwitchCommandingError {
|
||||
#[error("send error: {0}")]
|
||||
Send(#[from] GenericSendError),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum SwitchInfoError {
|
||||
/// This is a configuration error which should not occur.
|
||||
#[error("switch ID not in map")]
|
||||
SwitchIdNotInMap(SwitchId),
|
||||
#[error("switch set invalid")]
|
||||
SwitchSetInvalid,
|
||||
}
|
||||
|
||||
impl PowerSwitchHelper {
|
||||
pub fn send_switch_on_cmd(&self, switch_id: SwitchId) -> Result<(), GenericSendError> {
|
||||
self.switcher_tx
|
||||
.send(SwitchRequest::new(switch_id, SwitchStateBinary::On))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_switch_off_cmd(&self, switch_id: SwitchId) -> Result<(), GenericSendError> {
|
||||
self.switcher_tx
|
||||
.send(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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(new)]
|
||||
pub struct SwitchRequestInfo {
|
||||
pub requestor_info: MessageMetadata,
|
||||
pub switch_id: SwitchId,
|
||||
pub target_state: SwitchStateBinary,
|
||||
}
|
||||
|
||||
// Test switch helper which can be used for unittests.
|
||||
#[allow(dead_code)]
|
||||
pub struct TestSwitchHelper {
|
||||
pub switch_requests: RefCell<VecDeque<SwitchRequestInfo>>,
|
||||
pub switch_info_requests: RefCell<VecDeque<SwitchId>>,
|
||||
#[allow(dead_code)]
|
||||
pub switch_delay_request_count: u32,
|
||||
pub next_switch_delay: Duration,
|
||||
pub switch_map: RefCell<SwitchMapWrapper>,
|
||||
pub switch_map_valid: bool,
|
||||
}
|
||||
|
||||
impl Default for TestSwitchHelper {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
switch_requests: Default::default(),
|
||||
switch_info_requests: Default::default(),
|
||||
switch_delay_request_count: Default::default(),
|
||||
next_switch_delay: Duration::from_millis(1000),
|
||||
switch_map: Default::default(),
|
||||
switch_map_valid: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl TestSwitchHelper {
|
||||
// Helper function which can be used to force a switch to another state for test purposes.
|
||||
pub fn set_switch_state(&mut self, switch: SwitchId, state: SwitchState) {
|
||||
self.switch_map.get_mut().0.insert(switch, state);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,773 @@
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::{HashMap, VecDeque},
|
||||
sync::{Arc, Mutex, mpsc},
|
||||
};
|
||||
|
||||
use derive_new::new;
|
||||
use example_std::TimestampHelper;
|
||||
use minisim_types::{
|
||||
SimReply, SimRequestWithTime,
|
||||
eps::{PcduReply, PcduRequest},
|
||||
};
|
||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
use satrs::spacepackets::CcsdsPacketIdAndPsc;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::IntoEnumIterator as _;
|
||||
use types::{
|
||||
ComponentId, DeviceMode,
|
||||
ccsds::{CcsdsTcPacketOwned, CcsdsTmPacketOwned},
|
||||
pcdu::{
|
||||
self, SwitchId, SwitchMapBinary, SwitchMapBinaryWrapper, SwitchRequest, SwitchState,
|
||||
SwitchStateBinary, SwitchesBitfield,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::ccsds::pack_ccsds_tm_packet_for_now;
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SwitchSet {
|
||||
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 {
|
||||
type Error: core::fmt::Debug;
|
||||
|
||||
/// Send some data via the serial interface.
|
||||
fn send(&self, data: &[u8]) -> Result<(), Self::Error>;
|
||||
/// Receive all replies received on the serial interface so far. This function takes a closure
|
||||
/// and call its for each received packet, passing the received packet into it.
|
||||
fn try_recv_replies<ReplyHandler: FnMut(&[u8])>(
|
||||
&self,
|
||||
f: ReplyHandler,
|
||||
) -> Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
#[derive(new)]
|
||||
pub struct SerialInterfaceToSim {
|
||||
pub sim_request_tx: mpsc::Sender<SimRequestWithTime>,
|
||||
pub sim_reply_rx: mpsc::Receiver<SimReply>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
|
||||
#[repr(u32)]
|
||||
pub enum SetId {
|
||||
SwitcherSet = 0,
|
||||
}
|
||||
|
||||
impl SerialInterface for SerialInterfaceToSim {
|
||||
type Error = ();
|
||||
|
||||
fn send(&self, data: &[u8]) -> Result<(), Self::Error> {
|
||||
let request: PcduRequest = postcard::from_bytes(data).expect("expected a PCDU request");
|
||||
self.sim_request_tx
|
||||
.send(SimRequestWithTime::new_with_epoch_time(request))
|
||||
.expect("failed to send request to simulation");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn try_recv_replies<ReplyHandler: FnMut(&[u8])>(
|
||||
&self,
|
||||
mut f: ReplyHandler,
|
||||
) -> Result<(), Self::Error> {
|
||||
loop {
|
||||
match self.sim_reply_rx.try_recv() {
|
||||
Ok(reply) => {
|
||||
let reply = postcard::to_allocvec(&reply).unwrap();
|
||||
f(&reply);
|
||||
}
|
||||
Err(e) => match e {
|
||||
mpsc::TryRecvError::Empty => break,
|
||||
mpsc::TryRecvError::Disconnected => {
|
||||
log::warn!("sim reply sender has disconnected");
|
||||
break;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SerialInterfaceDummy {
|
||||
// Need interior mutability here for both fields.
|
||||
pub switch_map: RefCell<SwitchMapBinaryWrapper>,
|
||||
pub reply_deque: RefCell<VecDeque<SimReply>>,
|
||||
}
|
||||
|
||||
impl SerialInterface for SerialInterfaceDummy {
|
||||
type Error = ();
|
||||
|
||||
fn send(&self, data: &[u8]) -> Result<(), Self::Error> {
|
||||
let pcdu_req: PcduRequest = postcard::from_bytes(data).unwrap();
|
||||
let switch_map_mut = &mut self.switch_map.borrow_mut().0;
|
||||
match pcdu_req {
|
||||
PcduRequest::SwitchDevice { switch, state } => {
|
||||
switch_map_mut
|
||||
.insert(switch, state)
|
||||
.expect("switch map capacity exceeded");
|
||||
}
|
||||
PcduRequest::RequestSwitchInfo => {
|
||||
let mut reply_deque_mut = self.reply_deque.borrow_mut();
|
||||
reply_deque_mut.push_back(SimReply::from(PcduReply::SwitchInfo(
|
||||
switch_map_mut.clone(),
|
||||
)));
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn try_recv_replies<ReplyHandler: FnMut(&[u8])>(
|
||||
&self,
|
||||
mut f: ReplyHandler,
|
||||
) -> Result<(), Self::Error> {
|
||||
if self.reply_queue_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
loop {
|
||||
let reply = self.next_reply_serialized();
|
||||
f(&reply);
|
||||
if self.reply_queue_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SerialInterfaceDummy {
|
||||
fn next_reply_serialized(&self) -> Vec<u8> {
|
||||
let mut reply_deque_mut = self.reply_deque.borrow_mut();
|
||||
let next_reply = reply_deque_mut.pop_front().unwrap();
|
||||
postcard::to_allocvec(&next_reply).unwrap()
|
||||
}
|
||||
|
||||
fn reply_queue_empty(&self) -> bool {
|
||||
self.reply_deque.borrow().is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum SerialSimInterfaceWrapper {
|
||||
Dummy(SerialInterfaceDummy),
|
||||
Sim(SerialInterfaceToSim),
|
||||
}
|
||||
|
||||
impl SerialInterface for SerialSimInterfaceWrapper {
|
||||
type Error = ();
|
||||
|
||||
fn send(&self, data: &[u8]) -> Result<(), Self::Error> {
|
||||
match self {
|
||||
SerialSimInterfaceWrapper::Dummy(dummy) => dummy.send(data),
|
||||
SerialSimInterfaceWrapper::Sim(sim) => sim.send(data),
|
||||
}
|
||||
}
|
||||
|
||||
fn try_recv_replies<ReplyHandler: FnMut(&[u8])>(
|
||||
&self,
|
||||
f: ReplyHandler,
|
||||
) -> Result<(), Self::Error> {
|
||||
match self {
|
||||
SerialSimInterfaceWrapper::Dummy(dummy) => dummy.try_recv_replies(f),
|
||||
SerialSimInterfaceWrapper::Sim(sim) => sim.try_recv_replies(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum OpCode {
|
||||
RegularOp = 0,
|
||||
PollAndRecvReplies = 1,
|
||||
}
|
||||
|
||||
/// Example PCDU device handler.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub struct PcduHandler<ComInterface: SerialInterface> {
|
||||
dev_str: &'static str,
|
||||
switch_request_rx: mpsc::Receiver<SwitchRequest>,
|
||||
tc_rx: std::sync::mpsc::Receiver<CcsdsTcPacketOwned>,
|
||||
tm_tx: mpsc::SyncSender<CcsdsTmPacketOwned>,
|
||||
pub com_interface: ComInterface,
|
||||
shared_switch_map: Arc<Mutex<SwitchSet>>,
|
||||
mode: DeviceMode,
|
||||
stamp_helper: TimestampHelper,
|
||||
event_tx: mpsc::SyncSender<pcdu::Event>,
|
||||
}
|
||||
|
||||
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<SwitchRequest>,
|
||||
com_interface: ComInterface,
|
||||
shared_switch_map: Arc<Mutex<SwitchSet>>,
|
||||
init_mode: DeviceMode,
|
||||
event_tx: mpsc::SyncSender<pcdu::Event>,
|
||||
) -> 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,
|
||||
event_tx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn periodic_operation(&mut self, op_code: OpCode) {
|
||||
match op_code {
|
||||
OpCode::RegularOp => {
|
||||
self.stamp_helper.update_from_now();
|
||||
// Handle requests.
|
||||
self.handle_telecommands();
|
||||
self.handle_switch_requests();
|
||||
// Poll the switch states and/or telemetry regularly here.
|
||||
if self.mode() == DeviceMode::Normal || self.mode() == DeviceMode::On {
|
||||
self.handle_periodic_commands();
|
||||
}
|
||||
}
|
||||
OpCode::PollAndRecvReplies => {
|
||||
self.poll_and_handle_replies();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn mode(&self) -> DeviceMode {
|
||||
self.mode
|
||||
}
|
||||
|
||||
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::<pcdu::request::Request>(&packet.payload) {
|
||||
Ok(request) => {
|
||||
log::info!(
|
||||
"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_switches_bitfield_request(
|
||||
&mut self,
|
||||
switches: SwitchesBitfield,
|
||||
state: SwitchStateBinary,
|
||||
) {
|
||||
if switches.mgm0() {
|
||||
self.handle_device_switching(SwitchId::Mgm0, state);
|
||||
}
|
||||
if switches.mgm1() {
|
||||
self.handle_device_switching(SwitchId::Mgm1, state);
|
||||
}
|
||||
if switches.magnetorquer() {
|
||||
self.handle_device_switching(SwitchId::Mgt, state);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_tm(&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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_periodic_commands(&self) {
|
||||
let pcdu_req = PcduRequest::RequestSwitchInfo;
|
||||
let pcdu_req_ser = postcard::to_allocvec(&pcdu_req).unwrap();
|
||||
if let Err(_e) = self.com_interface.send(&pcdu_req_ser) {
|
||||
log::warn!("polling PCDU switch info failed");
|
||||
if let Err(e) = self.event_tx.send(pcdu::Event::SerialCommError) {
|
||||
log::warn!("failed to send comm error event: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
pub fn handle_mode_requests(&mut self) {
|
||||
loop {
|
||||
// TODO: Only allow one set mode request per cycle?
|
||||
match self.mode_node.try_recv_mode_request() {
|
||||
Ok(opt_msg) => {
|
||||
if let Some(msg) = opt_msg {
|
||||
let result = self.handle_mode_request(msg);
|
||||
// TODO: Trigger event?
|
||||
if result.is_err() {
|
||||
log::warn!(
|
||||
"{}: mode request failed with error {:?}",
|
||||
self.dev_str,
|
||||
result.err().unwrap()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(e) => match e {
|
||||
satrs::queue::GenericReceiveError::Empty => {
|
||||
break;
|
||||
}
|
||||
satrs::queue::GenericReceiveError::TxDisconnected(_) => {
|
||||
log::warn!("{}: failed to receive mode request: {:?}", self.dev_str, e);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
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 = postcard::to_allocvec(&pcdu_req).unwrap();
|
||||
self.com_interface
|
||||
.send(&pcdu_req_ser)
|
||||
.expect("failed to send switch request to PCDU");
|
||||
}
|
||||
|
||||
pub fn handle_switch_requests(&mut self) {
|
||||
loop {
|
||||
match self.switch_request_rx.try_recv() {
|
||||
Ok(switch_req) => {
|
||||
self.handle_device_switching(switch_req.switch_id(), switch_req.target_state());
|
||||
}
|
||||
Err(e) => match e {
|
||||
mpsc::TryRecvError::Empty => break,
|
||||
mpsc::TryRecvError::Disconnected => {
|
||||
log::warn!("switch request receiver has disconnected");
|
||||
break;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn poll_and_handle_replies(&mut self) {
|
||||
if let Err(e) = self.com_interface.try_recv_replies(|reply| {
|
||||
let sim_reply: SimReply = postcard::from_bytes(reply).expect("invalid reply format");
|
||||
let SimReply::Pcdu(pcdu_reply) = sim_reply else {
|
||||
log::warn!("unexpected PCDU SIM reply: {sim_reply:?}");
|
||||
return;
|
||||
};
|
||||
match pcdu_reply {
|
||||
PcduReply::SwitchInfo(switch_info) => {
|
||||
let switch_map_wrapper =
|
||||
SwitchMapWrapper::from_binary_switch_map_ref(&switch_info);
|
||||
let mut shared_switch_map = self
|
||||
.shared_switch_map
|
||||
.lock()
|
||||
.expect("failed to lock switch map");
|
||||
shared_switch_map.switch_map = switch_map_wrapper.0;
|
||||
shared_switch_map.valid = true;
|
||||
}
|
||||
}
|
||||
}) {
|
||||
log::warn!("receiving PCDU replies failed: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::mpsc;
|
||||
|
||||
use arbitrary_int::u11;
|
||||
use satrs::spacepackets::SpacePacketHeader;
|
||||
use types::{
|
||||
Apid, TcHeader,
|
||||
pcdu::{SwitchMapBinary, SwitchStateBinary},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
pub fn create_request_tc(
|
||||
request: types::pcdu::request::Request,
|
||||
) -> types::ccsds::CcsdsTcPacketOwned {
|
||||
types::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)]
|
||||
pub struct SerialInterfaceTest {
|
||||
pub inner: SerialInterfaceDummy,
|
||||
pub send_queue: RefCell<VecDeque<Vec<u8>>>,
|
||||
pub reply_queue: RefCell<VecDeque<Vec<u8>>>,
|
||||
/// Makes the next `send` call fail, to exercise comm-error handling.
|
||||
pub fail_next_send: RefCell<bool>,
|
||||
}
|
||||
|
||||
impl SerialInterface for SerialInterfaceTest {
|
||||
type Error = ();
|
||||
|
||||
fn send(&self, data: &[u8]) -> Result<(), Self::Error> {
|
||||
if self.fail_next_send.replace(false) {
|
||||
return Err(());
|
||||
}
|
||||
let mut send_queue_mut = self.send_queue.borrow_mut();
|
||||
send_queue_mut.push_back(data.to_vec());
|
||||
self.inner.send(data)
|
||||
}
|
||||
|
||||
fn try_recv_replies<ReplyHandler: FnMut(&[u8])>(
|
||||
&self,
|
||||
mut f: ReplyHandler,
|
||||
) -> Result<(), Self::Error> {
|
||||
if self.inner.reply_queue_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
loop {
|
||||
let reply = self.inner.next_reply_serialized();
|
||||
self.reply_queue.borrow_mut().push_back(reply.clone());
|
||||
f(&reply);
|
||||
if self.inner.reply_queue_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct PcduTestbench {
|
||||
pub mode_request_tx: mpsc::SyncSender<types::pcdu::request::Request>,
|
||||
pub mode_reply_rx_to_parent: mpsc::Receiver<types::pcdu::response::Response>,
|
||||
pub tc_tx: mpsc::SyncSender<CcsdsTcPacketOwned>,
|
||||
pub tm_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
|
||||
pub switch_request_tx: mpsc::Sender<SwitchRequest>,
|
||||
pub event_rx: mpsc::Receiver<pcdu::Event>,
|
||||
pub handler: PcduHandler<SerialInterfaceTest>,
|
||||
}
|
||||
|
||||
impl PcduTestbench {
|
||||
pub fn new() -> Self {
|
||||
let (mode_request_tx, _mode_request_rx) = 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 (tm_tx, tm_rx) = mpsc::sync_channel(5);
|
||||
let (switch_request_tx, switch_reqest_rx) = mpsc::channel();
|
||||
let (event_tx, event_rx) = mpsc::sync_channel(5);
|
||||
let shared_switch_map =
|
||||
Arc::new(Mutex::new(SwitchSet::new_with_init_switches_unknown()));
|
||||
let handler = PcduHandler::new(
|
||||
tc_rx,
|
||||
tm_tx.clone(),
|
||||
switch_reqest_rx,
|
||||
SerialInterfaceTest::default(),
|
||||
shared_switch_map,
|
||||
DeviceMode::Off,
|
||||
event_tx,
|
||||
);
|
||||
Self {
|
||||
mode_request_tx,
|
||||
mode_reply_rx_to_parent,
|
||||
tc_tx,
|
||||
tm_rx,
|
||||
switch_request_tx,
|
||||
event_rx,
|
||||
handler,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_switch_info_req_was_sent(&self, expected_queue_len: usize) {
|
||||
// Check that there is now communication happening.
|
||||
let mut send_queue_mut = self.handler.com_interface.send_queue.borrow_mut();
|
||||
assert_eq!(send_queue_mut.len(), expected_queue_len);
|
||||
let packet_sent = send_queue_mut.pop_front().unwrap();
|
||||
drop(send_queue_mut);
|
||||
let pcdu_req: PcduRequest = postcard::from_bytes(&packet_sent).unwrap();
|
||||
assert_eq!(pcdu_req, PcduRequest::RequestSwitchInfo);
|
||||
}
|
||||
|
||||
pub fn verify_switch_req_was_sent(
|
||||
&self,
|
||||
expected_queue_len: usize,
|
||||
switch_id: SwitchId,
|
||||
target_state: SwitchStateBinary,
|
||||
) {
|
||||
// Check that there is now communication happening.
|
||||
let mut send_queue_mut = self.handler.com_interface.send_queue.borrow_mut();
|
||||
assert_eq!(send_queue_mut.len(), expected_queue_len);
|
||||
let packet_sent = send_queue_mut.pop_front().unwrap();
|
||||
drop(send_queue_mut);
|
||||
let pcdu_req: PcduRequest = postcard::from_bytes(&packet_sent).unwrap();
|
||||
assert_eq!(
|
||||
pcdu_req,
|
||||
PcduRequest::SwitchDevice {
|
||||
switch: switch_id,
|
||||
state: target_state
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
pub fn verify_switch_reply_received(
|
||||
&self,
|
||||
expected_queue_len: usize,
|
||||
expected_map: SwitchMapBinary,
|
||||
) {
|
||||
// Check that a switch reply was read back.
|
||||
let mut reply_received_mut = self.handler.com_interface.reply_queue.borrow_mut();
|
||||
assert_eq!(reply_received_mut.len(), expected_queue_len);
|
||||
let reply_received = reply_received_mut.pop_front().unwrap();
|
||||
let sim_reply: SimReply = postcard::from_bytes(&reply_received).unwrap();
|
||||
assert_eq!(
|
||||
sim_reply,
|
||||
SimReply::Pcdu(PcduReply::SwitchInfo(expected_map))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_periodic_command_send_failure_sends_event() {
|
||||
let testbench = PcduTestbench::new();
|
||||
*testbench.handler.com_interface.fail_next_send.borrow_mut() = true;
|
||||
testbench.handler.handle_periodic_commands();
|
||||
let event = testbench
|
||||
.event_rx
|
||||
.try_recv()
|
||||
.expect("expected comm error event");
|
||||
assert!(matches!(event, pcdu::Event::SerialCommError));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_handler() {
|
||||
let mut testbench = PcduTestbench::new();
|
||||
assert_eq!(testbench.handler.com_interface.send_queue.borrow().len(), 0);
|
||||
assert_eq!(
|
||||
testbench.handler.com_interface.reply_queue.borrow().len(),
|
||||
0
|
||||
);
|
||||
assert_eq!(testbench.handler.mode(), DeviceMode::Off);
|
||||
testbench.handler.periodic_operation(OpCode::RegularOp);
|
||||
testbench
|
||||
.handler
|
||||
.periodic_operation(OpCode::PollAndRecvReplies);
|
||||
// Handler is OFF, no changes expected.
|
||||
assert_eq!(testbench.handler.com_interface.send_queue.borrow().len(), 0);
|
||||
assert_eq!(
|
||||
testbench.handler.com_interface.reply_queue.borrow().len(),
|
||||
0
|
||||
);
|
||||
assert_eq!(testbench.handler.mode(), DeviceMode::Off);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normal_mode() {
|
||||
let mut testbench = PcduTestbench::new();
|
||||
testbench
|
||||
.tc_tx
|
||||
.send(create_request_tc(pcdu::request::Request::Mode(
|
||||
DeviceMode::Normal,
|
||||
)))
|
||||
.unwrap();
|
||||
let switch_map_shared = testbench.handler.shared_switch_map.lock().unwrap();
|
||||
assert!(switch_map_shared.valid);
|
||||
drop(switch_map_shared);
|
||||
testbench.handler.periodic_operation(OpCode::RegularOp);
|
||||
testbench
|
||||
.handler
|
||||
.periodic_operation(OpCode::PollAndRecvReplies);
|
||||
// Check correctness of mode.
|
||||
assert_eq!(testbench.handler.mode(), DeviceMode::Normal);
|
||||
|
||||
testbench.verify_switch_info_req_was_sent(1);
|
||||
testbench.verify_switch_reply_received(1, SwitchMapBinaryWrapper::default().0);
|
||||
|
||||
let switch_map_shared = testbench.handler.shared_switch_map.lock().unwrap();
|
||||
assert!(switch_map_shared.valid);
|
||||
drop(switch_map_shared);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_switch_request_handling() {
|
||||
let mut testbench = PcduTestbench::new();
|
||||
testbench
|
||||
.tc_tx
|
||||
.send(create_request_tc(pcdu::request::Request::Mode(
|
||||
DeviceMode::Normal,
|
||||
)))
|
||||
.unwrap();
|
||||
testbench
|
||||
.switch_request_tx
|
||||
.send(SwitchRequest::new(SwitchId::Mgm0, SwitchStateBinary::On))
|
||||
.expect("failed to send switch request");
|
||||
testbench.handler.periodic_operation(OpCode::RegularOp);
|
||||
testbench
|
||||
.handler
|
||||
.periodic_operation(OpCode::PollAndRecvReplies);
|
||||
|
||||
testbench.verify_switch_req_was_sent(2, SwitchId::Mgm0, SwitchStateBinary::On);
|
||||
testbench.verify_switch_info_req_was_sent(1);
|
||||
let mut switch_map = SwitchMapBinaryWrapper::default().0;
|
||||
*switch_map
|
||||
.get_mut(&SwitchId::Mgm0)
|
||||
.expect("switch state setting failed") = SwitchStateBinary::On;
|
||||
testbench.verify_switch_reply_received(1, switch_map);
|
||||
|
||||
let switch_map_shared = testbench.handler.shared_switch_map.lock().unwrap();
|
||||
assert!(switch_map_shared.valid);
|
||||
drop(switch_map_shared);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use satrs::spacepackets::CcsdsPacketIdAndPsc;
|
||||
use types::{
|
||||
ComponentId, Event, EventId, Message,
|
||||
acs::{mgm, mgm_assembly, mgt},
|
||||
ccsds::{CcsdsTcPacketOwned, CcsdsTmPacketOwned},
|
||||
control,
|
||||
event_manager::{request::Request, response::Response},
|
||||
pcdu, tmtc,
|
||||
};
|
||||
|
||||
use crate::ccsds::pack_ccsds_tm_packet_for_now;
|
||||
|
||||
pub struct EventManager {
|
||||
pub tc_rx: std::sync::mpsc::Receiver<CcsdsTcPacketOwned>,
|
||||
pub ctrl_rx: std::sync::mpsc::Receiver<control::Event>,
|
||||
/// Shared by all MGM instances, which is why the sender ID is part of the message.
|
||||
pub mgm_rx: std::sync::mpsc::Receiver<(ComponentId, mgm::Event)>,
|
||||
pub mgm_assembly_rx: std::sync::mpsc::Receiver<mgm_assembly::Event>,
|
||||
pub mgt_rx: std::sync::mpsc::Receiver<mgt::Event>,
|
||||
pub pcdu_rx: std::sync::mpsc::Receiver<pcdu::Event>,
|
||||
/// Shared by all TC sources, which is why the sender ID is part of the message.
|
||||
pub tc_source_rx: std::sync::mpsc::Receiver<(ComponentId, tmtc::Event)>,
|
||||
pub tm_tx: std::sync::mpsc::SyncSender<CcsdsTmPacketOwned>,
|
||||
/// Senders with all their event TM generation disabled.
|
||||
disabled_components: HashSet<ComponentId>,
|
||||
/// Individual (sender, event ID) pairs with their TM generation disabled.
|
||||
disabled_events: HashSet<(ComponentId, u16)>,
|
||||
}
|
||||
|
||||
impl EventManager {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
tc_rx: std::sync::mpsc::Receiver<CcsdsTcPacketOwned>,
|
||||
ctrl_rx: std::sync::mpsc::Receiver<control::Event>,
|
||||
mgm_rx: std::sync::mpsc::Receiver<(ComponentId, mgm::Event)>,
|
||||
mgm_assembly_rx: std::sync::mpsc::Receiver<mgm_assembly::Event>,
|
||||
mgt_rx: std::sync::mpsc::Receiver<mgt::Event>,
|
||||
pcdu_rx: std::sync::mpsc::Receiver<pcdu::Event>,
|
||||
tc_source_rx: std::sync::mpsc::Receiver<(ComponentId, tmtc::Event)>,
|
||||
tm_tx: std::sync::mpsc::SyncSender<CcsdsTmPacketOwned>,
|
||||
) -> Self {
|
||||
Self {
|
||||
tc_rx,
|
||||
ctrl_rx,
|
||||
mgm_rx,
|
||||
mgm_assembly_rx,
|
||||
mgt_rx,
|
||||
pcdu_rx,
|
||||
tc_source_rx,
|
||||
tm_tx,
|
||||
disabled_components: HashSet::new(),
|
||||
disabled_events: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Silences all event TM generation for `sender_id`, regardless of the specific event.
|
||||
fn disable_component(&mut self, sender_id: ComponentId) {
|
||||
self.disabled_components.insert(sender_id);
|
||||
}
|
||||
|
||||
fn enable_component(&mut self, sender_id: ComponentId) {
|
||||
self.disabled_components.remove(&sender_id);
|
||||
}
|
||||
|
||||
/// Silences TM generation for one specific event of `sender_id`, leaving its other events
|
||||
/// unaffected.
|
||||
fn disable_event(&mut self, sender_id: ComponentId, event_id: u16) {
|
||||
self.disabled_events.insert((sender_id, event_id));
|
||||
}
|
||||
|
||||
fn enable_event(&mut self, sender_id: ComponentId, event_id: u16) {
|
||||
self.disabled_events.remove(&(sender_id, event_id));
|
||||
}
|
||||
|
||||
fn tm_generation_enabled(&self, sender_id: ComponentId, event_id: u16) -> bool {
|
||||
!self.disabled_components.contains(&sender_id)
|
||||
&& !self.disabled_events.contains(&(sender_id, event_id))
|
||||
}
|
||||
|
||||
pub fn periodic_operation(&mut self) {
|
||||
// Telecommands first, so filter changes already apply to the events of this cycle.
|
||||
self.handle_telecommands();
|
||||
while let Ok(event) = self.ctrl_rx.try_recv() {
|
||||
self.event_to_tm(ComponentId::Controller, &Event::ControllerEvent(event));
|
||||
}
|
||||
while let Ok((sender_id, event)) = self.mgm_rx.try_recv() {
|
||||
self.event_to_tm(sender_id, &event);
|
||||
}
|
||||
while let Ok(event) = self.mgm_assembly_rx.try_recv() {
|
||||
self.event_to_tm(ComponentId::AcsMgmAssembly, &event);
|
||||
}
|
||||
while let Ok(event) = self.mgt_rx.try_recv() {
|
||||
self.event_to_tm(ComponentId::AcsMgt, &event);
|
||||
}
|
||||
while let Ok(event) = self.pcdu_rx.try_recv() {
|
||||
self.event_to_tm(ComponentId::EpsPcdu, &event);
|
||||
}
|
||||
while let Ok((sender_id, event)) = self.tc_source_rx.try_recv() {
|
||||
self.event_to_tm(sender_id, &event);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_telecommands(&mut self) {
|
||||
while let Ok(packet) = self.tc_rx.try_recv() {
|
||||
let tc_id = CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet.sp_header);
|
||||
let request = match postcard::from_bytes::<Request>(&packet.payload) {
|
||||
Ok(request) => request,
|
||||
Err(e) => {
|
||||
log::warn!("failed to deserialize event manager request: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
log::info!(
|
||||
"received request {:?} with TC ID {:#010x}",
|
||||
request,
|
||||
tc_id.raw()
|
||||
);
|
||||
match request {
|
||||
Request::EnableComponent(sender_id) => self.enable_component(sender_id),
|
||||
Request::DisableComponent(sender_id) => self.disable_component(sender_id),
|
||||
Request::EnableEvent {
|
||||
sender_id,
|
||||
event_id,
|
||||
} => self.enable_event(sender_id, event_id),
|
||||
Request::DisableEvent {
|
||||
sender_id,
|
||||
event_id,
|
||||
} => self.disable_event(sender_id, event_id),
|
||||
}
|
||||
self.send_tm(ComponentId::EventManager, Some(tc_id), &Response::Ok);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn event_to_tm(
|
||||
&mut self,
|
||||
sender_id: ComponentId,
|
||||
event: &(impl serde::Serialize + Message + EventId + core::fmt::Debug),
|
||||
) {
|
||||
let tm_enabled = self.tm_generation_enabled(sender_id, event.event_id());
|
||||
log::info!(
|
||||
"event {:?} (ID {}) from {:?}{}",
|
||||
event,
|
||||
event.event_id(),
|
||||
sender_id,
|
||||
if tm_enabled { "" } else { ", TM disabled" }
|
||||
);
|
||||
if !tm_enabled {
|
||||
return;
|
||||
}
|
||||
self.send_tm(sender_id, None, event);
|
||||
}
|
||||
|
||||
fn send_tm(
|
||||
&self,
|
||||
sender_id: ComponentId,
|
||||
tc_id: Option<CcsdsPacketIdAndPsc>,
|
||||
payload: &(impl serde::Serialize + Message),
|
||||
) {
|
||||
match pack_ccsds_tm_packet_for_now(sender_id, tc_id, payload) {
|
||||
Ok(packet) => {
|
||||
if let Err(e) = self.tm_tx.send(packet) {
|
||||
log::warn!("error sending TM packet: {:?}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("error packing TM packet: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::mpsc::{self, TryRecvError};
|
||||
|
||||
use arbitrary_int::u11;
|
||||
use satrs::spacepackets::SpacePacketHeader;
|
||||
use types::{Apid, MessageType, TcHeader, acs::mgm};
|
||||
|
||||
use super::*;
|
||||
|
||||
struct Testbench {
|
||||
tc_tx: mpsc::SyncSender<CcsdsTcPacketOwned>,
|
||||
mgm_event_tx: mpsc::SyncSender<(ComponentId, mgm::Event)>,
|
||||
tm_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
|
||||
event_manager: EventManager,
|
||||
}
|
||||
|
||||
impl Testbench {
|
||||
fn new() -> Self {
|
||||
let (tc_tx, tc_rx) = mpsc::sync_channel(5);
|
||||
let (_ctrl_tx, ctrl_rx) = mpsc::sync_channel(5);
|
||||
let (mgm_event_tx, mgm_rx) = mpsc::sync_channel(5);
|
||||
let (_mgm_assembly_tx, mgm_assembly_rx) = mpsc::sync_channel(5);
|
||||
let (_mgt_tx, mgt_rx) = mpsc::sync_channel(5);
|
||||
let (_pcdu_tx, pcdu_rx) = mpsc::sync_channel(5);
|
||||
let (_tc_source_tx, tc_source_rx) = mpsc::sync_channel(5);
|
||||
let (tm_tx, tm_rx) = mpsc::sync_channel(5);
|
||||
Self {
|
||||
tc_tx,
|
||||
mgm_event_tx,
|
||||
tm_rx,
|
||||
event_manager: EventManager::new(
|
||||
tc_rx,
|
||||
ctrl_rx,
|
||||
mgm_rx,
|
||||
mgm_assembly_rx,
|
||||
mgt_rx,
|
||||
pcdu_rx,
|
||||
tc_source_rx,
|
||||
tm_tx,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn send_request(&self, request: Request) {
|
||||
self.tc_tx
|
||||
.send(CcsdsTcPacketOwned::new_with_request(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Tmtc as u16)),
|
||||
TcHeader::new(ComponentId::EventManager, MessageType::Event),
|
||||
request,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_events_enabled_by_default() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.mgm_event_tx
|
||||
.send((ComponentId::AcsMgm0, mgm::Event::SpiFaultThresholdExceeded))
|
||||
.unwrap();
|
||||
tb.event_manager.periodic_operation();
|
||||
tb.tm_rx.try_recv().expect("expected event TM");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disabled_component_silences_all_its_events() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.event_manager.disable_component(ComponentId::AcsMgm0);
|
||||
tb.mgm_event_tx
|
||||
.send((ComponentId::AcsMgm0, mgm::Event::SpiFaultThresholdExceeded))
|
||||
.unwrap();
|
||||
tb.mgm_event_tx
|
||||
.send((
|
||||
ComponentId::AcsMgm0,
|
||||
mgm::Event::ModeChanged(types::DeviceMode::Normal),
|
||||
))
|
||||
.unwrap();
|
||||
tb.event_manager.periodic_operation();
|
||||
assert!(matches!(tb.tm_rx.try_recv(), Err(TryRecvError::Empty)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disabled_component_does_not_affect_other_components() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.event_manager.disable_component(ComponentId::AcsMgm1);
|
||||
tb.mgm_event_tx
|
||||
.send((ComponentId::AcsMgm0, mgm::Event::SpiFaultThresholdExceeded))
|
||||
.unwrap();
|
||||
tb.event_manager.periodic_operation();
|
||||
tb.tm_rx.try_recv().expect("expected event TM");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disabled_event_only_silences_that_specific_event() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.event_manager.disable_event(
|
||||
ComponentId::AcsMgm0,
|
||||
mgm::Event::SpiFaultThresholdExceeded.event_id(),
|
||||
);
|
||||
tb.mgm_event_tx
|
||||
.send((ComponentId::AcsMgm0, mgm::Event::SpiFaultThresholdExceeded))
|
||||
.unwrap();
|
||||
tb.mgm_event_tx
|
||||
.send((
|
||||
ComponentId::AcsMgm0,
|
||||
mgm::Event::ModeChanged(types::DeviceMode::Normal),
|
||||
))
|
||||
.unwrap();
|
||||
tb.event_manager.periodic_operation();
|
||||
let tm = tb.tm_rx.try_recv().expect("expected event TM");
|
||||
let event: mgm::Event = postcard::from_bytes(&tm.payload).unwrap();
|
||||
assert!(matches!(event, mgm::Event::ModeChanged(_)));
|
||||
assert!(matches!(tb.tm_rx.try_recv(), Err(TryRecvError::Empty)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_re_enabling_a_component_restores_its_events() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.event_manager.disable_component(ComponentId::AcsMgm0);
|
||||
tb.event_manager.enable_component(ComponentId::AcsMgm0);
|
||||
tb.mgm_event_tx
|
||||
.send((ComponentId::AcsMgm0, mgm::Event::SpiFaultThresholdExceeded))
|
||||
.unwrap();
|
||||
tb.event_manager.periodic_operation();
|
||||
tb.tm_rx.try_recv().expect("expected event TM");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disable_component_request() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.send_request(Request::DisableComponent(ComponentId::AcsMgm0));
|
||||
tb.mgm_event_tx
|
||||
.send((ComponentId::AcsMgm0, mgm::Event::SpiFaultThresholdExceeded))
|
||||
.unwrap();
|
||||
tb.event_manager.periodic_operation();
|
||||
|
||||
let tm = tb.tm_rx.try_recv().expect("expected request response TM");
|
||||
assert_eq!(tm.tm_header.sender_id, ComponentId::EventManager);
|
||||
assert!(tm.tm_header.tc_id.is_some());
|
||||
let response: Response = postcard::from_bytes(&tm.payload).unwrap();
|
||||
assert_eq!(response, Response::Ok);
|
||||
assert!(matches!(tb.tm_rx.try_recv(), Err(TryRecvError::Empty)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disable_event_request() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.send_request(Request::DisableEvent {
|
||||
sender_id: ComponentId::AcsMgm0,
|
||||
event_id: mgm::Event::SpiFaultThresholdExceeded.event_id(),
|
||||
});
|
||||
tb.event_manager.periodic_operation();
|
||||
tb.tm_rx.try_recv().expect("expected request response TM");
|
||||
|
||||
tb.send_request(Request::EnableEvent {
|
||||
sender_id: ComponentId::AcsMgm0,
|
||||
event_id: mgm::Event::SpiFaultThresholdExceeded.event_id(),
|
||||
});
|
||||
tb.mgm_event_tx
|
||||
.send((ComponentId::AcsMgm0, mgm::Event::SpiFaultThresholdExceeded))
|
||||
.unwrap();
|
||||
tb.event_manager.periodic_operation();
|
||||
tb.tm_rx.try_recv().expect("expected request response TM");
|
||||
tb.tm_rx.try_recv().expect("expected event TM");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
//! This module contains all component related to the direct interface of the example.
|
||||
pub mod sim_client_udp;
|
||||
pub mod tcp;
|
||||
pub mod udp;
|
||||
@@ -0,0 +1,402 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
net::{Ipv4Addr, SocketAddr, SocketAddrV4, UdpSocket},
|
||||
sync::mpsc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use minisim_types::{ComponentId, SimReply, SimRequestWithTime, udp::SIM_CTRL_PORT};
|
||||
use minisim_types::{SimCtrlReply, SimCtrlRequest};
|
||||
use satrs::HandlingStatus;
|
||||
|
||||
struct SimReplyMap(pub HashMap<ComponentId, mpsc::Sender<SimReply>>);
|
||||
|
||||
pub fn create_sim_client(
|
||||
sim_request_rx: mpsc::Receiver<SimRequestWithTime>,
|
||||
) -> Option<SimClientUdp> {
|
||||
match SimClientUdp::new(
|
||||
SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, SIM_CTRL_PORT)),
|
||||
sim_request_rx,
|
||||
) {
|
||||
Ok(sim_client) => {
|
||||
log::info!("simulator client connection success");
|
||||
return Some(sim_client);
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("sim client creation error: {e}");
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum SimClientCreationError {
|
||||
#[error("io error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("timeout when trying to connect to sim UDP server")]
|
||||
Timeout,
|
||||
#[error("invalid ping reply when trying connection to UDP sim server: {0}")]
|
||||
InvalidReply(#[from] postcard::Error),
|
||||
#[error("invalid sim reply, not pong reply as expected: {0:?}")]
|
||||
ReplyIsNotPong(Box<SimReply>),
|
||||
}
|
||||
|
||||
pub struct SimClientUdp {
|
||||
udp_client: UdpSocket,
|
||||
simulator_addr: SocketAddr,
|
||||
sim_request_rx: mpsc::Receiver<SimRequestWithTime>,
|
||||
reply_map: SimReplyMap,
|
||||
reply_buf: [u8; 4096],
|
||||
}
|
||||
|
||||
impl SimClientUdp {
|
||||
pub fn new(
|
||||
simulator_addr: SocketAddr,
|
||||
sim_request_rx: mpsc::Receiver<SimRequestWithTime>,
|
||||
) -> Result<Self, SimClientCreationError> {
|
||||
let mut reply_buf: [u8; 4096] = [0; 4096];
|
||||
let mut udp_client = UdpSocket::bind("127.0.0.1:0")?;
|
||||
udp_client.set_read_timeout(Some(Duration::from_millis(100)))?;
|
||||
Self::attempt_connection(&mut udp_client, simulator_addr, &mut reply_buf)?;
|
||||
udp_client.set_nonblocking(true)?;
|
||||
Ok(Self {
|
||||
udp_client,
|
||||
simulator_addr,
|
||||
sim_request_rx,
|
||||
reply_map: SimReplyMap(HashMap::new()),
|
||||
reply_buf,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn attempt_connection(
|
||||
udp_client: &mut UdpSocket,
|
||||
simulator_addr: SocketAddr,
|
||||
reply_buf: &mut [u8],
|
||||
) -> Result<(), SimClientCreationError> {
|
||||
let sim_req = SimRequestWithTime::new_with_epoch_time(SimCtrlRequest::Ping);
|
||||
let sim_req_raw = postcard::to_allocvec(&sim_req).expect("failed to serialize SimRequest");
|
||||
udp_client.send_to(&sim_req_raw, simulator_addr)?;
|
||||
match udp_client.recv(reply_buf) {
|
||||
Ok(reply_len) => {
|
||||
let sim_reply: SimReply = postcard::from_bytes(&reply_buf[0..reply_len])?;
|
||||
match sim_reply {
|
||||
SimReply::SimCtrl(SimCtrlReply::Pong) => Ok(()),
|
||||
_ => Err(SimClientCreationError::ReplyIsNotPong(Box::new(sim_reply))),
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
if e.kind() == std::io::ErrorKind::TimedOut
|
||||
|| e.kind() == std::io::ErrorKind::WouldBlock
|
||||
{
|
||||
Err(SimClientCreationError::Timeout)
|
||||
} else {
|
||||
Err(SimClientCreationError::Io(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn operation(&mut self) -> HandlingStatus {
|
||||
let mut no_sim_requests_handled = true;
|
||||
let mut no_data_from_udp_server_received = true;
|
||||
loop {
|
||||
match self.sim_request_rx.try_recv() {
|
||||
Ok(request) => {
|
||||
let request_raw =
|
||||
postcard::to_allocvec(&request).expect("failed to serialize SimRequest");
|
||||
if let Err(e) = self.udp_client.send_to(&request_raw, self.simulator_addr) {
|
||||
log::error!("error sending data to UDP SIM server: {e}");
|
||||
break;
|
||||
} else {
|
||||
no_sim_requests_handled = false;
|
||||
}
|
||||
}
|
||||
Err(e) => match e {
|
||||
mpsc::TryRecvError::Empty => {
|
||||
break;
|
||||
}
|
||||
mpsc::TryRecvError::Disconnected => {
|
||||
log::warn!("SIM request sender disconnected");
|
||||
break;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
loop {
|
||||
match self.udp_client.recv(&mut self.reply_buf) {
|
||||
Ok(recvd_bytes) => {
|
||||
no_data_from_udp_server_received = false;
|
||||
let sim_reply_result: postcard::Result<SimReply> =
|
||||
postcard::from_bytes(&self.reply_buf[0..recvd_bytes]);
|
||||
match sim_reply_result {
|
||||
Ok(sim_reply) => {
|
||||
if let Some(sender) = self.reply_map.0.get(&sim_reply.component()) {
|
||||
sender.send(sim_reply).expect("failed to send SIM reply");
|
||||
} else {
|
||||
log::warn!(
|
||||
"no recipient for SIM reply from component {:?}",
|
||||
sim_reply.component()
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("failed to deserialize SIM reply: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
if e.kind() == std::io::ErrorKind::WouldBlock
|
||||
|| e.kind() == std::io::ErrorKind::TimedOut
|
||||
{
|
||||
break;
|
||||
}
|
||||
log::error!("error receiving data from UDP SIM server: {e}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if no_sim_requests_handled && no_data_from_udp_server_received {
|
||||
return HandlingStatus::Empty;
|
||||
}
|
||||
HandlingStatus::HandledOne
|
||||
}
|
||||
|
||||
pub fn add_reply_recipient(
|
||||
&mut self,
|
||||
component: ComponentId,
|
||||
reply_sender: mpsc::Sender<SimReply>,
|
||||
) {
|
||||
self.reply_map.0.insert(component, reply_sender);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use std::{
|
||||
net::{SocketAddr, UdpSocket},
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
mpsc,
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use minisim_types::{
|
||||
ComponentId, SimCtrlReply, SimCtrlRequest, SimReply, SimRequest, SimRequestWithTime,
|
||||
eps::{PcduReply, PcduRequest},
|
||||
};
|
||||
|
||||
use types::pcdu::SwitchMapBinary;
|
||||
|
||||
use super::SimClientUdp;
|
||||
|
||||
struct UdpSimTestServer {
|
||||
udp_server: UdpSocket,
|
||||
request_tx: mpsc::Sender<SimRequestWithTime>,
|
||||
reply_rx: mpsc::Receiver<SimReply>,
|
||||
last_sender: Option<SocketAddr>,
|
||||
stop_signal: Arc<AtomicBool>,
|
||||
recv_buf: [u8; 1024],
|
||||
}
|
||||
|
||||
impl UdpSimTestServer {
|
||||
pub fn new(
|
||||
request_tx: mpsc::Sender<SimRequestWithTime>,
|
||||
reply_rx: mpsc::Receiver<SimReply>,
|
||||
stop_signal: Arc<AtomicBool>,
|
||||
) -> Self {
|
||||
let udp_server = UdpSocket::bind("127.0.0.1:0").expect("creating UDP server failed");
|
||||
udp_server
|
||||
.set_nonblocking(true)
|
||||
.expect("failed to set UDP server to non-blocking");
|
||||
Self {
|
||||
udp_server,
|
||||
request_tx,
|
||||
reply_rx,
|
||||
last_sender: None,
|
||||
stop_signal,
|
||||
recv_buf: [0; 1024],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn operation(&mut self) {
|
||||
loop {
|
||||
let mut no_sim_replies_handled = true;
|
||||
let mut no_data_received = true;
|
||||
if self.stop_signal.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
if let Some(last_sender) = self.last_sender {
|
||||
loop {
|
||||
match self.reply_rx.try_recv() {
|
||||
Ok(sim_reply) => {
|
||||
let sim_reply_raw = postcard::to_allocvec(&sim_reply)
|
||||
.expect("failed to serialize SimReply");
|
||||
self.udp_server
|
||||
.send_to(&sim_reply_raw, last_sender)
|
||||
.expect("failed to send reply to client from UDP server");
|
||||
no_sim_replies_handled = false;
|
||||
}
|
||||
Err(e) => match e {
|
||||
mpsc::TryRecvError::Empty => break,
|
||||
mpsc::TryRecvError::Disconnected => {
|
||||
panic!("reply sender disconnected")
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
match self.udp_server.recv_from(&mut self.recv_buf) {
|
||||
Ok((read_bytes, from)) => {
|
||||
let sim_request: SimRequestWithTime =
|
||||
postcard::from_bytes(&self.recv_buf[0..read_bytes])
|
||||
.expect("failed to deserialize SimRequest");
|
||||
// For a ping, we perform the reply handling here directly
|
||||
if sim_request.request == SimRequest::SimCtrl(SimCtrlRequest::Ping) {
|
||||
no_data_received = false;
|
||||
self.last_sender = Some(from);
|
||||
let sim_reply = SimReply::from(SimCtrlReply::Pong);
|
||||
let sim_reply_raw = postcard::to_allocvec(&sim_reply)
|
||||
.expect("failed to serialize SimReply");
|
||||
self.udp_server
|
||||
.send_to(&sim_reply_raw, from)
|
||||
.expect("failed to send reply to client from UDP server");
|
||||
}
|
||||
// Forward each SIM request for testing purposes.
|
||||
self.request_tx
|
||||
.send(sim_request)
|
||||
.expect("failed to send request");
|
||||
}
|
||||
Err(e) => {
|
||||
if e.kind() != std::io::ErrorKind::WouldBlock
|
||||
&& e.kind() != std::io::ErrorKind::TimedOut
|
||||
{
|
||||
panic!("UDP server error: {}", e);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if no_sim_replies_handled && no_data_received {
|
||||
std::thread::sleep(Duration::from_millis(5));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn local_addr(&self) -> SocketAddr {
|
||||
self.udp_server.local_addr().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_connection_test() {
|
||||
let (server_sim_request_tx, server_sim_request_rx) = mpsc::channel();
|
||||
let (_server_sim_reply_tx, server_sim_reply_rx) = mpsc::channel();
|
||||
let stop_signal = Arc::new(AtomicBool::new(false));
|
||||
let mut udp_server = UdpSimTestServer::new(
|
||||
server_sim_request_tx,
|
||||
server_sim_reply_rx,
|
||||
stop_signal.clone(),
|
||||
);
|
||||
let server_addr = udp_server.local_addr();
|
||||
let (_client_sim_req_tx, client_sim_req_rx) = mpsc::channel();
|
||||
// Need to spawn the simulator UDP server before calling the client constructor.
|
||||
let jh0 = std::thread::spawn(move || {
|
||||
udp_server.operation();
|
||||
});
|
||||
// Creating the client also performs the connection test.
|
||||
SimClientUdp::new(server_addr, client_sim_req_rx).unwrap();
|
||||
let sim_request = server_sim_request_rx
|
||||
.recv_timeout(Duration::from_millis(50))
|
||||
.expect("no SIM request received");
|
||||
assert_eq!(
|
||||
sim_request.request,
|
||||
SimRequest::SimCtrl(SimCtrlRequest::Ping)
|
||||
);
|
||||
// Stop the server.
|
||||
stop_signal.store(true, Ordering::Relaxed);
|
||||
jh0.join().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_request_reply_test() {
|
||||
let (server_sim_request_tx, server_sim_request_rx) = mpsc::channel();
|
||||
let (server_sim_reply_tx, sever_sim_reply_rx) = mpsc::channel();
|
||||
let stop_signal = Arc::new(AtomicBool::new(false));
|
||||
let mut udp_server = UdpSimTestServer::new(
|
||||
server_sim_request_tx,
|
||||
sever_sim_reply_rx,
|
||||
stop_signal.clone(),
|
||||
);
|
||||
let server_addr = udp_server.local_addr();
|
||||
let (client_sim_req_tx, client_sim_req_rx) = mpsc::channel();
|
||||
let (client_pcdu_reply_tx, client_pcdu_reply_rx) = mpsc::channel();
|
||||
// Need to spawn the simulator UDP server before calling the client constructor.
|
||||
let jh0 = std::thread::spawn(move || {
|
||||
udp_server.operation();
|
||||
});
|
||||
|
||||
// Creating the client also performs the connection test.
|
||||
let mut client = SimClientUdp::new(server_addr, client_sim_req_rx).unwrap();
|
||||
client.add_reply_recipient(ComponentId::Pcdu, client_pcdu_reply_tx);
|
||||
|
||||
let sim_request = server_sim_request_rx
|
||||
.recv_timeout(Duration::from_millis(50))
|
||||
.expect("no SIM request received");
|
||||
assert_eq!(
|
||||
sim_request.request,
|
||||
SimRequest::SimCtrl(SimCtrlRequest::Ping)
|
||||
);
|
||||
|
||||
let pcdu_req = PcduRequest::RequestSwitchInfo;
|
||||
client_sim_req_tx
|
||||
.send(SimRequestWithTime::new_with_epoch_time(pcdu_req))
|
||||
.expect("send failed");
|
||||
client.operation();
|
||||
|
||||
// Check that the request arrives properly at the server.
|
||||
let sim_request = server_sim_request_rx
|
||||
.recv_timeout(Duration::from_millis(50))
|
||||
.expect("no SIM request received");
|
||||
assert_eq!(
|
||||
sim_request.request,
|
||||
SimRequest::Pcdu(PcduRequest::RequestSwitchInfo)
|
||||
);
|
||||
|
||||
// We inject the reply ourselves.
|
||||
let pcdu_reply = PcduReply::SwitchInfo(SwitchMapBinary::default());
|
||||
server_sim_reply_tx
|
||||
.send(SimReply::from(pcdu_reply.clone()))
|
||||
.expect("sending PCDU reply failed");
|
||||
|
||||
// Now we verify that the reply is sent by the UDP server back to the client, and then
|
||||
// forwarded by the clients internal map.
|
||||
let mut pcdu_reply_received = false;
|
||||
for _ in 0..3 {
|
||||
client.operation();
|
||||
|
||||
match client_pcdu_reply_rx.try_recv() {
|
||||
Ok(sim_reply) => {
|
||||
assert_eq!(sim_reply.component(), ComponentId::Pcdu);
|
||||
assert_eq!(sim_reply, SimReply::Pcdu(pcdu_reply.clone()));
|
||||
pcdu_reply_received = true;
|
||||
break;
|
||||
}
|
||||
Err(e) => match e {
|
||||
mpsc::TryRecvError::Empty => std::thread::sleep(Duration::from_millis(10)),
|
||||
mpsc::TryRecvError::Disconnected => panic!("reply sender disconnected"),
|
||||
},
|
||||
}
|
||||
}
|
||||
if !pcdu_reply_received {
|
||||
panic!("no reply received");
|
||||
}
|
||||
|
||||
// Stop the server.
|
||||
stop_signal.store(true, Ordering::Relaxed);
|
||||
jh0.join().unwrap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
use std::time::Duration;
|
||||
use std::{
|
||||
collections::{HashSet, VecDeque},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use log::{info, warn};
|
||||
use satrs::hal::std::tcp_spacepackets_server::CcsdsPacketParser;
|
||||
use satrs::{
|
||||
encoding::ccsds::{SpValidity, SpacePacketValidator},
|
||||
hal::std::tcp_server::{HandledConnectionHandler, ServerConfig, TcpSpacepacketsServer},
|
||||
spacepackets::{CcsdsPacket, PacketId},
|
||||
tmtc::PacketSource,
|
||||
};
|
||||
|
||||
use crate::tmtc::sender::TmTcSender;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ConnectionFinishedHandler {}
|
||||
|
||||
pub struct SimplePacketValidator {
|
||||
pub valid_ids: HashSet<PacketId>,
|
||||
}
|
||||
|
||||
impl SpacePacketValidator for SimplePacketValidator {
|
||||
fn validate(
|
||||
&self,
|
||||
sp_header: &satrs::spacepackets::SpHeader,
|
||||
_raw_buf: &[u8],
|
||||
) -> satrs::encoding::ccsds::SpValidity {
|
||||
if self.valid_ids.contains(&sp_header.packet_id()) {
|
||||
return SpValidity::Valid;
|
||||
}
|
||||
log::warn!("ignoring space packet with header {sp_header:?}");
|
||||
// We could perform a CRC check.. but lets keep this simple and assume that TCP ensures
|
||||
// data integrity.
|
||||
SpValidity::Skip
|
||||
}
|
||||
}
|
||||
|
||||
impl HandledConnectionHandler for ConnectionFinishedHandler {
|
||||
fn handled_connection(&mut self, info: satrs::hal::std::tcp_server::HandledConnectionInfo) {
|
||||
info!(
|
||||
"Served {} TMs and {} TCs for client {:?}",
|
||||
info.num_sent_tms, info.num_received_tcs, info.addr
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct SyncTcpTmSource {
|
||||
tm_queue: Arc<Mutex<VecDeque<Vec<u8>>>>,
|
||||
max_packets_stored: usize,
|
||||
pub silent_packet_overwrite: bool,
|
||||
}
|
||||
|
||||
impl SyncTcpTmSource {
|
||||
pub fn new(max_packets_stored: usize) -> Self {
|
||||
Self {
|
||||
tm_queue: Arc::default(),
|
||||
max_packets_stored,
|
||||
silent_packet_overwrite: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_tm(&mut self, tm: &[u8]) {
|
||||
let mut tm_queue = self.tm_queue.lock().expect("locking tm queue failec");
|
||||
if tm_queue.len() > self.max_packets_stored {
|
||||
if !self.silent_packet_overwrite {
|
||||
warn!("TPC TM source is full, deleting oldest packet");
|
||||
}
|
||||
tm_queue.pop_front();
|
||||
}
|
||||
tm_queue.push_back(tm.to_vec());
|
||||
}
|
||||
}
|
||||
|
||||
impl PacketSource for SyncTcpTmSource {
|
||||
type Error = ();
|
||||
|
||||
fn retrieve_packet(&mut self, buffer: &mut [u8]) -> Result<usize, Self::Error> {
|
||||
let mut tm_queue = self.tm_queue.lock().expect("locking tm queue failed");
|
||||
if !tm_queue.is_empty() {
|
||||
let next_vec = tm_queue.front().unwrap();
|
||||
if buffer.len() < next_vec.len() {
|
||||
panic!(
|
||||
"provided buffer too small, must be at least {} bytes",
|
||||
next_vec.len()
|
||||
);
|
||||
}
|
||||
let next_vec = tm_queue.pop_front().unwrap();
|
||||
buffer[0..next_vec.len()].copy_from_slice(&next_vec);
|
||||
if next_vec.len() > 9 {
|
||||
let service = next_vec[7];
|
||||
let subservice = next_vec[8];
|
||||
info!("Sending PUS TM[{service},{subservice}]")
|
||||
} else {
|
||||
info!("Sending PUS TM");
|
||||
}
|
||||
return Ok(next_vec.len());
|
||||
}
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
|
||||
pub type TcpServer<ReceivesTc> = TcpSpacepacketsServer<
|
||||
SyncTcpTmSource,
|
||||
ReceivesTc,
|
||||
SimplePacketValidator,
|
||||
ConnectionFinishedHandler,
|
||||
>;
|
||||
|
||||
pub struct TcpTask(pub TcpServer<TmTcSender>);
|
||||
|
||||
impl TcpTask {
|
||||
pub fn new(
|
||||
cfg: ServerConfig,
|
||||
tm_source: SyncTcpTmSource,
|
||||
tc_sender: TmTcSender,
|
||||
valid_ids: HashSet<PacketId>,
|
||||
) -> Result<Self, std::io::Error> {
|
||||
Ok(Self(TcpSpacepacketsServer::new(
|
||||
cfg,
|
||||
tm_source,
|
||||
CcsdsPacketParser::new(cfg.id, 2048, tc_sender, SimplePacketValidator { valid_ids }),
|
||||
ConnectionFinishedHandler::default(),
|
||||
None,
|
||||
)?))
|
||||
}
|
||||
|
||||
pub fn periodic_operation(&mut self) {
|
||||
let result = self
|
||||
.0
|
||||
.handle_all_connections(Some(Duration::from_millis(400)));
|
||||
match result {
|
||||
Ok(_conn_result) => (),
|
||||
Err(e) => {
|
||||
warn!("TCP server error: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
#![allow(dead_code)]
|
||||
use std::collections::VecDeque;
|
||||
use std::net::{SocketAddr, UdpSocket};
|
||||
use std::sync::{Arc, Mutex, mpsc};
|
||||
|
||||
use log::warn;
|
||||
use satrs::HandlingStatus;
|
||||
use satrs::hal::std::udp_server::{ReceiveResult, UdpTcServer};
|
||||
use satrs::queue::GenericSendError;
|
||||
use types::ccsds::CcsdsTmPacketOwned;
|
||||
|
||||
use crate::tmtc::sender::TmTcSender;
|
||||
|
||||
pub trait UdpTmHandlerProvider {
|
||||
fn send_tm_to_udp_client(&mut self, socket: &UdpSocket, recv_addr: &SocketAddr);
|
||||
}
|
||||
|
||||
pub struct UdpTmHandlerWithChannel {
|
||||
pub tm_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
|
||||
}
|
||||
|
||||
impl UdpTmHandlerProvider for UdpTmHandlerWithChannel {
|
||||
fn send_tm_to_udp_client(&mut self, socket: &UdpSocket, recv_addr: &SocketAddr) {
|
||||
while let Ok(tm) = self.tm_rx.try_recv() {
|
||||
log::debug!("sending TM from sender {:?}", tm.tm_header.sender_id);
|
||||
let result = socket.send_to(&tm.to_vec(), recv_addr);
|
||||
if let Err(e) = result {
|
||||
warn!("Sending TM with UDP socket failed: {e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct TestTmHandler {
|
||||
addrs_to_send_to: Arc<Mutex<VecDeque<SocketAddr>>>,
|
||||
}
|
||||
|
||||
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) {
|
||||
loop {
|
||||
if self.poll_tc_server() == HandlingStatus::Empty {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(recv_addr) = self.udp_tc_server.last_sender() {
|
||||
self.tm_handler
|
||||
.send_tm_to_udp_client(&self.udp_tc_server.socket, &recv_addr);
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_tc_server(&mut self) -> HandlingStatus {
|
||||
match self.udp_tc_server.try_recv_tc() {
|
||||
Ok(_) => HandlingStatus::HandledOne,
|
||||
Err(e) => {
|
||||
match e {
|
||||
ReceiveResult::NothingReceived => (),
|
||||
ReceiveResult::Io(e) => {
|
||||
warn!("IO error {e}");
|
||||
}
|
||||
ReceiveResult::Send(send_error) => {
|
||||
warn!("send error {send_error:?}");
|
||||
}
|
||||
}
|
||||
HandlingStatus::Empty
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::net::IpAddr;
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
use arbitrary_int::traits::Integer as _;
|
||||
use arbitrary_int::u14;
|
||||
use example_std::config::OBSW_SERVER_ADDR;
|
||||
use satrs::spacepackets::ecss::{CreatorConfig, MessageTypeId};
|
||||
use satrs::{
|
||||
ComponentId,
|
||||
spacepackets::{
|
||||
SpHeader,
|
||||
ecss::{WritablePusPacket, tc::PusTcCreator},
|
||||
},
|
||||
};
|
||||
use types::Apid;
|
||||
|
||||
use crate::tmtc::sender::MockSender;
|
||||
|
||||
use super::*;
|
||||
|
||||
const UDP_SERVER_ID: ComponentId = 0x05;
|
||||
|
||||
#[test]
|
||||
fn test_basic() {
|
||||
let sock_addr = SocketAddr::new(IpAddr::V4(OBSW_SERVER_ADDR), 0);
|
||||
let test_receiver = TmTcSender::Mock(MockSender::default());
|
||||
let udp_tc_server =
|
||||
UdpTcServer::new(UDP_SERVER_ID, sock_addr, 2048, test_receiver).unwrap();
|
||||
let tm_handler = TestTmHandler::default();
|
||||
let tm_handler_calls = tm_handler.addrs_to_send_to.clone();
|
||||
let mut udp_dyn_server = UdpTmtcServer {
|
||||
udp_tc_server,
|
||||
tm_handler: tm_handler.into(),
|
||||
};
|
||||
udp_dyn_server.periodic_operation();
|
||||
let queue = udp_dyn_server
|
||||
.udp_tc_server
|
||||
.tc_sender
|
||||
.get_mock_sender()
|
||||
.unwrap()
|
||||
.0
|
||||
.borrow();
|
||||
assert!(queue.is_empty());
|
||||
assert!(tm_handler_calls.lock().unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transactions() {
|
||||
let sock_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0);
|
||||
let test_receiver = TmTcSender::Mock(MockSender::default());
|
||||
let udp_tc_server =
|
||||
UdpTcServer::new(UDP_SERVER_ID, sock_addr, 2048, test_receiver).unwrap();
|
||||
let server_addr = udp_tc_server.socket.local_addr().unwrap();
|
||||
let tm_handler = TestTmHandler::default();
|
||||
let tm_handler_calls = tm_handler.addrs_to_send_to.clone();
|
||||
let mut udp_dyn_server = UdpTmtcServer {
|
||||
udp_tc_server,
|
||||
tm_handler: tm_handler.into(),
|
||||
};
|
||||
let sph = SpHeader::new_for_unseg_tc(Apid::Tmtc.raw_value(), u14::ZERO, 0);
|
||||
let ping_tc = PusTcCreator::new_simple(
|
||||
sph,
|
||||
MessageTypeId::new(17, 1),
|
||||
&[],
|
||||
CreatorConfig::default(),
|
||||
)
|
||||
.to_vec()
|
||||
.unwrap();
|
||||
let client = UdpSocket::bind("127.0.0.1:0").expect("Connecting to UDP server failed");
|
||||
let client_addr = client.local_addr().unwrap();
|
||||
println!("{}", server_addr);
|
||||
client.send_to(&ping_tc, server_addr).unwrap();
|
||||
udp_dyn_server.periodic_operation();
|
||||
{
|
||||
let mut queue = udp_dyn_server
|
||||
.udp_tc_server
|
||||
.tc_sender
|
||||
.get_mock_sender()
|
||||
.unwrap()
|
||||
.0
|
||||
.borrow_mut();
|
||||
assert!(!queue.is_empty());
|
||||
let packet_with_sender = queue.pop_front().unwrap();
|
||||
assert_eq!(packet_with_sender.packet, ping_tc);
|
||||
assert_eq!(packet_with_sender.sender_id, UDP_SERVER_ID);
|
||||
}
|
||||
|
||||
{
|
||||
let mut tm_handler_calls = tm_handler_calls.lock().unwrap();
|
||||
assert!(!tm_handler_calls.is_empty());
|
||||
assert_eq!(tm_handler_calls.len(), 1);
|
||||
let received_addr = tm_handler_calls.pop_front().unwrap();
|
||||
assert_eq!(received_addr, client_addr);
|
||||
}
|
||||
udp_dyn_server.periodic_operation();
|
||||
let queue = udp_dyn_server
|
||||
.udp_tc_server
|
||||
.tc_sender
|
||||
.get_mock_sender()
|
||||
.unwrap()
|
||||
.0
|
||||
.borrow();
|
||||
assert!(queue.is_empty());
|
||||
drop(queue);
|
||||
// Still tries to send to the same client.
|
||||
{
|
||||
let mut tm_handler_calls = tm_handler_calls.lock().unwrap();
|
||||
assert!(!tm_handler_calls.is_empty());
|
||||
assert_eq!(tm_handler_calls.len(), 1);
|
||||
let received_addr = tm_handler_calls.pop_front().unwrap();
|
||||
assert_eq!(received_addr, client_addr);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
use arbitrary_int::traits::Integer as _;
|
||||
use arbitrary_int::u11;
|
||||
use satrs::event_man_legacy::{EventMessageU32, EventRoutingError};
|
||||
use satrs::pus::event::EventTmHook;
|
||||
use satrs::pus::verification::VerificationReporter;
|
||||
use satrs::pus::EcssTmSender;
|
||||
use satrs::request::UniqueApidTargetId;
|
||||
use satrs::{
|
||||
event_man_legacy::{EventManagerWithBoundedMpsc, EventSendProvider, EventU32SenderMpscBounded},
|
||||
pus::{
|
||||
event_man::{
|
||||
DefaultPusEventU32TmCreator, EventReporter, EventRequest, EventRequestWithToken,
|
||||
},
|
||||
verification::{TcStateStarted, VerificationReportingProvider, VerificationToken},
|
||||
},
|
||||
spacepackets::time::cds::CdsTime,
|
||||
};
|
||||
use example_std::ids::generic_pus::PUS_EVENT_MANAGEMENT;
|
||||
|
||||
use crate::update_time;
|
||||
|
||||
// This helper sets the APID of the event sender for the PUS telemetry.
|
||||
#[derive(Default)]
|
||||
pub struct EventApidSetter {
|
||||
pub next_apid: u11,
|
||||
}
|
||||
|
||||
impl EventTmHook for EventApidSetter {
|
||||
fn modify_tm(&self, tm: &mut satrs::spacepackets::ecss::tm::PusTmCreator) {
|
||||
tm.set_apid(self.next_apid);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
/// 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.
|
||||
pub struct PusEventHandler<TmSender: EcssTmSender> {
|
||||
event_request_rx: mpsc::Receiver<EventRequestWithToken>,
|
||||
pus_event_tm_creator: DefaultPusEventU32TmCreator<EventApidSetter>,
|
||||
pus_event_man_rx: mpsc::Receiver<EventMessageU32>,
|
||||
tm_sender: TmSender,
|
||||
time_provider: CdsTime,
|
||||
timestamp: [u8; 7],
|
||||
small_data_buf: [u8; 64],
|
||||
verif_handler: VerificationReporter,
|
||||
}
|
||||
|
||||
impl<TmSender: EcssTmSender> PusEventHandler<TmSender> {
|
||||
pub fn new(
|
||||
tm_sender: TmSender,
|
||||
verif_handler: VerificationReporter,
|
||||
event_manager: &mut EventManagerWithBoundedMpsc,
|
||||
event_request_rx: mpsc::Receiver<EventRequestWithToken>,
|
||||
) -> Self {
|
||||
let event_queue_cap = 30;
|
||||
let (pus_event_man_tx, pus_event_man_rx) = mpsc::sync_channel(event_queue_cap);
|
||||
|
||||
// All events sent to the manager are routed to the PUS event manager, which generates PUS event
|
||||
// telemetry for each event.
|
||||
let event_reporter = EventReporter::new_with_hook(
|
||||
PUS_EVENT_MANAGEMENT.raw(),
|
||||
u11::ZERO,
|
||||
0,
|
||||
128,
|
||||
EventApidSetter::default(),
|
||||
);
|
||||
let pus_event_dispatcher =
|
||||
DefaultPusEventU32TmCreator::new_with_default_backend(event_reporter);
|
||||
let pus_event_man_send_provider = EventU32SenderMpscBounded::new(
|
||||
PUS_EVENT_MANAGEMENT.raw(),
|
||||
pus_event_man_tx,
|
||||
event_queue_cap,
|
||||
);
|
||||
|
||||
event_manager.subscribe_all(pus_event_man_send_provider.target_id());
|
||||
event_manager.add_sender(pus_event_man_send_provider);
|
||||
|
||||
Self {
|
||||
event_request_rx,
|
||||
pus_event_tm_creator: pus_event_dispatcher,
|
||||
pus_event_man_rx,
|
||||
time_provider: CdsTime::new_with_u16_days(0, 0),
|
||||
timestamp: [0; 7],
|
||||
small_data_buf: [0; 64],
|
||||
verif_handler,
|
||||
tm_sender,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_event_requests(&mut self) {
|
||||
let report_completion = |event_req: EventRequestWithToken, timestamp: &[u8]| {
|
||||
let started_token: VerificationToken<TcStateStarted> = event_req
|
||||
.token
|
||||
.try_into()
|
||||
.expect("expected start verification token");
|
||||
self.verif_handler
|
||||
.completion_success(&self.tm_sender, started_token, timestamp)
|
||||
.expect("Sending completion success failed");
|
||||
};
|
||||
loop {
|
||||
// handle event requests
|
||||
match self.event_request_rx.try_recv() {
|
||||
Ok(event_req) => match event_req.request {
|
||||
EventRequest::Enable(event) => {
|
||||
self.pus_event_tm_creator
|
||||
.enable_tm_for_event(&event)
|
||||
.expect("Enabling TM failed");
|
||||
update_time(&mut self.time_provider, &mut self.timestamp);
|
||||
report_completion(event_req, &self.timestamp);
|
||||
}
|
||||
EventRequest::Disable(event) => {
|
||||
self.pus_event_tm_creator
|
||||
.disable_tm_for_event(&event)
|
||||
.expect("Disabling TM failed");
|
||||
update_time(&mut self.time_provider, &mut self.timestamp);
|
||||
report_completion(event_req, &self.timestamp);
|
||||
}
|
||||
},
|
||||
Err(e) => match e {
|
||||
mpsc::TryRecvError::Empty => break,
|
||||
mpsc::TryRecvError::Disconnected => {
|
||||
log::warn!("all event request senders have disconnected");
|
||||
break;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_pus_event_tm(&mut self) {
|
||||
loop {
|
||||
// Perform the generation of PUS event packets
|
||||
match self.pus_event_man_rx.try_recv() {
|
||||
Ok(event_msg) => {
|
||||
// We use the TM modification hook to set the sender APID for each event.
|
||||
self.pus_event_tm_creator.reporter.tm_hook.next_apid =
|
||||
UniqueApidTargetId::from(event_msg.sender_id()).apid;
|
||||
update_time(&mut self.time_provider, &mut self.timestamp);
|
||||
let generation_result = self
|
||||
.pus_event_tm_creator
|
||||
.generate_pus_event_tm_generic_with_generic_params(
|
||||
&self.tm_sender,
|
||||
&self.timestamp,
|
||||
event_msg.event(),
|
||||
&mut self.small_data_buf,
|
||||
event_msg.params(),
|
||||
)
|
||||
.expect("Sending TM as event failed");
|
||||
if !generation_result.params_were_propagated {
|
||||
log::warn!(
|
||||
"Event TM parameters were not propagated: {:?}",
|
||||
event_msg.params()
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => match e {
|
||||
mpsc::TryRecvError::Empty => break,
|
||||
mpsc::TryRecvError::Disconnected => {
|
||||
log::warn!("All event senders have disconnected");
|
||||
break;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EventHandler<TmSender: EcssTmSender> {
|
||||
pub pus_event_handler: PusEventHandler<TmSender>,
|
||||
event_manager: EventManagerWithBoundedMpsc,
|
||||
}
|
||||
|
||||
impl<TmSender: EcssTmSender> EventHandler<TmSender> {
|
||||
pub fn new(
|
||||
tm_sender: TmSender,
|
||||
event_rx: mpsc::Receiver<EventMessageU32>,
|
||||
event_request_rx: mpsc::Receiver<EventRequestWithToken>,
|
||||
) -> Self {
|
||||
let mut event_manager = EventManagerWithBoundedMpsc::new(event_rx);
|
||||
let pus_event_handler = PusEventHandler::new(
|
||||
tm_sender,
|
||||
create_verification_reporter(PUS_EVENT_MANAGEMENT.id(), PUS_EVENT_MANAGEMENT.apid),
|
||||
&mut event_manager,
|
||||
event_request_rx,
|
||||
);
|
||||
|
||||
Self {
|
||||
pus_event_handler,
|
||||
event_manager,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn event_manager(&mut self) -> &mut EventManagerWithBoundedMpsc {
|
||||
&mut self.event_manager
|
||||
}
|
||||
|
||||
pub fn periodic_operation(&mut self) {
|
||||
self.pus_event_handler.handle_event_requests();
|
||||
self.try_event_routing();
|
||||
self.pus_event_handler.generate_pus_event_tm();
|
||||
}
|
||||
|
||||
pub fn try_event_routing(&mut self) {
|
||||
let error_handler = |event_msg: &EventMessageU32, error: EventRoutingError| {
|
||||
self.routing_error_handler(event_msg, error)
|
||||
};
|
||||
// Perform the event routing.
|
||||
self.event_manager.try_event_handling(error_handler);
|
||||
}
|
||||
|
||||
pub fn routing_error_handler(&self, event_msg: &EventMessageU32, error: EventRoutingError) {
|
||||
log::warn!("event routing error for event {event_msg:?}: {error:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use arbitrary_int::u21;
|
||||
use satrs::{
|
||||
events_legacy::EventU32,
|
||||
pus::verification::VerificationReporterConfig,
|
||||
spacepackets::ecss::{tm::PusTmReader, PusPacket},
|
||||
tmtc::PacketAsVec,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
const TEST_CREATOR_ID: UniqueApidTargetId = UniqueApidTargetId::new(u11::new(1), u21::new(2));
|
||||
const TEST_EVENT: EventU32 = EventU32::new(satrs::events_legacy::Severity::Info, 1, 1);
|
||||
|
||||
pub struct EventManagementTestbench {
|
||||
pub event_tx: mpsc::SyncSender<EventMessageU32>,
|
||||
pub event_manager: EventManagerWithBoundedMpsc,
|
||||
pub tm_receiver: mpsc::Receiver<PacketAsVec>,
|
||||
pub pus_event_handler: PusEventHandler<mpsc::Sender<PacketAsVec>>,
|
||||
}
|
||||
|
||||
impl EventManagementTestbench {
|
||||
pub fn new() -> Self {
|
||||
let (event_tx, event_rx) = mpsc::sync_channel(10);
|
||||
let (_event_req_tx, event_req_rx) = mpsc::sync_channel(10);
|
||||
let (tm_sender, tm_receiver) = mpsc::channel();
|
||||
let verif_reporter_cfg = VerificationReporterConfig::new(u11::new(0x05), 2, 2, 128);
|
||||
let verif_reporter =
|
||||
VerificationReporter::new(PUS_EVENT_MANAGEMENT.id(), &verif_reporter_cfg);
|
||||
let mut event_manager = EventManagerWithBoundedMpsc::new(event_rx);
|
||||
let pus_event_handler = PusEventHandler::<mpsc::Sender<PacketAsVec>>::new(
|
||||
tm_sender,
|
||||
verif_reporter,
|
||||
&mut event_manager,
|
||||
event_req_rx,
|
||||
);
|
||||
Self {
|
||||
event_tx,
|
||||
tm_receiver,
|
||||
event_manager,
|
||||
pus_event_handler,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_event_generation() {
|
||||
let mut testbench = EventManagementTestbench::new();
|
||||
testbench
|
||||
.event_tx
|
||||
.send(EventMessageU32::new(
|
||||
TEST_CREATOR_ID.id(),
|
||||
EventU32::new(satrs::events_legacy::Severity::Info, 1, 1),
|
||||
))
|
||||
.expect("failed to send event");
|
||||
testbench.pus_event_handler.handle_event_requests();
|
||||
testbench.event_manager.try_event_handling(|_, _| {});
|
||||
testbench.pus_event_handler.generate_pus_event_tm();
|
||||
let tm_packet = testbench
|
||||
.tm_receiver
|
||||
.try_recv()
|
||||
.expect("failed to receive TM packet");
|
||||
assert_eq!(tm_packet.sender_id, PUS_EVENT_MANAGEMENT.id());
|
||||
let tm_reader = PusTmReader::new(&tm_packet.packet, 7).expect("failed to create TM reader");
|
||||
assert_eq!(tm_reader.apid(), TEST_CREATOR_ID.apid);
|
||||
assert_eq!(tm_reader.user_data().len(), 4);
|
||||
let event_read_back = EventU32::from_be_bytes(tm_reader.user_data().try_into().unwrap());
|
||||
assert_eq!(event_read_back, TEST_EVENT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_event_disabled() {
|
||||
// TODO: Add test.
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -0,0 +1,74 @@
|
||||
use arbitrary_int::traits::Integer as _;
|
||||
use derive_new::new;
|
||||
use satrs::hk::UniqueId;
|
||||
use satrs::request::UniqueApidTargetId;
|
||||
use satrs::spacepackets::ecss::tm::{PusTmCreator, PusTmSecondaryHeader};
|
||||
use satrs::spacepackets::ecss::{hk, CreatorConfig, MessageTypeId};
|
||||
use satrs::spacepackets::{ByteConversionError, SpHeader};
|
||||
|
||||
#[derive(Debug, new, Copy, Clone)]
|
||||
pub struct HkUniqueId {
|
||||
target_id: UniqueApidTargetId,
|
||||
set_id: UniqueId,
|
||||
}
|
||||
|
||||
impl HkUniqueId {
|
||||
#[allow(dead_code)]
|
||||
pub fn target_id(&self) -> UniqueApidTargetId {
|
||||
self.target_id
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
pub fn set_id(&self) -> UniqueId {
|
||||
self.set_id
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn write_to_be_bytes(&self, buf: &mut [u8]) -> Result<usize, ByteConversionError> {
|
||||
if buf.len() < 8 {
|
||||
return Err(ByteConversionError::ToSliceTooSmall {
|
||||
found: buf.len(),
|
||||
expected: 8,
|
||||
});
|
||||
}
|
||||
buf[0..4].copy_from_slice(&self.target_id.unique_id.as_u32().to_be_bytes());
|
||||
buf[4..8].copy_from_slice(&self.set_id.to_be_bytes());
|
||||
|
||||
Ok(8)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(new)]
|
||||
pub struct PusHkHelper {
|
||||
component_id: UniqueApidTargetId,
|
||||
}
|
||||
|
||||
impl PusHkHelper {
|
||||
pub fn generate_hk_report_packet<
|
||||
'a,
|
||||
'b,
|
||||
HkWriter: FnMut(&mut [u8]) -> Result<usize, ByteConversionError>,
|
||||
>(
|
||||
&self,
|
||||
timestamp: &'a [u8],
|
||||
set_id: u32,
|
||||
hk_data_writer: &mut HkWriter,
|
||||
buf: &'b mut [u8],
|
||||
) -> Result<PusTmCreator<'a, 'b>, ByteConversionError> {
|
||||
let sec_header = PusTmSecondaryHeader::new(
|
||||
MessageTypeId::new(3, hk::MessageSubtypeId::TmHkPacket as u8),
|
||||
0,
|
||||
0,
|
||||
timestamp,
|
||||
);
|
||||
buf[0..4].copy_from_slice(&self.component_id.unique_id.as_u32().to_be_bytes());
|
||||
buf[4..8].copy_from_slice(&set_id.to_be_bytes());
|
||||
let (_, second_half) = buf.split_at_mut(8);
|
||||
let hk_data_len = hk_data_writer(second_half)?;
|
||||
Ok(PusTmCreator::new(
|
||||
SpHeader::new_from_apid(self.component_id.apid),
|
||||
sec_header,
|
||||
&buf[0..8 + hk_data_len],
|
||||
CreatorConfig::default(),
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,725 @@
|
||||
use log::warn;
|
||||
use satrs::action::{ActionRequest, ActionRequestVariant};
|
||||
use satrs::pus::action::{
|
||||
ActionReplyPus, ActionReplyVariant, ActivePusActionRequestStd, DefaultActiveActionRequestMap,
|
||||
};
|
||||
use satrs::pus::verification::{
|
||||
handle_completion_failure_with_generic_params, handle_step_failure_with_generic_params,
|
||||
FailParamHelper, FailParams, TcStateAccepted, TcStateStarted, VerificationReporter,
|
||||
VerificationReportingProvider, VerificationToken,
|
||||
};
|
||||
use satrs::pus::{
|
||||
ActiveRequest, EcssTcAndToken, EcssTcCacher, EcssTmSender, EcssTmtcError,
|
||||
GenericConversionError, MpscTcReceiver, PusPacketHandlingError, PusReplyHandler,
|
||||
PusServiceHelper, PusTcToRequestConverter,
|
||||
};
|
||||
use satrs::request::{GenericMessage, UniqueApidTargetId};
|
||||
use satrs::spacepackets::ecss::tc::PusTcReader;
|
||||
use satrs::spacepackets::ecss::{EcssEnumU16, PusPacket, PusServiceId};
|
||||
use example_std::config::tmtc_err;
|
||||
use example_std::ids;
|
||||
use example_std::ids::generic_pus::PUS_ACTION;
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::requests::GenericRequestRouter;
|
||||
use crate::tmtc::sender::TmTcSender;
|
||||
|
||||
use super::{
|
||||
create_verification_reporter, generic_pus_request_timeout_handler, HandlingStatus,
|
||||
PusTargetedRequestService, TargetedPusService,
|
||||
};
|
||||
|
||||
pub struct ActionReplyHandler {
|
||||
fail_data_buf: [u8; 128],
|
||||
}
|
||||
|
||||
impl Default for ActionReplyHandler {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
fail_data_buf: [0; 128],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PusReplyHandler<ActivePusActionRequestStd, ActionReplyPus> for ActionReplyHandler {
|
||||
type Error = EcssTmtcError;
|
||||
|
||||
fn handle_unrequested_reply(
|
||||
&mut self,
|
||||
reply: &GenericMessage<ActionReplyPus>,
|
||||
_tm_sender: &impl EcssTmSender,
|
||||
) -> Result<(), Self::Error> {
|
||||
warn!("received unexpected reply for service 8: {reply:?}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_reply(
|
||||
&mut self,
|
||||
reply: &GenericMessage<ActionReplyPus>,
|
||||
active_request: &ActivePusActionRequestStd,
|
||||
tm_sender: &(impl EcssTmSender + ?Sized),
|
||||
verification_handler: &impl VerificationReportingProvider,
|
||||
timestamp: &[u8],
|
||||
) -> Result<bool, Self::Error> {
|
||||
let verif_token: VerificationToken<TcStateStarted> = active_request
|
||||
.token()
|
||||
.try_into()
|
||||
.expect("invalid token state");
|
||||
let remove_entry = match &reply.message.variant {
|
||||
ActionReplyVariant::CompletionFailed { error_code, params } => {
|
||||
let error_propagated = handle_completion_failure_with_generic_params(
|
||||
tm_sender,
|
||||
verif_token,
|
||||
verification_handler,
|
||||
FailParamHelper {
|
||||
error_code,
|
||||
params: params.as_ref(),
|
||||
timestamp,
|
||||
small_data_buf: &mut self.fail_data_buf,
|
||||
},
|
||||
)?;
|
||||
if !error_propagated {
|
||||
log::warn!(
|
||||
"error params for completion failure were not propated: {:?}",
|
||||
params.as_ref()
|
||||
);
|
||||
}
|
||||
true
|
||||
}
|
||||
ActionReplyVariant::StepFailed {
|
||||
error_code,
|
||||
step,
|
||||
params,
|
||||
} => {
|
||||
let error_propagated = handle_step_failure_with_generic_params(
|
||||
tm_sender,
|
||||
verif_token,
|
||||
verification_handler,
|
||||
FailParamHelper {
|
||||
error_code,
|
||||
params: params.as_ref(),
|
||||
timestamp,
|
||||
small_data_buf: &mut self.fail_data_buf,
|
||||
},
|
||||
&EcssEnumU16::new(*step),
|
||||
)?;
|
||||
if !error_propagated {
|
||||
log::warn!(
|
||||
"error params for completion failure were not propated: {:?}",
|
||||
params.as_ref()
|
||||
);
|
||||
}
|
||||
true
|
||||
}
|
||||
ActionReplyVariant::Completed => {
|
||||
verification_handler.completion_success(tm_sender, verif_token, timestamp)?;
|
||||
true
|
||||
}
|
||||
ActionReplyVariant::StepSuccess { step } => {
|
||||
verification_handler.step_success(
|
||||
tm_sender,
|
||||
&verif_token,
|
||||
timestamp,
|
||||
EcssEnumU16::new(*step),
|
||||
)?;
|
||||
false
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
Ok(remove_entry)
|
||||
}
|
||||
|
||||
fn handle_request_timeout(
|
||||
&mut self,
|
||||
active_request: &ActivePusActionRequestStd,
|
||||
tm_sender: &impl EcssTmSender,
|
||||
verification_handler: &impl VerificationReportingProvider,
|
||||
time_stamp: &[u8],
|
||||
) -> Result<(), Self::Error> {
|
||||
generic_pus_request_timeout_handler(
|
||||
tm_sender,
|
||||
active_request,
|
||||
verification_handler,
|
||||
time_stamp,
|
||||
"action",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ActionRequestConverter {}
|
||||
|
||||
impl PusTcToRequestConverter<ActivePusActionRequestStd, ActionRequest> for ActionRequestConverter {
|
||||
type Error = GenericConversionError;
|
||||
|
||||
fn convert(
|
||||
&mut self,
|
||||
token: VerificationToken<TcStateAccepted>,
|
||||
tc: &PusTcReader,
|
||||
tm_sender: &(impl EcssTmSender + ?Sized),
|
||||
verif_reporter: &impl VerificationReportingProvider,
|
||||
time_stamp: &[u8],
|
||||
) -> Result<(ActivePusActionRequestStd, ActionRequest), Self::Error> {
|
||||
let subservice = tc.message_subtype_id();
|
||||
let user_data = tc.user_data();
|
||||
if user_data.len() < 8 {
|
||||
verif_reporter
|
||||
.start_failure(
|
||||
tm_sender,
|
||||
token,
|
||||
FailParams::new_no_fail_data(time_stamp, &tmtc_err::NOT_ENOUGH_APP_DATA),
|
||||
)
|
||||
.expect("Sending start failure failed");
|
||||
return Err(GenericConversionError::NotEnoughAppData {
|
||||
expected: 8,
|
||||
found: user_data.len(),
|
||||
});
|
||||
}
|
||||
let target_id_and_apid = UniqueApidTargetId::from_pus_tc(tc).unwrap();
|
||||
let action_id = u32::from_be_bytes(user_data[4..8].try_into().unwrap());
|
||||
if subservice == 128 {
|
||||
let req_variant = if user_data.len() == 8 {
|
||||
ActionRequestVariant::NoData
|
||||
} else {
|
||||
ActionRequestVariant::VecData(user_data[8..].to_vec())
|
||||
};
|
||||
Ok((
|
||||
ActivePusActionRequestStd::new(
|
||||
action_id,
|
||||
target_id_and_apid.into(),
|
||||
token.into(),
|
||||
Duration::from_secs(30),
|
||||
),
|
||||
ActionRequest::new(action_id, req_variant),
|
||||
))
|
||||
} else {
|
||||
verif_reporter
|
||||
.start_failure(
|
||||
tm_sender,
|
||||
token,
|
||||
FailParams::new_no_fail_data(time_stamp, &tmtc_err::INVALID_PUS_SUBSERVICE),
|
||||
)
|
||||
.expect("Sending start failure failed");
|
||||
Err(GenericConversionError::InvalidSubservice(subservice))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_action_service(
|
||||
tm_sender: TmTcSender,
|
||||
tc_in_mem_converter: EcssTcCacher,
|
||||
pus_action_rx: mpsc::Receiver<EcssTcAndToken>,
|
||||
action_router: GenericRequestRouter,
|
||||
reply_receiver: mpsc::Receiver<GenericMessage<ActionReplyPus>>,
|
||||
) -> ActionServiceWrapper {
|
||||
let action_request_handler = PusTargetedRequestService::new(
|
||||
PusServiceHelper::new(
|
||||
ids::generic_pus::PUS_ACTION.id(),
|
||||
pus_action_rx,
|
||||
tm_sender,
|
||||
create_verification_reporter(PUS_ACTION.id(), PUS_ACTION.apid),
|
||||
tc_in_mem_converter,
|
||||
),
|
||||
ActionRequestConverter::default(),
|
||||
// TODO: Implementation which does not use run-time allocation? Maybe something like
|
||||
// a bounded wrapper which pre-allocates using [HashMap::with_capacity]..
|
||||
DefaultActiveActionRequestMap::default(),
|
||||
ActionReplyHandler::default(),
|
||||
action_router,
|
||||
reply_receiver,
|
||||
);
|
||||
ActionServiceWrapper {
|
||||
service: action_request_handler,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ActionServiceWrapper {
|
||||
pub(crate) service: PusTargetedRequestService<
|
||||
MpscTcReceiver,
|
||||
VerificationReporter,
|
||||
ActionRequestConverter,
|
||||
ActionReplyHandler,
|
||||
DefaultActiveActionRequestMap,
|
||||
ActivePusActionRequestStd,
|
||||
ActionRequest,
|
||||
ActionReplyPus,
|
||||
>,
|
||||
}
|
||||
|
||||
impl TargetedPusService for ActionServiceWrapper {
|
||||
const SERVICE_ID: u8 = PusServiceId::Action as u8;
|
||||
const SERVICE_STR: &'static str = "action";
|
||||
|
||||
delegate::delegate! {
|
||||
to self.service {
|
||||
fn poll_and_handle_next_tc(
|
||||
&mut self,
|
||||
time_stamp: &[u8],
|
||||
) -> Result<HandlingStatus, PusPacketHandlingError>;
|
||||
|
||||
fn poll_and_handle_next_reply(
|
||||
&mut self,
|
||||
time_stamp: &[u8],
|
||||
) -> Result<HandlingStatus, EcssTmtcError>;
|
||||
|
||||
fn check_for_request_timeouts(&mut self);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use arbitrary_int::traits::Integer as _;
|
||||
use satrs::pus::test_util::{
|
||||
TEST_APID, TEST_COMPONENT_ID_0, TEST_COMPONENT_ID_1, TEST_UNIQUE_ID_0, TEST_UNIQUE_ID_1,
|
||||
};
|
||||
use satrs::pus::verification::test_util::TestVerificationReporter;
|
||||
use satrs::pus::{verification, EcssTcVecCacher};
|
||||
use satrs::request::MessageMetadata;
|
||||
use satrs::spacepackets::ecss::{CreatorConfig, MessageTypeId};
|
||||
use satrs::tmtc::PacketAsVec;
|
||||
use satrs::ComponentId;
|
||||
use satrs::{
|
||||
res_code::ResultU16,
|
||||
spacepackets::{
|
||||
ecss::{
|
||||
tc::{PusTcCreator, PusTcSecondaryHeader},
|
||||
tm::PusTmReader,
|
||||
WritablePusPacket,
|
||||
},
|
||||
SpHeader,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
pus::tests::{PusConverterTestbench, ReplyHandlerTestbench, TargetedPusRequestTestbench},
|
||||
requests::CompositeRequest,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
impl
|
||||
TargetedPusRequestTestbench<
|
||||
ActionRequestConverter,
|
||||
ActionReplyHandler,
|
||||
DefaultActiveActionRequestMap,
|
||||
ActivePusActionRequestStd,
|
||||
ActionRequest,
|
||||
ActionReplyPus,
|
||||
>
|
||||
{
|
||||
pub fn new_for_action(owner_id: ComponentId, target_id: ComponentId) -> Self {
|
||||
let _ = env_logger::builder().is_test(true).try_init();
|
||||
let (tm_funnel_tx, tm_funnel_rx) = mpsc::sync_channel(5);
|
||||
let (pus_action_tx, pus_action_rx) = mpsc::channel();
|
||||
let (action_reply_tx, action_reply_rx) = mpsc::channel();
|
||||
let (action_req_tx, action_req_rx) = mpsc::sync_channel(10);
|
||||
let verif_reporter = TestVerificationReporter::new(owner_id);
|
||||
let mut generic_req_router = GenericRequestRouter::default();
|
||||
generic_req_router
|
||||
.composite_router_map
|
||||
.insert(target_id, action_req_tx);
|
||||
Self {
|
||||
service: PusTargetedRequestService::new(
|
||||
PusServiceHelper::new(
|
||||
owner_id,
|
||||
pus_action_rx,
|
||||
TmTcSender::Heap(tm_funnel_tx.clone()),
|
||||
verif_reporter,
|
||||
EcssTcCacher::Heap(EcssTcVecCacher::default()),
|
||||
),
|
||||
ActionRequestConverter::default(),
|
||||
DefaultActiveActionRequestMap::default(),
|
||||
ActionReplyHandler::default(),
|
||||
generic_req_router,
|
||||
action_reply_rx,
|
||||
),
|
||||
request_id: None,
|
||||
pus_packet_tx: pus_action_tx,
|
||||
tm_funnel_rx,
|
||||
reply_tx: action_reply_tx,
|
||||
request_rx: action_req_rx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_packet_started(&self) {
|
||||
self.service
|
||||
.service_helper
|
||||
.common
|
||||
.verif_reporter
|
||||
.check_next_is_started_success(
|
||||
self.service.service_helper.id(),
|
||||
self.request_id.expect("request ID not set").into(),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn verify_packet_completed(&self) {
|
||||
self.service
|
||||
.service_helper
|
||||
.common
|
||||
.verif_reporter
|
||||
.check_next_is_completion_success(
|
||||
self.service.service_helper.id(),
|
||||
self.request_id.expect("request ID not set").into(),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn verify_tm_empty(&self) {
|
||||
let packet = self.tm_funnel_rx.try_recv();
|
||||
if let Err(mpsc::TryRecvError::Empty) = packet {
|
||||
} else {
|
||||
let tm = packet.unwrap();
|
||||
let unexpected_tm = PusTmReader::new(&tm.packet, 7).unwrap();
|
||||
panic!("unexpected TM packet {unexpected_tm:?}");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_next_tc_is_handled_properly(&mut self, time_stamp: &[u8]) {
|
||||
let result = self.service.poll_and_handle_next_tc(time_stamp);
|
||||
if let Err(e) = result {
|
||||
panic!("unexpected error {:?}", e);
|
||||
}
|
||||
let result = result.unwrap();
|
||||
match result {
|
||||
HandlingStatus::HandledOne => (),
|
||||
_ => panic!("unexpected result {result:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_all_tcs_handled(&mut self, time_stamp: &[u8]) {
|
||||
let result = self.service.poll_and_handle_next_tc(time_stamp);
|
||||
if let Err(e) = result {
|
||||
panic!("unexpected error {:?}", e);
|
||||
}
|
||||
let result = result.unwrap();
|
||||
match result {
|
||||
HandlingStatus::Empty => (),
|
||||
_ => panic!("unexpected result {result:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_next_reply_is_handled_properly(&mut self, time_stamp: &[u8]) {
|
||||
let result = self.service.poll_and_handle_next_reply(time_stamp);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), HandlingStatus::HandledOne);
|
||||
}
|
||||
|
||||
pub fn verify_all_replies_handled(&mut self, time_stamp: &[u8]) {
|
||||
let result = self.service.poll_and_handle_next_reply(time_stamp);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), HandlingStatus::Empty);
|
||||
}
|
||||
|
||||
pub fn add_tc(&mut self, tc: &PusTcCreator) {
|
||||
self.request_id = Some(verification::RequestId::new(tc).into());
|
||||
let token = self
|
||||
.service
|
||||
.service_helper
|
||||
.verif_reporter_mut()
|
||||
.start_verification(tc);
|
||||
let accepted_token = self
|
||||
.service
|
||||
.service_helper
|
||||
.verif_reporter()
|
||||
.acceptance_success(self.service.service_helper.tm_sender(), token, &[0; 7])
|
||||
.expect("TC acceptance failed");
|
||||
self.service
|
||||
.service_helper
|
||||
.verif_reporter()
|
||||
.check_next_was_added(accepted_token.request_id());
|
||||
let id = self.service.service_helper.id();
|
||||
self.service
|
||||
.service_helper
|
||||
.verif_reporter()
|
||||
.check_next_is_acceptance_success(id, accepted_token.request_id());
|
||||
self.pus_packet_tx
|
||||
.send(EcssTcAndToken::new(
|
||||
PacketAsVec::new(self.service.service_helper.id(), tc.to_vec().unwrap()),
|
||||
accepted_token,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_request() {
|
||||
let mut testbench = TargetedPusRequestTestbench::new_for_action(
|
||||
TEST_COMPONENT_ID_0.id(),
|
||||
TEST_COMPONENT_ID_1.id(),
|
||||
);
|
||||
// Create a basic action request and verify forwarding.
|
||||
let sp_header = SpHeader::new_from_apid(TEST_APID);
|
||||
let sec_header = PusTcSecondaryHeader::new_simple(MessageTypeId::new(8, 128));
|
||||
let action_id = 5_u32;
|
||||
let mut app_data: [u8; 8] = [0; 8];
|
||||
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_1.as_u32().to_be_bytes());
|
||||
app_data[4..8].copy_from_slice(&action_id.to_be_bytes());
|
||||
let pus8_packet =
|
||||
PusTcCreator::new(sp_header, sec_header, &app_data, CreatorConfig::default());
|
||||
testbench.add_tc(&pus8_packet);
|
||||
let time_stamp: [u8; 7] = [0; 7];
|
||||
testbench.verify_next_tc_is_handled_properly(&time_stamp);
|
||||
testbench.verify_all_tcs_handled(&time_stamp);
|
||||
|
||||
testbench.verify_packet_started();
|
||||
|
||||
let possible_req = testbench.request_rx.try_recv();
|
||||
assert!(possible_req.is_ok());
|
||||
let req = possible_req.unwrap();
|
||||
if let CompositeRequest::Action(action_req) = req.message {
|
||||
assert_eq!(action_req.action_id, action_id);
|
||||
assert_eq!(action_req.variant, ActionRequestVariant::NoData);
|
||||
let action_reply = ActionReplyPus::new(action_id, ActionReplyVariant::Completed);
|
||||
testbench
|
||||
.reply_tx
|
||||
.send(GenericMessage::new(req.requestor_info, action_reply))
|
||||
.unwrap();
|
||||
} else {
|
||||
panic!("unexpected request type");
|
||||
}
|
||||
testbench.verify_next_reply_is_handled_properly(&time_stamp);
|
||||
testbench.verify_all_replies_handled(&time_stamp);
|
||||
|
||||
testbench.verify_packet_completed();
|
||||
testbench.verify_tm_empty();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_request_routing_error() {
|
||||
let mut testbench = TargetedPusRequestTestbench::new_for_action(
|
||||
TEST_COMPONENT_ID_0.id(),
|
||||
TEST_COMPONENT_ID_1.id(),
|
||||
);
|
||||
// Create a basic action request and verify forwarding.
|
||||
let sec_header = PusTcSecondaryHeader::new_simple(MessageTypeId::new(8, 128));
|
||||
let action_id = 5_u32;
|
||||
let mut app_data: [u8; 8] = [0; 8];
|
||||
// Invalid ID, routing should fail.
|
||||
app_data[0..4].copy_from_slice(&0_u32.to_be_bytes());
|
||||
app_data[4..8].copy_from_slice(&action_id.to_be_bytes());
|
||||
let pus8_packet = PusTcCreator::new(
|
||||
SpHeader::new_from_apid(TEST_APID),
|
||||
sec_header,
|
||||
&app_data,
|
||||
CreatorConfig::default(),
|
||||
);
|
||||
testbench.add_tc(&pus8_packet);
|
||||
let time_stamp: [u8; 7] = [0; 7];
|
||||
|
||||
let result = testbench.service.poll_and_handle_next_tc(&time_stamp);
|
||||
assert!(result.is_err());
|
||||
// Verify the correct result and completion failure.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converter_action_req_no_data() {
|
||||
let mut testbench = PusConverterTestbench::new(
|
||||
TEST_COMPONENT_ID_0.raw(),
|
||||
ActionRequestConverter::default(),
|
||||
);
|
||||
let sec_header = PusTcSecondaryHeader::new_simple(MessageTypeId::new(8, 128));
|
||||
let action_id = 5_u32;
|
||||
let mut app_data: [u8; 8] = [0; 8];
|
||||
// Invalid ID, routing should fail.
|
||||
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_0.as_u32().to_be_bytes());
|
||||
app_data[4..8].copy_from_slice(&action_id.to_be_bytes());
|
||||
let pus8_packet = PusTcCreator::new(
|
||||
SpHeader::new_from_apid(TEST_APID),
|
||||
sec_header,
|
||||
&app_data,
|
||||
CreatorConfig::default(),
|
||||
);
|
||||
let token = testbench.add_tc(&pus8_packet);
|
||||
let result = testbench.convert(token, &[], TEST_APID, TEST_UNIQUE_ID_0);
|
||||
assert!(result.is_ok());
|
||||
let (active_req, request) = result.unwrap();
|
||||
if let ActionRequestVariant::NoData = request.variant {
|
||||
assert_eq!(request.action_id, action_id);
|
||||
assert_eq!(active_req.action_id, action_id);
|
||||
assert_eq!(
|
||||
active_req.target_id(),
|
||||
UniqueApidTargetId::new(TEST_APID, TEST_UNIQUE_ID_0).raw()
|
||||
);
|
||||
assert_eq!(
|
||||
active_req.token().request_id(),
|
||||
testbench.request_id().unwrap()
|
||||
);
|
||||
} else {
|
||||
panic!("unexpected action request variant");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converter_action_req_with_data() {
|
||||
let mut testbench =
|
||||
PusConverterTestbench::new(TEST_COMPONENT_ID_0.id(), ActionRequestConverter::default());
|
||||
let sec_header = PusTcSecondaryHeader::new_simple(MessageTypeId::new(8, 128));
|
||||
let action_id = 5_u32;
|
||||
let mut app_data: [u8; 16] = [0; 16];
|
||||
// Invalid ID, routing should fail.
|
||||
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_0.as_u32().to_be_bytes());
|
||||
app_data[4..8].copy_from_slice(&action_id.to_be_bytes());
|
||||
for i in 0..8 {
|
||||
app_data[i + 8] = i as u8;
|
||||
}
|
||||
let pus8_packet = PusTcCreator::new(
|
||||
SpHeader::new_from_apid(TEST_APID),
|
||||
sec_header,
|
||||
&app_data,
|
||||
CreatorConfig::default(),
|
||||
);
|
||||
let token = testbench.add_tc(&pus8_packet);
|
||||
let result = testbench.convert(token, &[], TEST_APID, TEST_UNIQUE_ID_0);
|
||||
assert!(result.is_ok());
|
||||
let (active_req, request) = result.unwrap();
|
||||
if let ActionRequestVariant::VecData(vec) = request.variant {
|
||||
assert_eq!(request.action_id, action_id);
|
||||
assert_eq!(active_req.action_id, action_id);
|
||||
assert_eq!(vec, app_data[8..].to_vec());
|
||||
} else {
|
||||
panic!("unexpected action request variant");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reply_handling_completion_success() {
|
||||
let mut testbench =
|
||||
ReplyHandlerTestbench::new(TEST_COMPONENT_ID_0.id(), ActionReplyHandler::default());
|
||||
let action_id = 5_u32;
|
||||
let (req_id, active_req) = testbench.add_tc(TEST_APID, TEST_UNIQUE_ID_0, &[]);
|
||||
let active_action_req =
|
||||
ActivePusActionRequestStd::new_from_common_req(action_id, active_req);
|
||||
let reply = ActionReplyPus::new(action_id, ActionReplyVariant::Completed);
|
||||
let generic_reply = GenericMessage::new(MessageMetadata::new(req_id.into(), 0), reply);
|
||||
let result = testbench.handle_reply(&generic_reply, &active_action_req, &[]);
|
||||
assert!(result.is_ok());
|
||||
assert!(result.unwrap());
|
||||
testbench.verif_reporter.assert_full_completion_success(
|
||||
TEST_COMPONENT_ID_0.id(),
|
||||
req_id,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reply_handling_completion_failure() {
|
||||
let mut testbench =
|
||||
ReplyHandlerTestbench::new(TEST_COMPONENT_ID_0.id(), ActionReplyHandler::default());
|
||||
let action_id = 5_u32;
|
||||
let (req_id, active_req) = testbench.add_tc(TEST_APID, TEST_UNIQUE_ID_0, &[]);
|
||||
let active_action_req =
|
||||
ActivePusActionRequestStd::new_from_common_req(action_id, active_req);
|
||||
let error_code = ResultU16::new(2, 3);
|
||||
let reply = ActionReplyPus::new(
|
||||
action_id,
|
||||
ActionReplyVariant::CompletionFailed {
|
||||
error_code,
|
||||
params: None,
|
||||
},
|
||||
);
|
||||
let generic_reply = GenericMessage::new(MessageMetadata::new(req_id.into(), 0), reply);
|
||||
let result = testbench.handle_reply(&generic_reply, &active_action_req, &[]);
|
||||
assert!(result.is_ok());
|
||||
assert!(result.unwrap());
|
||||
testbench.verif_reporter.assert_completion_failure(
|
||||
TEST_COMPONENT_ID_0.into(),
|
||||
req_id,
|
||||
None,
|
||||
error_code.raw() as u64,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reply_handling_step_success() {
|
||||
let mut testbench =
|
||||
ReplyHandlerTestbench::new(TEST_COMPONENT_ID_0.id(), ActionReplyHandler::default());
|
||||
let action_id = 5_u32;
|
||||
let (req_id, active_req) = testbench.add_tc(TEST_APID, TEST_UNIQUE_ID_0, &[]);
|
||||
let active_action_req =
|
||||
ActivePusActionRequestStd::new_from_common_req(action_id, active_req);
|
||||
let reply = ActionReplyPus::new(action_id, ActionReplyVariant::StepSuccess { step: 1 });
|
||||
let generic_reply = GenericMessage::new(MessageMetadata::new(req_id.into(), 0), reply);
|
||||
let result = testbench.handle_reply(&generic_reply, &active_action_req, &[]);
|
||||
assert!(result.is_ok());
|
||||
// Entry should not be removed, completion not done yet.
|
||||
assert!(!result.unwrap());
|
||||
testbench.verif_reporter.check_next_was_added(req_id);
|
||||
testbench
|
||||
.verif_reporter
|
||||
.check_next_is_acceptance_success(TEST_COMPONENT_ID_0.raw(), req_id);
|
||||
testbench
|
||||
.verif_reporter
|
||||
.check_next_is_started_success(TEST_COMPONENT_ID_0.raw(), req_id);
|
||||
testbench
|
||||
.verif_reporter
|
||||
.check_next_is_step_success(TEST_COMPONENT_ID_0.raw(), req_id, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reply_handling_step_failure() {
|
||||
let mut testbench =
|
||||
ReplyHandlerTestbench::new(TEST_COMPONENT_ID_0.id(), ActionReplyHandler::default());
|
||||
let action_id = 5_u32;
|
||||
let (req_id, active_req) = testbench.add_tc(TEST_APID, TEST_UNIQUE_ID_0, &[]);
|
||||
let active_action_req =
|
||||
ActivePusActionRequestStd::new_from_common_req(action_id, active_req);
|
||||
let error_code = ResultU16::new(2, 3);
|
||||
let reply = ActionReplyPus::new(
|
||||
action_id,
|
||||
ActionReplyVariant::StepFailed {
|
||||
error_code,
|
||||
step: 1,
|
||||
params: None,
|
||||
},
|
||||
);
|
||||
let generic_reply = GenericMessage::new(MessageMetadata::new(req_id.into(), 0), reply);
|
||||
let result = testbench.handle_reply(&generic_reply, &active_action_req, &[]);
|
||||
assert!(result.is_ok());
|
||||
assert!(result.unwrap());
|
||||
testbench.verif_reporter.check_next_was_added(req_id);
|
||||
testbench
|
||||
.verif_reporter
|
||||
.check_next_is_acceptance_success(TEST_COMPONENT_ID_0.id(), req_id);
|
||||
testbench
|
||||
.verif_reporter
|
||||
.check_next_is_started_success(TEST_COMPONENT_ID_0.id(), req_id);
|
||||
testbench.verif_reporter.check_next_is_step_failure(
|
||||
TEST_COMPONENT_ID_0.id(),
|
||||
req_id,
|
||||
error_code.raw().into(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reply_handling_unrequested_reply() {
|
||||
let mut testbench =
|
||||
ReplyHandlerTestbench::new(TEST_COMPONENT_ID_0.id(), ActionReplyHandler::default());
|
||||
let action_reply = ActionReplyPus::new(5_u32, ActionReplyVariant::Completed);
|
||||
let unrequested_reply =
|
||||
GenericMessage::new(MessageMetadata::new(10_u32, 15_u32), action_reply);
|
||||
// Right now this function does not do a lot. We simply check that it does not panic or do
|
||||
// weird stuff.
|
||||
let result = testbench.handle_unrequested_reply(&unrequested_reply);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reply_handling_reply_timeout() {
|
||||
let mut testbench =
|
||||
ReplyHandlerTestbench::new(TEST_COMPONENT_ID_0.id(), ActionReplyHandler::default());
|
||||
let action_id = 5_u32;
|
||||
let (req_id, active_request) = testbench.add_tc(TEST_APID, TEST_UNIQUE_ID_0, &[]);
|
||||
let result = testbench.handle_request_timeout(
|
||||
&ActivePusActionRequestStd::new_from_common_req(action_id, active_request),
|
||||
&[],
|
||||
);
|
||||
assert!(result.is_ok());
|
||||
testbench.verif_reporter.assert_completion_failure(
|
||||
TEST_COMPONENT_ID_0.raw(),
|
||||
req_id,
|
||||
None,
|
||||
tmtc_err::REQUEST_TIMEOUT.raw() as u64,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
use std::sync::mpsc;
|
||||
|
||||
use crate::pus::create_verification_reporter;
|
||||
use crate::tmtc::sender::TmTcSender;
|
||||
use satrs::pus::event_man::EventRequestWithToken;
|
||||
use satrs::pus::event_srv::PusEventServiceHandler;
|
||||
use satrs::pus::verification::VerificationReporter;
|
||||
use satrs::pus::{
|
||||
DirectPusPacketHandlerResult, EcssTcAndToken, EcssTcCacher, MpscTcReceiver,
|
||||
PartialPusHandlingError, PusServiceHelper,
|
||||
};
|
||||
use satrs::spacepackets::ecss::PusServiceId;
|
||||
use example_std::ids::generic_pus::PUS_EVENT_MANAGEMENT;
|
||||
|
||||
use super::{DirectPusService, HandlingStatus};
|
||||
|
||||
pub fn create_event_service(
|
||||
tm_sender: TmTcSender,
|
||||
tm_in_pool_converter: EcssTcCacher,
|
||||
pus_event_rx: mpsc::Receiver<EcssTcAndToken>,
|
||||
event_request_tx: mpsc::Sender<EventRequestWithToken>,
|
||||
) -> EventServiceWrapper {
|
||||
let pus_5_handler = PusEventServiceHandler::new(
|
||||
PusServiceHelper::new(
|
||||
PUS_EVENT_MANAGEMENT.id(),
|
||||
pus_event_rx,
|
||||
tm_sender,
|
||||
create_verification_reporter(PUS_EVENT_MANAGEMENT.id(), PUS_EVENT_MANAGEMENT.apid),
|
||||
tm_in_pool_converter,
|
||||
),
|
||||
event_request_tx,
|
||||
);
|
||||
EventServiceWrapper {
|
||||
handler: pus_5_handler,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EventServiceWrapper {
|
||||
pub handler:
|
||||
PusEventServiceHandler<MpscTcReceiver, TmTcSender, EcssTcCacher, VerificationReporter>,
|
||||
}
|
||||
|
||||
impl DirectPusService for EventServiceWrapper {
|
||||
const SERVICE_ID: u8 = PusServiceId::Event as u8;
|
||||
|
||||
const SERVICE_STR: &'static str = "events";
|
||||
|
||||
fn poll_and_handle_next_tc(&mut self, time_stamp: &[u8]) -> HandlingStatus {
|
||||
let error_handler = |partial_error: &PartialPusHandlingError| {
|
||||
log::warn!(
|
||||
"PUS {}({}) partial error: {:?}",
|
||||
Self::SERVICE_ID,
|
||||
Self::SERVICE_STR,
|
||||
partial_error
|
||||
);
|
||||
};
|
||||
let result = self
|
||||
.handler
|
||||
.poll_and_handle_next_tc(error_handler, time_stamp);
|
||||
if let Err(e) = result {
|
||||
log::warn!(
|
||||
"PUS {}({}) error: {:?}",
|
||||
Self::SERVICE_ID,
|
||||
Self::SERVICE_STR,
|
||||
e
|
||||
);
|
||||
// To avoid permanent loops on continuous errors.
|
||||
return HandlingStatus::Empty;
|
||||
}
|
||||
match result.unwrap() {
|
||||
DirectPusPacketHandlerResult::Handled(handling_status) => return handling_status,
|
||||
DirectPusPacketHandlerResult::CustomSubservice(subservice, _) => {
|
||||
log::warn!(
|
||||
"PUS {}({}) subservice {} not implemented",
|
||||
Self::SERVICE_ID,
|
||||
Self::SERVICE_STR,
|
||||
subservice
|
||||
);
|
||||
}
|
||||
DirectPusPacketHandlerResult::SubserviceNotImplemented(subservice, _) => {
|
||||
log::warn!(
|
||||
"PUS {}({}) subservice {} not implemented",
|
||||
Self::SERVICE_ID,
|
||||
Self::SERVICE_STR,
|
||||
subservice
|
||||
);
|
||||
}
|
||||
}
|
||||
HandlingStatus::HandledOne
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,524 @@
|
||||
use derive_new::new;
|
||||
use satrs::hk::{CollectionIntervalFactor, HkRequest, HkRequestVariant, UniqueId};
|
||||
use satrs::pus::verification::{
|
||||
FailParams, TcStateAccepted, TcStateStarted, VerificationReporter,
|
||||
VerificationReportingProvider, VerificationToken,
|
||||
};
|
||||
use satrs::pus::{
|
||||
ActivePusRequestStd, ActiveRequest, DefaultActiveRequestMap, EcssTcAndToken, EcssTcCacher,
|
||||
EcssTmSender, EcssTmtcError, GenericConversionError, MpscTcReceiver, PusPacketHandlingError,
|
||||
PusReplyHandler, PusServiceHelper, PusTcToRequestConverter,
|
||||
};
|
||||
use satrs::request::{GenericMessage, UniqueApidTargetId};
|
||||
use satrs::res_code::ResultU16;
|
||||
use satrs::spacepackets::ecss::tc::PusTcReader;
|
||||
use satrs::spacepackets::ecss::{hk, PusPacket, PusServiceId};
|
||||
use example_std::config::{hk_err, tmtc_err};
|
||||
use example_std::ids::generic_pus::PUS_HK;
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::pus::{create_verification_reporter, generic_pus_request_timeout_handler};
|
||||
use crate::requests::GenericRequestRouter;
|
||||
use crate::tmtc::sender::TmTcSender;
|
||||
|
||||
use super::{HandlingStatus, PusTargetedRequestService, TargetedPusService};
|
||||
|
||||
#[derive(Clone, PartialEq, Debug, new)]
|
||||
pub struct HkReply {
|
||||
pub unique_id: UniqueId,
|
||||
pub variant: HkReplyVariant,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub enum HkReplyVariant {
|
||||
Ack,
|
||||
Failed(ResultU16),
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct HkReplyHandler {}
|
||||
|
||||
impl PusReplyHandler<ActivePusRequestStd, HkReply> for HkReplyHandler {
|
||||
type Error = EcssTmtcError;
|
||||
|
||||
fn handle_unrequested_reply(
|
||||
&mut self,
|
||||
reply: &GenericMessage<HkReply>,
|
||||
_tm_sender: &impl EcssTmSender,
|
||||
) -> Result<(), Self::Error> {
|
||||
log::warn!("received unexpected reply for service 3: {reply:?}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_reply(
|
||||
&mut self,
|
||||
reply: &GenericMessage<HkReply>,
|
||||
active_request: &ActivePusRequestStd,
|
||||
tm_sender: &impl EcssTmSender,
|
||||
verification_handler: &impl VerificationReportingProvider,
|
||||
time_stamp: &[u8],
|
||||
) -> Result<bool, Self::Error> {
|
||||
let started_token: VerificationToken<TcStateStarted> = active_request
|
||||
.token()
|
||||
.try_into()
|
||||
.expect("invalid token state");
|
||||
match reply.message.variant {
|
||||
HkReplyVariant::Ack => {
|
||||
verification_handler
|
||||
.completion_success(tm_sender, started_token, time_stamp)
|
||||
.expect("sending completion success verification failed");
|
||||
}
|
||||
HkReplyVariant::Failed(failure_code) => {
|
||||
verification_handler
|
||||
.completion_failure(
|
||||
tm_sender,
|
||||
started_token,
|
||||
FailParams::new(time_stamp, &failure_code, &[]),
|
||||
)
|
||||
.expect("sending completion success verification failed");
|
||||
}
|
||||
};
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn handle_request_timeout(
|
||||
&mut self,
|
||||
active_request: &ActivePusRequestStd,
|
||||
tm_sender: &impl EcssTmSender,
|
||||
verification_handler: &impl VerificationReportingProvider,
|
||||
time_stamp: &[u8],
|
||||
) -> Result<(), Self::Error> {
|
||||
generic_pus_request_timeout_handler(
|
||||
tm_sender,
|
||||
active_request,
|
||||
verification_handler,
|
||||
time_stamp,
|
||||
"HK",
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HkRequestConverter {
|
||||
timeout: Duration,
|
||||
}
|
||||
|
||||
impl Default for HkRequestConverter {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
timeout: Duration::from_secs(60),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PusTcToRequestConverter<ActivePusRequestStd, HkRequest> for HkRequestConverter {
|
||||
type Error = GenericConversionError;
|
||||
|
||||
fn convert(
|
||||
&mut self,
|
||||
token: VerificationToken<TcStateAccepted>,
|
||||
tc: &PusTcReader,
|
||||
tm_sender: &(impl EcssTmSender + ?Sized),
|
||||
verif_reporter: &impl VerificationReportingProvider,
|
||||
time_stamp: &[u8],
|
||||
) -> Result<(ActivePusRequestStd, HkRequest), Self::Error> {
|
||||
let user_data = tc.user_data();
|
||||
if user_data.is_empty() {
|
||||
let user_data_len = user_data.len() as u32;
|
||||
let user_data_len_raw = user_data_len.to_be_bytes();
|
||||
verif_reporter
|
||||
.start_failure(
|
||||
tm_sender,
|
||||
token,
|
||||
FailParams::new(
|
||||
time_stamp,
|
||||
&tmtc_err::NOT_ENOUGH_APP_DATA,
|
||||
&user_data_len_raw,
|
||||
),
|
||||
)
|
||||
.expect("Sending start failure TM failed");
|
||||
return Err(GenericConversionError::NotEnoughAppData {
|
||||
expected: 4,
|
||||
found: 0,
|
||||
});
|
||||
}
|
||||
if user_data.len() < 8 {
|
||||
let err = if user_data.len() < 4 {
|
||||
&hk_err::TARGET_ID_MISSING
|
||||
} else {
|
||||
&hk_err::UNIQUE_ID_MISSING
|
||||
};
|
||||
let user_data_len = user_data.len() as u32;
|
||||
let user_data_len_raw = user_data_len.to_be_bytes();
|
||||
verif_reporter
|
||||
.start_failure(
|
||||
tm_sender,
|
||||
token,
|
||||
FailParams::new(time_stamp, err, &user_data_len_raw),
|
||||
)
|
||||
.expect("Sending start failure TM failed");
|
||||
return Err(GenericConversionError::NotEnoughAppData {
|
||||
expected: 8,
|
||||
found: 4,
|
||||
});
|
||||
}
|
||||
let subservice = tc.message_subtype_id();
|
||||
let target_id_and_apid = UniqueApidTargetId::from_pus_tc(tc).expect("invalid tc format");
|
||||
let unique_id = u32::from_be_bytes(tc.user_data()[4..8].try_into().unwrap());
|
||||
|
||||
let standard_subservice = hk::MessageSubtypeId::try_from(subservice);
|
||||
if standard_subservice.is_err() {
|
||||
verif_reporter
|
||||
.start_failure(
|
||||
tm_sender,
|
||||
token,
|
||||
FailParams::new(time_stamp, &tmtc_err::INVALID_PUS_SUBSERVICE, &[subservice]),
|
||||
)
|
||||
.expect("Sending start failure TM failed");
|
||||
return Err(GenericConversionError::InvalidSubservice(subservice));
|
||||
}
|
||||
let request = match standard_subservice.unwrap() {
|
||||
hk::MessageSubtypeId::TcEnableHkGeneration
|
||||
| hk::MessageSubtypeId::TcEnableDiagGeneration => {
|
||||
HkRequest::new(unique_id, HkRequestVariant::EnablePeriodic)
|
||||
}
|
||||
hk::MessageSubtypeId::TcDisableHkGeneration
|
||||
| hk::MessageSubtypeId::TcDisableDiagGeneration => {
|
||||
HkRequest::new(unique_id, HkRequestVariant::DisablePeriodic)
|
||||
}
|
||||
hk::MessageSubtypeId::TcReportHkReportStructures => todo!(),
|
||||
hk::MessageSubtypeId::TmHkPacket => todo!(),
|
||||
hk::MessageSubtypeId::TcGenerateOneShotHk
|
||||
| hk::MessageSubtypeId::TcGenerateOneShotDiag => {
|
||||
HkRequest::new(unique_id, HkRequestVariant::OneShot)
|
||||
}
|
||||
hk::MessageSubtypeId::TcModifyDiagCollectionInterval
|
||||
| hk::MessageSubtypeId::TcModifyHkCollectionInterval => {
|
||||
if user_data.len() < 12 {
|
||||
verif_reporter
|
||||
.start_failure(
|
||||
tm_sender,
|
||||
token,
|
||||
FailParams::new_no_fail_data(
|
||||
time_stamp,
|
||||
&tmtc_err::NOT_ENOUGH_APP_DATA,
|
||||
),
|
||||
)
|
||||
.expect("Sending start failure TM failed");
|
||||
return Err(GenericConversionError::NotEnoughAppData {
|
||||
expected: 12,
|
||||
found: user_data.len(),
|
||||
});
|
||||
}
|
||||
HkRequest::new(
|
||||
unique_id,
|
||||
HkRequestVariant::ModifyCollectionInterval(
|
||||
CollectionIntervalFactor::from_be_bytes(
|
||||
user_data[8..12].try_into().unwrap(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
verif_reporter
|
||||
.start_failure(
|
||||
tm_sender,
|
||||
token,
|
||||
FailParams::new(
|
||||
time_stamp,
|
||||
&tmtc_err::PUS_SUBSERVICE_NOT_IMPLEMENTED,
|
||||
&[subservice],
|
||||
),
|
||||
)
|
||||
.expect("Sending start failure TM failed");
|
||||
return Err(GenericConversionError::InvalidSubservice(subservice));
|
||||
}
|
||||
};
|
||||
Ok((
|
||||
ActivePusRequestStd::new(target_id_and_apid.into(), token, self.timeout),
|
||||
request,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_hk_service(
|
||||
tm_sender: TmTcSender,
|
||||
tc_in_mem_converter: EcssTcCacher,
|
||||
pus_hk_rx: mpsc::Receiver<EcssTcAndToken>,
|
||||
request_router: GenericRequestRouter,
|
||||
reply_receiver: mpsc::Receiver<GenericMessage<HkReply>>,
|
||||
) -> HkServiceWrapper {
|
||||
let pus_3_handler = PusTargetedRequestService::new(
|
||||
PusServiceHelper::new(
|
||||
PUS_HK.id(),
|
||||
pus_hk_rx,
|
||||
tm_sender,
|
||||
create_verification_reporter(PUS_HK.id(), PUS_HK.apid),
|
||||
tc_in_mem_converter,
|
||||
),
|
||||
HkRequestConverter::default(),
|
||||
DefaultActiveRequestMap::default(),
|
||||
HkReplyHandler::default(),
|
||||
request_router,
|
||||
reply_receiver,
|
||||
);
|
||||
HkServiceWrapper {
|
||||
service: pus_3_handler,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HkServiceWrapper {
|
||||
pub(crate) service: PusTargetedRequestService<
|
||||
MpscTcReceiver,
|
||||
VerificationReporter,
|
||||
HkRequestConverter,
|
||||
HkReplyHandler,
|
||||
DefaultActiveRequestMap<ActivePusRequestStd>,
|
||||
ActivePusRequestStd,
|
||||
HkRequest,
|
||||
HkReply,
|
||||
>,
|
||||
}
|
||||
|
||||
impl TargetedPusService for HkServiceWrapper {
|
||||
const SERVICE_ID: u8 = PusServiceId::Housekeeping as u8;
|
||||
const SERVICE_STR: &'static str = "housekeeping";
|
||||
|
||||
delegate::delegate! {
|
||||
to self.service {
|
||||
fn poll_and_handle_next_tc(
|
||||
&mut self,
|
||||
time_stamp: &[u8],
|
||||
) -> Result<HandlingStatus, PusPacketHandlingError>;
|
||||
|
||||
fn poll_and_handle_next_reply(
|
||||
&mut self,
|
||||
time_stamp: &[u8],
|
||||
) -> Result<HandlingStatus, EcssTmtcError>;
|
||||
|
||||
fn check_for_request_timeouts(&mut self);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use arbitrary_int::traits::Integer as _;
|
||||
use arbitrary_int::{u14, u21};
|
||||
use satrs::pus::test_util::{
|
||||
TEST_COMPONENT_ID_0, TEST_COMPONENT_ID_1, TEST_UNIQUE_ID_0, TEST_UNIQUE_ID_1,
|
||||
};
|
||||
use satrs::request::MessageMetadata;
|
||||
use satrs::spacepackets::ecss::{CreatorConfig, MessageTypeId};
|
||||
use satrs::{
|
||||
hk::HkRequestVariant,
|
||||
pus::test_util::TEST_APID,
|
||||
request::GenericMessage,
|
||||
spacepackets::{
|
||||
ecss::{hk::MessageSubtypeId, tc::PusTcCreator},
|
||||
SpHeader,
|
||||
},
|
||||
};
|
||||
use example_std::config::tmtc_err;
|
||||
|
||||
use crate::pus::{
|
||||
hk::HkReplyVariant,
|
||||
tests::{PusConverterTestbench, ReplyHandlerTestbench},
|
||||
};
|
||||
|
||||
use super::{HkReply, HkReplyHandler, HkRequestConverter};
|
||||
|
||||
#[test]
|
||||
fn hk_converter_one_shot_req() {
|
||||
let mut hk_bench =
|
||||
PusConverterTestbench::new(TEST_COMPONENT_ID_0.id(), HkRequestConverter::default());
|
||||
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, u14::ZERO, 0);
|
||||
let target_id = TEST_UNIQUE_ID_0;
|
||||
let unique_id = 5_u32;
|
||||
let mut app_data: [u8; 8] = [0; 8];
|
||||
app_data[0..4].copy_from_slice(&target_id.as_u32().to_be_bytes());
|
||||
app_data[4..8].copy_from_slice(&unique_id.to_be_bytes());
|
||||
|
||||
let hk_req = PusTcCreator::new_simple(
|
||||
sp_header,
|
||||
MessageTypeId::new(3, MessageSubtypeId::TcGenerateOneShotHk as u8),
|
||||
&app_data,
|
||||
CreatorConfig::default(),
|
||||
);
|
||||
let accepted_token = hk_bench.add_tc(&hk_req);
|
||||
let (_active_req, req) = hk_bench
|
||||
.convert(accepted_token, &[], TEST_APID, TEST_UNIQUE_ID_0)
|
||||
.expect("conversion failed");
|
||||
|
||||
assert_eq!(req.unique_id, unique_id);
|
||||
if let HkRequestVariant::OneShot = req.variant {
|
||||
} else {
|
||||
panic!("unexpected HK request")
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hk_converter_enable_periodic_generation() {
|
||||
let mut hk_bench =
|
||||
PusConverterTestbench::new(TEST_COMPONENT_ID_0.id(), HkRequestConverter::default());
|
||||
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, u14::ZERO, 0);
|
||||
let target_id = TEST_UNIQUE_ID_0;
|
||||
let unique_id = 5_u32;
|
||||
let mut app_data: [u8; 8] = [0; 8];
|
||||
app_data[0..4].copy_from_slice(&target_id.as_u32().to_be_bytes());
|
||||
app_data[4..8].copy_from_slice(&unique_id.to_be_bytes());
|
||||
let mut generic_check = |tc: &PusTcCreator| {
|
||||
let accepted_token = hk_bench.add_tc(tc);
|
||||
let (_active_req, req) = hk_bench
|
||||
.convert(accepted_token, &[], TEST_APID, TEST_UNIQUE_ID_0)
|
||||
.expect("conversion failed");
|
||||
assert_eq!(req.unique_id, unique_id);
|
||||
if let HkRequestVariant::EnablePeriodic = req.variant {
|
||||
} else {
|
||||
panic!("unexpected HK request")
|
||||
}
|
||||
};
|
||||
let tc0 = PusTcCreator::new_simple(
|
||||
sp_header,
|
||||
MessageTypeId::new(3, MessageSubtypeId::TcEnableHkGeneration as u8),
|
||||
&app_data,
|
||||
CreatorConfig::default(),
|
||||
);
|
||||
generic_check(&tc0);
|
||||
let tc1 = PusTcCreator::new_simple(
|
||||
sp_header,
|
||||
MessageTypeId::new(3, MessageSubtypeId::TcEnableDiagGeneration as u8),
|
||||
&app_data,
|
||||
CreatorConfig::default(),
|
||||
);
|
||||
generic_check(&tc1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hk_conversion_disable_periodic_generation() {
|
||||
let mut hk_bench =
|
||||
PusConverterTestbench::new(TEST_COMPONENT_ID_0.id(), HkRequestConverter::default());
|
||||
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, u14::ZERO, 0);
|
||||
let target_id = TEST_UNIQUE_ID_0;
|
||||
let unique_id = 5_u32;
|
||||
let mut app_data: [u8; 8] = [0; 8];
|
||||
app_data[0..4].copy_from_slice(&target_id.as_u32().to_be_bytes());
|
||||
app_data[4..8].copy_from_slice(&unique_id.to_be_bytes());
|
||||
let mut generic_check = |tc: &PusTcCreator| {
|
||||
let accepted_token = hk_bench.add_tc(tc);
|
||||
let (_active_req, req) = hk_bench
|
||||
.convert(accepted_token, &[], TEST_APID, TEST_UNIQUE_ID_0)
|
||||
.expect("conversion failed");
|
||||
assert_eq!(req.unique_id, unique_id);
|
||||
if let HkRequestVariant::DisablePeriodic = req.variant {
|
||||
} else {
|
||||
panic!("unexpected HK request")
|
||||
}
|
||||
};
|
||||
let tc0 = PusTcCreator::new_simple(
|
||||
sp_header,
|
||||
MessageTypeId::new(3, MessageSubtypeId::TcDisableHkGeneration as u8),
|
||||
&app_data,
|
||||
CreatorConfig::default(),
|
||||
);
|
||||
generic_check(&tc0);
|
||||
let tc1 = PusTcCreator::new_simple(
|
||||
sp_header,
|
||||
MessageTypeId::new(3, MessageSubtypeId::TcDisableDiagGeneration as u8),
|
||||
&app_data,
|
||||
CreatorConfig::default(),
|
||||
);
|
||||
generic_check(&tc1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hk_conversion_modify_interval() {
|
||||
let mut hk_bench =
|
||||
PusConverterTestbench::new(TEST_COMPONENT_ID_0.id(), HkRequestConverter::default());
|
||||
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, u14::ZERO, 0);
|
||||
let target_id = TEST_UNIQUE_ID_0;
|
||||
let unique_id = 5_u32;
|
||||
let mut app_data: [u8; 12] = [0; 12];
|
||||
let collection_interval_factor = 5_u32;
|
||||
app_data[0..4].copy_from_slice(&target_id.as_u32().to_be_bytes());
|
||||
app_data[4..8].copy_from_slice(&unique_id.to_be_bytes());
|
||||
app_data[8..12].copy_from_slice(&collection_interval_factor.to_be_bytes());
|
||||
|
||||
let mut generic_check = |tc: &PusTcCreator| {
|
||||
let accepted_token = hk_bench.add_tc(tc);
|
||||
let (_active_req, req) = hk_bench
|
||||
.convert(accepted_token, &[], TEST_APID, TEST_UNIQUE_ID_0)
|
||||
.expect("conversion failed");
|
||||
assert_eq!(req.unique_id, unique_id);
|
||||
if let HkRequestVariant::ModifyCollectionInterval(interval_factor) = req.variant {
|
||||
assert_eq!(interval_factor, collection_interval_factor);
|
||||
} else {
|
||||
panic!("unexpected HK request")
|
||||
}
|
||||
};
|
||||
let tc0 = PusTcCreator::new_simple(
|
||||
sp_header,
|
||||
MessageTypeId::new(3, MessageSubtypeId::TcModifyHkCollectionInterval as u8),
|
||||
&app_data,
|
||||
CreatorConfig::default(),
|
||||
);
|
||||
generic_check(&tc0);
|
||||
let tc1 = PusTcCreator::new_simple(
|
||||
sp_header,
|
||||
MessageTypeId::new(3, MessageSubtypeId::TcModifyDiagCollectionInterval as u8),
|
||||
&app_data,
|
||||
CreatorConfig::default(),
|
||||
);
|
||||
generic_check(&tc1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hk_reply_handler() {
|
||||
let mut reply_testbench =
|
||||
ReplyHandlerTestbench::new(TEST_COMPONENT_ID_0.id(), HkReplyHandler::default());
|
||||
let sender_id = 2_u32;
|
||||
let apid_target_id = u21::new(3);
|
||||
let unique_id = 5_u32;
|
||||
let (req_id, active_req) = reply_testbench.add_tc(TEST_APID, apid_target_id, &[]);
|
||||
let reply = GenericMessage::new(
|
||||
MessageMetadata::new(req_id.into(), sender_id),
|
||||
HkReply::new(unique_id, HkReplyVariant::Ack),
|
||||
);
|
||||
let result = reply_testbench.handle_reply(&reply, &active_req, &[]);
|
||||
assert!(result.is_ok());
|
||||
assert!(result.unwrap());
|
||||
reply_testbench
|
||||
.verif_reporter
|
||||
.assert_full_completion_success(TEST_COMPONENT_ID_0.raw(), req_id, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reply_handling_unrequested_reply() {
|
||||
let mut testbench =
|
||||
ReplyHandlerTestbench::new(TEST_COMPONENT_ID_1.id(), HkReplyHandler::default());
|
||||
let action_reply = HkReply::new(5_u32, HkReplyVariant::Ack);
|
||||
let unrequested_reply =
|
||||
GenericMessage::new(MessageMetadata::new(10_u32, 15_u32), action_reply);
|
||||
// Right now this function does not do a lot. We simply check that it does not panic or do
|
||||
// weird stuff.
|
||||
let result = testbench.handle_unrequested_reply(&unrequested_reply);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reply_handling_reply_timeout() {
|
||||
let mut testbench =
|
||||
ReplyHandlerTestbench::new(TEST_COMPONENT_ID_1.id(), HkReplyHandler::default());
|
||||
let (req_id, active_request) = testbench.add_tc(TEST_APID, TEST_UNIQUE_ID_1, &[]);
|
||||
let result = testbench.handle_request_timeout(&active_request, &[]);
|
||||
assert!(result.is_ok());
|
||||
testbench.verif_reporter.assert_completion_failure(
|
||||
TEST_COMPONENT_ID_1.raw(),
|
||||
req_id,
|
||||
None,
|
||||
tmtc_err::REQUEST_TIMEOUT.raw() as u64,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,779 @@
|
||||
use crate::requests::GenericRequestRouter;
|
||||
use crate::tmtc::sender::TmTcSender;
|
||||
use log::warn;
|
||||
use satrs::pool::PoolAddr;
|
||||
use satrs::pus::verification::{
|
||||
self, FailParams, TcStateAccepted, TcStateStarted, VerificationReporter,
|
||||
VerificationReporterConfig, VerificationReportingProvider, VerificationToken,
|
||||
};
|
||||
use satrs::pus::{
|
||||
ActiveRequest, ActiveRequestStore, CacheAndReadRawEcssTc, EcssTcAndToken, EcssTcCacher,
|
||||
EcssTcReceiver, EcssTmSender, EcssTmtcError, GenericConversionError, GenericRoutingError,
|
||||
HandlingStatus, PusPacketHandlingError, PusReplyHandler, PusRequestRouter, PusServiceHelper,
|
||||
PusTcToRequestConverter, TcInMemory,
|
||||
};
|
||||
use satrs::queue::{GenericReceiveError, GenericSendError};
|
||||
use satrs::request::{Apid, GenericMessage, MessageMetadata};
|
||||
use satrs::spacepackets::ecss::tc::PusTcReader;
|
||||
use satrs::spacepackets::ecss::{PusPacket, PusServiceId};
|
||||
use satrs::tmtc::{PacketAsVec, PacketInPool};
|
||||
use satrs::ComponentId;
|
||||
use example_std::config::{tmtc_err, CustomPusServiceId};
|
||||
use example_std::ids::generic_pus::PUS_ROUTING;
|
||||
use example_std::TimestampHelper;
|
||||
use std::fmt::Debug;
|
||||
use std::sync::mpsc;
|
||||
|
||||
pub mod action;
|
||||
pub mod event;
|
||||
pub mod hk;
|
||||
pub mod mode;
|
||||
pub mod scheduler;
|
||||
pub mod stack;
|
||||
pub mod test;
|
||||
|
||||
pub fn create_verification_reporter(owner_id: ComponentId, apid: Apid) -> VerificationReporter {
|
||||
let verif_cfg = VerificationReporterConfig::new(apid, 1, 2, 8);
|
||||
// Every software component which needs to generate verification telemetry, gets a cloned
|
||||
// verification reporter.
|
||||
VerificationReporter::new(owner_id, &verif_cfg)
|
||||
}
|
||||
|
||||
/*
|
||||
/// Simple router structure which forwards PUS telecommands to dedicated handlers.
|
||||
pub struct PusTcMpscRouter {
|
||||
pub test_tc_sender: mpsc::SyncSender<EcssTcAndToken>,
|
||||
pub event_tc_sender: mpsc::SyncSender<EcssTcAndToken>,
|
||||
pub sched_tc_sender: mpsc::SyncSender<EcssTcAndToken>,
|
||||
pub hk_tc_sender: mpsc::SyncSender<EcssTcAndToken>,
|
||||
#[allow(dead_code)]
|
||||
pub action_tc_sender: mpsc::SyncSender<EcssTcAndToken>,
|
||||
pub mode_tc_sender: mpsc::SyncSender<EcssTcAndToken>,
|
||||
}
|
||||
|
||||
pub struct PusTcDistributor {
|
||||
#[allow(dead_code)]
|
||||
pub id: ComponentId,
|
||||
pub tm_sender: TmTcSender,
|
||||
pub verif_reporter: VerificationReporter,
|
||||
pub pus_router: PusTcMpscRouter,
|
||||
stamp_helper: TimestampHelper,
|
||||
}
|
||||
|
||||
impl PusTcDistributor {
|
||||
pub fn new(tm_sender: TmTcSender, pus_router: PusTcMpscRouter) -> Self {
|
||||
Self {
|
||||
id: PUS_ROUTING.raw(),
|
||||
tm_sender,
|
||||
verif_reporter: create_verification_reporter(PUS_ROUTING.id(), PUS_ROUTING.apid),
|
||||
pus_router,
|
||||
stamp_helper: TimestampHelper::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_tc_packet_vec(
|
||||
&mut self,
|
||||
packet_as_vec: PacketAsVec,
|
||||
) -> Result<HandlingStatus, GenericSendError> {
|
||||
self.handle_tc_generic(packet_as_vec.sender_id, None, &packet_as_vec.packet)
|
||||
}
|
||||
|
||||
pub fn handle_tc_packet_in_store(
|
||||
&mut self,
|
||||
packet_in_pool: PacketInPool,
|
||||
pus_tc_copy: &[u8],
|
||||
) -> Result<HandlingStatus, GenericSendError> {
|
||||
self.handle_tc_generic(
|
||||
packet_in_pool.sender_id,
|
||||
Some(packet_in_pool.store_addr),
|
||||
pus_tc_copy,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn handle_tc_generic(
|
||||
&mut self,
|
||||
sender_id: ComponentId,
|
||||
addr_opt: Option<PoolAddr>,
|
||||
raw_tc: &[u8],
|
||||
) -> Result<HandlingStatus, GenericSendError> {
|
||||
let pus_tc_result = PusTcReader::new(raw_tc);
|
||||
if pus_tc_result.is_err() {
|
||||
log::warn!(
|
||||
"error creating PUS TC from raw data received from {}: {}",
|
||||
sender_id,
|
||||
pus_tc_result.unwrap_err()
|
||||
);
|
||||
log::warn!("raw data: {raw_tc:x?}");
|
||||
// TODO: Shouldn't this be an error?
|
||||
return Ok(HandlingStatus::HandledOne);
|
||||
}
|
||||
let pus_tc = pus_tc_result.unwrap();
|
||||
let init_token = self.verif_reporter.start_verification(&pus_tc);
|
||||
self.stamp_helper.update_from_now();
|
||||
let accepted_token = self
|
||||
.verif_reporter
|
||||
.acceptance_success(&self.tm_sender, init_token, self.stamp_helper.stamp())
|
||||
.expect("Acceptance success failure");
|
||||
let service = PusServiceId::try_from(pus_tc.service_type_id());
|
||||
let tc_in_memory: TcInMemory = if let Some(store_addr) = addr_opt {
|
||||
PacketInPool::new(sender_id, store_addr).into()
|
||||
} else {
|
||||
PacketAsVec::new(sender_id, Vec::from(raw_tc)).into()
|
||||
};
|
||||
match service {
|
||||
Ok(standard_service) => match standard_service {
|
||||
PusServiceId::Test => self.pus_router.test_tc_sender.send(EcssTcAndToken {
|
||||
tc_in_memory,
|
||||
token: Some(accepted_token.into()),
|
||||
})?,
|
||||
PusServiceId::Housekeeping => {
|
||||
self.pus_router.hk_tc_sender.send(EcssTcAndToken {
|
||||
tc_in_memory,
|
||||
token: Some(accepted_token.into()),
|
||||
})?
|
||||
}
|
||||
PusServiceId::Event => self.pus_router.event_tc_sender.send(EcssTcAndToken {
|
||||
tc_in_memory,
|
||||
token: Some(accepted_token.into()),
|
||||
})?,
|
||||
PusServiceId::Scheduling => {
|
||||
self.pus_router.sched_tc_sender.send(EcssTcAndToken {
|
||||
tc_in_memory,
|
||||
token: Some(accepted_token.into()),
|
||||
})?
|
||||
}
|
||||
_ => {
|
||||
let result = self.verif_reporter.start_failure(
|
||||
&self.tm_sender,
|
||||
accepted_token,
|
||||
FailParams::new(
|
||||
self.stamp_helper.stamp(),
|
||||
&tmtc_err::PUS_SERVICE_NOT_IMPLEMENTED,
|
||||
&[standard_service as u8],
|
||||
),
|
||||
);
|
||||
if result.is_err() {
|
||||
warn!("Sending verification failure failed");
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
if let Ok(custom_service) = CustomPusServiceId::try_from(e.number) {
|
||||
match custom_service {
|
||||
CustomPusServiceId::Mode => self
|
||||
.pus_router
|
||||
.mode_tc_sender
|
||||
.send(EcssTcAndToken {
|
||||
tc_in_memory,
|
||||
token: Some(accepted_token.into()),
|
||||
})
|
||||
.map_err(|_| GenericSendError::RxDisconnected)?,
|
||||
CustomPusServiceId::Health => {}
|
||||
}
|
||||
} else {
|
||||
self.verif_reporter
|
||||
.start_failure(
|
||||
&self.tm_sender,
|
||||
accepted_token,
|
||||
FailParams::new(
|
||||
self.stamp_helper.stamp(),
|
||||
&tmtc_err::INVALID_PUS_SUBSERVICE,
|
||||
&[e.number],
|
||||
),
|
||||
)
|
||||
.expect("Start failure verification failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(HandlingStatus::HandledOne)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
pub trait TargetedPusService {
|
||||
const SERVICE_ID: u8;
|
||||
const SERVICE_STR: &'static str;
|
||||
|
||||
fn poll_and_handle_next_tc_default_handler(&mut self, time_stamp: &[u8]) -> HandlingStatus {
|
||||
let result = self.poll_and_handle_next_tc(time_stamp);
|
||||
if let Err(e) = result {
|
||||
log::error!(
|
||||
"PUS service {}({})packet handling error: {:?}",
|
||||
Self::SERVICE_ID,
|
||||
Self::SERVICE_STR,
|
||||
e
|
||||
);
|
||||
// To avoid permanent loops on error cases.
|
||||
return HandlingStatus::Empty;
|
||||
}
|
||||
result.unwrap()
|
||||
}
|
||||
|
||||
fn poll_and_handle_next_reply_default_handler(&mut self, time_stamp: &[u8]) -> HandlingStatus {
|
||||
// This only fails if all senders disconnected. Treat it like an empty queue.
|
||||
self.poll_and_handle_next_reply(time_stamp)
|
||||
.unwrap_or_else(|e| {
|
||||
warn!(
|
||||
"PUS servce {}({}): Handling reply failed with error {:?}",
|
||||
Self::SERVICE_ID,
|
||||
Self::SERVICE_STR,
|
||||
e
|
||||
);
|
||||
HandlingStatus::Empty
|
||||
})
|
||||
}
|
||||
|
||||
fn poll_and_handle_next_tc(
|
||||
&mut self,
|
||||
time_stamp: &[u8],
|
||||
) -> Result<HandlingStatus, PusPacketHandlingError>;
|
||||
|
||||
fn poll_and_handle_next_reply(
|
||||
&mut self,
|
||||
time_stamp: &[u8],
|
||||
) -> Result<HandlingStatus, EcssTmtcError>;
|
||||
|
||||
fn check_for_request_timeouts(&mut self);
|
||||
}
|
||||
|
||||
/// Generic trait for services which handle packets directly. Kept minimal right now because
|
||||
/// of the difficulty to allow flexible user code for these services..
|
||||
pub trait DirectPusService {
|
||||
const SERVICE_ID: u8;
|
||||
const SERVICE_STR: &'static str;
|
||||
|
||||
fn poll_and_handle_next_tc(&mut self, timestamp: &[u8]) -> HandlingStatus;
|
||||
}
|
||||
|
||||
/// This is a generic handler class for all PUS services where a PUS telecommand is converted
|
||||
/// to a targeted request.
|
||||
///
|
||||
/// The generic steps for this process are the following
|
||||
///
|
||||
/// 1. Poll for TC packets
|
||||
/// 2. Convert the raw packets to a [PusTcReader].
|
||||
/// 3. Convert the PUS TC to a typed request using the [PusTcToRequestConverter].
|
||||
/// 4. Route the requests using the [GenericRequestRouter].
|
||||
/// 5. Add the request to the active request map using the [ActiveRequestMapProvider] abstraction.
|
||||
/// 6. Check for replies which complete the forwarded request. The handler takes care of
|
||||
/// the verification process.
|
||||
/// 7. Check for timeouts of active requests. Generally, the timeout on the service level should
|
||||
/// be highest expected timeout for the given target.
|
||||
///
|
||||
/// The handler exposes the following API:
|
||||
///
|
||||
/// 1. [Self::poll_and_handle_next_tc] which tries to poll and handle one TC packet, covering
|
||||
/// steps 1-5.
|
||||
/// 2. [Self::poll_and_check_next_reply] which tries to poll and handle one reply, covering step 6.
|
||||
/// 3. [Self::check_for_request_timeouts] which checks for request timeouts, covering step 7.
|
||||
pub struct PusTargetedRequestService<
|
||||
TcReceiver: EcssTcReceiver,
|
||||
VerificationReporter: VerificationReportingProvider,
|
||||
RequestConverter: PusTcToRequestConverter<ActiveRequestInfo, RequestType, Error = GenericConversionError>,
|
||||
ReplyHandler: PusReplyHandler<ActiveRequestInfo, ReplyType, Error = EcssTmtcError>,
|
||||
ActiveRequestMapInstance: ActiveRequestStore<ActiveRequestInfo>,
|
||||
ActiveRequestInfo: ActiveRequest,
|
||||
RequestType,
|
||||
ReplyType,
|
||||
> {
|
||||
pub service_helper:
|
||||
PusServiceHelper<TcReceiver, TmTcSender, EcssTcCacher, VerificationReporter>,
|
||||
pub request_router: GenericRequestRouter,
|
||||
pub request_converter: RequestConverter,
|
||||
pub active_request_map: ActiveRequestMapInstance,
|
||||
pub reply_handler: ReplyHandler,
|
||||
pub reply_receiver: mpsc::Receiver<GenericMessage<ReplyType>>,
|
||||
phantom: std::marker::PhantomData<(RequestType, ActiveRequestInfo, ReplyType)>,
|
||||
}
|
||||
|
||||
impl<
|
||||
TcReceiver: EcssTcReceiver,
|
||||
VerificationReporter: VerificationReportingProvider,
|
||||
RequestConverter: PusTcToRequestConverter<ActiveRequestInfo, RequestType, Error = GenericConversionError>,
|
||||
ReplyHandler: PusReplyHandler<ActiveRequestInfo, ReplyType, Error = EcssTmtcError>,
|
||||
ActiveRequestMapInstance: ActiveRequestStore<ActiveRequestInfo>,
|
||||
ActiveRequestInfo: ActiveRequest,
|
||||
RequestType,
|
||||
ReplyType,
|
||||
>
|
||||
PusTargetedRequestService<
|
||||
TcReceiver,
|
||||
VerificationReporter,
|
||||
RequestConverter,
|
||||
ReplyHandler,
|
||||
ActiveRequestMapInstance,
|
||||
ActiveRequestInfo,
|
||||
RequestType,
|
||||
ReplyType,
|
||||
>
|
||||
where
|
||||
GenericRequestRouter: PusRequestRouter<RequestType, Error = GenericRoutingError>,
|
||||
{
|
||||
pub fn new(
|
||||
service_helper: PusServiceHelper<
|
||||
TcReceiver,
|
||||
TmTcSender,
|
||||
EcssTcCacher,
|
||||
VerificationReporter,
|
||||
>,
|
||||
request_converter: RequestConverter,
|
||||
active_request_map: ActiveRequestMapInstance,
|
||||
reply_hook: ReplyHandler,
|
||||
request_router: GenericRequestRouter,
|
||||
reply_receiver: mpsc::Receiver<GenericMessage<ReplyType>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
service_helper,
|
||||
request_converter,
|
||||
active_request_map,
|
||||
reply_handler: reply_hook,
|
||||
request_router,
|
||||
reply_receiver,
|
||||
phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn poll_and_handle_next_tc(
|
||||
&mut self,
|
||||
time_stamp: &[u8],
|
||||
) -> Result<HandlingStatus, PusPacketHandlingError> {
|
||||
let possible_packet = self.service_helper.retrieve_and_accept_next_packet()?;
|
||||
if possible_packet.is_none() {
|
||||
return Ok(HandlingStatus::Empty);
|
||||
}
|
||||
let ecss_tc_and_token = possible_packet.unwrap();
|
||||
self.service_helper
|
||||
.tc_in_mem_converter_mut()
|
||||
.cache(&ecss_tc_and_token.tc_in_memory)?;
|
||||
let tc = self.service_helper.tc_in_mem_converter().convert()?;
|
||||
let (mut request_info, request) = match self.request_converter.convert(
|
||||
ecss_tc_and_token.token,
|
||||
&tc,
|
||||
self.service_helper.tm_sender(),
|
||||
&self.service_helper.common.verif_reporter,
|
||||
time_stamp,
|
||||
) {
|
||||
Ok((info, req)) => (info, req),
|
||||
Err(e) => {
|
||||
self.handle_conversion_to_request_error(&e, ecss_tc_and_token.token, time_stamp);
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
let accepted_token: VerificationToken<TcStateAccepted> = request_info
|
||||
.token()
|
||||
.try_into()
|
||||
.expect("token not in expected accepted state");
|
||||
let verif_request_id = verification::RequestId::new(&tc).raw();
|
||||
match self.request_router.route(
|
||||
MessageMetadata::new(verif_request_id, self.service_helper.id()),
|
||||
request_info.target_id(),
|
||||
request,
|
||||
) {
|
||||
Ok(()) => {
|
||||
let started_token = self
|
||||
.service_helper
|
||||
.verif_reporter()
|
||||
.start_success(
|
||||
&self.service_helper.common.tm_sender,
|
||||
accepted_token,
|
||||
time_stamp,
|
||||
)
|
||||
.expect("Start success failure");
|
||||
request_info.set_token(started_token.into());
|
||||
self.active_request_map
|
||||
.insert(&verif_request_id, request_info);
|
||||
}
|
||||
Err(e) => {
|
||||
self.request_router.handle_error_generic(
|
||||
&request_info,
|
||||
&tc,
|
||||
e.clone(),
|
||||
self.service_helper.tm_sender(),
|
||||
self.service_helper.verif_reporter(),
|
||||
time_stamp,
|
||||
);
|
||||
return Err(e.into());
|
||||
}
|
||||
}
|
||||
Ok(HandlingStatus::HandledOne)
|
||||
}
|
||||
|
||||
fn handle_conversion_to_request_error(
|
||||
&mut self,
|
||||
error: &GenericConversionError,
|
||||
token: VerificationToken<TcStateAccepted>,
|
||||
time_stamp: &[u8],
|
||||
) {
|
||||
match error {
|
||||
GenericConversionError::WrongService(service) => {
|
||||
let service_slice: [u8; 1] = [*service];
|
||||
self.service_helper
|
||||
.verif_reporter()
|
||||
.completion_failure(
|
||||
self.service_helper.tm_sender(),
|
||||
token,
|
||||
FailParams::new(time_stamp, &tmtc_err::INVALID_PUS_SERVICE, &service_slice),
|
||||
)
|
||||
.expect("Sending completion failure failed");
|
||||
}
|
||||
GenericConversionError::InvalidSubservice(subservice) => {
|
||||
let subservice_slice: [u8; 1] = [*subservice];
|
||||
self.service_helper
|
||||
.verif_reporter()
|
||||
.completion_failure(
|
||||
self.service_helper.tm_sender(),
|
||||
token,
|
||||
FailParams::new(
|
||||
time_stamp,
|
||||
&tmtc_err::INVALID_PUS_SUBSERVICE,
|
||||
&subservice_slice,
|
||||
),
|
||||
)
|
||||
.expect("Sending completion failure failed");
|
||||
}
|
||||
GenericConversionError::NotEnoughAppData { expected, found } => {
|
||||
let mut context_info = (*found as u32).to_be_bytes().to_vec();
|
||||
context_info.extend_from_slice(&(*expected as u32).to_be_bytes());
|
||||
self.service_helper
|
||||
.verif_reporter()
|
||||
.completion_failure(
|
||||
self.service_helper.tm_sender(),
|
||||
token,
|
||||
FailParams::new(time_stamp, &tmtc_err::NOT_ENOUGH_APP_DATA, &context_info),
|
||||
)
|
||||
.expect("Sending completion failure failed");
|
||||
}
|
||||
// Do nothing.. this is service-level and can not be handled generically here.
|
||||
GenericConversionError::InvalidAppData(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn poll_and_handle_next_reply(
|
||||
&mut self,
|
||||
time_stamp: &[u8],
|
||||
) -> Result<HandlingStatus, EcssTmtcError> {
|
||||
match self.reply_receiver.try_recv() {
|
||||
Ok(reply) => {
|
||||
self.handle_reply(&reply, time_stamp)?;
|
||||
Ok(HandlingStatus::HandledOne)
|
||||
}
|
||||
Err(e) => match e {
|
||||
mpsc::TryRecvError::Empty => Ok(HandlingStatus::Empty),
|
||||
mpsc::TryRecvError::Disconnected => Err(EcssTmtcError::Receive(
|
||||
GenericReceiveError::TxDisconnected(None),
|
||||
)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_reply(
|
||||
&mut self,
|
||||
reply: &GenericMessage<ReplyType>,
|
||||
time_stamp: &[u8],
|
||||
) -> Result<(), EcssTmtcError> {
|
||||
let active_req_opt = self.active_request_map.get(reply.request_id());
|
||||
if active_req_opt.is_none() {
|
||||
self.reply_handler
|
||||
.handle_unrequested_reply(reply, &self.service_helper.common.tm_sender)?;
|
||||
return Ok(());
|
||||
}
|
||||
let active_request = active_req_opt.unwrap();
|
||||
let result = self.reply_handler.handle_reply(
|
||||
reply,
|
||||
active_request,
|
||||
&self.service_helper.common.tm_sender,
|
||||
&self.service_helper.common.verif_reporter,
|
||||
time_stamp,
|
||||
);
|
||||
if result.is_err() || (result.is_ok() && *result.as_ref().unwrap()) {
|
||||
self.active_request_map.remove(reply.request_id());
|
||||
}
|
||||
result.map(|_| ())
|
||||
}
|
||||
|
||||
pub fn check_for_request_timeouts(&mut self) {
|
||||
let mut requests_to_delete = Vec::new();
|
||||
self.active_request_map
|
||||
.for_each(|request_id, request_info| {
|
||||
if request_info.has_timed_out() {
|
||||
requests_to_delete.push(*request_id);
|
||||
}
|
||||
});
|
||||
if !requests_to_delete.is_empty() {
|
||||
for request_id in requests_to_delete {
|
||||
self.active_request_map.remove(request_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic timeout handling: Handle the verification failure with a dedicated return code
|
||||
/// and also log the error.
|
||||
pub fn generic_pus_request_timeout_handler(
|
||||
sender: &(impl EcssTmSender + ?Sized),
|
||||
active_request: &(impl ActiveRequest + Debug),
|
||||
verification_handler: &impl VerificationReportingProvider,
|
||||
time_stamp: &[u8],
|
||||
service_str: &'static str,
|
||||
) -> Result<(), EcssTmtcError> {
|
||||
log::warn!("timeout for active request {active_request:?} on {service_str} service");
|
||||
let started_token: VerificationToken<TcStateStarted> = active_request
|
||||
.token()
|
||||
.try_into()
|
||||
.expect("token not in expected started state");
|
||||
verification_handler.completion_failure(
|
||||
sender,
|
||||
started_token,
|
||||
FailParams::new(time_stamp, &tmtc_err::REQUEST_TIMEOUT, &[]),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use arbitrary_int::{u11, u21};
|
||||
use satrs::pus::test_util::TEST_COMPONENT_ID_0;
|
||||
use satrs::pus::{MpscTmAsVecSender, PusTmVariant};
|
||||
use satrs::request::RequestId;
|
||||
use satrs::spacepackets::ecss::{CreatorConfig, MessageTypeId};
|
||||
use satrs::{
|
||||
pus::{
|
||||
verification::test_util::TestVerificationReporter, ActivePusRequestStd,
|
||||
ActiveRequestStore, MpscTcReceiver,
|
||||
},
|
||||
request::UniqueApidTargetId,
|
||||
spacepackets::{
|
||||
ecss::{
|
||||
tc::{PusTcCreator, PusTcSecondaryHeader},
|
||||
WritablePusPacket,
|
||||
},
|
||||
SpHeader,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::requests::CompositeRequest;
|
||||
|
||||
use super::*;
|
||||
|
||||
// Testbench dedicated to the testing of [PusReplyHandler]s
|
||||
pub struct ReplyHandlerTestbench<
|
||||
ReplyHandler: PusReplyHandler<ActiveRequestInfo, Reply, Error = EcssTmtcError>,
|
||||
ActiveRequestInfo: ActiveRequest,
|
||||
Reply,
|
||||
> {
|
||||
pub id: ComponentId,
|
||||
pub verif_reporter: TestVerificationReporter,
|
||||
pub reply_handler: ReplyHandler,
|
||||
pub tm_receiver: mpsc::Receiver<PacketAsVec>,
|
||||
pub default_timeout: Duration,
|
||||
tm_sender: MpscTmAsVecSender,
|
||||
phantom: std::marker::PhantomData<(ActiveRequestInfo, Reply)>,
|
||||
}
|
||||
|
||||
impl<
|
||||
ReplyHandler: PusReplyHandler<ActiveRequestInfo, Reply, Error = EcssTmtcError>,
|
||||
ActiveRequestInfo: ActiveRequest,
|
||||
Reply,
|
||||
> ReplyHandlerTestbench<ReplyHandler, ActiveRequestInfo, Reply>
|
||||
{
|
||||
pub fn new(owner_id: ComponentId, reply_handler: ReplyHandler) -> Self {
|
||||
let test_verif_reporter = TestVerificationReporter::new(owner_id);
|
||||
let (tm_sender, tm_receiver) = mpsc::channel();
|
||||
Self {
|
||||
id: TEST_COMPONENT_ID_0.raw(),
|
||||
verif_reporter: test_verif_reporter,
|
||||
reply_handler,
|
||||
default_timeout: Duration::from_secs(30),
|
||||
tm_sender,
|
||||
tm_receiver,
|
||||
phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_tc(
|
||||
&mut self,
|
||||
apid: u11,
|
||||
apid_target: u21,
|
||||
time_stamp: &[u8],
|
||||
) -> (verification::RequestId, ActivePusRequestStd) {
|
||||
let sp_header = SpHeader::new_from_apid(apid);
|
||||
let sec_header_dummy = PusTcSecondaryHeader::new_simple(MessageTypeId::new(0, 0));
|
||||
let init = self.verif_reporter.start_verification(&PusTcCreator::new(
|
||||
sp_header,
|
||||
sec_header_dummy,
|
||||
&[],
|
||||
CreatorConfig::default(),
|
||||
));
|
||||
let accepted = self
|
||||
.verif_reporter
|
||||
.acceptance_success(&self.tm_sender, init, time_stamp)
|
||||
.expect("acceptance failed");
|
||||
let started = self
|
||||
.verif_reporter
|
||||
.start_success(&self.tm_sender, accepted, time_stamp)
|
||||
.expect("start failed");
|
||||
(
|
||||
started.request_id(),
|
||||
ActivePusRequestStd::new(
|
||||
UniqueApidTargetId::new(apid, apid_target).raw(),
|
||||
started,
|
||||
self.default_timeout,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn handle_reply(
|
||||
&mut self,
|
||||
reply: &GenericMessage<Reply>,
|
||||
active_request: &ActiveRequestInfo,
|
||||
time_stamp: &[u8],
|
||||
) -> Result<bool, ReplyHandler::Error> {
|
||||
self.reply_handler.handle_reply(
|
||||
reply,
|
||||
active_request,
|
||||
&self.tm_sender,
|
||||
&self.verif_reporter,
|
||||
time_stamp,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn handle_unrequested_reply(
|
||||
&mut self,
|
||||
reply: &GenericMessage<Reply>,
|
||||
) -> Result<(), ReplyHandler::Error> {
|
||||
self.reply_handler
|
||||
.handle_unrequested_reply(reply, &self.tm_sender)
|
||||
}
|
||||
pub fn handle_request_timeout(
|
||||
&mut self,
|
||||
active_request_info: &ActiveRequestInfo,
|
||||
time_stamp: &[u8],
|
||||
) -> Result<(), ReplyHandler::Error> {
|
||||
self.reply_handler.handle_request_timeout(
|
||||
active_request_info,
|
||||
&self.tm_sender,
|
||||
&self.verif_reporter,
|
||||
time_stamp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct DummySender {}
|
||||
|
||||
/// Dummy sender component which does nothing on the [Self::send_tm] call.
|
||||
///
|
||||
/// Useful for unit tests.
|
||||
impl EcssTmSender for DummySender {
|
||||
fn send_tm(&self, _source_id: ComponentId, _tm: PusTmVariant) -> Result<(), EcssTmtcError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Testbench dedicated to the testing of [PusTcToRequestConverter]s
|
||||
pub struct PusConverterTestbench<
|
||||
Converter: PusTcToRequestConverter<ActiveRequestInfo, Request, Error = GenericConversionError>,
|
||||
ActiveRequestInfo: ActiveRequest,
|
||||
Request,
|
||||
> {
|
||||
pub id: ComponentId,
|
||||
pub verif_reporter: TestVerificationReporter,
|
||||
pub converter: Converter,
|
||||
dummy_sender: DummySender,
|
||||
current_request_id: Option<verification::RequestId>,
|
||||
current_packet: Option<Vec<u8>>,
|
||||
phantom: std::marker::PhantomData<(ActiveRequestInfo, Request)>,
|
||||
}
|
||||
|
||||
impl<
|
||||
Converter: PusTcToRequestConverter<ActiveRequestInfo, Request, Error = GenericConversionError>,
|
||||
ActiveRequestInfo: ActiveRequest,
|
||||
Request,
|
||||
> PusConverterTestbench<Converter, ActiveRequestInfo, Request>
|
||||
{
|
||||
pub fn new(owner_id: ComponentId, converter: Converter) -> Self {
|
||||
let test_verif_reporter = TestVerificationReporter::new(owner_id);
|
||||
Self {
|
||||
id: owner_id,
|
||||
verif_reporter: test_verif_reporter,
|
||||
converter,
|
||||
dummy_sender: DummySender::default(),
|
||||
current_request_id: None,
|
||||
current_packet: None,
|
||||
phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_tc(&mut self, tc: &PusTcCreator) -> VerificationToken<TcStateAccepted> {
|
||||
let token = self.verif_reporter.start_verification(tc);
|
||||
self.current_request_id = Some(verification::RequestId::new(tc));
|
||||
self.current_packet = Some(tc.to_vec().unwrap());
|
||||
self.verif_reporter
|
||||
.acceptance_success(&self.dummy_sender, token, &[])
|
||||
.expect("acceptance failed")
|
||||
}
|
||||
|
||||
pub fn request_id(&self) -> Option<verification::RequestId> {
|
||||
self.current_request_id
|
||||
}
|
||||
|
||||
pub fn convert(
|
||||
&mut self,
|
||||
token: VerificationToken<TcStateAccepted>,
|
||||
time_stamp: &[u8],
|
||||
expected_apid: u11,
|
||||
expected_apid_target: u21,
|
||||
) -> Result<(ActiveRequestInfo, Request), Converter::Error> {
|
||||
if self.current_packet.is_none() {
|
||||
return Err(GenericConversionError::InvalidAppData(
|
||||
"call add_tc first".to_string(),
|
||||
));
|
||||
}
|
||||
let current_packet = self.current_packet.take().unwrap();
|
||||
let tc_reader = PusTcReader::new(¤t_packet).unwrap();
|
||||
let (active_info, request) = self.converter.convert(
|
||||
token,
|
||||
&tc_reader,
|
||||
&self.dummy_sender,
|
||||
&self.verif_reporter,
|
||||
time_stamp,
|
||||
)?;
|
||||
assert_eq!(
|
||||
active_info.token().request_id(),
|
||||
self.request_id().expect("no request id is set")
|
||||
);
|
||||
assert_eq!(
|
||||
active_info.target_id(),
|
||||
UniqueApidTargetId::new(expected_apid, expected_apid_target).raw()
|
||||
);
|
||||
Ok((active_info, request))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TargetedPusRequestTestbench<
|
||||
RequestConverter: PusTcToRequestConverter<ActiveRequestInfo, RequestType, Error = GenericConversionError>,
|
||||
ReplyHandler: PusReplyHandler<ActiveRequestInfo, ReplyType, Error = EcssTmtcError>,
|
||||
ActiveRequestMapInstance: ActiveRequestStore<ActiveRequestInfo>,
|
||||
ActiveRequestInfo: ActiveRequest,
|
||||
RequestType,
|
||||
ReplyType,
|
||||
> {
|
||||
pub service: PusTargetedRequestService<
|
||||
MpscTcReceiver,
|
||||
TestVerificationReporter,
|
||||
RequestConverter,
|
||||
ReplyHandler,
|
||||
ActiveRequestMapInstance,
|
||||
ActiveRequestInfo,
|
||||
RequestType,
|
||||
ReplyType,
|
||||
>,
|
||||
pub request_id: Option<RequestId>,
|
||||
pub tm_funnel_rx: mpsc::Receiver<PacketAsVec>,
|
||||
pub pus_packet_tx: mpsc::Sender<EcssTcAndToken>,
|
||||
pub reply_tx: mpsc::Sender<GenericMessage<ReplyType>>,
|
||||
pub request_rx: mpsc::Receiver<GenericMessage<CompositeRequest>>,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,441 @@
|
||||
use arbitrary_int::traits::Integer as _;
|
||||
use arbitrary_int::u14;
|
||||
use derive_new::new;
|
||||
use satrs::mode_tree::{ModeNode, ModeParent};
|
||||
use satrs::spacepackets::ecss::{CreatorConfig, MessageTypeId};
|
||||
use example_std::ids;
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::requests::GenericRequestRouter;
|
||||
use crate::tmtc::sender::TmTcSender;
|
||||
use satrs::pus::verification::VerificationReporter;
|
||||
use satrs::pus::{
|
||||
DefaultActiveRequestMap, EcssTcAndToken, EcssTcCacher, MpscTcReceiver, PusPacketHandlingError,
|
||||
PusServiceHelper,
|
||||
};
|
||||
use satrs::request::GenericMessage;
|
||||
use satrs::{
|
||||
mode::{ModeAndSubmode, ModeReply, ModeRequest},
|
||||
pus::{
|
||||
mode::Subservice,
|
||||
verification::{
|
||||
self, FailParams, TcStateAccepted, TcStateStarted, VerificationReportingProvider,
|
||||
VerificationToken,
|
||||
},
|
||||
ActivePusRequestStd, ActiveRequest, EcssTmSender, EcssTmtcError, GenericConversionError,
|
||||
PusReplyHandler, PusTcToRequestConverter, PusTmVariant,
|
||||
},
|
||||
request::UniqueApidTargetId,
|
||||
spacepackets::{
|
||||
ecss::{
|
||||
tc::PusTcReader,
|
||||
tm::{PusTmCreator, PusTmSecondaryHeader},
|
||||
PusPacket,
|
||||
},
|
||||
SpHeader,
|
||||
},
|
||||
ComponentId,
|
||||
};
|
||||
use example_std::config::{mode_err, tmtc_err, CustomPusServiceId};
|
||||
|
||||
use super::{
|
||||
create_verification_reporter, generic_pus_request_timeout_handler, HandlingStatus,
|
||||
PusTargetedRequestService, TargetedPusService,
|
||||
};
|
||||
|
||||
#[derive(new)]
|
||||
pub struct ModeReplyHandler {
|
||||
owner_id: ComponentId,
|
||||
}
|
||||
|
||||
impl PusReplyHandler<ActivePusRequestStd, ModeReply> for ModeReplyHandler {
|
||||
type Error = EcssTmtcError;
|
||||
|
||||
fn handle_unrequested_reply(
|
||||
&mut self,
|
||||
reply: &GenericMessage<ModeReply>,
|
||||
_tm_sender: &impl EcssTmSender,
|
||||
) -> Result<(), Self::Error> {
|
||||
log::warn!("received unexpected reply for mode service 5: {reply:?}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_reply(
|
||||
&mut self,
|
||||
reply: &GenericMessage<ModeReply>,
|
||||
active_request: &ActivePusRequestStd,
|
||||
tm_sender: &impl EcssTmSender,
|
||||
verification_handler: &impl VerificationReportingProvider,
|
||||
time_stamp: &[u8],
|
||||
) -> Result<bool, Self::Error> {
|
||||
let started_token: VerificationToken<TcStateStarted> = active_request
|
||||
.token()
|
||||
.try_into()
|
||||
.expect("invalid token state");
|
||||
match reply.message {
|
||||
ModeReply::ModeReply(mode_reply) => {
|
||||
let mut source_data: [u8; 12] = [0; 12];
|
||||
mode_reply
|
||||
.write_to_be_bytes(&mut source_data)
|
||||
.expect("writing mode reply failed");
|
||||
let req_id = verification::RequestId::from(reply.request_id());
|
||||
let sp_header = SpHeader::new_for_unseg_tm(req_id.packet_id().apid(), u14::ZERO, 0);
|
||||
let sec_header = PusTmSecondaryHeader::new(
|
||||
MessageTypeId::new(200, Subservice::TmModeReply as u8),
|
||||
0,
|
||||
0,
|
||||
time_stamp,
|
||||
);
|
||||
let pus_tm = PusTmCreator::new(
|
||||
sp_header,
|
||||
sec_header,
|
||||
&source_data,
|
||||
CreatorConfig::default(),
|
||||
);
|
||||
tm_sender.send_tm(self.owner_id, PusTmVariant::Direct(pus_tm))?;
|
||||
verification_handler.completion_success(tm_sender, started_token, time_stamp)?;
|
||||
}
|
||||
ModeReply::CantReachMode(error_code) => {
|
||||
verification_handler.completion_failure(
|
||||
tm_sender,
|
||||
started_token,
|
||||
FailParams::new(time_stamp, &error_code, &[]),
|
||||
)?;
|
||||
}
|
||||
ModeReply::WrongMode { expected, reached } => {
|
||||
let mut error_info: [u8; 24] = [0; 24];
|
||||
let mut written_len = expected
|
||||
.write_to_be_bytes(&mut error_info[0..ModeAndSubmode::RAW_LEN])
|
||||
.expect("writing expected mode failed");
|
||||
written_len += reached
|
||||
.write_to_be_bytes(&mut error_info[ModeAndSubmode::RAW_LEN..])
|
||||
.expect("writing reached mode failed");
|
||||
verification_handler.completion_failure(
|
||||
tm_sender,
|
||||
started_token,
|
||||
FailParams::new(
|
||||
time_stamp,
|
||||
&mode_err::WRONG_MODE,
|
||||
&error_info[..written_len],
|
||||
),
|
||||
)?;
|
||||
}
|
||||
ModeReply::ModeInfo(_mode_and_submode) => (),
|
||||
};
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn handle_request_timeout(
|
||||
&mut self,
|
||||
active_request: &ActivePusRequestStd,
|
||||
tm_sender: &impl EcssTmSender,
|
||||
verification_handler: &impl VerificationReportingProvider,
|
||||
time_stamp: &[u8],
|
||||
) -> Result<(), Self::Error> {
|
||||
generic_pus_request_timeout_handler(
|
||||
tm_sender,
|
||||
active_request,
|
||||
verification_handler,
|
||||
time_stamp,
|
||||
"HK",
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ModeRequestConverter {}
|
||||
|
||||
impl PusTcToRequestConverter<ActivePusRequestStd, ModeRequest> for ModeRequestConverter {
|
||||
type Error = GenericConversionError;
|
||||
|
||||
fn convert(
|
||||
&mut self,
|
||||
token: VerificationToken<TcStateAccepted>,
|
||||
tc: &PusTcReader,
|
||||
tm_sender: &(impl EcssTmSender + ?Sized),
|
||||
verif_reporter: &impl VerificationReportingProvider,
|
||||
time_stamp: &[u8],
|
||||
) -> Result<(ActivePusRequestStd, ModeRequest), Self::Error> {
|
||||
let subservice = tc.message_subtype_id();
|
||||
let user_data = tc.user_data();
|
||||
let not_enough_app_data = |expected: usize| {
|
||||
verif_reporter
|
||||
.start_failure(
|
||||
tm_sender,
|
||||
token,
|
||||
FailParams::new_no_fail_data(time_stamp, &tmtc_err::NOT_ENOUGH_APP_DATA),
|
||||
)
|
||||
.expect("Sending start failure failed");
|
||||
Err(GenericConversionError::NotEnoughAppData {
|
||||
expected,
|
||||
found: user_data.len(),
|
||||
})
|
||||
};
|
||||
if user_data.len() < core::mem::size_of::<u32>() {
|
||||
return not_enough_app_data(4);
|
||||
}
|
||||
let target_id_and_apid = UniqueApidTargetId::from_pus_tc(tc).unwrap();
|
||||
let active_request =
|
||||
ActivePusRequestStd::new(target_id_and_apid.into(), token, Duration::from_secs(30));
|
||||
let subservice_typed = Subservice::try_from(subservice);
|
||||
let invalid_subservice = || {
|
||||
// Invalid subservice
|
||||
verif_reporter
|
||||
.start_failure(
|
||||
tm_sender,
|
||||
token,
|
||||
FailParams::new_no_fail_data(time_stamp, &tmtc_err::INVALID_PUS_SUBSERVICE),
|
||||
)
|
||||
.expect("Sending start failure failed");
|
||||
Err(GenericConversionError::InvalidSubservice(subservice))
|
||||
};
|
||||
if subservice_typed.is_err() {
|
||||
return invalid_subservice();
|
||||
}
|
||||
let subservice_typed = subservice_typed.unwrap();
|
||||
match subservice_typed {
|
||||
Subservice::TcSetMode => {
|
||||
if user_data.len() < core::mem::size_of::<u32>() + ModeAndSubmode::RAW_LEN {
|
||||
return not_enough_app_data(4 + ModeAndSubmode::RAW_LEN);
|
||||
}
|
||||
let mode_and_submode = ModeAndSubmode::from_be_bytes(&tc.user_data()[4..])
|
||||
.expect("mode and submode extraction failed");
|
||||
Ok((
|
||||
active_request,
|
||||
ModeRequest::SetMode {
|
||||
mode_and_submode,
|
||||
forced: false,
|
||||
},
|
||||
))
|
||||
}
|
||||
Subservice::TcReadMode => Ok((active_request, ModeRequest::ReadMode)),
|
||||
Subservice::TcAnnounceMode => Ok((active_request, ModeRequest::AnnounceMode)),
|
||||
Subservice::TcAnnounceModeRecursive => {
|
||||
Ok((active_request, ModeRequest::AnnounceModeRecursive))
|
||||
}
|
||||
_ => invalid_subservice(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_mode_service(
|
||||
tm_sender: TmTcSender,
|
||||
tc_in_mem_converter: EcssTcCacher,
|
||||
pus_action_rx: mpsc::Receiver<EcssTcAndToken>,
|
||||
mode_router: GenericRequestRouter,
|
||||
reply_receiver: mpsc::Receiver<GenericMessage<ModeReply>>,
|
||||
) -> ModeServiceWrapper {
|
||||
let mode_request_handler = PusTargetedRequestService::new(
|
||||
PusServiceHelper::new(
|
||||
ids::generic_pus::PUS_MODE.id(),
|
||||
pus_action_rx,
|
||||
tm_sender,
|
||||
create_verification_reporter(
|
||||
ids::generic_pus::PUS_MODE.id(),
|
||||
ids::generic_pus::PUS_MODE.apid,
|
||||
),
|
||||
tc_in_mem_converter,
|
||||
),
|
||||
ModeRequestConverter::default(),
|
||||
DefaultActiveRequestMap::default(),
|
||||
ModeReplyHandler::new(ids::generic_pus::PUS_MODE.id()),
|
||||
mode_router,
|
||||
reply_receiver,
|
||||
);
|
||||
ModeServiceWrapper {
|
||||
service: mode_request_handler,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ModeServiceWrapper {
|
||||
pub(crate) service: PusTargetedRequestService<
|
||||
MpscTcReceiver,
|
||||
VerificationReporter,
|
||||
ModeRequestConverter,
|
||||
ModeReplyHandler,
|
||||
DefaultActiveRequestMap<ActivePusRequestStd>,
|
||||
ActivePusRequestStd,
|
||||
ModeRequest,
|
||||
ModeReply,
|
||||
>,
|
||||
}
|
||||
|
||||
impl ModeNode for ModeServiceWrapper {
|
||||
fn id(&self) -> ComponentId {
|
||||
self.service.service_helper.id()
|
||||
}
|
||||
}
|
||||
|
||||
impl ModeParent for ModeServiceWrapper {
|
||||
type Sender = mpsc::SyncSender<GenericMessage<ModeRequest>>;
|
||||
|
||||
fn add_mode_child(&mut self, id: ComponentId, request_sender: Self::Sender) {
|
||||
self.service
|
||||
.request_router
|
||||
.mode_router_map
|
||||
.insert(id, request_sender);
|
||||
}
|
||||
}
|
||||
|
||||
impl TargetedPusService for ModeServiceWrapper {
|
||||
const SERVICE_ID: u8 = CustomPusServiceId::Mode as u8;
|
||||
const SERVICE_STR: &'static str = "mode";
|
||||
|
||||
delegate::delegate! {
|
||||
to self.service {
|
||||
fn poll_and_handle_next_tc(
|
||||
&mut self,
|
||||
time_stamp: &[u8],
|
||||
) -> Result<HandlingStatus, PusPacketHandlingError>;
|
||||
|
||||
fn poll_and_handle_next_reply(
|
||||
&mut self,
|
||||
time_stamp: &[u8],
|
||||
) -> Result<HandlingStatus, EcssTmtcError>;
|
||||
|
||||
fn check_for_request_timeouts(&mut self);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use arbitrary_int::traits::Integer;
|
||||
use arbitrary_int::u14;
|
||||
use satrs::pus::test_util::{TEST_APID, TEST_COMPONENT_ID_0, TEST_UNIQUE_ID_0};
|
||||
use satrs::request::MessageMetadata;
|
||||
use satrs::spacepackets::ecss::{CreatorConfig, MessageTypeId};
|
||||
use satrs::{
|
||||
mode::{ModeAndSubmode, ModeReply, ModeRequest},
|
||||
pus::mode::Subservice,
|
||||
request::GenericMessage,
|
||||
spacepackets::{
|
||||
ecss::tc::{PusTcCreator, PusTcSecondaryHeader},
|
||||
SpHeader,
|
||||
},
|
||||
};
|
||||
use example_std::config::tmtc_err;
|
||||
|
||||
use crate::pus::{
|
||||
mode::ModeReplyHandler,
|
||||
tests::{PusConverterTestbench, ReplyHandlerTestbench},
|
||||
};
|
||||
|
||||
use super::ModeRequestConverter;
|
||||
|
||||
#[test]
|
||||
fn mode_converter_read_mode_request() {
|
||||
let mut testbench =
|
||||
PusConverterTestbench::new(TEST_COMPONENT_ID_0.id(), ModeRequestConverter::default());
|
||||
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, u14::ZERO, 0);
|
||||
let sec_header =
|
||||
PusTcSecondaryHeader::new_simple(MessageTypeId::new(200, Subservice::TcReadMode as u8));
|
||||
let mut app_data: [u8; 4] = [0; 4];
|
||||
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_0.as_u32().to_be_bytes());
|
||||
let tc = PusTcCreator::new(sp_header, sec_header, &app_data, CreatorConfig::default());
|
||||
let token = testbench.add_tc(&tc);
|
||||
let (_active_req, req) = testbench
|
||||
.convert(token, &[], TEST_APID, TEST_UNIQUE_ID_0)
|
||||
.expect("conversion has failed");
|
||||
assert_eq!(req, ModeRequest::ReadMode);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mode_converter_set_mode_request() {
|
||||
let mut testbench =
|
||||
PusConverterTestbench::new(TEST_COMPONENT_ID_0.id(), ModeRequestConverter::default());
|
||||
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, u14::ZERO, 0);
|
||||
let sec_header =
|
||||
PusTcSecondaryHeader::new_simple(MessageTypeId::new(200, Subservice::TcSetMode as u8));
|
||||
let mut app_data: [u8; 4 + ModeAndSubmode::RAW_LEN] = [0; 4 + ModeAndSubmode::RAW_LEN];
|
||||
let mode_and_submode = ModeAndSubmode::new(2, 1);
|
||||
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_0.as_u32().to_be_bytes());
|
||||
mode_and_submode
|
||||
.write_to_be_bytes(&mut app_data[4..])
|
||||
.unwrap();
|
||||
let tc = PusTcCreator::new(sp_header, sec_header, &app_data, CreatorConfig::default());
|
||||
let token = testbench.add_tc(&tc);
|
||||
let (_active_req, req) = testbench
|
||||
.convert(token, &[], TEST_APID, TEST_UNIQUE_ID_0)
|
||||
.expect("conversion has failed");
|
||||
assert_eq!(
|
||||
req,
|
||||
ModeRequest::SetMode {
|
||||
mode_and_submode,
|
||||
forced: false
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mode_converter_announce_mode() {
|
||||
let mut testbench =
|
||||
PusConverterTestbench::new(TEST_COMPONENT_ID_0.id(), ModeRequestConverter::default());
|
||||
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, u14::ZERO, 0);
|
||||
let sec_header = PusTcSecondaryHeader::new_simple(MessageTypeId::new(
|
||||
200,
|
||||
Subservice::TcAnnounceMode as u8,
|
||||
));
|
||||
let mut app_data: [u8; 4] = [0; 4];
|
||||
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_0.as_u32().to_be_bytes());
|
||||
let tc = PusTcCreator::new(sp_header, sec_header, &app_data, CreatorConfig::default());
|
||||
let token = testbench.add_tc(&tc);
|
||||
let (_active_req, req) = testbench
|
||||
.convert(token, &[], TEST_APID, TEST_UNIQUE_ID_0)
|
||||
.expect("conversion has failed");
|
||||
assert_eq!(req, ModeRequest::AnnounceMode);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mode_converter_announce_mode_recursively() {
|
||||
let mut testbench =
|
||||
PusConverterTestbench::new(TEST_COMPONENT_ID_0.id(), ModeRequestConverter::default());
|
||||
let sp_header = SpHeader::new_for_unseg_tc(TEST_APID, u14::ZERO, 0);
|
||||
let sec_header = PusTcSecondaryHeader::new_simple(MessageTypeId::new(
|
||||
200,
|
||||
Subservice::TcAnnounceModeRecursive as u8,
|
||||
));
|
||||
let mut app_data: [u8; 4] = [0; 4];
|
||||
app_data[0..4].copy_from_slice(&TEST_UNIQUE_ID_0.as_u32().to_be_bytes());
|
||||
let tc = PusTcCreator::new(sp_header, sec_header, &app_data, CreatorConfig::default());
|
||||
let token = testbench.add_tc(&tc);
|
||||
let (_active_req, req) = testbench
|
||||
.convert(token, &[], TEST_APID, TEST_UNIQUE_ID_0)
|
||||
.expect("conversion has failed");
|
||||
assert_eq!(req, ModeRequest::AnnounceModeRecursive);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reply_handling_unrequested_reply() {
|
||||
let mut testbench = ReplyHandlerTestbench::new(
|
||||
TEST_COMPONENT_ID_0.id(),
|
||||
ModeReplyHandler::new(TEST_COMPONENT_ID_0.id()),
|
||||
);
|
||||
let mode_reply = ModeReply::ModeReply(ModeAndSubmode::new(5, 1));
|
||||
let unrequested_reply =
|
||||
GenericMessage::new(MessageMetadata::new(10_u32, 15_u32), mode_reply);
|
||||
// Right now this function does not do a lot. We simply check that it does not panic or do
|
||||
// weird stuff.
|
||||
let result = testbench.handle_unrequested_reply(&unrequested_reply);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reply_handling_reply_timeout() {
|
||||
let mut testbench = ReplyHandlerTestbench::new(
|
||||
TEST_COMPONENT_ID_0.id(),
|
||||
ModeReplyHandler::new(TEST_COMPONENT_ID_0.id()),
|
||||
);
|
||||
let (req_id, active_request) = testbench.add_tc(TEST_APID, TEST_UNIQUE_ID_0, &[]);
|
||||
let result = testbench.handle_request_timeout(&active_request, &[]);
|
||||
assert!(result.is_ok());
|
||||
testbench.verif_reporter.assert_completion_failure(
|
||||
TEST_COMPONENT_ID_0.raw(),
|
||||
req_id,
|
||||
None,
|
||||
tmtc_err::REQUEST_TIMEOUT.raw() as u64,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::pus::create_verification_reporter;
|
||||
use crate::tmtc::sender::TmTcSender;
|
||||
use log::info;
|
||||
use satrs::pool::{PoolProvider, StaticMemoryPool};
|
||||
use satrs::pus::scheduler::{PusSchedulerAlloc, TcInfo};
|
||||
use satrs::pus::scheduler_srv::PusSchedServiceHandler;
|
||||
use satrs::pus::verification::VerificationReporter;
|
||||
use satrs::pus::{
|
||||
DirectPusPacketHandlerResult, EcssTcAndToken, EcssTcCacher, MpscTcReceiver,
|
||||
PartialPusHandlingError, PusServiceHelper,
|
||||
};
|
||||
use satrs::spacepackets::ecss::PusServiceId;
|
||||
use satrs::tmtc::{PacketAsVec, PacketInPool, PacketSenderWithSharedPool};
|
||||
use satrs::ComponentId;
|
||||
use example_std::ids::sched::PUS_SCHED;
|
||||
|
||||
use super::{DirectPusService, HandlingStatus};
|
||||
|
||||
pub trait TcReleaseProvider {
|
||||
fn release(&mut self, sender_id: ComponentId, enabled: bool, info: &TcInfo, tc: &[u8]) -> bool;
|
||||
}
|
||||
|
||||
impl TcReleaseProvider for PacketSenderWithSharedPool {
|
||||
fn release(
|
||||
&mut self,
|
||||
sender_id: ComponentId,
|
||||
enabled: bool,
|
||||
_info: &TcInfo,
|
||||
tc: &[u8],
|
||||
) -> bool {
|
||||
if enabled {
|
||||
let shared_pool = self.shared_pool.get_mut();
|
||||
// Transfer TC from scheduler TC pool to shared TC pool.
|
||||
let released_tc_addr = shared_pool
|
||||
.0
|
||||
.write()
|
||||
.expect("locking pool failed")
|
||||
.add(tc)
|
||||
.expect("adding TC to shared pool failed");
|
||||
self.sender
|
||||
.send(PacketInPool::new(sender_id, released_tc_addr))
|
||||
.expect("sending TC to TC source failed");
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl TcReleaseProvider for mpsc::SyncSender<PacketAsVec> {
|
||||
fn release(
|
||||
&mut self,
|
||||
sender_id: ComponentId,
|
||||
enabled: bool,
|
||||
_info: &TcInfo,
|
||||
tc: &[u8],
|
||||
) -> bool {
|
||||
if enabled {
|
||||
// Send released TC to centralized TC source.
|
||||
self.send(PacketAsVec::new(sender_id, tc.to_vec()))
|
||||
.expect("sending TC to TC source failed");
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub enum TcReleaser {
|
||||
Static(PacketSenderWithSharedPool),
|
||||
Heap(mpsc::SyncSender<PacketAsVec>),
|
||||
}
|
||||
|
||||
impl TcReleaseProvider for TcReleaser {
|
||||
fn release(&mut self, sender_id: ComponentId, enabled: bool, info: &TcInfo, tc: &[u8]) -> bool {
|
||||
match self {
|
||||
TcReleaser::Static(sender) => sender.release(sender_id, enabled, info, tc),
|
||||
TcReleaser::Heap(sender) => sender.release(sender_id, enabled, info, tc),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SchedulingServiceWrapper {
|
||||
pub pus_11_handler: PusSchedServiceHandler<
|
||||
MpscTcReceiver,
|
||||
TmTcSender,
|
||||
EcssTcCacher,
|
||||
VerificationReporter,
|
||||
PusSchedulerAlloc,
|
||||
>,
|
||||
pub sched_tc_pool: StaticMemoryPool,
|
||||
pub releaser_buf: [u8; 4096],
|
||||
pub tc_releaser: TcReleaser,
|
||||
}
|
||||
|
||||
impl DirectPusService for SchedulingServiceWrapper {
|
||||
const SERVICE_ID: u8 = PusServiceId::Verification as u8;
|
||||
|
||||
const SERVICE_STR: &'static str = "verification";
|
||||
|
||||
fn poll_and_handle_next_tc(&mut self, time_stamp: &[u8]) -> HandlingStatus {
|
||||
let error_handler = |partial_error: &PartialPusHandlingError| {
|
||||
log::warn!(
|
||||
"PUS {}({}) partial error: {:?}",
|
||||
Self::SERVICE_ID,
|
||||
Self::SERVICE_STR,
|
||||
partial_error
|
||||
);
|
||||
};
|
||||
|
||||
let result = self.pus_11_handler.poll_and_handle_next_tc(
|
||||
error_handler,
|
||||
time_stamp,
|
||||
&mut self.sched_tc_pool,
|
||||
);
|
||||
if let Err(e) = result {
|
||||
log::warn!(
|
||||
"PUS {}({}) error: {:?}",
|
||||
Self::SERVICE_ID,
|
||||
Self::SERVICE_STR,
|
||||
e
|
||||
);
|
||||
// To avoid permanent loops on continuous errors.
|
||||
return HandlingStatus::Empty;
|
||||
}
|
||||
match result.unwrap() {
|
||||
DirectPusPacketHandlerResult::Handled(handling_status) => return handling_status,
|
||||
DirectPusPacketHandlerResult::CustomSubservice(subservice, _) => {
|
||||
log::warn!(
|
||||
"PUS {}({}) subservice {} not implemented",
|
||||
Self::SERVICE_ID,
|
||||
Self::SERVICE_STR,
|
||||
subservice
|
||||
);
|
||||
}
|
||||
DirectPusPacketHandlerResult::SubserviceNotImplemented(subservice, _) => {
|
||||
log::warn!(
|
||||
"PUS {}({}) subservice {} not implemented",
|
||||
Self::SERVICE_ID,
|
||||
Self::SERVICE_STR,
|
||||
subservice
|
||||
);
|
||||
}
|
||||
}
|
||||
HandlingStatus::HandledOne
|
||||
}
|
||||
}
|
||||
|
||||
impl SchedulingServiceWrapper {
|
||||
pub fn release_tcs(&mut self) {
|
||||
let id = self.pus_11_handler.service_helper.id();
|
||||
let releaser = |enabled: bool, info: &TcInfo, tc: &[u8]| -> bool {
|
||||
self.tc_releaser.release(id, enabled, info, tc)
|
||||
};
|
||||
|
||||
self.pus_11_handler
|
||||
.scheduler_mut()
|
||||
.update_time_from_now()
|
||||
.unwrap();
|
||||
let released_tcs = self
|
||||
.pus_11_handler
|
||||
.scheduler_mut()
|
||||
.release_telecommands_with_buffer(
|
||||
releaser,
|
||||
&mut self.sched_tc_pool,
|
||||
&mut self.releaser_buf,
|
||||
)
|
||||
.expect("releasing TCs failed");
|
||||
if released_tcs > 0 {
|
||||
info!("{released_tcs} TC(s) released from scheduler");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_scheduler_service(
|
||||
tm_sender: TmTcSender,
|
||||
tc_in_mem_converter: EcssTcCacher,
|
||||
tc_releaser: TcReleaser,
|
||||
pus_sched_rx: mpsc::Receiver<EcssTcAndToken>,
|
||||
sched_tc_pool: StaticMemoryPool,
|
||||
) -> SchedulingServiceWrapper {
|
||||
let scheduler = PusSchedulerAlloc::new_with_current_init_time(Duration::from_secs(5))
|
||||
.expect("Creating PUS Scheduler failed");
|
||||
let pus_11_handler = PusSchedServiceHandler::new(
|
||||
PusServiceHelper::new(
|
||||
PUS_SCHED.id(),
|
||||
pus_sched_rx,
|
||||
tm_sender,
|
||||
create_verification_reporter(PUS_SCHED.id(), PUS_SCHED.apid),
|
||||
tc_in_mem_converter,
|
||||
),
|
||||
scheduler,
|
||||
);
|
||||
SchedulingServiceWrapper {
|
||||
pus_11_handler,
|
||||
sched_tc_pool,
|
||||
releaser_buf: [0; 4096],
|
||||
tc_releaser,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
use crate::pus::mode::ModeServiceWrapper;
|
||||
use derive_new::new;
|
||||
use satrs::spacepackets::time::{cds, TimeWriter};
|
||||
|
||||
use super::{
|
||||
action::ActionServiceWrapper, event::EventServiceWrapper, hk::HkServiceWrapper,
|
||||
scheduler::SchedulingServiceWrapper, test::TestCustomServiceWrapper, DirectPusService,
|
||||
HandlingStatus, TargetedPusService,
|
||||
};
|
||||
|
||||
#[derive(new)]
|
||||
pub struct PusStack {
|
||||
pub test_srv: TestCustomServiceWrapper,
|
||||
pub hk_srv_wrapper: HkServiceWrapper,
|
||||
pub event_srv: EventServiceWrapper,
|
||||
pub action_srv_wrapper: ActionServiceWrapper,
|
||||
pub schedule_srv: SchedulingServiceWrapper,
|
||||
pub mode_srv: ModeServiceWrapper,
|
||||
}
|
||||
|
||||
impl PusStack {
|
||||
pub fn periodic_operation(&mut self) {
|
||||
// Release all telecommands which reached their release time before calling the service
|
||||
// handlers.
|
||||
self.schedule_srv.release_tcs();
|
||||
let timestamp = cds::CdsTime::now_with_u16_days()
|
||||
.expect("time stamp generation error")
|
||||
.to_vec()
|
||||
.unwrap();
|
||||
let mut loop_count = 0_u32;
|
||||
// Hot loop which will run continuously until all request and reply handling is done.
|
||||
loop {
|
||||
let mut nothing_to_do = true;
|
||||
Self::direct_service_checker(&mut self.test_srv, ×tamp, &mut nothing_to_do);
|
||||
Self::direct_service_checker(&mut self.schedule_srv, ×tamp, &mut nothing_to_do);
|
||||
Self::direct_service_checker(&mut self.event_srv, ×tamp, &mut nothing_to_do);
|
||||
Self::targeted_service_checker(
|
||||
&mut self.action_srv_wrapper,
|
||||
×tamp,
|
||||
&mut nothing_to_do,
|
||||
);
|
||||
Self::targeted_service_checker(
|
||||
&mut self.hk_srv_wrapper,
|
||||
×tamp,
|
||||
&mut nothing_to_do,
|
||||
);
|
||||
Self::targeted_service_checker(&mut self.mode_srv, ×tamp, &mut nothing_to_do);
|
||||
if nothing_to_do {
|
||||
// Timeout checking is only done once.
|
||||
self.action_srv_wrapper.check_for_request_timeouts();
|
||||
self.hk_srv_wrapper.check_for_request_timeouts();
|
||||
self.mode_srv.check_for_request_timeouts();
|
||||
break;
|
||||
}
|
||||
// Safety mechanism to avoid infinite loops.
|
||||
loop_count += 1;
|
||||
if loop_count >= 500 {
|
||||
log::warn!("reached PUS stack loop count 500, breaking");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn direct_service_checker<S: DirectPusService>(
|
||||
service: &mut S,
|
||||
timestamp: &[u8],
|
||||
nothing_to_do: &mut bool,
|
||||
) {
|
||||
let handling_status = service.poll_and_handle_next_tc(timestamp);
|
||||
if handling_status == HandlingStatus::HandledOne {
|
||||
*nothing_to_do = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn targeted_service_checker<S: TargetedPusService>(
|
||||
service: &mut S,
|
||||
timestamp: &[u8],
|
||||
nothing_to_do: &mut bool,
|
||||
) {
|
||||
let request_handling = service.poll_and_handle_next_tc_default_handler(timestamp);
|
||||
let reply_handling = service.poll_and_handle_next_reply_default_handler(timestamp);
|
||||
if request_handling == HandlingStatus::HandledOne
|
||||
|| reply_handling == HandlingStatus::HandledOne
|
||||
{
|
||||
*nothing_to_do = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
use crate::pus::create_verification_reporter;
|
||||
use crate::tmtc::sender::TmTcSender;
|
||||
use log::info;
|
||||
use satrs::event_man_legacy::{EventMessage, EventMessageU32};
|
||||
use satrs::pus::test::PusService17TestHandler;
|
||||
use satrs::pus::verification::{FailParams, VerificationReporter, VerificationReportingProvider};
|
||||
use satrs::pus::PartialPusHandlingError;
|
||||
use satrs::pus::{
|
||||
CacheAndReadRawEcssTc, DirectPusPacketHandlerResult, EcssTcAndToken, EcssTcCacher,
|
||||
MpscTcReceiver, PusServiceHelper,
|
||||
};
|
||||
use satrs::spacepackets::ecss::tc::PusTcReader;
|
||||
use satrs::spacepackets::ecss::{PusPacket, PusServiceId};
|
||||
use example_std::config::{tmtc_err, TEST_EVENT};
|
||||
use example_std::ids::generic_pus::PUS_TEST;
|
||||
use std::sync::mpsc;
|
||||
|
||||
use super::{DirectPusService, HandlingStatus};
|
||||
|
||||
pub fn create_test_service(
|
||||
tm_sender: TmTcSender,
|
||||
tc_in_mem_converter: EcssTcCacher,
|
||||
event_sender: mpsc::SyncSender<EventMessageU32>,
|
||||
pus_test_rx: mpsc::Receiver<EcssTcAndToken>,
|
||||
) -> TestCustomServiceWrapper {
|
||||
let pus17_handler = PusService17TestHandler::new(PusServiceHelper::new(
|
||||
PUS_TEST.id(),
|
||||
pus_test_rx,
|
||||
tm_sender,
|
||||
create_verification_reporter(PUS_TEST.id(), PUS_TEST.apid),
|
||||
tc_in_mem_converter,
|
||||
));
|
||||
TestCustomServiceWrapper {
|
||||
handler: pus17_handler,
|
||||
event_tx: event_sender,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TestCustomServiceWrapper {
|
||||
pub handler:
|
||||
PusService17TestHandler<MpscTcReceiver, TmTcSender, EcssTcCacher, VerificationReporter>,
|
||||
pub event_tx: mpsc::SyncSender<EventMessageU32>,
|
||||
}
|
||||
|
||||
impl DirectPusService for TestCustomServiceWrapper {
|
||||
const SERVICE_ID: u8 = PusServiceId::Test as u8;
|
||||
|
||||
const SERVICE_STR: &'static str = "test";
|
||||
|
||||
fn poll_and_handle_next_tc(&mut self, timestamp: &[u8]) -> HandlingStatus {
|
||||
let error_handler = |partial_error: &PartialPusHandlingError| {
|
||||
log::warn!(
|
||||
"PUS {}({}) partial error: {:?}",
|
||||
Self::SERVICE_ID,
|
||||
Self::SERVICE_STR,
|
||||
partial_error
|
||||
);
|
||||
};
|
||||
let res = self
|
||||
.handler
|
||||
.poll_and_handle_next_tc(error_handler, timestamp);
|
||||
if let Err(e) = res {
|
||||
log::warn!(
|
||||
"PUS {}({}) error: {:?}",
|
||||
Self::SERVICE_ID,
|
||||
Self::SERVICE_STR,
|
||||
e
|
||||
);
|
||||
// To avoid permanent loops on continuous errors.
|
||||
return HandlingStatus::Empty;
|
||||
}
|
||||
match res.unwrap() {
|
||||
DirectPusPacketHandlerResult::Handled(handling_status) => {
|
||||
if handling_status == HandlingStatus::HandledOne {
|
||||
info!("Received PUS ping command TC[17,1]");
|
||||
info!("Sent ping reply PUS TM[17,2]");
|
||||
}
|
||||
return handling_status;
|
||||
}
|
||||
DirectPusPacketHandlerResult::SubserviceNotImplemented(subservice, _) => {
|
||||
log::warn!(
|
||||
"PUS {}({}) subservice {} not implemented",
|
||||
Self::SERVICE_ID,
|
||||
Self::SERVICE_STR,
|
||||
subservice
|
||||
);
|
||||
}
|
||||
DirectPusPacketHandlerResult::CustomSubservice(subservice, token) => {
|
||||
let tc = PusTcReader::new(
|
||||
self.handler
|
||||
.service_helper
|
||||
.tc_in_mem_converter
|
||||
.tc_slice_raw(),
|
||||
)
|
||||
.unwrap();
|
||||
if subservice == 128 {
|
||||
info!("generating test event");
|
||||
self.event_tx
|
||||
.send(EventMessage::new(PUS_TEST.id(), TEST_EVENT.into()))
|
||||
.expect("Sending test event failed");
|
||||
match self.handler.service_helper.verif_reporter().start_success(
|
||||
self.handler.service_helper.tm_sender(),
|
||||
token,
|
||||
timestamp,
|
||||
) {
|
||||
Ok(started_token) => {
|
||||
if let Err(e) = self
|
||||
.handler
|
||||
.service_helper
|
||||
.verif_reporter()
|
||||
.completion_success(
|
||||
self.handler.service_helper.tm_sender(),
|
||||
started_token,
|
||||
timestamp,
|
||||
)
|
||||
{
|
||||
error_handler(&PartialPusHandlingError::Verification(e));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error_handler(&PartialPusHandlingError::Verification(e));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let fail_data = [tc.message_subtype_id()];
|
||||
self.handler
|
||||
.service_helper
|
||||
.verif_reporter()
|
||||
.start_failure(
|
||||
self.handler.service_helper.tm_sender(),
|
||||
token,
|
||||
FailParams::new(
|
||||
timestamp,
|
||||
&tmtc_err::INVALID_PUS_SUBSERVICE,
|
||||
&fail_data,
|
||||
),
|
||||
)
|
||||
.expect("Sending start failure verification failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
HandlingStatus::HandledOne
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::mpsc;
|
||||
|
||||
use log::warn;
|
||||
use satrs::action::ActionRequest;
|
||||
use satrs::hk::HkRequest;
|
||||
use satrs::mode::ModeRequest;
|
||||
use satrs::pus::verification::{
|
||||
FailParams, TcStateAccepted, VerificationReportingProvider, VerificationToken,
|
||||
};
|
||||
use satrs::pus::{ActiveRequest, EcssTmSender, GenericRoutingError, PusRequestRouter};
|
||||
use satrs::queue::GenericSendError;
|
||||
use satrs::request::{GenericMessage, MessageMetadata, UniqueApidTargetId};
|
||||
use satrs::spacepackets::ecss::tc::PusTcReader;
|
||||
use satrs::spacepackets::ecss::PusPacket;
|
||||
use satrs::ComponentId;
|
||||
use example_std::config::tmtc_err;
|
||||
|
||||
/*
|
||||
#[derive(Clone, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum CompositeRequest {
|
||||
Hk(HkRequest),
|
||||
Action(ActionRequest),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GenericRequestRouter {
|
||||
#[allow(dead_code)]
|
||||
pub id: ComponentId,
|
||||
// All messages which do not have a dedicated queue.
|
||||
pub composite_router_map:
|
||||
HashMap<ComponentId, mpsc::SyncSender<GenericMessage<CompositeRequest>>>,
|
||||
pub mode_router_map: HashMap<ComponentId, mpsc::SyncSender<GenericMessage<ModeRequest>>>,
|
||||
}
|
||||
|
||||
impl Default for GenericRequestRouter {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: ids::generic_pus::PUS_ROUTING.raw(),
|
||||
composite_router_map: Default::default(),
|
||||
mode_router_map: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl GenericRequestRouter {
|
||||
pub(crate) fn handle_error_generic(
|
||||
&self,
|
||||
active_request: &impl ActiveRequest,
|
||||
tc: &PusTcReader,
|
||||
error: GenericRoutingError,
|
||||
tm_sender: &(impl EcssTmSender + ?Sized),
|
||||
verif_reporter: &impl VerificationReportingProvider,
|
||||
time_stamp: &[u8],
|
||||
) {
|
||||
warn!(
|
||||
"Routing request for service {} failed: {error:?}",
|
||||
tc.service_type_id()
|
||||
);
|
||||
let accepted_token: VerificationToken<TcStateAccepted> = active_request
|
||||
.token()
|
||||
.try_into()
|
||||
.expect("token is not in accepted state");
|
||||
match error {
|
||||
GenericRoutingError::UnknownTargetId(id) => {
|
||||
let apid_target_id = UniqueApidTargetId::from(id);
|
||||
warn!("Target APID for request: {}", apid_target_id.apid);
|
||||
warn!("Target Unique ID for request: {}", apid_target_id.unique_id);
|
||||
let mut fail_data: [u8; core::mem::size_of::<ComponentId>()] =
|
||||
[0; core::mem::size_of::<ComponentId>()];
|
||||
fail_data.copy_from_slice(&id.to_be_bytes());
|
||||
verif_reporter
|
||||
.completion_failure(
|
||||
tm_sender,
|
||||
accepted_token,
|
||||
FailParams::new(time_stamp, &tmtc_err::UNKNOWN_TARGET_ID, &fail_data),
|
||||
)
|
||||
.expect("Sending start failure failed");
|
||||
}
|
||||
GenericRoutingError::Send(_) => {
|
||||
let mut fail_data: [u8; 8] = [0; 8];
|
||||
fail_data.copy_from_slice(&active_request.target_id().to_be_bytes());
|
||||
verif_reporter
|
||||
.completion_failure(
|
||||
tm_sender,
|
||||
accepted_token,
|
||||
FailParams::new(time_stamp, &tmtc_err::ROUTING_ERROR, &fail_data),
|
||||
)
|
||||
.expect("Sending start failure failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl PusRequestRouter<HkRequest> for GenericRequestRouter {
|
||||
type Error = GenericRoutingError;
|
||||
|
||||
fn route(
|
||||
&self,
|
||||
requestor_info: MessageMetadata,
|
||||
target_id: ComponentId,
|
||||
hk_request: HkRequest,
|
||||
) -> Result<(), Self::Error> {
|
||||
if let Some(sender) = self.composite_router_map.get(&target_id) {
|
||||
sender
|
||||
.send(GenericMessage::new(
|
||||
requestor_info,
|
||||
CompositeRequest::Hk(hk_request),
|
||||
))
|
||||
.map_err(|_| GenericRoutingError::Send(GenericSendError::RxDisconnected))?;
|
||||
return Ok(());
|
||||
}
|
||||
Err(GenericRoutingError::UnknownTargetId(target_id))
|
||||
}
|
||||
}
|
||||
|
||||
impl PusRequestRouter<ActionRequest> for GenericRequestRouter {
|
||||
type Error = GenericRoutingError;
|
||||
|
||||
fn route(
|
||||
&self,
|
||||
requestor_info: MessageMetadata,
|
||||
target_id: ComponentId,
|
||||
action_request: ActionRequest,
|
||||
) -> Result<(), Self::Error> {
|
||||
if let Some(sender) = self.composite_router_map.get(&target_id) {
|
||||
sender
|
||||
.send(GenericMessage::new(
|
||||
requestor_info,
|
||||
CompositeRequest::Action(action_request),
|
||||
))
|
||||
.map_err(|_| GenericRoutingError::Send(GenericSendError::RxDisconnected))?;
|
||||
return Ok(());
|
||||
}
|
||||
Err(GenericRoutingError::UnknownTargetId(target_id))
|
||||
}
|
||||
}
|
||||
|
||||
impl PusRequestRouter<ModeRequest> for GenericRequestRouter {
|
||||
type Error = GenericRoutingError;
|
||||
|
||||
fn route(
|
||||
&self,
|
||||
requestor_info: MessageMetadata,
|
||||
target_id: ComponentId,
|
||||
request: ModeRequest,
|
||||
) -> Result<(), Self::Error> {
|
||||
if let Some(sender) = self.mode_router_map.get(&target_id) {
|
||||
sender
|
||||
.send(GenericMessage::new(requestor_info, request))
|
||||
.map_err(|_| GenericRoutingError::Send(GenericSendError::RxDisconnected))?;
|
||||
return Ok(());
|
||||
}
|
||||
Err(GenericRoutingError::UnknownTargetId(target_id))
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -0,0 +1,158 @@
|
||||
extern crate alloc;
|
||||
|
||||
use std::{
|
||||
sync::mpsc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use satrs::spacepackets::{CcsdsPacketIdAndPsc, time::cds::CdsTime};
|
||||
pub use types::ComponentId;
|
||||
use types::ccsds::{CcsdsTcPacketOwned, CcsdsTmPacketOwned};
|
||||
|
||||
pub mod config;
|
||||
|
||||
/// Simple type modelling packet stored in the heap. This structure is intended to
|
||||
/// be used when sending a packet via a message queue, so it also contains the sender ID.
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct PacketAsVec {
|
||||
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 {
|
||||
stamper: CdsTime,
|
||||
time_stamp: [u8; 7],
|
||||
}
|
||||
|
||||
impl TimestampHelper {
|
||||
pub fn stamp(&self) -> &[u8] {
|
||||
&self.time_stamp
|
||||
}
|
||||
|
||||
pub fn update_from_now(&mut self) {
|
||||
self.stamper
|
||||
.update_from_now()
|
||||
.expect("Updating timestamp failed");
|
||||
self.stamper
|
||||
.write_to_bytes(&mut self.time_stamp)
|
||||
.expect("Writing timestamp failed");
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TimestampHelper {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
stamper: CdsTime::now_with_u16_days().expect("creating time stamper failed"),
|
||||
time_stamp: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
use std::str::FromStr as _;
|
||||
|
||||
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()
|
||||
.format(|out, message, record| {
|
||||
out.finish(format_args!(
|
||||
"{}[{}][{}] {}",
|
||||
chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"),
|
||||
std::thread::current().name().expect("unnamed_thread"),
|
||||
record.level(),
|
||||
message
|
||||
))
|
||||
})
|
||||
.level(log_level)
|
||||
.chain(std::io::stdout())
|
||||
.chain(fern::log_file("output.log")?)
|
||||
.apply()?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,475 @@
|
||||
use std::{
|
||||
net::{IpAddr, SocketAddr},
|
||||
sync::{
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
mpsc,
|
||||
},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use eps::{
|
||||
PowerSwitchHelper,
|
||||
pcdu::{PcduHandler, SerialInterfaceDummy, SerialInterfaceToSim, SerialSimInterfaceWrapper},
|
||||
};
|
||||
use example_std::{
|
||||
TmtcQueues,
|
||||
config::{
|
||||
OBSW_SERVER_ADDR, PACKET_ID_VALIDATOR, SERVER_PORT,
|
||||
tasks::{FREQ_MS_AOCS, FREQ_MS_CONTROLLER, FREQ_MS_UDP_TMTC, SIM_CLIENT_IDLE_DELAY_MS},
|
||||
},
|
||||
};
|
||||
use interface::{
|
||||
sim_client_udp::create_sim_client,
|
||||
tcp::{SyncTcpTmSource, TcpTask},
|
||||
udp::UdpTmtcServer,
|
||||
};
|
||||
use log::info;
|
||||
use logger::setup_logger;
|
||||
use satrs::{
|
||||
HandlingStatus,
|
||||
hal::std::{tcp_server::ServerConfig, udp_server::UdpTcServer},
|
||||
spacepackets::time::cds::CdsTime,
|
||||
};
|
||||
use tmtc::sender::TmTcSender;
|
||||
use tmtc::{tc_source::TcSourceTask, tm_sink::TmSink};
|
||||
use types::{ComponentId, DeviceMode};
|
||||
|
||||
use crate::{
|
||||
acs::{ctrl, mgm, mgm_assembly, mgt, subsystem},
|
||||
controller::Controller,
|
||||
eps::pcdu::SwitchSet,
|
||||
event_manager::EventManager,
|
||||
interface::udp::UdpTmHandlerWithChannel,
|
||||
tmtc::tc_source::CcsdsDistributor,
|
||||
};
|
||||
|
||||
mod acs;
|
||||
mod ccsds;
|
||||
mod controller;
|
||||
mod device_fdir;
|
||||
mod device_mode;
|
||||
mod eps;
|
||||
mod event_manager;
|
||||
mod interface;
|
||||
mod logger;
|
||||
mod tmtc;
|
||||
fn main() {
|
||||
static KILL_SIGNAL: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
setup_logger().expect("setting up logging with fern failed");
|
||||
println!("Runng OBSW example");
|
||||
ctrlc::set_handler(move || {
|
||||
log::info!("Received Ctrl-C, shutting down");
|
||||
KILL_SIGNAL.store(true, Ordering::Relaxed);
|
||||
})
|
||||
.expect("Error setting Ctrl-C handler");
|
||||
|
||||
let (tc_source_tx, tc_source_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 (sim_request_tx, sim_request_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 (mgt_sim_reply_tx, mgt_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 (mgm_0_handler_tc_tx, mgm_0_handler_tc_rx) = mpsc::sync_channel(10);
|
||||
let (mgm_1_handler_tc_tx, mgm_1_handler_tc_rx) = mpsc::sync_channel(10);
|
||||
let (mgt_handler_tc_tx, mgt_handler_tc_rx) = mpsc::sync_channel(10);
|
||||
let (mgm_assembly_tc_tx, mgm_assembly_tc_rx) = mpsc::sync_channel(10);
|
||||
let (acs_subsystem_tc_tx, acs_subsystem_tc_rx) = mpsc::sync_channel(10);
|
||||
let (pcdu_handler_tc_tx, pcdu_handler_tc_rx) = mpsc::sync_channel(30);
|
||||
let (controller_tc_tx, controller_tc_rx) = mpsc::sync_channel(10);
|
||||
let (event_manager_tc_tx, event_manager_tc_rx) = mpsc::sync_channel(10);
|
||||
|
||||
let (mgt_request_tx, mgt_request_rx) = mpsc::sync_channel(5);
|
||||
let (mgt_report_tx, mgt_report_rx) = mpsc::sync_channel(5);
|
||||
|
||||
let (acs_ctrl_request_tx, acs_ctrl_request_rx) = mpsc::sync_channel(5);
|
||||
let (acs_ctrl_response_tx, acs_ctrl_response_rx) = mpsc::sync_channel(5);
|
||||
|
||||
// These message handles need to go into the MGM assembly and ACS subsystem.
|
||||
let (mgm_assembly_request_tx, mgm_assembly_request_rx) = mpsc::sync_channel(5);
|
||||
let (mgm_assembly_report_tx, mgm_assembly_report_rx) = mpsc::sync_channel(5);
|
||||
|
||||
// These message handles need to go into the MGM assembly and MGM devices.
|
||||
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 (pcdu_handler_mode_tx, _pcdu_handler_mode_rx) = mpsc::sync_channel(5);
|
||||
|
||||
let (event_ctrl_tx, event_ctrl_rx) = mpsc::sync_channel(10);
|
||||
let (mgm_event_tx, mgm_event_rx) = mpsc::sync_channel(10);
|
||||
let (mgm_assembly_event_tx, mgm_assembly_event_rx) = mpsc::sync_channel(10);
|
||||
let (mgt_event_tx, mgt_event_rx) = mpsc::sync_channel(10);
|
||||
let (pcdu_event_tx, pcdu_event_rx) = mpsc::sync_channel(10);
|
||||
let (tc_source_event_tx, tc_source_event_rx) = mpsc::sync_channel(10);
|
||||
let mut event_manager = EventManager::new(
|
||||
event_manager_tc_rx,
|
||||
event_ctrl_rx,
|
||||
mgm_event_rx,
|
||||
mgm_assembly_event_rx,
|
||||
mgt_event_rx,
|
||||
pcdu_event_rx,
|
||||
tc_source_event_rx,
|
||||
tm_sink_tx.clone(),
|
||||
);
|
||||
|
||||
let mut controller = Controller::new(controller_tc_rx, tm_sink_tx.clone(), event_ctrl_tx);
|
||||
|
||||
let ccsds_distributor = CcsdsDistributor::default();
|
||||
let mut tc_source = TcSourceTask::new(tc_source_rx, ccsds_distributor, tc_source_event_tx);
|
||||
tc_source.add_target(ComponentId::EpsPcdu, pcdu_handler_tc_tx);
|
||||
tc_source.add_target(ComponentId::Controller, controller_tc_tx);
|
||||
tc_source.add_target(ComponentId::AcsMgm0, mgm_0_handler_tc_tx);
|
||||
tc_source.add_target(ComponentId::AcsMgm1, mgm_1_handler_tc_tx);
|
||||
tc_source.add_target(ComponentId::AcsMgmAssembly, mgm_assembly_tc_tx);
|
||||
tc_source.add_target(ComponentId::AcsMgt, mgt_handler_tc_tx);
|
||||
tc_source.add_target(ComponentId::AcsSubsystem, acs_subsystem_tc_tx);
|
||||
tc_source.add_target(ComponentId::EventManager, event_manager_tc_tx);
|
||||
|
||||
let tc_sender = TmTcSender::Normal(tc_source_tx.clone());
|
||||
let udp_tm_handler = UdpTmHandlerWithChannel {
|
||||
tm_rx: tm_server_rx,
|
||||
};
|
||||
|
||||
let sock_addr = SocketAddr::new(IpAddr::V4(OBSW_SERVER_ADDR), SERVER_PORT);
|
||||
let udp_tc_server = UdpTcServer::new(
|
||||
ComponentId::UdpServer as u32,
|
||||
sock_addr,
|
||||
2048,
|
||||
tc_sender.clone(),
|
||||
)
|
||||
.expect("creating UDP TMTC server failed");
|
||||
let mut udp_tmtc_server = UdpTmtcServer {
|
||||
udp_tc_server,
|
||||
tm_handler: udp_tm_handler.into(),
|
||||
};
|
||||
|
||||
let tcp_server_cfg = ServerConfig::new(
|
||||
ComponentId::TcpServer as u32,
|
||||
sock_addr,
|
||||
Duration::from_millis(400),
|
||||
4096,
|
||||
8192,
|
||||
);
|
||||
let sync_tm_tcp_source = SyncTcpTmSource::new(200);
|
||||
let mut tcp_server = TcpTask::new(
|
||||
tcp_server_cfg,
|
||||
sync_tm_tcp_source.clone(),
|
||||
tc_sender,
|
||||
PACKET_ID_VALIDATOR.clone(),
|
||||
)
|
||||
.expect("tcp server creation failed");
|
||||
|
||||
let mut tm_sink = TmSink::new(sync_tm_tcp_source, tm_sink_rx, tm_server_tx);
|
||||
|
||||
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_helper = PowerSwitchHelper::new(switch_request_tx, shared_switch_set.clone());
|
||||
|
||||
// Global FDIR health table, shared by all software objects.
|
||||
let health_table = satrs::health::HealthTableMapSync::default();
|
||||
|
||||
let shared_mgm_0_set = Arc::default();
|
||||
let shared_mgm_1_set = Arc::default();
|
||||
let (mgm_0_spi_interface, mgm_1_spi_interface) = if let Some(sim_client) =
|
||||
opt_sim_client.as_mut()
|
||||
{
|
||||
sim_client.add_reply_recipient(minisim_types::ComponentId::Mgm0Lis3Mdl, mgm_0_sim_reply_tx);
|
||||
sim_client.add_reply_recipient(minisim_types::ComponentId::Mgm1Lis3Mdl, mgm_1_sim_reply_tx);
|
||||
(
|
||||
mgm::SpiCommunication::Sim(mgm::SpiSimInterface {
|
||||
id: mgm::MgmId::_0,
|
||||
sim_request_tx: sim_request_tx.clone(),
|
||||
sim_reply_rx: mgm_0_sim_reply_rx,
|
||||
}),
|
||||
mgm::SpiCommunication::Sim(mgm::SpiSimInterface {
|
||||
id: mgm::MgmId::_1,
|
||||
sim_request_tx: sim_request_tx.clone(),
|
||||
sim_reply_rx: mgm_1_sim_reply_rx,
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
mgm::SpiCommunication::Dummy(mgm::SpiDummyInterface::default()),
|
||||
mgm::SpiCommunication::Dummy(mgm::SpiDummyInterface::default()),
|
||||
)
|
||||
};
|
||||
let mut mgm_0_handler = mgm::MgmHandlerLis3Mdl::new(
|
||||
mgm::MgmId::_0,
|
||||
TmtcQueues {
|
||||
tc_rx: mgm_0_handler_tc_rx,
|
||||
tm_tx: tm_sink_tx.clone(),
|
||||
},
|
||||
switch_helper.clone(),
|
||||
mgm_0_spi_interface,
|
||||
shared_mgm_0_set,
|
||||
mgm::ModeLeafHelper {
|
||||
request_rx: mgm_0_mode_request_rx,
|
||||
report_tx: mgm_0_mode_report_tx,
|
||||
},
|
||||
Duration::from_millis(1000),
|
||||
health_table.clone(),
|
||||
mgm_event_tx.clone(),
|
||||
);
|
||||
let mut mgm_1_handler = mgm::MgmHandlerLis3Mdl::new(
|
||||
mgm::MgmId::_1,
|
||||
TmtcQueues {
|
||||
tc_rx: mgm_1_handler_tc_rx,
|
||||
tm_tx: tm_sink_tx.clone(),
|
||||
},
|
||||
switch_helper.clone(),
|
||||
mgm_1_spi_interface,
|
||||
shared_mgm_1_set,
|
||||
mgm::ModeLeafHelper {
|
||||
request_rx: mgm_1_mode_request_rx,
|
||||
report_tx: mgm_1_mode_report_tx,
|
||||
},
|
||||
Duration::from_millis(1000),
|
||||
health_table.clone(),
|
||||
mgm_event_tx,
|
||||
);
|
||||
let mut mgm_assembly = mgm_assembly::Assembly::new(
|
||||
mgm_assembly::ParentQueueHelper {
|
||||
request_rx: mgm_assembly_request_rx,
|
||||
report_tx: mgm_assembly_report_tx,
|
||||
},
|
||||
mgm_assembly::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: mgm_assembly_tc_rx,
|
||||
tm_tx: tm_sink_tx.clone(),
|
||||
},
|
||||
Duration::from_millis(2000),
|
||||
mgm_assembly_event_tx,
|
||||
);
|
||||
|
||||
let mut acs_controller = ctrl::Controller::new(ctrl::ModeLeafHelper {
|
||||
request_rx: acs_ctrl_request_rx,
|
||||
report_tx: acs_ctrl_response_tx,
|
||||
});
|
||||
|
||||
let mgt_com = if let Some(sim_client) = opt_sim_client.as_mut() {
|
||||
sim_client.add_reply_recipient(minisim_types::ComponentId::Mgt, mgt_sim_reply_tx);
|
||||
mgt::MgtCommunication::Sim(mgt::SimInterface {
|
||||
sim_request_tx: sim_request_tx.clone(),
|
||||
sim_reply_rx: mgt_sim_reply_rx,
|
||||
})
|
||||
} else {
|
||||
mgt::MgtCommunication::Dummy(mgt::DummyInterface::default())
|
||||
};
|
||||
let mut mgt_handler = mgt::MgtHandler::new(
|
||||
TmtcQueues {
|
||||
tc_rx: mgt_handler_tc_rx,
|
||||
tm_tx: tm_sink_tx.clone(),
|
||||
},
|
||||
switch_helper.clone(),
|
||||
mgt_com,
|
||||
mgt::ModeLeafHelper {
|
||||
request_rx: mgt_request_rx,
|
||||
report_tx: mgt_report_tx,
|
||||
},
|
||||
Duration::from_millis(1000),
|
||||
health_table.clone(),
|
||||
mgt_event_tx,
|
||||
);
|
||||
|
||||
let mut acs_subsystem = subsystem::Subsystem::new(
|
||||
subsystem::ModeRequestSenders {
|
||||
mode_request_ctrl: acs_ctrl_request_tx,
|
||||
mode_request_mgm_assy: mgm_assembly_request_tx,
|
||||
mode_request_mgt: mgt_request_tx,
|
||||
},
|
||||
subsystem::ModeReportReceivers {
|
||||
mode_response_ctrl: acs_ctrl_response_rx,
|
||||
mode_response_mgm_assy: mgm_assembly_report_rx,
|
||||
mode_response_mgt: mgt_report_rx,
|
||||
},
|
||||
TmtcQueues {
|
||||
tc_rx: acs_subsystem_tc_rx,
|
||||
tm_tx: tm_sink_tx.clone(),
|
||||
},
|
||||
);
|
||||
|
||||
let pcdu_serial_interface = if let Some(sim_client) = opt_sim_client.as_mut() {
|
||||
sim_client.add_reply_recipient(minisim_types::ComponentId::Pcdu, pcdu_sim_reply_tx);
|
||||
SerialSimInterfaceWrapper::Sim(SerialInterfaceToSim::new(
|
||||
sim_request_tx.clone(),
|
||||
pcdu_sim_reply_rx,
|
||||
))
|
||||
} else {
|
||||
SerialSimInterfaceWrapper::Dummy(SerialInterfaceDummy::default())
|
||||
};
|
||||
let mut pcdu_handler = PcduHandler::new(
|
||||
pcdu_handler_tc_rx,
|
||||
tm_sink_tx.clone(),
|
||||
switch_request_rx,
|
||||
pcdu_serial_interface,
|
||||
shared_switch_set,
|
||||
DeviceMode::Normal,
|
||||
pcdu_event_tx,
|
||||
);
|
||||
|
||||
// The PCDU is a critical component which should be in normal mode immediately.
|
||||
pcdu_handler_mode_tx
|
||||
.send(types::pcdu::request::Request::Mode(DeviceMode::Normal))
|
||||
.expect("sending initial mode request failed");
|
||||
|
||||
info!("Starting TMTC and UDP task");
|
||||
let jh_udp_tmtc = thread::Builder::new()
|
||||
.name("TMTC & UDP".to_string())
|
||||
.spawn(move || {
|
||||
info!("Running UDP server on port {SERVER_PORT}");
|
||||
loop {
|
||||
if KILL_SIGNAL.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
udp_tmtc_server.periodic_operation();
|
||||
tc_source.periodic_operation();
|
||||
thread::sleep(Duration::from_millis(FREQ_MS_UDP_TMTC));
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
info!("Starting TCP task");
|
||||
let jh_tcp = thread::Builder::new()
|
||||
.name("TCP".to_string())
|
||||
.spawn(move || {
|
||||
info!("Running TCP server on port {SERVER_PORT}");
|
||||
loop {
|
||||
if KILL_SIGNAL.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
tcp_server.periodic_operation();
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
info!("Starting TM funnel task");
|
||||
let jh_tm_funnel = thread::Builder::new()
|
||||
.name("TM SINK".to_string())
|
||||
.spawn(move || {
|
||||
loop {
|
||||
if KILL_SIGNAL.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
tm_sink.operation();
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let mut opt_jh_sim_client = None;
|
||||
if let Some(mut sim_client) = opt_sim_client {
|
||||
info!("Starting UDP sim client task");
|
||||
opt_jh_sim_client = Some(
|
||||
thread::Builder::new()
|
||||
.name("SIM ADAPTER".to_string())
|
||||
.spawn(move || {
|
||||
loop {
|
||||
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(),
|
||||
);
|
||||
}
|
||||
|
||||
info!("Starting AOCS thread");
|
||||
let jh_aocs = thread::Builder::new()
|
||||
.name("AOCS".to_string())
|
||||
.spawn(move || {
|
||||
loop {
|
||||
if KILL_SIGNAL.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
mgm_0_handler.periodic_operation();
|
||||
mgm_1_handler.periodic_operation();
|
||||
mgm_assembly.periodic_operation();
|
||||
acs_controller.periodic_operation();
|
||||
mgt_handler.periodic_operation();
|
||||
acs_subsystem.periodic_operation();
|
||||
thread::sleep(Duration::from_millis(FREQ_MS_AOCS));
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
info!("Starting EPS thread");
|
||||
let jh_eps = thread::Builder::new()
|
||||
.name("EPS".to_string())
|
||||
.spawn(move || {
|
||||
loop {
|
||||
if KILL_SIGNAL.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
// TODO: We should introduce something like a fixed timeslot helper to allow a more
|
||||
// declarative API. It would also be very useful for the AOCS task.
|
||||
//
|
||||
// TODO: The fixed timeslot handler exists.. use it.
|
||||
// TODO: Why not just use sync code in the PCDU handler, and fully delay there?
|
||||
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();
|
||||
|
||||
info!("Starting controller thread");
|
||||
let jh_controller_thread = thread::Builder::new()
|
||||
.name("CTRL".to_string())
|
||||
.spawn(move || {
|
||||
loop {
|
||||
if KILL_SIGNAL.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
controller.periodic_operation();
|
||||
event_manager.periodic_operation();
|
||||
thread::sleep(Duration::from_millis(FREQ_MS_CONTROLLER));
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
jh_udp_tmtc
|
||||
.join()
|
||||
.expect("Joining UDP TMTC server thread failed");
|
||||
jh_tcp
|
||||
.join()
|
||||
.expect("Joining TCP TMTC server thread failed");
|
||||
jh_tm_funnel
|
||||
.join()
|
||||
.expect("Joining TM Funnel thread failed");
|
||||
if let Some(jh_sim_client) = opt_jh_sim_client {
|
||||
jh_sim_client
|
||||
.join()
|
||||
.expect("Joining SIM client thread failed");
|
||||
}
|
||||
jh_aocs.join().expect("Joining AOCS thread failed");
|
||||
jh_eps.join().expect("Joining EPS thread failed");
|
||||
jh_controller_thread
|
||||
.join()
|
||||
.expect("Joining PUS handler thread failed");
|
||||
}
|
||||
|
||||
pub fn update_time(time_provider: &mut CdsTime, timestamp: &mut [u8]) {
|
||||
time_provider
|
||||
.update_from_now()
|
||||
.expect("Could not get current time");
|
||||
time_provider
|
||||
.write_to_bytes(timestamp)
|
||||
.expect("Writing timestamp failed");
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod sender;
|
||||
pub mod tc_source;
|
||||
pub mod tm_sink;
|
||||
@@ -0,0 +1,55 @@
|
||||
use std::{cell::RefCell, collections::VecDeque, sync::mpsc};
|
||||
|
||||
use satrs::{
|
||||
ComponentId,
|
||||
queue::GenericSendError,
|
||||
tmtc::{PacketAsVec, PacketHandler},
|
||||
};
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct MockSender(pub RefCell<VecDeque<PacketAsVec>>);
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum TmTcSender {
|
||||
Normal(mpsc::SyncSender<PacketAsVec>),
|
||||
Mock(MockSender),
|
||||
}
|
||||
|
||||
impl TmTcSender {
|
||||
#[allow(dead_code)]
|
||||
pub fn get_mock_sender(&mut self) -> Option<&mut MockSender> {
|
||||
match self {
|
||||
TmTcSender::Mock(sender) => Some(sender),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PacketHandler for TmTcSender {
|
||||
type Error = GenericSendError;
|
||||
|
||||
fn handle_packet(&self, sender_id: ComponentId, packet: &[u8]) -> Result<(), Self::Error> {
|
||||
match self {
|
||||
TmTcSender::Normal(sync_sender) => {
|
||||
if let Err(e) = sync_sender.send(PacketAsVec::new(sender_id, packet.to_vec())) {
|
||||
log::error!("Error sending packet via Heap TM/TC sender: {:?}", e);
|
||||
}
|
||||
}
|
||||
TmTcSender::Mock(sender) => {
|
||||
sender.handle_packet(sender_id, packet).unwrap();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl PacketHandler for MockSender {
|
||||
type Error = GenericSendError;
|
||||
|
||||
fn handle_packet(&self, sender_id: ComponentId, tc_raw: &[u8]) -> Result<(), Self::Error> {
|
||||
let mut mut_queue = self.0.borrow_mut();
|
||||
mut_queue.push_back(PacketAsVec::new(sender_id, tc_raw.to_vec()));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
use satrs::{
|
||||
ComponentId as RawComponentId, HandlingStatus,
|
||||
spacepackets::{CcsdsPacketReader, ChecksumType},
|
||||
tmtc::PacketAsVec,
|
||||
};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::mpsc::{self, TryRecvError},
|
||||
};
|
||||
use types::{ComponentId, TcHeader, ccsds::CcsdsTcPacketOwned, tmtc};
|
||||
|
||||
pub type CcsdsDistributor = HashMap<ComponentId, std::sync::mpsc::SyncSender<CcsdsTcPacketOwned>>;
|
||||
|
||||
// TC source components where the heap is the backing memory of the received telecommands.
|
||||
pub struct TcSourceTask {
|
||||
pub tc_receiver: mpsc::Receiver<PacketAsVec>,
|
||||
ccsds_distributor: CcsdsDistributor,
|
||||
event_tx: mpsc::SyncSender<(ComponentId, tmtc::Event)>,
|
||||
}
|
||||
|
||||
impl TcSourceTask {
|
||||
pub fn new(
|
||||
tc_receiver: mpsc::Receiver<PacketAsVec>,
|
||||
ccsds_distributor: CcsdsDistributor,
|
||||
event_tx: mpsc::SyncSender<(ComponentId, tmtc::Event)>,
|
||||
) -> Self {
|
||||
Self {
|
||||
tc_receiver,
|
||||
ccsds_distributor,
|
||||
event_tx,
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
loop {
|
||||
if self.poll_tc() == HandlingStatus::Empty {
|
||||
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()
|
||||
);
|
||||
self.send_event(packet.sender_id, tmtc::Event::InvalidTcPacket);
|
||||
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()
|
||||
);
|
||||
self.send_event(packet.sender_id, tmtc::Event::InvalidTcHeader);
|
||||
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);
|
||||
self.send_event(
|
||||
packet.sender_id,
|
||||
tmtc::Event::UnknownTargetId(tc_header.target_id),
|
||||
);
|
||||
}
|
||||
HandlingStatus::HandledOne
|
||||
}
|
||||
Err(e) => match e {
|
||||
TryRecvError::Empty => HandlingStatus::Empty,
|
||||
TryRecvError::Disconnected => {
|
||||
log::warn!("tmtc thread: sender disconnected");
|
||||
HandlingStatus::Empty
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// `sender_id` is the raw ID tagged on the received packet, which is not necessarily a
|
||||
/// known [ComponentId] (e.g. a spoofed or garbled packet). Falls back to [ComponentId::Ground]
|
||||
/// as the event sender in that case.
|
||||
fn send_event(&self, sender_id: RawComponentId, event: tmtc::Event) {
|
||||
let sender_id = ComponentId::try_from(sender_id).unwrap_or_else(|_| {
|
||||
log::warn!("TC source event for unknown raw sender ID {}", sender_id);
|
||||
ComponentId::Ground
|
||||
});
|
||||
if let Err(e) = self.event_tx.send((sender_id, event)) {
|
||||
log::warn!("failed to send TC source event: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::mpsc::TryRecvError;
|
||||
|
||||
use arbitrary_int::u11;
|
||||
use satrs::spacepackets::{
|
||||
CcsdsPacketCreatorOwned, ChecksumType, PacketType, SpacePacketHeader,
|
||||
};
|
||||
use types::{Apid, MessageType, ccsds::CcsdsTcPacketOwned};
|
||||
|
||||
use super::*;
|
||||
|
||||
struct Testbench {
|
||||
tc_tx: mpsc::SyncSender<PacketAsVec>,
|
||||
target_rx: mpsc::Receiver<CcsdsTcPacketOwned>,
|
||||
event_rx: mpsc::Receiver<(ComponentId, tmtc::Event)>,
|
||||
tc_source: TcSourceTask,
|
||||
}
|
||||
|
||||
impl Testbench {
|
||||
fn new() -> Self {
|
||||
let (tc_tx, tc_source_rx) = mpsc::sync_channel(5);
|
||||
let (target_tx, target_rx) = mpsc::sync_channel(5);
|
||||
let (event_tx, event_rx) = mpsc::sync_channel(5);
|
||||
let mut tc_source =
|
||||
TcSourceTask::new(tc_source_rx, CcsdsDistributor::default(), event_tx);
|
||||
tc_source.add_target(ComponentId::EpsPcdu, target_tx);
|
||||
Self {
|
||||
tc_tx,
|
||||
target_rx,
|
||||
event_rx,
|
||||
tc_source,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn valid_tc_raw(target_id: ComponentId) -> Vec<u8> {
|
||||
CcsdsTcPacketOwned::new_with_request(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||
TcHeader::new(target_id, MessageType::Ping),
|
||||
(),
|
||||
)
|
||||
.to_vec()
|
||||
}
|
||||
|
||||
/// A structurally valid CCSDS packet (correct length and CRC), but with arbitrary user data
|
||||
/// instead of a postcard-encoded [TcHeader].
|
||||
fn raw_ccsds_tc_with_user_data(user_data: &[u8]) -> Vec<u8> {
|
||||
CcsdsPacketCreatorOwned::new(
|
||||
SpacePacketHeader::new_from_apid(u11::new(Apid::Acs as u16)),
|
||||
PacketType::Tc,
|
||||
user_data,
|
||||
Some(ChecksumType::WithCrc16),
|
||||
)
|
||||
.unwrap()
|
||||
.to_vec()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_tc_is_routed_without_event() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.tc_tx
|
||||
.send(PacketAsVec::new(
|
||||
ComponentId::UdpServer as u32,
|
||||
valid_tc_raw(ComponentId::EpsPcdu),
|
||||
))
|
||||
.unwrap();
|
||||
tb.tc_source.periodic_operation();
|
||||
tb.target_rx.try_recv().expect("TC was not routed");
|
||||
assert!(matches!(tb.event_rx.try_recv(), Err(TryRecvError::Empty)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_ccsds_packet_sends_event() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.tc_tx
|
||||
.send(PacketAsVec::new(
|
||||
ComponentId::UdpServer as u32,
|
||||
vec![1, 2, 3],
|
||||
))
|
||||
.unwrap();
|
||||
tb.tc_source.periodic_operation();
|
||||
let (sender_id, event) = tb.event_rx.try_recv().expect("expected event");
|
||||
assert_eq!(sender_id, ComponentId::UdpServer);
|
||||
assert!(matches!(event, tmtc::Event::InvalidTcPacket));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_tc_header_sends_event() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.tc_tx
|
||||
.send(PacketAsVec::new(
|
||||
ComponentId::UdpServer as u32,
|
||||
// A single byte is enough to decode the `ComponentId` discriminant, but not
|
||||
// enough for the trailing `MessageType`, so `TcHeader` deserialization fails
|
||||
// while the CCSDS packet itself stays valid.
|
||||
raw_ccsds_tc_with_user_data(&[0]),
|
||||
))
|
||||
.unwrap();
|
||||
tb.tc_source.periodic_operation();
|
||||
let (sender_id, event) = tb.event_rx.try_recv().expect("expected event");
|
||||
assert_eq!(sender_id, ComponentId::UdpServer);
|
||||
assert!(matches!(event, tmtc::Event::InvalidTcHeader));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unknown_target_id_sends_event() {
|
||||
let mut tb = Testbench::new();
|
||||
tb.tc_tx
|
||||
.send(PacketAsVec::new(
|
||||
ComponentId::UdpServer as u32,
|
||||
valid_tc_raw(ComponentId::Ground),
|
||||
))
|
||||
.unwrap();
|
||||
tb.tc_source.periodic_operation();
|
||||
let (sender_id, event) = tb.event_rx.try_recv().expect("expected event");
|
||||
assert_eq!(sender_id, ComponentId::UdpServer);
|
||||
assert!(matches!(
|
||||
event,
|
||||
tmtc::Event::UnknownTargetId(ComponentId::Ground)
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::mpsc::{self},
|
||||
};
|
||||
|
||||
use arbitrary_int::{u11, u14};
|
||||
use satrs::spacepackets::seq_count::{SequenceCounter, SequenceCounterCcsdsSimple};
|
||||
use types::ccsds::CcsdsTmPacketOwned;
|
||||
|
||||
use crate::interface::tcp::SyncTcpTmSource;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CcsdsSeqCounterMap {
|
||||
apid_seq_counter_map: HashMap<u11, SequenceCounterCcsdsSimple>,
|
||||
}
|
||||
impl CcsdsSeqCounterMap {
|
||||
pub fn get_and_increment(&mut self, apid: u11) -> u14 {
|
||||
u14::new(
|
||||
self.apid_seq_counter_map
|
||||
.entry(apid)
|
||||
.or_default()
|
||||
.get_and_increment(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TmSink {
|
||||
seq_counter_map: CcsdsSeqCounterMap,
|
||||
sync_tm_tcp_source: SyncTcpTmSource,
|
||||
tm_funnel_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
|
||||
tm_server_tx: mpsc::SyncSender<CcsdsTmPacketOwned>,
|
||||
}
|
||||
|
||||
impl TmSink {
|
||||
pub fn new(
|
||||
sync_tm_tcp_source: SyncTcpTmSource,
|
||||
tm_funnel_rx: mpsc::Receiver<CcsdsTmPacketOwned>,
|
||||
tm_server_tx: mpsc::SyncSender<CcsdsTmPacketOwned>,
|
||||
) -> Self {
|
||||
Self {
|
||||
seq_counter_map: Default::default(),
|
||||
sync_tm_tcp_source,
|
||||
tm_funnel_rx,
|
||||
tm_server_tx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn operation(&mut self) {
|
||||
if let Ok(mut tm) = self.tm_funnel_rx.try_recv() {
|
||||
tm.sp_header
|
||||
.set_seq_count(self.seq_counter_map.get_and_increment(tm.sp_header.apid()));
|
||||
self.sync_tm_tcp_source.add_tm(&tm.to_vec());
|
||||
self.tm_server_tx
|
||||
.send(tm)
|
||||
.expect("Sending TM to server failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "minisim-types"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
num_enum = { version = "0.7", default-features = false }
|
||||
serde = { version = "1", default-features = false, features = ["derive"] }
|
||||
tai-time = { version = "1", default-features = false, features = ["serde"] }
|
||||
thiserror = { version = "2", default-features = false }
|
||||
types = { path = "../types" }
|
||||
|
||||
[dev-dependencies]
|
||||
postcard = { version = "1", features = ["alloc"] }
|
||||
@@ -0,0 +1,472 @@
|
||||
#![no_std]
|
||||
extern crate alloc;
|
||||
|
||||
use alloc::vec::Vec;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tai_time::MonotonicTime;
|
||||
|
||||
use crate::{
|
||||
acs::{mgm, mgt},
|
||||
eps::{PcduReply, PcduRequest},
|
||||
};
|
||||
|
||||
/// Used by clients to route replies to the component handling them.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
|
||||
pub enum ComponentId {
|
||||
SimCtrl,
|
||||
Mgm0Lis3Mdl,
|
||||
Mgm1Lis3Mdl,
|
||||
Mgt,
|
||||
Pcdu,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum SimRequest {
|
||||
SimCtrl(SimCtrlRequest),
|
||||
Mgm {
|
||||
id: mgm::Id,
|
||||
request: mgm::Request,
|
||||
},
|
||||
/// Raw frame of the MGT serial protocol.
|
||||
Mgt(Vec<u8>),
|
||||
Pcdu(PcduRequest),
|
||||
}
|
||||
|
||||
impl From<SimCtrlRequest> for SimRequest {
|
||||
fn from(request: SimCtrlRequest) -> Self {
|
||||
Self::SimCtrl(request)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<mgt::Request> for SimRequest {
|
||||
fn from(request: mgt::Request) -> Self {
|
||||
Self::Mgt(request.to_frame())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PcduRequest> for SimRequest {
|
||||
fn from(request: PcduRequest) -> Self {
|
||||
Self::Pcdu(request)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SimRequestWithTime {
|
||||
pub request: SimRequest,
|
||||
pub timestamp: MonotonicTime,
|
||||
}
|
||||
|
||||
impl SimRequestWithTime {
|
||||
pub fn new(request: impl Into<SimRequest>, timestamp: MonotonicTime) -> Self {
|
||||
Self {
|
||||
request: request.into(),
|
||||
timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_epoch_time(request: impl Into<SimRequest>) -> Self {
|
||||
Self::new(request, MonotonicTime::EPOCH)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum SimReply {
|
||||
SimCtrl(SimCtrlReply),
|
||||
Mgm {
|
||||
id: mgm::Id,
|
||||
reply: mgm::Reply,
|
||||
},
|
||||
/// Raw frame of the MGT serial protocol.
|
||||
Mgt(Vec<u8>),
|
||||
Pcdu(PcduReply),
|
||||
}
|
||||
|
||||
impl SimReply {
|
||||
pub fn component(&self) -> ComponentId {
|
||||
match self {
|
||||
SimReply::SimCtrl(_) => ComponentId::SimCtrl,
|
||||
SimReply::Mgm { id, .. } => id.sim_component(),
|
||||
SimReply::Mgt(_) => ComponentId::Mgt,
|
||||
SimReply::Pcdu(_) => ComponentId::Pcdu,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SimCtrlReply> for SimReply {
|
||||
fn from(reply: SimCtrlReply) -> Self {
|
||||
Self::SimCtrl(reply)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<mgt::Reply> for SimReply {
|
||||
fn from(reply: mgt::Reply) -> Self {
|
||||
Self::Mgt(reply.to_frame())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PcduReply> for SimReply {
|
||||
fn from(reply: PcduReply) -> Self {
|
||||
Self::Pcdu(reply)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum SimCtrlRequest {
|
||||
Ping,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum SimCtrlReply {
|
||||
Pong,
|
||||
}
|
||||
|
||||
pub mod eps {
|
||||
use super::*;
|
||||
use types::pcdu::{SwitchId, SwitchMapBinary, SwitchStateBinary};
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PcduRequest {
|
||||
SwitchDevice {
|
||||
switch: SwitchId,
|
||||
state: SwitchStateBinary,
|
||||
},
|
||||
RequestSwitchInfo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum PcduReply {
|
||||
SwitchInfo(SwitchMapBinary),
|
||||
}
|
||||
}
|
||||
|
||||
pub mod acs {
|
||||
/// MGM module strongly based on the LIS3MDL device.
|
||||
pub mod mgm {
|
||||
use serde::{Deserialize, Serialize};
|
||||
use types::pcdu::SwitchStateBinary;
|
||||
|
||||
use crate::ComponentId;
|
||||
|
||||
/// Fault mode injected on the simulated SPI bus, independent of the switch state.
|
||||
///
|
||||
/// Models the classic symptom of a stuck SPI bus: an undriven MISO line commonly reads
|
||||
/// back as all-1s, a shorted/grounded one as all-0s.
|
||||
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum SpiFaultMode {
|
||||
#[default]
|
||||
None,
|
||||
AllZeros,
|
||||
AllOnes,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SpiFault {
|
||||
pub mode: SpiFaultMode,
|
||||
/// The fault is cleared when the device is switched off, so a power cycle recovers
|
||||
/// from it.
|
||||
pub cleared_by_power_cycle: bool,
|
||||
}
|
||||
|
||||
// Normally, small magnetometers generate their output as a signed 16 bit raw format or something
|
||||
// similar which needs to be converted to a signed float value with physical units. We will
|
||||
// simplify this now and generate the signed float values directly. The unit is micro tesla.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SensorValuesMicroTesla {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub z: f32,
|
||||
}
|
||||
|
||||
pub const MGT_GEN_MAGNETIC_FIELD: SensorValuesMicroTesla = SensorValuesMicroTesla {
|
||||
x: 30.0,
|
||||
y: -30.0,
|
||||
z: 30.0,
|
||||
};
|
||||
pub const ALL_ONES_SENSOR_VAL: i16 = 0xffff_u16 as i16;
|
||||
pub const ALL_ZEROS_SENSOR_VAL: i16 = 0;
|
||||
|
||||
// Field data register scaling
|
||||
pub const GAUSS_TO_MICROTESLA_FACTOR: u32 = 100;
|
||||
pub const FIELD_LSB_PER_GAUSS_4_SENS: f32 = 1.0 / 6842.0;
|
||||
|
||||
#[derive(Default, Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RawValues {
|
||||
pub x: i16,
|
||||
pub y: i16,
|
||||
pub z: i16,
|
||||
}
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Request {
|
||||
RequestSensorData,
|
||||
/// Force the raw register reply into a stuck-bus pattern, regardless of switch state.
|
||||
/// Used to test FDIR handling of SPI bus faults.
|
||||
SetSpiFault(SpiFault),
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Reply {
|
||||
pub switch_state: SwitchStateBinary,
|
||||
pub sensor_values: SensorValuesMicroTesla,
|
||||
// Raw sensor values which are transmitted by the LIS3 device in little-endian
|
||||
// order.
|
||||
pub raw: RawValues,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum Id {
|
||||
Mgm0,
|
||||
Mgm1,
|
||||
}
|
||||
|
||||
impl Id {
|
||||
pub const fn sim_component(&self) -> ComponentId {
|
||||
match self {
|
||||
Id::Mgm0 => ComponentId::Mgm0Lis3Mdl,
|
||||
Id::Mgm1 => ComponentId::Mgm1Lis3Mdl,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RawValues {
|
||||
pub const fn splat(value: i16) -> Self {
|
||||
Self {
|
||||
x: value,
|
||||
y: value,
|
||||
z: value,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple serial protocol of the magnetorquer.
|
||||
///
|
||||
/// The first byte of each frame is the packet ID. The high bit of the ID is set for replies.
|
||||
/// All fields are big endian. Every command is answered with exactly one reply, but only
|
||||
/// if the device is powered. The device drops invalid frames.
|
||||
///
|
||||
/// A data link layer is deliberately skipped for simplicity. A real serial link would need
|
||||
/// framing and error detection, for example COBS encoding and a CRC. Here, the transport
|
||||
/// always delivers complete and intact frames.
|
||||
pub mod mgt {
|
||||
use alloc::{vec, vec::Vec};
|
||||
use core::time::Duration;
|
||||
|
||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
pub use types::acs::mgt::Dipole;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
|
||||
#[repr(u8)]
|
||||
pub enum RequestId {
|
||||
RequestHk = 0x01,
|
||||
/// Payload: dipole (3 x i16), duration in milliseconds (u32).
|
||||
ApplyTorque = 0x02,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
|
||||
#[repr(u8)]
|
||||
pub enum ReplyId {
|
||||
/// Payload: dipole (3 x i16), torquing flag (u8).
|
||||
Hk = 0x81,
|
||||
/// Reply to [RequestId::ApplyTorque].
|
||||
Ack = 0x82,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum FrameError {
|
||||
#[error("empty frame")]
|
||||
Empty,
|
||||
#[error("unknown packet ID {0:#04x}")]
|
||||
UnknownPacketId(u8),
|
||||
#[error("invalid length {len} for packet ID {id:#04x}")]
|
||||
InvalidLength { id: u8, len: usize },
|
||||
}
|
||||
|
||||
fn split_packet_id(frame: &[u8]) -> Result<(u8, &[u8]), FrameError> {
|
||||
let (&id, payload) = frame.split_first().ok_or(FrameError::Empty)?;
|
||||
Ok((id, payload))
|
||||
}
|
||||
|
||||
fn payload_array<const N: usize>(id: u8, payload: &[u8]) -> Result<&[u8; N], FrameError> {
|
||||
payload.try_into().map_err(|_| FrameError::InvalidLength {
|
||||
id,
|
||||
len: payload.len() + 1,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum Request {
|
||||
/// The duration has millisecond resolution on the wire.
|
||||
ApplyTorque {
|
||||
duration: Duration,
|
||||
dipole: Dipole,
|
||||
},
|
||||
RequestHk,
|
||||
}
|
||||
|
||||
impl Request {
|
||||
pub fn to_frame(&self) -> Vec<u8> {
|
||||
match self {
|
||||
Request::RequestHk => vec![RequestId::RequestHk.into()],
|
||||
Request::ApplyTorque { duration, dipole } => {
|
||||
let mut frame = vec![RequestId::ApplyTorque.into()];
|
||||
frame.extend_from_slice(&dipole.to_be_bytes());
|
||||
let duration_ms = u32::try_from(duration.as_millis()).unwrap_or(u32::MAX);
|
||||
frame.extend_from_slice(&duration_ms.to_be_bytes());
|
||||
frame
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_frame(frame: &[u8]) -> Result<Self, FrameError> {
|
||||
let (id, payload) = split_packet_id(frame)?;
|
||||
match RequestId::try_from(id).map_err(|_| FrameError::UnknownPacketId(id))? {
|
||||
RequestId::RequestHk => {
|
||||
payload_array::<0>(id, payload)?;
|
||||
Ok(Request::RequestHk)
|
||||
}
|
||||
RequestId::ApplyTorque => {
|
||||
let payload: &[u8; Dipole::LEN + 4] = payload_array(id, payload)?;
|
||||
let [dipole @ .., d0, d1, d2, d3] = *payload;
|
||||
Ok(Request::ApplyTorque {
|
||||
duration: Duration::from_millis(
|
||||
u32::from_be_bytes([d0, d1, d2, d3]).into(),
|
||||
),
|
||||
dipole: Dipole::from_be_bytes(&dipole),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct HkSet {
|
||||
pub dipole: Dipole,
|
||||
pub torquing: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum Reply {
|
||||
Hk(HkSet),
|
||||
Ack,
|
||||
}
|
||||
|
||||
impl Reply {
|
||||
pub fn to_frame(&self) -> Vec<u8> {
|
||||
match self {
|
||||
Reply::Hk(hk) => {
|
||||
let mut frame = vec![ReplyId::Hk.into()];
|
||||
frame.extend_from_slice(&hk.dipole.to_be_bytes());
|
||||
frame.push(hk.torquing as u8);
|
||||
frame
|
||||
}
|
||||
Reply::Ack => vec![ReplyId::Ack.into()],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_frame(frame: &[u8]) -> Result<Self, FrameError> {
|
||||
let (id, payload) = split_packet_id(frame)?;
|
||||
match ReplyId::try_from(id).map_err(|_| FrameError::UnknownPacketId(id))? {
|
||||
ReplyId::Hk => {
|
||||
let payload: &[u8; Dipole::LEN + 1] = payload_array(id, payload)?;
|
||||
let [dipole @ .., torquing] = *payload;
|
||||
Ok(Reply::Hk(HkSet {
|
||||
dipole: Dipole::from_be_bytes(&dipole),
|
||||
torquing: torquing != 0,
|
||||
}))
|
||||
}
|
||||
ReplyId::Ack => {
|
||||
payload_array::<0>(id, payload)?;
|
||||
Ok(Reply::Ack)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_apply_torque_frame() {
|
||||
let request = Request::ApplyTorque {
|
||||
duration: Duration::from_millis(0x0102_0304),
|
||||
dipole: Dipole {
|
||||
x: -2,
|
||||
y: 0x0506,
|
||||
z: 0x0708,
|
||||
},
|
||||
};
|
||||
let frame = request.to_frame();
|
||||
assert_eq!(
|
||||
frame,
|
||||
[
|
||||
0x02, 0xff, 0xfe, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04
|
||||
]
|
||||
);
|
||||
assert_eq!(Request::from_frame(&frame), Ok(request));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_request_hk_frame() {
|
||||
assert_eq!(Request::RequestHk.to_frame(), [0x01]);
|
||||
assert_eq!(Request::from_frame(&[0x01]), Ok(Request::RequestHk));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reply_frames() {
|
||||
let hk = Reply::Hk(HkSet {
|
||||
dipole: Dipole { x: 1, y: 2, z: 3 },
|
||||
torquing: true,
|
||||
});
|
||||
let frame = hk.to_frame();
|
||||
assert_eq!(frame, [0x81, 0, 1, 0, 2, 0, 3, 1]);
|
||||
assert_eq!(Reply::from_frame(&frame), Ok(hk));
|
||||
assert_eq!(Reply::Ack.to_frame(), [0x82]);
|
||||
assert_eq!(Reply::from_frame(&[0x82]), Ok(Reply::Ack));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_frames() {
|
||||
assert_eq!(Request::from_frame(&[]), Err(FrameError::Empty));
|
||||
assert_eq!(
|
||||
Request::from_frame(&[0x81]),
|
||||
Err(FrameError::UnknownPacketId(0x81))
|
||||
);
|
||||
assert_eq!(
|
||||
Request::from_frame(&[0x01, 0x00]),
|
||||
Err(FrameError::InvalidLength { id: 0x01, len: 2 })
|
||||
);
|
||||
assert_eq!(
|
||||
Reply::from_frame(&[0x81, 0, 1]),
|
||||
Err(FrameError::InvalidLength { id: 0x81, len: 3 })
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod udp {
|
||||
pub const SIM_CTRL_PORT: u16 = 7303;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_request_serde_roundtrip() {
|
||||
let sim_request = SimRequestWithTime::new_with_epoch_time(SimCtrlRequest::Ping);
|
||||
let bytes = postcard::to_allocvec(&sim_request).unwrap();
|
||||
let deserialized: SimRequestWithTime = postcard::from_bytes(&bytes).unwrap();
|
||||
assert_eq!(deserialized, sim_request);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reply_serde_roundtrip() {
|
||||
let sim_reply = SimReply::from(SimCtrlReply::Pong);
|
||||
assert_eq!(sim_reply.component(), ComponentId::SimCtrl);
|
||||
let bytes = postcard::to_allocvec(&sim_reply).unwrap();
|
||||
let deserialized: SimReply = postcard::from_bytes(&bytes).unwrap();
|
||||
assert_eq!(deserialized, sim_reply);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "minisim"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
postcard = { version = "1", features = ["alloc"] }
|
||||
log = "0.4"
|
||||
thiserror = "2"
|
||||
fern = "0.7"
|
||||
strum = { version = "0.28", features = ["derive"] }
|
||||
num_enum = "0.7"
|
||||
humantime = "2"
|
||||
nexosim = "1"
|
||||
|
||||
satrs = { path = "../../satrs" }
|
||||
types = { path = "../types" }
|
||||
minisim-types = { path = "../minisim-types" }
|
||||
|
||||
[dev-dependencies]
|
||||
delegate = "0.13"
|
||||
@@ -0,0 +1,32 @@
|
||||
sat-rs minisim
|
||||
======
|
||||
|
||||
This crate contains a mini-simulator based on the open-source discrete-event simulation framework
|
||||
[nexosim](https://github.com/asynchronics/nexosim).
|
||||
|
||||
Right now, this crate is primarily used together with the
|
||||
[`example-std` application](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/examples/example-std)
|
||||
to simulate the devices connected to the example application.
|
||||
|
||||
You can simply run this application using
|
||||
|
||||
```sh
|
||||
cargo run
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```sh
|
||||
cargo run -p minisim
|
||||
```
|
||||
|
||||
in the workspace. The mini simulator uses the UDP port 7303 to exchange simulation requests and
|
||||
simulation replies with any other application.
|
||||
|
||||
The simulator was designed in a modular way to be scalable and adaptable to other communication
|
||||
schemes. This might allow it to serve a mini-simulator for other example applications which
|
||||
still have similar device handlers.
|
||||
|
||||
The following graph shows the high-level architecture of the mini-simulator.
|
||||
|
||||
<img src="../../images/minisim-arch/minisim-arch.png" alt="Mini simulator architecture" width="500" class="center"/>
|
||||
@@ -0,0 +1,262 @@
|
||||
use std::f32::consts::PI;
|
||||
|
||||
use minisim_types::{SimReply, acs::mgm};
|
||||
use nexosim::{
|
||||
model::{Context, Model},
|
||||
ports::Output,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use types::pcdu::SwitchStateBinary;
|
||||
|
||||
use crate::time::current_millis;
|
||||
// Earth magnetic field varies between roughly -30 uT and 30 uT
|
||||
const AMPLITUDE_MGM_UT: f32 = 30.0;
|
||||
// Lets start with a simple frequency here.
|
||||
const FREQUENCY_MGM: f32 = 1.0;
|
||||
const PHASE_X: f32 = 0.0;
|
||||
// Different phases to have different values on the other axes.
|
||||
const PHASE_Y: f32 = 0.1;
|
||||
const PHASE_Z: f32 = 0.2;
|
||||
|
||||
/// Simple model for a magnetometer where the measure magnetic fields are modeled with sine waves.
|
||||
///
|
||||
/// An ideal sensor would sample the magnetic field at a high fixed rate. This might not be
|
||||
/// possible for a general purpose OS, but self self-sampling at a relatively high rate (20-40 ms)
|
||||
/// might still be possible and is probably sufficient for many OBSW needs.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct MgmModel {
|
||||
id: mgm::Id,
|
||||
switch_state: SwitchStateBinary,
|
||||
external_mag_field: Option<mgm::SensorValuesMicroTesla>,
|
||||
spi_fault: mgm::SpiFault,
|
||||
pub reply: Output<SimReply>,
|
||||
}
|
||||
|
||||
#[Model]
|
||||
impl MgmModel {
|
||||
pub fn new(mgm_id: mgm::Id) -> Self {
|
||||
Self {
|
||||
id: mgm_id,
|
||||
switch_state: SwitchStateBinary::Off,
|
||||
external_mag_field: None,
|
||||
spi_fault: mgm::SpiFault::default(),
|
||||
reply: Output::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn switch_device(&mut self, switch_state: SwitchStateBinary) {
|
||||
self.switch_state = switch_state;
|
||||
if switch_state == SwitchStateBinary::Off && self.spi_fault.cleared_by_power_cycle {
|
||||
self.spi_fault = mgm::SpiFault::default();
|
||||
}
|
||||
}
|
||||
|
||||
/// Force (or clear) a stuck-bus SPI fault, for FDIR testing purposes.
|
||||
pub async fn set_spi_fault(&mut self, fault: mgm::SpiFault) {
|
||||
self.spi_fault = fault;
|
||||
}
|
||||
|
||||
pub async fn send_sensor_values(&mut self, _: (), cx: &Context<Self>) {
|
||||
let reply = SimReply::Mgm {
|
||||
id: self.id,
|
||||
reply: create_reply(
|
||||
self.switch_state,
|
||||
self.calculate_current_mgm_tuple(current_millis(cx.time())),
|
||||
self.spi_fault.mode,
|
||||
),
|
||||
};
|
||||
self.reply.send(reply).await;
|
||||
}
|
||||
|
||||
// Devices like magnetorquers generate a strong magnetic field which overrides the default
|
||||
// model for the measured magnetic field.
|
||||
pub async fn apply_external_magnetic_field(&mut self, field: mgm::SensorValuesMicroTesla) {
|
||||
self.external_mag_field = Some(field);
|
||||
}
|
||||
|
||||
pub async fn clear_external_magnetic_field(&mut self, _: ()) {
|
||||
self.external_mag_field = None;
|
||||
}
|
||||
|
||||
fn calculate_current_mgm_tuple(&self, time_ms: u64) -> mgm::SensorValuesMicroTesla {
|
||||
if SwitchStateBinary::On == self.switch_state {
|
||||
if let Some(ext_field) = self.external_mag_field {
|
||||
return ext_field;
|
||||
}
|
||||
let base_sin_val = 2.0 * PI * FREQUENCY_MGM * (time_ms as f32 / 1000.0);
|
||||
return mgm::SensorValuesMicroTesla {
|
||||
x: AMPLITUDE_MGM_UT * (base_sin_val + PHASE_X).sin(),
|
||||
y: AMPLITUDE_MGM_UT * (base_sin_val + PHASE_Y).sin(),
|
||||
z: AMPLITUDE_MGM_UT * (base_sin_val + PHASE_Z).sin(),
|
||||
};
|
||||
}
|
||||
mgm::SensorValuesMicroTesla {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
z: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the reply of the simulated LIS3MDL, including the raw register values.
|
||||
fn create_reply(
|
||||
switch_state: SwitchStateBinary,
|
||||
sensor_values: mgm::SensorValuesMicroTesla,
|
||||
fault_mode: mgm::SpiFaultMode,
|
||||
) -> mgm::Reply {
|
||||
// An injected fault always wins. A switched off device reads back like an undriven bus.
|
||||
let raw = match (fault_mode, switch_state) {
|
||||
(mgm::SpiFaultMode::AllZeros, _) => mgm::RawValues::splat(mgm::ALL_ZEROS_SENSOR_VAL),
|
||||
(mgm::SpiFaultMode::AllOnes, _) | (mgm::SpiFaultMode::None, SwitchStateBinary::Off) => {
|
||||
mgm::RawValues::splat(mgm::ALL_ONES_SENSOR_VAL)
|
||||
}
|
||||
(mgm::SpiFaultMode::None, SwitchStateBinary::On) => {
|
||||
raw_values_from_microtesla(sensor_values)
|
||||
}
|
||||
};
|
||||
mgm::Reply {
|
||||
switch_state,
|
||||
sensor_values,
|
||||
raw,
|
||||
}
|
||||
}
|
||||
|
||||
fn raw_values_from_microtesla(values: mgm::SensorValuesMicroTesla) -> mgm::RawValues {
|
||||
let to_raw = |microtesla: f32| {
|
||||
(microtesla / (mgm::GAUSS_TO_MICROTESLA_FACTOR as f32 * mgm::FIELD_LSB_PER_GAUSS_4_SENS))
|
||||
.round() as i16
|
||||
};
|
||||
mgm::RawValues {
|
||||
x: to_raw(values.x),
|
||||
y: to_raw(values.y),
|
||||
z: to_raw(values.z),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use minisim_types::{SimReply, SimRequest, acs::mgm};
|
||||
use types::pcdu::{SwitchId, SwitchStateBinary};
|
||||
|
||||
use crate::{
|
||||
eps::tests::{switch_device_off, switch_device_on},
|
||||
test_helpers::SimTestbench,
|
||||
};
|
||||
|
||||
fn request_sensor_data(sim_testbench: &mut SimTestbench, id: mgm::Id) -> mgm::Reply {
|
||||
let sim_reply = sim_testbench
|
||||
.request_reply(SimRequest::Mgm {
|
||||
id,
|
||||
request: mgm::Request::RequestSensorData,
|
||||
})
|
||||
.expect("no MGM reply received");
|
||||
let SimReply::Mgm {
|
||||
id: reply_id,
|
||||
reply,
|
||||
} = sim_reply
|
||||
else {
|
||||
panic!("unexpected reply {sim_reply:?}");
|
||||
};
|
||||
assert_eq!(reply_id, id);
|
||||
reply
|
||||
}
|
||||
|
||||
fn inject_spi_fault(sim_testbench: &mut SimTestbench, cleared_by_power_cycle: bool) {
|
||||
sim_testbench.send_and_step(SimRequest::Mgm {
|
||||
id: mgm::Id::Mgm0,
|
||||
request: mgm::Request::SetSpiFault(mgm::SpiFault {
|
||||
mode: mgm::SpiFaultMode::AllOnes,
|
||||
cleared_by_power_cycle,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
fn is_stuck_bus_reply(reply: &mgm::Reply) -> bool {
|
||||
reply.raw.x == -1 && reply.raw.y == -1 && reply.raw.z == -1
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_mgm_request() {
|
||||
let mut sim_testbench = SimTestbench::new();
|
||||
let reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
|
||||
assert_eq!(reply.switch_state, SwitchStateBinary::Off);
|
||||
assert_eq!(reply.sensor_values.x, 0.0);
|
||||
assert_eq!(reply.sensor_values.y, 0.0);
|
||||
assert_eq!(reply.sensor_values.z, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mgm_spi_fault_injection_all_ones() {
|
||||
let mut sim_testbench = SimTestbench::new();
|
||||
switch_device_on(&mut sim_testbench, SwitchId::Mgm0);
|
||||
inject_spi_fault(&mut sim_testbench, false);
|
||||
|
||||
let reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
|
||||
// Even though the device is switched on, the injected fault forces a stuck-bus reply.
|
||||
assert_eq!(reply.switch_state, SwitchStateBinary::On);
|
||||
assert!(is_stuck_bus_reply(&reply));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mgm_spi_fault_cleared_by_power_cycle() {
|
||||
let mut sim_testbench = SimTestbench::new();
|
||||
switch_device_on(&mut sim_testbench, SwitchId::Mgm0);
|
||||
inject_spi_fault(&mut sim_testbench, true);
|
||||
let reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
|
||||
assert!(is_stuck_bus_reply(&reply));
|
||||
|
||||
switch_device_off(&mut sim_testbench, SwitchId::Mgm0);
|
||||
switch_device_on(&mut sim_testbench, SwitchId::Mgm0);
|
||||
sim_testbench.step_until(Duration::from_millis(50)).unwrap();
|
||||
let reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
|
||||
assert!(!is_stuck_bus_reply(&reply));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mgm_spi_fault_persists_after_power_cycle() {
|
||||
let mut sim_testbench = SimTestbench::new();
|
||||
switch_device_on(&mut sim_testbench, SwitchId::Mgm0);
|
||||
inject_spi_fault(&mut sim_testbench, false);
|
||||
|
||||
switch_device_off(&mut sim_testbench, SwitchId::Mgm0);
|
||||
switch_device_on(&mut sim_testbench, SwitchId::Mgm0);
|
||||
let reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
|
||||
assert_eq!(reply.switch_state, SwitchStateBinary::On);
|
||||
assert!(is_stuck_bus_reply(&reply));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_mgm_request_switched_on() {
|
||||
let mut sim_testbench = SimTestbench::new();
|
||||
switch_device_on(&mut sim_testbench, SwitchId::Mgm0);
|
||||
|
||||
let first_reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
|
||||
sim_testbench.step_until(Duration::from_millis(50)).unwrap();
|
||||
let second_reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
|
||||
|
||||
let to_microtesla = |raw: i16| {
|
||||
raw as f32 * mgm::FIELD_LSB_PER_GAUSS_4_SENS * mgm::GAUSS_TO_MICROTESLA_FACTOR as f32
|
||||
};
|
||||
let values = second_reply.sensor_values;
|
||||
let raw = second_reply.raw;
|
||||
for (value, raw) in [(values.x, raw.x), (values.y, raw.y), (values.z, raw.z)] {
|
||||
let diff = (value - to_microtesla(raw)).abs();
|
||||
assert!(diff < 0.01, "raw value conversion diff too large: {diff}");
|
||||
}
|
||||
// Check that the values are changing.
|
||||
assert_ne!(first_reply, second_reply);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mgm_1_request_switched_on() {
|
||||
let mut sim_testbench = SimTestbench::new();
|
||||
switch_device_on(&mut sim_testbench, SwitchId::Mgm1);
|
||||
|
||||
let mgm_0_reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm0);
|
||||
assert_eq!(mgm_0_reply.switch_state, SwitchStateBinary::Off);
|
||||
let mgm_1_reply = request_sensor_data(&mut sim_testbench, mgm::Id::Mgm1);
|
||||
assert_eq!(mgm_1_reply.switch_state, SwitchStateBinary::On);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
use minisim_types::{
|
||||
SimReply,
|
||||
acs::{mgm, mgt},
|
||||
};
|
||||
use nexosim::{
|
||||
model::{Context, Model, schedulable},
|
||||
ports::Output,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use types::pcdu::SwitchStateBinary;
|
||||
|
||||
/// Time the device needs to answer a command.
|
||||
const REPLY_DELAY: Duration = Duration::from_millis(15);
|
||||
|
||||
/// Simple magnetorquer simulation model.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct MgtModel {
|
||||
switch_state: SwitchStateBinary,
|
||||
torquing: bool,
|
||||
torque_dipole: mgt::Dipole,
|
||||
pub gen_magnetic_field: Output<mgm::SensorValuesMicroTesla>,
|
||||
pub clear_magnetic_field: Output<()>,
|
||||
pub reply: Output<SimReply>,
|
||||
}
|
||||
|
||||
#[Model]
|
||||
impl MgtModel {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
switch_state: SwitchStateBinary::Off,
|
||||
torquing: false,
|
||||
torque_dipole: mgt::Dipole::default(),
|
||||
gen_magnetic_field: Output::new(),
|
||||
clear_magnetic_field: Output::new(),
|
||||
reply: Output::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn apply_torque(
|
||||
&mut self,
|
||||
duration_and_dipole: (Duration, mgt::Dipole),
|
||||
cx: &Context<Self>,
|
||||
) {
|
||||
if self.switch_state != SwitchStateBinary::On {
|
||||
return;
|
||||
}
|
||||
self.torque_dipole = duration_and_dipole.1;
|
||||
self.torquing = true;
|
||||
if cx
|
||||
.schedule_event(duration_and_dipole.0, schedulable!(Self::clear_torque), ())
|
||||
.is_err()
|
||||
{
|
||||
log::warn!("torque clearing can only be set for a future time.");
|
||||
}
|
||||
self.generate_magnetic_field(()).await;
|
||||
self.schedule_reply(mgt::Reply::Ack, cx);
|
||||
}
|
||||
|
||||
#[nexosim(schedulable)]
|
||||
async fn clear_torque(&mut self) {
|
||||
self.torque_dipole = mgt::Dipole::default();
|
||||
self.torquing = false;
|
||||
self.clear_magnetic_field.send(()).await;
|
||||
}
|
||||
|
||||
pub async fn switch_device(&mut self, switch_state: SwitchStateBinary) {
|
||||
self.switch_state = switch_state;
|
||||
match switch_state {
|
||||
SwitchStateBinary::On => self.generate_magnetic_field(()).await,
|
||||
SwitchStateBinary::Off => self.clear_torque().await,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn request_housekeeping_data(&mut self, _: (), cx: &Context<Self>) {
|
||||
if self.switch_state != SwitchStateBinary::On {
|
||||
return;
|
||||
}
|
||||
// The HK is sampled when the command is processed, not when the reply is sent.
|
||||
let hk = mgt::HkSet {
|
||||
dipole: self.torque_dipole,
|
||||
torquing: self.torquing,
|
||||
};
|
||||
self.schedule_reply(mgt::Reply::Hk(hk), cx);
|
||||
}
|
||||
|
||||
fn schedule_reply(&self, reply: mgt::Reply, cx: &Context<Self>) {
|
||||
cx.schedule_event(REPLY_DELAY, schedulable!(Self::send_reply), reply)
|
||||
.expect("scheduling MGT reply failed")
|
||||
}
|
||||
|
||||
#[nexosim(schedulable)]
|
||||
async fn send_reply(&mut self, reply: mgt::Reply) {
|
||||
self.reply.send(SimReply::from(reply)).await;
|
||||
}
|
||||
|
||||
fn calc_magnetic_field(&self, _: mgt::Dipole) -> mgm::SensorValuesMicroTesla {
|
||||
// Simplified model: Just returns some fixed magnetic field for now.
|
||||
// Later, we could make this more fancy by incorporating the commanded dipole.
|
||||
mgm::MGT_GEN_MAGNETIC_FIELD
|
||||
}
|
||||
|
||||
/// A torquing magnetorquer generates a magnetic field. This function can be used to apply
|
||||
/// the magnetic field.
|
||||
async fn generate_magnetic_field(&mut self, _: ()) {
|
||||
if self.switch_state != SwitchStateBinary::On || !self.torquing {
|
||||
return;
|
||||
}
|
||||
self.gen_magnetic_field
|
||||
.send(self.calc_magnetic_field(self.torque_dipole))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use minisim_types::{
|
||||
SimReply, SimRequest, SimRequestWithTime,
|
||||
acs::{mgm, mgt},
|
||||
eps::PcduRequest,
|
||||
};
|
||||
use types::pcdu::{SwitchId, SwitchStateBinary};
|
||||
|
||||
use crate::{eps::tests::switch_device_on, test_helpers::SimTestbench};
|
||||
|
||||
fn decode_reply(sim_reply: SimReply) -> mgt::Reply {
|
||||
let SimReply::Mgt(frame) = sim_reply else {
|
||||
panic!("unexpected reply {sim_reply:?}");
|
||||
};
|
||||
mgt::Reply::from_frame(&frame).expect("invalid MGT reply frame")
|
||||
}
|
||||
|
||||
fn request_hk(sim_testbench: &mut SimTestbench) -> Option<mgt::HkSet> {
|
||||
let sim_reply = sim_testbench.request_reply(mgt::Request::RequestHk)?;
|
||||
let mgt::Reply::Hk(hk) = decode_reply(sim_reply) else {
|
||||
panic!("unexpected MGT reply");
|
||||
};
|
||||
Some(hk)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_mgt_request_is_off() {
|
||||
let mut sim_testbench = SimTestbench::new();
|
||||
assert!(request_hk(&mut sim_testbench).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_mgt_request_is_on() {
|
||||
let mut sim_testbench = SimTestbench::new();
|
||||
switch_device_on(&mut sim_testbench, SwitchId::Mgt);
|
||||
assert_eq!(
|
||||
request_hk(&mut sim_testbench),
|
||||
Some(mgt::HkSet {
|
||||
dipole: mgt::Dipole::default(),
|
||||
torquing: false,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_mgt_request_is_on_and_torquing() {
|
||||
let mut sim_testbench = SimTestbench::new();
|
||||
switch_device_on(&mut sim_testbench, SwitchId::Mgt);
|
||||
let commanded_dipole = mgt::Dipole {
|
||||
x: -200,
|
||||
y: 200,
|
||||
z: 1000,
|
||||
};
|
||||
let request = SimRequestWithTime::new_with_epoch_time(mgt::Request::ApplyTorque {
|
||||
duration: Duration::from_millis(100),
|
||||
dipole: commanded_dipole,
|
||||
});
|
||||
sim_testbench
|
||||
.send_request(request)
|
||||
.expect("sending MGT request failed");
|
||||
sim_testbench.handle_sim_requests_time_agnostic();
|
||||
sim_testbench.step_until(Duration::from_millis(20)).unwrap();
|
||||
let ack = sim_testbench
|
||||
.try_receive_next_reply()
|
||||
.expect("no torque command ack");
|
||||
assert_eq!(decode_reply(ack), mgt::Reply::Ack);
|
||||
|
||||
assert_eq!(
|
||||
request_hk(&mut sim_testbench),
|
||||
Some(mgt::HkSet {
|
||||
dipole: commanded_dipole,
|
||||
torquing: true,
|
||||
})
|
||||
);
|
||||
sim_testbench
|
||||
.step_until(Duration::from_millis(100))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
request_hk(&mut sim_testbench),
|
||||
Some(mgt::HkSet {
|
||||
dipole: mgt::Dipole::default(),
|
||||
torquing: false,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_torque_command_not_acked_when_off() {
|
||||
let mut sim_testbench = SimTestbench::new();
|
||||
let reply = sim_testbench.request_reply(mgt::Request::ApplyTorque {
|
||||
duration: Duration::from_millis(100),
|
||||
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
|
||||
});
|
||||
assert!(reply.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_frame_is_dropped() {
|
||||
let mut sim_testbench = SimTestbench::new();
|
||||
switch_device_on(&mut sim_testbench, SwitchId::Mgt);
|
||||
assert!(
|
||||
sim_testbench
|
||||
.request_reply(SimRequest::Mgt(vec![0x01, 0x00]))
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
/// Processes the request without stepping, so scheduled events like the torque clearing do
|
||||
/// not fire.
|
||||
fn process_without_step(sim_testbench: &mut SimTestbench, request: impl Into<SimRequest>) {
|
||||
sim_testbench
|
||||
.send_request(SimRequestWithTime::new_with_epoch_time(request))
|
||||
.expect("sending request failed");
|
||||
sim_testbench.handle_sim_requests_time_agnostic();
|
||||
}
|
||||
|
||||
fn read_mgm_0_field(sim_testbench: &mut SimTestbench) -> mgm::SensorValuesMicroTesla {
|
||||
process_without_step(
|
||||
sim_testbench,
|
||||
SimRequest::Mgm {
|
||||
id: mgm::Id::Mgm0,
|
||||
request: mgm::Request::RequestSensorData,
|
||||
},
|
||||
);
|
||||
// Skips pending MGT replies, for example torque command acks.
|
||||
loop {
|
||||
let sim_reply = sim_testbench
|
||||
.try_receive_next_reply()
|
||||
.expect("no MGM reply received");
|
||||
if let SimReply::Mgm { reply, .. } = sim_reply {
|
||||
return reply.sensor_values;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn start_torquing(sim_testbench: &mut SimTestbench, duration: Duration) {
|
||||
switch_device_on(sim_testbench, SwitchId::Mgm0);
|
||||
switch_device_on(sim_testbench, SwitchId::Mgt);
|
||||
process_without_step(
|
||||
sim_testbench,
|
||||
mgt::Request::ApplyTorque {
|
||||
duration,
|
||||
dipole: mgt::Dipole { x: 1, y: 2, z: 3 },
|
||||
},
|
||||
);
|
||||
assert_eq!(read_mgm_0_field(sim_testbench), mgm::MGT_GEN_MAGNETIC_FIELD);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mgm_field_cleared_after_torquing() {
|
||||
let mut sim_testbench = SimTestbench::new();
|
||||
start_torquing(&mut sim_testbench, Duration::from_millis(100));
|
||||
sim_testbench
|
||||
.step_until(Duration::from_millis(100))
|
||||
.unwrap();
|
||||
assert_ne!(
|
||||
read_mgm_0_field(&mut sim_testbench),
|
||||
mgm::MGT_GEN_MAGNETIC_FIELD
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mgm_field_cleared_by_switching_mgt_off() {
|
||||
let mut sim_testbench = SimTestbench::new();
|
||||
start_torquing(&mut sim_testbench, Duration::from_millis(100));
|
||||
process_without_step(
|
||||
&mut sim_testbench,
|
||||
PcduRequest::SwitchDevice {
|
||||
switch: SwitchId::Mgt,
|
||||
state: SwitchStateBinary::Off,
|
||||
},
|
||||
);
|
||||
assert_ne!(
|
||||
read_mgm_0_field(&mut sim_testbench),
|
||||
mgm::MGT_GEN_MAGNETIC_FIELD
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod mgm;
|
||||
pub mod mgt;
|
||||
@@ -0,0 +1,309 @@
|
||||
use std::{
|
||||
sync::mpsc,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
use minisim_types::{
|
||||
SimCtrlReply, SimCtrlRequest, SimReply, SimRequest, SimRequestWithTime,
|
||||
acs::{mgm, mgt},
|
||||
eps::PcduRequest,
|
||||
};
|
||||
use nexosim::{
|
||||
ports::{EventQueueReader, EventSinkReader, EventSource, SinkState, event_queue},
|
||||
simulation::{EventId, ExecutionError, Mailbox, SimInit, Simulation},
|
||||
time::{Clock, Deadline, MonotonicTime, SystemClock},
|
||||
};
|
||||
use types::pcdu::{SwitchId, SwitchStateBinary};
|
||||
|
||||
use crate::{
|
||||
acs::{mgm::MgmModel, mgt::MgtModel},
|
||||
eps::PcduModel,
|
||||
};
|
||||
|
||||
const WARNING_FOR_STALE_DATA: bool = false;
|
||||
|
||||
const SIM_CTRL_REQ_WIRETAPPING: bool = false;
|
||||
const MGM_REQ_WIRETAPPING: bool = false;
|
||||
const PCDU_REQ_WIRETAPPING: bool = false;
|
||||
const MGT_REQ_WIRETAPPING: bool = false;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum ThreadingModel {
|
||||
Default = 0,
|
||||
Single = 1,
|
||||
}
|
||||
|
||||
struct MgmInputs {
|
||||
send_sensor_values: EventId<()>,
|
||||
set_spi_fault: EventId<mgm::SpiFault>,
|
||||
}
|
||||
|
||||
impl MgmInputs {
|
||||
fn register(sim_init: &mut SimInit, mailbox: &Mailbox<MgmModel>) -> Self {
|
||||
Self {
|
||||
send_sensor_values: EventSource::new()
|
||||
.connect(MgmModel::send_sensor_values, mailbox)
|
||||
.register(sim_init),
|
||||
set_spi_fault: EventSource::new()
|
||||
.connect(MgmModel::set_spi_fault, mailbox)
|
||||
.register(sim_init),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Model inputs which are driven by simulation requests.
|
||||
struct ModelInputs {
|
||||
mgm_0: MgmInputs,
|
||||
mgm_1: MgmInputs,
|
||||
pcdu_request_switch_info: EventId<()>,
|
||||
pcdu_switch_device: EventId<(SwitchId, SwitchStateBinary)>,
|
||||
mgt_apply_torque: EventId<(Duration, mgt::Dipole)>,
|
||||
mgt_request_hk: EventId<()>,
|
||||
}
|
||||
|
||||
// The simulation controller processes requests and drives the simulation.
|
||||
pub struct SimController {
|
||||
sys_clock: SystemClock,
|
||||
request_receiver: mpsc::Receiver<SimRequestWithTime>,
|
||||
reply_sender: mpsc::Sender<SimReply>,
|
||||
simulation: Simulation,
|
||||
inputs: ModelInputs,
|
||||
model_replies: EventQueueReader<SimReply>,
|
||||
}
|
||||
|
||||
impl SimController {
|
||||
pub fn new(
|
||||
threading_model: ThreadingModel,
|
||||
start_time: MonotonicTime,
|
||||
reply_sender: mpsc::Sender<SimReply>,
|
||||
request_receiver: mpsc::Receiver<SimRequestWithTime>,
|
||||
) -> Self {
|
||||
let mut mgm_0_model = MgmModel::new(mgm::Id::Mgm0);
|
||||
let mut mgm_1_model = MgmModel::new(mgm::Id::Mgm1);
|
||||
let mut pcdu_model = PcduModel::new();
|
||||
let mut mgt_model = MgtModel::new();
|
||||
|
||||
let mgm_0_mailbox = Mailbox::new();
|
||||
let mgm_1_mailbox = Mailbox::new();
|
||||
let pcdu_mailbox = Mailbox::new();
|
||||
let mgt_mailbox = Mailbox::new();
|
||||
|
||||
pcdu_model
|
||||
.mgm_0_switch
|
||||
.connect(MgmModel::switch_device, &mgm_0_mailbox);
|
||||
pcdu_model
|
||||
.mgm_1_switch
|
||||
.connect(MgmModel::switch_device, &mgm_1_mailbox);
|
||||
pcdu_model
|
||||
.mgt_switch
|
||||
.connect(MgtModel::switch_device, &mgt_mailbox);
|
||||
mgt_model
|
||||
.gen_magnetic_field
|
||||
.connect(MgmModel::apply_external_magnetic_field, &mgm_0_mailbox);
|
||||
mgt_model
|
||||
.gen_magnetic_field
|
||||
.connect(MgmModel::apply_external_magnetic_field, &mgm_1_mailbox);
|
||||
mgt_model
|
||||
.clear_magnetic_field
|
||||
.connect(MgmModel::clear_external_magnetic_field, &mgm_0_mailbox);
|
||||
mgt_model
|
||||
.clear_magnetic_field
|
||||
.connect(MgmModel::clear_external_magnetic_field, &mgm_1_mailbox);
|
||||
|
||||
let (reply_sink, model_replies) = event_queue(SinkState::Enabled);
|
||||
mgm_0_model.reply.connect_sink(reply_sink.clone());
|
||||
mgm_1_model.reply.connect_sink(reply_sink.clone());
|
||||
pcdu_model.reply.connect_sink(reply_sink.clone());
|
||||
mgt_model.reply.connect_sink(reply_sink);
|
||||
|
||||
let mut sim_init = if threading_model == ThreadingModel::Single {
|
||||
SimInit::with_num_threads(1)
|
||||
} else {
|
||||
SimInit::new()
|
||||
};
|
||||
let inputs = ModelInputs {
|
||||
mgm_0: MgmInputs::register(&mut sim_init, &mgm_0_mailbox),
|
||||
mgm_1: MgmInputs::register(&mut sim_init, &mgm_1_mailbox),
|
||||
pcdu_request_switch_info: EventSource::new()
|
||||
.connect(PcduModel::request_switch_info, &pcdu_mailbox)
|
||||
.register(&mut sim_init),
|
||||
pcdu_switch_device: EventSource::new()
|
||||
.connect(PcduModel::switch_device, &pcdu_mailbox)
|
||||
.register(&mut sim_init),
|
||||
mgt_apply_torque: EventSource::new()
|
||||
.connect(MgtModel::apply_torque, &mgt_mailbox)
|
||||
.register(&mut sim_init),
|
||||
mgt_request_hk: EventSource::new()
|
||||
.connect(MgtModel::request_housekeeping_data, &mgt_mailbox)
|
||||
.register(&mut sim_init),
|
||||
};
|
||||
let simulation = sim_init
|
||||
.add_model(mgm_0_model, mgm_0_mailbox, "MGM 0 model")
|
||||
.add_model(mgm_1_model, mgm_1_mailbox, "MGM 1 model")
|
||||
.add_model(pcdu_model, pcdu_mailbox, "PCDU model")
|
||||
.add_model(mgt_model, mgt_mailbox, "MGT model")
|
||||
.init(start_time)
|
||||
.unwrap();
|
||||
Self {
|
||||
sys_clock: SystemClock::from_system_time(start_time, SystemTime::now()),
|
||||
request_receiver,
|
||||
reply_sender,
|
||||
simulation,
|
||||
inputs,
|
||||
model_replies,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn step(&mut self) -> Result<(), ExecutionError> {
|
||||
self.simulation.step()?;
|
||||
self.forward_model_replies();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn step_until(&mut self, deadline: impl Deadline) -> Result<(), ExecutionError> {
|
||||
self.simulation.step_until(deadline)?;
|
||||
self.forward_model_replies();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn forward_model_replies(&mut self) {
|
||||
while let Some(reply) = self.model_replies.try_read() {
|
||||
self.reply_sender
|
||||
.send(reply)
|
||||
.expect("sending model reply failed");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(&mut self, start_time: MonotonicTime, udp_polling_interval_ms: u64) {
|
||||
let mut t = start_time;
|
||||
loop {
|
||||
let t_old = t;
|
||||
// Check for UDP requests every millisecond. Shift the simulator ahead here to prevent
|
||||
// replies lying in the past.
|
||||
t += Duration::from_millis(udp_polling_interval_ms);
|
||||
let _synch_status = self.sys_clock.synchronize(t);
|
||||
self.handle_sim_requests(t_old);
|
||||
self.step_until(t).expect("simulation step failed");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_sim_requests(&mut self, old_timestamp: MonotonicTime) {
|
||||
loop {
|
||||
match self.request_receiver.try_recv() {
|
||||
Ok(request) => {
|
||||
if request.timestamp < old_timestamp && WARNING_FOR_STALE_DATA {
|
||||
log::warn!("stale data with timestamp {:?} received", request.timestamp);
|
||||
}
|
||||
match request.request {
|
||||
SimRequest::SimCtrl(request) => self.handle_ctrl_request(request),
|
||||
SimRequest::Mgm { id, request } => self.handle_mgm_request(id, request),
|
||||
SimRequest::Mgt(request) => self.handle_mgt_request(request),
|
||||
SimRequest::Pcdu(request) => self.handle_pcdu_request(request),
|
||||
}
|
||||
}
|
||||
Err(e) => match e {
|
||||
mpsc::TryRecvError::Empty => break,
|
||||
mpsc::TryRecvError::Disconnected => {
|
||||
panic!("all request sender disconnected")
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
self.forward_model_replies();
|
||||
}
|
||||
|
||||
fn handle_ctrl_request(&mut self, sim_ctrl_request: SimCtrlRequest) {
|
||||
if SIM_CTRL_REQ_WIRETAPPING {
|
||||
log::info!("received sim ctrl request: {sim_ctrl_request:?}");
|
||||
}
|
||||
match sim_ctrl_request {
|
||||
SimCtrlRequest::Ping => {
|
||||
log::info!("received ping request, a client is connecting");
|
||||
self.reply_sender
|
||||
.send(SimReply::from(SimCtrlReply::Pong))
|
||||
.expect("sending reply from sim controller failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_mgm_request(&mut self, mgm_id: mgm::Id, mgm_request: mgm::Request) {
|
||||
let inputs = match mgm_id {
|
||||
mgm::Id::Mgm0 => &self.inputs.mgm_0,
|
||||
mgm::Id::Mgm1 => &self.inputs.mgm_1,
|
||||
};
|
||||
if MGM_REQ_WIRETAPPING {
|
||||
log::info!("received {mgm_id:?} request: {mgm_request:?}");
|
||||
}
|
||||
match mgm_request {
|
||||
mgm::Request::RequestSensorData => {
|
||||
self.simulation
|
||||
.process_event(&inputs.send_sensor_values, ())
|
||||
.expect("event execution error for mgm");
|
||||
}
|
||||
mgm::Request::SetSpiFault(fault_mode) => {
|
||||
log::info!("{mgm_id:?}: setting SPI fault mode to {fault_mode:?}");
|
||||
self.simulation
|
||||
.process_event(&inputs.set_spi_fault, fault_mode)
|
||||
.expect("event execution error for mgm");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_pcdu_request(&mut self, pcdu_request: PcduRequest) {
|
||||
if PCDU_REQ_WIRETAPPING {
|
||||
log::info!("received PCDU request: {pcdu_request:?}");
|
||||
}
|
||||
match pcdu_request {
|
||||
PcduRequest::RequestSwitchInfo => {
|
||||
self.simulation
|
||||
.process_event(&self.inputs.pcdu_request_switch_info, ())
|
||||
.unwrap();
|
||||
}
|
||||
PcduRequest::SwitchDevice { switch, state } => {
|
||||
self.simulation
|
||||
.process_event(&self.inputs.pcdu_switch_device, (switch, state))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_mgt_request(&mut self, frame: Vec<u8>) {
|
||||
let mgt_request = match mgt::Request::from_frame(&frame) {
|
||||
Ok(request) => request,
|
||||
Err(e) => {
|
||||
log::warn!("dropping invalid MGT frame {frame:02x?}: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
if MGT_REQ_WIRETAPPING {
|
||||
log::info!("received MGT request: {mgt_request:?}");
|
||||
}
|
||||
match mgt_request {
|
||||
mgt::Request::ApplyTorque { duration, dipole } => self
|
||||
.simulation
|
||||
.process_event(&self.inputs.mgt_apply_torque, (duration, dipole))
|
||||
.unwrap(),
|
||||
mgt::Request::RequestHk => self
|
||||
.simulation
|
||||
.process_event(&self.inputs.mgt_request_hk, ())
|
||||
.unwrap(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::test_helpers::SimTestbench;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_basic_ping() {
|
||||
let mut sim_testbench = SimTestbench::new();
|
||||
assert_eq!(
|
||||
sim_testbench.request_reply(SimCtrlRequest::Ping),
|
||||
Some(SimReply::SimCtrl(SimCtrlReply::Pong))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use minisim_types::{SimReply, eps::PcduReply};
|
||||
use nexosim::{
|
||||
model::{Context, Model, schedulable},
|
||||
ports::Output,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use types::pcdu::{SwitchId, SwitchMapBinary, SwitchMapBinaryWrapper, SwitchStateBinary};
|
||||
|
||||
pub const SWITCH_INFO_DELAY_MS: u64 = 10;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct PcduModel {
|
||||
switcher_map: SwitchMapBinary,
|
||||
pub mgm_0_switch: Output<SwitchStateBinary>,
|
||||
pub mgm_1_switch: Output<SwitchStateBinary>,
|
||||
pub mgt_switch: Output<SwitchStateBinary>,
|
||||
pub reply: Output<SimReply>,
|
||||
}
|
||||
|
||||
#[Model]
|
||||
impl PcduModel {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
switcher_map: SwitchMapBinaryWrapper::default().0,
|
||||
mgm_0_switch: Output::new(),
|
||||
mgm_1_switch: Output::new(),
|
||||
mgt_switch: Output::new(),
|
||||
reply: Output::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn request_switch_info(&mut self, _: (), cx: &Context<Self>) {
|
||||
cx.schedule_event(
|
||||
Duration::from_millis(SWITCH_INFO_DELAY_MS),
|
||||
schedulable!(Self::send_switch_info),
|
||||
(),
|
||||
)
|
||||
.expect("requesting switch info failed");
|
||||
}
|
||||
|
||||
#[nexosim(schedulable)]
|
||||
async fn send_switch_info(&mut self) {
|
||||
let reply = SimReply::from(PcduReply::SwitchInfo(self.switcher_map.clone()));
|
||||
self.reply.send(reply).await;
|
||||
}
|
||||
|
||||
pub async fn switch_device(&mut self, switch_and_target_state: (SwitchId, SwitchStateBinary)) {
|
||||
log::info!(
|
||||
"switching {:?} to {:?}",
|
||||
switch_and_target_state.0,
|
||||
switch_and_target_state.1
|
||||
);
|
||||
let val = self
|
||||
.switcher_map
|
||||
.get_mut(&switch_and_target_state.0)
|
||||
.unwrap_or_else(|| panic!("switch {:?} not found", switch_and_target_state.0));
|
||||
*val = switch_and_target_state.1;
|
||||
match switch_and_target_state.0 {
|
||||
SwitchId::Mgm0 => {
|
||||
self.mgm_0_switch.send(switch_and_target_state.1).await;
|
||||
}
|
||||
SwitchId::Mgm1 => {
|
||||
self.mgm_1_switch.send(switch_and_target_state.1).await;
|
||||
}
|
||||
SwitchId::Mgt => {
|
||||
self.mgt_switch.send(switch_and_target_state.1).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use std::time::Duration;
|
||||
|
||||
use minisim_types::{SimRequestWithTime, eps::PcduRequest};
|
||||
use types::pcdu::SwitchMapBinary;
|
||||
|
||||
use crate::test_helpers::SimTestbench;
|
||||
|
||||
fn switch_device(
|
||||
sim_testbench: &mut SimTestbench,
|
||||
switch: SwitchId,
|
||||
target: SwitchStateBinary,
|
||||
) {
|
||||
sim_testbench.send_and_step(PcduRequest::SwitchDevice {
|
||||
switch,
|
||||
state: target,
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn switch_device_off(sim_testbench: &mut SimTestbench, switch: SwitchId) {
|
||||
switch_device(sim_testbench, switch, SwitchStateBinary::Off);
|
||||
}
|
||||
pub(crate) fn switch_device_on(sim_testbench: &mut SimTestbench, switch: SwitchId) {
|
||||
switch_device(sim_testbench, switch, SwitchStateBinary::On);
|
||||
}
|
||||
|
||||
pub(crate) fn get_all_off_switch_map() -> SwitchMapBinary {
|
||||
SwitchMapBinaryWrapper::default().0
|
||||
}
|
||||
|
||||
fn unwrap_switch_map(sim_reply: SimReply) -> SwitchMapBinary {
|
||||
let SimReply::Pcdu(PcduReply::SwitchInfo(switch_map)) = sim_reply else {
|
||||
panic!("unexpected reply {sim_reply:?}");
|
||||
};
|
||||
switch_map
|
||||
}
|
||||
|
||||
fn check_switch_state(sim_testbench: &mut SimTestbench, expected_switch_map: &SwitchMapBinary) {
|
||||
let sim_reply = sim_testbench
|
||||
.request_reply(PcduRequest::RequestSwitchInfo)
|
||||
.expect("no PCDU reply received");
|
||||
assert_eq!(unwrap_switch_map(sim_reply), *expected_switch_map);
|
||||
}
|
||||
|
||||
fn test_pcdu_switching_single_switch(switch: SwitchId, target: SwitchStateBinary) {
|
||||
let mut sim_testbench = SimTestbench::new();
|
||||
switch_device(&mut sim_testbench, switch, target);
|
||||
let mut switcher_map = get_all_off_switch_map();
|
||||
*switcher_map.get_mut(&switch).unwrap() = target;
|
||||
check_switch_state(&mut sim_testbench, &switcher_map);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pcdu_switcher_request() {
|
||||
let mut sim_testbench = SimTestbench::new();
|
||||
let request = SimRequestWithTime::new_with_epoch_time(PcduRequest::RequestSwitchInfo);
|
||||
sim_testbench
|
||||
.send_request(request)
|
||||
.expect("sending PCDU request failed");
|
||||
sim_testbench.handle_sim_requests_time_agnostic();
|
||||
sim_testbench.step_until(Duration::from_millis(1)).unwrap();
|
||||
assert!(sim_testbench.try_receive_next_reply().is_none());
|
||||
|
||||
// The reply is delayed by SWITCH_INFO_DELAY_MS.
|
||||
sim_testbench.step_until(Duration::from_millis(25)).unwrap();
|
||||
let sim_reply = sim_testbench
|
||||
.try_receive_next_reply()
|
||||
.expect("no PCDU reply received");
|
||||
assert_eq!(unwrap_switch_map(sim_reply), get_all_off_switch_map());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pcdu_switching_mgm_on() {
|
||||
test_pcdu_switching_single_switch(SwitchId::Mgm0, SwitchStateBinary::On);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pcdu_switching_mgt_on() {
|
||||
test_pcdu_switching_single_switch(SwitchId::Mgt, SwitchStateBinary::On);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pcdu_switching_mgt_off() {
|
||||
test_pcdu_switching_single_switch(SwitchId::Mgt, SwitchStateBinary::On);
|
||||
test_pcdu_switching_single_switch(SwitchId::Mgt, SwitchStateBinary::Off);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
use controller::{SimController, ThreadingModel};
|
||||
use minisim_types::udp::SIM_CTRL_PORT;
|
||||
use nexosim::time::MonotonicTime;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use udp::SimUdpServer;
|
||||
|
||||
mod acs;
|
||||
mod controller;
|
||||
mod eps;
|
||||
#[cfg(test)]
|
||||
mod test_helpers;
|
||||
mod time;
|
||||
mod udp;
|
||||
|
||||
fn main() {
|
||||
let (request_sender, request_receiver) = mpsc::channel();
|
||||
let (reply_sender, reply_receiver) = mpsc::channel();
|
||||
let t0 = MonotonicTime::EPOCH;
|
||||
let mut sim_ctrl =
|
||||
SimController::new(ThreadingModel::Default, t0, reply_sender, request_receiver);
|
||||
// Configure logger at runtime
|
||||
fern::Dispatch::new()
|
||||
// Perform allocation-free log formatting
|
||||
.format(|out, message, record| {
|
||||
out.finish(format_args!(
|
||||
"[{} {} {}] {}",
|
||||
humantime::format_rfc3339(std::time::SystemTime::now()),
|
||||
record.level(),
|
||||
record.target(),
|
||||
message
|
||||
))
|
||||
})
|
||||
// Add blanket level filter -
|
||||
.level(log::LevelFilter::Debug)
|
||||
// - and per-module overrides
|
||||
// Output to stdout, files, and other Dispatch configurations
|
||||
.chain(std::io::stdout())
|
||||
.chain(fern::log_file("output.log").expect("could not open log output file"))
|
||||
// Apply globally
|
||||
.apply()
|
||||
.expect("could not apply logger configuration");
|
||||
|
||||
log::info!("starting simulation thread");
|
||||
// This thread schedules the simulator.
|
||||
let sim_thread = thread::spawn(move || {
|
||||
sim_ctrl.run(t0, 1);
|
||||
});
|
||||
|
||||
let mut udp_server =
|
||||
SimUdpServer::new(SIM_CTRL_PORT, request_sender, reply_receiver, 200, None)
|
||||
.expect("could not create UDP request server");
|
||||
log::info!("starting UDP server on port {SIM_CTRL_PORT}");
|
||||
// This thread manages the simulator UDP server.
|
||||
let udp_tc_thread = thread::spawn(move || {
|
||||
udp_server.run();
|
||||
});
|
||||
|
||||
sim_thread.join().expect("joining simulation thread failed");
|
||||
udp_tc_thread
|
||||
.join()
|
||||
.expect("joining UDP server thread failed");
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
use delegate::delegate;
|
||||
use std::sync::mpsc;
|
||||
|
||||
use minisim_types::{SimReply, SimRequest, SimRequestWithTime};
|
||||
use nexosim::{
|
||||
simulation::ExecutionError,
|
||||
time::{Deadline, MonotonicTime},
|
||||
};
|
||||
|
||||
use crate::controller::{SimController, ThreadingModel};
|
||||
|
||||
pub struct SimTestbench {
|
||||
pub sim_controller: SimController,
|
||||
pub reply_receiver: mpsc::Receiver<SimReply>,
|
||||
pub request_sender: mpsc::Sender<SimRequestWithTime>,
|
||||
}
|
||||
|
||||
impl SimTestbench {
|
||||
pub fn new() -> Self {
|
||||
let (request_sender, request_receiver) = mpsc::channel();
|
||||
let (reply_sender, reply_receiver) = mpsc::channel();
|
||||
let t0 = MonotonicTime::EPOCH;
|
||||
let sim_ctrl =
|
||||
SimController::new(ThreadingModel::Single, t0, reply_sender, request_receiver);
|
||||
|
||||
Self {
|
||||
sim_controller: sim_ctrl,
|
||||
reply_receiver,
|
||||
request_sender,
|
||||
}
|
||||
}
|
||||
pub fn handle_sim_requests_time_agnostic(&mut self) {
|
||||
self.handle_sim_requests(MonotonicTime::EPOCH);
|
||||
}
|
||||
|
||||
delegate! {
|
||||
to self.sim_controller {
|
||||
pub fn handle_sim_requests(&mut self, old_timestamp: MonotonicTime);
|
||||
pub fn step(&mut self) -> Result<(), ExecutionError>;
|
||||
pub fn step_until(&mut self, duration: impl Deadline) -> Result<(), ExecutionError>;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_request(
|
||||
&self,
|
||||
request: SimRequestWithTime,
|
||||
) -> Result<(), mpsc::SendError<SimRequestWithTime>> {
|
||||
self.request_sender.send(request)
|
||||
}
|
||||
|
||||
/// Sends the request and steps the simulation to the next scheduled event.
|
||||
pub fn send_and_step(&mut self, request: impl Into<SimRequest>) {
|
||||
self.send_request(SimRequestWithTime::new_with_epoch_time(request))
|
||||
.expect("sending request failed");
|
||||
self.handle_sim_requests_time_agnostic();
|
||||
self.step().unwrap();
|
||||
}
|
||||
|
||||
pub fn request_reply(&mut self, request: impl Into<SimRequest>) -> Option<SimReply> {
|
||||
self.send_and_step(request);
|
||||
self.try_receive_next_reply()
|
||||
}
|
||||
|
||||
pub fn try_receive_next_reply(&self) -> Option<SimReply> {
|
||||
match self.reply_receiver.try_recv() {
|
||||
Ok(reply) => Some(reply),
|
||||
Err(e) => {
|
||||
if e == mpsc::TryRecvError::Empty {
|
||||
None
|
||||
} else {
|
||||
panic!("reply_receiver disconnected");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
use nexosim::time::MonotonicTime;
|
||||
|
||||
pub fn current_millis(time: MonotonicTime) -> u64 {
|
||||
(time.as_secs() as u64 * 1000) + (time.subsec_nanos() as u64 / 1_000_000)
|
||||
}
|
||||
@@ -0,0 +1,443 @@
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
io::ErrorKind,
|
||||
net::{SocketAddr, UdpSocket},
|
||||
sync::{Arc, atomic::AtomicBool, mpsc},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use minisim_types::{SimReply, SimRequestWithTime};
|
||||
|
||||
// A UDP server which handles all TC received by a client application.
|
||||
pub struct SimUdpServer {
|
||||
socket: UdpSocket,
|
||||
request_sender: mpsc::Sender<SimRequestWithTime>,
|
||||
reply_receiver: mpsc::Receiver<SimReply>,
|
||||
reply_queue: VecDeque<SimReply>,
|
||||
max_num_replies: usize,
|
||||
// Stop signal to stop the server. Required for unittests and useful to allow clean shutdown
|
||||
// of the application.
|
||||
stop_signal: Option<Arc<AtomicBool>>,
|
||||
idle_sleep_period_ms: u64,
|
||||
req_buf: [u8; 4096],
|
||||
sender_addr: Option<SocketAddr>,
|
||||
}
|
||||
|
||||
impl SimUdpServer {
|
||||
pub fn new(
|
||||
local_port: u16,
|
||||
request_sender: mpsc::Sender<SimRequestWithTime>,
|
||||
reply_receiver: mpsc::Receiver<SimReply>,
|
||||
max_num_replies: usize,
|
||||
stop_signal: Option<Arc<AtomicBool>>,
|
||||
) -> std::io::Result<Self> {
|
||||
let socket = UdpSocket::bind(SocketAddr::from(([0, 0, 0, 0], local_port)))?;
|
||||
socket.set_nonblocking(true)?;
|
||||
Ok(Self {
|
||||
socket,
|
||||
request_sender,
|
||||
reply_receiver,
|
||||
reply_queue: VecDeque::new(),
|
||||
max_num_replies,
|
||||
stop_signal,
|
||||
idle_sleep_period_ms: 3,
|
||||
req_buf: [0; 4096],
|
||||
sender_addr: None,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn server_addr(&self) -> std::io::Result<SocketAddr> {
|
||||
self.socket.local_addr()
|
||||
}
|
||||
|
||||
pub fn run(&mut self) {
|
||||
loop {
|
||||
if let Some(stop_signal) = &self.stop_signal
|
||||
&& stop_signal.load(std::sync::atomic::Ordering::Relaxed)
|
||||
{
|
||||
break;
|
||||
}
|
||||
let processed_requests = self.process_requests();
|
||||
let processed_replies = self.process_replies();
|
||||
let sent_replies = self.send_replies();
|
||||
// Sleep for a bit if there is nothing to do to prevent burning CPU cycles. Delay
|
||||
// should be kept short to ensure responsiveness of the system.
|
||||
if !processed_requests && !processed_replies && !sent_replies {
|
||||
std::thread::sleep(Duration::from_millis(self.idle_sleep_period_ms));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_requests(&mut self) -> bool {
|
||||
let mut processed_requests = false;
|
||||
loop {
|
||||
// Blocks for a certain amount of time until data is received to allow doing periodic
|
||||
// work like checking the stop signal.
|
||||
let (bytes_read, src) = match self.socket.recv_from(&mut self.req_buf) {
|
||||
Ok((bytes_read, src)) => (bytes_read, src),
|
||||
Err(e) if e.kind() == ErrorKind::WouldBlock => {
|
||||
// Continue to perform regular checks like the stop signal.
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
// Handle unexpected errors (e.g., socket closed) here.
|
||||
log::error!("unexpected request server error: {e}");
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
self.sender_addr = Some(src);
|
||||
|
||||
let sim_req = postcard::from_bytes::<SimRequestWithTime>(&self.req_buf[..bytes_read]);
|
||||
if let Err(e) = sim_req {
|
||||
log::warn!("received UDP request with invalid format: {}", e);
|
||||
return processed_requests;
|
||||
}
|
||||
self.request_sender.send(sim_req.unwrap()).unwrap();
|
||||
processed_requests = true;
|
||||
}
|
||||
processed_requests
|
||||
}
|
||||
|
||||
fn process_replies(&mut self) -> bool {
|
||||
let mut processed_replies = false;
|
||||
loop {
|
||||
match self.reply_receiver.try_recv() {
|
||||
Ok(reply) => {
|
||||
if self.reply_queue.len() >= self.max_num_replies {
|
||||
self.reply_queue.pop_front();
|
||||
}
|
||||
self.reply_queue.push_back(reply);
|
||||
processed_replies = true;
|
||||
}
|
||||
Err(e) => match e {
|
||||
mpsc::TryRecvError::Empty => return processed_replies,
|
||||
mpsc::TryRecvError::Disconnected => {
|
||||
log::error!("all UDP reply senders disconnected")
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send_replies(&mut self) -> bool {
|
||||
if self.sender_addr.is_none() {
|
||||
return false;
|
||||
}
|
||||
let mut sent_replies = false;
|
||||
while !self.reply_queue.is_empty() {
|
||||
let next_reply_to_send = self.reply_queue.pop_front().unwrap();
|
||||
self.socket
|
||||
.send_to(
|
||||
&postcard::to_allocvec(&next_reply_to_send).unwrap(),
|
||||
self.sender_addr.unwrap(),
|
||||
)
|
||||
.expect("sending reply failed");
|
||||
sent_replies = true;
|
||||
}
|
||||
sent_replies
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
io::ErrorKind,
|
||||
net::{SocketAddr, UdpSocket},
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
mpsc,
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use minisim_types::{
|
||||
SimCtrlReply, SimCtrlRequest, SimReply, SimRequestWithTime,
|
||||
eps::{PcduReply, PcduRequest},
|
||||
};
|
||||
|
||||
use crate::eps::tests::get_all_off_switch_map;
|
||||
use delegate::delegate;
|
||||
|
||||
use super::SimUdpServer;
|
||||
|
||||
// Wait time to ensure even possibly laggy systems like CI servers can run the tests.
|
||||
const SERVER_WAIT_TIME_MS: u64 = 50;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ReceptionError {
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("postcard error: {0}")]
|
||||
Postcard(#[from] postcard::Error),
|
||||
}
|
||||
|
||||
pub struct SimUdpTestClient {
|
||||
socket: UdpSocket,
|
||||
pub reply_buf: [u8; 4096],
|
||||
}
|
||||
|
||||
impl SimUdpTestClient {
|
||||
pub fn new(
|
||||
server_addr: &SocketAddr,
|
||||
non_blocking: bool,
|
||||
read_timeot_ms: Option<u64>,
|
||||
) -> std::io::Result<Self> {
|
||||
let socket = UdpSocket::bind("127.0.0.1:0")?;
|
||||
socket.set_nonblocking(non_blocking)?;
|
||||
socket
|
||||
.connect(server_addr)
|
||||
.expect("could not connect to server addr");
|
||||
if let Some(read_timeout) = read_timeot_ms {
|
||||
// Set a read timeout so the test does not hang on failures.
|
||||
socket.set_read_timeout(Some(Duration::from_millis(read_timeout)))?;
|
||||
}
|
||||
Ok(Self {
|
||||
socket,
|
||||
reply_buf: [0; 4096],
|
||||
})
|
||||
}
|
||||
|
||||
pub fn send_request(&self, sim_request: &SimRequestWithTime) -> std::io::Result<usize> {
|
||||
self.socket.send(
|
||||
&postcard::to_allocvec(sim_request)
|
||||
.expect("conversion of request to vector failed"),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn recv_raw(&mut self) -> std::io::Result<usize> {
|
||||
self.socket.recv(&mut self.reply_buf)
|
||||
}
|
||||
|
||||
pub fn recv_sim_reply(&mut self) -> Result<SimReply, ReceptionError> {
|
||||
let read_len = self.recv_raw()?;
|
||||
Ok(postcard::from_bytes(&self.reply_buf[0..read_len])?)
|
||||
}
|
||||
}
|
||||
struct UdpTestbench {
|
||||
client: SimUdpTestClient,
|
||||
stop_signal: Arc<AtomicBool>,
|
||||
request_receiver: mpsc::Receiver<SimRequestWithTime>,
|
||||
reply_sender: mpsc::Sender<SimReply>,
|
||||
}
|
||||
|
||||
impl UdpTestbench {
|
||||
pub fn new(
|
||||
client_non_blocking: bool,
|
||||
client_read_timeout_ms: Option<u64>,
|
||||
max_num_replies: usize,
|
||||
) -> std::io::Result<(Self, SimUdpServer)> {
|
||||
let (request_sender, request_receiver) = mpsc::channel();
|
||||
let (reply_sender, reply_receiver) = mpsc::channel();
|
||||
let stop_signal = Arc::new(AtomicBool::new(false));
|
||||
let server = SimUdpServer::new(
|
||||
0,
|
||||
request_sender,
|
||||
reply_receiver,
|
||||
max_num_replies,
|
||||
Some(stop_signal.clone()),
|
||||
)?;
|
||||
let server_addr = server.server_addr()?;
|
||||
Ok((
|
||||
Self {
|
||||
client: SimUdpTestClient::new(
|
||||
&server_addr,
|
||||
client_non_blocking,
|
||||
client_read_timeout_ms,
|
||||
)?,
|
||||
stop_signal,
|
||||
request_receiver,
|
||||
reply_sender,
|
||||
},
|
||||
server,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn try_recv_request(&self) -> Result<SimRequestWithTime, mpsc::TryRecvError> {
|
||||
self.request_receiver.try_recv()
|
||||
}
|
||||
|
||||
pub fn stop(&self) {
|
||||
self.stop_signal.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn send_reply(&self, sim_reply: &SimReply) {
|
||||
self.reply_sender
|
||||
.send(sim_reply.clone())
|
||||
.expect("sending sim reply failed");
|
||||
}
|
||||
|
||||
delegate! {
|
||||
to self.client {
|
||||
pub fn send_request(&self, sim_request: &SimRequestWithTime) -> std::io::Result<usize>;
|
||||
pub fn recv_sim_reply(&mut self) -> Result<SimReply, ReceptionError>;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_no_sim_reply_available(&mut self) {
|
||||
if let Err(ReceptionError::Io(ref io_error)) = self.recv_sim_reply() {
|
||||
if io_error.kind() == ErrorKind::WouldBlock {
|
||||
// Continue to perform regular checks like the stop signal.
|
||||
return;
|
||||
} else {
|
||||
// Handle unexpected errors (e.g., socket closed) here.
|
||||
panic!("unexpected request server error: {io_error}");
|
||||
}
|
||||
}
|
||||
panic!("unexpected reply available");
|
||||
}
|
||||
|
||||
pub fn check_next_sim_reply(&mut self, expected_reply: &SimReply) {
|
||||
match self.recv_sim_reply() {
|
||||
Ok(received_sim_reply) => assert_eq!(expected_reply, &received_sim_reply),
|
||||
Err(e) => match e {
|
||||
ReceptionError::Io(ref io_error) => {
|
||||
if io_error.kind() == ErrorKind::WouldBlock {
|
||||
// Continue to perform regular checks like the stop signal.
|
||||
panic!("no simulation reply received");
|
||||
} else {
|
||||
// Handle unexpected errors (e.g., socket closed) here.
|
||||
panic!("unexpected request server error: {e}");
|
||||
}
|
||||
}
|
||||
ReceptionError::Postcard(postcard_error) => {
|
||||
panic!("unexpected postcard error: {postcard_error}");
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn test_basic_udp_request_reception() {
|
||||
let (udp_testbench, mut udp_server) =
|
||||
UdpTestbench::new(true, Some(SERVER_WAIT_TIME_MS), 10)
|
||||
.expect("could not create testbench");
|
||||
let server_thread = std::thread::spawn(move || udp_server.run());
|
||||
let sim_request = SimRequestWithTime::new_with_epoch_time(PcduRequest::RequestSwitchInfo);
|
||||
udp_testbench
|
||||
.send_request(&sim_request)
|
||||
.expect("sending request failed");
|
||||
std::thread::sleep(Duration::from_millis(SERVER_WAIT_TIME_MS));
|
||||
// Check that the sim request has arrives and was forwarded.
|
||||
let received_sim_request = udp_testbench
|
||||
.try_recv_request()
|
||||
.expect("did not receive request");
|
||||
assert_eq!(sim_request, received_sim_request);
|
||||
// Stop the server.
|
||||
udp_testbench.stop();
|
||||
server_thread.join().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_udp_reply_server() {
|
||||
let (mut udp_testbench, mut udp_server) =
|
||||
UdpTestbench::new(false, Some(SERVER_WAIT_TIME_MS), 10)
|
||||
.expect("could not create testbench");
|
||||
let server_thread = std::thread::spawn(move || udp_server.run());
|
||||
udp_testbench
|
||||
.send_request(&SimRequestWithTime::new_with_epoch_time(
|
||||
SimCtrlRequest::Ping,
|
||||
))
|
||||
.expect("sending request failed");
|
||||
|
||||
let sim_reply = SimReply::from(PcduReply::SwitchInfo(get_all_off_switch_map()));
|
||||
udp_testbench.send_reply(&sim_reply);
|
||||
|
||||
udp_testbench.check_next_sim_reply(&sim_reply);
|
||||
|
||||
// Stop the server.
|
||||
udp_testbench.stop();
|
||||
server_thread.join().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_udp_req_server_and_reply_sender() {
|
||||
let (mut udp_testbench, mut udp_server) =
|
||||
UdpTestbench::new(false, Some(SERVER_WAIT_TIME_MS), 10)
|
||||
.expect("could not create testbench");
|
||||
|
||||
let server_thread = std::thread::spawn(move || udp_server.run());
|
||||
|
||||
// Send a ping so that the server knows the address of the client.
|
||||
// Do not check that the request arrives on the receiver side, is done by other test.
|
||||
udp_testbench
|
||||
.send_request(&SimRequestWithTime::new_with_epoch_time(
|
||||
SimCtrlRequest::Ping,
|
||||
))
|
||||
.expect("sending request failed");
|
||||
|
||||
// Send a reply to the server, ensure it gets forwarded to the client.
|
||||
let sim_reply = SimReply::from(PcduReply::SwitchInfo(get_all_off_switch_map()));
|
||||
udp_testbench.send_reply(&sim_reply);
|
||||
std::thread::sleep(Duration::from_millis(SERVER_WAIT_TIME_MS));
|
||||
|
||||
// Now we check that the reply server can send back replies to the client.
|
||||
udp_testbench.check_next_sim_reply(&sim_reply);
|
||||
|
||||
udp_testbench.stop();
|
||||
server_thread.join().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_udp_replies_client_unconnected() {
|
||||
let (mut udp_testbench, mut udp_server) =
|
||||
UdpTestbench::new(true, None, 10).expect("could not create testbench");
|
||||
|
||||
let server_thread = std::thread::spawn(move || udp_server.run());
|
||||
|
||||
// Send a reply to the server. The client is not connected, so it won't get forwarded.
|
||||
let sim_reply = SimReply::from(PcduReply::SwitchInfo(get_all_off_switch_map()));
|
||||
udp_testbench.send_reply(&sim_reply);
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
|
||||
udp_testbench.check_no_sim_reply_available();
|
||||
|
||||
// Connect by sending a ping.
|
||||
udp_testbench
|
||||
.send_request(&SimRequestWithTime::new_with_epoch_time(
|
||||
SimCtrlRequest::Ping,
|
||||
))
|
||||
.expect("sending request failed");
|
||||
std::thread::sleep(Duration::from_millis(SERVER_WAIT_TIME_MS));
|
||||
|
||||
udp_testbench.check_next_sim_reply(&sim_reply);
|
||||
|
||||
// Now we check that the reply server can sent back replies to the client.
|
||||
udp_testbench.stop();
|
||||
server_thread.join().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_udp_reply_server_old_replies_overwritten() {
|
||||
let (mut udp_testbench, mut udp_server) =
|
||||
UdpTestbench::new(true, None, 3).expect("could not create testbench");
|
||||
|
||||
let server_thread = std::thread::spawn(move || udp_server.run());
|
||||
|
||||
// The server only caches up to 3 replies.
|
||||
let sim_reply = SimReply::from(SimCtrlReply::Pong);
|
||||
for _ in 0..4 {
|
||||
udp_testbench.send_reply(&sim_reply);
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
|
||||
udp_testbench.check_no_sim_reply_available();
|
||||
|
||||
// Connect by sending a ping.
|
||||
udp_testbench
|
||||
.send_request(&SimRequestWithTime::new_with_epoch_time(
|
||||
SimCtrlRequest::Ping,
|
||||
))
|
||||
.expect("sending request failed");
|
||||
std::thread::sleep(Duration::from_millis(SERVER_WAIT_TIME_MS));
|
||||
|
||||
for _ in 0..3 {
|
||||
udp_testbench.check_next_sim_reply(&sim_reply);
|
||||
}
|
||||
udp_testbench.check_no_sim_reply_available();
|
||||
udp_testbench.stop();
|
||||
server_thread.join().unwrap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
|
||||
# uncomment ONE of these three option to make `cargo run` start a GDB session
|
||||
# which option to pick depends on your system
|
||||
# You can also replace openocd.gdb by jlink.gdb when using a J-Link.
|
||||
# runner = "arm-none-eabi-gdb -q -x openocd.gdb"
|
||||
# runner = "gdb-multiarch -q -x openocd.gdb"
|
||||
# runner = "gdb -q -x openocd.gdb"
|
||||
runner = "probe-rs run --chip STM32F303VCTx"
|
||||
|
||||
rustflags = [
|
||||
"-C", "linker=flip-link",
|
||||
# LLD (shipped with the Rust toolchain) is used as the default linker
|
||||
"-C", "link-arg=-Tlink.x",
|
||||
"-C", "link-arg=-Tdefmt.x",
|
||||
|
||||
# This is needed if your flash or ram addresses are not aligned to 0x10000 in memory.x
|
||||
# See https://github.com/rust-embedded/cortex-m-quickstart/pull/95
|
||||
"-C", "link-arg=--nmagic",
|
||||
|
||||
# if you run into problems with LLD switch to the GNU linker by commenting out
|
||||
# this line
|
||||
# "-C", "linker=arm-none-eabi-ld",
|
||||
|
||||
# if you need to link to pre-compiled C libraries provided by a C toolchain
|
||||
# use GCC as the linker by commenting out both lines above and then
|
||||
# uncommenting the three lines below
|
||||
# "-C", "linker=arm-none-eabi-gcc",
|
||||
# "-C", "link-arg=-Wl,-Tlink.x",
|
||||
# "-C", "link-arg=-nostartfiles",
|
||||
]
|
||||
|
||||
[build]
|
||||
# comment out the following line if you intend to run unit tests on host machine
|
||||
target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU)
|
||||
|
||||
[env]
|
||||
DEFMT_LOG = "info"
|
||||
@@ -0,0 +1,4 @@
|
||||
/target
|
||||
/itm.txt
|
||||
/.cargo/config.toml
|
||||
/.vscode
|
||||
Generated
+1532
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,64 @@
|
||||
[package]
|
||||
name = "stm32f3-disco-rtic"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
default-run = "stm32f3-disco-rtic"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
embedded-types = { path = "../embedded-types", features = ["defmt"] }
|
||||
cortex-m = { version = "0.7", features = ["critical-section-single-core"] }
|
||||
cortex-m-rt = "0.7"
|
||||
defmt = "1"
|
||||
defmt-rtt = { version = "1" }
|
||||
panic-probe = { version = "1", features = ["print-defmt"] }
|
||||
embedded-hal = "1"
|
||||
cortex-m-semihosting = "0.5.0"
|
||||
embassy-stm32 = { version = "0.6", features = ["defmt", "stm32f303vc", "memory-x", "unstable-pac", "time-driver-any"] }
|
||||
embassy-time = { version = "0.5", features = ["defmt", "generic-queue-16", "defmt-timestamp-uptime-ms"]}
|
||||
enumset = "1"
|
||||
heapless = "0.9"
|
||||
embassy-sync = "0.8"
|
||||
spacepackets = { version = "0.18", default-features = false, features = ["defmt", "serde"] }
|
||||
static_cell = "2"
|
||||
cobs = { version = "0.5", default-features = false, features = ["defmt"] }
|
||||
postcard = { version = "1" }
|
||||
arbitrary-int = "2"
|
||||
thiserror = { version = "2", default-features = false }
|
||||
serde = { version = "1", default-features = false, features = ["derive"] }
|
||||
|
||||
rtic = { version = "2", features = ["thumbv7-backend"] }
|
||||
rtic-sync = { version = "1" }
|
||||
|
||||
[dev-dependencies]
|
||||
defmt-test = "0.5"
|
||||
|
||||
# cargo test
|
||||
[profile.test]
|
||||
codegen-units = 1
|
||||
debug = 2
|
||||
debug-assertions = true # <-
|
||||
incremental = false
|
||||
opt-level = "s" # <-
|
||||
overflow-checks = true # <-
|
||||
|
||||
# cargo build/run --release
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
debug = 2
|
||||
debug-assertions = false # <-
|
||||
incremental = false
|
||||
lto = 'fat'
|
||||
opt-level = "s" # <-
|
||||
overflow-checks = false # <-
|
||||
|
||||
# cargo test --release
|
||||
[profile.bench]
|
||||
codegen-units = 1
|
||||
debug = 2
|
||||
debug-assertions = false # <-
|
||||
incremental = false
|
||||
lto = 'fat'
|
||||
opt-level = "s" # <-
|
||||
overflow-checks = false # <-
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1 @@
|
||||
This software contains code developed at the University of Stuttgart's Institute of Space Systems.
|
||||
@@ -0,0 +1,106 @@
|
||||
sat-rs example for the STM32F3-Discovery board
|
||||
=======
|
||||
|
||||
This example application shows how the [sat-rs library](https://egit.irs.uni-stuttgart.de/rust/sat-rs)
|
||||
can be used on an embedded target.
|
||||
It also shows how a relatively simple OBSW could be built when no standard runtime is available.
|
||||
It uses [RTIC](https://rtic.rs/2/book/en/) as the concurrency framework and the
|
||||
[defmt](https://defmt.ferrous-systems.com/) framework for logging.
|
||||
|
||||
The STM32F3-Discovery device was picked because it is a cheap Cortex-M4 based device which is also
|
||||
used by the [Rust Embedded Book](https://docs.rust-embedded.org/book/intro/hardware.html) and the
|
||||
[Rust Discovery](https://docs.rust-embedded.org/discovery/f3discovery/) book as an introduction
|
||||
to embedded Rust.
|
||||
|
||||
## Pre-Requisites
|
||||
|
||||
Make sure the following tools are installed:
|
||||
|
||||
1. [`probe-rs`](https://probe.rs/): Application used to flash and debug the MCU.
|
||||
2. Optional and recommended: [VS Code](https://code.visualstudio.com/) with
|
||||
[probe-rs plugin](https://marketplace.visualstudio.com/items?itemName=probe-rs.probe-rs-debugger)
|
||||
for debugging.
|
||||
|
||||
## Preparing Rust and the repository
|
||||
|
||||
Building an application requires the `thumbv7em-none-eabihf` cross-compiler toolchain.
|
||||
If you have not installed it yet, you can do so with
|
||||
|
||||
```sh
|
||||
rustup target add thumbv7em-none-eabihf
|
||||
```
|
||||
|
||||
A default `.cargo` config file is provided for this project, but needs to be copied to have
|
||||
the correct name. This is so that the config file can be updated or edited for custom needs
|
||||
without being tracked by git.
|
||||
|
||||
```sh
|
||||
cp .cargo/config.toml.template .cargo/config.toml
|
||||
```
|
||||
|
||||
The configuration file will also set the target so it does not always have to be specified with
|
||||
the `--target` argument.
|
||||
|
||||
## Building
|
||||
|
||||
After that, assuming that you have a `.cargo/config.toml` setting the correct build target,
|
||||
you can simply build the application with
|
||||
|
||||
```sh
|
||||
cargo build
|
||||
```
|
||||
|
||||
## Flashing from the command line
|
||||
|
||||
You can flash the application from the command line using `probe-rs`:
|
||||
|
||||
```sh
|
||||
probe-rs run --chip STM32F303VCTx
|
||||
```
|
||||
|
||||
## Debugging with VS Code
|
||||
|
||||
The STM32F3-Discovery comes with an on-board ST-Link so all that is required to flash and debug
|
||||
the board is a Mini-USB cable. The code in this repository was debugged using [`probe-rs`](https://probe.rs/docs/tools/debuggerA)
|
||||
and the VS Code [`probe-rs` plugin](https://marketplace.visualstudio.com/items?itemName=probe-rs.probe-rs-debugger).
|
||||
Make sure to install this plugin first.
|
||||
|
||||
Sample configuration files are provided inside the `vscode` folder.
|
||||
Use `cp vscode .vscode -r` to use them for your project.
|
||||
|
||||
Some sample configuration files for VS Code were provided as well. You can simply use `Run` and `Debug`
|
||||
to automatically rebuild and flash your application.
|
||||
|
||||
The `tasks.json` and `launch.json` files are generic and you can use them immediately by opening
|
||||
the folder in VS code or adding it to a workspace.
|
||||
|
||||
## Commanding the board
|
||||
|
||||
When the software is running on the Discovery board, you can command the MCU via a serial
|
||||
interface. The telecommands are CCSDS space packets with a [`postcard`](https://docs.rs/postcard)
|
||||
serialized payload, using [COBS](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing)
|
||||
as the packet framing format.
|
||||
|
||||
The packets are exchanged using a dedicated serial interface. You can use any generic USB-to-UART
|
||||
converter device with the TX pin connected to the PA3 pin and the RX pin connected to the PA2 pin.
|
||||
|
||||
The [`embedded-client`](../embedded-client) application is used to command the board. Set the
|
||||
serial port of your USB-to-UART converter inside `embedded-client/config.toml`:
|
||||
|
||||
```toml
|
||||
[interface]
|
||||
serial_port = "/dev/ttyUSB0"
|
||||
```
|
||||
|
||||
Then run the client from inside the `embedded-client` directory. For example, you can send a ping
|
||||
to the MCU using
|
||||
|
||||
```sh
|
||||
cargo run --bin stm32f3-client -- --ping
|
||||
```
|
||||
|
||||
and set the LED blink frequency to 500 ms using
|
||||
|
||||
```sh
|
||||
cargo run --bin stm32f3-client -- --set-led-frequency 500
|
||||
```
|
||||
@@ -0,0 +1,56 @@
|
||||
#![no_main]
|
||||
#![no_std]
|
||||
|
||||
use panic_probe as _;
|
||||
use rtic::app;
|
||||
|
||||
#[app(device = embassy_stm32)]
|
||||
mod app {
|
||||
use embassy_time::Timer;
|
||||
use stm32f3_disco_rtic::{Direction, LedPinSet, Leds};
|
||||
|
||||
#[shared]
|
||||
struct Shared {}
|
||||
|
||||
#[local]
|
||||
struct Local {
|
||||
leds: Leds,
|
||||
current_dir: Direction,
|
||||
}
|
||||
|
||||
#[init]
|
||||
fn init(_cx: init::Context) -> (Shared, Local) {
|
||||
let p = embassy_stm32::init(Default::default());
|
||||
|
||||
defmt::info!("Starting sat-rs demo application for the STM32F3-Discovery using RTICv2");
|
||||
|
||||
let led_pin_set = LedPinSet {
|
||||
pin_n: p.PE8,
|
||||
pin_ne: p.PE9,
|
||||
pin_e: p.PE10,
|
||||
pin_se: p.PE11,
|
||||
pin_s: p.PE12,
|
||||
pin_sw: p.PE13,
|
||||
pin_w: p.PE14,
|
||||
pin_nw: p.PE15,
|
||||
};
|
||||
let leds = Leds::new(led_pin_set);
|
||||
|
||||
blinky::spawn().expect("failed to spawn blinky task");
|
||||
(
|
||||
Shared {},
|
||||
Local {
|
||||
leds,
|
||||
current_dir: Direction::North,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[task(local = [leds, current_dir])]
|
||||
async fn blinky(cx: blinky::Context) {
|
||||
loop {
|
||||
cx.local.leds.blink_next(cx.local.current_dir);
|
||||
Timer::after_millis(200).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
#![no_main]
|
||||
#![no_std]
|
||||
|
||||
use defmt_rtt as _;
|
||||
use panic_probe as _;
|
||||
|
||||
use arbitrary_int::u11;
|
||||
use embassy_stm32::gpio::Output;
|
||||
|
||||
pub const APID: u11 = u11::new(0x02);
|
||||
|
||||
#[derive(defmt::Format, serde::Serialize, serde::Deserialize, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum Direction {
|
||||
North,
|
||||
NorthEast,
|
||||
East,
|
||||
SouthEast,
|
||||
South,
|
||||
SouthWest,
|
||||
West,
|
||||
NorthWest,
|
||||
}
|
||||
|
||||
impl Direction {
|
||||
pub fn switch_to_next(&mut self) -> (Self, Self) {
|
||||
let curr = *self;
|
||||
*self = match self {
|
||||
Direction::North => Direction::NorthEast,
|
||||
Direction::NorthEast => Direction::East,
|
||||
Direction::East => Direction::SouthEast,
|
||||
Direction::SouthEast => Direction::South,
|
||||
Direction::South => Direction::SouthWest,
|
||||
Direction::SouthWest => Direction::West,
|
||||
Direction::West => Direction::NorthWest,
|
||||
Direction::NorthWest => Direction::North,
|
||||
};
|
||||
(curr, *self)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Leds {
|
||||
pub north: Output<'static>,
|
||||
pub north_east: Output<'static>,
|
||||
pub east: Output<'static>,
|
||||
pub south_east: Output<'static>,
|
||||
pub south: Output<'static>,
|
||||
pub south_west: Output<'static>,
|
||||
pub west: Output<'static>,
|
||||
pub north_west: Output<'static>,
|
||||
}
|
||||
|
||||
impl Leds {
|
||||
pub fn blink_next(&mut self, current_dir: &mut Direction) {
|
||||
let (prev, curr) = current_dir.switch_to_next();
|
||||
self.set_dir_low(prev);
|
||||
self.set_dir_high(curr);
|
||||
}
|
||||
|
||||
pub fn set_dir(&mut self, dir: Direction, level: embassy_stm32::gpio::Level) {
|
||||
match dir {
|
||||
Direction::North => self.north.set_level(level),
|
||||
Direction::NorthEast => self.north_east.set_level(level),
|
||||
Direction::East => self.east.set_level(level),
|
||||
Direction::SouthEast => self.south_east.set_level(level),
|
||||
Direction::South => self.south.set_level(level),
|
||||
Direction::SouthWest => self.south_west.set_level(level),
|
||||
Direction::West => self.west.set_level(level),
|
||||
Direction::NorthWest => self.north_west.set_level(level),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_dir_low(&mut self, dir: Direction) {
|
||||
self.set_dir(dir, embassy_stm32::gpio::Level::Low);
|
||||
}
|
||||
|
||||
pub fn set_dir_high(&mut self, dir: Direction) {
|
||||
self.set_dir(dir, embassy_stm32::gpio::Level::High);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LedPinSet {
|
||||
pub pin_n: embassy_stm32::Peri<'static, embassy_stm32::peripherals::PE8>,
|
||||
pub pin_ne: embassy_stm32::Peri<'static, embassy_stm32::peripherals::PE9>,
|
||||
pub pin_e: embassy_stm32::Peri<'static, embassy_stm32::peripherals::PE10>,
|
||||
pub pin_se: embassy_stm32::Peri<'static, embassy_stm32::peripherals::PE11>,
|
||||
pub pin_s: embassy_stm32::Peri<'static, embassy_stm32::peripherals::PE12>,
|
||||
pub pin_sw: embassy_stm32::Peri<'static, embassy_stm32::peripherals::PE13>,
|
||||
pub pin_w: embassy_stm32::Peri<'static, embassy_stm32::peripherals::PE14>,
|
||||
pub pin_nw: embassy_stm32::Peri<'static, embassy_stm32::peripherals::PE15>,
|
||||
}
|
||||
|
||||
impl Leds {
|
||||
pub fn new(pin_set: LedPinSet) -> Self {
|
||||
let led_n = Output::new(
|
||||
pin_set.pin_n,
|
||||
embassy_stm32::gpio::Level::Low,
|
||||
embassy_stm32::gpio::Speed::Medium,
|
||||
);
|
||||
let led_ne = Output::new(
|
||||
pin_set.pin_ne,
|
||||
embassy_stm32::gpio::Level::Low,
|
||||
embassy_stm32::gpio::Speed::Medium,
|
||||
);
|
||||
let led_e = Output::new(
|
||||
pin_set.pin_e,
|
||||
embassy_stm32::gpio::Level::Low,
|
||||
embassy_stm32::gpio::Speed::Medium,
|
||||
);
|
||||
let led_se = Output::new(
|
||||
pin_set.pin_se,
|
||||
embassy_stm32::gpio::Level::Low,
|
||||
embassy_stm32::gpio::Speed::Medium,
|
||||
);
|
||||
let led_s = Output::new(
|
||||
pin_set.pin_s,
|
||||
embassy_stm32::gpio::Level::Low,
|
||||
embassy_stm32::gpio::Speed::Medium,
|
||||
);
|
||||
let led_sw = Output::new(
|
||||
pin_set.pin_sw,
|
||||
embassy_stm32::gpio::Level::Low,
|
||||
embassy_stm32::gpio::Speed::Medium,
|
||||
);
|
||||
let led_w = Output::new(
|
||||
pin_set.pin_w,
|
||||
embassy_stm32::gpio::Level::Low,
|
||||
embassy_stm32::gpio::Speed::Medium,
|
||||
);
|
||||
let led_nw = Output::new(
|
||||
pin_set.pin_nw,
|
||||
embassy_stm32::gpio::Level::Low,
|
||||
embassy_stm32::gpio::Speed::Medium,
|
||||
);
|
||||
Self {
|
||||
north: led_n,
|
||||
north_east: led_ne,
|
||||
east: led_e,
|
||||
south_east: led_se,
|
||||
south: led_s,
|
||||
south_west: led_sw,
|
||||
west: led_w,
|
||||
north_west: led_nw,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
use arbitrary_int::u14;
|
||||
use cortex_m_semihosting::debug::{self, EXIT_FAILURE, EXIT_SUCCESS};
|
||||
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||
use embedded_types::{TmHeader, create_tm_packet, stm32f3, tm_size};
|
||||
use spacepackets::{CcsdsPacketCreationError, CcsdsPacketIdAndPsc, SpHeader};
|
||||
|
||||
use defmt_rtt as _; // global logger
|
||||
use panic_probe as _;
|
||||
|
||||
use rtic::app;
|
||||
|
||||
const UART_BAUD: u32 = 115200;
|
||||
const DEFAULT_BLINK_FREQ_MS: u32 = 1000;
|
||||
const MAX_TC_LEN: usize = 128;
|
||||
const MAX_TM_LEN: usize = 128;
|
||||
|
||||
// This is the predictable maximum overhead of the COBS encoding scheme.
|
||||
// It is simply the maximum packet lenght dividied by 254 rounded up.
|
||||
const COBS_TM_OVERHEAD: usize = cobs::max_encoding_overhead(MAX_TM_LEN);
|
||||
|
||||
const TM_BUF_LEN: usize = MAX_TC_LEN + COBS_TM_OVERHEAD;
|
||||
|
||||
const TC_DMA_BUF_LEN: usize = 512;
|
||||
|
||||
type TmPacket = heapless::Vec<u8, MAX_TM_LEN>;
|
||||
|
||||
static TM_QUEUE: embassy_sync::channel::Channel<CriticalSectionRawMutex, TmPacket, 16> =
|
||||
embassy_sync::channel::Channel::new();
|
||||
|
||||
#[derive(Debug, defmt::Format, thiserror::Error)]
|
||||
pub enum TmSendError {
|
||||
#[error("packet creation error: {0}")]
|
||||
PacketCreation(#[from] CcsdsPacketCreationError),
|
||||
#[error("queue error")]
|
||||
Queue,
|
||||
}
|
||||
|
||||
#[derive(Debug, defmt::Format)]
|
||||
pub struct RequestWithTcId {
|
||||
pub request: stm32f3::Request,
|
||||
pub tc_id: CcsdsPacketIdAndPsc,
|
||||
}
|
||||
|
||||
#[app(device = embassy_stm32)]
|
||||
mod app {
|
||||
use core::time::Duration;
|
||||
|
||||
use super::*;
|
||||
use arbitrary_int::u14;
|
||||
use embassy_time::Timer;
|
||||
use embedded_types::stm32f3::{Request, Response};
|
||||
use rtic::Mutex;
|
||||
use rtic_sync::{
|
||||
channel::{Receiver, Sender},
|
||||
make_channel,
|
||||
};
|
||||
use spacepackets::CcsdsPacketReader;
|
||||
use stm32f3_disco_rtic::LedPinSet;
|
||||
|
||||
embassy_stm32::bind_interrupts!(struct Irqs {
|
||||
USART2 => embassy_stm32::usart::InterruptHandler<embassy_stm32::peripherals::USART2>;
|
||||
DMA1_CHANNEL6 => embassy_stm32::dma::InterruptHandler<embassy_stm32::peripherals::DMA1_CH6>;
|
||||
DMA1_CHANNEL7 => embassy_stm32::dma::InterruptHandler<embassy_stm32::peripherals::DMA1_CH7>;
|
||||
});
|
||||
|
||||
#[shared]
|
||||
struct Shared {
|
||||
blink_freq: Duration,
|
||||
}
|
||||
|
||||
#[local]
|
||||
struct Local {
|
||||
leds: stm32f3_disco_rtic::Leds,
|
||||
current_dir: stm32f3_disco_rtic::Direction,
|
||||
seq_count: u14,
|
||||
tx: embassy_stm32::usart::UartTx<'static, embassy_stm32::mode::Async>,
|
||||
rx: embassy_stm32::usart::RingBufferedUartRx<'static>,
|
||||
}
|
||||
|
||||
#[init]
|
||||
fn init(_cx: init::Context) -> (Shared, Local) {
|
||||
static DMA_BUF: static_cell::ConstStaticCell<[u8; TC_DMA_BUF_LEN]> =
|
||||
static_cell::ConstStaticCell::new([0; TC_DMA_BUF_LEN]);
|
||||
|
||||
let p = embassy_stm32::init(Default::default());
|
||||
|
||||
let (req_sender, req_receiver) = make_channel!(RequestWithTcId, 16);
|
||||
|
||||
defmt::info!("sat-rs demo application for the STM32F3-Discovery with RTICv2");
|
||||
let led_pin_set = LedPinSet {
|
||||
pin_n: p.PE8,
|
||||
pin_ne: p.PE9,
|
||||
pin_e: p.PE10,
|
||||
pin_se: p.PE11,
|
||||
pin_s: p.PE12,
|
||||
pin_sw: p.PE13,
|
||||
pin_w: p.PE14,
|
||||
pin_nw: p.PE15,
|
||||
};
|
||||
let leds = stm32f3_disco_rtic::Leds::new(led_pin_set);
|
||||
|
||||
let mut config = embassy_stm32::usart::Config::default();
|
||||
config.baudrate = UART_BAUD;
|
||||
let uart = embassy_stm32::usart::Uart::new(
|
||||
p.USART2, p.PA3, p.PA2, p.DMA1_CH7, p.DMA1_CH6, Irqs, config,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (tx, rx) = uart.split();
|
||||
defmt::info!("Spawning tasks");
|
||||
blinky::spawn().unwrap();
|
||||
serial_tx_handler::spawn().unwrap();
|
||||
serial_rx_handler::spawn(req_sender).unwrap();
|
||||
req_handler::spawn(req_receiver).unwrap();
|
||||
|
||||
(
|
||||
Shared {
|
||||
blink_freq: Duration::from_millis(DEFAULT_BLINK_FREQ_MS as u64),
|
||||
},
|
||||
Local {
|
||||
leds,
|
||||
tx,
|
||||
seq_count: u14::new(0),
|
||||
rx: rx.into_ring_buffered(DMA_BUF.take()),
|
||||
current_dir: stm32f3_disco_rtic::Direction::North,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[task(local = [leds, current_dir], shared=[blink_freq])]
|
||||
async fn blinky(mut cx: blinky::Context) {
|
||||
loop {
|
||||
cx.local.leds.blink_next(cx.local.current_dir);
|
||||
let current_blink_freq = cx.shared.blink_freq.lock(|current| *current);
|
||||
Timer::after_millis(current_blink_freq.as_millis() as u64).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[task(
|
||||
local = [
|
||||
tx,
|
||||
encoded_buf: [u8; TM_BUF_LEN] = [0; TM_BUF_LEN]
|
||||
],
|
||||
shared = [],
|
||||
)]
|
||||
async fn serial_tx_handler(cx: serial_tx_handler::Context) {
|
||||
loop {
|
||||
loop {
|
||||
let vec = TM_QUEUE.receive().await;
|
||||
let encoded_len =
|
||||
cobs::encode_including_sentinels(&vec[0..vec.len()], cx.local.encoded_buf);
|
||||
defmt::debug!("sending {} bytes over UART", encoded_len);
|
||||
cx.local
|
||||
.tx
|
||||
.write(&cx.local.encoded_buf[0..encoded_len])
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[task(
|
||||
local = [
|
||||
rx,
|
||||
read_buf: [u8; 128] = [0; 128],
|
||||
decode_buf: [u8; MAX_TC_LEN] = [0; MAX_TC_LEN],
|
||||
],
|
||||
shared = [blink_freq]
|
||||
)]
|
||||
async fn serial_rx_handler(
|
||||
cx: serial_rx_handler::Context,
|
||||
mut sender: Sender<'static, RequestWithTcId, 16>,
|
||||
) {
|
||||
let mut decoder = cobs::CobsDecoder::new(cx.local.decode_buf);
|
||||
loop {
|
||||
match cx.local.rx.read(cx.local.read_buf).await {
|
||||
Ok(bytes) => {
|
||||
defmt::debug!("received {} bytes over UART", bytes);
|
||||
for byte in cx.local.read_buf[0..bytes].iter() {
|
||||
match decoder.feed(*byte) {
|
||||
Ok(None) => (),
|
||||
Ok(Some(packet_size)) => {
|
||||
match CcsdsPacketReader::new_with_checksum(
|
||||
&decoder.dest()[0..packet_size],
|
||||
) {
|
||||
Ok(packet) => {
|
||||
let tc_packet_id =
|
||||
CcsdsPacketIdAndPsc::new_from_ccsds_packet(&packet);
|
||||
if let Ok(request) =
|
||||
postcard::from_bytes::<Request>(packet.packet_data())
|
||||
{
|
||||
sender
|
||||
.send(RequestWithTcId {
|
||||
request,
|
||||
tc_id: tc_packet_id,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
defmt::error!("error unpacking ccsds packet: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
defmt::error!("cobs decoding error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
defmt::error!("uart read error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[task(shared = [blink_freq], local = [seq_count])]
|
||||
async fn req_handler(
|
||||
mut cx: req_handler::Context,
|
||||
mut receiver: Receiver<'static, RequestWithTcId, 16>,
|
||||
) {
|
||||
loop {
|
||||
match receiver.recv().await {
|
||||
Ok(request_with_tc_id) => {
|
||||
let tm_send_result = match request_with_tc_id.request {
|
||||
Request::Ping => {
|
||||
handle_ping_request(&mut cx, request_with_tc_id.tc_id).await
|
||||
}
|
||||
Request::ChangeBlinkFrequency(duration) => {
|
||||
handle_change_blink_frequency_request(
|
||||
&mut cx,
|
||||
request_with_tc_id.tc_id,
|
||||
duration,
|
||||
)
|
||||
.await
|
||||
}
|
||||
};
|
||||
if let Err(e) = tm_send_result {
|
||||
defmt::error!("error sending TM response: {}", e);
|
||||
}
|
||||
}
|
||||
Err(_e) => defmt::error!("request receive error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_ping_request(
|
||||
cx: &mut req_handler::Context<'_>,
|
||||
tc_packet_id: CcsdsPacketIdAndPsc,
|
||||
) -> Result<(), TmSendError> {
|
||||
defmt::info!("Received PUS ping telecommand, sending ping reply");
|
||||
send_tm(tc_packet_id, Response::Ok, *cx.local.seq_count).await?;
|
||||
*cx.local.seq_count = cx.local.seq_count.wrapping_add(u14::new(1));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_change_blink_frequency_request(
|
||||
cx: &mut req_handler::Context<'_>,
|
||||
tc_packet_id: CcsdsPacketIdAndPsc,
|
||||
duration: Duration,
|
||||
) -> Result<(), TmSendError> {
|
||||
defmt::info!(
|
||||
"Received ChangeBlinkFrequency request, new frequency: {} ms",
|
||||
duration.as_millis()
|
||||
);
|
||||
cx.shared
|
||||
.blink_freq
|
||||
.lock(|blink_freq| *blink_freq = duration);
|
||||
send_tm(tc_packet_id, Response::Ok, *cx.local.seq_count).await?;
|
||||
*cx.local.seq_count = cx.local.seq_count.wrapping_add(u14::new(1));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_tm(
|
||||
tc_packet_id: CcsdsPacketIdAndPsc,
|
||||
response: stm32f3::Response,
|
||||
current_seq_count: u14,
|
||||
) -> Result<(), TmSendError> {
|
||||
let sp_header = SpHeader::new_for_unseg_tc(stm32f3::PUS_APID, current_seq_count, 0);
|
||||
let tm_header = TmHeader {
|
||||
tc_packet_id: Some(tc_packet_id),
|
||||
uptime_millis: embassy_time::Instant::now().as_millis(),
|
||||
};
|
||||
let mut tm_packet = TmPacket::new();
|
||||
let tm_size = tm_size(&tm_header, &response);
|
||||
tm_packet.resize(tm_size, 0).expect("vec resize failed");
|
||||
create_tm_packet(&mut tm_packet, sp_header, tm_header, response)?;
|
||||
TM_QUEUE.send(tm_packet).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// same panicking *behavior* as `panic-probe` but doesn't print a panic message
|
||||
// this prevents the panic message being printed *twice* when `defmt::panic` is invoked
|
||||
#[defmt::panic_handler]
|
||||
fn panic() -> ! {
|
||||
cortex_m::asm::udf()
|
||||
}
|
||||
|
||||
/// Terminates the application and makes a semihosting-capable debug tool exit
|
||||
/// with status code 0.
|
||||
pub fn exit() -> ! {
|
||||
loop {
|
||||
debug::exit(EXIT_SUCCESS);
|
||||
}
|
||||
}
|
||||
|
||||
/// Hardfault handler.
|
||||
///
|
||||
/// Terminates the application and makes a semihosting-capable debug tool exit
|
||||
/// with an error. This seems better than the default, which is to spin in a
|
||||
/// loop.
|
||||
#[cortex_m_rt::exception]
|
||||
unsafe fn HardFault(_frame: &cortex_m_rt::ExceptionFrame) -> ! {
|
||||
loop {
|
||||
debug::exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
// defmt-test 0.3.0 has the limitation that this `#[tests]` attribute can only be used
|
||||
// once within a crate. the module can be in any file but there can only be at most
|
||||
// one `#[tests]` module in this library crate
|
||||
#[cfg(test)]
|
||||
#[defmt_test::tests]
|
||||
mod unit_tests {
|
||||
use defmt::assert;
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert!(true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
/settings.json
|
||||
/.cortex-debug.*
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
// 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": []
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"preLaunchTask": "${defaultBuildTask}",
|
||||
"type": "probe-rs-debug",
|
||||
"request": "launch",
|
||||
"name": "probe-rs Debugging ",
|
||||
"flashingConfig": {
|
||||
"flashingEnabled": true
|
||||
},
|
||||
"chip": "STM32F303VCTx",
|
||||
"coreConfigs": [
|
||||
{
|
||||
"programBinary": "${workspaceFolder}/target/thumbv7em-none-eabihf/debug/stm32f3-disco-rtic",
|
||||
"rttEnabled": true,
|
||||
"svdFile": "STM32F303.svd"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
#
|
||||
# Cortex-Debug extension calls this function during initialization. You can copy this
|
||||
# file, modify it and specifyy it as one of the config files supplied in launch.json
|
||||
# preferably at the beginning.
|
||||
#
|
||||
# Note that this file simply defines a function for use later when it is time to configure
|
||||
# for SWO.
|
||||
#
|
||||
set USE_SWO 0
|
||||
proc CDSWOConfigure { CDCPUFreqHz CDSWOFreqHz CDSWOOutput } {
|
||||
# Alternative option: Pipe ITM output into itm.txt file
|
||||
# tpiu config internal itm.txt uart off $CDCPUFreqHz
|
||||
|
||||
# Default option so SWO display of VS code works. Please note that this might not be required
|
||||
# anymore starting at openocd v0.12.0
|
||||
tpiu config internal $CDSWOOutput uart off $CDCPUFreqHz $CDSWOFreqHz
|
||||
itm port 0 on
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
// 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
|
||||
}
|
||||
},
|
||||
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
|
||||
runner = "probe-rs run --chip STM32H753ZITx"
|
||||
# runner = ["probe-rs", "run", "--chip", "$CHIP", "--log-format", "{L} {s}"]
|
||||
|
||||
rustflags = [
|
||||
"-C", "linker=flip-link",
|
||||
"-C", "link-arg=-Tlink.x",
|
||||
"-C", "link-arg=-Tdefmt.x",
|
||||
# This is needed if your flash or ram addresses are not aligned to 0x10000 in memory.x
|
||||
# See https://github.com/rust-embedded/cortex-m-quickstart/pull/95
|
||||
"-C", "link-arg=--nmagic",
|
||||
# Can be useful for debugging.
|
||||
# "-Clink-args=-Map=app.map"
|
||||
]
|
||||
|
||||
[build]
|
||||
# (`thumbv6m-*` is compatible with all ARM Cortex-M chips but using the right
|
||||
# target improves performance)
|
||||
# target = "thumbv6m-none-eabi" # Cortex-M0 and Cortex-M0+
|
||||
# target = "thumbv7m-none-eabi" # Cortex-M3
|
||||
# target = "thumbv7em-none-eabi" # Cortex-M4 and Cortex-M7 (no FPU)
|
||||
target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU)
|
||||
|
||||
[alias]
|
||||
rb = "run --bin"
|
||||
rrb = "run --release --bin"
|
||||
|
||||
[env]
|
||||
DEFMT_LOG = "info"
|
||||
@@ -0,0 +1,4 @@
|
||||
/target
|
||||
/.cargo/config.toml
|
||||
/.vscode
|
||||
/app.map
|
||||
Generated
+1311
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,74 @@
|
||||
[package]
|
||||
name = "stm32h7-nucleo-rtic"
|
||||
edition = "2024"
|
||||
version = "0.1.0"
|
||||
default-run = "stm32h7-nucleo-rtic"
|
||||
|
||||
[lib]
|
||||
harness = false
|
||||
|
||||
# needed for each integration test
|
||||
[[test]]
|
||||
name = "integration"
|
||||
harness = false
|
||||
|
||||
[dependencies]
|
||||
embedded-types = { path = "../embedded-types", features = ["defmt"] }
|
||||
|
||||
cortex-m = { version = "0.7", features = ["critical-section-single-core"] }
|
||||
arbitrary-int = "2"
|
||||
cortex-m-rt = "0.7"
|
||||
defmt = "1"
|
||||
defmt-rtt = "1"
|
||||
panic-probe = { version = "1", features = ["print-defmt"] }
|
||||
embedded-alloc = "0.7"
|
||||
static_cell = "2"
|
||||
rtic = { version = "2", features = ["thumbv7-backend"] }
|
||||
spacepackets = { version = "0.18", default-features = false, features = ["defmt"] }
|
||||
postcard = "1"
|
||||
|
||||
embassy-stm32 = { version = "0.6", features = ["stm32h753zi", "memory-x", "defmt", "time-driver-any"] }
|
||||
embassy-time = { version = "0.5", features = ["defmt-timestamp-uptime-ms", "generic-queue-16"] }
|
||||
embassy-net = { version = "0.9", features = ["medium-ethernet", "proto-ipv4", "tcp", "udp", "auto-icmp-echo-reply", "dhcpv4", "defmt"] }
|
||||
embassy-sync = "0.8"
|
||||
|
||||
[dev-dependencies]
|
||||
defmt-test = "0.5"
|
||||
|
||||
# cargo build/run
|
||||
[profile.dev]
|
||||
codegen-units = 1
|
||||
debug = 2
|
||||
debug-assertions = true # <-
|
||||
incremental = false
|
||||
opt-level = 's' # <-
|
||||
overflow-checks = true # <-
|
||||
|
||||
# cargo test
|
||||
[profile.test]
|
||||
codegen-units = 1
|
||||
debug = 2
|
||||
debug-assertions = true # <-
|
||||
incremental = false
|
||||
opt-level = 3 # <-
|
||||
overflow-checks = true # <-
|
||||
|
||||
# cargo build/run --release
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
debug = 2
|
||||
debug-assertions = false # <-
|
||||
incremental = false
|
||||
lto = 'fat'
|
||||
opt-level = 3 # <-
|
||||
overflow-checks = false # <-
|
||||
|
||||
# cargo test --release
|
||||
[profile.bench]
|
||||
codegen-units = 1
|
||||
debug = 2
|
||||
debug-assertions = false # <-
|
||||
incremental = false
|
||||
lto = 'fat'
|
||||
opt-level = 3 # <-
|
||||
overflow-checks = false # <-
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,115 @@
|
||||
sat-rs example for the STM32H753ZI-Nucleo board
|
||||
=======
|
||||
|
||||
This example application shows how the [sat-rs library](https://egit.irs.uni-stuttgart.de/rust/sat-rs)
|
||||
can be used on an embedded target.
|
||||
It also shows how a relatively simple OBSW could be built when no standard runtime is available.
|
||||
It uses [RTIC](https://rtic.rs/2/book/en/) as the concurrency framework and the
|
||||
[defmt](https://defmt.ferrous-systems.com/) framework for logging.
|
||||
|
||||
The STM32H753ZIT device was picked because it is one of the more powerful Cortex-M based STM32
|
||||
devices. It has more RAM available and allows commanding via Ethernet. The example is written for
|
||||
the NUCLEO-H753ZI board, which uses the MB1364 Nucleo-144 board layout.
|
||||
|
||||
## Pre-Requisites
|
||||
|
||||
Make sure the following tools are installed:
|
||||
|
||||
1. [`probe-rs`](https://probe.rs/): Application used to flash and debug the MCU.
|
||||
2. Optional and recommended: [VS Code](https://code.visualstudio.com/) with
|
||||
[probe-rs plugin](https://marketplace.visualstudio.com/items?itemName=probe-rs.probe-rs-debugger)
|
||||
for debugging.
|
||||
|
||||
## Preparing Rust and the repository
|
||||
|
||||
Building an application requires the `thumbv7em-none-eabihf` cross-compiler toolchain.
|
||||
If you have not installed it yet, you can do so with
|
||||
|
||||
```sh
|
||||
rustup target add thumbv7em-none-eabihf
|
||||
```
|
||||
|
||||
A default `.cargo` config file is provided as `.cargo/config.toml.template`. The build script
|
||||
copies it to `.cargo/config.toml` if that file does not exist yet. The copy is not tracked by git,
|
||||
so you can change settings like the runner for your setup.
|
||||
|
||||
Cargo reads the configuration before the build script runs, so the very first build on a fresh
|
||||
checkout does not use it yet and might fail. Simply run the build again, or copy the file
|
||||
manually beforehand:
|
||||
|
||||
```sh
|
||||
cp .cargo/config.toml.template .cargo/config.toml
|
||||
```
|
||||
|
||||
The configuration file also sets the target so it does not always have to be specified with
|
||||
the `--target` argument.
|
||||
|
||||
## Building
|
||||
|
||||
After that, assuming that you have a `.cargo/config.toml` setting the correct build target,
|
||||
you can simply build the application with
|
||||
|
||||
```sh
|
||||
cargo build
|
||||
```
|
||||
|
||||
## Flashing from the command line
|
||||
|
||||
You can flash the application from the command line using `probe-rs`:
|
||||
|
||||
```sh
|
||||
probe-rs run --chip STM32H753ZITx
|
||||
```
|
||||
|
||||
## Debugging with VS Code
|
||||
|
||||
The Nucleo board comes with an on-board ST-Link so all that is required to flash and debug
|
||||
the board is a USB cable. The code in this repository was debugged using [`probe-rs`](https://probe.rs/docs/tools/debuggerA)
|
||||
and the VS Code [`probe-rs` plugin](https://marketplace.visualstudio.com/items?itemName=probe-rs.probe-rs-debugger).
|
||||
Make sure to install this plugin first.
|
||||
|
||||
Sample configuration files are provided inside the `vscode` folder.
|
||||
Use `cp vscode .vscode -r` to use them for your project.
|
||||
|
||||
Some sample configuration files for VS Code were provided as well. You can simply use `Run` and `Debug`
|
||||
to automatically rebuild and flash your application.
|
||||
|
||||
The `tasks.json` and `launch.json` files are generic and you can use them immediately by opening
|
||||
the folder in VS code or adding it to a workspace.
|
||||
|
||||
## Commanding the board
|
||||
|
||||
The board is commanded via UDP on port 7301. It gets its IP address via DHCP, so it needs to be
|
||||
connected to a network with a DHCP server. The network configuration including the IP address is
|
||||
logged after startup. According to the board user manual UM2407, jumper JP6 and solder bridge SB72
|
||||
must be ON when using Ethernet. The telecommands are CCSDS space packets with a
|
||||
[`postcard`](https://docs.rs/postcard) serialized payload.
|
||||
|
||||
The [`embedded-client`](../embedded-client) application is used to command the board. Set the
|
||||
address of the board inside `embedded-client/config.toml`, for example:
|
||||
|
||||
```toml
|
||||
[interface]
|
||||
udp_addr = "192.168.1.50:7301"
|
||||
```
|
||||
|
||||
Then run the client from inside the `embedded-client` directory. For example, you can send a ping
|
||||
to the MCU using
|
||||
|
||||
```sh
|
||||
cargo run --bin stm32h7-client -- --ping
|
||||
```
|
||||
|
||||
and set the LED blink frequency to 500 ms using
|
||||
|
||||
```sh
|
||||
cargo run --bin stm32h7-client -- --set-led-frequency 500
|
||||
```
|
||||
|
||||
You can also pass the board address with `--udp-addr` instead of setting it inside the
|
||||
configuration file.
|
||||
|
||||
## Resources
|
||||
|
||||
- [STM32H743ZI Ethernet link checker example](https://github.com/stm32-rs/stm32h7xx-hal/blob/master/examples/ethernet-nucleo-h743zi2.rs)
|
||||
- [smoltcp DHCP client](https://github.com/smoltcp-rs/smoltcp/blob/main/examples/dhcp_client.rs)
|
||||
@@ -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.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();
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,119 @@
|
||||
/* Taken from https://github.com/stm32-rs/stm32h7xx-hal/pull/299, adapted slightly to work with */
|
||||
/* flip-link */
|
||||
MEMORY
|
||||
{
|
||||
/* This file is intended for parts in the STM32H743/743v/753/753v families (RM0433), */
|
||||
/* with the exception of the STM32H742/742v parts which have a different RAM layout. */
|
||||
/* - FLASH and RAM are mandatory memory sections. */
|
||||
/* - The sum of all non-FLASH sections must add to 1060K total device RAM. */
|
||||
/* - The FLASH section size must match your device, see table below. */
|
||||
|
||||
/* FLASH */
|
||||
/* Flash is divided in two independent banks (except 750xB). */
|
||||
/* Select the appropriate FLASH size for your device. */
|
||||
/* - STM32H750xB 128K (only FLASH1) */
|
||||
/* - STM32H750xB 1M (512K + 512K) */
|
||||
/* - STM32H743xI/753xI 2M ( 1M + 1M) */
|
||||
FLASH1 : ORIGIN = 0x08000000, LENGTH = 1M
|
||||
FLASH2 : ORIGIN = 0x08100000, LENGTH = 1M
|
||||
|
||||
/* Data TCM */
|
||||
/* - Two contiguous 64KB RAMs. */
|
||||
/* - Used for interrupt handlers, stacks and general RAM. */
|
||||
/* - Zero wait-states. */
|
||||
/* - The DTCM is taken as the origin of the base ram. (See below.) */
|
||||
/* This is also where the interrupt table and such will live, */
|
||||
/* which is required for deterministic performance. */
|
||||
/* Need a region called RAM */
|
||||
/* DTCM : ORIGIN = 0x20000000, LENGTH = 128K */
|
||||
RAM : ORIGIN = 0x20000000, LENGTH = 128K
|
||||
|
||||
/* Instruction TCM */
|
||||
/* - Used for latency-critical interrupt handlers etc. */
|
||||
/* - Zero wait-states. */
|
||||
ITCM : ORIGIN = 0x00000000, LENGTH = 64K
|
||||
|
||||
/* AXI SRAM */
|
||||
/* - AXISRAM is in D1 and accessible by all system masters except BDMA. */
|
||||
/* - Suitable for application data not stored in DTCM. */
|
||||
/* - Zero wait-states. */
|
||||
AXISRAM : ORIGIN = 0x24000000, LENGTH = 512K
|
||||
|
||||
/* AHB SRAM */
|
||||
/* - SRAM1-3 are in D2 and accessible by all system masters except BDMA. */
|
||||
/* Suitable for use as DMA buffers. */
|
||||
/* - SRAM4 is in D3 and additionally accessible by the BDMA. Used for BDMA */
|
||||
/* buffers, for storing application data in lower-power modes. */
|
||||
/* - Zero wait-states. */
|
||||
SRAM1 : ORIGIN = 0x30000000, LENGTH = 128K
|
||||
SRAM2 : ORIGIN = 0x30020000, LENGTH = 128K
|
||||
SRAM3 : ORIGIN = 0x30040000, LENGTH = 32K
|
||||
SRAM4 : ORIGIN = 0x38000000, LENGTH = 64K
|
||||
|
||||
/* Backup SRAM */
|
||||
BSRAM : ORIGIN = 0x38800000, LENGTH = 4K
|
||||
}
|
||||
|
||||
/*
|
||||
/* Assign the memory regions defined above for use. */
|
||||
/*
|
||||
|
||||
/* Provide the mandatory FLASH and RAM definitions for cortex-m-rt's linker script. */
|
||||
/* These do not work with flip-link */
|
||||
REGION_ALIAS(FLASH, FLASH1);
|
||||
/* REGION_ALIAS(RAM, DTCM); */
|
||||
|
||||
/* The location of the stack can be overridden using the `_stack_start` symbol. */
|
||||
/* - Set the stack location at the end of RAM, using all remaining space. */
|
||||
_stack_start = ORIGIN(RAM) + LENGTH(RAM);
|
||||
|
||||
/* The location of the .text section can be overridden using the */
|
||||
/* `_stext` symbol. By default it will place after .vector_table. */
|
||||
/* _stext = ORIGIN(FLASH) + 0x40c; */
|
||||
|
||||
/* Define sections for placing symbols into the extra memory regions above. */
|
||||
/* This makes them accessible from code. */
|
||||
/* - ITCM, DTCM and AXISRAM connect to a 64-bit wide bus -> align to 8 bytes. */
|
||||
/* - All other memories connect to a 32-bit wide bus -> align to 4 bytes. */
|
||||
SECTIONS {
|
||||
.flash2 (NOLOAD) : ALIGN(4) {
|
||||
*(.flash2 .flash2.*);
|
||||
. = ALIGN(4);
|
||||
} > FLASH2
|
||||
|
||||
.itcm (NOLOAD) : ALIGN(8) {
|
||||
*(.itcm .itcm.*);
|
||||
. = ALIGN(8);
|
||||
} > ITCM
|
||||
|
||||
.axisram (NOLOAD) : ALIGN(8) {
|
||||
*(.axisram .axisram.*);
|
||||
. = ALIGN(8);
|
||||
} > AXISRAM
|
||||
|
||||
.sram1 (NOLOAD) : ALIGN(8) {
|
||||
*(.sram1 .sram1.*);
|
||||
. = ALIGN(4);
|
||||
} > SRAM1
|
||||
|
||||
.sram2 (NOLOAD) : ALIGN(8) {
|
||||
*(.sram2 .sram2.*);
|
||||
. = ALIGN(4);
|
||||
} > SRAM2
|
||||
|
||||
.sram3 (NOLOAD) : ALIGN(4) {
|
||||
*(.sram3 .sram3.*);
|
||||
. = ALIGN(4);
|
||||
} > SRAM3
|
||||
|
||||
.sram4 (NOLOAD) : ALIGN(4) {
|
||||
*(.sram4 .sram4.*);
|
||||
. = ALIGN(4);
|
||||
} > SRAM4
|
||||
|
||||
.bsram (NOLOAD) : ALIGN(4) {
|
||||
*(.bsram .bsram.*);
|
||||
. = ALIGN(4);
|
||||
} > BSRAM
|
||||
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
targets = ["thumbv7em-none-eabihf"]
|
||||
@@ -0,0 +1,58 @@
|
||||
//! Blinks an LED
|
||||
//!
|
||||
//! This assumes that LD1 (green) is connected to PB0, LD2 (yellow) to PE1 and LD3 (red) to
|
||||
//! PB14. This assumption is true for the MB1364 Nucleo-144 board, for example the
|
||||
//! NUCLEO-H753ZI.
|
||||
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
use rtic::app;
|
||||
use stm32h7_nucleo_rtic as _;
|
||||
|
||||
#[app(device = embassy_stm32, peripherals = false, dispatchers = [SPI1])]
|
||||
mod app {
|
||||
use embassy_stm32::gpio;
|
||||
|
||||
#[shared]
|
||||
struct Shared {}
|
||||
|
||||
#[local]
|
||||
struct Local {}
|
||||
|
||||
#[init]
|
||||
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.PE1, gpio::Level::High, gpio::Speed::Low);
|
||||
let ld3 = gpio::Output::new(p.PB14, gpio::Level::High, gpio::Speed::Low);
|
||||
|
||||
// Schedule the blinking task
|
||||
blink::spawn(ld1, ld2, ld3).ok();
|
||||
|
||||
(Shared {}, Local {})
|
||||
}
|
||||
|
||||
#[task()]
|
||||
async fn blink(
|
||||
_cx: blink::Context,
|
||||
mut ld1: gpio::Output<'static>,
|
||||
mut ld2: gpio::Output<'static>,
|
||||
mut ld3: gpio::Output<'static>,
|
||||
) {
|
||||
loop {
|
||||
defmt::info!("high");
|
||||
ld1.set_high();
|
||||
ld2.set_high();
|
||||
ld3.set_high();
|
||||
embassy_time::Timer::after_millis(500).await;
|
||||
|
||||
defmt::info!("low");
|
||||
ld1.set_low();
|
||||
ld2.set_low();
|
||||
ld3.set_low();
|
||||
embassy_time::Timer::after_millis(500).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#![no_main]
|
||||
#![no_std]
|
||||
|
||||
// global logger + panicking-behavior + memory layout
|
||||
use stm32h7_nucleo_rtic as _;
|
||||
|
||||
#[cortex_m_rt::entry]
|
||||
fn main() -> ! {
|
||||
loop {
|
||||
defmt::println!("Hello, world!");
|
||||
cortex_m::asm::delay(100_000_000);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
#![no_main]
|
||||
#![no_std]
|
||||
|
||||
use defmt_rtt as _;
|
||||
use embassy_stm32 as _;
|
||||
use panic_probe as _;
|
||||
|
||||
// same panicking *behavior* as `panic-probe` but doesn't print a panic message
|
||||
// this prevents the panic message being printed *twice* when `defmt::panic` is invoked
|
||||
#[defmt::panic_handler]
|
||||
fn panic() -> ! {
|
||||
cortex_m::asm::udf()
|
||||
}
|
||||
|
||||
/// Hardfault handler.
|
||||
///
|
||||
/// Terminates the application and makes a semihosting-capable debug tool exit
|
||||
/// with an error. This seems better than the default, which is to spin in a
|
||||
/// loop.
|
||||
#[cortex_m_rt::exception]
|
||||
unsafe fn HardFault(_frame: &cortex_m_rt::ExceptionFrame) -> ! {
|
||||
panic!("unexpected hard fault");
|
||||
}
|
||||
|
||||
// defmt-test 0.3.0 has the limitation that this `#[tests]` attribute can only be used
|
||||
// once within a crate. the module can be in any file but there can only be at most
|
||||
// one `#[tests]` module in this library crate
|
||||
#[cfg(test)]
|
||||
#[defmt_test::tests]
|
||||
mod unit_tests {
|
||||
use defmt::assert;
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert!(true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
#![no_main]
|
||||
#![no_std]
|
||||
extern crate alloc;
|
||||
|
||||
use rtic::app;
|
||||
// global logger + panicking-behavior + memory layout
|
||||
use embassy_stm32::bind_interrupts;
|
||||
use stm32h7_nucleo_rtic as _;
|
||||
|
||||
use core::mem::MaybeUninit;
|
||||
use embedded_alloc::LlffHeap as Heap;
|
||||
|
||||
const DEFAULT_BLINK_FREQ_MS: u32 = 1000;
|
||||
const PORT: u16 = 7301;
|
||||
|
||||
const HEAP_SIZE: usize = 131_072;
|
||||
|
||||
#[global_allocator]
|
||||
static HEAP: Heap = Heap::empty();
|
||||
|
||||
/// Locally administered MAC address
|
||||
const MAC_ADDRESS: [u8; 6] = [0x02, 0x00, 0x11, 0x22, 0x33, 0x44];
|
||||
|
||||
const TC_QUEUE_DEPTH: usize = 32;
|
||||
const TM_QUEUE_DEPTH: usize = 32;
|
||||
|
||||
#[app(device = embassy_stm32, peripherals = false)]
|
||||
mod app {
|
||||
|
||||
use super::*;
|
||||
use arbitrary_int::u14;
|
||||
use embassy_net::StackResources;
|
||||
use embassy_net::udp::UdpSocket;
|
||||
use embassy_stm32::eth;
|
||||
use embassy_stm32::gpio;
|
||||
use embassy_stm32::peripherals;
|
||||
use embassy_stm32::rng;
|
||||
use embassy_sync::blocking_mutex::raw::NoopRawMutex;
|
||||
use embassy_time::Duration;
|
||||
use embassy_time::Timer;
|
||||
use embassy_time::WithTimeout as _;
|
||||
use embedded_types::TmHeader;
|
||||
use embedded_types::create_tm_packet;
|
||||
use embedded_types::stm32h7;
|
||||
use embedded_types::tm_size;
|
||||
use spacepackets::CcsdsPacketCreationError;
|
||||
use spacepackets::CcsdsPacketIdAndPsc;
|
||||
use spacepackets::CcsdsPacketReader;
|
||||
use spacepackets::SpHeader;
|
||||
use static_cell::StaticCell;
|
||||
|
||||
bind_interrupts!(struct Irqs {
|
||||
ETH => eth::InterruptHandler;
|
||||
HASH_RNG => rng::InterruptHandler<peripherals::RNG>;
|
||||
});
|
||||
|
||||
type Device = eth::Ethernet<
|
||||
'static,
|
||||
peripherals::ETH,
|
||||
eth::GenericPhy<eth::Sma<'static, peripherals::ETH_SMA>>,
|
||||
>;
|
||||
|
||||
struct BlinkyLeds {
|
||||
led1: gpio::Output<'static>,
|
||||
led2: gpio::Output<'static>,
|
||||
}
|
||||
|
||||
#[local]
|
||||
struct Local {
|
||||
net_runner: embassy_net::Runner<'static, Device>,
|
||||
net_stack: embassy_net::Stack<'static>,
|
||||
leds: BlinkyLeds,
|
||||
link_led: gpio::Output<'static>,
|
||||
tc_rx: embassy_sync::channel::Receiver<
|
||||
'static,
|
||||
NoopRawMutex,
|
||||
alloc::vec::Vec<u8>,
|
||||
TC_QUEUE_DEPTH,
|
||||
>,
|
||||
tc_tx: embassy_sync::channel::Sender<
|
||||
'static,
|
||||
NoopRawMutex,
|
||||
alloc::vec::Vec<u8>,
|
||||
TC_QUEUE_DEPTH,
|
||||
>,
|
||||
tm_rx: embassy_sync::channel::Receiver<
|
||||
'static,
|
||||
NoopRawMutex,
|
||||
alloc::vec::Vec<u8>,
|
||||
TM_QUEUE_DEPTH,
|
||||
>,
|
||||
tm_tx: embassy_sync::channel::Sender<
|
||||
'static,
|
||||
NoopRawMutex,
|
||||
alloc::vec::Vec<u8>,
|
||||
TM_QUEUE_DEPTH,
|
||||
>,
|
||||
}
|
||||
|
||||
#[shared]
|
||||
struct Shared {
|
||||
sequence_count: u14,
|
||||
blink_freq: embassy_time::Duration,
|
||||
}
|
||||
|
||||
#[init]
|
||||
fn init(_cx: init::Context) -> (Shared, Local) {
|
||||
defmt::println!("Starting sat-rs demo application for the STM32H753ZIT");
|
||||
|
||||
let mut config = embassy_stm32::Config::default();
|
||||
{
|
||||
use embassy_stm32::rcc::*;
|
||||
config.rcc.hsi = Some(HSIPrescaler::DIV1);
|
||||
config.rcc.csi = true;
|
||||
config.rcc.hsi48 = Some(Default::default()); // needed for RNG
|
||||
config.rcc.pll1 = Some(Pll {
|
||||
source: PllSource::HSI,
|
||||
prediv: PllPreDiv::DIV4,
|
||||
mul: PllMul::MUL50,
|
||||
divp: Some(PllDiv::DIV2),
|
||||
divq: None,
|
||||
divr: None,
|
||||
});
|
||||
config.rcc.sys = Sysclk::PLL1_P; // 400 Mhz
|
||||
config.rcc.ahb_pre = AHBPrescaler::DIV2; // 200 Mhz
|
||||
config.rcc.apb1_pre = APBPrescaler::DIV2; // 100 Mhz
|
||||
config.rcc.apb2_pre = APBPrescaler::DIV2; // 100 Mhz
|
||||
config.rcc.apb3_pre = APBPrescaler::DIV2; // 100 Mhz
|
||||
config.rcc.apb4_pre = APBPrescaler::DIV2; // 100 Mhz
|
||||
config.rcc.voltage_scale = VoltageScale::Scale1;
|
||||
}
|
||||
let periphs = embassy_stm32::init(config);
|
||||
|
||||
let link_led = gpio::Output::new(periphs.PB0, gpio::Level::Low, gpio::Speed::Medium);
|
||||
let mut led1 = gpio::Output::new(periphs.PE1, gpio::Level::Low, gpio::Speed::Medium);
|
||||
let mut led2 = gpio::Output::new(periphs.PB14, gpio::Level::Low, gpio::Speed::Medium);
|
||||
|
||||
// Criss-cross pattern looks cooler.
|
||||
led1.set_high();
|
||||
led2.set_low();
|
||||
let leds = BlinkyLeds { led1, led2 };
|
||||
|
||||
static PACKETS: StaticCell<eth::PacketQueue<4, 4>> = StaticCell::new();
|
||||
// warning: Not all STM32H7 devices have the exact same pins here
|
||||
// for STM32H747XIH, replace p.PB13 for PG12
|
||||
let device = eth::Ethernet::new(
|
||||
PACKETS.init(eth::PacketQueue::<4, 4>::new()),
|
||||
periphs.ETH,
|
||||
Irqs,
|
||||
periphs.PA1, // ref_clk
|
||||
periphs.PA7, // CRS_DV: Carrier Sense
|
||||
periphs.PC4, // RX_D0: Received Bit 0
|
||||
periphs.PC5, // RX_D1: Received Bit 1
|
||||
periphs.PG13, // TX_D0: Transmit Bit 0
|
||||
periphs.PB13, // TX_D1: Transmit Bit 1
|
||||
periphs.PG11, // TX_EN: Transmit Enable
|
||||
MAC_ADDRESS,
|
||||
periphs.ETH_SMA,
|
||||
periphs.PA2, // mdio
|
||||
periphs.PC1, // mdc
|
||||
);
|
||||
|
||||
let config = embassy_net::Config::dhcpv4(embassy_net::DhcpConfig::default());
|
||||
|
||||
// Generate random seed.
|
||||
let mut rng = rng::Rng::new(periphs.RNG, Irqs);
|
||||
let mut seed = [0; 8];
|
||||
rng.fill_bytes(&mut seed);
|
||||
let seed = u64::from_le_bytes(seed);
|
||||
|
||||
// Init network stack
|
||||
static RESOURCES: StaticCell<StackResources<3>> = StaticCell::new();
|
||||
let (stack, runner) =
|
||||
embassy_net::new(device, config, RESOURCES.init(StackResources::new()), seed);
|
||||
|
||||
// Set up global allocator. Use AXISRAM for the heap.
|
||||
#[unsafe(link_section = ".axisram")]
|
||||
static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE];
|
||||
unsafe { HEAP.init(&raw mut HEAP_MEM as usize, HEAP_SIZE) }
|
||||
|
||||
static TC_CHANNEL: static_cell::ConstStaticCell<
|
||||
embassy_sync::channel::Channel<NoopRawMutex, alloc::vec::Vec<u8>, TC_QUEUE_DEPTH>,
|
||||
> = static_cell::ConstStaticCell::new(embassy_sync::channel::Channel::new());
|
||||
let tc_channel = TC_CHANNEL.take();
|
||||
let tc_sender = tc_channel.sender();
|
||||
let tc_receiver = tc_channel.receiver();
|
||||
|
||||
static TM_CHANNEL: static_cell::ConstStaticCell<
|
||||
embassy_sync::channel::Channel<NoopRawMutex, alloc::vec::Vec<u8>, TM_QUEUE_DEPTH>,
|
||||
> = static_cell::ConstStaticCell::new(embassy_sync::channel::Channel::new());
|
||||
let tm_channel = TM_CHANNEL.take();
|
||||
let tm_sender = tm_channel.sender();
|
||||
let tm_receiver = tm_channel.receiver();
|
||||
|
||||
net_lib_task::spawn().expect("spawning net library task failed");
|
||||
net_app_task::spawn().expect("spawning net application task failed");
|
||||
blinky::spawn().expect("spawning blink task failed");
|
||||
tc_handler::spawn().expect("spawning TC handler task failed");
|
||||
|
||||
(
|
||||
Shared {
|
||||
blink_freq: Duration::from_millis(DEFAULT_BLINK_FREQ_MS as u64),
|
||||
sequence_count: u14::new(0),
|
||||
},
|
||||
Local {
|
||||
link_led,
|
||||
leds,
|
||||
net_runner: runner,
|
||||
net_stack: stack,
|
||||
tc_tx: tc_sender,
|
||||
tc_rx: tc_receiver,
|
||||
tm_tx: tm_sender,
|
||||
tm_rx: tm_receiver,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[task(local = [leds], shared=[blink_freq])]
|
||||
async fn blinky(mut cx: blinky::Context) {
|
||||
let leds = cx.local.leds;
|
||||
loop {
|
||||
leds.led1.toggle();
|
||||
leds.led2.toggle();
|
||||
let current_blink_freq = cx.shared.blink_freq.lock(|current| *current);
|
||||
Timer::after_millis(current_blink_freq.as_millis()).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[task(local=[net_runner])]
|
||||
async fn net_lib_task(cx: net_lib_task::Context) {
|
||||
cx.local.net_runner.run().await;
|
||||
}
|
||||
|
||||
#[task(local = [net_stack, link_led, tc_tx, tm_rx])]
|
||||
async fn net_app_task(cx: net_app_task::Context) {
|
||||
pub const MTU: usize = 1500;
|
||||
|
||||
// Ensure those are in the data section by making them static.
|
||||
static RX_UDP_META: static_cell::ConstStaticCell<[embassy_net::udp::PacketMetadata; 8]> =
|
||||
static_cell::ConstStaticCell::new([embassy_net::udp::PacketMetadata::EMPTY; 8]);
|
||||
static TX_UDP_META: static_cell::ConstStaticCell<[embassy_net::udp::PacketMetadata; 8]> =
|
||||
static_cell::ConstStaticCell::new([embassy_net::udp::PacketMetadata::EMPTY; 8]);
|
||||
static TX_UDP_BUFS: static_cell::ConstStaticCell<[u8; MTU]> =
|
||||
static_cell::ConstStaticCell::new([0; MTU]);
|
||||
static RX_UDP_BUFS: static_cell::ConstStaticCell<[u8; MTU]> =
|
||||
static_cell::ConstStaticCell::new([0; MTU]);
|
||||
|
||||
let rx_udp_meta = RX_UDP_META.take();
|
||||
let rx_udp_bufs = RX_UDP_BUFS.take();
|
||||
let tx_udp_meta = TX_UDP_META.take();
|
||||
let tx_udp_bufs = TX_UDP_BUFS.take();
|
||||
|
||||
let mut rx_buffer = [0; MTU];
|
||||
|
||||
loop {
|
||||
cx.local.net_stack.wait_link_up().await;
|
||||
cx.local.link_led.set_high();
|
||||
defmt::info!("Network link is up");
|
||||
|
||||
// Ensure DHCP configuration is up before trying connect
|
||||
cx.local.net_stack.wait_config_up().await;
|
||||
|
||||
let config = cx.local.net_stack.config_v4();
|
||||
defmt::info!("Network task initialized, config: {}", config);
|
||||
|
||||
let mut udp = UdpSocket::new(
|
||||
cx.local.net_stack.clone(),
|
||||
rx_udp_meta,
|
||||
rx_udp_bufs,
|
||||
tx_udp_meta,
|
||||
tx_udp_bufs,
|
||||
);
|
||||
defmt::info!("UDP socket bound to port {}", PORT);
|
||||
udp.bind(PORT).expect("failed to bind UDP socket");
|
||||
let mut remote_endpoint = None;
|
||||
loop {
|
||||
if !cx.local.net_stack.is_link_up() {
|
||||
defmt::warn!("Network link is down");
|
||||
cx.local.link_led.set_low();
|
||||
break;
|
||||
}
|
||||
match udp
|
||||
.recv_from(&mut rx_buffer)
|
||||
.with_timeout(Duration::from_millis(200))
|
||||
.await
|
||||
{
|
||||
Ok(result) => match result {
|
||||
Ok((data, meta)) => {
|
||||
remote_endpoint = Some(meta.endpoint);
|
||||
defmt::debug!("UDP RX {}, Meta: {}", data, meta);
|
||||
cx.local.tc_tx.send(rx_buffer[0..data].to_vec()).await;
|
||||
}
|
||||
Err(e) => {
|
||||
defmt::warn!("udp receive error: {}", e);
|
||||
Timer::after_millis(100).await;
|
||||
}
|
||||
},
|
||||
Err(_e) => (),
|
||||
}
|
||||
if let Some(endpoint) = remote_endpoint {
|
||||
while let Ok(packet) = cx.local.tm_rx.try_receive() {
|
||||
match udp.send_to(&packet, endpoint).await {
|
||||
Ok(_) => {
|
||||
defmt::debug!("UDP TX: {} bytes to: {}", packet.len(), endpoint)
|
||||
}
|
||||
Err(e) => defmt::warn!("udp send error: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[task(local = [tc_rx, tm_tx], shared=[sequence_count, blink_freq])]
|
||||
async fn tc_handler(mut cx: tc_handler::Context) {
|
||||
loop {
|
||||
let tc = cx.local.tc_rx.receive().await;
|
||||
|
||||
match CcsdsPacketReader::new_with_checksum(&tc) {
|
||||
Ok(packet) => {
|
||||
let packet_id = packet.packet_id();
|
||||
let psc = packet.psc();
|
||||
let tc_packet_id = CcsdsPacketIdAndPsc { packet_id, psc };
|
||||
if let Ok(request) =
|
||||
postcard::from_bytes::<stm32h7::Request>(packet.packet_data())
|
||||
{
|
||||
let response = match request {
|
||||
stm32h7::Request::Ping => {
|
||||
defmt::info!("Received Ping request");
|
||||
stm32h7::Response::Ok
|
||||
}
|
||||
stm32h7::Request::ChangeBlinkFrequency(duration) => {
|
||||
defmt::info!(
|
||||
"Received blinky frequency change request: {} ms",
|
||||
duration.as_millis()
|
||||
);
|
||||
cx.shared.blink_freq.lock(|current| {
|
||||
*current = Duration::from_millis(duration.as_millis() as u64)
|
||||
});
|
||||
stm32h7::Response::Ok
|
||||
}
|
||||
};
|
||||
let sequence_count = cx.shared.sequence_count.lock(|v| {
|
||||
let current = *v;
|
||||
*v = v.wrapping_add(u14::new(1));
|
||||
current
|
||||
});
|
||||
|
||||
// Send Pong/OK response immediately.
|
||||
if let Err(e) =
|
||||
send_tm(tc_packet_id, response, sequence_count, cx.local.tm_tx).await
|
||||
{
|
||||
defmt::warn!("Failed to send TM response: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => defmt::warn!("Failed to parse received TC packet: {}", e,),
|
||||
}
|
||||
defmt::info!("Received from UDP client: {}", tc.as_slice());
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_tm(
|
||||
tc_packet_id: CcsdsPacketIdAndPsc,
|
||||
response: stm32h7::Response,
|
||||
current_seq_count: u14,
|
||||
sender: &embassy_sync::channel::Sender<
|
||||
'static,
|
||||
NoopRawMutex,
|
||||
alloc::vec::Vec<u8>,
|
||||
TM_QUEUE_DEPTH,
|
||||
>,
|
||||
) -> Result<(), CcsdsPacketCreationError> {
|
||||
let sp_header = SpHeader::new_for_unseg_tc(stm32h7::PUS_APID, current_seq_count, 0);
|
||||
let tm_header = TmHeader {
|
||||
tc_packet_id: Some(tc_packet_id),
|
||||
uptime_millis: embassy_time::Instant::now().as_millis(),
|
||||
};
|
||||
let tm_size = tm_size(&tm_header, &response);
|
||||
let mut packet = alloc::vec![0; tm_size];
|
||||
create_tm_packet(&mut packet, sp_header, tm_header, response)?;
|
||||
sender.send(packet).await;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
|
||||
use 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'
|
||||
// feature)
|
||||
#[defmt_test::tests]
|
||||
mod tests {
|
||||
use defmt::assert;
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert!(true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "types"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", default-features = false, features = ["derive"] }
|
||||
spacepackets = { version = "0.18", default-features = false, features = ["serde"] }
|
||||
satrs = { path = "../../satrs", default-features = false, features = ["serde"] }
|
||||
num_enum = { version = "0.7", default-features = false }
|
||||
strum = { version = "0.28", default-features = false, features = ["derive"] }
|
||||
postcard = { version = "1", features = ["alloc"] }
|
||||
thiserror = { version = "2", default-features = false }
|
||||
bitbybit = "2"
|
||||
arbitrary-int = "2"
|
||||
heapless = { version = "0.9", features = ["serde"] }
|
||||
@@ -0,0 +1,39 @@
|
||||
#[derive(
|
||||
serde::Serialize,
|
||||
serde::Deserialize,
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
num_enum::IntoPrimitive,
|
||||
num_enum::TryFromPrimitive,
|
||||
)]
|
||||
#[repr(u32)]
|
||||
pub enum Mode {
|
||||
Passive,
|
||||
Safe,
|
||||
Idle,
|
||||
}
|
||||
|
||||
pub mod request {
|
||||
use super::Mode;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum ModeRequest {
|
||||
SetMode(Mode),
|
||||
ReadMode,
|
||||
}
|
||||
}
|
||||
|
||||
pub mod response {
|
||||
use super::Mode;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum ModeReport {
|
||||
/// Mode of the assembly.
|
||||
Mode(super::Mode),
|
||||
/// Children are in wrong mode after commanding.
|
||||
WrongMode([Option<Mode>; 2]),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
pub mod request {
|
||||
use crate::{DeviceMode, HealthRequest, 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),
|
||||
Health(HealthRequest),
|
||||
}
|
||||
|
||||
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,
|
||||
Request::Health(_health) => crate::MessageType::Health,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Message for Request {
|
||||
fn message_type(&self) -> crate::MessageType {
|
||||
self.message_type()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Copy, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct SensorData {
|
||||
pub valid: bool,
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub z: f32,
|
||||
}
|
||||
|
||||
#[derive(strum::EnumDiscriminants, serde::Serialize, serde::Deserialize, Clone, Copy, Debug)]
|
||||
#[strum_discriminants(derive(num_enum::IntoPrimitive))]
|
||||
#[repr(u16)]
|
||||
pub enum Event {
|
||||
/// The SPI fault counter exceeded its threshold. Followed by a recovery event.
|
||||
SpiFaultThresholdExceeded,
|
||||
/// A commanded or autonomous mode transition completed.
|
||||
ModeChanged(crate::DeviceMode),
|
||||
Recovery(satrs::fdir::RecoveryEvent),
|
||||
}
|
||||
|
||||
impl crate::Message for Event {
|
||||
fn message_type(&self) -> crate::MessageType {
|
||||
crate::MessageType::Event
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::EventId for Event {
|
||||
fn event_id(&self) -> u16 {
|
||||
match self {
|
||||
Event::Recovery(event) => crate::recovery_event_id(*event),
|
||||
_ => EventDiscriminants::from(self).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod response {
|
||||
use crate::{DeviceMode, Message, acs::mgm::SensorData};
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)]
|
||||
pub enum HkResponse {
|
||||
MgmData(SensorData),
|
||||
}
|
||||
|
||||
#[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,158 @@
|
||||
use core::str::FromStr;
|
||||
|
||||
use num_enum::TryFromPrimitive as _;
|
||||
use satrs::mode::ModeRaw;
|
||||
|
||||
use crate::DeviceMode;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Mode {
|
||||
/// 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 From<Mode> for ModeRaw {
|
||||
fn from(value: Mode) -> Self {
|
||||
match value {
|
||||
Mode::Device(device_mode) => device_mode.into(),
|
||||
Mode::NoModeKeeping => 5,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<ModeRaw> for Mode {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: ModeRaw) -> Result<Self, Self::Error> {
|
||||
match DeviceMode::try_from_primitive(value) {
|
||||
Ok(val) => Ok(Mode::Device(val)),
|
||||
Err(_) => {
|
||||
if value == 5 {
|
||||
Ok(Mode::NoModeKeeping)
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Mode {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"off" => Ok(Mode::Device(DeviceMode::Off)),
|
||||
"on" => Ok(Mode::Device(DeviceMode::On)),
|
||||
"normal" => Ok(Mode::Device(DeviceMode::Normal)),
|
||||
"no_mode_keeping" => Ok(Mode::NoModeKeeping),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod request {
|
||||
use crate::{HkRequestType, Message, acs::mgm_assembly::Mode};
|
||||
|
||||
#[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(Mode),
|
||||
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 ModeResponse {
|
||||
/// Mode of the assembly.
|
||||
Mode(super::Mode),
|
||||
/// 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(ModeResponse),
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(strum::EnumDiscriminants, serde::Serialize, serde::Deserialize, Clone, Copy, Debug)]
|
||||
#[strum_discriminants(derive(num_enum::IntoPrimitive))]
|
||||
#[repr(u16)]
|
||||
pub enum Event {
|
||||
/// A commanded or autonomous mode transition completed.
|
||||
ModeChanged(Mode),
|
||||
}
|
||||
|
||||
impl crate::Message for Event {
|
||||
fn message_type(&self) -> crate::MessageType {
|
||||
crate::MessageType::Event
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::EventId for Event {
|
||||
fn event_id(&self) -> u16 {
|
||||
EventDiscriminants::from(self).into()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
use crate::DeviceMode;
|
||||
|
||||
/// Commanded magnetic dipole. Simple model using raw values per axis.
|
||||
#[derive(serde::Serialize, serde::Deserialize, Default, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Dipole {
|
||||
pub x: i16,
|
||||
pub y: i16,
|
||||
pub z: i16,
|
||||
}
|
||||
|
||||
impl Dipole {
|
||||
pub const LEN: usize = 6;
|
||||
|
||||
pub fn to_be_bytes(&self) -> [u8; Self::LEN] {
|
||||
let [x0, x1] = self.x.to_be_bytes();
|
||||
let [y0, y1] = self.y.to_be_bytes();
|
||||
let [z0, z1] = self.z.to_be_bytes();
|
||||
[x0, x1, y0, y1, z0, z1]
|
||||
}
|
||||
|
||||
pub fn from_be_bytes(bytes: &[u8; Self::LEN]) -> Self {
|
||||
let [x0, x1, y0, y1, z0, z1] = *bytes;
|
||||
Self {
|
||||
x: i16::from_be_bytes([x0, x1]),
|
||||
y: i16::from_be_bytes([y0, y1]),
|
||||
z: i16::from_be_bytes([z0, z1]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Default, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct HkSet {
|
||||
pub valid: bool,
|
||||
pub dipole: Dipole,
|
||||
pub torquing: bool,
|
||||
}
|
||||
|
||||
pub mod request {
|
||||
use crate::{DeviceMode, HealthRequest, HkRequestType, Message};
|
||||
|
||||
use super::Dipole;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ModeRequest {
|
||||
SetMode(DeviceMode),
|
||||
ReadMode,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)]
|
||||
pub enum Request {
|
||||
Ping,
|
||||
Hk(HkRequestType),
|
||||
Mode(ModeRequest),
|
||||
/// Only accepted in normal mode.
|
||||
ApplyTorque {
|
||||
dipole: Dipole,
|
||||
duration: core::time::Duration,
|
||||
},
|
||||
Health(HealthRequest),
|
||||
}
|
||||
|
||||
impl Message for Request {
|
||||
fn message_type(&self) -> crate::MessageType {
|
||||
match self {
|
||||
Request::Ping => crate::MessageType::Verification,
|
||||
Request::Hk(_) => crate::MessageType::Hk,
|
||||
Request::Mode(_) => crate::MessageType::Mode,
|
||||
Request::ApplyTorque { .. } => crate::MessageType::Action,
|
||||
Request::Health(_) => crate::MessageType::Health,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(strum::EnumDiscriminants, serde::Serialize, serde::Deserialize, Clone, Copy, Debug)]
|
||||
#[strum_discriminants(derive(num_enum::IntoPrimitive))]
|
||||
#[repr(u16)]
|
||||
pub enum Event {
|
||||
/// A commanded mode transition completed.
|
||||
ModeChanged(DeviceMode),
|
||||
/// Too many requests were not answered correctly. Followed by a recovery event.
|
||||
ReplyFaultThresholdExceeded,
|
||||
Recovery(satrs::fdir::RecoveryEvent),
|
||||
}
|
||||
|
||||
impl crate::Message for Event {
|
||||
fn message_type(&self) -> crate::MessageType {
|
||||
crate::MessageType::Event
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::EventId for Event {
|
||||
fn event_id(&self) -> u16 {
|
||||
match self {
|
||||
Event::Recovery(event) => crate::recovery_event_id(*event),
|
||||
_ => EventDiscriminants::from(self).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod response {
|
||||
use crate::{DeviceMode, Message};
|
||||
|
||||
use super::HkSet;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ModeResponse {
|
||||
/// New mode has been set.
|
||||
Mode(DeviceMode),
|
||||
/// Setting a mode timed out.
|
||||
SetModeTimeout,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Response {
|
||||
Ok,
|
||||
Hk(HkSet),
|
||||
Mode(ModeResponse),
|
||||
/// The command requires the device to be in normal mode.
|
||||
NotInNormalMode,
|
||||
/// The device did not answer the command with a valid reply in time.
|
||||
ReplyTimeout,
|
||||
}
|
||||
|
||||
impl Message for Response {
|
||||
fn message_type(&self) -> crate::MessageType {
|
||||
match self {
|
||||
Response::Ok | Response::NotInNormalMode | Response::ReplyTimeout => {
|
||||
crate::MessageType::Verification
|
||||
}
|
||||
Response::Hk(_) => crate::MessageType::Hk,
|
||||
Response::Mode(_) => crate::MessageType::Mode,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod ctrl;
|
||||
pub mod mgm;
|
||||
pub mod mgm_assembly;
|
||||
pub mod mgt;
|
||||
pub mod subsystem;
|
||||
Loaded 100 of 130 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user