pamoja_modbus/lib.rs
1#![cfg_attr(not(any(test, feature = "port")), no_std)]
2// The crate doc links the feature-gated types, which a build without the features lacks.
3#![cfg_attr(not(feature = "port"), allow(rustdoc::broken_intra_doc_links))]
4
5//! Modbus RTU for the pamoja SDK: the frames, a server, and a client on a serial line.
6//!
7//! Modbus is the lingua franca of cheap industrial sensing. Soil NPK probes, energy
8//! meters, water-quality transmitters, and pump controllers overwhelmingly speak Modbus
9//! over RS485, a serial bus that reaches hundreds of meters down a single cable, which
10//! is exactly what a dispersed farm or a rural water network needs. To talk to those
11//! devices a node has to put the right bytes on the wire and trust the bytes it gets
12//! back, and Modbus RTU pins down precisely what those bytes are.
13//!
14//! The byte layer needs no serial port and no allocation:
15//!
16//! - [`crc16`] - the CRC-16/MODBUS that every RTU frame ends with, the check that lets a
17//! receiver reject a frame mangled by electrical noise on a long cable.
18//! - [`Pdu`] - the protocol data unit: a function code and its data. Constructors build
19//! the standard requests (read and write coils and registers), the replies a device sends
20//! to each, and the exception it sends instead, so callers never hand-pack a frame, with a
21//! [`raw`](Pdu::raw) escape hatch for the function codes this crate does not name.
22//! - [`Adu`] - the RTU application data unit: a unit address, a PDU, and the CRC. It both
23//! [assembles](Adu::from_pdu) a frame to send and [parses](Adu::parse) one received,
24//! verifying the CRC so a corrupt frame never reaches the application.
25//! - [`Response`] - reads the values back out of a reply: the 16-bit registers of a
26//! read-registers response and the packed bits of a read-coils response, plus the
27//! [`Exception`] a device returns when it refuses a request.
28//! - [`Request`] - reads a request the way a device does, checked against the quantity and
29//! value limits the specification sets.
30//!
31//! With the `alloc` feature, [`Server`] is a device: a unit address and the four tables it
32//! serves, answering each frame with the reply or the exception the specification gives,
33//! and staying silent where the serial line specification says a device does.
34//!
35//! With the `port` feature, which needs an operating system, [`Client`] runs transactions
36//! over a [`pamoja_hal::port::SerialPort`]: it leaves the line silent for 3.5 characters
37//! before each request, reads the reply to the length the request implies against a
38//! response timeout, and checks the reply's CRC, unit, and function before a value is read
39//! out of it. A server answers on a simulated port, and a [`Line`] carries several, so a
40//! polling loop runs end to end with nothing plugged in.
41//!
42//! # Examples
43//!
44//! ```
45//! use pamoja_modbus::{Adu, Exception, Function, Pdu, Request};
46//!
47//! // An energy meter at unit 17 keeps its voltage in tenths of a volt, its current in
48//! // milliamps, and a fault word in the three holding registers from 107.
49//! let request = Pdu::read_holding_registers(107, 3).to_adu(17);
50//!
51//! // The meter reads the request the way the specification says a device does.
52//! let asked = Request::parse(request.pdu()).expect("a request the meter can serve");
53//! assert_eq!(asked.span(), (107, 3));
54//!
55//! // Its reply carries the unit and a CRC, and a receiver checks both before it reads a
56//! // value, so a frame mangled on a long cable never reaches the application.
57//! let reply = Pdu::read_holding_registers_reply(&[2301, 418, 0])?.to_adu(17);
58//! let received = Adu::parse(reply.as_bytes())?;
59//! let registers: Vec<u16> = received.response().registers()?.collect();
60//! assert_eq!(registers, [2301, 418, 0]);
61//! assert_eq!(f64::from(registers[0]) / 10.0, 230.1);
62//!
63//! // Asked for a register it does not have, the meter answers with an exception instead.
64//! let function = Function::ReadHoldingRegisters.code();
65//! let refused = Pdu::exception(function, Exception::IllegalDataAddress).to_adu(17);
66//! let answer = Adu::parse(refused.as_bytes())?;
67//! assert_eq!(answer.exception(), Some(Exception::IllegalDataAddress));
68//! # Ok::<(), pamoja_modbus::ModbusError>(())
69//! ```
70
71#[cfg(feature = "alloc")]
72extern crate alloc;
73
74mod adu;
75#[cfg(feature = "port")]
76mod client;
77mod crc;
78mod error;
79mod function;
80mod pdu;
81mod request;
82mod response;
83#[cfg(feature = "alloc")]
84mod server;
85
86pub use adu::Adu;
87#[cfg(feature = "port")]
88pub use client::{Client, ClientError, RESPONSE_TIMEOUT, TURNAROUND};
89pub use crc::crc16;
90pub use error::ModbusError;
91pub use function::{Exception, Function};
92pub use pdu::Pdu;
93pub use request::Request;
94pub use response::{Coils, Registers, Response};
95#[cfg(feature = "port")]
96pub use server::Line;
97#[cfg(feature = "alloc")]
98pub use server::{Server, BROADCAST, UNITS};