From 56b5076230dc33222f7973861bf7b8834168859f Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sun, 28 Apr 2024 12:39:34 +0200 Subject: [PATCH 1/3] use pydantic instead of serde in Python --- Cargo.lock | 2 +- pytmtc/opssat_tmtc/camera.py | 20 ++++ pytmtc/opssat_tmtc/camera_params.py | 17 --- pytmtc/opssat_tmtc/common.py | 1 - pytmtc/opssat_tmtc/pus_tc.py | 1 - pytmtc/pyproject.toml | 5 +- pytmtc/tests/__init__.py | 0 pytmtc/tests/test_cam.py | 27 +++++ pytmtc/tests/test_serde.py | 20 ---- src/handlers/camera.rs | 164 +++++++++++++--------------- src/main.rs | 4 +- 11 files changed, 130 insertions(+), 131 deletions(-) create mode 100644 pytmtc/opssat_tmtc/camera.py delete mode 100644 pytmtc/opssat_tmtc/camera_params.py create mode 100644 pytmtc/tests/__init__.py create mode 100644 pytmtc/tests/test_cam.py delete mode 100644 pytmtc/tests/test_serde.py diff --git a/Cargo.lock b/Cargo.lock index 6d5110c..a90bdfb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -629,7 +629,7 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "ops-sat-rs" -version = "0.1.0" +version = "0.1.1" dependencies = [ "chrono", "derive-new", diff --git a/pytmtc/opssat_tmtc/camera.py b/pytmtc/opssat_tmtc/camera.py new file mode 100644 index 0000000..e20d0b3 --- /dev/null +++ b/pytmtc/opssat_tmtc/camera.py @@ -0,0 +1,20 @@ +import enum +from pydantic import BaseModel + + +class ActionId(enum.IntEnum): + DEFAULT_SINGLE = 1 + BALANCED_SINGLE = 2 + DEFAULT_SINGLE_FLATSAT = 3 + BALANCED_SNGLE_FLATSAT = 4 + CUSTOM_PARAMS = 5 + + +class CameraParameters(BaseModel): + R: int + G: int + B: int + N: int + P: bool + E: int + W: int diff --git a/pytmtc/opssat_tmtc/camera_params.py b/pytmtc/opssat_tmtc/camera_params.py deleted file mode 100644 index 34ee2c4..0000000 --- a/pytmtc/opssat_tmtc/camera_params.py +++ /dev/null @@ -1,17 +0,0 @@ -import struct -from serde import Model, fields - -from opssat_tmtc.common import EXPERIMENT_APID, UniqueId, make_unique_id - - -class CameraParameters(Model): - R: fields.Int() - G: fields.Int() - B: fields.Int() - N: fields.Int() - P: fields.Bool() - E: fields.Int() - W: fields.Int() - - def serialize_for_uplink(self) -> bytearray: - return self.to_json().encode("utf-8") diff --git a/pytmtc/opssat_tmtc/common.py b/pytmtc/opssat_tmtc/common.py index f9bfa9d..47f6481 100644 --- a/pytmtc/opssat_tmtc/common.py +++ b/pytmtc/opssat_tmtc/common.py @@ -24,7 +24,6 @@ class UniqueId(enum.IntEnum): class EventSeverity(enum.IntEnum): - INFO = 0 LOW = 1 MEDIUM = 2 diff --git a/pytmtc/opssat_tmtc/pus_tc.py b/pytmtc/opssat_tmtc/pus_tc.py index d148fbd..123c083 100644 --- a/pytmtc/opssat_tmtc/pus_tc.py +++ b/pytmtc/opssat_tmtc/pus_tc.py @@ -37,7 +37,6 @@ def create_set_mode_cmd( def create_cmd_definition_tree() -> CmdTreeNode: - root_node = CmdTreeNode.root_node() hk_node = CmdTreeNode("hk", "Housekeeping Node", hide_children_for_print=True) diff --git a/pytmtc/pyproject.toml b/pytmtc/pyproject.toml index 81460b4..8a78f27 100644 --- a/pytmtc/pyproject.toml +++ b/pytmtc/pyproject.toml @@ -14,12 +14,15 @@ authors = [ ] dependencies = [ "tmtccmd==8.0.0rc.2", - "serde==0.9.0" + "serde==0.9.0", + "pydantic==2.7.1" ] [tool.setuptools.packages] find = {} +[tool.ruff] +extend-exclude = ["archive"] [tool.ruff.lint] ignore = ["E501"] [tool.ruff.lint.extend-per-file-ignores] diff --git a/pytmtc/tests/__init__.py b/pytmtc/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pytmtc/tests/test_cam.py b/pytmtc/tests/test_cam.py new file mode 100644 index 0000000..31b41df --- /dev/null +++ b/pytmtc/tests/test_cam.py @@ -0,0 +1,27 @@ +from unittest import TestCase +from opssat_tmtc.camera_params import CameraParameters + + +TEST_CAM_PARAMS = CameraParameters(R=8, G=8, B=8, N=1, P=True, E=200, W=1000) +EXPECTED_JSON = '{"R":8,"G":8,"B":8,"N":1,"P":true,"E":200,"W":1000}' + + +class TestCamInterface(TestCase): + def test_serialization_to_dict(self): + model = TEST_CAM_PARAMS.model_dump() + self.assertEqual(model["R"], 8) + self.assertEqual(model["G"], 8) + self.assertEqual(model["B"], 8) + self.assertEqual(model["N"], 1) + self.assertEqual(model["P"], True) + self.assertEqual(model["E"], 200) + self.assertEqual(model["W"], 1000) + + def test_serialization_to_json(self): + json = TEST_CAM_PARAMS.model_dump_json() + self.assertEqual(json, EXPECTED_JSON) + print(json) + + def test_deserialization(self): + model_deserialized = CameraParameters.model_validate_json(EXPECTED_JSON) + self.assertEqual(TEST_CAM_PARAMS, model_deserialized) diff --git a/pytmtc/tests/test_serde.py b/pytmtc/tests/test_serde.py deleted file mode 100644 index 010f979..0000000 --- a/pytmtc/tests/test_serde.py +++ /dev/null @@ -1,20 +0,0 @@ -import struct -from opssat_tmtc.camera_params import CameraParameters -from opssat_tmtc.common import make_unique_id, EXPERIMENT_APID - - -def test_serde_serialization(): - # Example serializatn - data = bytearray(make_unique_id(EXPERIMENT_APID)) - params = CameraParameters(8, 8, 8, 1, True, 200, 1000) - serialized = params.to_json().encode("utf-8") - byte_string = bytearray(struct.pack("!{}s".format(len(serialized)), serialized)) - print(byte_string) - print(params.serialize_for_uplink()) - data.extend(params.serialize_for_uplink()) - print(data) - - # Example deserialization - data = '{"R": 100, "G": 150, "B": 200, "N": 3, "P": true, "E": 10, "W": 20}' - deserialized_params = CameraParameters.from_json(data) - print(deserialized_params) diff --git a/src/handlers/camera.rs b/src/handlers/camera.rs index 0897f7b..2fd6601 100644 --- a/src/handlers/camera.rs +++ b/src/handlers/camera.rs @@ -1,4 +1,3 @@ -use crate::pus::action::send_data_reply; /// Device handler implementation for the IMS-100 Imager used on the OPS-SAT mission. /// /// from the [OPSSAT Experimenter Wiki](https://opssat1.esoc.esa.int/projects/experimenter-information/wiki/Camera_Introduction): @@ -25,9 +24,11 @@ use crate::pus::action::send_data_reply; /// v Y /// /// see also https://opssat1.esoc.esa.int/dmsf/files/6/view +use crate::pus::action::send_data_reply; use crate::requests::CompositeRequest; use derive_new::new; use log::{debug, info}; +use num_enum::TryFromPrimitive; use ops_sat_rs::TimeStampHelper; use satrs::action::{ActionRequest, ActionRequestVariant}; use satrs::hk::HkRequest; @@ -88,8 +89,9 @@ const BALANCED_SINGLE_FLATSAT_CAM_PARAMS: CameraPictureParameters = CameraPictur // TODO ls -l via cfdp // TODO howto downlink -#[derive(Debug)] -pub enum CameraActionId { +#[derive(Debug, TryFromPrimitive)] +#[repr(u32)] +pub enum ActionId { DefaultSingle = 1, BalancedSingle = 2, DefaultSingleFlatSat = 3, @@ -97,31 +99,6 @@ pub enum CameraActionId { CustomParameters = 5, } -impl TryFrom for CameraActionId { - type Error = (); - - fn try_from(value: u32) -> Result { - match value { - value if value == CameraActionId::DefaultSingle as u32 => { - Ok(CameraActionId::DefaultSingle) - } - value if value == CameraActionId::BalancedSingle as u32 => { - Ok(CameraActionId::BalancedSingle) - } - value if value == CameraActionId::DefaultSingleFlatSat as u32 => { - Ok(CameraActionId::DefaultSingleFlatSat) - } - value if value == CameraActionId::BalancedSingleFlatSat as u32 => { - Ok(CameraActionId::BalancedSingleFlatSat) - } - value if value == CameraActionId::CustomParameters as u32 => { - Ok(CameraActionId::CustomParameters) - } - _ => Err(()), - } - } -} - // TODO what happens if limits are exceded #[allow(non_snake_case)] #[derive(Debug, Serialize, Deserialize, new)] @@ -189,11 +166,9 @@ impl fmt::Display for CameraError { #[allow(dead_code)] #[derive(Debug)] -pub struct IMS100BatchHandler { +pub struct Ims100BatchHandler { id: UniqueApidTargetId, - // mode_interface: MpscModeLeafInterface, composite_request_rx: mpsc::Receiver>, - // hk_reply_sender: mpsc::Sender>, tm_tx: mpsc::Sender, action_reply_tx: mpsc::Sender>, stamp_helper: TimeStampHelper, @@ -201,7 +176,7 @@ pub struct IMS100BatchHandler { #[allow(non_snake_case)] #[allow(dead_code)] -impl IMS100BatchHandler { +impl Ims100BatchHandler { pub fn new( id: UniqueApidTargetId, composite_request_rx: mpsc::Receiver>, @@ -222,7 +197,6 @@ impl IMS100BatchHandler { self.stamp_helper.update_from_now(); // Handle requests. self.handle_composite_requests(); - // self.handle_mode_requests(); } pub fn handle_composite_requests(&mut self) { @@ -264,32 +238,31 @@ impl IMS100BatchHandler { requestor_info: &MessageMetadata, action_request: &ActionRequest, ) -> Result<(), CameraError> { - let param = - match CameraActionId::try_from(action_request.action_id).expect("Invalid action id") { - CameraActionId::DefaultSingle => DEFAULT_SINGLE_CAM_PARAMS, - CameraActionId::BalancedSingle => BALANCED_SINGLE_CAM_PARAMS, - CameraActionId::DefaultSingleFlatSat => DEFAULT_SINGLE_FLATSAT_CAM_PARAMS, - CameraActionId::BalancedSingleFlatSat => BALANCED_SINGLE_FLATSAT_CAM_PARAMS, - CameraActionId::CustomParameters => match &action_request.variant { - ActionRequestVariant::NoData => return Err(CameraError::NoDataSent), - ActionRequestVariant::StoreData(_) => { - // let param = serde_json::from_slice() - // TODO implement non dynamic version - return Err(CameraError::VariantNotImplemented); - } - ActionRequestVariant::VecData(data) => { - let param: serde_json::Result = - serde_json::from_slice(data.as_slice()); - match param { - Ok(param) => param, - Err(_) => { - return Err(CameraError::DeserializeError); - } + let param = match ActionId::try_from(action_request.action_id).expect("Invalid action id") { + ActionId::DefaultSingle => DEFAULT_SINGLE_CAM_PARAMS, + ActionId::BalancedSingle => BALANCED_SINGLE_CAM_PARAMS, + ActionId::DefaultSingleFlatSat => DEFAULT_SINGLE_FLATSAT_CAM_PARAMS, + ActionId::BalancedSingleFlatSat => BALANCED_SINGLE_FLATSAT_CAM_PARAMS, + ActionId::CustomParameters => match &action_request.variant { + ActionRequestVariant::NoData => return Err(CameraError::NoDataSent), + ActionRequestVariant::StoreData(_) => { + // let param = serde_json::from_slice() + // TODO implement non dynamic version + return Err(CameraError::VariantNotImplemented); + } + ActionRequestVariant::VecData(data) => { + let param: serde_json::Result = + serde_json::from_slice(data.as_slice()); + match param { + Ok(param) => param, + Err(_) => { + return Err(CameraError::DeserializeError); } } - _ => return Err(CameraError::VariantNotImplemented), - }, - }; + } + _ => return Err(CameraError::VariantNotImplemented), + }, + }; let output = self.take_picture(param)?; info!("Sending action reply!"); send_data_reply(self.id, output.stdout, &self.stamp_helper, &self.tm_tx)?; @@ -388,8 +361,7 @@ impl IMS100BatchHandler { #[cfg(test)] mod tests { use crate::handlers::camera::{ - CameraActionId, CameraPictureParameters, IMS100BatchHandler, - DEFAULT_SINGLE_FLATSAT_CAM_PARAMS, + ActionId, CameraPictureParameters, Ims100BatchHandler, DEFAULT_SINGLE_FLATSAT_CAM_PARAMS, }; use crate::requests::CompositeRequest; use ops_sat_rs::config::components::CAMERA_HANDLER; @@ -399,32 +371,47 @@ mod tests { use satrs::request::{GenericMessage, MessageMetadata}; use satrs::tmtc::PacketAsVec; use std::sync::mpsc; - use std::sync::mpsc::{Receiver, Sender}; - fn create_handler() -> ( - IMS100BatchHandler, - Sender>, - Receiver, - Receiver>, - ) { - let (composite_request_tx, composite_request_rx) = mpsc::channel(); - let (tm_tx, tm_rx) = mpsc::channel(); - let (action_reply_tx, action_reply_rx) = mpsc::channel(); - let time_helper = TimeStampHelper::default(); - let cam_handler: IMS100BatchHandler = IMS100BatchHandler::new( - CAMERA_HANDLER, - composite_request_rx, - tm_tx, - action_reply_tx, - time_helper, - ); - (cam_handler, composite_request_tx, tm_rx, action_reply_rx) + struct Ims1000Testbench { + pub handler: Ims100BatchHandler, + pub composite_req_tx: mpsc::Sender>, + pub tm_receiver: mpsc::Receiver, + pub action_reply_rx: mpsc::Receiver>, + } + + impl Default for Ims1000Testbench { + fn default() -> Self { + let (composite_request_tx, composite_request_rx) = mpsc::channel(); + let (tm_tx, tm_rx) = mpsc::channel(); + let (action_reply_tx, action_reply_rx) = mpsc::channel(); + let time_helper = TimeStampHelper::default(); + let cam_handler: Ims100BatchHandler = Ims100BatchHandler::new( + CAMERA_HANDLER, + composite_request_rx, + tm_tx, + action_reply_tx, + time_helper, + ); + Ims1000Testbench { + handler: Ims100BatchHandler::new( + CAMERA_HANDLER, + composite_request_rx, + tm_tx, + action_reply_tx, + time_helper, + ), + composite_req_tx: composite_request_tx, + tm_receiver: tm_rx, + action_reply_rx, + } + } } #[test] fn command_line_execution() { - let (mut cam_handler, req_tx, tm_rx, action_reply_rx) = create_handler(); - cam_handler + let mut testbench = Ims1000Testbench::default(); + testbench + .handler .take_picture(DEFAULT_SINGLE_FLATSAT_CAM_PARAMS) .unwrap(); } @@ -439,33 +426,34 @@ mod tests { #[test] fn test_action_req() { - let (mut cam_handler, req_tx, tm_rx, action_reply_rx) = create_handler(); - + let mut testbench = Ims1000Testbench::default(); let data = serde_json::to_string(&DEFAULT_SINGLE_FLATSAT_CAM_PARAMS).unwrap(); let req = ActionRequest::new( - CameraActionId::CustomParameters as u32, + ActionId::CustomParameters as u32, ActionRequestVariant::VecData(data.as_bytes().to_vec()), ); - cam_handler + testbench + .handler .handle_action_request(&MessageMetadata::new(1, 1), &req) .unwrap(); } #[test] fn test_action_req_channel() { - let (mut cam_handler, req_tx, tm_rx, action_reply_rx) = create_handler(); + let mut testbench = Ims1000Testbench::default(); let data = serde_json::to_string(&DEFAULT_SINGLE_FLATSAT_CAM_PARAMS).unwrap(); let req = ActionRequest::new( - CameraActionId::CustomParameters as u32, + ActionId::CustomParameters as u32, ActionRequestVariant::VecData(data.as_bytes().to_vec()), ); let req = CompositeRequest::Action(req); - req_tx + testbench + .composite_req_tx .send(GenericMessage::new(MessageMetadata::new(1, 1), req)) .unwrap(); - cam_handler.periodic_operation(); + testbench.handler.periodic_operation(); } } diff --git a/src/main.rs b/src/main.rs index 5184c6d..46c540a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,7 +33,7 @@ use crate::{ PusTcDistributor, PusTcMpscRouter, }, }; -use crate::{handlers::camera::IMS100BatchHandler, pus::event::create_event_service}; +use crate::{handlers::camera::Ims100BatchHandler, pus::event::create_event_service}; use crate::{ interface::tcp_server::{SyncTcpTmSource, TcpTask}, interface::udp_server::{DynamicUdpTmHandler, UdpTmtcServer}, @@ -211,7 +211,7 @@ fn main() { .expect("creating TCP SPP client failed"); let timestamp_helper = TimeStampHelper::default(); - let mut camera_handler: IMS100BatchHandler = IMS100BatchHandler::new( + let mut camera_handler: Ims100BatchHandler = Ims100BatchHandler::new( CAMERA_HANDLER, camera_composite_rx, tm_funnel_tx.clone(), -- 2.43.0 From b4bf834c394741922b295b2d10634f1848193243 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sun, 28 Apr 2024 12:45:01 +0200 Subject: [PATCH 2/3] minor tweak --- src/handlers/camera.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/handlers/camera.rs b/src/handlers/camera.rs index 2fd6601..a3c3481 100644 --- a/src/handlers/camera.rs +++ b/src/handlers/camera.rs @@ -164,7 +164,6 @@ impl fmt::Display for CameraError { } } -#[allow(dead_code)] #[derive(Debug)] pub struct Ims100BatchHandler { id: UniqueApidTargetId, @@ -175,7 +174,6 @@ pub struct Ims100BatchHandler { } #[allow(non_snake_case)] -#[allow(dead_code)] impl Ims100BatchHandler { pub fn new( id: UniqueApidTargetId, @@ -306,6 +304,7 @@ impl Ims100BatchHandler { Ok(output) } + #[allow(dead_code)] pub fn list_current_images(&self) -> Result, CameraError> { let output = Command::new("ls").arg("-l").arg("*.png").output()?; @@ -319,6 +318,7 @@ impl Ims100BatchHandler { } #[allow(clippy::too_many_arguments)] + #[allow(dead_code)] pub fn take_picture_from_str( &mut self, R: &str, -- 2.43.0 From 2c34f46eca860192f17507e197f5e559f59b4150 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sun, 28 Apr 2024 12:47:08 +0200 Subject: [PATCH 3/3] update dependencies --- pytmtc/pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pytmtc/pyproject.toml b/pytmtc/pyproject.toml index 8a78f27..ec8d152 100644 --- a/pytmtc/pyproject.toml +++ b/pytmtc/pyproject.toml @@ -14,7 +14,6 @@ authors = [ ] dependencies = [ "tmtccmd==8.0.0rc.2", - "serde==0.9.0", "pydantic==2.7.1" ] -- 2.43.0