Web-based Modbus RTU / ASCII inspector (monitor & tester) powered by the Web Serial API and Preact. It lets you connect to a serial Modbus device directly from Chrome (or any Chromium browser supporting Web Serial), send read/write requests, monitor periodic polling, and inspect raw frames.
- Runs 100% in the browser (no native installs) using Web Serial API
- Supports Modbus RTU and ASCII protocols with full framing validation
- Single read (FC 01/02/03/04) & write (FC 05/06) requests
- Multi-write operations (FC 15/16) with array input validation
- Periodic monitoring (polling) with adjustable interval in code (default 1000 ms)
- Hex or decimal display toggle for register values & addresses
- Real‑time communication log (TX/RX) with copy single / copy all and automatic trimming
- CRC16 (RTU) and LRC (ASCII) validation for frame integrity
- Simple buffering & response correlation (caller-controlled cancellation via AbortSignal)
- Clean, responsive UI (desktop & mobile)
- Saving / loading session profiles
- Export logs / captured values to CSV
- Custom polling interval in UI
- Advanced diagnostics functions (FC08+) and extended exception decoding
Layer | File(s) | Responsibility |
---|---|---|
UI (Preact) | src/frontend/App.tsx |
Component implementation of the full UI |
Pure Modbus API | src/rtu.ts , src/ascii.ts , src/frameBuilder.ts , src/frameParser.ts |
Pure, side‑effect free request frame build + parse functions (RTU & ASCII) |
Transport abstraction | src/transport/* |
Swappable transports (Serial / Mock / TCP placeholder) implementing a minimal event API |
Low-level serial wrapper | src/serial.ts |
Web Serial API wrapper + typed EventEmitter |
Errors & helpers | src/errors.ts , src/functionCodes.ts , src/crc.ts , src/lrc.ts |
Shared utilities / error types |
Entry point: index.html
-> src/frontend/main.tsx
-> App
.
Legacy class-based Modbus client code has been replaced by a transport + pure function API (see below). The previous class facade is intentionally omitted for a smaller surface and easier testing.
The transport layer provides a unified, MessagePort-like interface for different communication backends.
SerialTransport
– Web Serial APIMockTransport
– Deterministic test / development transport (auto responses, error injection)TcpTransport
– Placeholder (throws in browsers; add a WebSocket bridge instead)
You can create transports directly or via the registry helper.
import { createTransport, SerialTransport } from "./src/transport";
import type { SerialTransportConfig } from "./src/transport";
const cfg: SerialTransportConfig = {
type: "serial",
baudRate: 9600,
dataBits: 8,
parity: "none",
stopBits: 1,
};
// Using helper
const transport = createTransport(cfg);
await transport.connect();
transport.addEventListener("message", (ev) => {
console.log("RX", Array.from(ev.detail));
});
// Raw write (already framed Modbus RTU/ASCII bytes)
transport.postMessage(new Uint8Array([1,3,0,0,0,1,0x84,0x0A]));
import { MockTransport, type MockTransportConfig } from "./src/transport";
const mockCfg: MockTransportConfig = { type: "mock", name: "demo" };
const mock = new MockTransport(mockCfg, { connectDelay: 50 });
await mock.connect();
// Auto response: when request bytes match, reply automatically
const request = new Uint8Array([1,3,0,0,0,1]);
const response = new Uint8Array([1,3,2,0x12,0x34]);
mock.setAutoResponse(request, response);
mock.addEventListener("message", (ev) => {
console.log("Mock RX", ev.detail);
});
mock.postMessage(request); // Will trigger response shortly
Event | Detail | When |
---|---|---|
open |
- | Successful connect |
close |
- | Graceful or unexpected disconnect |
statechange |
TransportState |
Any state transition |
message |
Uint8Array |
Raw received frame bytes |
error |
Error |
Transport-level error |
High-level Modbus operations are exposed as pure async functions operating on an IModbusTransport
instance. They return Result<T, Error>
objects (from option-t/plain_result
) instead of throwing for predictable error handling.
Two protocol modules exist: rtu.ts
and ascii.ts
(function names are the same).
import { readHoldingRegisters } from "./src/rtu"; // or "./src/ascii"
import { isOk } from "option-t/plain_result";
const res = await readHoldingRegisters(transport, 1, 0, 10);
if (isOk(res)) {
console.log("Registers", res.ok.data);
}
import { writeSingleRegister, writeMultipleRegisters } from "./src/rtu";
import { isErr } from "option-t/plain_result";
const r1 = await writeSingleRegister(transport, 1, 100, 0x1234);
const r2 = await writeMultipleRegisters(transport, 1, 0, [0x1234, 0x5678]);
for (const r of [r1, r2]) if (isErr(r)) console.error(r.err.message);
Each read/write accepts { signal?: AbortSignal }
in the final options parameter. Provide an AbortController
to enforce timeouts.
const controller = new AbortController();
setTimeout(() => controller.abort(new Error("Timeout")), 1500);
const result = await readCoils(transport, 1, 0, 16, { signal: controller.signal });
The project uses the functional Result type from option-t
:
Helper | Purpose |
---|---|
createOk(data) |
Construct success result |
createErr(error) |
Construct error result |
isOk(r) / isErr(r) |
Type guards |
unwrapOk(r) |
Extract value (assumes Ok) |
- Avoids try/catch in hot paths
- Composable transformations / branching
- Easier test assertions (no thrown side effects)
Aspect | Transport Abstraction | Pure Function API |
---|---|---|
Testability | Mock transport, deterministic timing | No hidden state, simple inputs/outputs |
Extensibility | Register new transports dynamically | Compose new higher operations easily |
Error Handling | Structured events & explicit states | Explicit Result objects |
Tree Shaking | Import only needed transports | Import only used operations |
The codebase uses TypeScript's strict typing for Modbus function codes to prevent runtime errors from unsupported codes:
// Type-safe function code definitions
type ReadFunctionCode = 1 | 2 | 3 | 4 // Read operations only
type WriteSingleFunctionCode = 5 | 6 // Single write operations
type WriteMultiFunctionCode = 15 | 16 // Multi-write operations
type WriteFunctionCode = WriteSingleFunctionCode | WriteMultiFunctionCode
// Usage in configuration interfaces
interface ModbusReadConfig {
functionCode: ReadFunctionCode // Only 1|2|3|4 allowed
// ... other properties
}
interface ModbusWriteConfig {
functionCode: WriteFunctionCode // Only 5|6|15|16 allowed
// ... other properties
}
Benefits:
- Compile-time validation: TypeScript will reject invalid function codes (e.g.,
functionCode: 7
) - Enhanced IDE support: Auto-completion shows only valid function codes
- Documentation: Types serve as inline documentation of supported operations
- Refactoring safety: Changes to supported function codes are automatically reflected
Example:
// ✅ Valid - compiles successfully
const readConfig: ModbusReadConfig = { functionCode: 3, ... }
const writeConfig: ModbusWriteConfig = { functionCode: 6, ... }
// ❌ Invalid - TypeScript compilation error
const invalidConfig: ModbusReadConfig = { functionCode: 7, ... } // Error!
- Chromium-based browser with Web Serial API (Chrome 89+, Edge, etc.). Firefox & Safari currently lack required API.
- A Modbus slave device connected via a serial adapter recognizable by the OS (USB/RS485, etc.).
Install dependencies (pnpm recommended):
pnpm install
pnpm dev
Then open the printed local URL (default http://localhost:5173
).
For a production build:
pnpm build
pnpm preview
- Open the app in a supported browser.
- Click "Select Port" and choose your serial interface (RS-485 adapter, etc.).
- Adjust serial parameters (baud rate, data bits, parity, stop bits) and Modbus settings (Slave ID, protocol) if needed.
- Press "Connect".
- For a single read: choose a function code (e.g. Holding Registers = FC03), start address, and quantity; click "Read".
- To start periodic polling: click "Start Monitor" (click again to stop). Default interval is 1000 ms; adjust in
App.tsx
ormodbus.ts
if needed. - To write:
- Single writes (FC05/06): Select function (05 coil / 06 single register), address, and value (prefix with
0x
for hex) then click "Write". - Multi-coil writes (FC15): Select "15 - Write Multiple Coils", enter start address, and provide comma or space-separated coil values (0 or 1). Max 1968 coils.
- Multi-register writes (FC16): Select "16 - Write Multiple Registers", enter start address, and provide comma/space/line-separated register values (0-65535). Max 123 registers.
- Single writes (FC05/06): Select function (05 coil / 06 single register), address, and value (prefix with
- Toggle "Hex Display" to view values / addresses in hexadecimal.
- Use "Clear Logs" or "Copy All Logs" for log management; each log line also has an individual copy button.
- Info: general status messages
- Sent: outbound Modbus frame bytes
- Received: inbound Modbus frame bytes
- Error: serial / protocol / timeout issues
The FC01 interface allows you to read multiple coils from the Modbus device. Enter the start address and quantity of coils to read. The response will show each coil as a bit value (0 or 1).
The FC02 interface allows you to read discrete inputs from the Modbus device. Similar to coils, but typically read-only status inputs. Enter the start address and quantity of discrete inputs to read.
The FC03 interface allows you to read holding registers from the Modbus device. These are typically read/write registers used for configuration and data storage. Enter the start address and quantity of registers to read.
The FC04 interface allows you to read input registers from the Modbus device. These are typically read-only registers containing measurement data or status information. Enter the start address and quantity of registers to read.
The FC05 interface allows you to write a single coil value. Enter the coil address and value (0 for OFF, 1 for ON). This is used for controlling individual digital outputs or control points.
The FC06 interface allows you to write a single register value. Enter the register address and value (0-65535). Supports hexadecimal values when hex display mode is enabled (prefix with 0x).
The FC15 interface allows you to write multiple coils at once. Enter coil values as comma or space-separated bits (0 or 1), with a maximum of 1968 coils.
The FC16 interface allows you to write multiple registers at once. Enter register values as comma, space, or line-separated numbers (0-65535), with support for hexadecimal values when hex display mode is enabled. Maximum of 123 registers.
Read request (example FC03):
| Slave | Func | Start Hi | Start Lo | Qty Hi | Qty Lo | CRC Lo | CRC Hi |
Single register write (FC06):
| Slave | 06 | Addr Hi | Addr Lo | Value Hi | Value Lo | CRC Lo | CRC Hi |
CRC16 polynomial 0xA001 (LSB first) is used. Exception responses are decoded with a basic Japanese->English translated map.
ASCII frames are encoded in hexadecimal text format with start/end delimiters:
:AABBCCDDDD...EELR\r\n
Where:
:
= Start character (0x3A)AA
,BB
, etc. = Hexadecimal pairs representing data bytes (uppercase)LR
= LRC (Longitudinal Redundancy Check) as hex pair\r\n
= Termination (CR+LF, 0x0D 0x0A)
LRC Calculation: LRC = (256 - (sum of all data bytes % 256)) % 256
Example read request (FC03):
:01030000000AFB\r\n
- Slave: 01, Function: 03, Start: 0000, Quantity: 000A, LRC: FB
The ASCII implementation handles:
- Proper frame detection (
:
start,\r\n
end) - Hex decoding with validation
- LRC calculation and verification
- Buffer management for partial/concatenated frames
- Error handling identical to RTU (LRC mismatch triggers error event)
Pure functions perform a single in-flight request per call; concurrency control is handled by the caller (UI serialises by design). Provide an AbortSignal
for timeouts. Modbus exception frames (function code | 0x80) become ModbusExceptionError
. Corrupted frames trigger CRC / LRC specific errors; RTU logic attempts resync scanning for plausible frame starts before discarding.
Problem: Serial port becomes unexpectedly disconnected during operation.
Common Causes:
- Cable/adapter unplugged - Physical disconnection of USB-to-serial adapter
- Browser permission revocation - User or browser policy revoked serial port access
- Device power loss - Target Modbus device lost power or reset
- Driver issues - USB serial driver problems or conflicts
Symptoms:
- Orange warning banner appears: "Port Disconnected"
- Communication log shows "Serial port disconnected unexpectedly"
- Connection status changes to "Disconnected"
Resolution:
- Check physical connections - Ensure USB cable and serial connections are secure
- Use the Reconnect button - Click the "Reconnect" button in the disconnect banner
- Refresh browser permissions - If reconnection fails, refresh the page and re-select the port
- Check device status - Verify the target Modbus device is powered and responsive
- Try different USB port - Switch to a different USB port if using a USB-to-serial adapter
Problem: Browser denies access to serial ports.
Symptoms:
- "Port selection error" when clicking "Select Port"
- Browser doesn't show the port selection dialog
Resolution:
- Check browser compatibility - Use Chrome 89+ or Edge 89+
- Enable secure context - Ensure using HTTPS or localhost
- Check browser permissions - Go to browser settings and check site permissions
- Clear browser data - Clear site data and cookies, then try again
Problem: No response from Modbus device or timeout errors.
Symptoms:
- "Request timed out" errors in communication log
- No response data received after sending commands
Resolution:
- Verify serial settings - Check baud rate, data bits, parity, and stop bits match device
- Check slave ID - Ensure the configured slave ID matches the device
- Test with simple reads - Start with basic function codes (FC03) on known registers
- Check wiring - Verify RS-485 A/B wiring polarity and termination resistors
- Monitor traffic - Use the communication log to verify frames are being sent correctly
Problem: Corrupted frames or electrical noise causing communication errors.
Symptoms:
- "CRC error" messages in communication log
- Intermittent communication failures
- Valid responses occasionally missed after noise events
How it works: The application includes intelligent buffer resynchronization for RTU protocol:
- When a CRC error occurs, the system doesn't immediately clear the entire buffer
- Instead, it scans for the next plausible frame start (valid slave ID 1-247 + function code)
- If a candidate frame is found, the buffer advances to that position
- If no valid frame is detected, falls back to complete buffer reset
- This allows recovery of valid frames that follow corrupted data
Supported frame detection:
- Valid slave IDs: 1-247 (0x01-0xF7)
- Function codes: 1, 2, 3, 4, 5, 6, 15, 16
- Exception responses: 0x81-0x86, 0x8F, 0x90
Resolution:
- Check cable quality - Use shielded cables for long runs or noisy environments
- Verify grounding - Ensure proper grounding of RS-485 network
- Check termination - Add 120Ω termination resistors at both ends of RS-485 bus
- Reduce noise sources - Keep communication cables away from power lines and motors
- Lower baud rate - Reduce communication speed if errors persist
- Use ASCII mode - Consider ASCII protocol for extremely noisy environments (slower but more robust)
The Web Serial API requires a user gesture to open a port; the page cannot access serial devices silently. All communication happens locally; no data leaves the browser unless you manually copy it.
This application provides full support for standard Modbus read and write operations with proper type differentiation:
- FC01 - Read Coils: Digital outputs/coils status (read/write bits)
- FC02 - Read Discrete Inputs: Digital inputs status (read-only bits)
- FC03 - Read Holding Registers: Analog/data registers (read/write registers)
- FC04 - Read Input Registers: Input/measurement registers (read-only registers)
- FC05 - Write Single Coil: Write single digital output
- FC06 - Write Single Register: Write single analog/data register
- FC15 - Write Multiple Coils: Write multiple digital outputs (up to 1968 coils)
- FC16 - Write Multiple Registers: Write multiple registers (up to 123 registers)
- Function code labeling: UI displays specific types (Coils, Discrete Inputs, etc.) in data tables and logs
- Unified parsing: Dedicated utilities for bit-based (FC01/02) and register-based (FC03/04) responses
- Protocol support: Full RTU and ASCII mode compatibility with proper CRC16/LRC validation
- Extensible design: Structured for easy addition of future function codes
- Exception responses are decoded with basic error code translation
- Advanced diagnostics functions (FC08+) are not yet implemented
- File transfer and program control functions are not supported
Script | Description |
---|---|
pnpm dev |
Start Vite dev server |
pnpm build |
Production build |
pnpm preview |
Preview built assets |
pnpm check |
Lint & TS (biome + tsgo) |
pnpm fix |
Auto-fix issues |
pnpm test |
Run all tests |
pnpm test:coverage |
Run tests with coverage report |
pnpm test:watch |
Run tests in watch mode |
Tests are colocated with their source: every *.ts
/ *.tsx
may have a sibling *.test.ts
/ *.test.tsx
. Only true cross‑module integration or fuzz/resync scenarios live under src/__tests__/
.
Category | Example | Purpose |
---|---|---|
Protocol unit | src/frameParser.test.ts |
Exhaust normal + error branches for a single module |
Transport unit | src/transport/transport.test.ts |
Connect / send / auto response / error flows |
UI component | src/frontend/components/WritePanel.test.tsx |
Input validation & event handling |
UI hooks | src/frontend/hooks/useSerial.test.tsx |
Serial state transitions & side‑effects |
Property / fuzz | src/__tests__/modbus-fuzzing.test.ts |
Random frame robustness |
Integration | src/__tests__/integration.test.ts |
RTU/ASCII + transport + parser end‑to‑end |
Resync / noise | src/__tests__/modbus-resync.test.ts |
Noise, abort, buffer resynchronisation |
Legacy test/
directory and synthetic “coverage push” suites were removed; meaningful edge cases were merged into their owning module tests.
Metric | Threshold |
---|---|
Statements | 90% |
Branches | 85% |
Lines | 90% |
Functions | 90% |
modbus.ts
/ transport.ts
are type‑only (no runtime); 0% there is acceptable or can be excluded via coverage.exclude
if desired.
# Run all tests
pnpm test
# With coverage
pnpm test:coverage
# Watch mode
pnpm test:watch
# Single file
pnpm test src/frameParser.test.ts
- fast-check property based fuzzing (frame corruption / randomisation)
- AbortSignal driven cancellation & timeout path tests
- Buffer resynchronisation after CRC/LRC failure (gap & partial frame scenarios)
- Deterministic Web Serial mocks (
MockTransport
,MockSerialPort
) - UI tests assert minimal side‑effects: value transformations & rendering only
CI runs on Node 18 / 20 / 22 and uploads coverage to Codecov.
Issues & PRs welcome. Please keep the UI clean and lightweight. For significant protocol enhancements (multi-part frames, advanced diagnostics), consider adding tests and clear logs.
MIT © 2025 takker99
- Web Serial API team & MDN docs
- Preact for a minimal footprint
- Modbus protocol community references
Enjoy inspecting your Modbus devices directly from the browser.