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()));
-// }
-// }