Skip to content

cyborgize/morsel

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

Watchers

Forks

Releases

Packages

Contributors

Languages