# Server Write Hooks Write-hook behavior generated by `#[modbus_app(...)]` for coils and holding registers. --- ## Scope These rules describe the macro-generated write path. If you implement the split traits manually, you control the mutation order yourself. --- ## Hook Types | 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. --- ## Critical Behavior For macro-generated handlers, hooks run before the map is mutated. That means: - `Ok(())` allows the write to be committed - `Err(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. --- ## Single-Write Flow For FC05 and FC06, the generated logic is: 1. If `on_write_N` exists for that address, call it first with the old and new value 2. Otherwise, if the field was marked `notify_via_batch = true` and the routed group has `on_batch_write`, call the batch hook with `quantity = 1` 3. If the chosen hook returns `Ok(())`, commit the write 4. If the chosen hook returns `Err(...)`, reject the write If neither hook path applies, the macro commits the write silently. ### Per-field signature ```rust 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. ### Example ```rust #[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(()) } } ``` --- ## Batch-Write Flow For FC0F, FC10, and the write half of FC17, only the batch hook participates. The generated order is: 1. call `on_batch_write(...)` if configured for the routed map 2. if it returns `Ok(())`, commit the write 3. if it returns `Err(...)`, reject the write There is no automatic per-address `on_write_N` expansion for multi-address writes. ### Batch signatures For coils: ```rust fn on_batch_write( &mut self, address: u16, quantity: u16, packed_bits: &[u8], ) -> Result<(), MbusError> ``` For holding registers: ```rust fn on_batch_write( &mut self, address: u16, quantity: u16, values: &[u16], ) -> Result<(), MbusError> ``` ### Example ```rust #[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` `notify_via_batch = true` changes only the single-write path. ```rust #[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 `1` goes to `on_batch_write(...)` if no `on_write_1` hook 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. --- ## Combining Hooks 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_N` wins when present - `on_batch_write` is used only when no per-address hook handled that single write and `notify_via_batch = true` applies For FC0F, FC10, and FC17 writes: - only `on_batch_write` runs --- ## Common Uses ### Validation ```rust fn on_write_0(&mut self, _addr: u16, _old: u16, new: u16) -> Result<(), MbusError> { if new > 1000 { return Err(MbusError::InvalidValue); } Ok(()) } ``` ### Side effects before commit ```rust fn on_write_0(&mut self, _addr: u16, old: bool, new: bool) -> Result<(), MbusError> { if !old && new { self.start_motor(); } Ok(()) } ``` ### Durable-save scheduling ```rust fn on_registers_written(&mut self, _start: u16, _qty: u16, _values: &[u16]) -> Result<(), MbusError> { self.needs_save = true; Ok(()) } ``` --- ## See Also - [Macros](macros.md) - [Sync Server Applications](sync.md) - [Function Codes](function_codes.md)