# mbus-ffi WASM/JS, Native C/C++ FFI, **Python**, **.NET (C#)**, **Go**, and **Node.js** bindings for the `modbus-rs` stack. --- ## Python Bindings (`modbus-rs` on PyPI) The `python` feature compiles `mbus-ffi` into a Python extension module via [PyO3](https://pyo3.rs) and [Maturin](https://maturin.rs). PyPI package: **`modbus-rs`** · Import name: `modbus_rs` ### Installation ```bash pip install modbus-rs ``` ### Quick Start — Synchronous TCP client ```python import modbus_rs with modbus_rs.TcpTransport.connect("192.168.1.10", port=502) as transport: client = transport.create_client(unit_id=1) regs = client.read_holding_registers(0, 10) print(regs) # [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] client.write_coil(0, True) coils = client.read_coils(0, 8) print(coils) # [True, False, ...] ``` ### Quick Start — Asyncio TCP client ```python import asyncio import modbus_rs async def main(): async with await modbus_rs.AsyncTcpTransport.connect("192.168.1.10") as transport: client = transport.create_client(unit_id=1) regs = await client.read_holding_registers(0, 10) print(regs) asyncio.run(main()) ``` ### Quick Start — Serial client (RTU) ```python import modbus_rs with modbus_rs.RtuTransport.open("/dev/ttyUSB0", baud_rate=9600) as transport: client = transport.create_client(unit_id=1) regs = client.read_holding_registers(0, 5) print(regs) ``` ### Quick Start — TCP server ```python import asyncio import modbus_rs class MyApp(modbus_rs.ModbusApp): def handle_read_holding_registers(self, address, count): return [address + i for i in range(count)] def handle_write_register(self, address, value): pass # accept silently async def main(): async with modbus_rs.AsyncTcpServer("0.0.0.0", MyApp(), port=5020, unit_id=1) as srv: await srv.serve_forever() asyncio.run(main()) ``` ### Exception hierarchy ``` ModbusError ├── ModbusTimeout ├── ModbusConnectionError ├── ModbusProtocolError │ └── ModbusDeviceException └── ModbusConfigError ``` ### Building from source Requires a Rust toolchain and [Maturin](https://maturin.rs) (`pip install maturin`). ```bash # install into an active venv (editable / dev mode) cd mbus-ffi && maturin develop --features python-full # build a release wheel cd mbus-ffi && maturin build --release --features python-full ``` ### Running the Python test suite ```bash pip install pytest pytest-asyncio source .venv/bin/activate pytest mbus-ffi/tests/python/ -v ``` ### Modbus TCP Gateway (`python-gateway` feature) Enable the optional `python-gateway` feature to expose `TcpGateway` (sync) and `AsyncTcpGateway` (asyncio) classes that bridge an upstream Modbus TCP listener to one or more downstream Modbus TCP servers. ```bash cd mbus-ffi && maturin develop --features python-full ``` Minimal sync example: ```python import modbus_rs gw = modbus_rs.TcpGateway("0.0.0.0:5020") ch = gw.add_tcp_downstream("192.168.1.10", 502) gw.add_unit_route(unit=1, channel=ch) gw.serve_forever() # call gw.stop() from another thread to exit ``` Runnable demos: [`python/examples/python_gateway/sync_demo.py`](python/examples/python_gateway/sync_demo.py) and [`python/examples/python_gateway/async_demo.py`](python/examples/python_gateway/async_demo.py). > The `event_handler=GatewayEventHandler()` constructor argument can be passed > to receive telemetry and lifecycle events (such as forwarding, responses, timeouts, > routing misses, and disconnects) from the underlying async gateway server. --- ## Position In Workspace `mbus-ffi` is an implementation wrapper crate inside this workspace. It encapsulates the core state machines of the `modbus-rs` stack, mapping them natively across two distinct abstraction boundaries: 1. **Web Run-times (WASM)** via Javascript Promises. 2. **Native Run-times (C/C++)** via opaque pointers, static client ID pools, and dependency-injected function callbacks. --- ## Native C/C++ Bindings (FFI) The native FFI is designed specifically for **Strict `no_std`** and embedded use cases: - **Zero Heap Allocations**: The FFI path absolutely avoids `alloc` (No `Box`, `Vec`, or dynamic dispatch). - **Static Client Pool**: All Modbus clients allocated for native FFI exist in a thread-safe static pool using an ID-based system (`MbusClientId`), preventing raw pointers from leaking across boundaries to easily integrate with memory-unsafe languages. - **Zero OS dependencies**: TCP/Serial abstractions are stripped out native compilation. The host application performs all network sockets, timer integrations, and byte transmissions by sending them upward into the Modbus stack via `MbusTransportCallbacks`. - **`panic=abort`**: Automatically detects `no_std` execution properties gracefully through `build.rs`. ### Pool Configuration Pool sizing is determined strictly at compile time. By default, the system provisions exactly `1` slot. To increase maximum clients, set both environment variables explicitly: ```bash MBUS_MAX_TCP_CLIENTS=10 MBUS_MAX_SERIAL_CLIENTS=10 cargo build -p mbus-ffi --features full ``` - `MBUS_MAX_TCP_CLIENTS`: valid range `1..=255` - `MBUS_MAX_SERIAL_CLIENTS`: valid range `1..=255` ### Build & Link `mbus-ffi` supports compiling directly to shared (`.so`/`.dylib`) and static (`.a`) libraries: ```bash cargo build --release -p mbus-ffi --features full ``` *Note: Even in strict `no_std` environments, standard LLVM targets like `target/debug` on mac/linux will naturally map underlying memory routines (`memcpy`, `memmove`) strictly via system libc. When targeting explicit embedded system triples like `thumbv7em-none-eabihf`, compiler built-ins will resolve them.* ### Automatic C Header Generation We use `cbindgen` to emit the public C headers from the current Rust API surface. Client header: ```bash cbindgen --config mbus-ffi/cbindgen_client.toml --crate mbus-ffi --output target/mbus-ffi/include/modbus_rs_client.h ``` Server header: ```bash cbindgen --config mbus-ffi/cbindgen_server.toml --crate mbus-ffi --output target/mbus-ffi/include/modbus_rs_server.h ``` The workspace also provides an xtask helper to generate the client header, build the release FFI library, and bundle them together: ```bash # Default: bundles to target/mbus-ffi/ cargo run -p xtask -- gen-client-lib # Custom SDK path (creates include/ and library/) cargo run -p xtask -- gen-client-lib --out-dir /path/to/my_sdk ``` ### Header / Feature Compatibility `modbus_rs_client.h` is generated from the Rust API shape of the enabled feature set. The generated header is intended for native C builds with: ```bash --features full ``` For native server builds, `modbus_rs_server.h` is generated from the `c-server` API surface, while YAML-driven server apps additionally emit `target/mbus-ffi/include/mbus_server_app.h` from the device config. ### C API Quick Start (Transport Polling) Instead of passing system sockets, you attach your exact runtime logic using POSIX or embedded UART controls directly via `MbusTransportCallbacks`: ```c #include "modbus_rs_client.h" // 1. Setup specific connection rules struct MbusTcpConfig config = {0}; config.host = "192.168.1.10"; config.port = 502; // ... (timeouts/retries) // 2. Setup your OS networking functions struct MbusTransportCallbacks transport = {0}; transport.userdata = &my_posix_socket_context; transport.on_connect = my_os_connect; transport.on_send = my_os_send; transport.on_recv = my_os_recv; // ... // 3. Setup Response callbacks struct MbusCallbacks app_callbacks = {0}; app_callbacks.on_read_coils = my_app_read_coils; // ... MbusClientId client_id = mbus_tcp_client_new(&config, &transport, &app_callbacks); // Request the connection internally mbus_tcp_connect(client_id); mbus_tcp_read_coils(client_id, 42 /* txn_id */, 1 /* unit_id */, 0 /* address */, 10 /* quantity */); // Must be continuously ticked within your device's task loop while(1) { mbus_tcp_poll(client_id); } ``` *For a full operational POSIX socket example and a self-contained serial PTY smoke path, view `mbus-ffi/examples/c_client_demo/main.c`.* ### C Smoke Example Build the client smoke demo with xtask: ```bash cargo run -p xtask -- build-c-demo --demo c_client_demo ``` This configures and builds the CMake target for `mbus-ffi/examples/c_client_demo`, then runs its registered CTest cases. The smoke binary also supports manual execution: ```bash cd mbus-ffi/examples/c_client_demo/build ./c_smoke_test --serial-pty ./c_smoke_test --tcp 127.0.0.1 502 ``` The `--serial-pty` mode is self-contained and does not require hardware. It creates a pseudo-terminal pair, opens the slave side through the FFI serial client, and serves a single RTU read-coils response from the master side so CI can exercise the serial path deterministically. ### Modbus TCP Gateway (`c-gateway` feature) The `c-gateway` feature exposes a `no_std` C API that mirrors the client/server pool design: the application provides upstream and downstream transport callbacks (`MbusTransportCallbacks`) and `mbus_pool_lock` / `mbus_pool_unlock` plus `mbus_gateway_lock(id)` / `mbus_gateway_unlock(id)` routines, then drives the gateway with `mbus_gateway_poll(id)` from its event loop. ```bash cargo build -p mbus-ffi --features c-gateway --no-default-features ``` A complete end-to-end CMake demo (POSIX sockets, in-process echo Modbus server, event logging) lives at [`examples/c_gateway_demo/main.c`](examples/c_gateway_demo/main.c). Build and run it with: ```bash cd mbus-ffi/examples/c_gateway_demo mkdir build && cd build CC=/usr/bin/clang cmake .. # macOS: avoid Homebrew llvm make ./c_gateway_demo ``` ### Running FFI Tests The FFI layer has three distinct test paths: 1. Rust-side unit tests inside `mbus-ffi` 2. Native C binding-layer tests compiled from `mbus-ffi/tests/c_api/test_binding_layer.c` 3. Higher-level native smoke tests under `examples/c_client_demo` #### Rust-side FFI tests Run the Rust unit tests and doc tests for `mbus-ffi`: ```bash cargo test -p mbus-ffi ``` This validates the Rust-side FFI mapping code such as config translation, error/status mapping, and static client-pool behavior. #### Native C binding-layer tests First build the native FFI library with the C API enabled: ```bash cargo build -p mbus-ffi --features full ``` Then compile the standalone C binding test source: ```bash cc -I target/mbus-ffi/include mbus-ffi/tests/c_api/test_binding_layer.c -L target/debug -lmbus_ffi -o /tmp/test_binding_layer ``` Run it against the freshly built shared library: macOS: ```bash DYLD_LIBRARY_PATH="$PWD/target/debug" /tmp/test_binding_layer ``` Linux: ```bash LD_LIBRARY_PATH="$PWD/target/debug" /tmp/test_binding_layer ``` This exercises the C ABI directly from C code, covering null-handling, invalid client IDs, status strings, accessor helpers, and native error paths. If you already have the CMake-based test harness built under `mbus-ffi/tests/c_api/build`, you can also run: ```bash ./mbus-ffi/tests/c_api/build/c_api_tests ``` #### Native C smoke test Build or run the end-to-end client smoke demo with xtask: ```bash cargo run -p xtask -- build-c-demo --demo c_client_demo cargo run -p xtask -- run-c-demo --demo c_client_demo --mode serial-pty ``` This builds `mbus-ffi` with the demo's declared feature set, configures the CMake project in `mbus-ffi/examples/c_client_demo`, builds `c_smoke_test`, and can run the PTY-backed smoke path without external hardware. ### C Server — Two Integration Approaches There are two ways to build a Modbus TCP server with the C FFI. #### Approach 1: Hand-Written Handlers (`c_server_demo`) All FC callbacks are wired directly in `main.c` without any code generation. This is the simplest starting point when the register layout is small or fixed. ```bash # Build and self-test cargo run -p xtask -- build-c-demo --demo c_server_demo # Static link cargo run -p xtask -- build-c-demo --demo c_server_demo --static ``` Source: `mbus-ffi/examples/c_server_demo/main.c`. It creates a Modbus TCP server via `mbus_tcp_server_new`, registers FC01/FC05/FC03 callbacks, runs an in-process self-test with synthetic ADUs, and validates responses. #### Approach 2: YAML-Driven Code Generation (`c_server_demo_yaml`) The device register/coil map is declared in a YAML file. `build.rs` generates a type-safe Rust dispatcher into `$OUT_DIR` at compile time (never tracked in git), and `gen-server-app` regenerates the matching C header. ```bash # Build, generate C header, compile, and self-test MBUS_SERVER_APP_CONFIG=mbus-ffi/examples/c_server_demo_yaml/mbus_server_app.example.yaml \ cargo run -p xtask -- build-c-demo --demo c_server_demo_yaml --static --features c-server ``` Or through xtask (sets the env var automatically from `demo.yaml`): ```bash cargo run -p xtask -- build-c-demo --demo c_server_demo_yaml --static ``` Source: `mbus-ffi/examples/c_server_demo_yaml/`. Full walkthrough: [`examples/c_server_demo_yaml/README.md`](examples/c_server_demo_yaml/README.md). When `MBUS_SERVER_APP_CONFIG` is not set, `build.rs` falls back to the bundled `examples/c_server_demo_yaml/mbus_server_app.example.yaml` when building inside this workspace. That fallback exists to keep workspace builds and tests working; external `mbus-ffi` consumers should set `MBUS_SERVER_APP_CONFIG` explicitly. #### Choosing an approach | | Hand-written | YAML-driven | |---|---|---| | Register map changes | Edit C callbacks directly | Edit YAML → rebuild auto-updates C header and Rust dispatcher | | Generated files in git | None | None | | Rust dispatcher source | Written by hand | Generated by `build.rs` into `$OUT_DIR` | | Write-notification hooks | Your C function, wired manually | Declared in YAML, called by generated dispatcher | | Best for | Quick integration / small maps | Production devices with many registers | --- ## .NET (C#) Bindings The `dotnet` feature compiles the `mbus_dn_*` entry-point family — a flat, versioned C ABI specifically designed for .NET P/Invoke (`[LibraryImport]`). The managed code: ```bash # Build the native cdylib with .NET support and all FCs cargo build -p mbus-ffi --features dotnet-full ``` Output: `target/debug/mbus_ffi.dll` (Windows) / `libmbus_ffi.so` (Linux) / `libmbus_ffi.dylib` (macOS). ### Native DLL search `[LibraryImport("mbus_ffi")]` loads the native library from the application output directory, `PATH`, or the working directory. The example projects copy the DLL from `target\debug\` (or `target\release\`) automatically via MSBuild. See [DLL Deployment](../documentation/dotnet_bindings.md#native-dll-deployment) for the full copy rule and VS 2022 setup. ### Quick start (C#) ```csharp using ModbusRs; using var client = new ModbusTcpClient("192.168.1.10", 502); await client.ConnectAsync(); ushort[] regs = await client.ReadHoldingRegistersAsync(unitId: 1, address: 0, quantity: 4); bool[] coils = await client.ReadCoilsAsync(unitId: 1, address: 0, quantity: 8); await client.DisconnectAsync(); ``` 📖 **[Full .NET Binding Documentation →](../documentation/dotnet_bindings.md)** --- ## Go Bindings The `go` feature compiles `libmbus_ffi` into a native static archive or shared library, exposing Go APIs through a cgo module at `github.com/Raghava-Ch/modbus-rs/mbus-ffi/go`. ### Build & Test The bindings require cgo. Build the native library and run the Go test suite: ```bash # Build the native static library for Go and run tests ./mbus-ffi/go/scripts/build_native.sh cd mbus-ffi/go go test ./... ``` ### Quick Start (Go) ```go package main import ( "context" "fmt" "time" "github.com/Raghava-Ch/modbus-rs/mbus-ffi/go/client/tcp" ) func main() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() client, err := tcp.NewClient("192.168.1.10", 502) if err != nil { panic(err) } defer client.Close() if err := client.Connect(ctx); err != nil { panic(err) } regs, err := client.ReadHoldingRegisters(ctx, 1, 0, 10) if err != nil { panic(err) } fmt.Println(regs) } ``` 📖 **[Full Go Binding Documentation →](../documentation/go_bindings.md)** --- ## Node.js Bindings (`modbus-rs` on npm) The `nodejs` feature compiles `mbus-ffi` into a Node.js native addon via [napi-rs](https://napi.rs/), exposing an idiomatic Promise-based JavaScript and TypeScript API. The package source lives in [`mbus-ffi/javascript/`](javascript/). ```bash # Build the native addon with all Modbus features cargo build -p mbus-ffi --features nodejs-full ``` # Then build the npm package cd mbus-ffi/javascript npm install npm run build ``` Minimum supported Node.js version: **20 LTS**. Prebuilt binaries are published per-platform via the `optionalDependencies` mechanism so end users do not need a Rust toolchain. ### Quick start (JavaScript) ```js import { AsyncTcpTransport } from 'modbus-rs'; const transport = await AsyncTcpTransport.connect({ host: '192.168.1.10', port: 502, timeoutMs: 2000, }); const client = transport.createClient({ unitId: 1 }); const regs = await client.readHoldingRegisters({ address: 0, quantity: 4 }); await client.writeMultipleRegisters({ address: 10, values: [1, 2, 3, 4] }); await transport.close(); ``` 📖 **[Full JavaScript Binding Documentation →](../documentation/javascript_bindings.md)** --- ## Thread Safety The C FFI layer is designed to be thread-safe, but it requires the **host application** to provide lock/unlock symbols that the Rust library resolves at **link time**. For the client FFI, define: ```c // Required by the native client pool and client operations. void mbus_pool_lock(void); void mbus_pool_unlock(void); void mbus_client_lock(MbusClientId id); void mbus_client_unlock(MbusClientId id); ``` For the server FFI, define: ```c // Required by the native server pool and server operations. void mbus_pool_lock(void); void mbus_pool_unlock(void); void mbus_server_lock(MbusServerId id); void mbus_server_unlock(MbusServerId id); ``` These are **not** function pointers in `MbusTransportCallbacks` — they are ordinary `extern` symbols that the linker expects to find in your object files, exactly like implementing `malloc` for a bare-metal libc. Your application defines them and the Rust FFI resolves them at link time. | Symbol | Called when | |---|---| | `mbus_pool_lock` / `mbus_pool_unlock` | Any operation that allocates or frees a client or server slot | | `mbus_client_lock(id)` / `mbus_client_unlock(id)` | Any operation that reads or mutates a specific client | | `mbus_server_lock(id)` / `mbus_server_unlock(id)` | Any operation that polls, connects, disconnects, or mutates a specific server | **Single-threaded** — define them as no-ops: ```c void mbus_pool_lock(void) {} void mbus_pool_unlock(void) {} void mbus_client_lock(MbusClientId id) { (void)id; } void mbus_client_unlock(MbusClientId id) { (void)id; } void mbus_server_lock(MbusServerId id) { (void)id; } void mbus_server_unlock(MbusServerId id) { (void)id; } ``` **Multi-threaded** — back them with real mutexes: ```c static pthread_mutex_t g_pool_mutex = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t g_client_mutex[256] = { [0 ... 255] = PTHREAD_MUTEX_INITIALIZER }; static pthread_mutex_t g_server_mutex[256] = { [0 ... 255] = PTHREAD_MUTEX_INITIALIZER }; void mbus_pool_lock(void) { pthread_mutex_lock(&g_pool_mutex); } void mbus_pool_unlock(void) { pthread_mutex_unlock(&g_pool_mutex); } void mbus_client_lock(MbusClientId id) { pthread_mutex_lock(&g_client_mutex[id]); } void mbus_client_unlock(MbusClientId id) { pthread_mutex_unlock(&g_client_mutex[id]); } void mbus_server_lock(MbusServerId id) { pthread_mutex_lock(&g_server_mutex[id & 0xFFu]); } void mbus_server_unlock(MbusServerId id) { pthread_mutex_unlock(&g_server_mutex[id & 0xFFu]); } ``` ### Callbacks fire inside the client lock **All response callbacks** (`on_read_coils`, `on_request_failed`, …) are invoked **synchronously from within `mbus_tcp_poll` / `mbus_serial_poll`**, while `mbus_client_lock(id)` is already held for that client. Consequence: **do not call any `mbus_tcp_*` / `mbus_serial_*` API from inside a callback** — the attempt to re-acquire the same client lock will deadlock or return `MBUS_ERR_BUSY`. Queue follow-up requests in a user-owned buffer and enqueue them after `poll` returns. For the server FFI, hold `mbus_server_lock(id)` around `mbus_tcp_server_poll`, `mbus_tcp_server_connect`, `mbus_tcp_server_disconnect`, and the corresponding serial server calls. Use `mbus_pool_lock` / `mbus_pool_unlock` for server allocation and free paths. --- ## WASM Browser Bindings `mbus-ffi` securely exports internal modbus logic to JavaScript via `wasm-pack`, exposing: - `WasmModbusClient` (WebSocket transport mapper) - `WasmSerialModbusClient` + `request_serial_port()` (Web Serial hardware mapper) - `WasmTcpServer` + `WasmTcpGatewayConfig` (WASM server binding over network transport) - `WasmSerialServer` + `WasmSerialServerConfig` (WASM server binding over serial transport) All APIs are Promise-based and are designed specifically for browser runtimes (`wasm32`). Building native targets does not interact with Javascript wrappers. ### WASM Server Transport Ownership Boundary WASM transport implementations are owned by transport crates only: - `mbus-network` owns the WASM websocket transport implementation. - `mbus-serial` owns the WASM Web Serial transport implementation. `mbus-ffi` server bindings orchestrate lifecycle and JS callback bridging; they do not reimplement transport I/O. ### WASM Server Protocol Contract (Current vs Planned) Current contractual surface (available now): - `WasmTcpServer` / `WasmSerialServer` lifecycle: `start()`, `stop()`, `is_running()` - JS callback bridge via `dispatch_request(...)` (direct value or Promise return) - Raw frame passthrough helpers: `send_frame(...)`, `recv_frame(...)` - WebSocket readiness helpers (TCP server): `transport_connecting()`, `transport_connected()` - Transport ownership remains in `mbus-network` and `mbus-serial` Not part of the current server binding contract (planned later): - Built-in Modbus function-code dispatcher/routing inside `WasmTcpServer` / `WasmSerialServer` - Guaranteed typed FC response helpers at the WASM server binding layer - Fully managed end-to-end server request loop owned by `mbus-ffi` This separation is intentional for phase stability: current bindings provide lifecycle + bridge + transport plumbing, while higher-level protocol handling is integrated incrementally. ### Build WASM Package To build the distribution packages, navigate to the `wasm` directory and use the npm build script: ```bash cd mbus-ffi/wasm npm install npm run build ``` This builds both the web and bundler targets under the `dist/` folder. ### Quick Start (Web Target - Direct Imports) For direct browser usage without a bundler, import from `dist/web/modbus-rs.js` and await the initialization promise: ```javascript import init, { WasmTcpTransport } from "./wasm/dist/web/modbus-rs.js"; await init(); // Create the connection transport const transport = new WasmTcpTransport("ws://127.0.0.1:8080", { responseTimeoutMs: 3000, retryAttempts: 1, tickIntervalMs: 20, }); // Bind lightweight clients to different Unit IDs sharing the same WebSocket transport const client1 = transport.createClient({ unitId: 1 }); const client2 = transport.createClient({ unitId: 2 }); const regs = await client1.read_holding_registers(0, 2); console.log(Array.from(regs)); ``` ### Quick Start (Bundlers - Vite, Svelte, React, etc.) When using a bundler (Vite, Webpack, etc.), import directly from the package: ```javascript import { WasmTcpTransport } from "modbus-rs-wasm"; // Create the transport and clients const transport = new WasmTcpTransport("ws://127.0.0.1:8080", { responseTimeoutMs: 3000 }); const client = transport.createClient({ unitId: 1 }); ``` ### Quick Start (Web Serial) ```javascript import init, { request_serial_port, WasmSerialTransport } from "./wasm/dist/web/modbus-rs.js"; await init(); // Must be called from a user gesture (e.g. click handler) const portHandle = await request_serial_port(); // Open one transport for the physical serial port const transport = new WasmSerialTransport(portHandle, { mode: "rtu", // "rtu" | "ascii" baudRate: 9600, dataBits: 8, stopBits: 1, parity: "even", responseTimeoutMs: 1000, retryAttempts: 3, tickIntervalMs: 20, }); // Create multiple clients on the same port (e.g. RS-485 multi-drop) const slave1 = transport.createClient({ unitId: 1 }); const slave2 = transport.createClient({ unitId: 2 }); const ok = await slave1.read_single_coil(0); ``` ### Quick Start (WASM Server Bindings) ```javascript import init, { WasmTcpServer, WasmTcpGatewayConfig, WasmSerialServer, WasmSerialServerConfig, } from "./wasm/dist/web/modbus-rs.js"; await init(); const tcpServer = new WasmTcpServer( new WasmTcpGatewayConfig("ws://127.0.0.1:8080"), (req) => Promise.resolve({ ok: true, echo: req }) ); await tcpServer.start(); const serialServer = new WasmSerialServer( WasmSerialServerConfig.rtu(), (req) => Promise.resolve(req) ); // Attach browser SerialPort object from navigator.serial.requestPort() before start. ``` ### Server Observability (Status + Last Error) Use `status_snapshot()` for lightweight counters and lifecycle state, and `last_error_message()` / `clear_last_error()` to track and reset the latest binding-level failure. ```javascript const snap = tcpServer.status_snapshot(); console.log({ transport: snap.transport(), running: snap.running(), connected: snap.transport_connected(), dispatched: snap.dispatched_requests(), sent: snap.sent_frames(), received: snap.received_frames(), hasError: snap.last_error_present(), }); const lastErr = tcpServer.last_error_message(); if (lastErr !== undefined) { console.warn("server last error:", lastErr); tcpServer.clear_last_error(); } ``` ### Example Web Pages Use the browser examples under `mbus-ffi/wasm/examples`: - `wasm_client/network_smoke.html` (WebSocket/TCP path client) - `wasm_client/serial_smoke.html` (Web Serial path client, full serial API smoke runner) - `wasm_server/network_smoke.html` (WASM TCP server lifecycle + dispatch + frame passthrough) - `wasm_server/serial_smoke.html` (WASM Serial server lifecycle + dispatch + frame passthrough) Serve the examples over localhost: ```bash cd mbus-ffi/wasm python3 -m http.server 8089 ``` ## Feature Gates and Supported Modbus Operations `mbus-ffi` exposes internal Modbus stack features configured via Cargo feature flags. These are divided into **Service (Function Code) Features**, **Transport Features**, **Language/Platform Binding Features**, and **Convenience Aliases**. ### 1. Service Features (Function Codes) These features enable compile-time support for specific Modbus function codes: | Feature | Description | Mapped Function Codes | |---|---|---| | `coils` | Read/Write coils | FC01 (Read Coils), FC05 (Write Single Coil), FC0F (Write Multiple Coils) | | `holding-registers` | Read/Write holding registers | FC03 (Read Holding Registers), FC06 (Write Single Register), FC10 (Write Multiple Registers), FC16 (Mask Write), FC17 (Read/Write Multiple) | | `input-registers` | Read input registers | FC04 (Read Input Registers) | | `registers` | Convenience helper | Enables both `holding-registers` and `input-registers` | | `discrete-inputs` | Read discrete inputs | FC02 (Read Discrete Inputs) | | `fifo` | Read FIFO queue | FC18 (Read FIFO Queue) | | `file-record` | Read/Write file records | FC14 (Read File Record), FC15 (Write File Record) | | `diagnostics` | Server diagnostics & IDs | FC07 (Read Exception Status), FC08 (Diagnostics), FC0B (Comm Event Counter), FC0C (Comm Event Log), FC11 (Report Server ID), FC2B (Read Device ID) | | `traffic` | Traffic monitoring | Enables traffic notification callbacks on async clients/servers | ### 2. Transport Features These features control compile-time transport support: | Feature | Description | |---|---| | `network-tcp` | Enables Modbus TCP network transport support | | `serial-rtu` | Enables Modbus RTU serial transport support | | `serial-ascii` | Enables Modbus ASCII serial transport support | ### 3. Language & Platform Bindings These features compile native binding interfaces and platform bindings: | Feature | Description | |---|---| | `wasm` | Enables browser WebAssembly bindings and transports (`mbus-network/wasm`, `mbus-serial/wasm`) | | `c-client` | Enables native C FFI client APIs and static client pool | | `c-server` | Enables native C FFI server APIs (YAML dispatch, custom handlers) | | `c-gateway` | Enables native C FFI gateway APIs | | `server-traffic` | Enables traffic callbacks specifically for the native C server | | `python` | Enables Python bindings via PyO3 and Maturin | | `python-gateway` | Enables Python bindings for `TcpGateway` / `AsyncTcpGateway` | | `go` | Enables Go cgo bindings | | `go-traffic` | Enables traffic notification callbacks for the Go server/client | | `nodejs` | Enables Node.js native addon bindings via napi-rs | | `nodejs-traffic` | Enables traffic callbacks for the Node.js server/client | | `dotnet` | Enables flat C ABI bindings specifically for .NET P/Invoke | ### 4. Convenience Aliases These bundle related features for simple compile commands: | Feature Alias | Bundled Features | |---|---| | `full` | `c-client`, `c-server`, `c-gateway`, `coils`, `registers`, `discrete-inputs`, `fifo`, `file-record`, `diagnostics`, `traffic`, `error-trait`, `network-tcp`, `serial-rtu`, `serial-ascii` | | `server-full` | `c-server` along with all service and transport features | | `python-full` | `python`, `c-client`, `c-gateway`, `coils`, `registers`, `discrete-inputs`, `fifo`, `file-record`, `diagnostics`, `traffic`, `error-trait`, `network-tcp`, `serial-rtu`, `serial-ascii` | | `go-full` | `go`, `c-client`, `c-gateway`, `coils`, `registers`, `discrete-inputs`, `fifo`, `file-record`, `diagnostics`, `traffic`, `error-trait`, `network-tcp`, `serial-rtu`, `serial-ascii` | | `nodejs-full` | `nodejs`, `c-client`, `c-gateway`, `coils`, `registers`, `discrete-inputs`, `fifo`, `file-record`, `diagnostics`, `traffic`, `error-trait`, `network-tcp`, `serial-rtu`, `serial-ascii` | | `dotnet-full` | `dotnet`, `c-client`, `c-gateway`, `coils`, `registers`, `discrete-inputs`, `fifo`, `file-record`, `diagnostics`, `traffic`, `error-trait`, `network-tcp`, `serial-rtu`, `serial-ascii` | ## WASM Test Coverage WASM browser tests are in `mbus-ffi/tests/wasm_e2e.rs` and cover: - Promise behavior and typed payload mapping for client APIs. - WASM server lifecycle/dispatch surface (`WasmTcpServer`, `WasmSerialServer`). - Adapter passthrough to transport crates (`mbus-network`, `mbus-serial`). Run wasm-target checks: ```bash cargo check -p mbus-ffi --target wasm32-unknown-unknown --features wasm-client cargo check -p mbus-ffi --target wasm32-unknown-unknown --features wasm-full ``` Run browser E2E tests: ```bash bash mbus-ffi/scripts/run_wasm_browser_tests.sh ``` Prerequisites for browser E2E test runs: - `wasm-pack` installed and available on `PATH` - Chromium-based browser installed (`google-chrome`, `chromium`, or `chromium-browser`) - `chromedriver` installed on `PATH` with major version matching local browser Notes: - The script enforces these prerequisites before executing tests. - Canonical command target is fixed to `wasm-pack test --headless --chrome --features wasm-full --test wasm_e2e`. ## Licensing Copyright (C) 2025 Raghava Challari This project is licensed under the GNU General Public License v3.0 (GPLv3). For details, refer to the [LICENSE](../LICENSE) file or the [GPLv3 official site](https://www.gnu.org/licenses/gpl-3.0.en.html). This crate is licensed under GPLv3. Commercial licenses are also available for proprietary use; contact [ch.raghava44@gmail.com](mailto:ch.raghava44@gmail.com). ## Contact **Name:** Raghava Ch **Email:** [ch.raghava44@gmail.com](mailto:ch.raghava44@gmail.com) **Repository:** [github.com/Raghava-Ch/modbus-rs](https://github.com/Raghava-Ch/modbus-rs)