Serial RTU server
The library supports serial communication using the RTU transmission mode. Several servers can share one serial line, each answering for its own unit IDs, and a client can broadcast writes to all of them at once.
The library handles RTU framing for clients and servers alike: it derives the frame timing from the baud rate, and after line noise or a partial frame it discards bytes until a valid frame starts, logging a warning, without closing the port.
Creating a server
First build a DeviceMap for each unit ID to which the server will respond, as for a TCP server.
Then use the create_rtu static method of the Server class (spawn_rtu_server_task in Rust). It takes the same serial port
path and settings as an RTU client, and a RetryStrategy.
The server opens the port in the background. If the port can't be opened, or fails later, for example when a USB-to-serial
adapter is unplugged, the server reopens it, waiting as the RetryStrategy sets.
- Rust
- C
- C++
- Java
- C#
let handler = SimpleHandler::new(POINT_COUNT).wrap();
// map unit ids to a handler for processing requests
let map = ServerHandlerMap::single(UnitId::new(UNIT_ID), handler.clone());
let server = rodbus::server::spawn_rtu_server_task(
path,
rodbus::SerialSettings::default(),
default_retry_strategy(),
map,
DecodeLevel::new(
AppDecodeLevel::DataHeaders,
FrameDecodeLevel::Nothing,
PhysDecodeLevel::Nothing,
),
)?;
rodbus_device_map_t *map = rodbus_device_map_create();
rodbus_device_map_add_endpoint(map,
UNIT_ID, // unit id
get_write_handler(), // handler for write requests
(rodbus_database_callback_t){.callback = &configure_db} // callback for the initial state of the database
);
rodbus_server_t *server = NULL;
rodbus_decode_level_t decode_level = rodbus_decode_level_build(RODBUS_APP_DECODE_LEVEL_DATA_HEADERS, RODBUS_FRAME_DECODE_LEVEL_NOTHING, RODBUS_PHYS_DECODE_LEVEL_NOTHING);
rodbus_param_error_t err = rodbus_server_create_rtu(runtime, path, rodbus_serial_port_settings_init(), rodbus_retry_strategy_init(), map, decode_level, &server);
rodbus_device_map_destroy(map);
if (err) {
printf("unable to create server: %s\n", rodbus_param_error_to_string(err));
return NULL;
}
// POINT_COUNT points of each type, starting at index 0
auto init_transaction = rodbus::functional::database_callback([](rodbus::Database &db) {
for (uint16_t i = 0; i < POINT_COUNT; ++i) {
db.add_coil(i, false);
db.add_discrete_input(i, false);
db.add_holding_register(i, 0);
db.add_input_register(i, 0);
}
});
auto map = rodbus::DeviceMap();
map.add_endpoint(UNIT_ID, create_write_handler(), init_transaction);
auto server = rodbus::Server::create_rtu(
runtime,
path,
rodbus::SerialPortSettings(),
rodbus::RetryStrategy(),
map,
rodbus::DecodeLevel(rodbus::AppDecodeLevel::data_headers, rodbus::FrameDecodeLevel::nothing, rodbus::PhysDecodeLevel::nothing)
);
DeviceMap map = new DeviceMap();
map.addEndpoint(ubyte(UNIT_ID), new ExampleWriteHandler(), db -> {
for (int i = 0; i < POINT_COUNT; i++) {
db.addCoil(ushort(i), false);
db.addDiscreteInput(ushort(i), false);
db.addHoldingRegister(ushort(i), ushort(0));
db.addInputRegister(ushort(i), ushort(0));
}
});
Server server = Server.createRtu(
runtime,
path,
new SerialPortSettings(),
new RetryStrategy(),
map,
new DecodeLevel(AppDecodeLevel.DATA_HEADERS, FrameDecodeLevel.NOTHING, PhysDecodeLevel.NOTHING)
);
var map = new DeviceMap();
map.AddEndpoint(UnitId, new WriteHandler(), db =>
{
for (ushort i = 0; i < PointCount; ++i)
{
db.AddCoil(i, false);
db.AddDiscreteInput(i, false);
db.AddHoldingRegister(i, 0);
db.AddInputRegister(i, 0);
}
});
var server = Server.CreateRtu(
runtime,
path,
new SerialPortSettings(), // 9600 baud, 8 data bits, no parity, 1 stop bit
new RetryStrategy(),
map,
new DecodeLevel(AppDecodeLevel.DataHeaders, FrameDecodeLevel.Nothing, PhysDecodeLevel.Nothing)
);
Broadcast
The server applies a write request sent to the broadcast unit ID (0x00) to every unit in its map, and doesn't reply, so an
exception returned by your handler goes nowhere. It ignores any other broadcast, such as a read.
Reserved addresses
Don't use unit IDs 248 (0xF8) to 255 (0xFF), which the specification reserves. The library processes them like any other
unit ID, but logs a warning as a reminder of potential interoperability issues.