sat-rs/satrs-minisim/src/lib.rs
Robin Mueller 29167736db
Some checks are pending
Rust/sat-rs/pipeline/pr-main Build started...
Integration of the mini simulator into the sat-rs example
2024-06-03 15:18:23 +02:00

471 lines
14 KiB
Rust

use asynchronix::time::MonotonicTime;
use num_enum::{IntoPrimitive, TryFromPrimitive};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
pub enum SimComponent {
SimCtrl,
MgmLis3Mdl,
Mgt,
Pcdu,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SimMessage {
pub target: SimComponent,
pub payload: String,
}
/// A generic simulation request type. Right now, the payload data is expected to be
/// JSON, which might be changed in the future.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SimRequest {
inner: SimMessage,
pub timestamp: MonotonicTime,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SimMessageType {
Request,
Reply,
}
/// Generic trait implemented by simulation request or reply payloads. It ties the request or
/// reply to a specific target and provides an API which does boilerplate tasks like checking the
/// validity of the target.
pub trait SerializableSimMsgPayload<P: SimMessageProvider>:
Serialize + DeserializeOwned + Sized
{
const TARGET: SimComponent;
fn from_sim_message(sim_message: &P) -> Result<Self, SimMessageError<P>> {
if sim_message.component() == Self::TARGET {
return Ok(serde_json::from_str(sim_message.payload())?);
}
Err(SimMessageError::TargetRequestMissmatch(sim_message.clone()))
}
}
pub trait SimMessageProvider: Serialize + DeserializeOwned + Clone + Sized {
fn msg_type(&self) -> SimMessageType;
fn component(&self) -> SimComponent;
fn payload(&self) -> &String;
fn from_raw_data(data: &[u8]) -> serde_json::Result<Self> {
serde_json::from_slice(data)
}
}
impl SimRequest {
pub fn new_with_epoch_time<T: SerializableSimMsgPayload<SimRequest>>(
serializable_request: T,
) -> Self {
Self::new(serializable_request, MonotonicTime::EPOCH)
}
pub fn new<T: SerializableSimMsgPayload<SimRequest>>(
serializable_request: T,
timestamp: MonotonicTime,
) -> Self {
Self {
inner: SimMessage {
target: T::TARGET,
payload: serde_json::to_string(&serializable_request).unwrap(),
},
timestamp,
}
}
}
impl SimMessageProvider for SimRequest {
fn component(&self) -> SimComponent {
self.inner.target
}
fn payload(&self) -> &String {
&self.inner.payload
}
fn msg_type(&self) -> SimMessageType {
SimMessageType::Request
}
}
/// A generic simulation reply type. Right now, the payload data is expected to be
/// JSON, which might be changed in the future.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SimReply {
inner: SimMessage,
}
impl SimReply {
pub fn new<T: SerializableSimMsgPayload<SimReply>>(serializable_reply: &T) -> Self {
Self {
inner: SimMessage {
target: T::TARGET,
payload: serde_json::to_string(serializable_reply).unwrap(),
},
}
}
}
impl SimMessageProvider for SimReply {
fn component(&self) -> SimComponent {
self.inner.target
}
fn payload(&self) -> &String {
&self.inner.payload
}
fn msg_type(&self) -> SimMessageType {
SimMessageType::Reply
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SimCtrlRequest {
Ping,
}
impl SerializableSimMsgPayload<SimRequest> for SimCtrlRequest {
const TARGET: SimComponent = SimComponent::SimCtrl;
}
pub type SimReplyError = SimMessageError<SimReply>;
pub type SimRequestError = SimMessageError<SimRequest>;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SimMessageError<P> {
SerdeJson(String),
TargetRequestMissmatch(P),
}
impl<P> From<serde_json::Error> for SimMessageError<P> {
fn from(error: serde_json::Error) -> SimMessageError<P> {
SimMessageError::SerdeJson(error.to_string())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SimCtrlReply {
Pong,
InvalidRequest(SimRequestError),
}
impl SerializableSimMsgPayload<SimReply> for SimCtrlReply {
const TARGET: SimComponent = SimComponent::SimCtrl;
}
impl From<SimRequestError> for SimCtrlReply {
fn from(error: SimRequestError) -> Self {
SimCtrlReply::InvalidRequest(error)
}
}
pub mod eps {
use super::*;
use satrs::power::{SwitchState, SwitchStateBinary};
use std::collections::HashMap;
use strum::{EnumIter, IntoEnumIterator};
pub type SwitchMap = HashMap<PcduSwitch, SwitchState>;
pub type SwitchMapBinary = HashMap<PcduSwitch, SwitchStateBinary>;
pub struct SwitchMapWrapper(pub SwitchMap);
pub struct SwitchMapBinaryWrapper(pub SwitchMapBinary);
#[derive(
Debug,
Copy,
Clone,
PartialEq,
Eq,
Serialize,
Deserialize,
Hash,
EnumIter,
IntoPrimitive,
TryFromPrimitive,
)]
#[repr(u16)]
pub enum PcduSwitch {
Mgm = 0,
Mgt = 1,
}
impl Default for SwitchMapBinaryWrapper {
fn default() -> Self {
let mut switch_map = SwitchMapBinary::default();
for entry in PcduSwitch::iter() {
switch_map.insert(entry, SwitchStateBinary::Off);
}
Self(switch_map)
}
}
impl Default for SwitchMapWrapper {
fn default() -> Self {
let mut switch_map = SwitchMap::default();
for entry in PcduSwitch::iter() {
switch_map.insert(entry, SwitchState::Unknown);
}
Self(switch_map)
}
}
impl SwitchMapWrapper {
pub fn new_with_init_switches_off() -> Self {
let mut switch_map = SwitchMap::default();
for entry in PcduSwitch::iter() {
switch_map.insert(entry, SwitchState::Off);
}
Self(switch_map)
}
pub fn from_binary_switch_map_ref(switch_map: &SwitchMapBinary) -> Self {
Self(
switch_map
.iter()
.map(|(key, value)| (*key, SwitchState::from(*value)))
.collect(),
)
}
}
#[derive(Debug, Copy, Clone)]
#[repr(u8)]
pub enum PcduRequestId {
SwitchDevice = 0,
RequestSwitchInfo = 1,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PcduRequest {
SwitchDevice {
switch: PcduSwitch,
state: SwitchStateBinary,
},
RequestSwitchInfo,
}
impl SerializableSimMsgPayload<SimRequest> for PcduRequest {
const TARGET: SimComponent = SimComponent::Pcdu;
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum PcduReply {
// Ack,
SwitchInfo(SwitchMapBinary),
}
impl SerializableSimMsgPayload<SimReply> for PcduReply {
const TARGET: SimComponent = SimComponent::Pcdu;
}
}
pub mod acs {
use std::time::Duration;
use satrs::power::SwitchStateBinary;
use super::*;
pub trait MgmReplyProvider: Send + 'static {
fn create_mgm_reply(common: MgmReplyCommon) -> SimReply;
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
pub enum MgmRequestLis3Mdl {
RequestSensorData,
}
impl SerializableSimMsgPayload<SimRequest> for MgmRequestLis3Mdl {
const TARGET: SimComponent = SimComponent::MgmLis3Mdl;
}
// 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 MgmSensorValuesMicroTesla {
pub x: f32,
pub y: f32,
pub z: f32,
}
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub struct MgmReplyCommon {
pub switch_state: SwitchStateBinary,
pub sensor_values: MgmSensorValuesMicroTesla,
}
pub const MGT_GEN_MAGNETIC_FIELD: MgmSensorValuesMicroTesla = MgmSensorValuesMicroTesla {
x: 30.0,
y: -30.0,
z: 30.0,
};
pub const ALL_ONES_SENSOR_VAL: i16 = 0xffff_u16 as i16;
pub mod lis3mdl {
use super::*;
// 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;
pub const FIELD_LSB_PER_GAUSS_8_SENS: f32 = 1.0 / 3421.0;
pub const FIELD_LSB_PER_GAUSS_12_SENS: f32 = 1.0 / 2281.0;
pub const FIELD_LSB_PER_GAUSS_16_SENS: f32 = 1.0 / 1711.0;
#[derive(Default, Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub struct MgmLis3RawValues {
pub x: i16,
pub y: i16,
pub z: i16,
}
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub struct MgmLis3MdlReply {
pub common: MgmReplyCommon,
// Raw sensor values which are transmitted by the LIS3 device in little-endian
// order.
pub raw: MgmLis3RawValues,
}
impl MgmLis3MdlReply {
pub fn new(common: MgmReplyCommon) -> Self {
match common.switch_state {
SwitchStateBinary::Off => Self {
common,
raw: MgmLis3RawValues {
x: ALL_ONES_SENSOR_VAL,
y: ALL_ONES_SENSOR_VAL,
z: ALL_ONES_SENSOR_VAL,
},
},
SwitchStateBinary::On => {
let mut raw_reply: [u8; 7] = [0; 7];
let raw_x: i16 = (common.sensor_values.x
/ (GAUSS_TO_MICROTESLA_FACTOR as f32 * FIELD_LSB_PER_GAUSS_4_SENS))
.round() as i16;
let raw_y: i16 = (common.sensor_values.y
/ (GAUSS_TO_MICROTESLA_FACTOR as f32 * FIELD_LSB_PER_GAUSS_4_SENS))
.round() as i16;
let raw_z: i16 = (common.sensor_values.z
/ (GAUSS_TO_MICROTESLA_FACTOR as f32 * FIELD_LSB_PER_GAUSS_4_SENS))
.round() as i16;
// The first byte is a dummy byte.
raw_reply[1..3].copy_from_slice(&raw_x.to_be_bytes());
raw_reply[3..5].copy_from_slice(&raw_y.to_be_bytes());
raw_reply[5..7].copy_from_slice(&raw_z.to_be_bytes());
Self {
common,
raw: MgmLis3RawValues {
x: raw_x,
y: raw_y,
z: raw_z,
},
}
}
}
}
}
impl SerializableSimMsgPayload<SimReply> for MgmLis3MdlReply {
const TARGET: SimComponent = SimComponent::MgmLis3Mdl;
}
impl MgmReplyProvider for MgmLis3MdlReply {
fn create_mgm_reply(common: MgmReplyCommon) -> SimReply {
SimReply::new(&Self::new(common))
}
}
}
// Simple model using i16 values.
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MgtDipole {
pub x: i16,
pub y: i16,
pub z: i16,
}
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub enum MgtRequestType {
ApplyTorque,
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
pub enum MgtRequest {
ApplyTorque {
duration: Duration,
dipole: MgtDipole,
},
RequestHk,
}
impl SerializableSimMsgPayload<SimRequest> for MgtRequest {
const TARGET: SimComponent = SimComponent::Mgt;
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MgtHkSet {
pub dipole: MgtDipole,
pub torquing: bool,
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
pub enum MgtReply {
Ack(MgtRequestType),
Nak(MgtRequestType),
Hk(MgtHkSet),
}
impl SerializableSimMsgPayload<SimReply> for MgtReply {
const TARGET: SimComponent = SimComponent::MgmLis3Mdl;
}
}
pub mod udp {
pub const SIM_CTRL_PORT: u16 = 7303;
}
#[cfg(test)]
pub mod tests {
use super::*;
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DummyRequest {
Ping,
}
impl SerializableSimMsgPayload<SimRequest> for DummyRequest {
const TARGET: SimComponent = SimComponent::SimCtrl;
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DummyReply {
Pong,
}
impl SerializableSimMsgPayload<SimReply> for DummyReply {
const TARGET: SimComponent = SimComponent::SimCtrl;
}
#[test]
fn test_basic_request() {
let sim_request = SimRequest::new_with_epoch_time(DummyRequest::Ping);
assert_eq!(sim_request.component(), SimComponent::SimCtrl);
assert_eq!(sim_request.msg_type(), SimMessageType::Request);
let dummy_request =
DummyRequest::from_sim_message(&sim_request).expect("deserialization failed");
assert_eq!(dummy_request, DummyRequest::Ping);
}
#[test]
fn test_basic_reply() {
let sim_reply = SimReply::new(&DummyReply::Pong);
assert_eq!(sim_reply.component(), SimComponent::SimCtrl);
assert_eq!(sim_reply.msg_type(), SimMessageType::Reply);
let dummy_request =
DummyReply::from_sim_message(&sim_reply).expect("deserialization failed");
assert_eq!(dummy_request, DummyReply::Pong);
}
}