Skip to main content

ecr17_protocol/
lrc.rs

1//! LRC checksum and the [`LrcMode`] framing selector.
2//!
3//! The ECR17 application frame is `STX payload ETX LRC`. The LRC starts from the base
4//! `0x7F` and XOR-folds the payload; which of the framing bytes (`STX`/`ETX`) are also
5//! folded is selected by [`LrcMode`], because it varies by terminal/firmware.
6//!
7//! Port of the reference C++ `Lcr`.
8
9use serde::{Deserialize, Serialize};
10
11/// LRC base value (`0x7F`).
12pub const BASE: u8 = 0x7F;
13
14const STX: u8 = 0x02;
15const ETX: u8 = 0x03;
16
17/// Which framing bytes are folded into the LRC (base [`BASE`]):
18///
19/// - [`LrcMode::Stx`] — `STX + payload + ETX`
20/// - [`LrcMode::Std`] — payload only
21/// - [`LrcMode::Noext`] — `payload + ETX`
22/// - [`LrcMode::StxNoext`] — `STX + payload`
23///
24/// Serializes to the wire/JSON string union `"stx" | "std" | "noext" | "stx_noext"`.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
26#[serde(rename_all = "snake_case")]
27pub enum LrcMode {
28    /// `STX + payload + ETX`.
29    Stx,
30    /// payload only.
31    #[default]
32    Std,
33    /// `payload + ETX`.
34    Noext,
35    /// `STX + payload`.
36    StxNoext,
37}
38
39impl LrcMode {
40    /// Computes the LRC of `payload` under this mode.
41    ///
42    /// ```
43    /// use ecr17_protocol::lrc::LrcMode;
44    /// assert_eq!(LrcMode::Std.compute(b""), 0x7F);
45    /// assert_eq!(LrcMode::Stx.compute(b""), 0x7E); // 0x7F ^ STX ^ ETX
46    /// ```
47    #[must_use]
48    pub fn compute(self, payload: &[u8]) -> u8 {
49        let mut lrc = BASE;
50        if matches!(self, LrcMode::Stx | LrcMode::StxNoext) {
51            lrc ^= STX;
52        }
53        for &b in payload {
54            lrc ^= b;
55        }
56        if matches!(self, LrcMode::Stx | LrcMode::Noext) {
57            lrc ^= ETX;
58        }
59        lrc
60    }
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    // Reference implementation kept intentionally independent from the production
68    // code, so the tests assert against first principles rather than a copy.
69    fn reference(payload: &[u8], mode: LrcMode) -> u8 {
70        let mut lrc = 0x7Fu8;
71        if mode == LrcMode::Stx || mode == LrcMode::StxNoext {
72            lrc ^= 0x02;
73        }
74        for &b in payload {
75            lrc ^= b;
76        }
77        if mode == LrcMode::Stx || mode == LrcMode::Noext {
78            lrc ^= 0x03;
79        }
80        lrc
81    }
82
83    #[test]
84    fn empty_payload_std_is_base() {
85        assert_eq!(LrcMode::Std.compute(&[]), BASE);
86    }
87
88    #[test]
89    fn empty_payload_stx_folds_stx_and_etx() {
90        // 0x7F ^ 0x02 ^ 0x03 == 0x7E
91        assert_eq!(LrcMode::Stx.compute(&[]), 0x7E);
92    }
93
94    #[test]
95    fn known_vector_all_modes() {
96        let payload = *b"A"; // 0x41
97        assert_eq!(LrcMode::Std.compute(&payload), 0x3E);
98        assert_eq!(LrcMode::Stx.compute(&payload), 0x3F);
99        assert_eq!(LrcMode::Noext.compute(&payload), 0x3D);
100        assert_eq!(LrcMode::StxNoext.compute(&payload), 0x3C);
101    }
102
103    #[test]
104    fn matches_reference_for_every_mode() {
105        let payload = [0x00, 0x7F, 0x55, 0xAA, b'Z', 0x10];
106        for mode in [
107            LrcMode::Stx,
108            LrcMode::Std,
109            LrcMode::Noext,
110            LrcMode::StxNoext,
111        ] {
112            assert_eq!(mode.compute(&payload), reference(&payload, mode));
113        }
114    }
115
116    #[test]
117    fn str_and_bytes_agree() {
118        let payload = "12345678P0";
119        for mode in [
120            LrcMode::Stx,
121            LrcMode::Std,
122            LrcMode::Noext,
123            LrcMode::StxNoext,
124        ] {
125            assert_eq!(
126                mode.compute(payload.as_bytes()),
127                mode.compute(&payload.bytes().collect::<Vec<_>>())
128            );
129        }
130    }
131
132    #[test]
133    fn serde_snake_case_round_trip() {
134        assert_eq!(
135            serde_json::to_string(&LrcMode::StxNoext).unwrap(),
136            "\"stx_noext\""
137        );
138        assert_eq!(serde_json::to_string(&LrcMode::Std).unwrap(), "\"std\"");
139        let m: LrcMode = serde_json::from_str("\"noext\"").unwrap();
140        assert_eq!(m, LrcMode::Noext);
141    }
142}