Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

History

1 Commit

Repository files navigation

Morsel

A lightweight C library for decoding Morse code keyed on a microcontroller button input. A companion to misclick — same timer-agnostic, callback-based architecture, same event-feed API.

Features

  • Straight-key decoding - Feed raw key up/down edges, get decoded characters
  • Hardware debouncing - Filters out electrical noise from key contacts
  • Full code table - Letters, digits, and standard punctuation (53 characters)
  • Element events - Optional per-dot/dash/gap callbacks (e.g. for a sidetone or echo display)
  • Encoder included - Character to ".-" string lookup for playback
  • Configurable timing - Keying speed set by a single unit time
  • Callback-based - Non-blocking event-driven architecture
  • Memory efficient - Low memory footprint
  • Timer agnostic - Works with any timer implementation

How decoding works

Standard Morse timing, relative to the unit (dot) time:

ElementNominalClassified as
Dotkey down 1 unitdown < 2 units
Dashkey down 3 unitsdown ≥ 2 units
Intra-character gapkey up 1 unitup < 2 units
Letter gapkey up 3 unitsup ≥ 2 units → character emitted
Word gapkey up 7 unitsup ≥ 5 units → ' ' emitted

The thresholds sit between the nominal values so hand keying has slack both ways. The default unit time of 100 ms corresponds to ≈12 WPM; lower it as your fist improves.

Decoded characters arrive on the character callback as a plain text stream: uppercase letters, digits, punctuation, ' ' for word gaps, and 0 for an element sequence that matches no known code.

Usage

Integration with CMake

Add this to your CMakeLists.txt to automatically download and build the library:

include(FetchContent)
FetchContent_Declare(
morsel
GIT_REPOSITORY https://github.com/cyborgize/morsel.git
GIT_TAG main # or a specific version tag like v1.0.0
)
FetchContent_MakeAvailable(morsel)
# Link to your targettarget_link_libraries(your_targetPRIVATEmorsel)

Implementation Example

This example shows integration with Zephyr RTOS, but the same patterns apply to other platforms (it is intentionally identical to the misclick integration — the two libraries can share their GPIO plumbing).

1. Timer Implementation

#include<zephyr/kernel.h>#include<zephyr/drivers/gpio.h>#include"morsel/morsel.h"// Zephyr timers for the morsel librarystaticstructk_timermorsel_state_timer;
staticstructk_timermorsel_gap_timer;
// Timer callback handlersstaticvoidmorsel_state_timer_handler(structk_timer*timer) {
morsel_handle_state_timeout(k_uptime_get() *1000); // Convert ms to us
}
staticvoidmorsel_gap_timer_handler(structk_timer*timer) {
morsel_handle_gap_timeout(k_uptime_get() *1000); // Convert ms to us
}
// Timer interface functions for morsel librarystaticvoidmorsel_stop_timer(void*handle) {
structk_timer*timer= (structk_timer*)handle;
k_timer_stop(timer);
}
staticvoidmorsel_start_timer(void*handle, int64_ttimeout_us) {
structk_timer*timer= (structk_timer*)handle;
k_timer_start(timer, K_USEC(timeout_us), K_NO_WAIT);
}

2. Decode Callbacks

// Decoded text stream: letters/digits/punctuation, ' ' on word gaps,// 0 for an unrecognized element sequencestaticvoidkey_char_callback(void*callback_arg, intkey_id,
charc, int64_ttimestamp) {
if (c==0) {
printk("?"); // bad sequence
} else {
printk("%c", c);
}
}
// Optional: individual element events, e.g. for a sidetone or echo displaystaticvoidkey_element_callback(void*callback_arg, intkey_id,
enummorsel_element_telement, int64_ttimestamp) {
switch (element) {
caseMORSEL_KEY_DOWN: /* sidetone on */break;
caseMORSEL_KEY_UP: /* sidetone off */break;
caseMORSEL_DOT: printk("."); break;
caseMORSEL_DASH: printk("-"); break;
caseMORSEL_LETTER_END: printk(" "); break;
caseMORSEL_WORD_END: printk(" / "); break;
}
}

3. Library Initialization

staticstructmorsel_t*morse_key=NULL;
staticvoidinit_morse_key(void) {
// Initialize timersk_timer_init(&morsel_state_timer, morsel_state_timer_handler, NULL);
k_timer_init(&morsel_gap_timer, morsel_gap_timer_handler, NULL);
// Configure the morsel libraryint64_tunit_us=DEFAULT_MORSEL_UNIT_TIME_US; // 100 ms/unit ≈ 12 WPMstructmorsel_config_tconfig= {
.state_timer_handle=&morsel_state_timer,
.gap_timer_handle=&morsel_gap_timer,
.stop_timer=morsel_stop_timer,
.start_timer=morsel_start_timer,
.debounce_time_us=DEFAULT_MORSEL_DEBOUNCE_TIME_US,
.dash_time_us=MORSEL_DASH_TIME_US(unit_us),
.letter_gap_time_us=MORSEL_LETTER_GAP_TIME_US(unit_us),
.word_gap_time_us=MORSEL_WORD_GAP_TIME_US(unit_us),
};
morsel_init(&config);
// Add the keystructmorsel_params_tkey_params= {
.key_id=0,
.callback_arg=NULL,
.char_callback=key_char_callback,
.element_callback=key_element_callback,
};
morse_key=morsel_add(&key_params);
}

4. GPIO Interrupt Handler

// GPIO interrupt callback (called from ISR context)staticvoidkey_gpio_callback(conststructdevice*dev,
structgpio_callback*cb, uint32_tpins) {
if (!morse_key) {
return;
}
// Read current key state (inverted since the key is active low);// sample semantics match misclick: 0 = down, non-zero = upintkey_state= !gpio_pin_get_dt(&key_gpio);
// Send key event to morsel library// Convert milliseconds to microseconds for timestampmorsel_handle_input_event(morse_key, key_state, k_uptime_get() *1000);
}

Encoding

The code table is also exposed directly (no morsel_init required):

charseq[8];
intn=morsel_encode('R', seq, sizeof(seq)); // seq = ".-.", n = 3charc=morsel_decode("-.-"); // c = 'K', 0 if unrecognized

Playback timing is the caller's job: 1 unit on per dot, 3 on per dash, 1 off between elements, 3 off between letters, 7 off between words.

Configuration

Default Timing Values

#defineDEFAULT_MORSEL_DEBOUNCE_TIME_US 5000 // 5ms debounce
#defineDEFAULT_MORSEL_UNIT_TIME_US 100000 // 100ms unit ≈ 12 WPM
#defineMORSEL_DASH_TIME_US(unit_us) (2 * (unit_us))
#defineMORSEL_LETTER_GAP_TIME_US(unit_us) (2 * (unit_us))
#defineMORSEL_WORD_GAP_TIME_US(unit_us) (5 * (unit_us))

Per-key debounce override via morsel_params_t.debounce_time_us: 0 inherits the global value, a positive value overrides it, a negative value disables software debounce (for inputs already debounced in hardware).

Timer Integration

The library requires two timers with microsecond precision:

  • State timer: Used for input debouncing
  • Gap timer: Used for letter/word gap detection

Requirements:

  • Create two separate timer instances (for state and gap timers)
  • Configure timers for one-shot mode (fire once, then stop)
  • Implement microsecond-precision timing
  • Call the library timeout handlers from your timer interrupt callbacks

Testing

Host-side tests (simulated timers, hand-keyed input) run with CTest:

cmake -B build && cmake --build build && ctest --test-dir build

Memory Usage

  • Per key: ~60 bytes
  • Global state: ~80 bytes + a 106-byte code table in ROM
  • No dynamic allocation after initialization

Thread Safety

This library is not thread-safe. If using in a multi-threaded environment, provide your own synchronization.

License

Licensed under the Apache License, Version 2.0. See LICENSE for the full license text.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages