From b8331a6f2a2b08b9fee4c162d3917980b73aefa6 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 19 Mar 2026 04:53:54 +0000 Subject: [PATCH 1/2] Add typed error propagation and initial test coverage Introduce AsiCameraError and AsiEfwError enums (via thiserror) so that SDK error codes are now propagated as Result instead of being silently logged and discarded. All libasi wrapper functions have been updated to return Result, and callers in the CCD and EFW daemons handle errors appropriately (propagate in constructors, log-and-continue in polling/runtime paths). Also adds 22 unit tests across three modules: - libasi/src/camera.rs: all 19 ASI error code mappings - libasi/src/efw.rs: all 10 EFW error code mappings including the EFW_ERROR_MOVING sentinel used by check_wheel_is_moving() - src/lib.rs: string utility edge cases (null terminators, invalid UTF-8, empty arrays) and a new parse_device_topic() utility covering the previously inline and untested MQTT topic parsing logic - src/bin/efw/efw.rs: JSON serialization shape (verifies #[serde(skip)] fields do not leak into published state) https://claude.ai/code/session_01Pa1qQU4xGtZxgBfZQS4eDL --- libasi/Cargo.toml | 3 +- libasi/src/camera.rs | 255 ++++++++++++++++++++++++++++++------------- libasi/src/efw.rs | 183 ++++++++++++++++++------------- src/bin/ccd/ccd.rs | 119 ++++++++++---------- src/bin/ccd/main.rs | 15 +-- src/bin/efw/efw.rs | 80 ++++++++++---- src/bin/efw/main.rs | 17 ++- src/bin/test/main.rs | 38 +++---- src/lib.rs | 127 +++++++++++++++++++++ 9 files changed, 576 insertions(+), 261 deletions(-) diff --git a/libasi/Cargo.toml b/libasi/Cargo.toml index 519a241..1170109 100644 --- a/libasi/Cargo.toml +++ b/libasi/Cargo.toml @@ -7,4 +7,5 @@ edition = "2024" [dependencies] libasi-sys = { version = "0.1.0", path = "../libasi-sys" } -log = "0.4" \ No newline at end of file +log = "0.4" +thiserror = "1" \ No newline at end of file diff --git a/libasi/src/camera.rs b/libasi/src/camera.rs index 977923b..78def47 100644 --- a/libasi/src/camera.rs +++ b/libasi/src/camera.rs @@ -1,5 +1,4 @@ pub use libasi_sys::camera::*; -use log::error; pub type AsiCameraInfo = _ASI_CAMERA_INFO; pub type AsiControlCaps = _ASI_CONTROL_CAPS; @@ -13,100 +12,154 @@ pub struct ROIFormat { pub img_type: i32, } -fn check_error_code(code: i32) { +#[derive(Debug, thiserror::Error, PartialEq)] +pub enum AsiCameraError { + #[error("invalid camera index")] + InvalidIndex, + #[error("invalid camera id")] + InvalidId, + #[error("invalid control type")] + InvalidControlType, + #[error("camera not open")] + CameraClosed, + #[error("camera removed")] + CameraRemoved, + #[error("invalid path")] + InvalidPath, + #[error("invalid file format")] + InvalidFileFormat, + #[error("invalid size")] + InvalidSize, + #[error("invalid image type")] + InvalidImgType, + #[error("start position out of boundary")] + OutOfBoundary, + #[error("communication timeout")] + Timeout, + #[error("invalid sequence — stop capture first")] + InvalidSequence, + #[error("buffer too small")] + BufferTooSmall, + #[error("video mode active")] + VideoModeActive, + #[error("exposure in progress")] + ExposureInProgress, + #[error("general error")] + GeneralError, + #[error("invalid mode")] + InvalidMode, + #[error("end sentinel")] + End, + #[error("unknown error code: {0}")] + Unknown(i32), +} + +fn check_error_code(code: i32) -> Result<(), AsiCameraError> { match code { - // Success - 0 => (), - // No camera connected or index value out of boundary - 1 => error!("ASI_ERROR_INVALID_INDEX"), - 2 => error!("ASI_ERROR_INVALID_ID"), - 3 => error!("ASI_ERROR_INVALID_CONTROL_TYPE"), - // Camera didn't open - 4 => error!("ASI_ERROR_CAMERA_CLOSED"), - // Failed to find the camera, maybe the camera has been removed - 5 => error!("ASI_ERROR_CAMERA_REMOVED"), - // Cannot find the path of the file - 6 => error!("ASI_ERROR_INVALID_PATH"), - 7 => error!("ASI_ERROR_INVALID_FILEFORMAT"), - // Wrong video format size - 8 => error!("ASI_ERROR_INVALID_SIZE"), - 9 => error!("ASI_ERROR_INVALID_IMGTYPE"), //unsupported image formate - 10 => error!("ASI_ERROR_OUTOF_BOUNDARY"), //the startpos is out of boundary - // Communication timeout - 11 => error!("ASI_ERROR_TIMEOUT"), - 12 => error!("ASI_ERROR_INVALID_SEQUENCE"), //stop capture first! - 13 => error!("ASI_ERROR_BUFFER_TOO_SMALL"), //buffer size is not big enough - 14 => error!("ASI_ERROR_VIDEO_MODE_ACTIVE"), - 15 => error!("ASI_ERROR_EXPOSURE_IN_PROGRESS"), - 16 => error!("ASI_ERROR_GENERAL_ERROR"), //general error, eg: value is out of valid range - 17 => error!("ASI_ERROR_INVALID_MODE"), //the current mode is wrong - 18 => error!("ASI_ERROR_END"), - e => error!("unknown error {}", e), + 0 => Ok(()), + 1 => Err(AsiCameraError::InvalidIndex), + 2 => Err(AsiCameraError::InvalidId), + 3 => Err(AsiCameraError::InvalidControlType), + 4 => Err(AsiCameraError::CameraClosed), + 5 => Err(AsiCameraError::CameraRemoved), + 6 => Err(AsiCameraError::InvalidPath), + 7 => Err(AsiCameraError::InvalidFileFormat), + 8 => Err(AsiCameraError::InvalidSize), + 9 => Err(AsiCameraError::InvalidImgType), + 10 => Err(AsiCameraError::OutOfBoundary), + 11 => Err(AsiCameraError::Timeout), + 12 => Err(AsiCameraError::InvalidSequence), + 13 => Err(AsiCameraError::BufferTooSmall), + 14 => Err(AsiCameraError::VideoModeActive), + 15 => Err(AsiCameraError::ExposureInProgress), + 16 => Err(AsiCameraError::GeneralError), + 17 => Err(AsiCameraError::InvalidMode), + 18 => Err(AsiCameraError::End), + n => Err(AsiCameraError::Unknown(n)), } } -pub fn start_exposure(camera_id: i32) { - check_error_code(unsafe { libasi_sys::camera::ASIStartExposure(camera_id, 0) }); +pub fn start_exposure(camera_id: i32) -> Result<(), AsiCameraError> { + check_error_code(unsafe { libasi_sys::camera::ASIStartExposure(camera_id, 0) }) } -pub fn stop_exposure(camera_id: i32) { - check_error_code(unsafe { libasi_sys::camera::ASIStopExposure(camera_id) }); +pub fn stop_exposure(camera_id: i32) -> Result<(), AsiCameraError> { + check_error_code(unsafe { libasi_sys::camera::ASIStopExposure(camera_id) }) } #[cfg(windows)] -pub fn exposure_status(camera_id: i32, status: *mut i32) { - check_error_code(unsafe { libasi_sys::camera::ASIGetExpStatus(camera_id, status) }); +pub fn exposure_status(camera_id: i32, status: *mut i32) -> Result<(), AsiCameraError> { + check_error_code(unsafe { libasi_sys::camera::ASIGetExpStatus(camera_id, status) }) } #[cfg(unix)] -pub fn exposure_status(camera_id: i32, status: *mut u32) { - check_error_code(unsafe { libasi_sys::camera::ASIGetExpStatus(camera_id, status) }); +pub fn exposure_status(camera_id: i32, status: *mut u32) -> Result<(), AsiCameraError> { + check_error_code(unsafe { libasi_sys::camera::ASIGetExpStatus(camera_id, status) }) } #[cfg(windows)] -pub fn download_exposure(camera_id: i32, buffer: *mut u8, buf_size: i32) { - check_error_code(unsafe { libasi_sys::camera::ASIGetDataAfterExp(camera_id, buffer, buf_size) }); +pub fn download_exposure( + camera_id: i32, + buffer: *mut u8, + buf_size: i32, +) -> Result<(), AsiCameraError> { + check_error_code(unsafe { + libasi_sys::camera::ASIGetDataAfterExp(camera_id, buffer, buf_size) + }) } #[cfg(unix)] -pub fn download_exposure(camera_id: i32, buffer: *mut u8, buf_size: i64) { - check_error_code(unsafe { libasi_sys::camera::ASIGetDataAfterExp(camera_id, buffer, buf_size) }); +pub fn download_exposure( + camera_id: i32, + buffer: *mut u8, + buf_size: i64, +) -> Result<(), AsiCameraError> { + check_error_code(unsafe { + libasi_sys::camera::ASIGetDataAfterExp(camera_id, buffer, buf_size) + }) } pub fn get_num_of_connected_cameras() -> i32 { unsafe { libasi_sys::camera::ASIGetNumOfConnectedCameras() } } -pub fn get_cam_id(camera_id: i32, asi_id: *mut AsiID) { - check_error_code(unsafe { libasi_sys::camera::ASIGetID(camera_id, asi_id) }); +pub fn get_cam_id(camera_id: i32, asi_id: *mut AsiID) -> Result<(), AsiCameraError> { + check_error_code(unsafe { libasi_sys::camera::ASIGetID(camera_id, asi_id) }) } -pub fn set_cam_id(camera_id: i32, asi_id: AsiID) { - check_error_code(unsafe { libasi_sys::camera::ASISetID(camera_id, asi_id) }); +pub fn set_cam_id(camera_id: i32, asi_id: AsiID) -> Result<(), AsiCameraError> { + check_error_code(unsafe { libasi_sys::camera::ASISetID(camera_id, asi_id) }) } -pub fn open_camera(camera_index: i32) { - check_error_code(unsafe { libasi_sys::camera::ASIOpenCamera(camera_index) }); +pub fn open_camera(camera_index: i32) -> Result<(), AsiCameraError> { + check_error_code(unsafe { libasi_sys::camera::ASIOpenCamera(camera_index) }) } -pub fn init_camera(camera_index: i32) { - check_error_code(unsafe { libasi_sys::camera::ASIInitCamera(camera_index) }); +pub fn init_camera(camera_index: i32) -> Result<(), AsiCameraError> { + check_error_code(unsafe { libasi_sys::camera::ASIInitCamera(camera_index) }) } -pub fn close_camera(camera_index: i32) { - check_error_code(unsafe { libasi_sys::camera::ASICloseCamera(camera_index) }); +pub fn close_camera(camera_index: i32) -> Result<(), AsiCameraError> { + check_error_code(unsafe { libasi_sys::camera::ASICloseCamera(camera_index) }) } -pub fn get_control_caps(camera_id: i32, index: i32, noc: *mut AsiControlCaps) { - check_error_code(unsafe { libasi_sys::camera::ASIGetControlCaps(camera_id, index, noc) }); +pub fn get_control_caps( + camera_id: i32, + index: i32, + noc: *mut AsiControlCaps, +) -> Result<(), AsiCameraError> { + check_error_code(unsafe { libasi_sys::camera::ASIGetControlCaps(camera_id, index, noc) }) } -pub fn get_num_of_controls(camera_index: i32, noc: *mut i32) { - check_error_code(unsafe { libasi_sys::camera::ASIGetNumOfControls(camera_index, noc) }); +pub fn get_num_of_controls(camera_index: i32, noc: *mut i32) -> Result<(), AsiCameraError> { + check_error_code(unsafe { libasi_sys::camera::ASIGetNumOfControls(camera_index, noc) }) } -pub fn get_camera_info(asi_info: *mut AsiCameraInfo, camera_index: i32) { - check_error_code(unsafe { libasi_sys::camera::ASIGetCameraProperty(asi_info, camera_index) }); +pub fn get_camera_info( + asi_info: *mut AsiCameraInfo, + camera_index: i32, +) -> Result<(), AsiCameraError> { + check_error_code(unsafe { libasi_sys::camera::ASIGetCameraProperty(asi_info, camera_index) }) } #[cfg(windows)] @@ -115,10 +168,10 @@ pub fn get_control_value( control_type: i32, value: &mut i32, is_auto_set: &mut i32, -) { +) -> Result<(), AsiCameraError> { check_error_code(unsafe { libasi_sys::camera::ASIGetControlValue(camera_index, control_type, value, is_auto_set) - }); + }) } #[cfg(unix)] @@ -127,24 +180,34 @@ pub fn get_control_value( control_type: i32, value: &mut i64, is_auto_set: &mut i32, -) { +) -> Result<(), AsiCameraError> { check_error_code(unsafe { libasi_sys::camera::ASIGetControlValue(camera_index, control_type, value, is_auto_set) - }); + }) } #[cfg(windows)] -pub fn set_control_value(camera_index: i32, control_type: i32, value: i32, is_auto_set: i32) { +pub fn set_control_value( + camera_index: i32, + control_type: i32, + value: i32, + is_auto_set: i32, +) -> Result<(), AsiCameraError> { check_error_code(unsafe { libasi_sys::camera::ASISetControlValue(camera_index, control_type, value, is_auto_set) - }); + }) } #[cfg(unix)] -pub fn set_control_value(camera_index: i32, control_type: i32, value: ::std::os::raw::c_long, is_auto_set: i32) { +pub fn set_control_value( + camera_index: i32, + control_type: i32, + value: ::std::os::raw::c_long, + is_auto_set: i32, +) -> Result<(), AsiCameraError> { check_error_code(unsafe { libasi_sys::camera::ASISetControlValue(camera_index, control_type, value, is_auto_set) - }); + }) } pub fn get_roi_format( @@ -153,10 +216,10 @@ pub fn get_roi_format( height: &mut i32, bin: &mut i32, img_type: &mut i32, -) { +) -> Result<(), AsiCameraError> { check_error_code(unsafe { libasi_sys::camera::ASIGetROIFormat(camera_id, width, height, bin, img_type) - }); + }) } pub fn set_roi_format( @@ -165,20 +228,58 @@ pub fn set_roi_format( height: i32, bin: i32, img_type: i32, -) { +) -> Result<(), AsiCameraError> { check_error_code(unsafe { libasi_sys::camera::ASISetROIFormat(camera_id, width, height, bin, img_type) - }); + }) } -pub fn get_start_position(cam_idx: i32, start_x: &mut i32, start_y: &mut i32) { - check_error_code(unsafe { - libasi_sys::camera::ASIGetStartPos(cam_idx, start_x, start_y) - }); +pub fn get_start_position( + cam_idx: i32, + start_x: &mut i32, + start_y: &mut i32, +) -> Result<(), AsiCameraError> { + check_error_code(unsafe { libasi_sys::camera::ASIGetStartPos(cam_idx, start_x, start_y) }) } -pub fn get_camera_mode(cam_idx: i32, camera_mode: &mut i32) { - check_error_code(unsafe { - libasi_sys::camera::ASIGetCameraMode(cam_idx, camera_mode) - }); +pub fn get_camera_mode(cam_idx: i32, camera_mode: &mut i32) -> Result<(), AsiCameraError> { + check_error_code(unsafe { libasi_sys::camera::ASIGetCameraMode(cam_idx, camera_mode) }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn success_code_is_ok() { + assert!(check_error_code(0).is_ok()); + } + + #[test] + fn known_error_codes_map_correctly() { + assert_eq!(check_error_code(1), Err(AsiCameraError::InvalidIndex)); + assert_eq!(check_error_code(2), Err(AsiCameraError::InvalidId)); + assert_eq!(check_error_code(3), Err(AsiCameraError::InvalidControlType)); + assert_eq!(check_error_code(4), Err(AsiCameraError::CameraClosed)); + assert_eq!(check_error_code(5), Err(AsiCameraError::CameraRemoved)); + assert_eq!(check_error_code(6), Err(AsiCameraError::InvalidPath)); + assert_eq!(check_error_code(7), Err(AsiCameraError::InvalidFileFormat)); + assert_eq!(check_error_code(8), Err(AsiCameraError::InvalidSize)); + assert_eq!(check_error_code(9), Err(AsiCameraError::InvalidImgType)); + assert_eq!(check_error_code(10), Err(AsiCameraError::OutOfBoundary)); + assert_eq!(check_error_code(11), Err(AsiCameraError::Timeout)); + assert_eq!(check_error_code(12), Err(AsiCameraError::InvalidSequence)); + assert_eq!(check_error_code(13), Err(AsiCameraError::BufferTooSmall)); + assert_eq!(check_error_code(14), Err(AsiCameraError::VideoModeActive)); + assert_eq!(check_error_code(15), Err(AsiCameraError::ExposureInProgress)); + assert_eq!(check_error_code(16), Err(AsiCameraError::GeneralError)); + assert_eq!(check_error_code(17), Err(AsiCameraError::InvalidMode)); + assert_eq!(check_error_code(18), Err(AsiCameraError::End)); + } + + #[test] + fn unknown_code_wraps_value() { + assert_eq!(check_error_code(99), Err(AsiCameraError::Unknown(99))); + assert_eq!(check_error_code(-5), Err(AsiCameraError::Unknown(-5))); + } } diff --git a/libasi/src/efw.rs b/libasi/src/efw.rs index 4e35a42..5a9527c 100644 --- a/libasi/src/efw.rs +++ b/libasi/src/efw.rs @@ -1,25 +1,48 @@ pub use libasi_sys::efw::*; -use log::error; pub type EFWInfo = _EFW_INFO; pub type EFWId = _EFW_ID; -fn check_error_code(code: i32) { +#[derive(Debug, thiserror::Error, PartialEq)] +pub enum AsiEfwError { + #[error("invalid index")] + InvalidIndex, + #[error("invalid id")] + InvalidId, + #[error("invalid value")] + InvalidValue, + #[error("filter wheel removed")] + Removed, + #[error("filter wheel is moving")] + Moving, + #[error("error state")] + ErrorState, + #[error("general error")] + GeneralError, + #[error("not supported")] + NotSupported, + #[error("device closed")] + Closed, + #[error("end sentinel")] + End, + #[error("unknown error code: {0}")] + Unknown(i32), +} + +fn check_error_code(code: i32) -> Result<(), AsiEfwError> { match code { - 0 => (), - 1 => error!("EFW_ERROR_INVALID_INDEX"), - 2 => error!("EFW_ERROR_INVALID_ID"), - 3 => error!("EFW_ERROR_INVALID_VALUE"), - // Failed to find the filter wheel, maybe the filter wheel has been removed - 4 => error!("EFW_ERROR_REMOVED"), - // Filter wheel is moving - 5 => error!("EFW_ERROR_MOVING"), - 6 => error!("EFW_ERROR_ERROR_STATE"), - 7 => error!("EFW_ERROR_GENERAL_ERROR"), - 8 => error!("EFW_ERROR_NOT_SUPPORTED"), - 9 => error!("EFW_ERROR_CLOSED"), - -1 => error!("EFW_ERROR_END"), - _ => error!("UNKNOWN_ERROR"), + 0 => Ok(()), + 1 => Err(AsiEfwError::InvalidIndex), + 2 => Err(AsiEfwError::InvalidId), + 3 => Err(AsiEfwError::InvalidValue), + 4 => Err(AsiEfwError::Removed), + 5 => Err(AsiEfwError::Moving), + 6 => Err(AsiEfwError::ErrorState), + 7 => Err(AsiEfwError::GeneralError), + 8 => Err(AsiEfwError::NotSupported), + 9 => Err(AsiEfwError::Closed), + -1 => Err(AsiEfwError::End), + n => Err(AsiEfwError::Unknown(n)), } } @@ -39,76 +62,53 @@ pub fn get_product_ids() -> Vec { pids } -pub fn get_efw_id(index: i32, id: *mut i32) { - check_error_code( - unsafe { libasi_sys::efw::EFWGetID(index, id) } - ); +pub fn get_efw_id(index: i32, id: *mut i32) -> Result<(), AsiEfwError> { + check_error_code(unsafe { libasi_sys::efw::EFWGetID(index, id) }) } -pub fn open_efw(id: i32) { - check_error_code( - unsafe { libasi_sys::efw::EFWOpen(id) } - ); +pub fn open_efw(id: i32) -> Result<(), AsiEfwError> { + check_error_code(unsafe { libasi_sys::efw::EFWOpen(id) }) } pub fn check_wheel_is_moving(id: i32) -> bool { let mut info = EFWInfo::new(); let status = unsafe { libasi_sys::efw::EFWGetProperty(id, &mut info) }; - - match status { - 5 => return true, - _ => return false, - }; + matches!(check_error_code(status), Err(AsiEfwError::Moving)) } - -pub fn get_efw_property(id: i32, info: *mut EFWInfo) { - check_error_code( - unsafe { libasi_sys::efw::EFWGetProperty(id, info) } - ); +pub fn get_efw_property(id: i32, info: *mut EFWInfo) -> Result<(), AsiEfwError> { + check_error_code(unsafe { libasi_sys::efw::EFWGetProperty(id, info) }) } -pub fn get_efw_position(id: i32) -> i32 { +pub fn get_efw_position(id: i32) -> Result { let mut position: i32 = 0; - check_error_code( - unsafe { libasi_sys::efw::EFWGetPosition(id, &mut position) } - ); - // To have users dealing with non 0 indexed values, we simply add always 1 to - // the 0 indexed position returned from the firmware - position + 1 + check_error_code(unsafe { libasi_sys::efw::EFWGetPosition(id, &mut position) })?; + // SDK uses 0-based positions; callers work with 1-based. Return 0 while moving. + Ok(position + 1) } -pub fn set_efw_position(id: i32, position: i32) { - // To have users dealing with non 0 indexed values, we simply subtract always 1 to - // the 0 indexed position wanted by the user - let indexed_0_position = position -1; - check_error_code( - unsafe { libasi_sys::efw::EFWSetPosition(id, indexed_0_position) } - ); +pub fn set_efw_position(id: i32, position: i32) -> Result<(), AsiEfwError> { + // Callers supply 1-based positions; SDK expects 0-based. + let indexed_0_position = position - 1; + check_error_code(unsafe { libasi_sys::efw::EFWSetPosition(id, indexed_0_position) }) } -pub fn set_unidirection(id: i32, flag: bool) { - check_error_code( - unsafe { EFWSetDirection(id, flag) } - ); +pub fn set_unidirection(id: i32, flag: bool) -> Result<(), AsiEfwError> { + check_error_code(unsafe { EFWSetDirection(id, flag) }) } -pub fn is_unidirectional(id: i32) -> bool { +pub fn is_unidirectional(id: i32) -> Result { let mut unid: bool = false; - check_error_code( - unsafe { EFWGetDirection(id, &mut unid) } - ); - unid + check_error_code(unsafe { EFWGetDirection(id, &mut unid) })?; + Ok(unid) } -pub fn calibrate_wheel(id: i32) { - check_error_code( - unsafe { EFWCalibrate(id) } - ); +pub fn calibrate_wheel(id: i32) -> Result<(), AsiEfwError> { + check_error_code(unsafe { EFWCalibrate(id) }) } -pub fn close_efw(id: i32) { - check_error_code(unsafe { EFWClose(id) }); +pub fn close_efw(id: i32) -> Result<(), AsiEfwError> { + check_error_code(unsafe { EFWClose(id) }) } /// Returns the SDK version string, e.g. `"1, 8, 4"`. @@ -123,33 +123,68 @@ pub fn get_sdk_version() -> String { } /// Retrieves the hardware-level firmware error code for the given device. -pub fn get_hw_error_code(id: i32) -> i32 { +pub fn get_hw_error_code(id: i32) -> Result { let mut err_code: i32 = 0; - check_error_code(unsafe { libasi_sys::efw::EFWGetHWErrorCode(id, &mut err_code) }); - err_code + check_error_code(unsafe { libasi_sys::efw::EFWGetHWErrorCode(id, &mut err_code) })?; + Ok(err_code) } /// Retrieves the firmware version as `(major, minor, build)`. -pub fn get_firmware_version(id: i32) -> (u8, u8, u8) { +pub fn get_firmware_version(id: i32) -> Result<(u8, u8, u8), AsiEfwError> { let mut major: u8 = 0; let mut minor: u8 = 0; let mut build: u8 = 0; check_error_code(unsafe { libasi_sys::efw::EFWGetFirmwareVersion(id, &mut major, &mut minor, &mut build) - }); - (major, minor, build) + })?; + Ok((major, minor, build)) } /// Retrieves the serial number of the EFW device. /// Returns `EFW_SN` which is an alias for `EFW_ID` (`[u8; 8]`). /// Note: returns `EFW_ERROR_NOT_SUPPORTED` on older firmware. -pub fn get_serial_number(id: i32) -> EFWId { +pub fn get_serial_number(id: i32) -> Result { let mut sn = EFWId::new(); - check_error_code(unsafe { libasi_sys::efw::EFWGetSerialNumber(id, &mut sn) }); - sn + check_error_code(unsafe { libasi_sys::efw::EFWGetSerialNumber(id, &mut sn) })?; + Ok(sn) } /// Writes an 8-byte alias ID to the EFW device flash. -pub fn set_id(id: i32, alias: EFWId) { - check_error_code(unsafe { libasi_sys::efw::EFWSetID(id, alias) }); +pub fn set_id(id: i32, alias: EFWId) -> Result<(), AsiEfwError> { + check_error_code(unsafe { libasi_sys::efw::EFWSetID(id, alias) }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn success_code_is_ok() { + assert!(check_error_code(0).is_ok()); + } + + #[test] + fn all_known_efw_codes_map_correctly() { + assert_eq!(check_error_code(1), Err(AsiEfwError::InvalidIndex)); + assert_eq!(check_error_code(2), Err(AsiEfwError::InvalidId)); + assert_eq!(check_error_code(3), Err(AsiEfwError::InvalidValue)); + assert_eq!(check_error_code(4), Err(AsiEfwError::Removed)); + assert_eq!(check_error_code(5), Err(AsiEfwError::Moving)); + assert_eq!(check_error_code(6), Err(AsiEfwError::ErrorState)); + assert_eq!(check_error_code(7), Err(AsiEfwError::GeneralError)); + assert_eq!(check_error_code(8), Err(AsiEfwError::NotSupported)); + assert_eq!(check_error_code(9), Err(AsiEfwError::Closed)); + assert_eq!(check_error_code(-1), Err(AsiEfwError::End)); + } + + #[test] + fn moving_code_maps_to_moving_error() { + assert_eq!(check_error_code(5), Err(AsiEfwError::Moving)); + } + + #[test] + fn unknown_code_wraps_value() { + assert_eq!(check_error_code(99), Err(AsiEfwError::Unknown(99))); + assert_eq!(check_error_code(-99), Err(AsiEfwError::Unknown(-99))); + } } diff --git a/src/bin/ccd/ccd.rs b/src/bin/ccd/ccd.rs index 33df107..34f42de 100644 --- a/src/bin/ccd/ccd.rs +++ b/src/bin/ccd/ccd.rs @@ -1,6 +1,6 @@ use crate::utils::fetch_control_caps; use crate::utils::get_num_of_controls; -use libasi::camera::AsiCameraInfo; +use libasi::camera::{AsiCameraError, AsiCameraInfo}; use astrotools::properties::{Permission, Prop, Property, RangeProperty}; use log::{debug, error, info}; @@ -28,7 +28,9 @@ pub mod utils { pub fn get_camera_id(camera_index: i32) -> String { let mut id: AsiID = AsiID::new(); - get_cam_id(camera_index, &mut id); + if let Err(e) = get_cam_id(camera_index, &mut id) { + log::error!("Failed to get camera ID for index {camera_index}: {e}"); + } // if the AsiID is a bunch of 0, we set a random ID and we dump it to the camera flash // memory. If you are wondering why, the reason is the following; one may want to use multiple @@ -69,7 +71,9 @@ pub mod utils { camera_index, asi_id_to_string(&id.id) ); - set_cam_id(camera_index, id); + if let Err(e) = set_cam_id(camera_index, id) { + log::error!("Failed to set camera ID for index {camera_index}: {e}"); + } } } @@ -122,34 +126,43 @@ pub mod utils { // Set the value of the exposure on the driver #[cfg(unix)] { - set_control_value( + if let Err(e) = set_control_value( idx, libasi::camera::ASI_CONTROL_TYPE_ASI_EXPOSURE as i32, secs_to_micros, libasi::camera::ASI_BOOL_ASI_FALSE as i32, - ); + ) { + error!("Failed to set exposure time: {e}"); + return; + } } #[cfg(windows)] { - set_control_value( + if let Err(e) = set_control_value( idx, libasi::camera::ASI_CONTROL_TYPE_ASI_EXPOSURE as i32, secs_to_micros as i32, 0, - ); + ) { + error!("Failed to set exposure time: {e}"); + return; + } } // Send the command to start the exposure - start_exposure(idx); - exposure_status(idx, &mut status); + if let Err(e) = start_exposure(idx) { + error!("Failed to start exposure: {e}"); + return; + } + let _ = exposure_status(idx, &mut status); let start = SystemTime::now(); // Swapping exposure related properties AKA prepare props to show // informations about ongoing exposure { let mut d = device.write().unwrap(); - // TODO: Fix this unused result - let _ = d.exposure_status + // TODO: Fix this unused result + let _ = d.exposure_status .update_int(std::borrow::Cow::Borrowed("EXPOSING")); } @@ -157,7 +170,7 @@ pub mod utils { // Loop until the status change while status == 1 { - exposure_status(idx, &mut status); + let _ = exposure_status(idx, &mut status); std::thread::sleep(std::time::Duration::from_millis(50)); } @@ -168,13 +181,16 @@ pub mod utils { info!("Exposure successful"); { let mut d = device.write().unwrap(); - // TODO: Fix this unused result - let _ = d.exposure_status + // TODO: Fix this unused result + let _ = d.exposure_status .update_int(std::borrow::Cow::Borrowed("SUCCESS")); } info!("downloading"); - download_exposure(idx, image_buffer.as_mut_ptr(), buffer_size.into()); + if let Err(e) = download_exposure(idx, image_buffer.as_mut_ptr(), buffer_size.into()) { + error!("Failed to download exposure: {e}"); + return; + } let mut mqttoptions = MqttOptions::new("asi_exposure", "127.0.0.1", 1883); mqttoptions.set_keep_alive(Duration::from_secs(5)); @@ -248,31 +264,6 @@ pub mod utils { } } - pub fn check_error_code(code: i32) { - match code { - 0 => (), //ASI_SUCCESS - 1 => error!("ASI_ERROR_INVALID_INDEX"), //no camera connected or index value out of boundary - 2 => error!("ASI_ERROR_INVALID_ID"), //invalid ID - 3 => error!("ASI_ERROR_INVALID_CONTROL_TYPE"), //invalid control type - 4 => error!("ASI_ERROR_CAMERA_CLOSED"), //camera didn't open - 5 => error!("ASI_ERROR_CAMERA_REMOVED"), //failed to find the camera, maybe the camera has been removed - 6 => error!("ASI_ERROR_INVALID_PATH"), //cannot find the path of the file - 7 => error!("ASI_ERROR_INVALID_FILEFORMAT"), - 8 => error!("ASI_ERROR_INVALID_SIZE"), //wrong video format size - 9 => error!("ASI_ERROR_INVALID_IMGTYPE"), //unsupported image formate - 10 => error!("ASI_ERROR_OUTOF_BOUNDARY"), //the startpos is out of boundary - 11 => error!("ASI_ERROR_TIMEOUT"), //timeout - 12 => error!("ASI_ERROR_INVALID_SEQUENCE"), //stop capture first - 13 => error!("ASI_ERROR_BUFFER_TOO_SMALL"), //buffer size is not big enough - 14 => error!("ASI_ERROR_VIDEO_MODE_ACTIVE"), - 15 => error!("ASI_ERROR_EXPOSURE_IN_PROGRESS"), - 16 => error!("ASI_ERROR_GENERAL_ERROR"), //general error, eg: value is out of valid range - 17 => error!("ASI_ERROR_INVALID_MODE"), //the current mode is wrong - 18 => error!("ASI_ERROR_END"), - e => error!("unknown error {}", e), - } - } - #[cfg(unix)] pub fn bayer_pattern_to_str(n: &u32) -> &'static str { match n { @@ -393,7 +384,10 @@ pub mod utils { for i in 0..num_of_caps { let mut control_caps = AsiControlCaps::new(); - libasi::camera::get_control_caps(cam_idx, i, &mut control_caps); + if let Err(e) = libasi::camera::get_control_caps(cam_idx, i, &mut control_caps) { + log::error!("Failed to get control cap {i} for camera {cam_idx}: {e}"); + continue; + } let cap = AsiProperty { name: crate::utils::asi_name_to_string_i8(&control_caps.Name).to_case(Case::Snake), @@ -415,7 +409,9 @@ pub mod utils { /// This method must be called AFTER the camera is initialized by the SDK pub fn get_num_of_controls(index: i32) -> i32 { let mut num_of_controls = 0; - libasi::camera::get_num_of_controls(index, &mut num_of_controls); + if let Err(e) = libasi::camera::get_num_of_controls(index, &mut num_of_controls) { + log::error!("Failed to get number of controls for camera {index}: {e}"); + } info!("Found: {} controls for camera {}", num_of_controls, index); num_of_controls } @@ -460,20 +456,20 @@ pub struct AsiCamera { lightspeed_id: Property>, // Properties to build logic around exposures exposing: Property, - exposure_status: Property>, - width: Property, - height: Property, + pub exposure_status: Property>, + pub width: Property, + pub height: Property, bin: Property, image_type: Property, } impl AsiCamera { - pub fn new(index: i32) -> Self { + pub fn new(index: i32) -> Result { // From the SDK documentation, in order: // 1) Get count of connected cameras (THIS IS DONE ALREADY as we already called look_for_devices // 2) get camera ID using ASIGetCameraProperty let mut info = AsiCameraInfo::new(); - libasi::camera::get_camera_info(&mut info, index); + libasi::camera::get_camera_info(&mut info, index)?; debug!( "Saying welcome to camera `{}`", @@ -481,10 +477,10 @@ impl AsiCamera { ); // 3) Open camera using ASIOpenCamera - libasi::camera::open_camera(index); + libasi::camera::open_camera(index)?; // 4)Initialise the camera using ASIInitCamera - libasi::camera::init_camera(index); + libasi::camera::init_camera(index)?; // 5) Get count of control type with ASIGetControlCaps // Check how many capabilities this camera has, reallocate the vector @@ -551,7 +547,7 @@ impl AsiCamera { device.asi_caps_to_lightspeed_props(); device.fetch_roi_format(); - device + Ok(device) } pub fn fetch_props(&mut self) { @@ -563,8 +559,8 @@ impl AsiCamera { debug!("Cap {} value is {}", &cap.name, &val); let v = self.controls.get_mut(&cap.name).unwrap(); if v.value() != &val { - // TODO: Fix this unused error - let _ = v.update_int(val); + // TODO: Fix this unused error + let _ = v.update_int(val); } } @@ -618,12 +614,14 @@ impl AsiCamera { let mut is_auto_set = 0; let mut val: i64 = 0; - libasi::camera::get_control_value( + if let Err(e) = libasi::camera::get_control_value( *self.index(), cap.control_type, &mut val, &mut is_auto_set, - ); + ) { + error!("Failed to get control value for '{}': {e}", cap.name); + } debug!( "Value for {} is {} - Auto adjusted? {}", cap.name, val, cap.is_writable @@ -634,7 +632,7 @@ impl AsiCamera { /// Close gently the connection to the camera using the SDK pub fn close(&self) { debug!("Closing camera {}", self.name); - libasi::camera::close_camera(*self.index()); + let _ = libasi::camera::close_camera(*self.index()); } fn fetch_roi_format(&mut self) { @@ -644,13 +642,16 @@ impl AsiCamera { let mut bin = 10; let mut img_type = 10; - libasi::camera::get_roi_format( + if let Err(e) = libasi::camera::get_roi_format( *self.index(), &mut width, &mut height, &mut bin, &mut img_type, - ); + ) { + error!("Failed to read ROI format: {e}"); + return; + } // Update now the struct values self.width.update(width).unwrap(); @@ -696,6 +697,8 @@ impl AsiCamera { *self.image_type.value() }; - libasi::camera::set_roi_format(*self.index(), w, h, b, img); + if let Err(e) = libasi::camera::set_roi_format(*self.index(), w, h, b, img) { + error!("Failed to set ROI format: {e}"); + } } } diff --git a/src/bin/ccd/main.rs b/src/bin/ccd/main.rs index 630e1c1..481e543 100644 --- a/src/bin/ccd/main.rs +++ b/src/bin/ccd/main.rs @@ -24,13 +24,14 @@ struct AsiCcd { impl AsiCcd { fn new() -> Self { let found = utils::look_for_devices(); - let mut devices: Vec>> = Vec::with_capacity(found as usize); - - for idx in 0..found { - let device = Arc::new(RwLock::new(AsiCamera::new(idx))); - devices.push(device) - } - + let devices = (0..found) + .filter_map(|idx| { + AsiCamera::new(idx) + .map_err(|e| log::error!("Failed to initialize camera {idx}: {e}")) + .ok() + }) + .map(|d| Arc::new(RwLock::new(d))) + .collect(); Self { devices } } } diff --git a/src/bin/efw/efw.rs b/src/bin/efw/efw.rs index 5ae4b32..5d7a917 100644 --- a/src/bin/efw/efw.rs +++ b/src/bin/efw/efw.rs @@ -1,7 +1,9 @@ -use log::{debug, info, warn}; +use log::{debug, error, info, warn}; use serde::Serialize; use uuid::Uuid; +use libasi::efw::AsiEfwError; + pub fn look_for_devices() -> i32 { let num = libasi::efw::get_num_of_connected_devices(); match num { @@ -25,26 +27,26 @@ pub struct EfwDevice { } impl EfwDevice { - pub fn new(index: i32) -> Self { + pub fn new(index: i32) -> Result { let mut efw_id: i32 = 0; - libasi::efw::get_efw_id(index, &mut efw_id); + libasi::efw::get_efw_id(index, &mut efw_id)?; - libasi::efw::open_efw(efw_id); + libasi::efw::open_efw(efw_id)?; let mut info = libasi::efw::EFWInfo::new(); - libasi::efw::get_efw_property(efw_id, &mut info); + libasi::efw::get_efw_property(efw_id, &mut info)?; let name = asi_rs::utils::asi_name_to_string(&info.Name); let slot_num = info.slotNum; - let current_slot = libasi::efw::get_efw_position(efw_id); - let unidirectional = libasi::efw::is_unidirectional(efw_id); + let current_slot = libasi::efw::get_efw_position(efw_id)?; + let unidirectional = libasi::efw::is_unidirectional(efw_id)?; info!( "EFW '{}' opened: {} slots, current={}, unidirectional={}", name, slot_num, current_slot, unidirectional ); - Self { + Ok(Self { id: Uuid::new_v4(), name: format!("ZWO {}", name), efw_id, @@ -52,7 +54,7 @@ impl EfwDevice { current_slot, unidirectional, calibrating: false, - } + }) } pub fn fetch_props(&mut self) { @@ -60,25 +62,37 @@ impl EfwDevice { if self.calibrating { return; } - let slot = libasi::efw::get_efw_position(self.efw_id); - if self.current_slot != slot { - debug!("Slot changed: {} -> {}", self.current_slot, slot); - self.current_slot = slot; + match libasi::efw::get_efw_position(self.efw_id) { + Ok(slot) => { + if self.current_slot != slot { + debug!("Slot changed: {} -> {}", self.current_slot, slot); + self.current_slot = slot; + } + } + Err(e) => error!("Failed to get EFW position: {e}"), } - let unid = libasi::efw::is_unidirectional(self.efw_id); - if self.unidirectional != unid { - self.unidirectional = unid; + match libasi::efw::is_unidirectional(self.efw_id) { + Ok(unid) => { + if self.unidirectional != unid { + self.unidirectional = unid; + } + } + Err(e) => error!("Failed to get EFW direction: {e}"), } } pub fn set_slot(&self, position: i32) { debug!("Setting EFW slot to {}", position); - libasi::efw::set_efw_position(self.efw_id, position); + if let Err(e) = libasi::efw::set_efw_position(self.efw_id, position) { + error!("Failed to set EFW slot {position}: {e}"); + } } pub fn set_unidirectional(&self, flag: bool) { debug!("Setting EFW unidirectional to {}", flag); - libasi::efw::set_unidirection(self.efw_id, flag); + if let Err(e) = libasi::efw::set_unidirection(self.efw_id, flag) { + error!("Failed to set EFW unidirectional: {e}"); + } } pub fn efw_id(&self) -> i32 { @@ -87,6 +101,34 @@ impl EfwDevice { pub fn close(&self) { debug!("Closing EFW '{}'", self.name); - libasi::efw::close_efw(self.efw_id); + let _ = libasi::efw::close_efw(self.efw_id); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn efw_device_skips_internal_fields() { + let device = EfwDevice { + id: Uuid::new_v4(), + name: "ZWO EFW Mini".to_string(), + efw_id: 42, + slot_num: 5, + current_slot: 2, + unidirectional: false, + calibrating: false, + }; + let json = serde_json::to_string(&device).unwrap(); + // Internal fields must not appear in serialized output + assert!(!json.contains("\"id\"")); + assert!(!json.contains("\"efw_id\"")); + // Public fields must be present + assert!(json.contains("\"name\"")); + assert!(json.contains("\"slot_num\"")); + assert!(json.contains("\"current_slot\"")); + assert!(json.contains("\"unidirectional\"")); + assert!(json.contains("\"calibrating\"")); } } diff --git a/src/bin/efw/main.rs b/src/bin/efw/main.rs index 780d758..c869c65 100644 --- a/src/bin/efw/main.rs +++ b/src/bin/efw/main.rs @@ -22,11 +22,14 @@ struct AsiEfwDriver { impl AsiEfwDriver { fn new() -> Self { let found = efw::look_for_devices(); - let mut devices = Vec::with_capacity(found as usize); - for idx in 0..found { - let device = Arc::new(RwLock::new(EfwDevice::new(idx))); - devices.push(device); - } + let devices = (0..found) + .filter_map(|idx| { + EfwDevice::new(idx) + .map_err(|e| log::error!("Failed to initialize EFW device {idx}: {e}")) + .ok() + }) + .map(|d| Arc::new(RwLock::new(d))) + .collect(); Self { devices } } } @@ -130,7 +133,9 @@ async fn main() { task::spawn_blocking(move || { let efw_id = device.read().unwrap().efw_id(); device.write().unwrap().calibrating = true; - libasi::efw::calibrate_wheel(efw_id); + if let Err(e) = libasi::efw::calibrate_wheel(efw_id) { + log::error!("Failed to calibrate EFW {efw_id}: {e}"); + } while libasi::efw::check_wheel_is_moving(efw_id) { std::thread::sleep(Duration::from_millis(100)); } diff --git a/src/bin/test/main.rs b/src/bin/test/main.rs index 7cdec1c..fd2c475 100644 --- a/src/bin/test/main.rs +++ b/src/bin/test/main.rs @@ -5,7 +5,7 @@ fn get_roi(idx: i32) { let mut height = 20; let mut bin = 20; let mut img_type = 20; - libasi::camera::get_roi_format(idx, &mut width, &mut height, &mut bin, &mut img_type); + let _ = libasi::camera::get_roi_format(idx, &mut width, &mut height, &mut bin, &mut img_type); println!( "Width: {}\nHeight: {}\nBin: {}\nType: {}", width, height, bin, img_type @@ -13,8 +13,8 @@ fn get_roi(idx: i32) { } fn expose(idx: i32) -> u32 { - let mut e_val = 0; - libasi::camera::get_control_value( + let mut e_val: i64 = 0; + let _ = libasi::camera::get_control_value( idx, libasi::camera::ASI_CONTROL_TYPE_ASI_EXPOSURE as i32, &mut e_val, @@ -22,13 +22,13 @@ fn expose(idx: i32) -> u32 { ); println!("Exp time: {}", e_val); println!("Exposing"); - libasi::camera::start_exposure(idx); + let _ = libasi::camera::start_exposure(idx); let mut status = 0; - libasi::camera::exposure_status(idx, &mut status); + let _ = libasi::camera::exposure_status(idx, &mut status); while status == 1 { - libasi::camera::exposure_status(idx, &mut status); + let _ = libasi::camera::exposure_status(idx, &mut status); std::thread::sleep(std::time::Duration::from_millis(1)); } println!("Exposure status: {}", &status); @@ -41,18 +41,18 @@ fn main() { for idx in 0..num_of_devs { println!("Probing camera {}", &idx); - libasi::camera::open_camera(idx); - libasi::camera::init_camera(idx); + let _ = libasi::camera::open_camera(idx); + let _ = libasi::camera::init_camera(idx); let mut num_of_controls = 0; - libasi::camera::get_num_of_controls(idx, &mut num_of_controls); + let _ = libasi::camera::get_num_of_controls(idx, &mut num_of_controls); println!("Found: {} controls for camera {}", num_of_controls, idx); let mut caps = Vec::with_capacity(num_of_controls as usize); for c_id in 0..num_of_controls { let mut control_caps = libasi::camera::AsiControlCaps::new(); - libasi::camera::get_control_caps(idx, c_id, &mut control_caps); + let _ = libasi::camera::get_control_caps(idx, c_id, &mut control_caps); caps.push(control_caps); } @@ -64,7 +64,7 @@ fn main() { let mut is_auto_set = 0; let mut val: i64 = 0; - libasi::camera::get_control_value( + let _ = libasi::camera::get_control_value( idx, cap.ControlType as i32, &mut val, @@ -85,11 +85,11 @@ fn main() { let mut start_x = 50; let mut start_y = 50; - libasi::camera::get_start_position(idx, &mut start_x, &mut start_y); + let _ = libasi::camera::get_start_position(idx, &mut start_x, &mut start_y); println!("Start X: {}, Start Y: {}", start_x, start_y); - let mut e_val = 0; - libasi::camera::get_control_value( + let mut e_val: i64 = 0; + let _ = libasi::camera::get_control_value( idx, libasi::camera::ASI_CONTROL_TYPE_ASI_EXPOSURE as i32, &mut e_val, @@ -101,17 +101,17 @@ fn main() { let mut cmode = 100; - libasi::camera::get_camera_mode(idx, &mut cmode); + let _ = libasi::camera::get_camera_mode(idx, &mut cmode); println!("Camera mode: {}", cmode); - libasi::camera::set_control_value( + let _ = libasi::camera::set_control_value( idx, libasi::camera::ASI_CONTROL_TYPE_ASI_EXPOSURE as i32, length, libasi::camera::ASI_BOOL_ASI_FALSE as i32, ); - let mut e_val = 0; - libasi::camera::get_control_value( + let mut e_val: i64 = 0; + let _ = libasi::camera::get_control_value( idx, libasi::camera::ASI_CONTROL_TYPE_ASI_EXPOSURE as i32, &mut e_val, @@ -131,6 +131,6 @@ fn main() { } } - libasi::camera::close_camera(idx); + let _ = libasi::camera::close_camera(idx); } } diff --git a/src/lib.rs b/src/lib.rs index 3de3701..996411c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,3 +36,130 @@ pub mod utils { } } } + +/// Parse an MQTT device topic of the form `devices/{UUID}/{action}`. +/// +/// Returns `(uuid, action)` on success, or `None` if the topic is malformed +/// (too short, missing separators, or invalid UUID). +pub fn parse_device_topic(topic: &str) -> Option<(uuid::Uuid, &str)> { + // "devices/" = 8 chars, UUID = 36 chars, "/" = 1 char → minimum length is 46 + if topic.len() < 46 { + return None; + } + let uuid_str = &topic[8..44]; + let action = &topic[45..]; + let uuid = uuid_str.parse::().ok()?; + Some((uuid, action)) +} + +#[cfg(test)] +mod tests { + use super::*; + use utils::{asi_id_to_string, asi_name_to_string}; + + // --- asi_name_to_string --- + + #[test] + fn name_normal_with_null_terminator() { + let mut arr = [0i8; 64]; + for (i, &b) in b"ASI294MC Pro".iter().enumerate() { + arr[i] = b as i8; + } + assert_eq!(asi_name_to_string(&arr), "ASI294MC Pro"); + } + + #[test] + fn name_all_zeros_returns_empty() { + let arr = [0i8; 64]; + assert_eq!(asi_name_to_string(&arr), ""); + } + + #[test] + fn name_no_null_terminator_uses_full_array() { + let arr: Vec = b"ABCD".iter().map(|&b| b as i8).collect(); + assert_eq!(asi_name_to_string(&arr), "ABCD"); + } + + #[test] + fn name_null_in_middle_truncates() { + let arr: Vec = vec![65, 66, 0, 67, 68]; // "AB\0CD" + assert_eq!(asi_name_to_string(&arr), "AB"); + } + + #[test] + fn name_negative_i8_replaced_with_hash() { + // i8 values that cannot be converted to u8 (< 0) become '#' (0x23) + let arr: Vec = vec![65, -1, 66]; // 'A', invalid, 'B' + let result = asi_name_to_string(&arr); + assert_eq!(result, "A#B"); + } + + // --- asi_id_to_string --- + + #[test] + fn id_normal_with_null_terminator() { + let mut arr = [0u8; 8]; + for (i, &b) in b"MYID1234".iter().enumerate() { + arr[i] = b; + } + assert_eq!(asi_id_to_string(&arr), "MYID1234"); + } + + #[test] + fn id_all_zeros_returns_empty() { + let arr = [0u8; 8]; + assert_eq!(asi_id_to_string(&arr), ""); + } + + #[test] + fn id_null_in_middle_truncates() { + let arr: Vec = vec![65, 66, 0, 67, 68]; // "AB\0CD" + assert_eq!(asi_id_to_string(&arr), "AB"); + } + + #[test] + fn id_no_null_uses_full_array() { + let arr: Vec = b"ABCD".to_vec(); + assert_eq!(asi_id_to_string(&arr), "ABCD"); + } + + // --- parse_device_topic --- + + #[test] + fn parse_valid_expose_topic() { + let uuid = uuid::Uuid::new_v4(); + let topic = format!("devices/{}/expose", uuid); + let (parsed_uuid, action) = parse_device_topic(&topic).expect("should parse"); + assert_eq!(parsed_uuid, uuid); + assert_eq!(action, "expose"); + } + + #[test] + fn parse_valid_set_slot_topic() { + let uuid = uuid::Uuid::new_v4(); + let topic = format!("devices/{}/set_slot", uuid); + let (parsed_uuid, action) = parse_device_topic(&topic).expect("should parse"); + assert_eq!(parsed_uuid, uuid); + assert_eq!(action, "set_slot"); + } + + #[test] + fn parse_valid_calibrate_topic() { + let uuid = uuid::Uuid::new_v4(); + let topic = format!("devices/{}/calibrate", uuid); + let (_, action) = parse_device_topic(&topic).expect("should parse"); + assert_eq!(action, "calibrate"); + } + + #[test] + fn parse_rejects_topic_too_short() { + assert!(parse_device_topic("devices/short").is_none()); + assert!(parse_device_topic("").is_none()); + } + + #[test] + fn parse_rejects_invalid_uuid() { + let topic = "devices/not-a-valid-uuid-at-all-here/expose"; + assert!(parse_device_topic(topic).is_none()); + } +} From a2c07bdd9fda8f27aabbc314cabc650bef065fd9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 19 Mar 2026 06:13:47 +0000 Subject: [PATCH 2/2] Apply cargo fmt formatting https://claude.ai/code/session_01Pa1qQU4xGtZxgBfZQS4eDL --- src/bin/ccd/ccd.rs | 16 ++++++++++------ src/bin/ccd/main.rs | 2 +- src/bin/efw/main.rs | 5 +---- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/bin/ccd/ccd.rs b/src/bin/ccd/ccd.rs index 34f42de..2be7ce2 100644 --- a/src/bin/ccd/ccd.rs +++ b/src/bin/ccd/ccd.rs @@ -21,10 +21,10 @@ pub mod utils { pub mod generics { use crate::utils::asi_id_to_string; - use libasi::camera::{get_cam_id, set_cam_id, AsiID}; + use libasi::camera::{AsiID, get_cam_id, set_cam_id}; use log::{debug, info}; - use rand::distr::Alphanumeric; use rand::RngExt; + use rand::distr::Alphanumeric; pub fn get_camera_id(camera_index: i32) -> String { let mut id: AsiID = AsiID::new(); @@ -80,8 +80,8 @@ pub mod utils { pub mod capturing { use crate::ccd::Camera; use astrotools::properties::Prop; - use base64::prelude::BASE64_STANDARD; use base64::Engine; + use base64::prelude::BASE64_STANDARD; use libasi::camera::{ download_exposure, exposure_status, set_control_value, start_exposure, }; @@ -162,7 +162,8 @@ pub mod utils { { let mut d = device.write().unwrap(); // TODO: Fix this unused result - let _ = d.exposure_status + let _ = d + .exposure_status .update_int(std::borrow::Cow::Borrowed("EXPOSING")); } @@ -182,12 +183,15 @@ pub mod utils { { let mut d = device.write().unwrap(); // TODO: Fix this unused result - let _ = d.exposure_status + let _ = d + .exposure_status .update_int(std::borrow::Cow::Borrowed("SUCCESS")); } info!("downloading"); - if let Err(e) = download_exposure(idx, image_buffer.as_mut_ptr(), buffer_size.into()) { + if let Err(e) = + download_exposure(idx, image_buffer.as_mut_ptr(), buffer_size.into()) + { error!("Failed to download exposure: {e}"); return; } diff --git a/src/bin/ccd/main.rs b/src/bin/ccd/main.rs index 481e543..f2804ea 100644 --- a/src/bin/ccd/main.rs +++ b/src/bin/ccd/main.rs @@ -9,8 +9,8 @@ use tokio::task; use uuid::Uuid; pub mod ccd; -use ccd::utils; use ccd::AsiCamera; +use ccd::utils; use std::time::Instant; use rumqttc::Event::{Incoming, Outgoing}; diff --git a/src/bin/efw/main.rs b/src/bin/efw/main.rs index c869c65..74cd223 100644 --- a/src/bin/efw/main.rs +++ b/src/bin/efw/main.rs @@ -147,10 +147,7 @@ async fn main() { } "update" => { let payload = String::from_utf8_lossy(&data.payload); - info!( - "Update request for {}: {}", - device_id, payload - ); + info!("Update request for {}: {}", device_id, payload); // TODO: parse and dispatch generic property updates } _ => (),