Write-hook behavior generated by #[modbus_app(...)] for coils and holding registers.
These rules describe the macro-generated write path. If you implement the split traits manually, you control the mutation order yourself.
| Hook | Applies to | Trigger |
|---|---|---|
on_write_N = fn_name |
coils, holding_registers |
FC05 or FC06 for address N |
on_batch_write = fn_name |
coils, holding_registers |
FC0F, FC10, FC17 write half, and FC05 or FC06 when notify_via_batch = true applies |
Per-field hooks are address-specific. Batch hooks see the exact payload slice that is about to be committed for that routed map.
For macro-generated handlers, hooks run before the map is mutated.
That means:
Ok(())allows the write to be committedErr(MbusError::...)rejects the write and causes an exception response- hook errors do not apply the mutation first and then report failure
This is the main difference from the older documentation.
For FC05 and FC06, the generated logic is:
- If
on_write_Nexists for that address, call it first with the old and new value - Otherwise, if the field was marked
notify_via_batch = trueand the routed group hason_batch_write, call the batch hook withquantity = 1 - If the chosen hook returns
Ok(()), commit the write - If the chosen hook returns
Err(...), reject the write
If neither hook path applies, the macro commits the write silently.
fn on_write_N(&mut self, address: u16, old_value: T, new_value: T) -> Result<(), MbusError>Where T is bool for coils and u16 for holding registers.
#[derive(Default, CoilsModel)]
struct Outputs {
#[coil(addr = 0)]
motor_enable: bool,
}
#[modbus_app(
coils(outputs, on_write_0 = on_motor_enable_changed),
)]
struct App {
outputs: Outputs,
alarm_active: bool,
}
impl App {
fn on_motor_enable_changed(
&mut self,
address: u16,
old_value: bool,
new_value: bool,
) -> Result<(), MbusError> {
if self.alarm_active && new_value {
return Err(MbusError::InvalidValue);
}
println!("coil {} changed: {} -> {}", address, old_value, new_value);
Ok(())
}
}For FC0F, FC10, and the write half of FC17, only the batch hook participates.
The generated order is:
- call
on_batch_write(...)if configured for the routed map - if it returns
Ok(()), commit the write - if it returns
Err(...), reject the write
There is no automatic per-address on_write_N expansion for multi-address writes.
For coils:
fn on_batch_write(
&mut self,
address: u16,
quantity: u16,
packed_bits: &[u8],
) -> Result<(), MbusError>For holding registers:
fn on_batch_write(
&mut self,
address: u16,
quantity: u16,
values: &[u16],
) -> Result<(), MbusError>#[derive(Default, HoldingRegistersModel)]
struct Registers {
#[reg(addr = 0)]
setpoint: u16,
#[reg(addr = 1)]
limit: u16,
}
#[modbus_app(
holding_registers(registers, on_batch_write = on_registers_written),
)]
struct App {
registers: Registers,
dirty: bool,
}
impl App {
fn on_registers_written(
&mut self,
start: u16,
quantity: u16,
values: &[u16],
) -> Result<(), MbusError> {
println!("write start={} qty={} values={:?}", start, quantity, values);
self.dirty = true;
Ok(())
}
}notify_via_batch = true changes only the single-write path.
#[derive(Default, HoldingRegistersModel)]
struct Registers {
#[reg(addr = 0)]
ordinary_field: u16,
#[reg(addr = 1, notify_via_batch = true)]
batch_routed_field: u16,
}With that field configuration:
- FC06 to address
1goes toon_batch_write(...)if noon_write_1hook exists - the batch hook receives
quantity = 1 - the write is committed only if the hook returns
Ok(())
If notify_via_batch = true is used without an on_batch_write hook on that routed group, the macro emits a compile error.
You can declare both on_write_N and on_batch_write on the same routed group, but they do not stack for a single-address write.
For FC05 and FC06:
on_write_Nwins when presenton_batch_writeis used only when no per-address hook handled that single write andnotify_via_batch = trueapplies
For FC0F, FC10, and FC17 writes:
- only
on_batch_writeruns
fn on_write_0(&mut self, _addr: u16, _old: u16, new: u16) -> Result<(), MbusError> {
if new > 1000 {
return Err(MbusError::InvalidValue);
}
Ok(())
}fn on_write_0(&mut self, _addr: u16, old: bool, new: bool) -> Result<(), MbusError> {
if !old && new {
self.start_motor();
}
Ok(())
}fn on_registers_written(&mut self, _start: u16, _qty: u16, _values: &[u16]) -> Result<(), MbusError> {
self.needs_save = true;
Ok(())
}