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