Docs
Beginner's guide
Modbus Sandbox is a free server on the internet that pretends to be ten Modbus devices at once. Point your client - the program that polls field devices - at it and see how it reads and writes data. No sign-up needed, everything happens in real time.
What to know before you start:
- Modbus-TCP talks over an address and a port. Ours is
modbus-sendbox.electroman.by, port5020. - Inside live 10 devices - they are called slaves and numbered 1 to 10. The device number in a request is the unit ID.
- Data lives in registers - cells holding numbers or bits. Four areas: HR (holding registers) can be read and written, IR (input registers) are read-only, CL (coils) are writable bits, DI (discrete inputs) are read-only bits.
- Clients talk to the server with functions: 1 and 2 read bits, 3 and 4 read registers, 5 and 6 write a single cell, 16 writes up to two registers at once. The server rejects other functions.
Your first check, step by step:
- Open ModbusToolkit (or any other Modbus client) and create a Modbus TCP connection to host
modbus-sendbox.electroman.byport5020. - Set unit ID 1 and read 10 holding registers from address 1 with function 3. A list of numbers comes back.
- Write your own value to the first register with function 6 and read it again - the value matches.
- Open the «Register map» section of this site and find the same address: you see the same value. Every connected user sees it too, and anyone may overwrite it.
- Look at «Behaviors» to assign a generator (counter, saw, sine) to a register, and at «Injections» to give a slave an error or a delayed response.
A quick map of the site sections:
- «Overview» - address, port and data formats per slave.
- «Register map» - live values and editing.
- «Behaviors» - automatic value generation.
- «Time zones» - 27 time zones and the Unix timestamp of slave 10.
- «Injections» - faking device faults.
- «Journal» - who changed what.
- «Tools» - byte converter, ready configs for tools and snapshot comparison.
Usage examples
Below are short client checks against live data; each takes a few minutes. The examples use ModbusToolkit, but any Modbus client works. Demo slots fill themselves in, and free registers are shared - another participant may overwrite them.
Check 32-bit value packing (uint32 CDAB)
The CDAB word order is the most common mistake: the high word is stored second. Open the «Register map», slave 3, and find demo slot 1009 or the sine at slot 1015. With function 3 read two registers from that address (device_id=3), assemble the value the way your client does, and compare it with the UI. A match means your word order is right. Then write your own value with function 6 into the free register at address 1 and read it back.
Check the reaction to a missing reply (timeout)
A client must not hang when the device goes silent. In the «Injections» section add a timeout injection on slave 1 with a 30 s TTL and run a read loop in your client with a 1-2 s timeout. While the injection lives, no replies come: the client must report an error instead of freezing. When the TTL ends, reading recovers on its own, no client restart needed. A useful variation is an exception injection with code 0x06: the client should handle Device Busy properly.
Check floats with swapped bytes (float DCBA)
The DCBA order is easy to miss: garbage numbers can look plausible. Take slave 9 and demo slot 1015 with the sine, read two registers in your client and assemble a float. Then paste the same two registers into the converter in «Tools»: it shows the value in all four orders. Your value should match the DCBA row and the live slot value.
Check time and time zones (slave 10)
The «Time zones» section shows 27 zones with date, time and UTC offset, plus the Unix timestamp at registers 300..301. Read them from your client and compare with the system clock: a drift of more than a couple of seconds means the uint32 or time zone parsing is wrong.
Format specification
Each unit ID emulates its own HR/IR data format. A 32-bit value occupies two registers N and N+1; the value address is the address of the high register (grid 1, 3, …, 1023). ABCD letters are the value bytes from the high byte (A) to the low byte (D).
ABCD- big-endian words (high first), big-endian bytes in words;CDAB- little-endian words (low first), big-endian bytes in words;BADC- big-endian words, little-endian bytes in words (swap bytes);DCBA- little-endian words, little-endian bytes in words (swap bytes and swap words).
| Unit ID | HR/IR format | HR | IR | CL | DI |
|---|---|---|---|---|---|
| 1 | uint16 | 1..1024 | 1..1024 | 1..64 | 1..64 |
| 2 | uint32 ABCD | 1,3,…,1023 | 1,3,…,1023 | 1..64 | 1..64 |
| 3 | uint32 CDAB | 1,3,…,1023 | 1,3,…,1023 | 1..64 | 1..64 |
| 4 | uint32 BADC | 1,3,…,1023 | 1,3,…,1023 | 1..64 | 1..64 |
| 5 | uint32 DCBA | 1,3,…,1023 | 1,3,…,1023 | 1..64 | 1..64 |
| 6 | float ABCD | 1,3,…,1023 | 1,3,…,1023 | 1..64 | 1..64 |
| 7 | float CDAB | 1,3,…,1023 | 1,3,…,1023 | 1..64 | 1..64 |
| 8 | float BADC | 1,3,…,1023 | 1,3,…,1023 | 1..64 | 1..64 |
| 9 | float DCBA | 1,3,…,1023 | 1,3,…,1023 | 1..64 | 1..64 |
| 10 | uint16 | - | 1..268, 300..301 | - | - |
Slave 10 serves IR only. Addresses 1..268 hold metrics for 27 time zones (8 registers per zone, unused block registers are zero), addresses 300..301 hold the Unix timestamp (uint32 ABCD, 0.5 s accuracy). The full specification lives in docs/HRIR_SPEC.md of the repository.
Limits
- Allowed functions
1, 2, 3, 4, 5, 6, 16. Anything else is rejected as Illegal Function (0x01). - FC 5 and FC 6 write a single CL/HR, FC 16 writes HR and accepts no more than 2 registers per request (otherwise Illegal Data Value, 0x03).
- Slave 10 serves input registers only. Reading CL/DI/HR returns Illegal Data Address (0x02).
- Addressing follows docs/HRIR_SPEC.md with no conversion. The value address equals the wire address.
- Environment limits: connections per IP - 20, Modbus writes per second per connection - 200, UI writes per minute per IP - 50.
- Error injections last at most 300 s (INJECTION_MAX_TTL_S).
- The journal keeps the last 1000 events. Register values and the whole history live in memory only, a server restart wipes everything.
Multiplayer rules
- HR and CL are shared (shared last-write-wins). Any user write is immediately visible to everyone else and can be overwritten by anyone at any time. This is how the server works, not a bug.
- IR and DI are generated automatically by the behavior engine (counters, patterns) and are read-only.
- Behavior settings and error injections are shared across all connections too.
- There is no authentication. Do not pass sensitive data through the server and do not use it as a bench for real equipment.
API
The REST API is described in Swagger UI at /api/docs (prefix /api/modbus/). WebSocket /ws/state streams live slave state, /ws/events streams journal events. Connection examples are collected below.
Open Swagger UI OpenAPI schema (JSON)
Connect from ModbusToolkit
ModbusToolkit is a separate desktop Modbus client by the same author; Modbus Sandbox is not tied to it and works with any client. Create a Modbus TCP connection to the sandbox and test your client code against live data.
- Create a Modbus TCP connection: host
modbus-sendbox.electroman.by, port5020, Unit ID 1..10. - Read 10 holding registers of slave 1 with function 3 from address 1. Slave 1 values use the uint16 format.
- Write a value with function 6 and see that the data is shared. Every connected client sees the write (shared last-write-wins).
Download ModbusToolkit at modbustoolkit.electroman.by. Product docs: docs.html. News and support: t.me/modbustoolkit.
Code examples
Ready snippets for pymodbus, modbus-serial and NModbus. Addressing follows docs/HRIR_SPEC.md with no conversion; unit ID is 1..10. HR/CL are shared across all connections (shared last-write-wins).
Python · pymodbus
# pip install "pymodbus>=3.6"
from pymodbus.client import ModbusTcpClient
client = ModbusTcpClient("modbus-sendbox.electroman.by", port=5020)
if not client.connect():
raise SystemExit("no connection to the server")
# FC 3: read 10 holding registers of slave 1 (uint16) from address 1
result = client.read_holding_registers(1, count=10, device_id=1)
if result.isError():
raise SystemExit(f"Modbus error: {result}")
print(result.registers)
# FC 4: input registers (slave 10 has only these)
result = client.read_input_registers(1, count=8, device_id=10)
# FC 5 / FC 6 / FC 16: write coils and holding registers (values are shared!)
client.write_coil(1, True, device_id=1)
client.write_register(1, 42, device_id=1)
client.write_registers(1, [1, 2], device_id=1) # FC 16, max 2 registers per write
client.close()
Node.js · modbus-serial
// npm install modbus-serial
const ModbusRTU = require("modbus-serial");
async function main() {
const client = new ModbusRTU();
await client.connectTCP("modbus-sendbox.electroman.by", { port: 5020 });
client.setID(1); // unit ID 1..10
// FC 3: 10 holding registers from address 1
const result = await client.readHoldingRegisters(1, 10);
console.log(result.data);
// FC 4: input registers (slave 10 has only these)
client.setID(10);
console.log((await client.readInputRegisters(1, 8)).data);
// FC 5 / FC 6: write coils and holding registers (values are shared!)
client.setID(1);
await client.writeCoil(1, true);
await client.writeRegister(1, 42);
client.close();
}
main().catch(console.error);
C# · NModbus
// dotnet add package NModbus
using System.Net.Sockets;
using NModbus;
var tcp = new TcpClient("modbus-sendbox.electroman.by", 5020);
var client = new ModbusFactory().CreateMaster(tcp);
// FC 3: read 10 holding registers of slave 1 (uint16) from address 1
ushort[] regs = client.ReadHoldingRegisters(slaveId: 1, startAddress: 1, numberOfPoints: 10);
Console.WriteLine(string.Join(", ", regs));
// FC 4: input registers (slave 10 has only these)
ushort[] ir = client.ReadInputRegisters(1, 1, 8);
// FC 5 / FC 6: write coils and holding registers (values are shared!)
client.WriteSingleCoil(1, 1, true);
client.WriteSingleRegister(1, 1, 42);
Source code and your own server
The server code is open and lives on GitHub: StarLeG/modbustoolkit-sandbox. Run your own copy on an isolated bench network without internet access, or wherever injections and writes must not affect other users.
1. Clone the repository
git clone https://github.com/StarLeG/modbustoolkit-sandbox.git
cd modbustoolkit-sandbox
2. Run with uv (recommended)
# requires Python 3.11+ and uv: https://docs.astral.sh/uv/
uv sync
uv run python -m app
…or in Docker
cp .env.example .env # adjust MODBUS_PUBLIC_HOST and the ports
docker compose up -d --build
…or with pip, without uv
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install .
python -m app
Once started, the web UI opens at http://127.0.0.1:8765 and Modbus listens on port 5020 on all interfaces (variables WEB_HOST/WEB_PORT and MODBUS_TCP_PORT). Set MODBUS_PUBLIC_HOST to your machine's IP or name, otherwise «Overview» and the config generator show the public server address. Only Docker Compose reads the .env file; with python -m app set the variables in the environment. All variables are listed in .env.example, nginx and systemd setups are in docs/DEPLOY.md.
The server is free and stays that way. We are raising money for a test bench with real instruments to run every ModbusToolkit build against.