use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::time::Duration;
use gloo_timers::future::sleep;
use js_sys::{Function, Promise, Reflect};
use mbus_client::services::ClientServices;
use mbus_client::services::coil::Coils;
#[cfg(feature = "file-record")]
use mbus_client::services::file_record::SubRequest;
use mbus_core::data_unit::common::MAX_ADU_FRAME_LEN;
#[cfg(feature = "file-record")]
use mbus_core::data_unit::common::MAX_PDU_DATA_LEN;
use mbus_core::errors::MbusError;
#[cfg(feature = "diagnostics")]
use mbus_core::function_codes::public::DiagnosticSubFunction;
#[cfg(feature = "diagnostics")]
use mbus_core::models::diagnostic::{ObjectId, ReadDeviceIdCode};
use mbus_core::transport::{
BackoffStrategy, BaudRate, DataBits, JitterStrategy, ModbusConfig, ModbusSerialConfig, Parity,
SerialMode, UnitIdOrSlaveAddr,
};
use mbus_serial::{WasmAsciiTransport, WasmRtuTransport};
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::{JsFuture, spawn_local};
use super::app::{PendingHandle, PendingMap, WasmAppRouter};
const PIPELINE: usize = 1;
enum WasmRuntimeSerialTransport {
Rtu(WasmRtuTransport),
Ascii(WasmAsciiTransport),
}
impl WasmRuntimeSerialTransport {
fn new(mode: SerialMode) -> Self {
match mode {
SerialMode::Rtu => Self::Rtu(WasmRtuTransport::new()),
SerialMode::Ascii => Self::Ascii(WasmAsciiTransport::new()),
}
}
fn attach_port(&mut self, port: JsValue) {
match self {
Self::Rtu(transport) => transport.attach_port(port),
Self::Ascii(transport) => transport.attach_port(port),
}
}
}
impl mbus_core::transport::Transport for WasmRuntimeSerialTransport {
type Error = mbus_core::transport::TransportError;
const SUPPORTS_BROADCAST_WRITES: bool = true;
const TRANSPORT_TYPE: mbus_core::transport::TransportType =
mbus_core::transport::TransportType::CustomSerial(SerialMode::Rtu);
fn connect(&mut self, config: &ModbusConfig) -> Result<(), Self::Error> {
match self {
Self::Rtu(transport) => transport.connect(config),
Self::Ascii(transport) => transport.connect(config),
}
}
fn disconnect(&mut self) -> Result<(), Self::Error> {
match self {
Self::Rtu(transport) => transport.disconnect(),
Self::Ascii(transport) => transport.disconnect(),
}
}
fn send(&mut self, adu: &[u8]) -> Result<(), Self::Error> {
match self {
Self::Rtu(transport) => transport.send(adu),
Self::Ascii(transport) => transport.send(adu),
}
}
fn recv(&mut self) -> Result<heapless::Vec<u8, MAX_ADU_FRAME_LEN>, Self::Error> {
match self {
Self::Rtu(transport) => transport.recv(),
Self::Ascii(transport) => transport.recv(),
}
}
fn is_connected(&self) -> bool {
match self {
Self::Rtu(transport) => transport.is_connected(),
Self::Ascii(transport) => transport.is_connected(),
}
}
}
type Inner = ClientServices<WasmRuntimeSerialTransport, WasmAppRouter, PIPELINE>;
#[wasm_bindgen]
pub struct WasmSerialPortHandle {
port: JsValue,
}
#[wasm_bindgen]
impl WasmSerialPortHandle {
pub fn is_valid(&self) -> bool {
!self.port.is_null() && !self.port.is_undefined()
}
}
impl WasmSerialPortHandle {
fn clone_port(&self) -> JsValue {
self.port.clone()
}
}
impl WasmSerialPortHandle {
#[doc(hidden)]
pub fn new_for_testing(port: JsValue) -> Self {
WasmSerialPortHandle { port }
}
}
#[wasm_bindgen]
pub async fn request_serial_port() -> Result<WasmSerialPortHandle, JsValue> {
let global = js_sys::global();
let navigator = Reflect::get(&global, &JsValue::from_str("navigator"))?;
let serial = Reflect::get(&navigator, &JsValue::from_str("serial"))?;
if serial.is_null() || serial.is_undefined() {
return Err(JsValue::from_str(
"Web Serial API unavailable. Use a Chromium-based browser over HTTPS/localhost.",
));
}
let request_port = Reflect::get(&serial, &JsValue::from_str("requestPort"))?
.dyn_into::<Function>()
.map_err(|_| JsValue::from_str("navigator.serial.requestPort is not callable"))?;
let promise = request_port
.call0(&serial)?
.dyn_into::<Promise>()
.map_err(|_| JsValue::from_str("requestPort did not return a Promise"))?;
let port = JsFuture::from(promise).await?;
Ok(WasmSerialPortHandle { port })
}
#[wasm_bindgen]
pub struct WasmSerialModbusClient {
inner: Rc<RefCell<Inner>>,
pending: PendingMap,
unit_id: u8,
next_txn: u16,
}
#[wasm_bindgen]
impl WasmSerialModbusClient {
#[wasm_bindgen(constructor)]
pub fn new(
port_handle: &WasmSerialPortHandle,
unit_id: u8,
mode: &str,
baud_rate: u32,
data_bits: u8,
stop_bits: u8,
parity: &str,
response_timeout_ms: u32,
retry_attempts: u8,
tick_interval_ms: u32,
) -> Result<WasmSerialModbusClient, JsValue> {
let serial_mode = match mode.to_ascii_lowercase().as_str() {
"rtu" => SerialMode::Rtu,
"ascii" => SerialMode::Ascii,
_ => return Err(JsValue::from_str("mode must be 'rtu' or 'ascii'")),
};
let parity_cfg = match parity.to_ascii_lowercase().as_str() {
"none" => Parity::None,
"even" => Parity::Even,
"odd" => Parity::Odd,
_ => return Err(JsValue::from_str("parity must be 'none', 'even', or 'odd'")),
};
let data_bits_cfg = match data_bits {
5 => DataBits::Five,
6 => DataBits::Six,
7 => DataBits::Seven,
8 => DataBits::Eight,
_ => return Err(JsValue::from_str("data_bits must be one of 5, 6, 7, or 8")),
};
let mut transport = WasmRuntimeSerialTransport::new(serial_mode);
transport.attach_port(port_handle.clone_port());
let pending: PendingMap = Rc::new(RefCell::new(HashMap::new()));
let app = WasmAppRouter::new(pending.clone());
let mut port_path = heapless::String::new();
port_path
.push_str("web-serial")
.map_err(|_| JsValue::from_str("failed to build serial port_path"))?;
let config = ModbusConfig::Serial(ModbusSerialConfig {
port_path,
mode: serial_mode,
baud_rate: match baud_rate {
9600 => BaudRate::Baud9600,
19200 => BaudRate::Baud19200,
_ => BaudRate::Custom(baud_rate),
},
data_bits: data_bits_cfg,
stop_bits,
parity: parity_cfg,
response_timeout_ms,
retry_attempts,
retry_backoff_strategy: BackoffStrategy::Immediate,
retry_jitter_strategy: JitterStrategy::None,
retry_random_fn: None,
});
let mut inner_client = ClientServices::new(transport, app, config)
.map_err(|e| JsValue::from_str(&format!("{:?}", e)))?;
let _ = inner_client.connect();
let inner = Rc::new(RefCell::new(inner_client));
let weak = Rc::downgrade(&inner);
let tick_ms = tick_interval_ms as u64;
let idle_ms = core::cmp::max(50, tick_ms.saturating_mul(5));
spawn_local(async move {
loop {
match weak.upgrade() {
Some(rc) => {
let should_poll = {
let client = rc.borrow();
client.is_connected() && client.has_pending_requests()
};
if should_poll {
rc.borrow_mut().poll();
sleep(Duration::from_millis(tick_ms)).await;
} else {
sleep(Duration::from_millis(idle_ms)).await;
}
continue;
}
None => break,
}
}
});
Ok(WasmSerialModbusClient {
inner,
pending,
unit_id,
next_txn: 1,
})
}
pub fn is_connected(&self) -> bool {
self.inner.borrow().is_connected()
}
pub fn has_pending_requests(&self) -> bool {
self.inner.borrow().has_pending_requests()
}
pub fn reconnect(&mut self) -> bool {
for (_, handle) in self.pending.borrow_mut().drain() {
let _ = handle
.reject
.call1(&JsValue::NULL, &JsValue::from_str("ConnectionLost"));
}
self.inner.borrow_mut().reconnect().is_ok()
}
pub fn read_coils(&mut self, address: u16, quantity: u16) -> Promise {
let txn_id = self.alloc_txn();
let (promise, resolve, reject) = make_promise();
self.pending
.borrow_mut()
.insert(txn_id, PendingHandle { resolve, reject });
let unit_addr = UnitIdOrSlaveAddr::new(self.unit_id).unwrap_or_default();
let result = self
.inner
.borrow_mut()
.coils()
.read_multiple_coils(txn_id, unit_addr, address, quantity);
if let Err(e) = result {
self.reject_immediate(txn_id, e);
}
promise
}
pub fn read_holding_registers(&mut self, address: u16, quantity: u16) -> Promise {
let txn_id = self.alloc_txn();
let (promise, resolve, reject) = make_promise();
self.pending
.borrow_mut()
.insert(txn_id, PendingHandle { resolve, reject });
let unit_addr = UnitIdOrSlaveAddr::new(self.unit_id).unwrap_or_default();
let result = self
.inner
.borrow_mut()
.registers()
.read_holding_registers(txn_id, unit_addr, address, quantity);
if let Err(e) = result {
self.reject_immediate(txn_id, e);
}
promise
}
pub fn read_input_registers(&mut self, address: u16, quantity: u16) -> Promise {
let txn_id = self.alloc_txn();
let (promise, resolve, reject) = make_promise();
self.pending
.borrow_mut()
.insert(txn_id, PendingHandle { resolve, reject });
let unit_addr = UnitIdOrSlaveAddr::new(self.unit_id).unwrap_or_default();
let result = self
.inner
.borrow_mut()
.registers()
.read_input_registers(txn_id, unit_addr, address, quantity);
if let Err(e) = result {
self.reject_immediate(txn_id, e);
}
promise
}
pub fn write_single_register(&mut self, address: u16, value: u16) -> Promise {
let txn_id = self.alloc_txn();
let (promise, resolve, reject) = make_promise();
self.pending
.borrow_mut()
.insert(txn_id, PendingHandle { resolve, reject });
let unit_addr = UnitIdOrSlaveAddr::new(self.unit_id).unwrap_or_default();
let result = self
.inner
.borrow_mut()
.registers()
.write_single_register(txn_id, unit_addr, address, value);
if let Err(e) = result {
self.reject_immediate(txn_id, e);
}
promise
}
pub fn write_single_coil(&mut self, address: u16, value: bool) -> Promise {
let txn_id = self.alloc_txn();
let (promise, resolve, reject) = make_promise();
self.pending
.borrow_mut()
.insert(txn_id, PendingHandle { resolve, reject });
let unit_addr = UnitIdOrSlaveAddr::new(self.unit_id).unwrap_or_default();
let result = self
.inner
.borrow_mut()
.coils()
.write_single_coil(txn_id, unit_addr, address, value);
if let Err(e) = result {
self.reject_immediate(txn_id, e);
}
promise
}
pub fn write_multiple_registers(
&mut self,
address: u16,
quantity: u16,
values: &[u16],
) -> Promise {
let txn_id = self.alloc_txn();
let (promise, resolve, reject) = make_promise();
self.pending
.borrow_mut()
.insert(txn_id, PendingHandle { resolve, reject });
let unit_addr = UnitIdOrSlaveAddr::new(self.unit_id).unwrap_or_default();
let result = self
.inner
.borrow_mut()
.registers()
.write_multiple_registers(txn_id, unit_addr, address, quantity, values);
if let Err(e) = result {
self.reject_immediate(txn_id, e);
}
promise
}
pub fn read_discrete_inputs(&mut self, address: u16, quantity: u16) -> Promise {
let txn_id = self.alloc_txn();
let (promise, resolve, reject) = make_promise();
self.pending
.borrow_mut()
.insert(txn_id, PendingHandle { resolve, reject });
let unit_addr = UnitIdOrSlaveAddr::new(self.unit_id).unwrap_or_default();
let result = self
.inner
.borrow_mut()
.discrete_inputs()
.read_discrete_inputs(txn_id, unit_addr, address, quantity);
if let Err(e) = result {
self.reject_immediate(txn_id, e);
}
promise
}
}
#[wasm_bindgen]
impl WasmSerialModbusClient {
pub fn read_single_coil(&mut self, address: u16) -> Promise {
let txn_id = self.alloc_txn();
let (promise, resolve, reject) = make_promise();
self.pending
.borrow_mut()
.insert(txn_id, PendingHandle { resolve, reject });
let unit_addr = UnitIdOrSlaveAddr::new(self.unit_id).unwrap_or_default();
let result = self
.inner
.borrow_mut()
.coils()
.read_single_coil(txn_id, unit_addr, address);
if let Err(e) = result {
self.reject_immediate(txn_id, e);
}
promise
}
pub fn write_multiple_coils(
&mut self,
address: u16,
quantity: u16,
packed_bytes: &[u8],
) -> Promise {
let txn_id = self.alloc_txn();
let (promise, resolve, reject) = make_promise();
self.pending
.borrow_mut()
.insert(txn_id, PendingHandle { resolve, reject });
let unit_addr = UnitIdOrSlaveAddr::new(self.unit_id).unwrap_or_default();
let coils_result =
Coils::new(address, quantity).and_then(|c| c.with_values(packed_bytes, quantity));
let result = match coils_result {
Ok(coils) => self
.inner
.borrow_mut()
.coils()
.write_multiple_coils(txn_id, unit_addr, address, &coils),
Err(e) => Err(e),
};
if let Err(e) = result {
self.reject_immediate(txn_id, e);
}
promise
}
pub fn read_single_holding_register(&mut self, address: u16) -> Promise {
let txn_id = self.alloc_txn();
let (promise, resolve, reject) = make_promise();
self.pending
.borrow_mut()
.insert(txn_id, PendingHandle { resolve, reject });
let unit_addr = UnitIdOrSlaveAddr::new(self.unit_id).unwrap_or_default();
let result = self
.inner
.borrow_mut()
.registers()
.read_single_holding_register(txn_id, unit_addr, address);
if let Err(e) = result {
self.reject_immediate(txn_id, e);
}
promise
}
pub fn read_single_input_register(&mut self, address: u16) -> Promise {
let txn_id = self.alloc_txn();
let (promise, resolve, reject) = make_promise();
self.pending
.borrow_mut()
.insert(txn_id, PendingHandle { resolve, reject });
let unit_addr = UnitIdOrSlaveAddr::new(self.unit_id).unwrap_or_default();
let result = self
.inner
.borrow_mut()
.registers()
.read_single_input_register(txn_id, unit_addr, address);
if let Err(e) = result {
self.reject_immediate(txn_id, e);
}
promise
}
pub fn read_write_multiple_registers(
&mut self,
read_address: u16,
read_quantity: u16,
write_address: u16,
_write_quantity: u16,
values: &[u16],
) -> Promise {
let txn_id = self.alloc_txn();
let (promise, resolve, reject) = make_promise();
self.pending
.borrow_mut()
.insert(txn_id, PendingHandle { resolve, reject });
let unit_addr = UnitIdOrSlaveAddr::new(self.unit_id).unwrap_or_default();
let result = self
.inner
.borrow_mut()
.registers()
.read_write_multiple_registers(
txn_id,
unit_addr,
read_address,
read_quantity,
write_address,
values,
);
if let Err(e) = result {
self.reject_immediate(txn_id, e);
}
promise
}
pub fn mask_write_register(&mut self, address: u16, and_mask: u16, or_mask: u16) -> Promise {
let txn_id = self.alloc_txn();
let (promise, resolve, reject) = make_promise();
self.pending
.borrow_mut()
.insert(txn_id, PendingHandle { resolve, reject });
let unit_addr = UnitIdOrSlaveAddr::new(self.unit_id).unwrap_or_default();
let result = self
.inner
.borrow_mut()
.registers()
.mask_write_register(txn_id, unit_addr, address, and_mask, or_mask);
if let Err(e) = result {
self.reject_immediate(txn_id, e);
}
promise
}
pub fn read_single_discrete_input(&mut self, address: u16) -> Promise {
let txn_id = self.alloc_txn();
let (promise, resolve, reject) = make_promise();
self.pending
.borrow_mut()
.insert(txn_id, PendingHandle { resolve, reject });
let unit_addr = UnitIdOrSlaveAddr::new(self.unit_id).unwrap_or_default();
let result = self
.inner
.borrow_mut()
.discrete_inputs()
.read_single_discrete_input(txn_id, unit_addr, address);
if let Err(e) = result {
self.reject_immediate(txn_id, e);
}
promise
}
#[cfg(feature = "fifo")]
pub fn read_fifo_queue(&mut self, address: u16) -> Promise {
let txn_id = self.alloc_txn();
let (promise, resolve, reject) = make_promise();
self.pending
.borrow_mut()
.insert(txn_id, PendingHandle { resolve, reject });
let unit_addr = UnitIdOrSlaveAddr::new(self.unit_id).unwrap_or_default();
let result = self
.inner
.borrow_mut()
.fifo()
.read_fifo_queue(txn_id, unit_addr, address);
if let Err(e) = result {
self.reject_immediate(txn_id, e);
}
promise
}
#[cfg(feature = "file-record")]
pub fn read_file_record(
&mut self,
file_number: u16,
record_number: u16,
record_length: u16,
) -> Promise {
let txn_id = self.alloc_txn();
let (promise, resolve, reject) = make_promise();
self.pending
.borrow_mut()
.insert(txn_id, PendingHandle { resolve, reject });
let unit_addr = UnitIdOrSlaveAddr::new(self.unit_id).unwrap_or_default();
let mut sub_req = SubRequest::new();
let result = sub_req
.add_read_sub_request(file_number, record_number, record_length)
.and_then(|_| {
self.inner
.borrow_mut()
.file_records()
.read_file_record(txn_id, unit_addr, &sub_req)
});
if let Err(e) = result {
self.reject_immediate(txn_id, e);
}
promise
}
#[cfg(feature = "file-record")]
pub fn write_file_record(
&mut self,
file_number: u16,
record_number: u16,
values: &[u16],
) -> Promise {
let txn_id = self.alloc_txn();
let (promise, resolve, reject) = make_promise();
self.pending
.borrow_mut()
.insert(txn_id, PendingHandle { resolve, reject });
let unit_addr = UnitIdOrSlaveAddr::new(self.unit_id).unwrap_or_default();
let record_length = values.len() as u16;
let mut hv = heapless::Vec::<u16, MAX_PDU_DATA_LEN>::new();
for &v in values {
if hv.push(v).is_err() {
self.reject_immediate(txn_id, MbusError::BufferTooSmall);
return promise;
}
}
let mut sub_req = SubRequest::new();
let result = sub_req
.add_write_sub_request(file_number, record_number, record_length, hv)
.and_then(|_| {
self.inner
.borrow_mut()
.file_records()
.write_file_record(txn_id, unit_addr, &sub_req)
});
if let Err(e) = result {
self.reject_immediate(txn_id, e);
}
promise
}
#[cfg(feature = "diagnostics")]
pub fn read_exception_status(&mut self) -> Promise {
let txn_id = self.alloc_txn();
let (promise, resolve, reject) = make_promise();
self.pending
.borrow_mut()
.insert(txn_id, PendingHandle { resolve, reject });
let unit_addr = UnitIdOrSlaveAddr::new(self.unit_id).unwrap_or_default();
let result = self
.inner
.borrow_mut()
.diagnostic()
.read_exception_status(txn_id, unit_addr);
if let Err(e) = result {
self.reject_immediate(txn_id, e);
}
promise
}
#[cfg(feature = "diagnostics")]
pub fn diagnostics(&mut self, sub_function: u16, data: &[u16]) -> Promise {
let txn_id = self.alloc_txn();
let (promise, resolve, reject) = make_promise();
self.pending
.borrow_mut()
.insert(txn_id, PendingHandle { resolve, reject });
let unit_addr = UnitIdOrSlaveAddr::new(self.unit_id).unwrap_or_default();
let result = DiagnosticSubFunction::try_from(sub_function)
.map_err(|_| MbusError::ReservedSubFunction(sub_function))
.and_then(|sf| {
self.inner
.borrow_mut()
.diagnostic()
.diagnostics(txn_id, unit_addr, sf, data)
});
if let Err(e) = result {
self.reject_immediate(txn_id, e);
}
promise
}
#[cfg(feature = "diagnostics")]
pub fn get_comm_event_counter(&mut self) -> Promise {
let txn_id = self.alloc_txn();
let (promise, resolve, reject) = make_promise();
self.pending
.borrow_mut()
.insert(txn_id, PendingHandle { resolve, reject });
let unit_addr = UnitIdOrSlaveAddr::new(self.unit_id).unwrap_or_default();
let result = self
.inner
.borrow_mut()
.diagnostic()
.get_comm_event_counter(txn_id, unit_addr);
if let Err(e) = result {
self.reject_immediate(txn_id, e);
}
promise
}
#[cfg(feature = "diagnostics")]
pub fn get_comm_event_log(&mut self) -> Promise {
let txn_id = self.alloc_txn();
let (promise, resolve, reject) = make_promise();
self.pending
.borrow_mut()
.insert(txn_id, PendingHandle { resolve, reject });
let unit_addr = UnitIdOrSlaveAddr::new(self.unit_id).unwrap_or_default();
let result = self
.inner
.borrow_mut()
.diagnostic()
.get_comm_event_log(txn_id, unit_addr);
if let Err(e) = result {
self.reject_immediate(txn_id, e);
}
promise
}
#[cfg(feature = "diagnostics")]
pub fn report_server_id(&mut self) -> Promise {
let txn_id = self.alloc_txn();
let (promise, resolve, reject) = make_promise();
self.pending
.borrow_mut()
.insert(txn_id, PendingHandle { resolve, reject });
let unit_addr = UnitIdOrSlaveAddr::new(self.unit_id).unwrap_or_default();
let result = self
.inner
.borrow_mut()
.diagnostic()
.report_server_id(txn_id, unit_addr);
if let Err(e) = result {
self.reject_immediate(txn_id, e);
}
promise
}
#[cfg(feature = "diagnostics")]
pub fn read_device_identification(
&mut self,
read_device_id_code: u8,
object_id: u8,
) -> Promise {
let txn_id = self.alloc_txn();
let (promise, resolve, reject) = make_promise();
self.pending
.borrow_mut()
.insert(txn_id, PendingHandle { resolve, reject });
let unit_addr = UnitIdOrSlaveAddr::new(self.unit_id).unwrap_or_default();
let result = ReadDeviceIdCode::try_from(read_device_id_code)
.map_err(|_| MbusError::InvalidDeviceIdCode)
.and_then(|code| {
self.inner
.borrow_mut()
.diagnostic()
.read_device_identification(txn_id, unit_addr, code, ObjectId::from(object_id))
});
if let Err(e) = result {
self.reject_immediate(txn_id, e);
}
promise
}
}
impl WasmSerialModbusClient {
fn alloc_txn(&mut self) -> u16 {
let id = self.next_txn;
self.next_txn = self.next_txn.wrapping_add(1).max(1);
id
}
fn reject_immediate(&self, txn_id: u16, error: MbusError) {
if let Some(handle) = self.pending.borrow_mut().remove(&txn_id) {
let _ = handle
.reject
.call1(&JsValue::NULL, &JsValue::from_str(&format!("{:?}", error)));
}
}
}
fn make_promise() -> (Promise, Function, Function) {
let resolve_holder: Rc<RefCell<Option<Function>>> = Rc::new(RefCell::new(None));
let reject_holder: Rc<RefCell<Option<Function>>> = Rc::new(RefCell::new(None));
let r = resolve_holder.clone();
let rj = reject_holder.clone();
let promise = Promise::new(&mut move |res, rej| {
*r.borrow_mut() = Some(res);
*rj.borrow_mut() = Some(rej);
});
let resolve = resolve_holder.borrow_mut().take().unwrap();
let reject = reject_holder.borrow_mut().take().unwrap();
(promise, resolve, reject)
}