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
//! # Erdnuss Comms
//!
//! This is the "netstack" of the erdnuss project. It's intended to be used on
//! an RS-485 bus. Right now it's only really expected to work on bare metal
//! devices at a fixed network speed of 7.812MHz.
//!
//! This netstack is intended for use on a half-duplex RS-485 bus.
//!
//! At the moment, only 32 devices on a single bus are supported. This also
//! happens to be the upper limit supported by low cost hardware transcievers.
//!
//! At the moment, all communications on the bus are either Controller-to-one-Target, or
//! one-target-to-controller. There is no provision yet for Target-to-Target messaging.
//!
//! ## Entities
//!
//! There are two roles in this netstack:
//!
//! 1. The Controller (or CON) role, which is responsible for commanding and
//!    managing the time slices given to all other devices on the bus
//! 2. The Target (or TGT) role, which only responds to commands from
//!    the Controller.
//!
//! This nomenclature matches those used by I3C and other similar bus protocols
//! with a single device "in charge" of driving communications.
//!
//! At the moment, these roles are expected to be permanently assigned at
//! compile time. There must always be exactly one Controller on any bus.
//!
//! ## Message Framing
//!
//! Messages on the bus are framed by a Line Break, notifying them of an
//! "end of frame" condition. This is intended to allow relatively low active
//! CPU usage, with most nodes in a loop of:
//!
//! 1. Start DMA receive, until a Line Break interrupt occurs
//! 2. Go off and do whatever
//! 3. Once a Line Break occurs, check whether the received message was valid,
//!    and addressed to this node. If yes: process it and potentially respond.
//!    If no: ignore the frame and return to step 1.
//!
//! A line break was chosen because it is well supported by the RP2040 hardware
//! UART implementation.
//!
//! A line break was chosen over 9-bit messages (where the msbit is used as an
//! address/data flag), because the RP2040 doesn't support 9-bit serial, and
//! has no address-match interrupt, which devices like STM32 often have.
//!
//! A line break was chosen over a "line idle" interrupt, because while the RP2040
//! DOES have a line idle interrupt, it does not work when using DMA, as the idle
//! interrupt only fires when the line is quiet AND data is present in the receive
//! FIFO, which will not occur when an RX DMA is actively draining bytes from the
//! receive FIFO.
//!
//! ## Time Division
//!
//! As RS-485 is a half-duplex, shared medium, bus; it is necessary to coordinate
//! betwee all senders to avoid message collisions.
//!
//! This is achieved by having the Controller be "in charge" of a bus. The communication
//! between the Controller and Target is polling-based, and generally looks like this:
//!
//! 1. The line is idle, and the Controller decides to send to a specific Target
//! 2. The Controller sends a command-address byte with the ID of the controller
//! 3. If the Controller has a pending message to that Target, it then sends that payload
//!    (zero or one data frames)
//! 4. Once the Controller is done sending, it signals "end of frame" with a line break, and
//!    begins listening for 1ms, or until a line break occurs, whichever comes first.
//! 5. The addressed Target notices it has been addressed, and all other non-addressed
//!    Targets go back to listening.
//! 6. The addressed Target sends a response-address byte with its own ID
//! 7. If the Target has a pending message for the CON, it then sends that payload
//!    (zero or one data frames)
//! 8. One the Target is done sending, it signals "end of frame" with a line break, and
//!    begins listening again
//! 9. The Controller hears the line break, processes the received message if any, and goes
//!    back to step 1 for the next Target
//!
//! ## Automatic logical addressing
//!
//! All devices are expected to have universally unique 64-bit hardware address, analogous
//! to a MAC-address on ethernet/wifi devices. For RP2040 based nodes, this is typically
//! achieved by using the unique serial number of the QSPI flash chip.
//!
//! In order to reduce overhead on the bus for addressing, devices are dynamically assigned
//! a 5-bit address (0..32).
//!
//! When a Target first boots, it does not have a logical address. The Controller will periodically
//! offer unused addresses, and any Targets without an address will random decide whether to
//! claim the address.
//!
//! As there may be multiple Targets that attempt to claim the address at the same time, the act
//! of being assigned an address takes multiple steps:
//!
//! 1. The Controller offers an address, and includes 8 bytes of random data
//! 2. A Target with no address randomly decides whether to attempt to claim the address.
//!    This random chance is aimed at reducing collisions where two or more nodes attempt
//!    to claim the same address.
//! 3. If a Target decides to go ahead, it takes the 8 random bytes, and XORs them with its own
//!    8-byte unique hardware ID, and sends a "claim" message back
//! 4. If the Controller hears this claim, it takes the received 8 bytes, and XORs them with the
//!    original 8 random bytes. If there was not a collision, it should be left with the MAC
//!    address of the new Target. The Controller marks this address and unique ID as "pending"
//! 5. At a later time, the Controller sends a message to the logical address, containing the MAC address
//!    it thinks it heard in step 4, and waits for an acknowledgement.
//! 6. If the Target hears the logical address it claimed, AND the unique ID matches its own unique ID,
//!    then it sends an acknowledgement, and considers itself as having "joined" the bus, exclusively
//!    owning that logical address
//! 7. If the controller hears the ACK, it marks that address as fully assigned. If it does not hear an
//!    ACK, it marks the address from "pending" to "free".
//!
//! At the moment, the random chance in step 2 is a 1/8 chance, though this may change in the future.
//!
//! ## Controller "steps"
//!
//! So far, we've described the process of a single CON/TGT communication. This must be carried
//! out for all TGTs on the bus. In general, the CON performs an endless polling loop, consisting
//! of three phases:
//!
//! 1. For each known TGT with an assigned logical address:
//!     * Address the TGT, additionally sending it 0 or 1 data frames
//!     * Wait for the TGT to respond.
//!         * If it DOES respond, it will respond with 0 or 1 data frames, and clear the "failure counter".
//!         * If it DOES NOT respond, we increment a "failure counter".
//! 2. For each address in the "pending" phase:
//!     * We send the confirmation message (step 5 above)
//!     * Wait for the TGT to respond or a timeout to occur (step 7 above)
//! 3. If we have any remaining un-assigned logical addresses:
//!     * Send an "offer message" (step 1 above)
//!     * Wait for the TGT to respond or a timeout to occur (step 4 above)
//!
//! At the moment, it is up to the application to decide how often to perform a "step". This could be
//! continuously, every N milliseconds, or on some other metric.
//!
//! More steps/sec means:
//!
//! * More CPU time spent checking and responding to messages
//! * Less latency for messages waiting to be transferred from CON to TGT or TGT to CON
//! * Higher data throughput on the bus
//!
//! Fewer steps/sec will mean the inverse. In the future, there might be a better way to
//! adaptively poll in a more intelligent manner.
//!
//! ## Culling of inactive devices
//!
//! As all Targets are expected to quickly respond to all queries from the Controller, the Controller uses
//! a "three strikes you're out" rule to avoid wasting bus time on timeouts from
//! unresponsive Targets. If a Target fails to respond three times in a row, it is dropped, and
//! the address is marked as free.

#![cfg_attr(not(any(test, feature = "std")), no_std)]
#![allow(async_fn_in_trait)]
#![warn(missing_docs)]

#[macro_use]
mod macros;

pub mod controller;
pub mod frame_pool;
mod peer;
pub mod target;
#[cfg(feature = "postcard-rpc-helpers")]
pub mod wirehelp;
use embassy_time::Instant;

/// The maximum number of Targets supported by a Controller.
pub const MAX_TARGETS: usize = 31;

pub use crate::controller::Controller;

/// An error type for the [`FrameSerial`] trait
#[derive(Debug, PartialEq)]
#[non_exhaustive]
pub enum Error<E> {
    /// Some error with the underlying hardware serial port
    Serial(E),
}

impl<E> From<E> for Error<E> {
    fn from(value: E) -> Self {
        Self::Serial(value)
    }
}

/// A time-snapshotted data frame
pub struct TimedFrame<'a> {
    /// The timestamp measure as closely as possible to the end
    /// of the frame reception time.
    pub end_of_rx: Instant,
    /// The received data frame
    pub frame: &'a mut [u8],
}

/// A trait representing the communication interface of the RS-485 bus
pub trait FrameSerial {
    /// The error type of the underlying serial port
    type SerError;

    /// Send a single frame.
    ///
    /// The hardware should begin sending the data as soon as possible,
    /// and the future MUST not return until the data is completely done
    /// sending, e.g. all data is "flushed".
    ///
    /// This function is responsible for activating the "send mode" of the
    /// transceiver on entry (e.g. asserting DE), and disabling it on
    /// exit (e.g. deasserting DE).
    ///
    /// This function MUST be cancellation safe, including the de-assertion
    /// of the "send mode".
    async fn send_frame(&mut self, data: &[u8]) -> Result<(), Error<Self::SerError>>;

    /// Receive a single frame, waiting until a Line Break occurs, signalling
    /// the end of a frame
    async fn recv<'a>(
        &mut self,
        frame: &'a mut [u8],
    ) -> Result<TimedFrame<'a>, Error<Self::SerError>>;
}

/// Command + Address byte
///
/// [CmdAddr] is a combined Command and Address byte. It consists of:
///
/// * 3 command-bits (`0..=7`)
/// * 5 address-bits (`0..=31`)
///
/// The command bits are most significant, and the address bits are least
/// significant, e.g. `0bCCC_AAAAA`, where C are command bits and A are
/// address bits.
///
/// The address bits are the logical address of the target, which may be
/// a source or destination, depending on the message kind.
///
/// Commands 1, 2, 4, 5, and 7 are assigned as described below. Commands
/// 0, 3, and 6 are reserved for future use, and currently considered
/// invalid.
#[non_exhaustive]
#[derive(Debug, PartialEq, Eq)]
pub enum CmdAddr {
    /// Select - `0b001`
    ///
    /// Used when the Controller is addressing a Target.
    SelectAddr(u8),
    /// Reply - `0b010`
    ///
    /// Used when the Target is responding to the Controller.
    ReplyFromAddr(u8),
    /// Discovery Offer - `0b100`
    ///
    /// Used when the Controller is offering an unused logical address
    /// to Targets without one. The offered address is the one in the
    /// 5-bit address field.
    DiscoveryOffer(u8),
    /// Discovery Claim - `0b0101`
    ///
    /// Used when a Target attempts to claim a Discovery Offer message.
    DiscoveryClaim(u8),
    /// Discovery Success - `0b111`
    ///
    /// used when the Controller is informing a Target that its address
    /// claim is (tentatively) successful. The Target must respond to
    /// this message with an empty Reply.
    DiscoverySuccess(u8),
}

/// Command Address Error
#[derive(Debug, PartialEq)]
#[non_exhaustive]
pub enum CmdAddrError {
    /// A reserved bit pattern was found
    Reserved,
}

impl CmdAddr {
    const SELECT_ADDR: u8 = 0b001;
    const REPLY_FROM_ADDR: u8 = 0b010;
    const DISCOVERY_OFFER: u8 = 0b100;
    const DISCOVERY_CLAIM: u8 = 0b101;
    const DISCOVERY_SUCCESS: u8 = 0b111;
}

impl TryFrom<u8> for CmdAddr {
    type Error = CmdAddrError;

    fn try_from(value: u8) -> Result<Self, Self::Error> {
        let cmd = value >> 5;
        let addr = value & 0b000_11111;
        match cmd {
            Self::SELECT_ADDR => Ok(CmdAddr::SelectAddr(addr)),
            Self::REPLY_FROM_ADDR => Ok(CmdAddr::ReplyFromAddr(addr)),
            Self::DISCOVERY_OFFER => Ok(CmdAddr::DiscoveryOffer(addr)),
            Self::DISCOVERY_CLAIM => Ok(CmdAddr::DiscoveryClaim(addr)),
            Self::DISCOVERY_SUCCESS => Ok(CmdAddr::DiscoverySuccess(addr)),
            _ => Err(CmdAddrError::Reserved),
        }
    }
}

impl From<CmdAddr> for u8 {
    fn from(val: CmdAddr) -> Self {
        let (cmd, addr) = match val {
            CmdAddr::SelectAddr(addr) => (CmdAddr::SELECT_ADDR, addr),
            CmdAddr::ReplyFromAddr(addr) => (CmdAddr::REPLY_FROM_ADDR, addr),
            CmdAddr::DiscoveryOffer(addr) => (CmdAddr::DISCOVERY_OFFER, addr),
            CmdAddr::DiscoveryClaim(addr) => (CmdAddr::DISCOVERY_CLAIM, addr),
            CmdAddr::DiscoverySuccess(addr) => (CmdAddr::DISCOVERY_SUCCESS, addr),
        };
        (cmd << 5) | (addr & 0b000_11111)
    }
}