diff --git a/Cargo.lock b/Cargo.lock index 2a274e62b772fcfeb6774c9a31437b42fffa2f48..07afa9ac43aa849a19eaec33167f6ff8609d32d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,18 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ahash" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42cd52102d3df161c77a887b608d7a4897d7cc112886a9537b738a887a03aaff" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.2" @@ -11,6 +23,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + [[package]] name = "autocfg" version = "1.1.0" @@ -42,13 +60,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] -name = "cstr" -version = "0.2.11" +name = "hashbrown" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8aa998c33a6d3271e3678950a22134cd7dd27cef86dee1b611b5b14207d1d90b" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" dependencies = [ - "proc-macro2", - "quote", + "ahash", + "allocator-api2", ] [[package]] @@ -175,6 +193,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + [[package]] name = "pkg-config" version = "0.3.30" @@ -300,7 +324,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" name = "serial-bridge" version = "0.1.0" dependencies = [ - "cstr", + "hashbrown", "lazy_static", "rustmex", "serialport", @@ -383,6 +407,12 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "winapi" version = "0.3.9" @@ -404,3 +434,23 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] diff --git a/Cargo.toml b/Cargo.toml index 943c4091b48139beb861848a6c00d27732b82eeb..29e620c6c45edae208c58b8445da3aa246c92950 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" crate-type = ["cdylib"] [dependencies] -cstr = "0.2.11" +hashbrown = "0.14.3" lazy_static = "1.4.0" rustmex = { path = "../rustmex/rustmex", features = ["matlab800"] } serialport = "4.3.0" diff --git a/justfile b/justfile index b6337501d7df41ddfb0faf93a5c0f60068fa6d57..83e5defd2bac355b9afcd9769d748b770e3c9e53 100644 --- a/justfile +++ b/justfile @@ -1,4 +1,5 @@ -alias b := build +alias br := build-release +alias bd := build-debug alias c := clean alias d := docs @@ -10,10 +11,14 @@ export MATLAB_LIB_DIR := '/Applications/MATLAB_R2021b.app/bin/maci64' default: just --choose -build: +build-release: cargo build --release mv target/release/libserial_bridge.dylib serialbridge.mexmaci64 +build-debug: + cargo build + mv target/debug/libserial_bridge.dylib serialbridge.mexmaci64 + clean: cargo clean rm serialbridge.mexmaci64 diff --git a/src/error.rs b/src/error.rs index c88808ec2a3ee6c8613b4bebef9a8a8da4819833..2b27c3470d164c36be17eba3fa39c402eab8958b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,11 +1,16 @@ -use rustmex::{message::AdHoc, mxArray, FromMatlabError}; +use rustmex::{message::AdHoc, mxArray, FromMatlabError, MxArray}; -pub trait MapMexError<T> { +pub type SResult<T> = std::result::Result<T, Error>; + +pub trait MapMexError<T, E> { fn map_err_adhoc(self, id: &str, msg: &str) -> Result<T, rustmex::Error>; fn mexerr(self, err: Error) -> Result<T, rustmex::Error>; + fn map_mexerr<O>(self, err: O) -> Result<T, rustmex::Error> + where + O: FnOnce(E) -> Error; } -impl<T, E> MapMexError<T> for Result<T, E> { +impl<T, E> MapMexError<T, E> for Result<T, E> { fn map_err_adhoc(self, id: &str, msg: &str) -> Result<T, rustmex::Error> { Ok(self.map_err(|_| AdHoc(id, msg))?) } @@ -13,9 +18,16 @@ impl<T, E> MapMexError<T> for Result<T, E> { fn mexerr(self, err: Error) -> Result<T, rustmex::Error> { self.map_err(|_| err.to_mexerr()) } + + fn map_mexerr<O>(self, err: O) -> Result<T, rustmex::Error> + where + O: FnOnce(E) -> Error, + { + self.map_err(|e| err(e).to_mexerr()) + } } -impl<T> MapMexError<T> for Option<T> { +impl<T> MapMexError<T, ()> for Option<T> { fn map_err_adhoc(self, id: &str, msg: &str) -> Result<T, rustmex::Error> { Ok(self.ok_or(AdHoc(id, msg))?) } @@ -23,10 +35,19 @@ impl<T> MapMexError<T> for Option<T> { fn mexerr(self, err: Error) -> Result<T, rustmex::Error> { self.ok_or_else(|| err.to_mexerr()) } + + fn map_mexerr<O>(self, err: O) -> Result<T, rustmex::Error> + where + O: FnOnce(()) -> Error, + { + self.ok_or_else(|| err(()).to_mexerr()) + } } #[derive(Debug, thiserror::Error)] pub enum Error { + #[error("Too many parameters passed")] + TooManyParameters, #[error("Missing serial mode (first argument)")] MissingSerialMode, #[error("Missing port name (second argument)")] @@ -35,8 +56,20 @@ pub enum Error { MissingBaudrate, #[error("String contains invalid characters")] String(#[from] std::ffi::IntoStringError), + #[error("Invalid Matlab type used, {0}")] + InvalidMatlabType(String), #[error("Invalid serial mode (choose from 'Open', 'Close', 'Read', 'Write')")] InvalidMode, + #[error("Invalid port name (2nd argument): {0}")] + InvalidPortName(Box<Self>), + #[error("Invalid baud rate (3rd argument): {0}")] + InvalidBaudrate(Box<Self>), + #[error("Serial port error: {0}")] + SerialPort(#[from] serialport::Error), + #[error("Parse error")] + Parse, + #[error("Matlab error: {0}")] + Matlab(#[from] rustmex::FromMatlabError<MxArray>), #[error("{0}")] Rustmex(#[from] rustmex::Error), } @@ -44,11 +77,18 @@ pub enum Error { impl Error { fn id(&self) -> &str { match self { + Error::TooManyParameters => "serialbridge:invalid_input", Error::MissingSerialMode => "serialbridge:missing_input", Error::MissingPortName => "serialbridge:missing_input", Error::MissingBaudrate => "serialbridge:missing_input", Error::String(_) => "serialbridge:invalid_input", + Error::InvalidMatlabType(_) => "serialbridge:invalid_input", Error::InvalidMode => "serialbridge:invalid_input", + Error::InvalidPortName(_) => "serialbridge:invalid_input", + Error::InvalidBaudrate(_) => "serialbridge:invalid_input", + Error::SerialPort(_) => "serialbridge:serial_error", + Error::Parse => "serialbridge:parse_error", + Error::Matlab(_) => "serialbridge:matlab_error", Error::Rustmex(err) => err.id(), } } diff --git a/src/lib.rs b/src/lib.rs index fb660aa60150f3865da63179b56b2da4e24351de..2bc0f89408fbe7de5d6efeca7ca3a40142f4db84 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,33 +1,64 @@ mod error; +mod serial; mod types; -use std::sync::RwLock; +use std::sync::Mutex; -use rustmex::{char::CharArray, convert::ToMatlab, prelude::*, MatlabClass}; +use rustmex::{char::CharArray, prelude::*, warning, MatlabClass}; -use error::{Error, MapMexError}; -use types::Mode; +use error::{Error, MapMexError, SResult}; +use types::{MatlabType, Mode}; + +use crate::serial::SerialManager; lazy_static::lazy_static! { - static ref TEST: RwLock<i32> = RwLock::new(42); + static ref SERIAL: Mutex<SerialManager> = Mutex::new(SerialManager::new()); +} + +/// this compiles only in debug mode +fn warn(msg: &str) { + #[cfg(debug_assertions)] + warning("serialbridge:debug", msg); +} + +struct Args<'a>(Rhs<'a, 'a>); + +impl<'a> Args<'a> { + fn new(rhs: Rhs<'a, 'a>) -> Self { + Self(rhs) + } + + fn assert_params_max_len(&self, len: usize) -> SResult<()> { + if self.0.len() - 1 > len { + Err(Error::TooManyParameters) + } else { + Ok(()) + } + } + + fn get(&self, ix: usize, missing_err: Error) -> SResult<MxArray> { + Ok(self.0.get(ix).mexerr(missing_err)?.to_owned().to_owned()) + } } #[rustmex::entrypoint] fn serialbridge(lhs: Lhs, rhs: Rhs) -> rustmex::Result<()> { - if let Some(r) = lhs.get_mut(0) { - let res = TEST.read().unwrap().to_matlab(); - r.replace(res); - }; + // if let Some(r) = lhs.get_mut(0) { + // let res = TEST.read().unwrap().to_matlab(); + // r.replace(res); + // }; - let arg0 = rhs.first().mexerr(Error::MissingSerialMode)?; + let args = Args::new(rhs); // Get the mode argument ("Open", "Close", "Read", "Write") - let mode = get_mode(arg0)?; + let mode = get_mode(args.get(0, Error::MissingSerialMode)?)?; + + println!("Mode: {:?}", mode); // Dispatch to the appropriate function match mode { - Mode::Open => open_serial(lhs, rhs)?, - Mode::Close => {} + Mode::Open => open_serial(args)?, + Mode::Close => close_serial(args)?, Mode::Read => todo!(), Mode::Write => todo!(), } @@ -35,29 +66,48 @@ fn serialbridge(lhs: Lhs, rhs: Rhs) -> rustmex::Result<()> { Ok(()) } -fn get_mode(arg: &mxArray) -> Result<Mode, Error> { +fn get_mode(arg: MxArray) -> SResult<Mode> { CharArray::from_mx_array(arg)? .get_cstring() .into_string()? .parse() } -fn open_serial(lhs: Lhs, rhs: Rhs) -> Result<(), Error> { - let nw = *TEST.read().unwrap() + 1; - *TEST.write().unwrap() = nw; +fn open_serial(args: Args<'_>) -> SResult<()> { + args.assert_params_max_len(2)?; + + let port: String = args + .get(1, Error::MissingPortName)? + .convert() + .map_mexerr(|e| Error::InvalidPortName(Box::new(e)))?; + // Matlab defaults to f64 when inserting numbers, to improve UX we take a + // f64 and cast to a u32 + let arg2: f64 = args + .get(2, Error::MissingBaudrate)? + .convert() + .map_mexerr(|e| Error::InvalidBaudrate(Box::new(e)))?; + // Check for arg2 to resemble a baud rate (this type mismatch should be + // fixed later on) + if arg2 != arg2.floor() || !(0.0..=115_200.0).contains(&arg2) { + return Err(Error::InvalidBaudrate(Box::new(Error::InvalidMatlabType( + "do not use decimal units, use a number between 0 and 115200".into(), + )))); + } + let baudrate = arg2 as u32; - if let Some(r) = lhs.get_mut(0) { - let res = TEST.read().unwrap().to_matlab(); - r.replace(res); - }; + warn(&format!( + "Open serial port {} with baudrate {}", + port, baudrate + )); - // let port = rhs.get(1).mexerr(Error::MissingPortName)?; - // let baudrate = rhs.get(2).mexerr(Error::MissingBaudrate)?; + SERIAL.lock().unwrap().open(&port, baudrate)?; - // let port = CharArray::from_mx_array(port)?.get_cstring().into_string()?; - // let baudrate = CharArray::from_mx_array(baudrate)?.get_cstring().into_string()?; + Ok(()) +} - // todo!(); +fn close_serial(args: Args<'_>) -> SResult<()> { + args.assert_params_max_len(0)?; + SERIAL.lock().unwrap().close()?; Ok(()) } diff --git a/src/serial.rs b/src/serial.rs new file mode 100644 index 0000000000000000000000000000000000000000..454313b3eb6f7d08f5f7cfec979bbc08c651aff9 --- /dev/null +++ b/src/serial.rs @@ -0,0 +1,26 @@ +use std::sync::RwLock; + +use serialport::SerialPort; + +use crate::error::SResult; + +pub struct SerialManager { + serial: Option<RwLock<Box<dyn SerialPort>>>, +} + +impl SerialManager { + pub fn new() -> Self { + Self { serial: None } + } + + pub fn open(&mut self, port: &str, baudrate: u32) -> SResult<()> { + let port = serialport::new(port, baudrate).open()?; + self.serial.replace(RwLock::new(port)); + Ok(()) + } + + pub fn close(&mut self) -> SResult<()> { + self.serial.take(); + Ok(()) + } +} diff --git a/src/types.rs b/src/types.rs index 1e58747b4d8f930e76fc576defbcfb2265a33911..4862e6c8b243ef10ffd1977418b258e3a13748d1 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,22 +1,45 @@ -use std::{ - ffi::{CStr, CString}, - ops::Deref, - str::FromStr, -}; +use std::{ffi::CString, str::FromStr}; -use cstr::cstr; use rustmex::{ - convert::ToMatlab, - error, + char::CharArray, numeric::{Numeric, NumericArray}, - structs::{ScalarStruct, Struct}, MatlabClass, MxArray, }; -use crate::error::Error; +use crate::error::{Error, MapMexError, SResult}; + +pub trait MatlabType<T> { + fn convert(self) -> SResult<T>; +} + +impl MatlabType<CString> for MxArray { + fn convert(self) -> SResult<CString> { + Ok(CharArray::from_mx_array(self) + .mexerr(Error::InvalidMatlabType("use a string instead".into()))? + .get_cstring()) + } +} -#[derive(Debug, Clone)] -pub struct Serial(ScalarStruct<MxArray>); +impl MatlabType<String> for MxArray { + fn convert(self) -> SResult<String> { + let c: CString = self.convert()?; + Ok(c.into_string()?) + } +} + +impl MatlabType<f64> for MxArray { + fn convert(self) -> SResult<f64> { + let out = Numeric::<f64, _>::from_mx_array(self) + .mexerr(Error::InvalidMatlabType( + "use a numerical type instead".into(), + ))? + .data() + .first() + .mexerr(Error::Parse)? + .to_owned(); + Ok(out) + } +} #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Mode { @@ -39,67 +62,3 @@ impl FromStr for Mode { } } } - -// #[derive(Debug, Clone)] -// struct ByteString(MxArray); - -// impl From<&str> for ByteString { -// fn from(value: &str) -> Self { -// let raw = value.as_bytes(); -// Self( -// Numeric::new(raw.into(), &[1, raw.len()]) -// .expect("Failed to create byte string") -// .into_inner(), -// ) -// } -// } - -// impl ByteString { -// fn from_string(value: String) -> Self { -// value.as_str().into() -// } - -// fn into_string(self) -> rustmex::Result<String> { -// let value = Numeric::<u8, MxArray>::from_mx_array(self.0)?.data(); -// Ok(String::from_utf8(value.to_vec()).expect("Failed to convert byte string to string")) -// } -// } - -// impl Deref for ByteString { -// type Target = MxArray; - -// fn deref(&self) -> &Self::Target { -// &self.0 -// } -// } - -// impl Serial { -// pub fn new(device: String, baudrate: u32) -> Self { -// let device_f = cstr!("serialport"); -// let baud_f = cstr!("baudrate"); -// let mut s = Struct::new(&[1, 1], &[device_f, baud_f]) -// .into_scalar() -// .unwrap(); -// // let device = CString::new(device.into()).unwrap(); -// s.set(device_f, *ByteString::from_string(device)).unwrap(); -// s.set(baud_f, baudrate.to_matlab()).unwrap(); -// Serial(s) -// } - -// pub fn into_struct(self) -> ScalarStruct<MxArray> { -// self.0 -// } - -// pub fn from_struct(s: ScalarStruct<MxArray>) -> Self { -// Serial(s) -// } - -// pub fn device(&self) -> rustmex::Result<String> { -// let device_f = cstr!("serialport"); -// let a = self -// .0 -// .get(device_f) -// .map(|x| x.ok_or_else(|| error!("serialbridge:missing_input", "Device not found")))? -// .map(|x| ByteString(x.to_owned())); -// } -// }