Skip to main content

bacnet_mstp_core/
config.rs

1//! Configuration and Clause 9 timing validation for the MS/TP master core.
2
3use core::fmt;
4
5use bacnet_mstp_codec::{
6    MAX_EXTENDED_FRAME_LENGTH, MAX_EXTENDED_MPDU_DATA, MAX_MASTER, MAX_STANDARD_FRAME_LENGTH,
7    MAX_STANDARD_MPDU_DATA,
8};
9use bacnet_time::Duration;
10
11const DEFAULT_BAUD_RATE: u32 = 9_600;
12/// Npoll (Clause 9.5.3): the token uses between two maintenance Poll For
13/// Master cycles, 50. Fixed by the standard, so not a configuration field.
14pub const N_POLL: u8 = 50;
15/// Nretry_token (Clause 9.5.3): how many times the core sends its Token
16/// again to a successor that shows no sign of using it before it looks for
17/// another, once. Fixed by the standard.
18pub const N_RETRY_TOKEN: u8 = 1;
19/// Nmin_octets (Clause 9.5.3): a station that hears more receive events
20/// (octets and receive errors) than this takes the line to be in use, 4.
21/// Fixed by the standard.
22pub const N_MIN_OCTETS: u8 = 4;
23/// T_no_token (Clause 9.5.3): the silence after which a station takes the
24/// token to be lost, 500 ms. Fixed by the standard.
25pub const T_NO_TOKEN: Duration = Duration::from_millis(500);
26/// T_slot (Clause 9.5.3): the width of each station's turn to generate a
27/// lost token, 10 ms. Fixed by the standard.
28pub const T_SLOT: Duration = Duration::from_millis(10);
29/// The longest T_frame_abort a station may use (Clause 9.5.3): 100 ms.
30/// [`MstpCoreConfig::validate`] refuses a longer one.
31pub const MAX_FRAME_ABORT: Duration = Duration::from_millis(100);
32/// The shortest T_reply_timeout a station may use (Clause 9.5.3): 255 ms,
33/// the baud-rate constructors' value. [`MstpCoreConfig::validate`] refuses a
34/// shorter one. No requester stops waiting for a reply sooner, so the core
35/// also uses it to bound how late its own answer may start.
36pub const MIN_REPLY_TIMEOUT: Duration = Duration::from_millis(255);
37/// The longest T_reply_timeout a station may use (Clause 9.5.3): 300 ms.
38/// [`MstpCoreConfig::validate`] refuses a longer one.
39pub const MAX_REPLY_TIMEOUT: Duration = Duration::from_millis(300);
40/// The shortest T_usage_timeout a station may use (Clause 9.5.3): 20 ms, the
41/// baud-rate constructors' value. [`MstpCoreConfig::validate`] refuses a
42/// shorter one. No poller stops waiting for the answer to its poll sooner,
43/// so the core also uses it to bound how late its own answer may start.
44pub const MIN_USAGE_TIMEOUT: Duration = Duration::from_millis(20);
45/// The longest T_usage_timeout a station may use (Clause 9.5.3): 35 ms.
46/// [`MstpCoreConfig::validate`] refuses a longer one.
47pub const MAX_USAGE_TIMEOUT: Duration = Duration::from_millis(35);
48/// The longest T_reply_delay (Clause 9.5.3), the constructors' value.
49const MAX_REPLY_DELAY: Duration = Duration::from_millis(250);
50/// The longest T_usage_delay (Clause 9.5.3), the constructors' value.
51const MAX_USAGE_DELAY: Duration = Duration::from_millis(15);
52/// Default for [`MstpCoreConfig::starvation_limit`], which queues also use
53/// until a core hands them the configured one.
54pub(crate) const DEFAULT_STARVATION_LIMIT: u8 = 8;
55/// Default for [`MstpCoreConfig::reply_transmit_margin`], the value the host
56/// MS/TP transport reserves.
57const DEFAULT_REPLY_TRANSMIT_MARGIN: Duration = Duration::from_millis(25);
58/// Default for [`MstpCoreConfig::transmit_watchdog`] where it clears the
59/// longest frame.
60const DEFAULT_TRANSMIT_WATCHDOG: Duration = Duration::from_secs(1);
61/// Kept in [`MstpCoreConfig::minimum_transmit_watchdog`] beyond the longest
62/// frame and T_postdrive, for the adapter to report the completion.
63const WATCHDOG_COMPLETION_MARGIN: Duration = Duration::from_millis(1);
64/// Default for [`MstpCoreConfig::late_deadline`].
65pub const DEFAULT_LATE_DEADLINE: Duration = Duration::from_millis(1);
66
67/// A configuration value that violates a protocol or local safety bound.
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69#[non_exhaustive]
70pub enum ConfigError {
71    /// A baud rate of zero cannot be used to derive bit-time limits.
72    ZeroBaudRate,
73    /// A derived timing calculation overflowed its arithmetic.
74    TimingOverflow,
75    /// Max_Master is a seven-bit station address.
76    MaxMasterOutOfRange {
77        /// Supplied Max_Master value.
78        value: u8,
79    },
80    /// This station cannot be beyond Max_Master.
81    StationExceedsMaxMaster {
82        /// Local station address.
83        station: u8,
84        /// Configured Max_Master value.
85        max_master: u8,
86    },
87    /// Max_Info_Frames must be in the inclusive 1..=255 range.
88    MaxInfoFramesOutOfRange {
89        /// Supplied Max_Info_Frames value.
90        value: u8,
91    },
92    /// T_frame_abort is outside the 60-bit-time through 100 ms range.
93    FrameAbortOutOfRange {
94        /// Supplied frame-abort duration.
95        value: Duration,
96        /// Minimum permitted duration.
97        minimum: Duration,
98        /// Maximum permitted duration.
99        maximum: Duration,
100    },
101    /// T_frame_gap is longer than 20 bit times.
102    FrameGapTooLong {
103        /// Supplied frame-gap duration.
104        value: Duration,
105        /// Maximum permitted duration.
106        maximum: Duration,
107    },
108    /// T_postdrive is longer than 15 bit times.
109    PostdriveTooLong {
110        /// Supplied postdrive duration.
111        value: Duration,
112        /// Maximum permitted duration.
113        maximum: Duration,
114    },
115    /// T_reply_delay cannot exceed 250 ms.
116    ReplyDelayTooLong {
117        /// Supplied reply-delay duration.
118        value: Duration,
119        /// Maximum permitted duration.
120        maximum: Duration,
121    },
122    /// The reply transmit margin must leave part of T_reply_delay for the
123    /// application to answer.
124    ReplyMarginTooLong {
125        /// Supplied margin.
126        margin: Duration,
127        /// Configured T_reply_delay.
128        reply_delay: Duration,
129    },
130    /// T_reply_timeout must be between 255 and 300 ms.
131    ReplyTimeoutOutOfRange {
132        /// Supplied reply-timeout duration.
133        value: Duration,
134        /// Minimum permitted duration.
135        minimum: Duration,
136        /// Maximum permitted duration.
137        maximum: Duration,
138    },
139    /// T_turnaround must cover at least 40 bit times.
140    TurnaroundTooShort {
141        /// Supplied turnaround duration.
142        value: Duration,
143        /// Minimum permitted duration.
144        minimum: Duration,
145    },
146    /// T_turnaround cannot exceed the maximum T_usage_delay response window.
147    TurnaroundExceedsUsageDelay {
148        /// Supplied turnaround duration.
149        turnaround: Duration,
150        /// Configured usage-delay duration.
151        usage_delay: Duration,
152    },
153    /// T_usage_delay cannot exceed 15 ms.
154    UsageDelayTooLong {
155        /// Supplied usage-delay duration.
156        value: Duration,
157        /// Maximum permitted duration.
158        maximum: Duration,
159    },
160    /// T_usage_timeout must be between 20 and 35 ms.
161    UsageTimeoutOutOfRange {
162        /// Supplied usage-timeout duration.
163        value: Duration,
164        /// Minimum permitted duration.
165        minimum: Duration,
166        /// Maximum permitted duration.
167        maximum: Duration,
168    },
169    /// A local queue age or watchdog of zero would disable the safety bound.
170    ZeroLocalDuration {
171        /// Local duration setting that was zero.
172        field: LocalDurationField,
173    },
174    /// A local scheduling ratio or starvation limit of zero is invalid.
175    ZeroLocalLimit {
176        /// Local count or ratio setting that was zero.
177        field: LocalLimitField,
178    },
179    /// Multiple-frame ratio is a percentage and cannot exceed 100.
180    MultipleFrameRatioOutOfRange {
181        /// Supplied percentage value.
182        value: u8,
183    },
184    /// The transmit watchdog would abort the longest frame this
185    /// configuration sends before the driver could go off: a standard frame
186    /// of 511 octets takes 1.06 s at 4800 baud, and an extended one of 1516
187    /// octets 1.58 s at 9600. See
188    /// [`MstpCoreConfig::minimum_transmit_watchdog`]; the baud-rate
189    /// constructors and [`MstpCoreConfig::with_extended_frames`] set one
190    /// that clears it.
191    TransmitWatchdogTooShort {
192        /// Supplied watchdog.
193        value: Duration,
194        /// The shortest watchdog accepted,
195        /// [`MstpCoreConfig::minimum_transmit_watchdog`].
196        minimum: Duration,
197    },
198    /// [`MstpCoreConfig::extended_frames`] is on, but a buffer the core was
199    /// given can't hold the largest extended frame or NPDU. Only
200    /// [`MasterCore::new`](crate::MasterCore::new) and
201    /// [`MasterCore::from_parts`](crate::MasterCore::from_parts) report it,
202    /// since only they see the buffers.
203    ExtendedStorageTooSmall {
204        /// The buffer that is too small.
205        buffer: CoreBuffer,
206        /// Its size in octets.
207        length: usize,
208        /// The size extended frames need: the longest extended frame on the
209        /// wire for the receive storage and the transmit frame, the longest
210        /// NPDU one carries for a delivery slot and a queue slot.
211        minimum: usize,
212    },
213    /// The receive storage the core was given can't hold even the shortest
214    /// frame. Only [`MasterCore::new`](crate::MasterCore::new) and
215    /// [`MasterCore::from_parts`](crate::MasterCore::from_parts) report it.
216    ReceiveStorageTooSmall {
217        /// Supplied storage length.
218        length: usize,
219        /// [`MIN_RX_STORAGE`](crate::MIN_RX_STORAGE).
220        minimum: usize,
221    },
222}
223
224impl fmt::Display for ConfigError {
225    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226        match *self {
227            Self::ZeroBaudRate => f.write_str("the baud rate is zero"),
228            Self::TimingOverflow => f.write_str("a bit-time bound overflowed"),
229            Self::MaxMasterOutOfRange { value } => {
230                write!(f, "Max_Master {value} is over {MAX_MASTER}")
231            }
232            Self::StationExceedsMaxMaster {
233                station,
234                max_master,
235            } => write!(f, "station {station} is over Max_Master {max_master}"),
236            Self::MaxInfoFramesOutOfRange { value } => {
237                write!(f, "Max_Info_Frames {value} is not 1 to 255")
238            }
239            Self::FrameAbortOutOfRange {
240                value,
241                minimum,
242                maximum,
243            } => write!(
244                f,
245                "T_frame_abort {value:?} is not {minimum:?} to {maximum:?}"
246            ),
247            Self::FrameGapTooLong { value, maximum } => {
248                write!(f, "T_frame_gap {value:?} is over {maximum:?}")
249            }
250            Self::PostdriveTooLong { value, maximum } => {
251                write!(f, "T_postdrive {value:?} is over {maximum:?}")
252            }
253            Self::ReplyDelayTooLong { value, maximum } => {
254                write!(f, "T_reply_delay {value:?} is over {maximum:?}")
255            }
256            Self::ReplyMarginTooLong {
257                margin,
258                reply_delay,
259            } => write!(
260                f,
261                "the reply transmit margin {margin:?} leaves nothing of T_reply_delay {reply_delay:?}"
262            ),
263            Self::ReplyTimeoutOutOfRange {
264                value,
265                minimum,
266                maximum,
267            } => write!(
268                f,
269                "T_reply_timeout {value:?} is not {minimum:?} to {maximum:?}"
270            ),
271            Self::TurnaroundTooShort { value, minimum } => {
272                write!(f, "T_turnaround {value:?} is under {minimum:?}")
273            }
274            Self::TurnaroundExceedsUsageDelay {
275                turnaround,
276                usage_delay,
277            } => write!(
278                f,
279                "T_turnaround {turnaround:?} is over T_usage_delay {usage_delay:?}"
280            ),
281            Self::UsageDelayTooLong { value, maximum } => {
282                write!(f, "T_usage_delay {value:?} is over {maximum:?}")
283            }
284            Self::UsageTimeoutOutOfRange {
285                value,
286                minimum,
287                maximum,
288            } => write!(
289                f,
290                "T_usage_timeout {value:?} is not {minimum:?} to {maximum:?}"
291            ),
292            Self::ZeroLocalDuration { field } => write!(f, "{field} is zero"),
293            Self::ZeroLocalLimit { field } => write!(f, "{field} is zero"),
294            Self::MultipleFrameRatioOutOfRange { value } => {
295                write!(f, "multiple_frame_ratio {value} is over 100")
296            }
297            Self::TransmitWatchdogTooShort { value, minimum } => write!(
298                f,
299                "the transmit watchdog {value:?} is under {minimum:?}, the longest frame's"
300            ),
301            Self::ExtendedStorageTooSmall {
302                buffer,
303                length,
304                minimum,
305            } => write!(
306                f,
307                "the {buffer} holds {length} octets; extended frames need {minimum}"
308            ),
309            Self::ReceiveStorageTooSmall { length, minimum } => write!(
310                f,
311                "receive storage of {length} octets is under the {minimum} of the shortest frame"
312            ),
313        }
314    }
315}
316
317impl core::error::Error for ConfigError {}
318
319/// The buffers [`ConfigError::ExtendedStorageTooSmall`] names.
320#[derive(Debug, Clone, Copy, PartialEq, Eq)]
321#[non_exhaustive]
322pub enum CoreBuffer {
323    /// The receive storage, which must hold [`MAX_EXTENDED_FRAME_LENGTH`]
324    /// octets.
325    ReceiveStorage,
326    /// The [`ActionBuffers`](crate::ActionBuffers) frame buffer, its `FRAME`,
327    /// which must hold as much.
328    TransmitFrame,
329    /// Each [`ActionBuffers`](crate::ActionBuffers) delivery slot, its
330    /// `NPDU`, which must hold
331    /// [`MAX_EXTENDED_MPDU_DATA`]
332    /// octets.
333    DeliverySlot,
334    /// Each queue slot, as
335    /// [`QueueStorage::max_npdu`](crate::QueueStorage::max_npdu)
336    /// reports it, which must hold as much.
337    QueueSlot,
338}
339
340/// Names of local duration settings used by [`ConfigError::ZeroLocalDuration`].
341#[derive(Debug, Clone, Copy, PartialEq, Eq)]
342#[non_exhaustive]
343pub enum LocalDurationField {
344    /// [`MstpCoreConfig::queue_max_age`].
345    QueueMaxAge,
346    /// [`MstpCoreConfig::transmit_watchdog`].
347    TransmitWatchdog,
348}
349
350/// Names of local count/ratio settings used by [`ConfigError::ZeroLocalLimit`].
351#[derive(Debug, Clone, Copy, PartialEq, Eq)]
352#[non_exhaustive]
353pub enum LocalLimitField {
354    /// [`MstpCoreConfig::broadcast_unicast_ratio`].
355    BroadcastUnicastRatio,
356    /// [`MstpCoreConfig::multiple_frame_ratio`].
357    MultipleFrameRatio,
358    /// [`MstpCoreConfig::starvation_limit`].
359    StarvationLimit,
360}
361
362/// The buffer, as an error message names it.
363impl fmt::Display for CoreBuffer {
364    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
365        f.write_str(match self {
366            Self::ReceiveStorage => "receive storage",
367            Self::TransmitFrame => "transmit frame buffer",
368            Self::DeliverySlot => "delivery slot",
369            Self::QueueSlot => "queue slot",
370        })
371    }
372}
373
374/// The field's name.
375impl fmt::Display for LocalDurationField {
376    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
377        f.write_str(match self {
378            Self::QueueMaxAge => "queue_max_age",
379            Self::TransmitWatchdog => "transmit_watchdog",
380        })
381    }
382}
383
384/// The field's name.
385impl fmt::Display for LocalLimitField {
386    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
387        f.write_str(match self {
388            Self::BroadcastUnicastRatio => "broadcast_unicast_ratio",
389            Self::MultipleFrameRatio => "multiple_frame_ratio",
390            Self::StarvationLimit => "starvation_limit",
391        })
392    }
393}
394
395/// All timing, addressing, and bounded-queue parameters needed by an MS/TP
396/// master.
397///
398/// Durations are `bacnet-time` [`Duration`]s, in nanoseconds. Values whose
399/// limits are stated in bit times are still stored as absolute durations so a
400/// core instance can keep absolute deadlines without retaining a platform
401/// clock or baud-rate conversion helper. The bit-time values the constructors
402/// derive from the baud rate, and the bounds [`Self::validate`] checks them
403/// against, are whole microseconds (see [`Self::octet_time`]).
404///
405/// The values Clause 9.5.3 fixes are constants, not fields: [`N_POLL`],
406/// [`N_RETRY_TOKEN`], [`N_MIN_OCTETS`], [`T_NO_TOKEN`] and [`T_SLOT`].
407///
408/// It is `#[non_exhaustive]`, so a new setting isn't a breaking change:
409/// start from [`Self::STANDARD`] or [`Self::for_baud_rate`] and set the
410/// fields to change, then let [`MasterCore::new`](crate::MasterCore::new)
411/// or [`Self::validate`] check the result.
412///
413/// ```
414/// use bacnet_mstp_core::MstpCoreConfig;
415///
416/// const CONFIG: MstpCoreConfig = {
417///     let mut config = MstpCoreConfig::STANDARD;
418///     config.station = 3;
419///     config.max_master = 15;
420///     config
421/// };
422/// assert_eq!(CONFIG.validate(), Ok(()));
423/// ```
424#[derive(Debug, Clone, Copy, PartialEq, Eq)]
425#[non_exhaustive]
426pub struct MstpCoreConfig {
427    /// This station's MAC address.
428    pub station: u8,
429    /// Max_Master, inclusive.
430    pub max_master: u8,
431    /// Max_Info_Frames, the number of information frames per token use.
432    pub max_info_frames: u8,
433    /// Serial baud rate used to validate bit-time-derived limits.
434    pub baud_rate: u32,
435
436    /// T_frame_abort.
437    pub t_frame_abort: Duration,
438    /// T_frame_gap.
439    pub t_frame_gap: Duration,
440    /// T_postdrive.
441    pub t_postdrive: Duration,
442    /// T_reply_delay: the longest a reply to a data-expecting-reply frame may
443    /// take to start.
444    pub t_reply_delay: Duration,
445    /// T_reply_timeout.
446    pub t_reply_timeout: Duration,
447    /// T_turnaround.
448    pub t_turnaround: Duration,
449    /// T_usage_delay.
450    pub t_usage_delay: Duration,
451    /// T_usage_timeout.
452    pub t_usage_timeout: Duration,
453
454    /// Time kept back from T_reply_delay for the platform to get a reply onto
455    /// the wire.
456    ///
457    /// The core waits for the application's answer until T_reply_delay less
458    /// this margin, then sends Reply Postponed itself, so that adapter and
459    /// UART latency still lets the frame start within T_reply_delay. It must
460    /// be shorter than T_reply_delay. Default 25 ms.
461    pub reply_transmit_margin: Duration,
462    /// The lateness at which a serviced deadline counts in
463    /// [`MstpCounters::late_deadlines`](crate::MstpCounters::late_deadlines):
464    /// a deadline serviced this long or longer after it fell due counts,
465    /// one serviced sooner doesn't. Every serviced deadline counts in the
466    /// lateness histogram whatever this is.
467    ///
468    /// A local setting, which nothing in the core's timing depends on. Set
469    /// it from the adapter's timer: lateness under its resolution is
470    /// ordinary, not a sign of load. Default [`DEFAULT_LATE_DEADLINE`],
471    /// 1 ms, for an adapter with a fine timer; a host whose timer rounds
472    /// sleeps to the millisecond sets more.
473    pub late_deadline: Duration,
474    /// Maximum time an item may remain in a local transmit queue.
475    pub queue_max_age: Duration,
476    /// Number of broadcast frames scheduled per unicast frame while both
477    /// classes remain eligible.
478    pub broadcast_unicast_ratio: u8,
479    /// Percentage of `max_info_frames` usable during one token hold.
480    ///
481    /// The effective limit is rounded up and is always at least one. A value
482    /// of 100 leaves Clause 9 `Max_Info_Frames` unchanged.
483    pub multiple_frame_ratio: u8,
484    /// Number of service opportunities after which an item is promoted.
485    ///
486    /// The one source of the queues' starvation limit:
487    /// [`MasterCore::new`](crate::MasterCore::new) hands it to them through
488    /// [`QueueStorage::set_starvation_limit`](crate::QueueStorage::set_starvation_limit).
489    /// Default 8.
490    pub starvation_limit: u8,
491    /// Maximum time allowed for one platform transmit operation, from its
492    /// confirmed start to its completion.
493    ///
494    /// It must clear [`Self::minimum_transmit_watchdog`], the longest
495    /// frame's time on the wire plus T_postdrive, or validation refuses it
496    /// with [`ConfigError::TransmitWatchdogTooShort`]. The baud-rate
497    /// constructors give one second, or, where the longest standard frame
498    /// would not fit in that (below about 5.1 kbaud), the longest frame's
499    /// time plus one second.
500    pub transmit_watchdog: Duration,
501
502    /// Take and send extended frames, Types 32 and 33, which carry NPDUs of
503    /// 502 to 1497 octets COBS-encoded (Clauses 9.3.9, 9.3.10). Off by
504    /// default.
505    ///
506    /// Clause 9.3 requires COBS-encoded frames of a routing node and leaves
507    /// them optional for any other node, so a router turns this on for
508    /// every MS/TP port it routes and a device leaves it off unless it wants
509    /// the larger NPDUs.
510    ///
511    /// - **Off**, the core is a node without COBS support. It refuses an
512    ///   NPDU over 501 octets, and an extended frame it hears counts as an
513    ///   invalid frame when it is for this station or broadcast.
514    /// - **On**, a Type 32 for this station is a data request like a Type 5,
515    ///   and a Type 33 data like a Type 6, a reply to this station's request
516    ///   included; broadcasts of either go up as for Types 5 and 6. A
517    ///   reserved COBS type (34-127) is checked and, with a good CRC-32K,
518    ///   taken as a valid frame of a type the core doesn't know (Clause
519    ///   9.5.4.9). An NPDU of 502 to 1497 octets, queued or as a reply, goes
520    ///   out as Type 32 when it expects a reply and Type 33 otherwise.
521    ///   [`MasterCore::new`](crate::MasterCore::new) then needs buffers
522    ///   sized for extended frames, and refuses smaller ones with
523    ///   [`ConfigError::ExtendedStorageTooSmall`]. The transmit watchdog
524    ///   must clear the longest extended frame
525    ///   ([`ConfigError::TransmitWatchdogTooShort`]);
526    ///   [`Self::with_extended_frames`] sets both.
527    pub extended_frames: bool,
528}
529
530/// [`MstpCoreConfig::STANDARD`].
531impl Default for MstpCoreConfig {
532    fn default() -> Self {
533        Self::STANDARD
534    }
535}
536
537impl MstpCoreConfig {
538    /// The standard settings at 9600 baud: station 0, Max_Master 127, one
539    /// information frame per token use, the bit-time timers derived from the
540    /// baud rate, the shortest T_reply_timeout and T_usage_timeout Clause
541    /// 9.5.3 allows, the longest T_reply_delay and T_usage_delay, and the
542    /// local defaults below; extended frames off.
543    pub const STANDARD: Self = Self::for_baud_rate_unchecked(DEFAULT_BAUD_RATE);
544
545    /// The standard settings at `baud_rate`, as [`Self::STANDARD`] has them
546    /// at 9600, validated.
547    ///
548    /// # Errors
549    ///
550    /// [`ConfigError::ZeroBaudRate`] for zero, and what [`Self::validate`]
551    /// refuses at a rate too low for Clause 9 timing (below 2667 baud).
552    pub const fn for_baud_rate(baud_rate: u32) -> Result<Self, ConfigError> {
553        if baud_rate == 0 {
554            return Err(ConfigError::ZeroBaudRate);
555        }
556        let config = Self::for_baud_rate_unchecked(baud_rate);
557        match config.validate() {
558            Ok(()) => Ok(config),
559            Err(error) => Err(error),
560        }
561    }
562
563    /// The time one octet takes on the wire: ten bit times, rounded up to a
564    /// whole microsecond.
565    ///
566    /// Every time derived from the baud rate is a whole number of
567    /// microseconds, rounded up for a lower bound and down for an upper one:
568    /// the octet time, T_frame_abort, T_frame_gap, T_postdrive and
569    /// T_turnaround, and validation's bounds on them. That keeps the
570    /// arithmetic in 32 bits, with no 64-bit division on a 32-bit target.
571    #[must_use]
572    pub const fn octet_time(&self) -> Duration {
573        or_max(ceil_bit_times(self.baud_rate, 10))
574    }
575
576    /// Validate all protocol and local bounds.
577    ///
578    /// # Errors
579    ///
580    /// The first bound the configuration breaks, as a [`ConfigError`].
581    pub const fn validate(&self) -> Result<(), ConfigError> {
582        if self.baud_rate == 0 {
583            return Err(ConfigError::ZeroBaudRate);
584        }
585        if self.max_master > MAX_MASTER {
586            return Err(ConfigError::MaxMasterOutOfRange {
587                value: self.max_master,
588            });
589        }
590        if self.station > self.max_master {
591            return Err(ConfigError::StationExceedsMaxMaster {
592                station: self.station,
593                max_master: self.max_master,
594            });
595        }
596        if self.max_info_frames == 0 {
597            return Err(ConfigError::MaxInfoFramesOutOfRange {
598                value: self.max_info_frames,
599            });
600        }
601
602        let (Some(frame_abort_min), Some(frame_gap_max), Some(postdrive_max), Some(turnaround_min)) = (
603            ceil_bit_times(self.baud_rate, 60),
604            floor_bit_times(self.baud_rate, 20),
605            floor_bit_times(self.baud_rate, 15),
606            ceil_bit_times(self.baud_rate, 40),
607        ) else {
608            return Err(ConfigError::TimingOverflow);
609        };
610        if shorter(self.t_frame_abort, frame_abort_min)
611            || longer(self.t_frame_abort, MAX_FRAME_ABORT)
612        {
613            return Err(ConfigError::FrameAbortOutOfRange {
614                value: self.t_frame_abort,
615                minimum: frame_abort_min,
616                maximum: MAX_FRAME_ABORT,
617            });
618        }
619        if longer(self.t_frame_gap, frame_gap_max) {
620            return Err(ConfigError::FrameGapTooLong {
621                value: self.t_frame_gap,
622                maximum: frame_gap_max,
623            });
624        }
625        if longer(self.t_postdrive, postdrive_max) {
626            return Err(ConfigError::PostdriveTooLong {
627                value: self.t_postdrive,
628                maximum: postdrive_max,
629            });
630        }
631        if longer(self.t_reply_delay, MAX_REPLY_DELAY) {
632            return Err(ConfigError::ReplyDelayTooLong {
633                value: self.t_reply_delay,
634                maximum: MAX_REPLY_DELAY,
635            });
636        }
637        if !shorter(self.reply_transmit_margin, self.t_reply_delay) {
638            return Err(ConfigError::ReplyMarginTooLong {
639                margin: self.reply_transmit_margin,
640                reply_delay: self.t_reply_delay,
641            });
642        }
643        if shorter(self.t_reply_timeout, MIN_REPLY_TIMEOUT)
644            || longer(self.t_reply_timeout, MAX_REPLY_TIMEOUT)
645        {
646            return Err(ConfigError::ReplyTimeoutOutOfRange {
647                value: self.t_reply_timeout,
648                minimum: MIN_REPLY_TIMEOUT,
649                maximum: MAX_REPLY_TIMEOUT,
650            });
651        }
652        if shorter(self.t_turnaround, turnaround_min) {
653            return Err(ConfigError::TurnaroundTooShort {
654                value: self.t_turnaround,
655                minimum: turnaround_min,
656            });
657        }
658        if longer(self.t_turnaround, self.t_usage_delay) {
659            return Err(ConfigError::TurnaroundExceedsUsageDelay {
660                turnaround: self.t_turnaround,
661                usage_delay: self.t_usage_delay,
662            });
663        }
664        if longer(self.t_usage_delay, MAX_USAGE_DELAY) {
665            return Err(ConfigError::UsageDelayTooLong {
666                value: self.t_usage_delay,
667                maximum: MAX_USAGE_DELAY,
668            });
669        }
670        if shorter(self.t_usage_timeout, MIN_USAGE_TIMEOUT)
671            || longer(self.t_usage_timeout, MAX_USAGE_TIMEOUT)
672        {
673            return Err(ConfigError::UsageTimeoutOutOfRange {
674                value: self.t_usage_timeout,
675                minimum: MIN_USAGE_TIMEOUT,
676                maximum: MAX_USAGE_TIMEOUT,
677            });
678        }
679        if self.queue_max_age.is_zero() {
680            return Err(ConfigError::ZeroLocalDuration {
681                field: LocalDurationField::QueueMaxAge,
682            });
683        }
684        if self.transmit_watchdog.is_zero() {
685            return Err(ConfigError::ZeroLocalDuration {
686                field: LocalDurationField::TransmitWatchdog,
687            });
688        }
689        if shorter(self.transmit_watchdog, self.minimum_transmit_watchdog()) {
690            return Err(ConfigError::TransmitWatchdogTooShort {
691                value: self.transmit_watchdog,
692                minimum: self.minimum_transmit_watchdog(),
693            });
694        }
695        if self.broadcast_unicast_ratio == 0 {
696            return Err(ConfigError::ZeroLocalLimit {
697                field: LocalLimitField::BroadcastUnicastRatio,
698            });
699        }
700        if self.multiple_frame_ratio == 0 {
701            return Err(ConfigError::ZeroLocalLimit {
702                field: LocalLimitField::MultipleFrameRatio,
703            });
704        }
705        if self.multiple_frame_ratio > 100 {
706            return Err(ConfigError::MultipleFrameRatioOutOfRange {
707                value: self.multiple_frame_ratio,
708            });
709        }
710        if self.starvation_limit == 0 {
711            return Err(ConfigError::ZeroLocalLimit {
712                field: LocalLimitField::StarvationLimit,
713            });
714        }
715        Ok(())
716    }
717
718    const fn for_baud_rate_unchecked(baud_rate: u32) -> Self {
719        let config = Self {
720            station: 0,
721            max_master: MAX_MASTER,
722            max_info_frames: 1,
723            baud_rate,
724            t_frame_abort: or_max(ceil_bit_times(baud_rate, 60)),
725            t_frame_gap: or_max(floor_bit_times(baud_rate, 20)),
726            t_postdrive: or_max(floor_bit_times(baud_rate, 15)),
727            t_reply_delay: MAX_REPLY_DELAY,
728            t_reply_timeout: MIN_REPLY_TIMEOUT,
729            t_turnaround: or_max(ceil_bit_times(baud_rate, 40)),
730            t_usage_delay: MAX_USAGE_DELAY,
731            t_usage_timeout: MIN_USAGE_TIMEOUT,
732            reply_transmit_margin: DEFAULT_REPLY_TRANSMIT_MARGIN,
733            late_deadline: DEFAULT_LATE_DEADLINE,
734            queue_max_age: Duration::from_secs(5),
735            broadcast_unicast_ratio: 1,
736            multiple_frame_ratio: 100,
737            starvation_limit: DEFAULT_STARVATION_LIMIT,
738            transmit_watchdog: DEFAULT_TRANSMIT_WATCHDOG,
739            extended_frames: false,
740        };
741        config.with_covering_watchdog()
742    }
743
744    /// The longest NPDU this configuration sends or takes: 1497 octets with
745    /// [`Self::extended_frames`] on (Clauses 9.3.9, 9.3.10), else 501
746    /// (Clauses 9.3.6, 9.3.7).
747    #[must_use]
748    pub const fn max_npdu(&self) -> usize {
749        if self.extended_frames {
750            MAX_EXTENDED_MPDU_DATA
751        } else {
752            MAX_STANDARD_MPDU_DATA
753        }
754    }
755
756    /// The time the longest frame this configuration sends takes on the
757    /// wire: [`MAX_EXTENDED_FRAME_LENGTH`] octets with
758    /// [`Self::extended_frames`] on, else [`MAX_STANDARD_FRAME_LENGTH`].
759    #[must_use]
760    pub const fn longest_frame_time(&self) -> Duration {
761        let octets = if self.extended_frames {
762            MAX_EXTENDED_FRAME_LENGTH
763        } else {
764            MAX_STANDARD_FRAME_LENGTH
765        };
766        // At most 1516 octets, so the cast is exact.
767        self.octet_time().saturating_mul(octets as u32)
768    }
769
770    /// The shortest [`Self::transmit_watchdog`] validation accepts: the
771    /// longest frame's time on the wire ([`Self::longest_frame_time`]) plus
772    /// T_postdrive, which the watchdog's span from the confirmed start to
773    /// completion also covers, plus 1 ms for the adapter to report the
774    /// completion, the default lateness at which the core counts a deadline
775    /// late ([`DEFAULT_LATE_DEADLINE`]). A watchdog at this floor
776    /// leaves nothing more for a slow adapter; the constructors' defaults
777    /// leave a second or more.
778    #[must_use]
779    pub const fn minimum_transmit_watchdog(&self) -> Duration {
780        self.longest_frame_time()
781            .saturating_add(self.t_postdrive)
782            .saturating_add(WATCHDOG_COMPLETION_MARGIN)
783    }
784
785    /// This configuration with [`Self::extended_frames`] on, and a transmit
786    /// watchdog that clears the longest extended frame: the one it has if
787    /// that clears it already, otherwise that frame's time on the wire plus
788    /// one second.
789    #[must_use]
790    pub const fn with_extended_frames(mut self) -> Self {
791        self.extended_frames = true;
792        self.with_covering_watchdog()
793    }
794
795    /// This configuration, with a transmit watchdog that clears
796    /// [`Self::minimum_transmit_watchdog`]: the one it has if it does, else
797    /// the longest frame's time plus the default one second.
798    const fn with_covering_watchdog(mut self) -> Self {
799        if self.transmit_watchdog.as_nanos() < self.minimum_transmit_watchdog().as_nanos() {
800            self.transmit_watchdog = self
801                .longest_frame_time()
802                .saturating_add(DEFAULT_TRANSMIT_WATCHDOG);
803        }
804        self
805    }
806}
807
808/// `bits` bit times at `baud_rate`, rounded up to a whole microsecond, for
809/// minimum bounds. `None` for a zero baud rate or on overflow.
810///
811/// The arithmetic is 32-bit: every caller asks for at most 60 bit times,
812/// whose 60,000,000 microsecond-bits fit a `u32`, so the result is exact and
813/// a 32-bit target links no 64-bit division routine for it. Nanosecond steps
814/// would need 60,000,000,000, past a `u32`, and whole microseconds keep each
815/// value what it was before the core counted nanoseconds.
816const fn ceil_bit_times(baud_rate: u32, bits: u32) -> Option<Duration> {
817    match bits.checked_mul(1_000_000) {
818        Some(numerator) if baud_rate != 0 => {
819            Some(Duration::from_micros(numerator.div_ceil(baud_rate) as u64))
820        }
821        _ => None,
822    }
823}
824
825/// `bits` bit times at `baud_rate`, rounded down to a whole microsecond, for
826/// maximum bounds. `None` for a zero baud rate or on overflow. 32-bit, as
827/// [`ceil_bit_times`] is.
828const fn floor_bit_times(baud_rate: u32, bits: u32) -> Option<Duration> {
829    match bits.checked_mul(1_000_000) {
830        Some(numerator) => match numerator.checked_div(baud_rate) {
831            Some(micros) => Some(Duration::from_micros(micros as u64)),
832            None => None,
833        },
834        None => None,
835    }
836}
837
838/// Whether `value` is shorter than `bound`: `<`, which a `const fn` can't
839/// call on [`Duration`].
840const fn shorter(value: Duration, bound: Duration) -> bool {
841    value.as_nanos() < bound.as_nanos()
842}
843
844/// Whether `value` is longer than `bound`.
845const fn longer(value: Duration, bound: Duration) -> bool {
846    value.as_nanos() > bound.as_nanos()
847}
848
849/// A bit-time default for [`MstpCoreConfig::for_baud_rate_unchecked`]: a
850/// zero baud rate gives [`Duration::MAX`], which `validate` then rejects.
851const fn or_max(duration: Option<Duration>) -> Duration {
852    match duration {
853        Some(duration) => duration,
854        None => Duration::MAX,
855    }
856}
857
858#[cfg(test)]
859mod tests;