1use heapless::LinearMap;
9use postcard::experimental::max_size::MaxSize;
10use serde::{Deserialize, Serialize};
11
12use crate::action::Action;
13use crate::constants::MORSE_SIZE;
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, MaxSize)]
21#[cfg_attr(feature = "defmt", derive(defmt::Format))]
22#[repr(u8)]
23#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
24#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
25pub enum MorseMode {
26 PermissiveHold,
30 HoldOnOtherPress,
32 Normal,
34}
35
36#[derive(PartialEq, Eq, Clone, Copy, Debug, MaxSize)]
56#[cfg_attr(feature = "defmt", derive(defmt::Format))]
57pub struct MorseProfile(u64);
58
59const TIMEOUT_MASK: u64 = 0x1FFF;
60const TIMEOUT_MAX_MS: u16 = TIMEOUT_MASK as u16;
61const GAP_TIMEOUT_SHIFT: u32 = 17;
62const HOLD_TIMEOUT_MASK: u64 = TIMEOUT_MASK;
63const GAP_TIMEOUT_MASK: u64 = TIMEOUT_MASK << GAP_TIMEOUT_SHIFT;
64const UNI_TAP_LOW_BIT: u64 = 0x0000_8000;
65const UNI_TAP_HIGH_BIT: u64 = 0x0001_0000;
66const UNI_TAP_MASK: u64 = UNI_TAP_LOW_BIT | UNI_TAP_HIGH_BIT;
67const FLOW_TAP_LOW_BIT: u64 = 0x0000_2000;
68const FLOW_TAP_HIGH_BIT: u64 = 0x0000_4000;
69const FLOW_TAP_MASK: u64 = FLOW_TAP_LOW_BIT | FLOW_TAP_HIGH_BIT;
70const MODE_MASK: u64 = 0xC000_0000;
71const QT_VALUE_MASK: u64 = TIMEOUT_MASK << 32;
72const QT_SET_BIT: u64 = 1 << 45;
73
74const fn encode_timeout_ms(t: u16) -> u64 {
75 if t > TIMEOUT_MAX_MS {
76 TIMEOUT_MAX_MS as u64
77 } else {
78 t as u64
79 }
80}
81
82impl MorseProfile {
83 pub const fn const_default() -> Self {
84 Self(0)
85 }
86
87 pub fn unilateral_tap(self) -> Option<bool> {
89 match (self.0 & UNI_TAP_MASK) >> 15 {
90 3 => Some(true),
91 2 => Some(false),
92 _ => None,
93 }
94 }
95
96 pub const fn with_unilateral_tap(self, b: Option<bool>) -> Self {
97 Self(
98 (self.0 & !UNI_TAP_MASK)
99 | match b {
100 Some(true) => UNI_TAP_MASK,
101 Some(false) => UNI_TAP_HIGH_BIT,
102 None => 0,
103 },
104 )
105 }
106
107 pub fn enable_flow_tap(self) -> Option<bool> {
110 match (self.0 & FLOW_TAP_MASK) >> 13 {
111 3 => Some(true),
112 2 => Some(false),
113 _ => None,
114 }
115 }
116
117 pub const fn with_enable_flow_tap(self, b: Option<bool>) -> Self {
118 Self(
119 (self.0 & !FLOW_TAP_MASK)
120 | match b {
121 Some(true) => FLOW_TAP_MASK,
122 Some(false) => FLOW_TAP_HIGH_BIT,
123 None => 0,
124 },
125 )
126 }
127
128 pub fn mode(self) -> Option<MorseMode> {
130 match self.0 & MODE_MASK {
131 MODE_MASK => Some(MorseMode::Normal),
132 0x8000_0000 => Some(MorseMode::HoldOnOtherPress),
133 0x4000_0000 => Some(MorseMode::PermissiveHold),
134 _ => None,
135 }
136 }
137
138 pub const fn with_mode(self, m: Option<MorseMode>) -> Self {
139 Self(
140 (self.0 & !MODE_MASK)
141 | match m {
142 Some(MorseMode::Normal) => MODE_MASK,
143 Some(MorseMode::HoldOnOtherPress) => 0x8000_0000,
144 Some(MorseMode::PermissiveHold) => 0x4000_0000,
145 None => 0,
146 },
147 )
148 }
149
150 pub fn hold_timeout_ms(self) -> Option<u16> {
152 let t = (self.0 & HOLD_TIMEOUT_MASK) as u16;
153 if t == 0 { None } else { Some(t) }
154 }
155
156 pub const fn with_hold_timeout_ms(self, t: Option<u16>) -> Self {
157 if let Some(t) = t {
158 Self((self.0 & !HOLD_TIMEOUT_MASK) | encode_timeout_ms(t))
159 } else {
160 Self(self.0 & !HOLD_TIMEOUT_MASK)
161 }
162 }
163
164 pub const fn set_hold_timeout_ms(&mut self, t: u16) {
165 self.0 = (self.0 & !HOLD_TIMEOUT_MASK) | encode_timeout_ms(t)
166 }
167
168 pub const fn set_gap_timeout_ms(&mut self, t: u16) {
169 self.0 = (self.0 & !GAP_TIMEOUT_MASK) | (encode_timeout_ms(t) << GAP_TIMEOUT_SHIFT)
170 }
171
172 pub fn gap_timeout_ms(self) -> Option<u16> {
174 let t = ((self.0 & GAP_TIMEOUT_MASK) >> GAP_TIMEOUT_SHIFT) as u16;
175 if t == 0 { None } else { Some(t) }
176 }
177
178 pub const fn with_gap_timeout_ms(self, t: Option<u16>) -> Self {
179 if let Some(t) = t {
180 Self((self.0 & !GAP_TIMEOUT_MASK) | (encode_timeout_ms(t) << GAP_TIMEOUT_SHIFT))
181 } else {
182 Self(self.0 & !GAP_TIMEOUT_MASK)
183 }
184 }
185
186 pub const fn quick_tap_timeout_ms(self) -> Option<u16> {
187 if self.0 & QT_SET_BIT != 0 {
188 Some(((self.0 >> 32) & TIMEOUT_MASK) as u16)
189 } else {
190 None
191 }
192 }
193
194 pub const fn with_quick_tap_timeout_ms(self, t: Option<u16>) -> Self {
195 if let Some(t) = t {
196 Self((self.0 & !(QT_VALUE_MASK | QT_SET_BIT)) | (encode_timeout_ms(t) << 32) | QT_SET_BIT)
197 } else {
198 Self(self.0 & !(QT_VALUE_MASK | QT_SET_BIT))
199 }
200 }
201
202 pub const fn set_quick_tap_timeout_ms(&mut self, t: u16) {
203 self.0 = (self.0 & !(QT_VALUE_MASK | QT_SET_BIT)) | (encode_timeout_ms(t) << 32) | QT_SET_BIT;
204 }
205
206 pub const fn new(
207 unilateral_tap: Option<bool>,
208 mode: Option<MorseMode>,
209 hold_timeout_ms: Option<u16>,
210 gap_timeout_ms: Option<u16>,
211 ) -> Self {
212 let mut v = 0u64;
213 if let Some(t) = hold_timeout_ms {
214 v = encode_timeout_ms(t);
215 }
216 if let Some(t) = gap_timeout_ms {
217 v |= encode_timeout_ms(t) << GAP_TIMEOUT_SHIFT;
218 }
219 if let Some(b) = unilateral_tap {
220 v |= if b { UNI_TAP_MASK } else { UNI_TAP_HIGH_BIT };
221 }
222 if let Some(m) = mode {
223 v |= match m {
224 MorseMode::Normal => MODE_MASK,
225 MorseMode::HoldOnOtherPress => 0x8000_0000,
226 MorseMode::PermissiveHold => 0x4000_0000,
227 };
228 }
229 MorseProfile(v)
230 }
231}
232
233impl Default for MorseProfile {
234 fn default() -> Self {
235 MorseProfile::const_default()
236 }
237}
238
239impl From<u64> for MorseProfile {
240 fn from(v: u64) -> Self {
241 MorseProfile(v)
242 }
243}
244
245impl From<MorseProfile> for u64 {
246 fn from(val: MorseProfile) -> Self {
247 val.0
248 }
249}
250
251impl Serialize for MorseProfile {
253 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
254 if serializer.is_human_readable() {
255 #[derive(Serialize)]
256 struct Repr {
257 unilateral_tap: Option<bool>,
258 enable_flow_tap: Option<bool>,
259 mode: Option<MorseMode>,
260 hold_timeout_ms: Option<u16>,
261 gap_timeout_ms: Option<u16>,
262 quick_tap_timeout_ms: Option<u16>,
263 }
264 Repr {
265 unilateral_tap: self.unilateral_tap(),
266 enable_flow_tap: self.enable_flow_tap(),
267 mode: self.mode(),
268 hold_timeout_ms: self.hold_timeout_ms(),
269 gap_timeout_ms: self.gap_timeout_ms(),
270 quick_tap_timeout_ms: self.quick_tap_timeout_ms(),
271 }
272 .serialize(serializer)
273 } else {
274 serializer.serialize_u64(self.0)
275 }
276 }
277}
278
279impl<'de> Deserialize<'de> for MorseProfile {
280 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
281 if deserializer.is_human_readable() {
282 #[derive(Deserialize)]
283 struct Repr {
284 unilateral_tap: Option<bool>,
285 enable_flow_tap: Option<bool>,
286 mode: Option<MorseMode>,
287 hold_timeout_ms: Option<u16>,
288 gap_timeout_ms: Option<u16>,
289 quick_tap_timeout_ms: Option<u16>,
290 }
291 let r = Repr::deserialize(deserializer)?;
292 Ok(
293 MorseProfile::new(r.unilateral_tap, r.mode, r.hold_timeout_ms, r.gap_timeout_ms)
294 .with_enable_flow_tap(r.enable_flow_tap)
295 .with_quick_tap_timeout_ms(r.quick_tap_timeout_ms),
296 )
297 } else {
298 Ok(MorseProfile(u64::deserialize(deserializer)?))
299 }
300 }
301}
302
303#[cfg(feature = "wasm")]
305const _: () = {
306 #[::wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)]
307 const TS_APPEND_CONTENT: &'static str = "export type MorseProfile = { unilateral_tap: boolean | undefined; enable_flow_tap: boolean | undefined; mode: MorseMode | undefined; hold_timeout_ms: number | undefined; gap_timeout_ms: number | undefined; quick_tap_timeout_ms: number | undefined; };";
308};
309crate::wasm_object_abi!(MorseProfile, "MorseProfile");
310
311#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, MaxSize)]
318#[cfg_attr(feature = "defmt", derive(defmt::Format))]
319#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
320#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
321pub struct MorsePattern(u16);
322
323pub const TAP: MorsePattern = MorsePattern(0b10);
324pub const HOLD: MorsePattern = MorsePattern(0b11);
325pub const DOUBLE_TAP: MorsePattern = MorsePattern(0b100);
326pub const HOLD_AFTER_TAP: MorsePattern = MorsePattern(0b101);
327
328impl Default for MorsePattern {
329 fn default() -> Self {
330 MorsePattern(0b1) }
332}
333
334impl MorsePattern {
335 pub fn max_taps() -> usize {
336 15 }
338
339 pub fn from_u16(value: u16) -> Self {
345 debug_assert!(value != 0, "MorsePattern 0 is invalid; the empty pattern is 0b1");
346 MorsePattern(value)
347 }
348
349 pub fn to_u16(&self) -> u16 {
350 self.0
351 }
352
353 pub fn is_empty(&self) -> bool {
354 self.0 == 0b1
355 }
356
357 pub fn is_full(&self) -> bool {
358 (self.0 & 0b1000_0000_0000_0000) != 0
359 }
360
361 pub fn pattern_length(&self) -> usize {
362 15usize.saturating_sub(self.0.leading_zeros() as usize)
365 }
366
367 pub fn starts_with(&self, pattern_start: MorsePattern) -> bool {
369 let n = pattern_start.0.leading_zeros();
370 let m = self.0.leading_zeros();
371 m <= n && (self.0 >> (n - m) == pattern_start.0)
372 }
373
374 pub fn last_is_hold(&self) -> bool {
377 !self.is_empty() && self.0 & 0b1 == 0b1
378 }
379
380 pub fn followed_by_tap(&self) -> Self {
381 MorsePattern(self.0 << 1)
383 }
384
385 pub fn followed_by_hold(&self) -> Self {
386 MorsePattern((self.0 << 1) | 0b1)
388 }
389
390 pub fn is_all_taps(&self) -> bool {
393 self.0 > 0b1 && self.0 & (self.0 - 1) == 0
394 }
395}
396
397#[derive(Debug, Clone, Default, Serialize, Deserialize)]
407#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
408#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
409pub struct Morse {
410 pub profile: MorseProfile,
413 #[serde(with = "morse_actions_serde")]
415 #[cfg_attr(feature = "wasm", tsify(type = "[number, Action][]"))]
416 pub actions: LinearMap<MorsePattern, Action, MORSE_SIZE>,
417}
418
419impl MaxSize for Morse {
420 const POSTCARD_MAX_SIZE: usize =
424 MorseProfile::POSTCARD_MAX_SIZE + crate::heapless_vec_max_size::<(u16, Action), MORSE_SIZE>();
425}
426
427#[cfg(feature = "defmt")]
428impl defmt::Format for Morse {
429 fn format(&self, f: defmt::Formatter<'_>) {
430 defmt::write!(f, "profile: MorseProfile({:?}), ", self.profile);
431 defmt::write!(f, "actions: [");
432 for item in self.actions.iter() {
433 defmt::write!(f, "{:?},", item);
434 }
435 defmt::write!(f, "]");
436 }
437}
438
439impl PartialEq for Morse {
440 fn eq(&self, other: &Self) -> bool {
441 if self.profile != other.profile || self.actions.len() != other.actions.len() {
442 return false;
443 }
444 self.actions.iter().all(|(k, v)| other.actions.get(k) == Some(v))
445 }
446}
447
448impl Eq for Morse {}
449
450impl Morse {
454 pub fn new_from_vial(
455 tap: Action,
456 hold: Action,
457 hold_after_tap: Action,
458 double_tap: Action,
459 profile: MorseProfile,
460 ) -> Self {
461 let mut result = Self {
462 profile,
463 ..Default::default()
464 };
465
466 if tap != Action::No {
467 _ = result.actions.insert(TAP, tap);
468 }
469 if hold != Action::No {
470 _ = result.actions.insert(HOLD, hold);
471 }
472 if double_tap != Action::No {
473 _ = result.actions.insert(DOUBLE_TAP, double_tap);
474 }
475 if hold_after_tap != Action::No {
476 _ = result.actions.insert(HOLD_AFTER_TAP, hold_after_tap);
477 }
478 result
479 }
480
481 pub fn new_with_actions(
482 tap_actions: heapless::Vec<Action, MORSE_SIZE>,
483 hold_actions: heapless::Vec<Action, MORSE_SIZE>,
484 profile: MorseProfile,
485 ) -> Self {
486 let mut result = Self {
487 profile,
488 ..Default::default()
489 };
490
491 let mut pattern = 0b1u16;
492 for item in tap_actions.iter() {
493 pattern <<= 1;
494 let _ = result.put(MorsePattern::from_u16(pattern), *item);
495 }
496
497 let mut pattern = 0b1u16;
498 for item in hold_actions.iter() {
499 pattern <<= 1;
500 let _ = result.put(MorsePattern::from_u16(pattern | 0b1), *item);
501 }
502
503 result
504 }
505
506 pub fn max_pattern_length(&self) -> usize {
507 let mut max_length = 0;
508 for pair in self.actions.iter() {
509 max_length = max_length.max(pair.0.pattern_length());
510 }
511 max_length
512 }
513
514 pub fn try_predict_final_action(&self, pattern_start: MorsePattern) -> Option<Action> {
515 if !self.actions.contains_key(&pattern_start) {
516 return None;
517 }
518 for (pattern, _) in self.actions.iter() {
519 if *pattern != pattern_start && pattern.starts_with(pattern_start) {
520 return None;
521 }
522 }
523 self.actions.get(&pattern_start).copied()
524 }
525
526 pub fn can_fire_early(&self, pattern: MorsePattern) -> bool {
527 let Some(current_action) = self.actions.get(&pattern) else {
528 return false;
529 };
530 if self.actions.contains_key(&pattern.followed_by_tap()) {
531 return false;
532 }
533 self.actions
534 .get(&pattern.followed_by_hold())
535 .is_some_and(|a| *a == *current_action)
536 }
537
538 pub fn has_pattern_or_continuation(&self, pattern: MorsePattern) -> bool {
539 self.actions.iter().any(|(p, _)| p.starts_with(pattern))
540 }
541
542 pub fn get(&self, pattern: MorsePattern) -> Option<Action> {
543 self.actions.get(&pattern).copied()
544 }
545
546 pub fn put(&mut self, pattern: MorsePattern, action: Action) -> Result<(), (MorsePattern, Action)> {
550 if action != Action::No {
551 self.actions.insert(pattern, action).map(|_| ())
552 } else {
553 let _ = self.actions.remove(&pattern);
554 Ok(())
555 }
556 }
557}
558
559mod morse_actions_serde {
561 use serde::de::Error;
562 use serde::{Deserializer, Serializer};
563
564 use super::*;
565
566 pub fn serialize<S>(map: &LinearMap<MorsePattern, Action, MORSE_SIZE>, serializer: S) -> Result<S::Ok, S::Error>
567 where
568 S: Serializer,
569 {
570 let vec: heapless::Vec<(u16, Action), MORSE_SIZE> = map.iter().map(|(k, v)| (k.to_u16(), *v)).collect();
572 vec.serialize(serializer)
573 }
574
575 pub fn deserialize<'de, D>(deserializer: D) -> Result<LinearMap<MorsePattern, Action, MORSE_SIZE>, D::Error>
576 where
577 D: Deserializer<'de>,
578 {
579 use core::fmt;
580
581 use serde::de::{SeqAccess, Visitor};
582
583 struct VecVisitor;
584
585 impl<'de> Visitor<'de> for VecVisitor {
586 type Value = heapless::Vec<(u16, Action), MORSE_SIZE>;
587
588 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
589 write!(formatter, "a sequence of (u16, Action) tuples")
590 }
591
592 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
593 where
594 A: SeqAccess<'de>,
595 {
596 let mut vec = heapless::Vec::new();
597 while let Some(elem) = seq.next_element::<(u16, Action)>()? {
598 vec.push(elem)
599 .map_err(|_| serde::de::Error::custom("Vec capacity exceeded"))?;
600 }
601 Ok(vec)
602 }
603 }
604
605 let vec = deserializer.deserialize_seq(VecVisitor)?;
606 let mut map = LinearMap::new();
607 for (pattern, action) in vec {
608 if pattern == 0 {
609 return Err(D::Error::custom("MorsePattern 0 is invalid; the empty pattern is 0b1"));
610 }
611 map.insert(MorsePattern::from_u16(pattern), action)
612 .map_err(|_| D::Error::custom("Failed to insert into LinearMap"))?;
613 }
614 Ok(map)
615 }
616}
617
618#[cfg(test)]
619mod tests {
620 extern crate alloc;
621
622 use super::*;
623 use crate::action::Action;
624 use crate::keycode::{HidKeyCode, KeyCode};
625
626 #[test]
627 fn test_linear_map_serde_empty() {
628 let morse = Morse::default();
629
630 let mut buffer = [0u8; 128];
631 let serialized = postcard::to_slice(&morse, &mut buffer).unwrap();
632 let deserialized: Morse = postcard::from_bytes(serialized).unwrap();
633
634 assert_eq!(morse.actions.len(), deserialized.actions.len());
635 assert_eq!(morse.actions.len(), 0);
636 }
637
638 #[test]
639 fn test_linear_map_serde_single_entry() {
640 let mut morse = Morse::default();
641 morse.actions.insert(TAP, Action::Key(KeyCode::Hid(HidKeyCode::A))).ok();
642
643 let mut buffer = [0u8; 128];
644 let serialized = postcard::to_slice(&morse, &mut buffer).unwrap();
645 let deserialized: Morse = postcard::from_bytes(serialized).unwrap();
646
647 assert_eq!(morse.actions.len(), deserialized.actions.len());
648 assert_eq!(
649 deserialized.actions.get(&TAP),
650 Some(&Action::Key(KeyCode::Hid(HidKeyCode::A)))
651 );
652 }
653
654 #[test]
655 fn test_linear_map_serde_multiple_entries() {
656 let mut morse = Morse::default();
657 morse.actions.insert(TAP, Action::Key(KeyCode::Hid(HidKeyCode::A))).ok();
658 morse
659 .actions
660 .insert(HOLD, Action::Key(KeyCode::Hid(HidKeyCode::B)))
661 .ok();
662 morse
663 .actions
664 .insert(DOUBLE_TAP, Action::Key(KeyCode::Hid(HidKeyCode::C)))
665 .ok();
666 morse
667 .actions
668 .insert(HOLD_AFTER_TAP, Action::Key(KeyCode::Hid(HidKeyCode::D)))
669 .ok();
670
671 let mut buffer = [0u8; 128];
672 let serialized = postcard::to_slice(&morse, &mut buffer).unwrap();
673 let deserialized: Morse = postcard::from_bytes(serialized).unwrap();
674
675 assert_eq!(morse.actions.len(), deserialized.actions.len());
676 assert_eq!(morse.actions.len(), 4);
677
678 assert_eq!(
679 deserialized.actions.get(&TAP),
680 Some(&Action::Key(KeyCode::Hid(HidKeyCode::A)))
681 );
682 assert_eq!(
683 deserialized.actions.get(&HOLD),
684 Some(&Action::Key(KeyCode::Hid(HidKeyCode::B)))
685 );
686 assert_eq!(
687 deserialized.actions.get(&DOUBLE_TAP),
688 Some(&Action::Key(KeyCode::Hid(HidKeyCode::C)))
689 );
690 assert_eq!(
691 deserialized.actions.get(&HOLD_AFTER_TAP),
692 Some(&Action::Key(KeyCode::Hid(HidKeyCode::D)))
693 );
694 }
695
696 #[test]
697 fn test_linear_map_serde_with_profile() {
698 let mut morse = Morse {
699 profile: MorseProfile::new(Some(true), Some(MorseMode::PermissiveHold), Some(200), Some(150)),
700 ..Default::default()
701 };
702 morse.actions.insert(TAP, Action::Key(KeyCode::Hid(HidKeyCode::H))).ok();
703 morse
704 .actions
705 .insert(HOLD, Action::Key(KeyCode::Hid(HidKeyCode::I)))
706 .ok();
707
708 let mut buffer = [0u8; 128];
709 let serialized = postcard::to_slice(&morse, &mut buffer).unwrap();
710 let deserialized: Morse = postcard::from_bytes(serialized).unwrap();
711
712 assert_eq!(morse.profile, deserialized.profile);
713 assert_eq!(morse.actions.len(), deserialized.actions.len());
714 }
715
716 #[test]
717 fn morse_pattern_max_size_matches_u16() {
718 assert_eq!(MorsePattern::POSTCARD_MAX_SIZE, u16::POSTCARD_MAX_SIZE,);
722 }
723
724 #[test]
725 fn test_morse_profile_timeout_setters() {
726 let mut profile = MorseProfile::new(Some(true), Some(MorseMode::PermissiveHold), Some(1000), Some(2000));
727
728 assert_eq!(profile.hold_timeout_ms(), Some(1000));
729 assert_eq!(profile.gap_timeout_ms(), Some(2000));
730 assert_eq!(profile.unilateral_tap(), Some(true));
731 assert_eq!(profile.mode(), Some(MorseMode::PermissiveHold));
732
733 profile.set_hold_timeout_ms(1500);
734 assert_eq!(profile.hold_timeout_ms(), Some(1500));
735 assert_eq!(profile.gap_timeout_ms(), Some(2000));
736 assert_eq!(profile.unilateral_tap(), Some(true));
737 assert_eq!(profile.mode(), Some(MorseMode::PermissiveHold));
738
739 profile.set_gap_timeout_ms(2500);
740 assert_eq!(profile.hold_timeout_ms(), Some(1500));
741 assert_eq!(profile.gap_timeout_ms(), Some(2500));
742 assert_eq!(profile.unilateral_tap(), Some(true));
743 assert_eq!(profile.mode(), Some(MorseMode::PermissiveHold));
744
745 profile.set_hold_timeout_ms(0xFFFF);
746 profile.set_gap_timeout_ms(0xFFFF);
747 assert_eq!(profile.hold_timeout_ms(), Some(TIMEOUT_MAX_MS));
748 assert_eq!(profile.gap_timeout_ms(), Some(TIMEOUT_MAX_MS));
749
750 profile.set_hold_timeout_ms(0);
751 profile.set_gap_timeout_ms(0);
752 assert_eq!(profile.hold_timeout_ms(), None);
753 assert_eq!(profile.gap_timeout_ms(), None);
754
755 let p = MorseProfile::const_default().with_quick_tap_timeout_ms(Some(300));
756 assert_eq!(p.quick_tap_timeout_ms(), Some(300));
757 assert_eq!(p.hold_timeout_ms(), MorseProfile::const_default().hold_timeout_ms());
758 assert_eq!(p.gap_timeout_ms(), MorseProfile::const_default().gap_timeout_ms());
759
760 let mut p2 = p;
761 p2.set_quick_tap_timeout_ms(0);
762 assert_eq!(p2.quick_tap_timeout_ms(), Some(0), "set_*_ms(0) is explicit");
763
764 p2.set_quick_tap_timeout_ms(0xFFFF);
765 assert_eq!(p2.quick_tap_timeout_ms(), Some(TIMEOUT_MAX_MS));
766
767 let p3 = p.with_quick_tap_timeout_ms(None);
768 assert_eq!(p3.quick_tap_timeout_ms(), None, "with_*(None) clears the field");
769
770 let p4 = MorseProfile::const_default().with_quick_tap_timeout_ms(Some(0));
771 assert_eq!(p4.quick_tap_timeout_ms(), Some(0), "Some(0) is explicitly disabled");
772 }
773
774 #[test]
775 fn is_all_taps_encoding_invariant() {
776 let tap = MorsePattern::from_u16(0b10);
777 let tap_tap = MorsePattern::from_u16(0b100);
778 let tap_tap_tap = MorsePattern::from_u16(0b1000);
779 let hold = MorsePattern::from_u16(0b11);
780 let tap_hold = MorsePattern::from_u16(0b101);
781 let hold_tap = MorsePattern::from_u16(0b110);
782 let empty = MorsePattern::default();
783
784 assert!(tap.is_all_taps());
785 assert!(tap_tap.is_all_taps());
786 assert!(tap_tap_tap.is_all_taps());
787 assert!(!hold.is_all_taps());
788 assert!(!tap_hold.is_all_taps());
789 assert!(!hold_tap.is_all_taps());
790 assert!(!empty.is_all_taps());
791
792 assert_eq!(tap, MorsePattern::default().followed_by_tap());
793 assert_eq!(tap_tap, tap.followed_by_tap());
794 assert_eq!(hold, MorsePattern::default().followed_by_hold());
795 }
796
797 #[test]
798 fn test_morse_profile_packed_layout_matches_docs() {
799 let profile = MorseProfile::new(
800 Some(false),
801 Some(MorseMode::HoldOnOtherPress),
802 Some(0x0123),
803 Some(0x0456),
804 )
805 .with_enable_flow_tap(Some(false));
806 assert_eq!(
807 u64::from(profile),
808 0x8000_0000 | (0x0456u64 << 17) | 0x0001_0000 | 0x0000_4000 | 0x0123
809 );
810 assert_eq!(profile.unilateral_tap(), Some(false));
811 assert_eq!(profile.enable_flow_tap(), Some(false));
812
813 let profile = MorseProfile::new(Some(true), Some(MorseMode::Normal), Some(0x0123), Some(0x0456))
814 .with_enable_flow_tap(Some(true));
815 assert_eq!(
816 u64::from(profile),
817 0xC000_0000 | (0x0456u64 << 17) | 0x0001_8000 | 0x0000_6000 | 0x0123
818 );
819 assert_eq!(profile.unilateral_tap(), Some(true));
820 assert_eq!(profile.enable_flow_tap(), Some(true));
821 }
822
823 #[test]
824 fn test_morse_profile_enable_flow_tap_accessors_preserve_packed_fields() {
825 assert_eq!(core::mem::size_of::<MorseProfile>(), 8);
826 assert_eq!(MorseProfile::POSTCARD_MAX_SIZE, u64::POSTCARD_MAX_SIZE);
827 assert_eq!(MorseProfile::const_default().enable_flow_tap(), None);
828
829 let profile = MorseProfile::new(Some(true), Some(MorseMode::PermissiveHold), Some(1000), Some(2000));
830 let profile = profile.with_enable_flow_tap(Some(true));
831 assert_eq!(profile.enable_flow_tap(), Some(true));
832 assert_eq!(profile.hold_timeout_ms(), Some(1000));
833 assert_eq!(profile.gap_timeout_ms(), Some(2000));
834 assert_eq!(profile.unilateral_tap(), Some(true));
835 assert_eq!(profile.mode(), Some(MorseMode::PermissiveHold));
836
837 let profile = profile.with_enable_flow_tap(Some(false));
838 assert_eq!(profile.enable_flow_tap(), Some(false));
839 assert_eq!(profile.hold_timeout_ms(), Some(1000));
840 assert_eq!(profile.gap_timeout_ms(), Some(2000));
841 assert_eq!(profile.unilateral_tap(), Some(true));
842 assert_eq!(profile.mode(), Some(MorseMode::PermissiveHold));
843
844 let profile = profile.with_enable_flow_tap(None);
845 assert_eq!(profile.enable_flow_tap(), None);
846 assert_eq!(profile.hold_timeout_ms(), Some(1000));
847 assert_eq!(profile.gap_timeout_ms(), Some(2000));
848 assert_eq!(profile.unilateral_tap(), Some(true));
849 assert_eq!(profile.mode(), Some(MorseMode::PermissiveHold));
850 }
851
852 #[test]
855 fn morse_profile_parts_roundtrip() {
856 for p in [
857 MorseProfile::new(
858 Some(false),
859 Some(MorseMode::HoldOnOtherPress),
860 Some(TIMEOUT_MAX_MS),
861 Some(1),
862 )
863 .with_enable_flow_tap(Some(false)),
864 MorseProfile::new(Some(true), Some(MorseMode::Normal), Some(200), Some(150))
865 .with_enable_flow_tap(Some(true)),
866 MorseProfile::const_default(),
867 ] {
868 let parts = MorseProfile::new(p.unilateral_tap(), p.mode(), p.hold_timeout_ms(), p.gap_timeout_ms())
869 .with_enable_flow_tap(p.enable_flow_tap());
870 assert_eq!(p, parts);
871 }
872 }
873
874 #[test]
881 fn morse_wire_format() {
882 use postcard::to_slice;
883
884 let mut morse = Morse::default();
886 morse.actions.insert(MorsePattern::from_u16(0b11), Action::No).unwrap();
887
888 let mut buf = [0u8; 256];
890 let bytes = to_slice(&morse, &mut buf).unwrap();
891
892 let (profile, rest): (MorseProfile, &[u8]) =
895 postcard::take_from_bytes(bytes).expect("should deserialize MorseProfile first");
896 assert_eq!(profile, MorseProfile::const_default());
897
898 let (actions, rest): (heapless::Vec<(u16, Action), MORSE_SIZE>, &[u8]) =
900 postcard::take_from_bytes(rest).expect("should deserialize actions vec second");
901 assert!(rest.is_empty(), "no trailing bytes should remain");
902
903 assert_eq!(actions.len(), 1);
904 assert_eq!(actions[0], (0b11u16, Action::No));
905 }
906}