1use rtcom_config::{
24 profile::{LineEndingsSection, SerialSection},
25 Profile,
26};
27use rtcom_core::{
28 DataBits, FlowControl, LineEnding, LineEndingConfig, Parity, SerialConfig, StopBits,
29 DEFAULT_READ_TIMEOUT,
30};
31
32#[must_use]
37pub fn serial_section_to_config(s: &SerialSection) -> SerialConfig {
38 SerialConfig {
39 baud_rate: s.baud,
40 data_bits: parse_data_bits(s.data_bits),
41 stop_bits: parse_stop_bits(s.stop_bits),
42 parity: parse_parity(&s.parity),
43 flow_control: parse_flow(&s.flow),
44 read_timeout: DEFAULT_READ_TIMEOUT,
45 }
46}
47
48#[must_use]
52pub fn serial_config_to_section(c: &SerialConfig) -> SerialSection {
53 SerialSection {
54 baud: c.baud_rate,
55 data_bits: c.data_bits.bits(),
56 stop_bits: stop_bits_number(c.stop_bits),
57 parity: parity_word(c.parity).into(),
58 flow: flow_word(c.flow_control).into(),
59 }
60}
61
62#[must_use]
66pub fn line_endings_from_profile(p: &Profile) -> LineEndingConfig {
67 LineEndingConfig {
68 omap: parse_line_ending(&p.line_endings.omap),
69 imap: parse_line_ending(&p.line_endings.imap),
70 emap: parse_line_ending(&p.line_endings.emap),
71 }
72}
73
74#[must_use]
81pub fn line_ending_config_to_section(c: &LineEndingConfig) -> LineEndingsSection {
82 LineEndingsSection {
83 omap: line_ending_word(c.omap).into(),
84 imap: line_ending_word(c.imap).into(),
85 emap: line_ending_word(c.emap).into(),
86 }
87}
88
89const fn line_ending_word(le: LineEnding) -> &'static str {
90 match le {
91 LineEnding::None => "none",
92 LineEnding::AddCrToLf => "crlf",
93 LineEnding::AddLfToCr => "lfcr",
94 LineEnding::DropCr => "igncr",
95 LineEnding::DropLf => "ignlf",
96 }
97}
98
99fn parse_parity(s: &str) -> Parity {
100 match s.to_ascii_lowercase().as_str() {
101 "even" => Parity::Even,
102 "odd" => Parity::Odd,
103 "mark" => Parity::Mark,
104 "space" => Parity::Space,
105 _ => Parity::None,
106 }
107}
108
109fn parse_flow(s: &str) -> FlowControl {
110 match s.to_ascii_lowercase().as_str() {
111 "hw" | "hardware" | "rtscts" => FlowControl::Hardware,
112 "sw" | "software" | "xonxoff" => FlowControl::Software,
113 _ => FlowControl::None,
114 }
115}
116
117const fn parse_data_bits(n: u8) -> DataBits {
118 match n {
119 5 => DataBits::Five,
120 6 => DataBits::Six,
121 7 => DataBits::Seven,
122 _ => DataBits::Eight,
123 }
124}
125
126const fn parse_stop_bits(n: u8) -> StopBits {
127 match n {
128 2 => StopBits::Two,
129 _ => StopBits::One,
130 }
131}
132
133#[must_use]
136pub fn parse_line_ending(s: &str) -> LineEnding {
137 match s.to_ascii_lowercase().as_str() {
138 "crlf" => LineEnding::AddCrToLf,
139 "lfcr" => LineEnding::AddLfToCr,
140 "igncr" => LineEnding::DropCr,
141 "ignlf" => LineEnding::DropLf,
142 _ => LineEnding::None,
143 }
144}
145
146const fn parity_word(p: Parity) -> &'static str {
147 match p {
148 Parity::None => "none",
149 Parity::Even => "even",
150 Parity::Odd => "odd",
151 Parity::Mark => "mark",
152 Parity::Space => "space",
153 }
154}
155
156const fn flow_word(f: FlowControl) -> &'static str {
157 match f {
158 FlowControl::None => "none",
159 FlowControl::Hardware => "hw",
160 FlowControl::Software => "sw",
161 }
162}
163
164const fn stop_bits_number(s: StopBits) -> u8 {
165 match s {
166 StopBits::One => 1,
167 StopBits::Two => 2,
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn serial_section_round_trip() {
177 let original = SerialConfig {
178 baud_rate: 9600,
179 data_bits: DataBits::Seven,
180 stop_bits: StopBits::Two,
181 parity: Parity::Even,
182 flow_control: FlowControl::Hardware,
183 read_timeout: DEFAULT_READ_TIMEOUT,
184 };
185 let section = serial_config_to_section(&original);
186 let back = serial_section_to_config(§ion);
187 assert_eq!(back.baud_rate, original.baud_rate);
188 assert_eq!(back.data_bits, original.data_bits);
189 assert_eq!(back.stop_bits, original.stop_bits);
190 assert_eq!(back.parity, original.parity);
191 assert_eq!(back.flow_control, original.flow_control);
192 }
193
194 #[test]
195 fn unknown_parity_string_falls_back_to_none() {
196 let section = SerialSection {
197 parity: "quantum".into(),
198 ..SerialSection::default()
199 };
200 let cfg = serial_section_to_config(§ion);
201 assert_eq!(cfg.parity, Parity::None);
202 }
203
204 #[test]
205 fn unknown_flow_string_falls_back_to_none() {
206 let section = SerialSection {
207 flow: "teleport".into(),
208 ..SerialSection::default()
209 };
210 let cfg = serial_section_to_config(§ion);
211 assert_eq!(cfg.flow_control, FlowControl::None);
212 }
213
214 #[test]
215 fn out_of_range_data_bits_fall_back_to_eight() {
216 let section = SerialSection {
217 data_bits: 42,
218 ..SerialSection::default()
219 };
220 let cfg = serial_section_to_config(§ion);
221 assert_eq!(cfg.data_bits, DataBits::Eight);
222 }
223
224 #[test]
225 fn out_of_range_stop_bits_fall_back_to_one() {
226 let section = SerialSection {
227 stop_bits: 9,
228 ..SerialSection::default()
229 };
230 let cfg = serial_section_to_config(§ion);
231 assert_eq!(cfg.stop_bits, StopBits::One);
232 }
233
234 #[test]
235 fn line_endings_from_profile_reads_all_three_slots() {
236 let mut profile = Profile::default();
237 profile.line_endings.omap = "crlf".into();
238 profile.line_endings.imap = "igncr".into();
239 profile.line_endings.emap = "lfcr".into();
240 let le = line_endings_from_profile(&profile);
241 assert_eq!(le.omap, LineEnding::AddCrToLf);
242 assert_eq!(le.imap, LineEnding::DropCr);
243 assert_eq!(le.emap, LineEnding::AddLfToCr);
244 }
245
246 #[test]
247 fn line_endings_from_profile_default_is_all_none() {
248 let profile = Profile::default();
249 let le = line_endings_from_profile(&profile);
250 assert_eq!(le.omap, LineEnding::None);
251 assert_eq!(le.imap, LineEnding::None);
252 assert_eq!(le.emap, LineEnding::None);
253 }
254
255 #[test]
256 fn parse_line_ending_covers_all_known_forms() {
257 assert_eq!(parse_line_ending("crlf"), LineEnding::AddCrToLf);
258 assert_eq!(parse_line_ending("lfcr"), LineEnding::AddLfToCr);
259 assert_eq!(parse_line_ending("igncr"), LineEnding::DropCr);
260 assert_eq!(parse_line_ending("ignlf"), LineEnding::DropLf);
261 assert_eq!(parse_line_ending("none"), LineEnding::None);
262 assert_eq!(parse_line_ending("bogus"), LineEnding::None);
263 }
264
265 #[test]
266 fn line_ending_config_to_section_round_trips() {
267 let original = LineEndingConfig {
268 omap: LineEnding::AddCrToLf,
269 imap: LineEnding::DropLf,
270 emap: LineEnding::None,
271 };
272 let section = line_ending_config_to_section(&original);
273 let back = line_endings_from_profile(&Profile {
274 line_endings: section,
275 ..Profile::default()
276 });
277 assert_eq!(back.omap, LineEnding::AddCrToLf);
278 assert_eq!(back.imap, LineEnding::DropLf);
279 assert_eq!(back.emap, LineEnding::None);
280 }
281
282 #[test]
283 fn line_ending_config_to_section_emits_known_vocabulary() {
284 let cfg = LineEndingConfig {
285 omap: LineEnding::AddCrToLf,
286 imap: LineEnding::AddLfToCr,
287 emap: LineEnding::DropCr,
288 };
289 let section = line_ending_config_to_section(&cfg);
290 assert_eq!(section.omap, "crlf");
291 assert_eq!(section.imap, "lfcr");
292 assert_eq!(section.emap, "igncr");
293 }
294
295 #[test]
296 fn serial_config_to_section_emits_stable_strings() {
297 let cfg = SerialConfig {
298 baud_rate: 9600,
299 data_bits: DataBits::Seven,
300 stop_bits: StopBits::Two,
301 parity: Parity::Even,
302 flow_control: FlowControl::Hardware,
303 read_timeout: DEFAULT_READ_TIMEOUT,
304 };
305 let section = serial_config_to_section(&cfg);
306 assert_eq!(section.baud, 9600);
307 assert_eq!(section.data_bits, 7);
308 assert_eq!(section.stop_bits, 2);
309 assert_eq!(section.parity, "even");
310 assert_eq!(section.flow, "hw");
311 }
312}