a/
lib.rs

1//! `a` implements the **HJ 212** ASCII protocol (protocol-layer only).
2//!
3//! This crate focuses on:
4//! - **Receiving**: stream deframing (`Framer`) + frame parsing (`parse_frame`)
5//! - **Sending**: building `CP=&&...&&` and payloads (`CpBuilder`, `PayloadBuilder`) + frame building (`build_frame`)
6//!
7//! It intentionally does **not** include networking/serial I/O stacks.
8//!
9//! # Parse a frame
10//!
11//! ```
12//! use a::{build_frame, parse_frame};
13//!
14//! let payload = "QN=1;ST=22;CN=2011;PW=123456;MN=ABC;Flag=7;CP=&&DataTime=20250101010101;a21026-Rtd=12.3&&";
15//! let frame = build_frame(payload);
16//! let pkt = parse_frame(&frame).unwrap();
17//!
18//! assert_eq!(pkt.mn.as_deref(), Some("ABC"));
19//! assert_eq!(pkt.cp.get("a21026-Rtd").map(String::as_str), Some("12.3"));
20//! ```
21//!
22//! # Deframe from a stream (TCP/serial)
23//!
24//! ```no_run
25//! use a::{Framer, parse_frame};
26//!
27//! let mut framer = Framer::new();
28//! framer.push(b"##0025QN=1;ST=22;CN=2011;CP=&&&&");
29//!
30//! while let Some(frame_bytes) = framer.next_frame() {
31//!     let frame = String::from_utf8(frame_bytes).expect("HJ212 is ASCII");
32//!     let _pkt = parse_frame(&frame)?;
33//! }
34//!
35//! # Ok::<(), a::Hj212Error>(())
36//! ```
37//!
38//! # Build a sending frame
39//!
40//! ```
41//! use a::{CpBuilder, PayloadBuilder, parse_frame};
42//!
43//! let mut cp = CpBuilder::new();
44//! cp.data_time("20250101010101")
45//!   .rtd_flag("a21026", "12.3", "N");
46//!
47//! let frame = PayloadBuilder::new("QN1", "123456", "ABC", cp.build()).frame();
48//! let pkt = parse_frame(&frame).unwrap();
49//! assert_eq!(pkt.mn.as_deref(), Some("ABC"));
50//! ```
51
52mod builder;
53mod crc;
54mod datatime;
55mod error;
56pub mod crypto;
57mod flag;
58mod frame;
59mod framer;
60mod packet;
61mod multimedia;
62
63pub use builder::{build_data_ack, build_exe_rtn, build_notify_ack, build_qn_rtn, CpBuilder, PayloadBuilder};
64pub use crc::{
65    crc16_ansi, crc16_hex_lower, crc16_hex_upper, crc16_modbus, crc16_modbus_hex_lower, crc16_modbus_hex_upper,
66};
67pub use crypto::{decrypt_data_segment, encrypt_data_segment, BlockCipher16, EncryptedDataSegment};
68pub use datatime::parse_datatime_to_utc;
69pub use error::Hj212Error;
70pub use flag::Hj212Flag;
71pub use frame::{build_frame, build_frame_compat, build_frame_standard, parse_frame, parse_frame_strict};
72pub use framer::Framer;
73pub use multimedia::{MediaFormat, MediaType};
74pub use packet::Hj212Packet;
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn build_and_parse_roundtrip() {
82        let payload = "QN=1;ST=22;CN=2011;PW=123;MN=ABC;Flag=7;CP=&&DataTime=20250101010101;a21026-Rtd=12.3&&";
83        let frame = build_frame(payload);
84        assert!(frame.ends_with("\r\n"));
85        let pkt = parse_frame(&frame).unwrap();
86        assert_eq!(pkt.mn.as_deref(), Some("ABC"));
87        assert_eq!(pkt.data_time.as_deref(), Some("20250101010101"));
88        assert_eq!(pkt.cp.get("a21026-Rtd").map(String::as_str), Some("12.3"));
89    }
90
91    #[test]
92    fn framer_extracts_frames() {
93        let payload = "QN=1;ST=22;CN=2011;PW=123;MN=ABC;Flag=7;CP=&&DataTime=20250101010101&&";
94        let frame = build_frame(payload);
95        let mut fr = Framer::new();
96        fr.push(frame[..5].as_bytes());
97        assert!(fr.next_frame().is_none());
98        fr.push(frame[5..].as_bytes());
99        let out = fr.next_frame().unwrap();
100        assert_eq!(String::from_utf8_lossy(&out), frame);
101    }
102
103    #[test]
104    fn standard_frame_roundtrip_and_suffix() {
105        let payload = "QN=20250101010101001;ST=22;CN=2011;PW=123;MN=ABC;Flag=9;CP=&&DataTime=20250101010101&&";
106        let frame = build_frame_standard(payload);
107        assert!(frame.ends_with("\r\n"));
108        let pkt = parse_frame_strict(&frame).unwrap();
109        let flag = pkt.flag_bits().unwrap();
110        assert_eq!(flag.version(), 2);
111        assert_eq!(flag.need_ack(), true);
112        assert_eq!(flag.has_packet(), false);
113    }
114
115    #[test]
116    fn strict_parse_standard_text_example_frame() {
117        // Standard text example (line breaks in documents are for formatting; payload bytes do not include '\n').
118        let frame = "##0087QN=20240601085857223;ST=32;CN=1011;PW=123456;MN=010000A8900016F000169DC0;Flag=9;CP=&&&&2200\r\n";
119        let pkt = parse_frame_strict(frame).unwrap();
120
121        assert_eq!(pkt.length_hint, Some(87));
122        assert_eq!(pkt.crc_hex.as_deref(), Some("2200"));
123
124        assert_eq!(pkt.qn.as_deref(), Some("20240601085857223"));
125        assert_eq!(pkt.st.as_deref(), Some("32"));
126        assert_eq!(pkt.cn.as_deref(), Some("1011"));
127        assert_eq!(pkt.pw.as_deref(), Some("123456"));
128        assert_eq!(pkt.mn.as_deref(), Some("010000A8900016F000169DC0"));
129        assert_eq!(pkt.flag.as_deref(), Some("9"));
130
131        // Empty CP body: CP=&&&&
132        assert!(pkt.cp.is_empty());
133    }
134
135    #[test]
136    fn parse_user_provided_compat_frame_len453_crc_c0c1() {
137        // User-provided (compat) frame: 3-digit LEN, lowercase CRC, no CRLF.
138        let frame = "##453QN=20160801085000001;ST=22;CN=2011;PW=123456;MN=010000A8900016F000169DC0;Flag=7;CP=&&DataTime=20160801084000;a21005-Rtd=1.1,a21005-Flag=N;a21004-Rtd=112,a21004-Flag=N;a21026-Rtd=58,a21026-Flag=N;LA-td=50.1,LA-Flag=N;a34004-Rtd=207,a34004-Flag=N;a34002-Rtd=295,a34002-Flag=N;a01001-Rtd=12.6,a01001-Flag=N;a01002-Rtd=32,a01002-Flag=N;a01006-Rtd=101.02,a01006-Flag=N;a01007-Rtd=2.1,a01007-Flag=N;a01008-Rtd=120,a01008-Flag=N;a34001-Rtd=217,a34001-Flag=N;&&c0c1";
139
140        // Validate LEN/CRC consistency under the crate's default ANSI CRC16.
141        let bytes = frame.as_bytes();
142        let mut idx = 2;
143        while idx < bytes.len() && idx < 2 + 4 && (bytes[idx] as char).is_ascii_digit() {
144            idx += 1;
145        }
146        let len_str = std::str::from_utf8(&bytes[2..idx]).unwrap();
147        let declared_len: usize = len_str.parse().unwrap();
148        assert_eq!(declared_len, 453);
149        let payload_start = idx;
150        let payload_end = payload_start + declared_len;
151        let crc_end = payload_end + 4;
152        assert!(bytes.len() >= crc_end);
153        let payload = &bytes[payload_start..payload_end];
154        let got_crc = std::str::from_utf8(&bytes[payload_end..crc_end]).unwrap();
155        let expected_crc = crc16_hex_lower(payload);
156
157        assert_eq!(got_crc.to_lowercase(), "c0c1");
158        assert_eq!(expected_crc, "c0c1");
159
160        // And it should parse successfully.
161        let pkt = parse_frame(frame).unwrap();
162        assert_eq!(pkt.length_hint, Some(453));
163        assert_eq!(pkt.crc_hex.as_deref(), Some("c0c1"));
164        assert_eq!(pkt.qn.as_deref(), Some("20160801085000001"));
165        assert_eq!(pkt.mn.as_deref(), Some("010000A8900016F000169DC0"));
166        assert_eq!(pkt.data_time.as_deref(), Some("20160801084000"));
167
168        // Note: CP parsing splits only on ';', so commas remain inside values.
169        assert_eq!(
170            pkt.cp.get("a21005-Rtd").map(String::as_str),
171            Some("1.1,a21005-Flag=N")
172        );
173    }
174}