From abf4e5c4fa0f6f1fd8d6a58c8510989bea68a9c6 Mon Sep 17 00:00:00 2001 From: Alexander Clausen Date: Wed, 17 May 2023 14:53:12 +0200 Subject: [PATCH] Misc updates - microsecond timestamps for logging - expose more detector info/config/layout to Python - expose ServalClient to Python - add first frame meta to AcquisitionStart message - `wait_for_arm` returns `PendingAcquisition` with detector config and first frame meta info - `serval-client`: - add more structs for detector info/config/layout - error handling: don't panic --- .../examples/mpx3_example_03_acquisition.py | 7 +- libertem_asi_mpx3/examples/simple.py | 5 +- libertem_asi_mpx3/src/common.rs | 12 +- libertem_asi_mpx3/src/main_py.rs | 115 ++++++++- libertem_asi_mpx3/src/receiver.rs | 22 +- serval-client/src/lib.rs | 235 +++++++++++++++++- 6 files changed, 370 insertions(+), 26 deletions(-) diff --git a/libertem_asi_mpx3/examples/mpx3_example_03_acquisition.py b/libertem_asi_mpx3/examples/mpx3_example_03_acquisition.py index e80bce8d..0219bc60 100644 --- a/libertem_asi_mpx3/examples/mpx3_example_03_acquisition.py +++ b/libertem_asi_mpx3/examples/mpx3_example_03_acquisition.py @@ -95,8 +95,8 @@ def init_acquisition(serverurl, detector_config): # 0.1 second = 10fps # 0.0005 second = 2000fps - dwell_time = 0.0001 - # dwell_time = 0.0005 + # dwell_time = 0.0001 + dwell_time = 0.0005 # Sets the trigger period (time between triggers) in seconds. detector_config["TriggerPeriod"] = dwell_time @@ -193,6 +193,9 @@ def acquisition_test(serverurl): data = response.text print('Selected destination : ' + data) + print(f"Layout: {json.dumps(get_request(url=serverurl + '/detector/layout').json(), indent=2)}") + print(f"Info: {json.dumps(get_request(url=serverurl + '/detector/info').json(), indent=2)}") + # Running acquisition process acquisition_test(serverurl) diff --git a/libertem_asi_mpx3/examples/simple.py b/libertem_asi_mpx3/examples/simple.py index f6c108cd..7f39bd89 100644 --- a/libertem_asi_mpx3/examples/simple.py +++ b/libertem_asi_mpx3/examples/simple.py @@ -14,6 +14,8 @@ conn.start_passive() +cam_client = None + try: while True: config = None @@ -54,4 +56,5 @@ tq.close() finally: conn.close() # clean up background thread etc. - cam_client.close() + if cam_client is not None: + cam_client.close() diff --git a/libertem_asi_mpx3/src/common.rs b/libertem_asi_mpx3/src/common.rs index 3726fb39..f02593f0 100644 --- a/libertem_asi_mpx3/src/common.rs +++ b/libertem_asi_mpx3/src/common.rs @@ -1,4 +1,4 @@ -use pyo3::pyclass; +use pyo3::{pyclass, pymethods}; use serde::{Deserialize, Serialize}; #[derive(PartialEq, Eq, Clone, Serialize, Deserialize, Debug)] @@ -25,6 +25,16 @@ impl DType { } } +#[pymethods] +impl DType { + fn as_string(&self) -> String { + match self { + Self::U8 => "uint8".to_string(), + Self::U16 => "uint16".to_string(), + } + } +} + #[derive(PartialEq, Eq, Clone, Serialize, Deserialize, Debug)] pub struct FrameMeta { pub sequence: u64, diff --git a/libertem_asi_mpx3/src/main_py.rs b/libertem_asi_mpx3/src/main_py.rs index bf27fec3..3f50d5f3 100644 --- a/libertem_asi_mpx3/src/main_py.rs +++ b/libertem_asi_mpx3/src/main_py.rs @@ -8,7 +8,7 @@ use std::{ use crate::{ cam_client::CamClient, - common::DType, + common::{DType, FrameMeta}, exceptions::{ConnectionError, TimeoutError}, frame_stack::FrameStackHandle, receiver::{ReceiverStatus, ResultMsg, ServalReceiver}, @@ -20,7 +20,7 @@ use pyo3::{ exceptions::{self, PyRuntimeError}, prelude::*, }; -use serval_client::DetectorConfig; +use serval_client::{DetectorConfig, DetectorInfo, DetectorLayout, ServalClient}; use stats::Stats; #[pymodule] @@ -33,6 +33,8 @@ fn libertem_asi_mpx3(py: Python, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add("TimeoutError", py.get_type::())?; @@ -41,7 +43,9 @@ fn libertem_asi_mpx3(py: Python, m: &PyModule) -> PyResult<()> { let env = env_logger::Env::default() .filter_or("LIBERTEM_ASI_LOG_LEVEL", "error") .write_style_or("LIBERTEM_ASI_LOG_STYLE", "always"); - env_logger::init_from_env(env); + env_logger::Builder::from_env(env) + .format_timestamp_micros() + .init(); Ok(()) } @@ -69,6 +73,71 @@ impl PyDetectorConfig { } } +#[derive(Debug)] +#[pyclass(name = "DetectorInfo")] +struct PyDetectorInfo { + info: DetectorInfo, +} + +#[pymethods] +impl PyDetectorInfo { + fn __repr__(&self) -> String { + format!("{:?}", self) + } + + fn get_pix_count(&self) -> u64 { + self.info.pix_count + } +} + +#[derive(Debug)] +#[pyclass(name = "DetectorLayout")] +struct PyDetectorLayout { + info: DetectorLayout, +} + +#[pymethods] +impl PyDetectorLayout { + fn __repr__(&self) -> String { + format!("{:?}", self) + } +} + +#[pyclass(name = "ServalAPIClient")] +struct PyServalClient { + client: ServalClient, + base_url: String, +} + +#[pymethods] +impl PyServalClient { + #[new] + fn new(base_url: &str) -> Self { + Self { + client: ServalClient::new(base_url), + base_url: base_url.to_string(), + } + } + + fn __repr__(&self) -> String { + format!("", self.base_url) + } + + fn get_detector_config(&self) -> PyResult { + self.client + .get_detector_config() + .map_err(|e| PyRuntimeError::new_err(e.to_string())) + .map(|value| PyDetectorConfig { config: value }) + } + + fn get_detector_info(&self) -> PyResult { + self.client + .get_detector_info() + .map_err(|e| PyRuntimeError::new_err(e.to_string())) + .map(|value| PyDetectorInfo { info: value }) + } +} + struct FrameChunkedIterator<'a, 'b, 'c, 'd> { receiver: &'a mut ServalReceiver, shm: &'b mut SharedSlabAllocator, @@ -139,9 +208,12 @@ impl<'a, 'b, 'c, 'd> FrameChunkedIterator<'a, 'b, 'c, 'd> { continue; } Some(ResultMsg::ParseError { msg }) => { - todo!(); + return Err(exceptions::PyRuntimeError::new_err(msg)) } - Some(ResultMsg::AcquisitionStart { detector_config }) => { + Some(ResultMsg::AcquisitionStart { + detector_config: _, + first_frame_meta: _, + }) => { // FIXME: in case of "passive" mode, we should actually not hit this, // as the "outer" structure (`ServalConnection`) handles it? continue; @@ -196,6 +268,29 @@ impl ServalConnection { } } +#[pyclass] +struct PendingAcquisition { + config: DetectorConfig, + first_frame_meta: FrameMeta, +} + +#[pymethods] +impl PendingAcquisition { + fn get_detector_config(&self) -> PyDetectorConfig { + PyDetectorConfig { + config: self.config.clone(), + } + } + + fn get_frame_width(&self) -> u16 { + self.first_frame_meta.width + } + + fn get_frame_height(&self) -> u16 { + self.first_frame_meta.height + } +} + #[pymethods] impl ServalConnection { #[new] @@ -238,7 +333,7 @@ impl ServalConnection { /// Wait until the detector is armed, or until the timeout expires (in seconds) /// Returns `None` in case of timeout, the detector config otherwise. /// This method drops the GIL to allow concurrent Python threads. - fn wait_for_arm(&mut self, timeout: f32, py: Python) -> PyResult> { + fn wait_for_arm(&mut self, timeout: f32, py: Python) -> PyResult> { let timeout = Duration::from_secs_f32(timeout); let deadline = Instant::now() + timeout; let step = Duration::from_millis(100); @@ -253,9 +348,13 @@ impl ServalConnection { }); match res { - Some(ResultMsg::AcquisitionStart { detector_config }) => { - return Ok(Some(PyDetectorConfig { + Some(ResultMsg::AcquisitionStart { + detector_config, + first_frame_meta, + }) => { + return Ok(Some(PendingAcquisition { config: detector_config, + first_frame_meta, })) } msg @ Some(ResultMsg::End { .. }) | msg @ Some(ResultMsg::FrameStack { .. }) => { diff --git a/libertem_asi_mpx3/src/receiver.rs b/libertem_asi_mpx3/src/receiver.rs index 1c59b9c4..ea66408a 100644 --- a/libertem_asi_mpx3/src/receiver.rs +++ b/libertem_asi_mpx3/src/receiver.rs @@ -11,7 +11,7 @@ use std::{ use crossbeam_channel::{unbounded, Receiver, RecvTimeoutError, SendError, Sender, TryRecvError}; use ipc_test::{SHMHandle, SharedSlabAllocator}; use log::{debug, error, info, trace, warn}; -use serval_client::{DetectorConfig, ServalClient}; +use serval_client::{DetectorConfig, ServalClient, ServalError}; use crate::{ common::{DType, FrameMeta}, @@ -31,6 +31,7 @@ pub enum ResultMsg { AcquisitionStart { detector_config: DetectorConfig, + first_frame_meta: FrameMeta, }, /// A stack of frames, part of an acquisition @@ -282,6 +283,7 @@ enum AcquisitionError { ConfigurationError { msg: String }, ParseError { msg: String }, ConnectionError { msg: String }, + APIError { msg: String }, } impl Display for AcquisitionError { @@ -308,6 +310,9 @@ impl Display for AcquisitionError { AcquisitionError::ConnectionError { msg } => { write!(f, "connection error: {msg}") } + AcquisitionError::APIError { msg } => { + write!(f, "serval HTTP API error: {msg}") + } } } } @@ -326,6 +331,14 @@ impl From> for AcquisitionError { } } +impl From for AcquisitionError { + fn from(value: ServalError) -> Self { + Self::APIError { + msg: value.to_string(), + } + } +} + /// With a running acquisition, check for control messages; /// especially convert `ControlMsg::StopThread` to `AcquisitionError::Cancelled`. fn check_for_control(control_channel: &Receiver) -> Result<(), AcquisitionError> { @@ -374,7 +387,7 @@ fn passive_acquisition( }; // block until we get the first frame: - let _first_frame = match peek_header(&mut stream) { + let first_frame_meta = match peek_header(&mut stream) { Ok(m) => m, Err(AcquisitionError::ConnectionError { msg }) => { warn!("connection error while peeking first frame: {msg}; reconnecting"); @@ -385,12 +398,13 @@ fn passive_acquisition( // then, we should be able to reliably get the detector config // (we assume once data arrives, the config is immutable) - let detector_config = client.get_detector_config(); + let detector_config = client.get_detector_config()?; acquisition( control_channel, from_thread_s, &detector_config, + &first_frame_meta, &mut stream, frame_stack_size, shm, @@ -408,6 +422,7 @@ fn acquisition( to_thread_r: &Receiver, from_thread_s: &Sender, detector_config: &DetectorConfig, + first_frame_meta: &FrameMeta, stream: &mut TcpStream, frame_stack_size: usize, shm: &mut SharedSlabAllocator, @@ -417,6 +432,7 @@ fn acquisition( from_thread_s.send(ResultMsg::AcquisitionStart { detector_config: detector_config.clone(), + first_frame_meta: first_frame_meta.clone(), })?; debug!("acquisition starting"); diff --git a/serval-client/src/lib.rs b/serval-client/src/lib.rs index 5420517f..006b764c 100644 --- a/serval-client/src/lib.rs +++ b/serval-client/src/lib.rs @@ -1,6 +1,49 @@ -use serde::{Deserialize, Serialize}; +use std::fmt::Display; + +use serde::{de::DeserializeOwned, Deserialize, Serialize}; use url::Url; +#[derive(Debug)] +pub enum ServalError { + RequestFailed { msg: String }, + SerializationError { msg: String }, + URLError { msg: String }, +} + +impl From for ServalError { + fn from(value: url::ParseError) -> Self { + Self::URLError { + msg: value.to_string(), + } + } +} + +impl From for ServalError { + fn from(value: serde_json::Error) -> Self { + Self::SerializationError { + msg: value.to_string(), + } + } +} + +impl From for ServalError { + fn from(value: reqwest::Error) -> Self { + Self::RequestFailed { + msg: value.to_string(), + } + } +} + +impl Display for ServalError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ServalError::RequestFailed { msg } => write!(f, "request failed: {msg}"), + ServalError::SerializationError { msg } => write!(f, "serialization error: {msg}"), + ServalError::URLError { msg } => write!(f, "URL error: {msg}"), + } + } +} + #[derive(PartialEq, Eq, Clone, Serialize, Deserialize, Debug)] pub enum TriggerMode { /// Start: Positive Edge External Trigger Input, Stop: Negative Edge @@ -55,6 +98,165 @@ pub struct DetectorConfig { pub trigger_mode: TriggerMode, } +#[derive(PartialEq, Clone, Serialize, Deserialize, Debug)] +pub struct DetectorBoard { + #[serde(rename = "ChipboardId")] + pub chipboard_id: String, + + #[serde(rename = "IpAddress")] + pub ip_address: String, + + #[serde(rename = "FirmwareVersion")] + pub firmare_version: String, + + #[serde(rename = "Chips")] + pub chips: Vec, +} + +#[derive(PartialEq, Clone, Serialize, Deserialize, Debug)] +pub struct DetectorChip { + #[serde(rename = "Index")] + pub index: usize, + + #[serde(rename = "Id")] + pub id: u64, + + #[serde(rename = "Name")] + pub name: String, +} + +#[derive(PartialEq, Clone, Serialize, Deserialize, Debug)] +pub struct DetectorInfo { + #[serde(rename = "IfaceName")] + pub iface_name: String, + + #[serde(rename = "SW_version")] + pub sw_version: String, + + #[serde(rename = "FW_version")] + pub fw_version: String, + + #[serde(rename = "PixCount")] + pub pix_count: u64, + + #[serde(rename = "RowLen")] + pub row_len: u16, + + #[serde(rename = "NumberOfChips")] + pub number_of_chips: u16, + + #[serde(rename = "NumberOfRows")] + pub number_of_rows: u16, + + #[serde(rename = "MpxType")] + pub mpx_type: u64, + + #[serde(rename = "Boards")] + pub boards: Vec, + + #[serde(rename = "SuppAcqModes")] + pub supp_acq_modes: u64, + + #[serde(rename = "ClockReadout")] + pub clock_readout: f32, + + #[serde(rename = "MaxPulseCount")] + pub max_pulse_count: u64, + + #[serde(rename = "MaxPulseHeight")] + pub max_pulse_height: f32, + + #[serde(rename = "MaxPulsePeriod")] + pub max_pulse_period: f32, + + #[serde(rename = "TimerMaxVal")] + pub timer_max_val: f32, + + #[serde(rename = "TimerMinVal")] + pub timer_min_val: f32, + + #[serde(rename = "TimerStep")] + pub timer_step: f32, +} + +#[derive(PartialEq, Clone, Serialize, Deserialize, Debug)] +pub enum DetectorOrientation { + #[serde(rename = "UP")] + Up, + + #[serde(rename = "RIGHT")] + Right, + + #[serde(rename = "DOWN")] + Down, + + #[serde(rename = "LEFT")] + Left, + + #[serde(rename = "UP_MIRRORED")] + UpMirrored, + + #[serde(rename = "RIGHT_MIRRORED")] + RightMirrored, + + #[serde(rename = "DOWN_MIRRORED")] + DownMirrored, + + #[serde(rename = "LEFT_MIRRORED")] + LeftMirrored, +} + +#[derive(PartialEq, Clone, Serialize, Deserialize, Debug)] +pub enum ChipOrientation { + LtRBtT, + RtLBtT, + LtRTtB, + RtLTtB, + BtTLtR, + TtBLtR, + BtTRtL, + TtBRtL, +} + +#[derive(PartialEq, Clone, Serialize, Deserialize, Debug)] +pub struct DetectorLayout { + #[serde(rename = "Orientation")] + orientation: DetectorOrientation, + + #[serde(rename = "Original")] + original: DetectorLayoutInner, + + #[serde(rename = "Rotated")] + rotated: DetectorLayoutInner, +} + +#[derive(PartialEq, Clone, Serialize, Deserialize, Debug)] +pub struct DetectorLayoutChip { + #[serde(rename = "Chip")] + chip: u16, + + #[serde(rename = "X")] + x: u16, + + #[serde(rename = "Y")] + y: u16, + + #[serde(rename = "Orientation")] + orientation: ChipOrientation, +} + +#[derive(PartialEq, Clone, Serialize, Deserialize, Debug)] +pub struct DetectorLayoutInner { + #[serde(rename = "Width")] + width: u16, + + #[serde(rename = "Height")] + height: u16, + + #[serde(rename = "Chips")] + chips: Vec, +} + pub struct ServalClient { base_url: Url, } @@ -66,19 +268,30 @@ impl ServalClient { } } - pub fn set_detector_config(&self) { - todo!(); + fn get_request(&self, path: &str) -> Result + where + T: DeserializeOwned, + { + let url = self.base_url.join(path)?; + let resp = reqwest::blocking::get(url)?; + let resp_text = resp.text()?; + let config: T = serde_json::from_str(&resp_text)?; + Ok(config) + } + + pub fn get_detector_config(&self) -> Result { + self.get_request("/detector/config/") + } + + pub fn get_detector_info(&self) -> Result { + self.get_request("/detector/info/") } - pub fn get_detector_config(&self) -> DetectorConfig { - let url = self.base_url.join("/detector/config/").unwrap(); - let resp = reqwest::blocking::get(url).unwrap(); - let resp_text = resp.text().unwrap(); - let config: DetectorConfig = serde_json::from_str(&resp_text).unwrap(); - config + pub fn get_detector_layout(&self) -> Result { + self.get_request("/detector/layout/") } - pub fn start_acquisition(&self) { + pub fn start_measurement(&self) { todo!(); } } @@ -91,7 +304,7 @@ mod test { fn test_stuff() { let client = ServalClient::new("http://localhost:8080"); - println!("{:?}", client.get_detector_config()); + println!("{:?}", client.get_detector_config().unwrap()); panic!("at the disco?"); } }