Skip to content

Commit f2006d7

Browse files
committed
Add Slave and SlaveContext
1 parent fa612b1 commit f2006d7

5 files changed

Lines changed: 169 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33

44
# Changelog
55

6+
## v0.3.0 (unpublished)
7+
8+
- Added `Slave` and `SlaveContext`
9+
610
## v0.2.0 (2025-09-30)
711

812
- Added TCP client implementation

src/codec/rtu/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
//! Modbus RTU
55
66
use super::*;
7+
use crate::SlaveId;
78
use byteorder::{BigEndian, ByteOrder};
89

910
pub mod client;

src/frame/rtu.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
// SPDX-FileCopyrightText: Copyright (c) 2018-2025 slowtec GmbH <post@slowtec.de>
22
// SPDX-License-Identifier: MIT OR Apache-2.0
33

4-
use super::*;
4+
use crate::SlaveId;
55

6-
/// Slave ID
7-
pub type SlaveId = u8;
6+
use super::*;
87

98
/// RTU header
109
#[cfg_attr(all(feature = "defmt", target_os = "none"), derive(defmt::Format))]

src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,17 @@
1616
#![allow(clippy::similar_names)] // TODO
1717
#![allow(clippy::wildcard_imports)]
1818

19+
#[cfg(feature = "std")]
20+
extern crate std;
21+
1922
mod codec;
2023
mod error;
2124
mod frame;
25+
mod slave;
2226

2327
pub use codec::rtu;
2428
pub use codec::tcp;
2529
pub use codec::*;
2630
pub use error::*;
2731
pub use frame::*;
32+
pub use slave::*;

src/slave.rs

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2018-2025 slowtec GmbH <post@slowtec.de>
2+
// SPDX-License-Identifier: MIT OR Apache-2.0
3+
4+
//! Modbus devices
5+
6+
use core::{fmt, num::ParseIntError, str::FromStr};
7+
8+
/// Slave identifier
9+
pub type SlaveId = u8;
10+
11+
/// A single byte for addressing Modbus slave devices.
12+
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
13+
pub struct Slave(pub SlaveId);
14+
15+
impl Slave {
16+
/// The special address for sending a broadcast message to all
17+
/// connected Modbus slave devices at once. Broadcast messages
18+
/// are one-way and sent from the master to all slaves, i.e.
19+
/// a request without a response.
20+
///
21+
/// Some devices may use a custom id from the reserved range
22+
/// 248-255 for broadcasting.
23+
#[must_use]
24+
pub const fn broadcast() -> Self {
25+
Slave(0)
26+
}
27+
28+
/// The minimum address of a single Modbus slave device.
29+
#[must_use]
30+
pub const fn min_device() -> Self {
31+
Slave(1)
32+
}
33+
34+
/// The maximum address of a single Modbus slave device.
35+
#[must_use]
36+
pub const fn max_device() -> Self {
37+
Slave(247)
38+
}
39+
40+
/// The reserved address for sending a message to a directly
41+
/// connected Modbus TCP device, i.e. if not forwarded through
42+
/// a TCP/RTU gateway according to the unit identifier.
43+
///
44+
/// [Modbus Messaging on TCP/IP Implementation Guide](http://www.modbus.org/docs/Modbus_Messaging_Implementation_Guide_V1_0b.pdf), page 23
45+
/// "On TCP/IP, the Modbus server is addressed using its IP address; therefore,
46+
/// the Modbus Unit Identifier is useless. The value 0xFF has to be used."
47+
#[must_use]
48+
pub const fn tcp_device() -> Self {
49+
Slave(255)
50+
}
51+
52+
/// Check if the [`SlaveId`] is used for broadcasting
53+
#[must_use]
54+
pub fn is_broadcast(self) -> bool {
55+
self == Self::broadcast()
56+
}
57+
58+
/// Check if the [`SlaveId`] addresses a single device
59+
#[must_use]
60+
pub fn is_single_device(self) -> bool {
61+
self >= Self::min_device() && self <= Self::max_device()
62+
}
63+
64+
/// Check if the [`SlaveId`] is reserved
65+
#[must_use]
66+
pub fn is_reserved(self) -> bool {
67+
self > Self::max_device()
68+
}
69+
}
70+
71+
impl From<SlaveId> for Slave {
72+
fn from(from: SlaveId) -> Self {
73+
Slave(from)
74+
}
75+
}
76+
77+
impl From<Slave> for SlaveId {
78+
fn from(from: Slave) -> Self {
79+
from.0
80+
}
81+
}
82+
83+
impl FromStr for Slave {
84+
type Err = ParseIntError;
85+
86+
fn from_str(s: &str) -> Result<Self, Self::Err> {
87+
let slave_id = match s.parse::<u8>() {
88+
Ok(slave_id) => Ok(slave_id),
89+
Err(err) => {
90+
if let Some(stripped) = s.strip_prefix("0x") {
91+
u8::from_str_radix(stripped, 16)
92+
} else {
93+
Err(err)
94+
}
95+
}
96+
}?;
97+
Ok(Slave(slave_id))
98+
}
99+
}
100+
101+
impl fmt::Display for Slave {
102+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103+
write!(f, "{} (0x{:0>2X})", self.0, self.0)
104+
}
105+
}
106+
107+
/// Stateful management of the currently active device.
108+
///
109+
/// RTU devices are addressed by their assigned *slave id*.
110+
///
111+
/// TCP devices are either addressed directly (= implicitly) by using the
112+
/// reserved *unit id* `Slave::tcp_device() = 0xFF` (default) or indirectly
113+
/// through an TCP/RTU gateway by setting the *unit id* to the desired
114+
/// *slave id*.
115+
///
116+
/// The names *slave id* and *unit id* are used synonymously depending
117+
/// on the context. This library consistently adopted the term *slave*.
118+
pub trait SlaveContext {
119+
/// Select a slave device for all subsequent outgoing requests.
120+
fn set_slave(&mut self, slave: Slave);
121+
}
122+
123+
#[cfg(all(test, feature = "std"))]
124+
mod tests {
125+
use super::*;
126+
127+
use std::format;
128+
129+
#[test]
130+
fn parse_dec() {
131+
assert_eq!(Slave(0), Slave::from_str("0").unwrap());
132+
assert_eq!(Slave(123), Slave::from_str("123").unwrap());
133+
assert_eq!(Slave(255), Slave::from_str("255").unwrap());
134+
assert!(Slave::from_str("-1").is_err());
135+
assert!(Slave::from_str("256").is_err());
136+
}
137+
138+
#[test]
139+
fn parse_hex() {
140+
assert_eq!(Slave(0), Slave::from_str("0x00").unwrap());
141+
assert_eq!(Slave(123), Slave::from_str("0x7b").unwrap());
142+
assert_eq!(Slave(123), Slave::from_str("0x7B").unwrap());
143+
assert_eq!(Slave(255), Slave::from_str("0xff").unwrap());
144+
assert_eq!(Slave(255), Slave::from_str("0xFF").unwrap());
145+
assert!(Slave::from_str("0X00").is_err());
146+
assert!(Slave::from_str("0x100").is_err());
147+
assert!(Slave::from_str("0xfff").is_err());
148+
assert!(Slave::from_str("0xFFF").is_err());
149+
}
150+
151+
#[test]
152+
fn format() {
153+
assert!(format!("{}", Slave(123)).contains("123"));
154+
assert!(format!("{}", Slave(0x7B)).contains("0x7B"));
155+
assert!(!format!("{}", Slave(0x7B)).contains("0x7b"));
156+
}
157+
}

0 commit comments

Comments
 (0)