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
// SPDX-FileCopyrightText: Copyright (c) 2017-2024 slowtec GmbH <post@slowtec.de>
// SPDX-License-Identifier: MIT OR Apache-2.0
//! Modbus devices
use std::{fmt, num::ParseIntError, str::FromStr};
/// Slave identifier
pub type SlaveId = u8;
/// A single byte for addressing Modbus slave devices.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Slave(pub SlaveId);
impl Slave {
/// The special address for sending a broadcast message to all
/// connected Modbus slave devices at once. Broadcast messages
/// are one-way and sent from the master to all slaves, i.e.
/// a request without a response.
///
/// Some devices may use a custom id from the reserved range
/// 248-255 for broadcasting.
#[must_use]
pub const fn broadcast() -> Self {
Slave(0)
}
/// The minimum address of a single Modbus slave device.
#[must_use]
pub const fn min_device() -> Self {
Slave(1)
}
/// The maximum address of a single Modbus slave device.
#[must_use]
pub const fn max_device() -> Self {
Slave(247)
}
/// The reserved address for sending a message to a directly
/// connected Modbus TCP device, i.e. if not forwarded through
/// a TCP/RTU gateway according to the unit identifier.
///
/// [Modbus Messaging on TCP/IP Implementation Guide](http://www.modbus.org/docs/Modbus_Messaging_Implementation_Guide_V1_0b.pdf), page 23
/// "On TCP/IP, the Modbus server is addressed using its IP address; therefore,
/// the Modbus Unit Identifier is useless. The value 0xFF has to be used."
#[must_use]
pub const fn tcp_device() -> Self {
Slave(255)
}
/// Check if the [`SlaveId`] is used for broadcasting
#[must_use]
pub fn is_broadcast(self) -> bool {
self == Self::broadcast()
}
/// Check if the [`SlaveId`] addresses a single device
#[must_use]
pub fn is_single_device(self) -> bool {
self >= Self::min_device() && self <= Self::max_device()
}
/// Check if the [`SlaveId`] is reserved
#[must_use]
pub fn is_reserved(self) -> bool {
self > Self::max_device()
}
}
impl From<SlaveId> for Slave {
fn from(from: SlaveId) -> Self {
Slave(from)
}
}
impl From<Slave> for SlaveId {
fn from(from: Slave) -> Self {
from.0
}
}
impl FromStr for Slave {
type Err = ParseIntError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let slave_id = match s.parse::<u8>() {
Ok(slave_id) => Ok(slave_id),
Err(err) => {
if let Some(stripped) = s.strip_prefix("0x") {
u8::from_str_radix(stripped, 16)
} else {
Err(err)
}
}
}?;
Ok(Slave(slave_id))
}
}
impl fmt::Display for Slave {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} (0x{:0>2X})", self.0, self.0)
}
}
/// Stateful management of the currently active device.
///
/// RTU devices are addressed by their assigned *slave id*.
///
/// TCP devices are either addressed directly (= implicitly) by using the
/// reserved *unit id* `Slave::tcp_device() = 0xFF` (default) or indirectly
/// through an TCP/RTU gateway by setting the *unit id* to the desired
/// *slave id*.
///
/// The names *slave id* and *unit id* are used synonymously depending
/// on the context. This library consistently adopted the term *slave*.
pub trait SlaveContext {
/// Select a slave device for all subsequent outgoing requests.
fn set_slave(&mut self, slave: Slave);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_dec() {
assert_eq!(Slave(0), Slave::from_str("0").unwrap());
assert_eq!(Slave(123), Slave::from_str("123").unwrap());
assert_eq!(Slave(255), Slave::from_str("255").unwrap());
assert!(Slave::from_str("-1").is_err());
assert!(Slave::from_str("256").is_err());
}
#[test]
fn parse_hex() {
assert_eq!(Slave(0), Slave::from_str("0x00").unwrap());
assert_eq!(Slave(123), Slave::from_str("0x7b").unwrap());
assert_eq!(Slave(123), Slave::from_str("0x7B").unwrap());
assert_eq!(Slave(255), Slave::from_str("0xff").unwrap());
assert_eq!(Slave(255), Slave::from_str("0xFF").unwrap());
assert!(Slave::from_str("0X00").is_err());
assert!(Slave::from_str("0x100").is_err());
assert!(Slave::from_str("0xfff").is_err());
assert!(Slave::from_str("0xFFF").is_err());
}
#[test]
fn format() {
assert!(format!("{}", Slave(123)).contains("123"));
assert!(format!("{}", Slave(0x7B)).contains("0x7B"));
assert!(!format!("{}", Slave(0x7B)).contains("0x7b"));
}
}