diff --git a/src/error.rs b/src/error.rs
index 2b27c3470d164c36be17eba3fa39c402eab8958b..bb3b7f9418a2a2ff0470291b53caa8805a361af6 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -54,6 +54,10 @@ pub enum Error {
     MissingPortName,
     #[error("Missing baudrate (third argument)")]
     MissingBaudrate,
+    #[error("Missing read amount (second argument)")]
+    MissingReadAmount,
+    #[error("Missing write data (second argument)")]
+    MissingWriteData,
     #[error("String contains invalid characters")]
     String(#[from] std::ffi::IntoStringError),
     #[error("Invalid Matlab type used, {0}")]
@@ -64,14 +68,24 @@ pub enum Error {
     InvalidPortName(Box<Self>),
     #[error("Invalid baud rate (3rd argument): {0}")]
     InvalidBaudrate(Box<Self>),
+    #[error("Invalid read amount (2nd argument): {0}")]
+    InvalidReadAmount(Box<Self>),
+    #[error("Invalid write data (2nd argument): {0}")]
+    InvalidWriteData(Box<Self>),
     #[error("Serial port error: {0}")]
     SerialPort(#[from] serialport::Error),
+    #[error("I/O error: {0}")]
+    IO(#[from] std::io::Error),
     #[error("Parse error")]
     Parse,
     #[error("Matlab error: {0}")]
     Matlab(#[from] rustmex::FromMatlabError<MxArray>),
     #[error("{0}")]
     Rustmex(#[from] rustmex::Error),
+    #[error("Return type cannot be assigned")]
+    ReturnType,
+    #[error("Serial port is not open")]
+    SerialNotOpen,
 }
 
 impl Error {
@@ -81,15 +95,22 @@ impl Error {
             Error::MissingSerialMode => "serialbridge:missing_input",
             Error::MissingPortName => "serialbridge:missing_input",
             Error::MissingBaudrate => "serialbridge:missing_input",
+            Error::MissingReadAmount => "serialbridge:missing_input",
+            Error::MissingWriteData => "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::InvalidReadAmount(_) => "serialbridge:invalid_input",
+            Error::InvalidWriteData(_) => "serialbridge:invalid_input",
             Error::SerialPort(_) => "serialbridge:serial_error",
+            Error::IO(_) => "serialbridge:serial_error",
             Error::Parse => "serialbridge:parse_error",
             Error::Matlab(_) => "serialbridge:matlab_error",
             Error::Rustmex(err) => err.id(),
+            Error::ReturnType => "serialbridge:invalid_output",
+            Error::SerialNotOpen => "serialbridge:serial_error",
         }
     }
 
diff --git a/src/lib.rs b/src/lib.rs
index fe8b0bf1a9b24c5e0a60ef2a0f4776610a0cc28f..02185c575ee7a6acdb88a90d6706c173bf6665a4 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -2,24 +2,25 @@ mod error;
 mod serial;
 mod types;
 
-use std::sync::Mutex;
+use std::sync::RwLock;
 
 use rustmex::{char::CharArray, prelude::*, warning, MatlabClass};
 
 use error::{Error, MapMexError, SResult};
-use types::{MatlabType, Mode};
+use types::{IntoMatlabType, IntoRustType, Mode};
 
 use crate::serial::SerialManager;
 
 lazy_static::lazy_static! {
-    static ref SERIAL: Mutex<SerialManager> = Mutex::new(SerialManager::new());
+    static ref SERIAL: RwLock<SerialManager> = RwLock::new(SerialManager::new());
 }
 
 /// this compiles only in debug mode
 #[cfg(debug_assertions)]
+#[macro_export]
 macro_rules! warn_debug {
     ($msg:literal, $($arg:expr),*) => {
-        warning("serialbridge:debug", format!($msg, $($arg),*));
+        rustmex::warning("serialbridge:debug", format!($msg, $($arg),*));
     };
 }
 
@@ -31,11 +32,7 @@ macro_rules! warn_debug {
 struct Args<'a>(Rhs<'a, 'a>);
 struct Output<'a>(Lhs<'a>);
 
-impl<'a> Args<'a> {
-    fn new(rhs: Rhs<'a, 'a>) -> Self {
-        Self(rhs)
-    }
-
+impl Args<'_> {
     fn assert_params_max_len(&self, len: usize) -> SResult<()> {
         if self.0.len() - 1 > len {
             Err(Error::TooManyParameters)
@@ -49,9 +46,20 @@ impl<'a> Args<'a> {
     }
 }
 
+impl Output<'_> {
+    fn set<T: IntoMatlabType>(&mut self, val: T) -> SResult<()> {
+        let Some(ret) = self.0.get_mut(0) else {
+            return Err(Error::ReturnType);
+        };
+
+        ret.replace(val.into_matlab()?);
+        Ok(())
+    }
+}
+
 #[rustmex::entrypoint]
 fn serialbridge(lhs: Lhs, rhs: Rhs) -> rustmex::Result<()> {
-    let args = Args::new(rhs);
+    let args = Args(rhs);
     let out = Output(lhs);
 
     // Get the mode argument ("Open", "Close", "Read", "Write")
@@ -82,13 +90,13 @@ fn open_serial(args: Args<'_>) -> SResult<()> {
 
     let port: String = args
         .get(1, Error::MissingPortName)?
-        .convert()
+        .into_rust()
         .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()
+        .into_rust()
         .map_mexerr(|e| Error::InvalidBaudrate(Box::new(e)))?;
     // Check for arg2 to resemble a baud rate (this type mismatch should be
     // fixed later on)
@@ -101,7 +109,7 @@ fn open_serial(args: Args<'_>) -> SResult<()> {
 
     warn_debug!("Open serial port {} with baudrate {}", port, baudrate);
 
-    SERIAL.lock().unwrap().open(&port, baudrate)?;
+    SERIAL.write().unwrap().open(&port, baudrate)?;
 
     Ok(())
 }
@@ -109,22 +117,45 @@ fn open_serial(args: Args<'_>) -> SResult<()> {
 fn close_serial(args: Args<'_>) -> SResult<()> {
     args.assert_params_max_len(0)?;
 
-    SERIAL.lock().unwrap().close()?;
+    SERIAL.write().unwrap().close()?;
     Ok(())
 }
 
-fn read_from_serial(outputs: Output<'_>, args: Args<'_>) -> SResult<()> {
+fn read_from_serial(mut outputs: Output<'_>, args: Args<'_>) -> SResult<()> {
     args.assert_params_max_len(1)?;
 
-    todo!();
+    let arg: f64 = args
+        .get(1, Error::MissingReadAmount)?
+        .into_rust()
+        .map_mexerr(|e| Error::InvalidReadAmount(Box::new(e)))?;
+    // Check for arg to resemble a unsigned integer (this type mismatch should be
+    // fixed later on)
+    if arg != arg.floor() || arg < 0.0 {
+        return Err(Error::InvalidReadAmount(Box::new(
+            Error::InvalidMatlabType("do not use decimal units, use a positive integer".into()),
+        )));
+    }
+    let n_doubles = arg as usize;
+
+    // Read n_doubles from the serial port
+    let bytes = SERIAL.read().unwrap().read_n_bytes(n_doubles)?;
+    let doubles = unsafe { std::slice::from_raw_parts(bytes.as_ptr() as *const f64, n_doubles) };
+    warn_debug!("Read {} bytes from serial port", n_doubles);
 
+    outputs.set(doubles.to_vec())?;
     Ok(())
 }
 
 fn write_to_serial(args: Args<'_>) -> SResult<()> {
     args.assert_params_max_len(1)?;
 
-    todo!();
+    let data: Vec<f64> = args
+        .get(1, Error::MissingWriteData)?
+        .into_rust()
+        .map_mexerr(|e| Error::InvalidWriteData(Box::new(e)))?;
 
+    let data: Vec<u8> = data.iter().flat_map(|&x| x.to_be_bytes()).collect();
+    SERIAL.read().unwrap().write_bytes(&data)?;
+    warn_debug!("Wrote {} bytes to serial port", data.len());
     Ok(())
 }
diff --git a/src/serial.rs b/src/serial.rs
index 454313b3eb6f7d08f5f7cfec979bbc08c651aff9..3365897a2c26e118f75d8960ee1c8afbe9d64e78 100644
--- a/src/serial.rs
+++ b/src/serial.rs
@@ -1,11 +1,14 @@
-use std::sync::RwLock;
+use std::sync::Mutex;
 
 use serialport::SerialPort;
 
-use crate::error::SResult;
+use crate::{
+    error::{Error, SResult},
+    warn_debug,
+};
 
 pub struct SerialManager {
-    serial: Option<RwLock<Box<dyn SerialPort>>>,
+    serial: Option<Mutex<Box<dyn SerialPort>>>,
 }
 
 impl SerialManager {
@@ -15,7 +18,7 @@ impl SerialManager {
 
     pub fn open(&mut self, port: &str, baudrate: u32) -> SResult<()> {
         let port = serialport::new(port, baudrate).open()?;
-        self.serial.replace(RwLock::new(port));
+        self.serial.replace(Mutex::new(port));
         Ok(())
     }
 
@@ -23,4 +26,27 @@ impl SerialManager {
         self.serial.take();
         Ok(())
     }
+
+    pub fn read_n_bytes(&self, n: usize) -> SResult<Vec<u8>> {
+        let mut port = self
+            .serial
+            .as_ref()
+            .ok_or(Error::SerialNotOpen)?
+            .lock()
+            .unwrap();
+        let mut buf = vec![0; n];
+        port.read_exact(&mut buf)?;
+        Ok(buf)
+    }
+
+    pub fn write_bytes(&self, data: &[u8]) -> SResult<()> {
+        let mut port = self
+            .serial
+            .as_ref()
+            .ok_or(Error::SerialNotOpen)?
+            .lock()
+            .unwrap();
+        port.write_all(data)?;
+        Ok(())
+    }
 }
diff --git a/src/types.rs b/src/types.rs
index 4862e6c8b243ef10ffd1977418b258e3a13748d1..7060416c2e8c7ff1b0a70ee1470818ee0a8c6543 100644
--- a/src/types.rs
+++ b/src/types.rs
@@ -1,4 +1,4 @@
-use std::{ffi::CString, str::FromStr};
+use std::{ffi::CString, ops::Deref, str::FromStr};
 
 use rustmex::{
     char::CharArray,
@@ -8,27 +8,27 @@ use rustmex::{
 
 use crate::error::{Error, MapMexError, SResult};
 
-pub trait MatlabType<T> {
-    fn convert(self) -> SResult<T>;
+pub trait IntoRustType<T> {
+    fn into_rust(self) -> SResult<T>;
 }
 
-impl MatlabType<CString> for MxArray {
-    fn convert(self) -> SResult<CString> {
+impl IntoRustType<CString> for MxArray {
+    fn into_rust(self) -> SResult<CString> {
         Ok(CharArray::from_mx_array(self)
             .mexerr(Error::InvalidMatlabType("use a string instead".into()))?
             .get_cstring())
     }
 }
 
-impl MatlabType<String> for MxArray {
-    fn convert(self) -> SResult<String> {
-        let c: CString = self.convert()?;
+impl IntoRustType<String> for MxArray {
+    fn into_rust(self) -> SResult<String> {
+        let c: CString = self.into_rust()?;
         Ok(c.into_string()?)
     }
 }
 
-impl MatlabType<f64> for MxArray {
-    fn convert(self) -> SResult<f64> {
+impl IntoRustType<f64> for MxArray {
+    fn into_rust(self) -> SResult<f64> {
         let out = Numeric::<f64, _>::from_mx_array(self)
             .mexerr(Error::InvalidMatlabType(
                 "use a numerical type instead".into(),
@@ -41,6 +41,32 @@ impl MatlabType<f64> for MxArray {
     }
 }
 
+impl IntoRustType<Vec<f64>> for MxArray {
+    fn into_rust(self) -> SResult<Vec<f64>> {
+        let out = Numeric::<f64, _>::from_mx_array(self)
+            .mexerr(Error::InvalidMatlabType(
+                "use a numerical type instead".into(),
+            ))?
+            .data()
+            .to_owned();
+        Ok(out)
+    }
+}
+
+pub trait IntoMatlabType {
+    fn into_matlab(self) -> SResult<MxArray>;
+}
+
+impl IntoMatlabType for Vec<f64> {
+    fn into_matlab(self) -> SResult<MxArray> {
+        let len = self.len();
+        Ok(Numeric::<f64, _>::new(self.into_boxed_slice(), &[len])
+            .unwrap()
+            .deref()
+            .to_owned())
+    }
+}
+
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub enum Mode {
     Open,