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:

Element Nominal Classified as
Dot key down 1 unit down < 2 units
Dash key down 3 units down ≥ 2 units
Intra-character gap key up 1 unit up < 2 units
Letter gap key up 3 units up ≥ 2 units → character emitted
Word gap key up 7 units up ≥ 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 target
target_link_libraries(your_target PRIVATE morsel)

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 library
static struct k_timer morsel_state_timer;
static struct k_timer morsel_gap_timer;

// Timer callback handlers
static void morsel_state_timer_handler(struct k_timer *timer) {
    morsel_handle_state_timeout(k_uptime_get() * 1000); // Convert ms to us
}

static void morsel_gap_timer_handler(struct k_timer *timer) {
    morsel_handle_gap_timeout(k_uptime_get() * 1000); // Convert ms to us
}

// Timer interface functions for morsel library
static void morsel_stop_timer(void *handle) {
    struct k_timer *timer = (struct k_timer *)handle;
    k_timer_stop(timer);
}

static void morsel_start_timer(void *handle, int64_t timeout_us) {
    struct k_timer *timer = (struct k_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 sequence
static void key_char_callback(void *callback_arg, int key_id,
                              char c, int64_t timestamp) {
    if (c == 0) {
        printk("?");  // bad sequence
    } else {
        printk("%c", c);
    }
}

// Optional: individual element events, e.g. for a sidetone or echo display
static void key_element_callback(void *callback_arg, int key_id,
                                 enum morsel_element_t element, int64_t timestamp) {
    switch (element) {
        case MORSEL_KEY_DOWN:   /* sidetone on */  break;
        case MORSEL_KEY_UP:     /* sidetone off */ break;
        case MORSEL_DOT:        printk(".");       break;
        case MORSEL_DASH:       printk("-");       break;
        case MORSEL_LETTER_END: printk(" ");       break;
        case MORSEL_WORD_END:   printk(" / ");     break;
    }
}

3. Library Initialization

static struct morsel_t *morse_key = NULL;

static void init_morse_key(void) {
    // Initialize timers
    k_timer_init(&morsel_state_timer, morsel_state_timer_handler, NULL);
    k_timer_init(&morsel_gap_timer, morsel_gap_timer_handler, NULL);

    // Configure the morsel library
    int64_t unit_us = DEFAULT_MORSEL_UNIT_TIME_US; // 100 ms/unit ≈ 12 WPM
    struct morsel_config_t config = {
        .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 key
    struct morsel_params_t key_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)
static void key_gpio_callback(const struct device *dev,
                              struct gpio_callback *cb, uint32_t pins) {
    if (!morse_key) {
        return;
    }

    // Read current key state (inverted since the key is active low);
    // sample semantics match misclick: 0 = down, non-zero = up
    int key_state = !gpio_pin_get_dt(&key_gpio);

    // Send key event to morsel library
    // Convert milliseconds to microseconds for timestamp
    morsel_handle_input_event(morse_key, key_state, k_uptime_get() * 1000);
}

Encoding

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

char seq[8];
int n = morsel_encode('R', seq, sizeof(seq)); // seq = ".-.", n = 3
char c = 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

#define DEFAULT_MORSEL_DEBOUNCE_TIME_US     5000   // 5ms debounce
#define DEFAULT_MORSEL_UNIT_TIME_US       100000   // 100ms unit ≈ 12 WPM

#define MORSEL_DASH_TIME_US(unit_us)       (2 * (unit_us))
#define MORSEL_LETTER_GAP_TIME_US(unit_us) (2 * (unit_us))
#define MORSEL_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