Skip to content

Commit

Permalink
Implement WebSerial/wasm
Browse files Browse the repository at this point in the history
  • Loading branch information
ferris committed Oct 23, 2024
1 parent a93db85 commit b542aeb
Show file tree
Hide file tree
Showing 4 changed files with 304 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@
# by default to avoid race conditions (see
# https://github.com/rust-lang/cargo/issues/8430).
RUST_TEST_THREADS = "1"

[target.wasm32-unknown-unknown]
rustflags = ["--cfg=web_sys_unstable_apis"]
9 changes: 9 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ license = "MPL-2.0"
keywords = ["serial", "hardware", "system", "RS232"]
categories = ["hardware-support"]


[target.'cfg(target_family = "wasm")'.dependencies]
js-sys = { version = "0.3.72" }
wasm-bindgen = { version = "0.2.95" }
web-sys = { version = "0.3.72", features = ['Window', 'SerialPort', 'SerialOutputSignals', 'SerialOptions', 'ReadableStream', 'WritableStream', 'WritableStreamDefaultWriter', 'ReadableStreamDefaultReader', 'ReadableStreamByobReader', 'ReadableStreamGetReaderOptions', 'ReadableStreamReaderMode', 'FlowControlType', 'ParityType'] }
wasm-bindgen-futures = { version = "0.4.45" }
futures-executor = { version = "0.3" }
futures = { version = "0.3" }

[target."cfg(unix)".dependencies]
bitflags = "2.4.0"
nix = { version = "0.26", default-features = false, features = ["fs", "ioctl", "poll", "signal", "term"] }
Expand Down
5 changes: 5 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ mod windows;
#[cfg(windows)]
pub use windows::COMPort;

#[cfg(target_family = "wasm")]
mod wasm;
#[cfg(target_family = "wasm")]
pub use wasm::WebPort;

#[cfg(test)]
pub(crate) mod tests;

Expand Down
287 changes: 287 additions & 0 deletions src/wasm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
use std::str::FromStr;

use wasm_bindgen::{JsCast, UnwrapThrowExt};
use wasm_bindgen_futures::JsFuture;
use web_sys::{ReadableStreamByobReader, ReadableStreamGetReaderOptions, SerialOutputSignals};

use crate::{DataBits, FlowControl, Parity, SerialPort, StopBits};

/// Provides a blocking interface into the WebSerial api
#[derive(Debug)]
pub struct WebPort {
inner: web_sys::SerialPort,
name: String,

options: SerialPortOptions,
timeout: std::time::Duration,
}

#[derive(Debug, Clone)]
struct SerialPortOptions {
baud_rate: u32,
data_bits: DataBits,
flow_control: FlowControl,
parity: Parity,
stop_bits: StopBits,
buffer_size: u32,
}

impl Default for SerialPortOptions {
fn default() -> Self {
SerialPortOptions {
baud_rate: 9600,
data_bits: DataBits::Eight,
flow_control: FlowControl::None,
parity: Parity::None,
stop_bits: StopBits::One,
buffer_size: 255,
}
}
}

impl From<SerialPortOptions> for web_sys::SerialOptions {
fn from(value: SerialPortOptions) -> Self {
let options = web_sys::SerialOptions::new(value.baud_rate);
options.set_buffer_size(value.buffer_size);
options.set_data_bits(value.data_bits.into());
options.set_stop_bits(value.stop_bits.into());
options.set_flow_control(match value.flow_control {
FlowControl::None => web_sys::FlowControlType::None,
FlowControl::Software => web_sys::FlowControlType::None,
FlowControl::Hardware => web_sys::FlowControlType::Hardware,
});
options.set_parity(match value.parity {
Parity::None => web_sys::ParityType::None,
Parity::Odd => web_sys::ParityType::Odd,
Parity::Even => web_sys::ParityType::Even,
});
options
}
}

unsafe impl Send for WebPort {}

impl WebPort {
fn write_signals(&self, signals: SerialOutputSignals) {
futures_executor::block_on(async {
JsFuture::from(self.inner.set_signals_with_signals(&signals))
.await
.unwrap_throw();
});
}

fn reopen(&self) -> crate::Result<()> {
futures_executor::block_on(async {
JsFuture::from(self.inner.close()).await.map_err(|v| {
crate::Error::new(crate::ErrorKind::InvalidInput, v.as_string().unwrap_throw())
})?;
JsFuture::from(self.inner.open(&self.options.clone().into()))
.await
.map_err(|v| {
crate::Error::new(crate::ErrorKind::InvalidInput, v.as_string().unwrap_throw())
})?;
Ok(())
})
}
}

impl std::io::Write for WebPort {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let writable = self.inner.writable();
let writer = writable
.get_writer()
.map_err(|_| std::io::ErrorKind::Unsupported)?;

let writable_size = writer
.desired_size()
.unwrap_or_default()
.map(|v| v as usize)
.unwrap_or(255);

futures_executor::block_on(async {
let buffer = unsafe { js_sys::Uint8Array::view(&buf[..writable_size]) };
// FIXME: native promises are not cancelable and thus cannot not have a timeout, this needs to be done by storing the future for the next call instead

// This should instantly resolve, given desired_size; so we don't have a timeout
JsFuture::from(writer.write_with_chunk(&buffer))
.await
.unwrap_throw();
});
Ok(writable_size)
}

fn flush(&mut self) -> std::io::Result<()> {
let writable = self.inner.writable();
let writer = writable
.get_writer()
.map_err(|_| std::io::ErrorKind::Unsupported)?;

futures_executor::block_on(async {
JsFuture::from(writer.ready()).await.unwrap_throw();
});

Ok(())
}
}

impl std::io::Read for WebPort {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let readable = self.inner.readable();
let reader_options = ReadableStreamGetReaderOptions::new();
reader_options.set_mode(web_sys::ReadableStreamReaderMode::Byob);
let reader = readable.get_reader_with_options(&reader_options);
let reader = ReadableStreamByobReader::unchecked_from_js_ref(&reader);

let buffer = unsafe { js_sys::Uint8Array::view(buf) };
let result = futures_executor::block_on(async {
// FIXME: native promises are not cancelable and thus cannot not have a timeout, this needs to be done by storing the future for the next call instead
JsFuture::from(reader.read_with_array_buffer_view(&buffer))
.await
.unwrap_throw()
});

let result = js_sys::Object::unchecked_from_js(result);
let done =
js_sys::Reflect::get(&result, &js_sys::JsString::from_str("done").unwrap_throw())
.unwrap_throw();

if done.is_truthy() {
todo!("Stream closed")
}

let value =
js_sys::Reflect::get(&result, &js_sys::JsString::from_str("value").unwrap_throw())
.unwrap_throw();

let value = js_sys::Uint8Array::unchecked_from_js(value);
Ok(value.byte_length() as usize)
}
}

impl SerialPort for WebPort {
fn name(&self) -> Option<String> {
Some(self.name.clone())
}

fn baud_rate(&self) -> crate::Result<u32> {
Ok(self.options.baud_rate)
}

fn data_bits(&self) -> crate::Result<crate::DataBits> {
Ok(self.options.data_bits)
}

fn flow_control(&self) -> crate::Result<crate::FlowControl> {
Ok(self.options.flow_control)
}

fn parity(&self) -> crate::Result<crate::Parity> {
Ok(self.options.parity)
}

fn stop_bits(&self) -> crate::Result<crate::StopBits> {
Ok(self.options.stop_bits)
}

fn timeout(&self) -> std::time::Duration {
self.timeout.clone()
}

fn write_request_to_send(&mut self, level: bool) -> crate::Result<()> {
let signals = SerialOutputSignals::new();
signals.set_request_to_send(level);
self.write_signals(signals);
Ok(())
}

fn write_data_terminal_ready(&mut self, level: bool) -> crate::Result<()> {
let signals = SerialOutputSignals::new();
signals.set_data_terminal_ready(level);
self.write_signals(signals);
Ok(())
}

fn clear_break(&self) -> crate::Result<()> {
let signals = SerialOutputSignals::new();
signals.set_break(false);
self.write_signals(signals);
Ok(())
}

fn set_break(&self) -> crate::Result<()> {
let signals = SerialOutputSignals::new();
signals.set_break(true);
self.write_signals(signals);
Ok(())
}

fn set_timeout(&mut self, timeout: std::time::Duration) -> crate::Result<()> {
self.timeout = timeout;
Ok(())
}

fn set_baud_rate(&mut self, baud_rate: u32) -> crate::Result<()> {
self.options.baud_rate = baud_rate;
self.reopen()
}

fn set_data_bits(&mut self, data_bits: crate::DataBits) -> crate::Result<()> {
self.options.data_bits = data_bits;
self.reopen()
}

fn set_flow_control(&mut self, flow_control: crate::FlowControl) -> crate::Result<()> {
self.options.flow_control = flow_control;
self.reopen()
}

fn set_parity(&mut self, parity: crate::Parity) -> crate::Result<()> {
self.options.parity = parity;
self.reopen()
}

fn set_stop_bits(&mut self, stop_bits: crate::StopBits) -> crate::Result<()> {
self.options.stop_bits = stop_bits;
self.reopen()
}

fn read_clear_to_send(&mut self) -> crate::Result<bool> {
todo!()
}

fn read_data_set_ready(&mut self) -> crate::Result<bool> {
todo!()
}

fn read_ring_indicator(&mut self) -> crate::Result<bool> {
todo!()
}

fn read_carrier_detect(&mut self) -> crate::Result<bool> {
todo!()
}

// TODO: in oredr to implement *_to_read you'd either need to implement your own {Read,Writ}ableStream or greedily read using an interval poll
fn bytes_to_read(&self) -> crate::Result<u32> {
// Not exposed functionality
Ok(0)
}

fn bytes_to_write(&self) -> crate::Result<u32> {
// Not exposed functionality
Ok(0)
}

fn clear(&self, _buffer_to_clear: crate::ClearBuffer) -> crate::Result<()> {
// We don't store an internal buffer, but the easiest way to to close() & open()
self.reopen()
}

fn try_clone(&self) -> crate::Result<Box<dyn SerialPort>> {
// TODO: serial is clonable, but also is locked so not sure if we should
Err(crate::Error::new(
crate::ErrorKind::NoDevice,
"WebSerial device is not clonable",
))
}
}

0 comments on commit b542aeb

Please sign in to comment.