#![no_std]
#![forbid(unsafe_code)]
pub const HISTORY_CAPACITY: usize = 32;
pub struct Decoder {
state: State,
last_transition_us: u64,
accum: u8,
space_emitted: bool,
history: heapless::Deque<u8, HISTORY_CAPACITY>,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum State {
Idle,
Down,
Up,
}
impl Decoder {
pub const fn new() -> Self {
Self {
state: State::Idle,
last_transition_us: 0,
accum: 1,
space_emitted: false,
history: heapless::Deque::new(),
}
}
pub fn on_transition(&mut self, now_us: u64, key_down: bool, wpm: u8) {
let dit_us = dit_duration_us(wpm) as u64;
let elapsed = now_us.saturating_sub(self.last_transition_us);
if key_down {
if self.state == State::Up {
self.process_gap(elapsed, dit_us);
}
self.state = State::Down;
} else {
if self.state == State::Down {
self.process_pulse(elapsed, dit_us);
}
self.state = State::Up;
self.space_emitted = false;
}
self.last_transition_us = now_us;
}
pub fn poll(&mut self, now_us: u64, wpm: u8) {
if self.state != State::Up {
return;
}
let dit_us = dit_duration_us(wpm) as u64;
let gap = now_us.saturating_sub(self.last_transition_us);
self.process_gap(gap, dit_us);
}
pub fn snapshot(&self, out: &mut [u8]) -> usize {
let mut n = 0;
for &ch in self.history.iter() {
if n >= out.len() {
break;
}
out[n] = ch;
n += 1;
}
n
}
pub fn len(&self) -> usize {
self.history.len()
}
pub fn is_empty(&self) -> bool {
self.history.is_empty()
}
pub fn clear(&mut self) {
self.history.clear();
self.accum = 1;
self.space_emitted = false;
self.state = State::Idle;
}
fn process_pulse(&mut self, pulse_us: u64, dit_us: u64) {
if self.accum == 0 {
return;
}
if self.accum & 0b1000_0000 != 0 {
self.accum = 0;
return;
}
let bit = if pulse_us >= 2 * dit_us { 1 } else { 0 };
self.accum = (self.accum << 1) | bit;
}
fn process_gap(&mut self, gap_us: u64, dit_us: u64) {
if gap_us >= 6 * dit_us {
self.emit_pending_char();
if !self.space_emitted {
self.push_char(b' ');
self.space_emitted = true;
}
} else if gap_us >= 2 * dit_us {
self.emit_pending_char();
}
}
fn emit_pending_char(&mut self) {
if self.accum > 1 && self.accum < 128 {
let ch = LOOKUP[self.accum as usize];
if ch != 0 {
self.push_char(ch);
}
}
self.accum = 1;
}
fn push_char(&mut self, ch: u8) {
if self.history.is_full() {
let _ = self.history.pop_front();
}
let _ = self.history.push_back(ch);
}
}
impl Default for Decoder {
fn default() -> Self {
Self::new()
}
}
fn dit_duration_us(wpm: u8) -> u32 {
1_200_000 / wpm.max(1) as u32
}
const LOOKUP: [u8; 128] = {
let mut t = [0u8; 128];
t[2] = b'E';
t[3] = b'T';
t[4] = b'I';
t[5] = b'A';
t[6] = b'N';
t[7] = b'M';
t[8] = b'S';
t[9] = b'U';
t[10] = b'R';
t[11] = b'W';
t[12] = b'D';
t[13] = b'K';
t[14] = b'G';
t[15] = b'O';
t[16] = b'H';
t[17] = b'V';
t[18] = b'F';
t[20] = b'L';
t[22] = b'P';
t[23] = b'J';
t[24] = b'B';
t[25] = b'X';
t[26] = b'C';
t[27] = b'Y';
t[28] = b'Z';
t[29] = b'Q';
t[32] = b'5';
t[33] = b'4';
t[35] = b'3';
t[39] = b'2';
t[47] = b'1';
t[48] = b'6';
t[56] = b'7';
t[60] = b'8';
t[62] = b'9';
t[63] = b'0';
t[49] = b'='; t[50] = b'/'; t[76] = b'?'; t[85] = b'.'; t[115] = b','; t
};
#[cfg(test)]
mod tests {
use super::*;
fn decode(wpm: u8, durations: &[(bool, u64)]) -> heapless::String<64> {
let mut d = Decoder::new();
let mut now = 0u64;
for &(k, dur) in durations {
d.on_transition(now, k, wpm);
now += dur;
}
d.poll(now, wpm);
let mut buf = [0u8; 64];
let n = d.snapshot(&mut buf);
let mut out: heapless::String<64> = heapless::String::new();
for &b in &buf[..n] {
let _ = out.push(b as char);
}
out
}
const fn dit_us(wpm: u8) -> u64 {
1_200_000 / wpm as u64
}
#[test]
fn decodes_single_dit_as_e() {
let d = dit_us(20);
let r = decode(20, &[(true, d), (false, 4 * d)]);
assert_eq!(r.as_str(), "E");
}
#[test]
fn decodes_single_dah_as_t() {
let d = dit_us(20);
let r = decode(20, &[(true, 3 * d), (false, 4 * d)]);
assert_eq!(r.as_str(), "T");
}
#[test]
fn decodes_paris() {
let d = dit_us(20);
let s = 3 * d;
let g_intra = d;
let g_inter = 3 * d;
let g_final = 4 * d;
let seq = &[
(true, d),
(false, g_intra),
(true, s),
(false, g_intra),
(true, s),
(false, g_intra),
(true, d),
(false, g_inter),
(true, d),
(false, g_intra),
(true, s),
(false, g_inter),
(true, d),
(false, g_intra),
(true, s),
(false, g_intra),
(true, d),
(false, g_inter),
(true, d),
(false, g_intra),
(true, d),
(false, g_inter),
(true, d),
(false, g_intra),
(true, d),
(false, g_intra),
(true, d),
(false, g_final),
];
assert_eq!(decode(20, seq).as_str(), "PARIS");
}
#[test]
fn emits_word_space_after_7_dit_gap() {
let d = dit_us(20);
let s = 3 * d;
let seq = &[
(true, s),
(false, 7 * d), (true, d),
(false, 4 * d), ];
assert_eq!(decode(20, seq).as_str(), "T E");
}
#[test]
fn space_at_exact_inter_word_threshold() {
let d = dit_us(20);
let s = 3 * d;
let seq = &[(true, s), (false, 6 * d), (true, d), (false, 4 * d)];
assert_eq!(decode(20, seq).as_str(), "T E");
}
#[test]
fn no_space_below_inter_word_threshold() {
let d = dit_us(20);
let s = 3 * d;
let seq = &[(true, s), (false, 5 * d), (true, d), (false, 4 * d)];
assert_eq!(decode(20, seq).as_str(), "TE");
}
#[test]
fn decodes_digits() {
let d = dit_us(20);
let s = 3 * d;
let g_intra = d;
let g_inter = 3 * d;
let g_final = 4 * d;
let seq = &[
(true, s),
(false, g_intra),
(true, s),
(false, g_intra),
(true, d),
(false, g_intra),
(true, d),
(false, g_intra),
(true, d),
(false, g_inter),
(true, d),
(false, g_intra),
(true, d),
(false, g_intra),
(true, d),
(false, g_intra),
(true, s),
(false, g_intra),
(true, s),
(false, g_final),
];
assert_eq!(decode(20, seq).as_str(), "73");
}
#[test]
fn poll_alone_flushes_pending_character() {
let d = dit_us(20);
let mut dec = Decoder::new();
dec.on_transition(0, true, 20); dec.on_transition(d, false, 20); dec.poll(d + 4 * d, 20);
let mut buf = [0u8; 4];
let n = dec.snapshot(&mut buf);
assert_eq!(&buf[..n], b"E");
}
#[test]
fn repeated_poll_during_silence_emits_one_space() {
let d = dit_us(20);
let mut dec = Decoder::new();
dec.on_transition(0, true, 20);
dec.on_transition(d, false, 20);
for _ in 0..3 {
dec.poll(d + 10 * d, 20);
}
let mut buf = [0u8; 8];
let n = dec.snapshot(&mut buf);
assert_eq!(&buf[..n], b"E ");
}
#[test]
fn clear_resets_history_and_in_flight() {
let d = dit_us(20);
let mut dec = Decoder::new();
dec.on_transition(0, true, 20);
dec.on_transition(d, false, 20);
dec.poll(d + 4 * d, 20);
assert_eq!(dec.len(), 1);
dec.clear();
assert!(dec.is_empty());
dec.on_transition(0, true, 20);
dec.on_transition(3 * d, false, 20);
dec.poll(3 * d + 4 * d, 20);
let mut buf = [0u8; 4];
let n = dec.snapshot(&mut buf);
assert_eq!(&buf[..n], b"T");
}
#[test]
fn overflowed_character_silently_dropped() {
let d = dit_us(20);
let mut dec = Decoder::new();
let mut now = 0u64;
let mut last_edge = 0u64;
for _ in 0..10 {
dec.on_transition(now, true, 20);
now += d;
dec.on_transition(now, false, 20);
last_edge = now;
now += d; }
dec.poll(last_edge + 3 * d, 20);
assert!(
dec.is_empty(),
"expected no decoded chars from overflow, got len={}",
dec.len()
);
}
#[test]
fn history_evicts_oldest_on_overflow() {
let d = dit_us(20);
let mut dec = Decoder::new();
let mut now = 0u64;
for _ in 0..40 {
dec.on_transition(now, true, 20);
now += d;
dec.on_transition(now, false, 20);
now += 3 * d;
}
dec.poll(now + 2 * d, 20);
let mut buf = [0u8; 64];
let n = dec.snapshot(&mut buf);
assert_eq!(n, HISTORY_CAPACITY);
assert!(buf[..n].iter().all(|&b| b == b'E'));
}
}