Skip to content

Latest commit

 

History

History
242 lines (175 loc) · 5.3 KB

File metadata and controls

242 lines (175 loc) · 5.3 KB

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

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

#[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:

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>

Example

#[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.

#[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

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

fn on_write_0(&mut self, _addr: u16, old: bool, new: bool) -> Result<(), MbusError> {
    if !old && new {
        self.start_motor();
    }
    Ok(())
}

Durable-save scheduling

fn on_registers_written(&mut self, _start: u16, _qty: u16, _values: &[u16]) -> Result<(), MbusError> {
    self.needs_save = true;
    Ok(())
}

See Also