mbus-client 0.6.0

Modbus client stack for embedded and std environments with TCP, RTU, and ASCII transport support for modbus-rs project
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
use crate::app::RegisterResponse;
use crate::services::{
    ClientCommon, ClientServices, Mask, Multiple, OperationMeta, Single, register,
};
use mbus_core::{
    errors::MbusError,
    transport::{Transport, UnitIdOrSlaveAddr},
};

impl<TRANSPORT, APP, const N: usize> ClientServices<TRANSPORT, APP, N>
where
    TRANSPORT: Transport,
    APP: RegisterResponse + ClientCommon,
{
    /// Sends a Read Holding Registers request to the specified unit ID and address range, and records the expected response.
    ///
    /// # Parameters
    /// - `txn_id`: Transaction ID of the original request. While Modbus Serial (RTU/ASCII)
    ///   does not natively use transaction IDs, the stack preserves the ID provided in
    ///   the request and returns it here to allow for asynchronous tracking.
    /// - `unit_id_slave_addr`: The target Modbus unit ID or slave address.
    ///   - `unit_id`: if transport is tcp
    ///   - `slave_addr`: if transport is serial
    /// - `from_address`: The starting address of the holding registers to read.
    /// - `quantity`: The number of holding registers to read.
    ///
    /// # Returns
    /// `Ok(())` if the request was successfully enqueued and transmitted.
    ///
    /// # Errors
    /// Returns `Err(MbusError::BroadcastNotAllowed)` if attempting to read from address `0` (Broadcast).
    #[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
    pub fn read_holding_registers(
        &mut self,
        txn_id: u16,
        unit_id_slave_addr: UnitIdOrSlaveAddr,
        from_address: u16,
        quantity: u16,
    ) -> Result<(), MbusError> {
        if unit_id_slave_addr.is_broadcast() {
            return Err(MbusError::BroadcastNotAllowed); // Modbus forbids broadcast Read operations
        }

        let frame = register::service::ServiceBuilder::read_holding_registers(
            txn_id,
            unit_id_slave_addr.get(),
            from_address,
            quantity,
            TRANSPORT::TRANSPORT_TYPE,
        )?;

        self.add_an_expectation(
            txn_id,
            unit_id_slave_addr,
            &frame,
            OperationMeta::Multiple(Multiple {
                address: from_address, // Starting address of the read operation
                quantity,              // Number of registers to read
            }),
            Self::handle_read_holding_registers_response,
        )?;

        self.dispatch_request_frame(txn_id, unit_id_slave_addr, &frame)?;

        Ok(())
    }

    /// Sends a Read Holding Registers request for a single register (Function Code 0x03).
    ///
    /// This is a convenience wrapper around `read_holding_registers` with a quantity of 1.
    /// It allows the application to receive a simplified `read_single_holding_register_response`
    /// callback instead of handling a register collection.
    ///
    /// # Parameters
    /// - `txn_id`: Transaction ID of the original request. While Modbus Serial (RTU/ASCII)
    ///   does not natively use transaction IDs, the stack preserves the ID provided in
    ///   the request and returns it here to allow for asynchronous tracking.
    /// - `unit_id_slave_addr`: The target Modbus unit ID or slave address.
    ///   - `unit_id`: if transport is tcp
    ///   - `slave_addr`: if transport is serial
    /// - `address`: The starting address of the holding registers to read.
    ///
    /// # Returns
    /// `Ok(())` if the request was successfully enqueued and transmitted.
    ///
    /// # Errors
    /// Returns `Err(MbusError::BroadcastNotAllowed)` if attempting to read from address `0` (Broadcast).
    #[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
    pub fn read_single_holding_register(
        &mut self,
        txn_id: u16,
        unit_id_slave_addr: UnitIdOrSlaveAddr,
        address: u16,
    ) -> Result<(), MbusError> {
        use crate::services::Single;

        // Modbus protocol specification: Broadcast is not supported for Read operations.
        if unit_id_slave_addr.is_broadcast() {
            return Err(MbusError::BroadcastNotAllowed); // Modbus forbids broadcast Read operations
        }

        // Construct the ADU frame using the register service builder with quantity = 1
        let frame = register::service::ServiceBuilder::read_holding_registers(
            txn_id,
            unit_id_slave_addr.get(),
            address,
            1, // quantity = 1
            TRANSPORT::TRANSPORT_TYPE,
        )?;

        // Register an expectation. We use OperationMeta::Single to signal the response
        // handler to trigger the single-register specific callback in the app layer.
        self.add_an_expectation(
            txn_id,
            unit_id_slave_addr,
            &frame,
            OperationMeta::Single(Single {
                address,  // Address of the single register
                value: 0, // Value is not relevant for read requests
            }),
            Self::handle_read_holding_registers_response,
        )?;

        // Dispatch the compiled frame through the underlying transport.
        self.dispatch_request_frame(txn_id, unit_id_slave_addr, &frame)?;

        Ok(())
    }

    /// Sends a Read Input Registers request (Function Code 0x04).
    ///
    /// This function is used to read from 1 to 125 contiguous input registers in a remote device.
    /// Input registers are typically used for read-only data like sensor readings.
    ///
    /// # Parameters
    /// - `txn_id`: Transaction ID of the original request. While Modbus Serial (RTU/ASCII)
    ///   does not natively use transaction IDs, the stack preserves the ID provided in
    ///   the request and returns it here to allow for asynchronous tracking.
    /// - `unit_id_slave_addr`: The target Modbus unit ID or slave address.
    ///   - `unit_id`: if transport is tcp
    ///   - `slave_addr`: if transport is serial
    /// - `address`: The starting address of the input registers to read (0x0000 to 0xFFFF).
    /// - `quantity`: The number of input registers to read (1 to 125).
    ///
    /// # Returns
    /// - `Ok(())`: If the request was successfully built, the expectation was queued,
    ///   and the frame was transmitted.
    ///
    /// # Errors
    /// Returns `Err(MbusError::BroadcastNotAllowed)` if attempting to read from address `0` (Broadcast).
    #[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
    pub fn read_input_registers(
        &mut self,
        txn_id: u16,
        unit_id_slave_addr: UnitIdOrSlaveAddr,
        address: u16,
        quantity: u16,
    ) -> Result<(), MbusError> {
        if unit_id_slave_addr.is_broadcast() {
            return Err(MbusError::BroadcastNotAllowed); // Modbus forbids broadcast Read operations
        }

        let frame = register::service::ServiceBuilder::read_input_registers(
            txn_id,
            unit_id_slave_addr.get(),
            address,
            quantity,
            TRANSPORT::TRANSPORT_TYPE,
        )?;

        self.add_an_expectation(
            txn_id,
            unit_id_slave_addr,
            &frame,
            OperationMeta::Multiple(Multiple {
                address,  // Starting address of the read operation
                quantity, // Number of registers to read
            }),
            Self::handle_read_input_registers_response,
        )?;

        self.dispatch_request_frame(txn_id, unit_id_slave_addr, &frame)?;

        Ok(())
    }

    /// Sends a Read Input Registers request for a single register (Function Code 0x04).
    ///
    /// This is a convenience wrapper around `read_input_registers` with a quantity of 1.
    /// It allows the application to receive a simplified `read_single_input_register_response`
    /// callback instead of handling a register collection.
    ///
    /// # Parameters
    /// - `txn_id`: Transaction ID of the original request. While Modbus Serial (RTU/ASCII)
    ///   does not natively use transaction IDs, the stack preserves the ID provided in
    ///   the request and returns it here to allow for asynchronous tracking.
    /// - `unit_id_slave_addr`: The target Modbus unit ID or slave address.
    ///   - `unit_id`: if transport is tcp
    ///   - `slave_addr`: if transport is serial
    /// - `address`: The exact address of the input register to read.
    ///
    /// # Returns
    /// `Ok(())` if the request was successfully enqueued and transmitted.
    ///
    /// # Errors
    /// Returns `Err(MbusError::BroadcastNotAllowed)` if attempting to read from a broadcast address.
    #[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
    pub fn read_single_input_register(
        &mut self,
        txn_id: u16,
        unit_id_slave_addr: UnitIdOrSlaveAddr,
        address: u16,
    ) -> Result<(), MbusError> {
        if unit_id_slave_addr.is_broadcast() {
            return Err(MbusError::BroadcastNotAllowed); // Modbus forbids broadcast Read operations
        }

        let frame = register::service::ServiceBuilder::read_input_registers(
            txn_id,
            unit_id_slave_addr.get(),
            address,
            1,
            TRANSPORT::TRANSPORT_TYPE,
        )?;

        self.add_an_expectation(
            txn_id,
            unit_id_slave_addr,
            &frame,
            OperationMeta::Single(Single {
                address,  // Address of the single register
                value: 0, // Value is not relevant for read requests
            }),
            Self::handle_read_input_registers_response,
        )?;

        self.dispatch_request_frame(txn_id, unit_id_slave_addr, &frame)?;

        Ok(())
    }

    /// Sends a Write Single Register request (Function Code 0x06).
    ///
    /// This function is used to write a single holding register in a remote device.
    ///
    /// # Parameters
    /// - `txn_id`: Transaction ID of the original request. While Modbus Serial (RTU/ASCII)
    ///   does not natively use transaction IDs, the stack preserves the ID provided in
    ///   the request and returns it here to allow for asynchronous tracking.
    /// - `unit_id_slave_addr`: The target Modbus unit ID or slave address.
    ///   - `unit_id`: if transport is tcp
    ///   - `slave_addr`: if transport is serial
    /// - `address`: The address of the holding register to be written.
    /// - `value`: The 16-bit value to be written to the register.
    ///
    /// # Returns
    /// `Ok(())` if the request was successfully enqueued and transmitted.
    ///
    /// # Broadcast Support
    /// Serial Modbus (RTU/ASCII) allows broadcast writes (Slave Address 0). In this case,
    /// the request is sent to all slaves, and no response is expected or queued.
    ///
    /// # Errors
    /// Returns `Err(MbusError::BroadcastNotAllowed)` if attempting to broadcast over TCP.
    #[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
    pub fn write_single_register(
        &mut self,
        txn_id: u16,
        unit_id_slave_addr: UnitIdOrSlaveAddr,
        address: u16,
        value: u16,
    ) -> Result<(), MbusError> {
        let transport_type = TRANSPORT::TRANSPORT_TYPE;
        let frame = register::service::ServiceBuilder::write_single_register(
            txn_id,
            unit_id_slave_addr.get(),
            address,
            value,
            transport_type,
        )?;

        // Modbus TCP typically does not support broadcast.
        // Serial Modbus (RTU/ASCII) allows broadcast writes, but the client MUST NOT
        // expect a response from the server(s).
        if unit_id_slave_addr.is_broadcast() {
            if transport_type.is_tcp_type() {
                return Err(MbusError::BroadcastNotAllowed); // Modbus TCP typically does not support broadcast
            }
        } else {
            self.add_an_expectation(
                txn_id,
                unit_id_slave_addr,
                &frame,
                OperationMeta::Single(Single { address, value }),
                Self::handle_write_single_register_response, // Callback for successful response
            )?; // Expect a response for non-broadcast writes
        }

        self.dispatch_request_frame(txn_id, unit_id_slave_addr, &frame)?;
        Ok(())
    }

    /// Sends a Write Multiple Registers request (Function Code 0x10).
    ///
    /// This function is used to write a block of contiguous registers (1 to 123 registers)
    /// in a remote device.
    ///
    /// # Parameters
    /// - `txn_id`: Transaction ID of the original request. While Modbus Serial (RTU/ASCII)
    ///   does not natively use transaction IDs, the stack preserves the ID provided in
    ///   the request and returns it here to allow for asynchronous tracking.
    /// - `unit_id_slave_addr`: The target Modbus unit ID or slave address.
    ///   - `unit_id`: if transport is tcp
    ///   - `slave_addr`: if transport is serial
    /// - `quantity`: The number of registers to write (1 to 123).
    /// - `values`: A slice of `u16` values to be written. The length must match `quantity`.
    ///
    /// # Returns
    /// `Ok(())` if the request was successfully enqueued and transmitted.
    ///
    /// # Broadcast Support
    /// Serial Modbus allows broadcast. No response is expected for broadcast requests.
    ///
    /// # Errors
    /// Returns `Err(MbusError::BroadcastNotAllowed)` if attempting to broadcast over TCP.
    #[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
    pub fn write_multiple_registers(
        &mut self,
        txn_id: u16,
        unit_id_slave_addr: UnitIdOrSlaveAddr,
        address: u16,
        quantity: u16,
        values: &[u16],
    ) -> Result<(), MbusError> {
        let transport_type = TRANSPORT::TRANSPORT_TYPE;
        let frame = register::service::ServiceBuilder::write_multiple_registers(
            txn_id,
            unit_id_slave_addr.get(),
            address,
            quantity,
            values,
            transport_type,
        )?;

        // Modbus TCP typically does not support broadcast.
        // Serial Modbus (RTU/ASCII) allows broadcast writes, but the client MUST NOT
        // expect a response from the server(s).
        if unit_id_slave_addr.is_broadcast() {
            if transport_type.is_tcp_type() {
                return Err(MbusError::BroadcastNotAllowed); // Modbus TCP typically does not support broadcast
            }
        } else {
            self.add_an_expectation(
                txn_id,
                unit_id_slave_addr,
                &frame,
                OperationMeta::Multiple(Multiple { address, quantity }),
                Self::handle_write_multiple_registers_response, // Callback for successful response
            )?; // Expect a response for non-broadcast writes
        }

        self.dispatch_request_frame(txn_id, unit_id_slave_addr, &frame)?;
        Ok(())
    }

    /// Sends a Read/Write Multiple Registers request (FC 23).
    ///
    /// This function performs a combination of one read operation and one write operation in a single
    /// Modbus transaction. The write operation is performed before the read.
    ///
    /// # Parameters
    /// - `txn_id`: Transaction ID of the original request. While Modbus Serial (RTU/ASCII)
    ///   does not natively use transaction IDs, the stack preserves the ID provided in
    ///   the request and returns it here to allow for asynchronous tracking.
    /// - `unit_id_slave_addr`: The target Modbus unit ID or slave address.
    ///   - `unit_id`: if transport is tcp
    ///   - `slave_addr`: if transport is serial
    /// - `read_address`: The starting address of the registers to read.
    /// - `read_quantity`: The number of registers to read.
    /// - `write_address`: The starting address of the registers to write.
    /// - `write_values`: A slice of `u16` values to be written to the device.
    ///
    /// # Returns
    /// `Ok(())` if the request was successfully sent, or an `MbusError` if there was an error
    /// constructing the request (e.g., invalid quantity) or sending it over the transport.
    #[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
    pub fn read_write_multiple_registers(
        &mut self,
        txn_id: u16,
        unit_id_slave_addr: UnitIdOrSlaveAddr,
        read_address: u16,
        read_quantity: u16,
        write_address: u16,
        write_values: &[u16],
    ) -> Result<(), MbusError> {
        if unit_id_slave_addr.is_broadcast() {
            return Err(MbusError::BroadcastNotAllowed); // FC 23 explicitly forbids broadcast
        }

        // 1. Construct the ADU frame using the register service
        let transport_type = TRANSPORT::TRANSPORT_TYPE;
        let frame = register::service::ServiceBuilder::read_write_multiple_registers(
            txn_id,
            unit_id_slave_addr.get(),
            read_address,
            read_quantity,
            write_address,
            write_values,
            transport_type,
        )?;

        // 2. Queue the expected response to match against the incoming server reply
        self.add_an_expectation(
            txn_id,
            unit_id_slave_addr,
            &frame,
            OperationMeta::Multiple(Multiple {
                address: read_address,   // Starting address of the read operation
                quantity: read_quantity, // Number of registers to read
            }),
            Self::handle_read_write_multiple_registers_response,
        )?;

        // 3. Transmit the frame via the configured transport
        self.dispatch_request_frame(txn_id, unit_id_slave_addr, &frame)?;
        Ok(())
    }

    /// Sends a Mask Write Register request.
    ///
    /// This function is used to modify the contents of a single holding register using a combination
    /// of an AND mask and an OR mask. The new value of the register is calculated as:
    /// `(current_value AND and_mask) OR (or_mask AND (NOT and_mask))`
    ///
    /// The request is added to the `expected_responses` queue to await a corresponding reply from the Modbus server.
    ///
    /// # Parameters
    /// - `txn_id`: Transaction ID of the original request. While Modbus Serial (RTU/ASCII)
    ///   does not natively use transaction IDs, the stack preserves the ID provided in
    ///   the request and returns it here to allow for asynchronous tracking.
    /// - `unit_id_slave_addr`: The target Modbus unit ID or slave address.
    ///   - `unit_id`: if transport is tcp
    ///   - `slave_addr`: if transport is serial
    /// - `address`: The address of the register to apply the mask to.
    /// - `and_mask`: The 16-bit AND mask to apply to the current register value.
    /// - `or_mask`: The 16-bit OR mask to apply to the current register value.
    ///
    /// # Returns
    /// `Ok(())` if the request was successfully sent and queued for a response,
    /// or an `MbusError` if there was an error during request construction,
    /// sending over the transport, or if the `expected_responses` queue is full.
    #[must_use = "request submission errors should be handled; the request may not have been queued/sent"]
    pub fn mask_write_register(
        &mut self,
        txn_id: u16,
        unit_id_slave_addr: UnitIdOrSlaveAddr,
        address: u16,
        and_mask: u16,
        or_mask: u16,
    ) -> Result<(), MbusError> {
        let frame = register::service::ServiceBuilder::mask_write_register(
            txn_id,
            unit_id_slave_addr.get(),
            address,
            and_mask,
            or_mask,
            TRANSPORT::TRANSPORT_TYPE,
        )?;

        if unit_id_slave_addr.is_broadcast() {
            if TRANSPORT::TRANSPORT_TYPE.is_tcp_type() {
                return Err(MbusError::BroadcastNotAllowed);
            }
        } else {
            self.add_an_expectation(
                txn_id,
                unit_id_slave_addr,
                &frame,
                OperationMeta::Masking(Mask {
                    address,  // Address of the register to mask
                    and_mask, // AND mask used in the request
                    or_mask,  // OR mask used in the request
                }),
                Self::handle_mask_write_register_response,
            )?;
        }

        self.dispatch_request_frame(txn_id, unit_id_slave_addr, &frame)?;
        Ok(())
    }
}