morse_codec/encoder.rs
1//! Morse code encoder to turn text into morse code text or signals.
2//!
3//! The encoder takes [&str] literals or characters and
4//! turns them into a fixed length char array. Then client code can encode these characters
5//! to morse code either character by character, from slices, or all in one go.
6//! Encoded morse code can be retrieved as morse character arrays ie. ['.','-','.'] or Signal
7//! Duration Multipliers [SDMArray] to calculate individual signal durations by the client code.
8//!
9//! This module is designed to be no_std compliant so it also should work on embedded platforms.
10//!
11//! ```rust
12//! use morse_codec::encoder::Encoder;
13//!
14//! const MSG_MAX: usize = 16;
15//! let mut encoder = Encoder::<MSG_MAX>::new()
16//! // We have the message to encode ready and pass it to the builder.
17//! // We pass true as second parameter to tell the encoder editing will
18//! // continue from the end of this first string.
19//! .with_message("SOS", true)
20//! .build();
21//!
22//! // Encode the whole message
23//! encoder.encode_message_all();
24//!
25//! let encoded_charrays = encoder.get_encoded_message_as_morse_charrays();
26//!
27//! encoded_charrays.for_each(|charray| {
28//! for ch in charray.unwrap().iter()
29//! .filter(|ch| ch.is_some()) {
30//! print!("{}", ch.unwrap() as char);
31//! }
32//!
33//! print!(" ");
34//! });
35//!
36//! // This should print "... --- ..."
37
38use crate::{
39 message::Message,
40 CharacterSet,
41 MorseCodeSet,
42 MorseCodeArray,
43 MorseSignal::{Long as L, Short as S},
44 DEFAULT_MORSE_CODE_SET,
45 DEFAULT_CHARACTER_SET,
46 MORSE_ARRAY_LENGTH,
47 MORSE_DEFAULT_CHAR,
48 LONG_SIGNAL_MULTIPLIER,
49 WORD_SPACE_MULTIPLIER,
50 Character,
51};
52
53const DIT: Character = '.' as Character;
54const DAH: Character = '-' as Character;
55const WORD_DELIMITER: Character = '/' as Character;
56const SDM_LENGTH: usize = 12;
57
58/// Signal Duration Multiplier can be 1x (short), 3x (long) or 7x (word space).
59/// SDM signals are either High, or Low which corresponds to
60/// electrically closed active signals or spaces inbetween them.
61#[derive(PartialEq, Copy, Clone, Debug)]
62pub enum SDM {
63 Empty,
64 High(u8),
65 Low(u8),
66}
67
68use SDM::{Empty as SDMEmpty, High as SDMHigh, Low as SDMLow};
69
70pub type MorseCharray = [Option<Character>; MORSE_ARRAY_LENGTH];
71
72/// Signal Duration Multipliers are arrays of u8 values
73/// which can be used to multiply by a short signal duration constant
74/// to calculate durations of all signals in a letter or message.
75///
76/// This makes it easier to write code that plays audio
77/// signals with lenghts of these durations or create visual
78/// representations of morse code.
79pub type SDMArray = [SDM; SDM_LENGTH];
80
81pub struct Encoder<const MSG_MAX: usize> {
82 // User defined
83 message: Message<MSG_MAX>,
84 character_set: CharacterSet,
85 morse_code_set: MorseCodeSet,
86 // Internal stuff
87 encoded_message: [MorseCodeArray; MSG_MAX],
88}
89
90impl<const MSG_MAX: usize> Default for Encoder<MSG_MAX> {
91 fn default() -> Self {
92 Self::new()
93 }
94}
95
96impl<const MSG_MAX: usize> Encoder<MSG_MAX> {
97 pub fn new() -> Self {
98 Self {
99 message: Message::default(),
100 character_set: DEFAULT_CHARACTER_SET,
101 morse_code_set: DEFAULT_MORSE_CODE_SET,
102 encoded_message: [MORSE_DEFAULT_CHAR; MSG_MAX],
103 }
104 }
105
106 /// Build encoder with a starting message.
107 ///
108 /// edit_pos_end means we'll continue encoding from the end of this string.
109 /// If you pass false to it, we'll start from the beginning.
110 pub fn with_message(mut self, message_str: &str, edit_pos_end: bool) -> Self {
111 self.message = Message::new(message_str, edit_pos_end, self.message.is_edit_clamped());
112
113 self
114 }
115
116 /// Build encoder with an arbitrary editing start position.
117 ///
118 /// Maybe client code saved the previous editing position to an EEPROM, harddisk, local
119 /// storage in web and wants to continue from that.
120 pub fn with_edit_position(mut self, pos: usize) -> Self {
121 self.message.set_edit_pos(pos);
122
123 self
124 }
125
126 /// Use a different character set than default english alphabet.
127 ///
128 /// This can be helpful to create a message with trivial encryption.
129 /// Letters can be shuffled for example. With utf-8 feature flag, a somewhat
130 /// stronger encryption can be used. These kind of encryptions can
131 /// easily be broken with powerful algorithms and AI.
132 /// **DON'T** use it for secure communication.
133 pub fn with_character_set(mut self, character_set: CharacterSet) -> Self {
134 self.character_set = character_set;
135
136 self
137 }
138
139 /// Use a different morse code set than the default.
140 ///
141 /// It's mainly useful for a custom morse code set with utf8
142 /// character sets. Different alphabets have different corresponding morse code sets.
143 pub fn with_morse_code_set(mut self, morse_code_set: MorseCodeSet) -> Self {
144 self.morse_code_set = morse_code_set;
145
146 self
147 }
148
149 /// Change the wrapping behaviour of message position to clamping.
150 ///
151 /// This will prevent the position cycling back to 0 when overflows or
152 /// jumping forward to max when falls below 0. Effectively limiting the position
153 /// to move within the message length from 0 to message length maximum without jumps.
154 ///
155 /// If at one point you want to change it back to wrapping again:
156 ///
157 /// ```ignore
158 /// encoder.message.set_edit_position_clamp(false);
159 /// ```
160 pub fn with_message_pos_clamping(mut self) -> Self {
161 self.message.set_edit_position_clamp(true);
162
163 self
164 }
165
166 /// Build and get yourself a shiny new [MorseEncoder].
167 ///
168 /// The ring is yours now...
169 pub fn build(self) -> MorseEncoder<MSG_MAX> {
170 let Encoder {
171 message,
172 character_set,
173 morse_code_set,
174 encoded_message,
175 } = self;
176
177 MorseEncoder::<MSG_MAX> {
178 message,
179 character_set,
180 morse_code_set,
181 encoded_message,
182 }
183 }
184}
185
186pub struct MorseEncoder<const MSG_MAX: usize> {
187 // User defined
188 pub message: Message<MSG_MAX>,
189 character_set: CharacterSet,
190 morse_code_set: MorseCodeSet,
191 // Internal stuff
192 encoded_message: [MorseCodeArray; MSG_MAX],
193}
194
195// Private internal methods
196impl<const MSG_MAX: usize> MorseEncoder<MSG_MAX> {
197 fn get_morse_char_from_char(&self, ch: &Character) -> Option<MorseCodeArray> {
198 let index = self.character_set
199 .iter()
200 .position(|setchar| setchar == ch);
201
202 if let Some(i) = index {
203 Some(self.morse_code_set[i].clone())
204 } else {
205 None
206 }
207 }
208
209 fn get_encoded_char_as_morse_charray(&self, index: usize) -> Option<MorseCharray> {
210 if index < self.message.len() {
211 let encoded_char = self.encoded_message[index].clone();
212 if encoded_char == MORSE_DEFAULT_CHAR {
213 Some([Some(WORD_DELIMITER), None, None, None, None, None])
214 } else {
215 Some(encoded_char.map(|mchar| {
216 match mchar {
217 Some(S) => Some(DIT),
218 Some(L) => Some(DAH),
219 _ => None,
220 }
221 }))
222 }
223 } else {
224 None
225 }
226 }
227
228 fn get_encoded_char_as_sdm(&self, index: usize) -> Option<SDMArray> {
229 if index < self.message.len() {
230 let mut sdm_array = [SDMEmpty; SDM_LENGTH];
231
232 let encoded_char = self.encoded_message[index].clone();
233 if encoded_char == MORSE_DEFAULT_CHAR {
234 sdm_array[0] = SDMLow(WORD_SPACE_MULTIPLIER as u8);
235 } else {
236 let mut sdm_iter = sdm_array.iter_mut();
237 let mut encoded_iter = encoded_char.iter().filter(|mchar| mchar.is_some()).peekable();
238
239 while let Some(mchar) = encoded_iter.next() {
240 *sdm_iter.next().unwrap() = match mchar {
241 Some(S) => SDMHigh(1),
242 Some(L) => SDMHigh(LONG_SIGNAL_MULTIPLIER as u8),
243 _ => SDMEmpty,
244 };
245
246 // If we have a character in the future, we put a
247 // signal space between this signal and the next.
248 if encoded_iter.peek().is_some() {
249 *sdm_iter.next().unwrap() = SDMLow(1);
250 }
251 }
252
253 // Put a character ending long signal at the end.
254 *sdm_iter.next().unwrap() = SDMLow(LONG_SIGNAL_MULTIPLIER as u8);
255 }
256
257 Some(sdm_array)
258 } else {
259 None
260 }
261 }
262
263 #[cfg(not(feature = "utf8"))]
264 fn encode(&mut self, ch: &Character, index: usize) -> Result<Character, &'static str> {
265 if ch.is_ascii() {
266 let ch_upper = ch.to_ascii_uppercase();
267 match self.get_morse_char_from_char(&ch_upper) {
268 Some(mchar) => {
269 self.encoded_message[index] = mchar;
270
271 Ok(ch_upper)
272 },
273 None => Err("Encoding error: Could not find character in character set.")
274 }
275 } else {
276 Err("Encoding error: Character is not ASCII")
277 }
278 }
279
280 #[cfg(feature = "utf8")]
281 fn encode(&mut self, ch: &Character, index: usize) -> Result<Character, &'static str> {
282 let mut ch_upper = ch.to_uppercase();
283
284 if let Some(ch) = ch_upper.next() {
285 match self.get_morse_char_from_char(&ch) {
286 Some(mchar) => {
287 self.encoded_message[index] = mchar;
288
289 Ok(ch)
290 },
291 None => Err("Encoding error: Could not find character in character set.")
292 }
293 } else {
294 Err("Encoding error: Could not convert character to uppercase.")
295 }
296 }
297}
298
299// Public API
300impl<const MSG_MAX: usize> MorseEncoder<MSG_MAX> {
301 // INPUTS
302
303 /// Encode a single character at the edit position
304 /// and add it both to the message and encoded_message.
305 pub fn encode_character(&mut self, ch: &Character) -> Result<(), &str> {
306 let pos = self.message.get_edit_pos();
307
308 if pos < MSG_MAX {
309 let ch_uppercase = self.encode(ch, pos);
310
311 match ch_uppercase {
312 Ok(ch) => {
313 self.message.add_char(ch);
314
315 // If message position is clamping then this should not do anything
316 // at the end of message position.
317 // If wrapping then it should reset the position to 0, so above condition
318 // should pass next time.
319 self.message.shift_edit_right();
320
321 Ok(())
322 },
323 Err(err) => Err(err)
324 }
325 } else {
326 Ok(())
327 }
328 }
329
330 /// Encode a &str slice at the edit position
331 /// and add it both to the message and encoded message.
332 ///
333 /// Note if the slice exceeds maximum message length it will return an error.
334 /// Non-ASCII characters will be ignored.
335 #[cfg(not(feature = "utf8"))]
336 pub fn encode_slice(&mut self, str_slice: &str) -> Result<(), &str> {
337 let ascii_count = str_slice.chars().filter(|ch| ch.is_ascii()).count();
338
339 if self.message.len() + ascii_count < MSG_MAX {
340 str_slice.chars()
341 .filter(|ch| ch.is_ascii())
342 .for_each(|ch| {
343 let byte = ch as u8;
344 self.encode_character(&byte).unwrap();
345 });
346
347 Ok(())
348 } else {
349 Err("String slice length exceeds maximum message length.")
350 }
351 }
352
353 #[cfg(feature = "utf8")]
354 pub fn encode_slice(&mut self, str_slice: &str) -> Result<(), &str> {
355 if self.message.len() + str_slice.len() < MSG_MAX {
356 str_slice.chars()
357 .for_each(|ch| {
358 self.encode_character(&ch).unwrap();
359 });
360
361 Ok(())
362 } else {
363 Err("String slice length exceeds maximum message length.")
364 }
365 }
366
367 /// Encode the entire message from start to finish
368 /// and save it to encoded_message.
369 pub fn encode_message_all(&mut self) {
370 for index in 0..self.message.len() {
371 let ch = &self.message.char_at(index).clone();
372
373 self.encode(ch, index).unwrap();
374 }
375 }
376
377 // OUTPUTS
378 /// Get last encoded message character as `Option<Character>` arrays of morse code.
379 ///
380 /// Arrays will have a fixed length of `MORSE_ARRAY_LENGTH` and if there's no
381 /// signal the option will be None.
382 pub fn get_last_char_as_morse_charray(&self) -> Option<MorseCharray> {
383 let pos = self.message.get_last_changed_index();
384 self.get_encoded_char_as_morse_charray(pos)
385 }
386
387 /// Get last encoded message character as `Option<SDM>` arrays of morse code.
388 ///
389 /// The multiplier values then can be used to calculate durations of individual
390 /// signals to play or animate the morse code.
391 /// It'll be great to filter-out `Empty` values of SDM arrays.
392 pub fn get_last_char_as_sdm(&self) -> Option<SDMArray> {
393 let pos = self.message.get_last_changed_index();
394 self.get_encoded_char_as_sdm(pos)
395 }
396
397 /// Get an iterator to encoded message as `Option<Character>` arrays of morse code.
398 /// Arrays will have a fixed length of `MORSE_ARRAY_LENGTH` and if there's no
399 /// signal the option will be `None`. So it will be good to filter them out.
400 pub fn get_encoded_message_as_morse_charrays(&self) -> impl Iterator<Item = Option<MorseCharray>> + '_ {
401 (0..self.message.len()).map(|index| {
402 self.get_encoded_char_as_morse_charray(index)
403 })
404 }
405
406 /// Get an iterator to entire encoded message as `Option<SDM>` arrays of morse code.
407 /// The multiplier values then can be used to calculate durations of individual
408 /// signals to play or animate the morse code.
409 /// It'll be good to filter `Empty` values that might fill the arrays at the end.
410 pub fn get_encoded_message_as_sdm_arrays(&self) -> impl Iterator<Item = Option<SDMArray>> + '_ {
411 (0..self.message.len()).map(|index| {
412 self.get_encoded_char_as_sdm(index)
413 })
414 }
415}