sondahub / Modbus TCP simulator
Modbus TCP simulator
A Modbus TCP gateway with four devices behind it that do something: a lift station that pumps down its wet well when you start a pump, a three-phase power meter, a drive that ramps to the speed you ask and trips when you rush it, and a sensor so slow your timeouts get tested.
Real Modbus TCP frames over WebSocket, or JSON. Each connection gets its own devices.
Connect
- Address
- wss://api.sondahub.com/modbus
- Binary messages
- Modbus TCP ADUs: transaction, protocol 0, length, unit, PDU
- Text messages
- JSON, answered in JSON
- Subprotocol
- modbus (optional)
Send the frame you would send to port 502 as one binary WebSocket message (several frames in one message, or one frame across two, are cut apart by their MBAP length) and the answer comes back the same way, transaction id kept. A frame whose protocol id is not 0 is dropped, as a server would.
→ 00 01 00 00 00 06 01 03 00 00 00 0A ← 00 01 00 00 00 17 01 03 14 01 40 00 50 01 C2 00 28 00 01 03 52 00 02 00 78 00 01 00 00
JSON, from any WebSocket client
A text message is a request in JSON — the function by code or by name, the values for a write — and the answer is decoded, with the request and response frames in hex beside it. A text message that is a frame in hex works too; help lists the units.
{"unit":1,"function":"read_input_registers","address":0,"count":22}
{"unit":1,"function":5,"address":3,"value":false} # manual mode
{"unit":1,"function":5,"address":0,"value":true} # start pump 1
{"unit":3,"function":16,"address":0,"values":[1,3000]} # drive: run at 30 Hz
{"unit":2,"function":43,"code":2} # device identification
{"hex":"000100000006010300000002"}
{"unit":1,"function":3,"name":"Read Holding Registers","transaction":1,
"request":"00010000000601030000000a","address":0,
"values":[320,80,450,40,1,850,2,120,1,0],
"response":"000100000017010314…","ms":1}
The gateway
| Unit | What answers |
|---|---|
| 1 | Lift station LS-01 (PLC) |
| 2 | Power meter PM-02 |
| 3 | Variable frequency drive VFD-03 |
| 4 | Room sensor on a radio link |
| 0 | Broadcast: a write reaches units 1 to 4 and nobody answers, as the specification says. A read to unit 0 gets no answer either. |
| 9 | Configured on the gateway, switched off in the field: after a second, exception 11 (gateway target device failed to respond). |
| 255 | Unit 1, the way a device that ignores the unit id answers. |
| any other | Exception 10 (gateway path unavailable), at once. |
Exceptions as the specification orders them: an unsupported function is 1, a bad quantity or value 3, an address outside the map 2. Multiple-register writes are checked whole — one bad value and nothing is written.
Function codes
| Code | Function | Notes |
|---|---|---|
| 1 0x01 | Read Coils read_coils | Up to 2,000 coils from a start address. |
| 2 0x02 | Read Discrete Inputs read_discrete_inputs | Up to 2,000 inputs. |
| 3 0x03 | Read Holding Registers read_holding_registers | Up to 125 registers. |
| 4 0x04 | Read Input Registers read_input_registers | Up to 125 registers. |
| 5 0x05 | Write Single Coil write_single_coil | FF00 for on, 0000 for off; anything else is exception 3. |
| 6 0x06 | Write Single Register write_single_register | One register; the answer echoes the request. |
| 15 0x0F | Write Multiple Coils write_multiple_coils | Up to 1,968 coils, packed eight to a byte, low bit first. |
| 16 0x10 | Write Multiple Registers write_multiple_registers | Up to 123 registers, checked all together: one bad value and nothing is written. |
| 17 0x11 | Report Server ID report_server_id | The device id, the run indicator and the model. |
| 22 0x16 | Mask Write Register mask_write_register | (current AND and_mask) OR (or_mask AND NOT and_mask). |
| 23 0x17 | Read/Write Multiple Registers read_write_multiple_registers | The write first, then the read, in one transaction. |
| 43 0x2B | Read Device Identification read_device_identification | MEI type 14: basic, regular, extended or one object. |
Unit 1: Lift station LS-01 (PLC)
Three pumps over a 2 m² wet well. In auto the PLC pumps down between the start and stop levels and alternates the lead pump; in manual you start and stop them. Unit 255 reaches it too. Functions 1, 2, 3, 4, 5, 6, 15, 16, 17, 22, 23, 43.
Coils (0x)
| Address | Name | Notes |
|---|---|---|
| 0–2 | Pump 1–3 run commandread/write bit | In auto the PLC writes these itself on every scan; switch coil 3 off to drive the pumps by hand. |
| 3 | Auto moderead/write bit | 1: the PLC starts the lead pump at the start level, a lag pump at the high alarm level, stops them at the stop level and alternates the lead. 0: manual. |
| 4 | Alarm resetread/write bit | Write 1: latched alarms that are no longer true clear, and the coil drops back to 0. |
| 5 | Inlet valve openread/write bit | Closed: nothing flows in. |
| 6 | Outlet valve openread/write bit | Closed: running pumps deadhead — pressure up, flow and current down. |
| 7 | Dosing pump enableread/write bit | |
| 8–63 | Freeread/write bit | Plain memory for your own tests. |
Discrete inputs (1x)
| Address | Name | Notes |
|---|---|---|
| 0–2 | Pump 1–3 running bit | Feedback, not the command: a pump the dry-run protection stopped shows 0 here with its run coil still 1. |
| 6 | High level float bit | Level at or above 470 cm. |
| 7 | Low level float bit | Level at or below 30 cm. |
| 8 | Door open bit | The kiosk door opens for 45 seconds every 1,000 (and latches the intrusion alarm). |
| 9 | Mains power OK bit |
Input registers (3x)
| Address | Name | Notes |
|---|---|---|
| 0 | Wet well level uint16, cm | 0–500. Rises with the inflow, falls while pumps run. |
| 1 | Inflow uint16, L/s × 10 | |
| 2 | Outflow uint16, L/s × 10 | 45 L/s per running pump at full speed. |
| 3–5 | Pump 1–3 motor current uint16, A × 10 | |
| 6 | Discharge pressure uint16, kPa | |
| 7–9 | Pump 1–3 starts uint16 | Counts every start, wraps at 65,535. |
| 10–11 | Total pumped volume uint32, m³, high word first | |
| 12–13 | Wet well level float32, m, ABCD | The level again, as an IEEE 754 float. |
| 14–15 | Discharge pressure float32, bar, ABCD | |
| 16 | Water temperature int16, °C × 10 | |
| 17 | Ambient temperature int16, °C × 10 | Goes below zero in the small hours: a signed register, two’s complement. |
| 18–19 | PLC uptime uint32, s | |
| 20 | Alarm word bits | Latched until reset: bit 0 high level, 1 low level, 5 intrusion, 7 overflow. |
| 21 | Status word bits | Bit 0 auto, 1–3 pumps running, 4 inlet open, 5 outlet open, 6 dosing, 7 an alarm is active. |
| 30–31 | Test float, ABCD float32 | 123.456, big-endian (0x42F6 0xE979). |
| 32–33 | Test float, CDAB float32 | Words swapped — the order many PLCs use. |
| 34–35 | Test float, BADC float32 | Bytes swapped inside each word. |
| 36–37 | Test float, DCBA float32 | Little-endian. |
| 38–39 | Test int32 int32, ABCD | -123456. |
| 40–43 | Test float64 float64, ABCDEFGH | 123.456. |
| 44–45 | Test uint32 uint32, ABCD | 3000000000 — wrong if read as signed. |
| 46–49 | Test int64 int64, ABCDEFGH | -1234567890123. |
| 50–59 | Station name ASCII, 2 per register, high byte first | “LS-01 LIFT STATION”. |
Holding registers (4x)
| Address | Name | Notes |
|---|---|---|
| 0 | Start levelread/write uint16, cm | Default 320; 0–500, else exception 3. |
| 1 | Stop levelread/write uint16, cm | Default 80. |
| 2 | High level alarmread/write uint16, cm | Default 450; a lag pump starts here. |
| 3 | Low level alarmread/write uint16, cm | Default 40; every pump stops here, auto or manual, and stays stopped until the level is 20 cm above it (dry-run protection). |
| 4 | Lead pumpread/write uint16, 1–3 | Alternates by itself after each pump-down. |
| 5 | Pump speedread/write uint16, % × 10 | Default 850 (85 %); 0–1000. Flow, current and pressure follow. |
| 6 | Most pumps at onceread/write uint16, 1–3 | Default 2. |
| 7 | Dosing rateread/write uint16, mL/min | 0–5000. |
| 8 | Station numberread/write uint16 | |
| 10–15 | PLC clockread/write year, month, day, hour, minute, second (UTC) | Write it and it keeps your time from then on. |
| 16–199 | Freeread/write uint16 | Plain memory for your own tests (9 too). |
Unit 2: Power meter PM-02
A three-phase meter on the main incomer: twenty float32 measurements, read with function 3 or 4, and a configuration block at 1000. Functions 3, 4, 6, 16, 17, 43.
Input registers (3x)
| Address | Name | Notes |
|---|---|---|
| 0–1 | Voltage L1–N float32, V, ABCD | |
| 2–3 | Voltage L2–N float32, V, ABCD | |
| 4–5 | Voltage L3–N float32, V, ABCD | |
| 6–7 | Voltage L1–L2 float32, V, ABCD | |
| 8–9 | Voltage L2–L3 float32, V, ABCD | |
| 10–11 | Voltage L3–L1 float32, V, ABCD | |
| 12–13 | Current L1 float32, A, ABCD | |
| 14–15 | Current L2 float32, A, ABCD | |
| 16–17 | Current L3 float32, A, ABCD | |
| 18–19 | Current N float32, A, ABCD | |
| 20–21 | Active power float32, kW, ABCD | |
| 22–23 | Reactive power float32, kvar, ABCD | |
| 24–25 | Apparent power float32, kVA, ABCD | |
| 26–27 | Power factor float32, ABCD | |
| 28–29 | Frequency float32, Hz, ABCD | |
| 30–31 | Active energy import float32, kWh, ABCD | Climbs for as long as the hub runs. |
| 32–33 | Reactive energy float32, kvarh, ABCD | |
| 34–35 | THD voltage float32, %, ABCD | |
| 36–37 | THD current float32, %, ABCD | |
| 38–39 | Maximum demand today float32, kW, ABCD | |
| 100–109 | Serial number ASCII | “SH-PM-000231”. |
| 110 | Firmware uint16, BCD-ish | 0x0213: version 2.19. |
Holding registers (4x)
| Address | Name | Notes |
|---|---|---|
| 0–127 | The same measurements as above | Function 3 reads the same map as function 4, as many meters allow; a write here is exception 2. |
| 1000 | CT primaryread/write uint16, A | Default 200; 5–9999. Set it wrong and every current, power and energy reading scales with it — as on a real meter. |
| 1001 | VT ratioread/write uint16 | Default 1; 1–1000. |
| 1002 | Demand intervalread/write uint16, min | Default 15; 1–60. |
| 1003 | Wiringread/write uint16 | 0 three-phase four-wire, 1 three-wire, 2 single-phase. |
| 1004–1009 | Freeread/write uint16 |
Unit 3: Variable frequency drive VFD-03
Pump 1’s drive: a control word, a speed reference and ramps that take the time you set — and an overcurrent trip if you ask for the impossible. Functions 3, 4, 6, 16, 17, 22, 43.
Input registers (3x)
| Address | Name | Notes |
|---|---|---|
| 0 | Status word bits | Bit 0 ready, 1 running, 2 at speed, 3 fault, 4 reverse, 5 remote. |
| 1 | Output frequency uint16, Hz × 100 | Ramps toward the reference at the times you set. |
| 2 | Motor current uint16, A × 10 | |
| 3 | Motor speed uint16, rpm | |
| 4 | DC bus voltage uint16, V | |
| 5 | Output power uint16, kW × 10 | |
| 6 | Heatsink temperature uint16, °C | |
| 7 | Fault code uint16 | 0 none, 1 overcurrent. Clear it with bit 2 of the control word. |
| 8 | Run hours uint16, h |
Holding registers (4x)
| Address | Name | Notes |
|---|---|---|
| 0 | Control wordread/write bits | Bit 0 run, bit 1 reverse, bit 2 fault reset. Starts running. |
| 1 | Speed referenceread/write uint16, Hz × 100 | Default 4500; above the maximum frequency is exception 3. |
| 2 | Acceleration timeread/write uint16, s × 10 | From 0 to the maximum. Start from standstill with 1.0 s or less and the drive trips on overcurrent. |
| 3 | Deceleration timeread/write uint16, s × 10 | |
| 4 | Maximum frequencyread/write uint16, Hz × 100 | Default 5000; 1000–6000. |
| 5–9 | Freeread/write uint16 |
Unit 4: Room sensor on a radio link
A battery sensor behind a slow link: every answer takes 0.6 to 1.8 seconds, for testing timeouts and transactions that overlap. Functions 3, 4, 6, 16, 17, 43.
Input registers (3x)
| Address | Name | Notes |
|---|---|---|
| 0 | Temperature int16, °C × 10 | |
| 1 | Relative humidity uint16, % × 10 | |
| 2 | CO₂ uint16, ppm | |
| 3 | Air pressure uint16, hPa × 10 | |
| 4 | Battery uint16, % | |
| 5 | Radio signal int16, dBm | Negative: signed. |
Holding registers (4x)
| Address | Name | Notes |
|---|---|---|
| 0 | Report intervalread/write uint16, s | 10–3600. |
| 1–9 | Freeread/write uint16 |
From a tool that only speaks TCP
Modbus Poll, pymodbus, libmodbus, a PLC’s own library — they open a TCP connection to port 502. The bridge is a small Node script that runs on your computer, listens on the usual TCP port and carries each message to the hub over WebSocket — nothing else happens in it, and nothing is installed:
curl -O https://sondahub.com/industrial/bridge.mjs node bridge.mjs modbus # then point the tool at localhost, port 5020
Questions
Can I use Modbus Poll, pymodbus or a PLC library?
Through the bridge: it listens on localhost port 5020 and carries each Modbus TCP frame to the gateway over WebSocket. Point the tool at 127.0.0.1:5020 and use the unit ids below. Port 502 needs administrator rights on most systems, so the bridge uses 5020 unless you give it another port.
Why does unit 0 never answer?
Because a broadcast never does: the specification has no reply to unit 0. Writes to unit 0 reach every device behind the gateway; reads to it go unanswered, and a client should time out — which is the point of trying it.
How are 32-bit values laid out?
High word first (ABCD) everywhere on the devices. Input registers 30 to 49 of unit 1 hold the same reference values in every order a device might use — float32 in ABCD, CDAB, BADC and DCBA, int32, uint32, float64 and int64 — so a client’s word and byte order settings can be checked against known numbers.