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