From 206e5231e1eba10ce2d4f1056045dd4dc08f3436 Mon Sep 17 00:00:00 2001 From: afiqzudinhadi Date: Mon, 22 Jun 2026 20:49:01 +0800 Subject: [PATCH] Initial skeleton: ZMK module structure + Javelin submodule + stub shims MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 scaffold for porting Javelin steno engine as a ZMK module. All platform shims are stubs — compiles but no steno processing yet. --- .gitmodules | 3 + CMakeLists.txt | 202 ++++++++++++++++++ Kconfig | 48 +++++ .../behaviors/zmk,behavior-javelin-steno.yaml | 11 + include/zmk_javelin_steno/zmk_platform_shim.h | 12 ++ javelin | 1 + src/behaviors/behavior_javelin_steno.c | 48 +++++ src/engine_init.cc | 38 ++++ src/platform/zmk_clock_shim.cc | 19 ++ src/platform/zmk_connection_shim.cc | 10 + src/platform/zmk_console_shim.cc | 9 + src/platform/zmk_flash_shim.cc | 19 ++ src/platform/zmk_key_shim.cc | 20 ++ zephyr/module.yml | 5 + 14 files changed, 445 insertions(+) create mode 100644 .gitmodules create mode 100644 CMakeLists.txt create mode 100644 Kconfig create mode 100644 dts/bindings/behaviors/zmk,behavior-javelin-steno.yaml create mode 100644 include/zmk_javelin_steno/zmk_platform_shim.h create mode 160000 javelin create mode 100644 src/behaviors/behavior_javelin_steno.c create mode 100644 src/engine_init.cc create mode 100644 src/platform/zmk_clock_shim.cc create mode 100644 src/platform/zmk_connection_shim.cc create mode 100644 src/platform/zmk_console_shim.cc create mode 100644 src/platform/zmk_flash_shim.cc create mode 100644 src/platform/zmk_key_shim.cc create mode 100644 zephyr/module.yml diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..3107399 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "javelin"] + path = javelin + url = https://github.com/jthlim/javelin-steno.git diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..0f103d4 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,202 @@ +if(CONFIG_ZMK_JAVELIN_STENO) + + # Only compile on central side of split keyboards (or non-split) + if((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_ROLE_CENTRAL) + + zephyr_include_directories(javelin) + zephyr_include_directories(include) + + # Core engine sources + set(JAVELIN_CORE_SRC + javelin/asset_manager.cc + javelin/base64.cc + javelin/bit_field.cc + javelin/bit.cc + javelin/button_script_manager.cc + javelin/button_script.cc + javelin/clock.cc + javelin/console_input_buffer.cc + javelin/console.cc + javelin/container/cyclic_queue.cc + javelin/container/list.cc + javelin/crc32.cc + javelin/date_time.cc + javelin/engine_add_translation_mode.cc + javelin/engine_binding.cc + javelin/engine_console_mode.cc + javelin/engine_normal_mode.cc + javelin/engine.cc + javelin/flash.cc + javelin/hash.cc + javelin/host_layout.cc + javelin/key_code.cc + javelin/key_press_parser.cc + javelin/key.cc + javelin/mem.cc + javelin/orthography.cc + javelin/pattern_component.cc + javelin/pattern.cc + javelin/pool_allocate.cc + javelin/random.cc + javelin/script_byte_code.cc + javelin/script_storage.cc + javelin/script.cc + javelin/segment_builder.cc + javelin/segment.cc + javelin/start_console_command_detector.cc + javelin/state.cc + javelin/steno_key_code_buffer_functions.cc + javelin/steno_key_code_buffer.cc + javelin/steno_key_code_emitter.cc + javelin/steno_key_code.cc + javelin/steno_key_state.cc + javelin/str.cc + javelin/stroke_history.cc + javelin/stroke_list_parser.cc + javelin/stroke.cc + javelin/system.cc + javelin/thread.cc + javelin/timer_manager.cc + javelin/unicode_script.cc + javelin/unicode.cc + javelin/utf8_pointer.cc + javelin/word_list.cc + javelin/wpm_tracker.cc + javelin/writer.cc + ) + + # Dictionary sources + set(JAVELIN_DICT_SRC + javelin/dictionary/cache_dictionary.cc + javelin/dictionary/compact_map_dictionary.cc + javelin/dictionary/debug_dictionary.cc + javelin/dictionary/dictionary_definition.cc + javelin/dictionary/dictionary_list.cc + javelin/dictionary/dictionary.cc + javelin/dictionary/emily_symbols_dictionary.cc + javelin/dictionary/full_map_dictionary.cc + javelin/dictionary/invalid_dictionary.cc + javelin/dictionary/jeff_numbers_dictionary.cc + javelin/dictionary/jeff_phrasing_dictionary_data.cc + javelin/dictionary/jeff_phrasing_dictionary.cc + javelin/dictionary/jeff_show_stroke_dictionary.cc + javelin/dictionary/orthospelling_data.cc + javelin/dictionary/orthospelling_dictionary.cc + javelin/dictionary/reverse_auto_suffix_dictionary.cc + javelin/dictionary/reverse_map_dictionary.cc + javelin/dictionary/reverse_prefix_dictionary.cc + javelin/dictionary/reverse_suffix_dictionary.cc + javelin/dictionary/unicode_dictionary.cc + javelin/dictionary/user_dictionary.cc + javelin/dictionary/wrapped_dictionary.cc + ) + + # Processor sources + set(JAVELIN_PROC_SRC + javelin/processor/all_up.cc + javelin/processor/fake_processor.cc + javelin/processor/first_up.cc + javelin/processor/gemini.cc + javelin/processor/jeff_modifiers.cc + javelin/processor/paper_tape.cc + javelin/processor/passport.cc + javelin/processor/plover_hid.cc + javelin/processor/procat.cc + javelin/processor/processor_list.cc + javelin/processor/processor.cc + javelin/processor/repeat.cc + javelin/processor/tx_bolt.cc + ) + + # HAL sources (weak defaults — our shims override the critical ones) + set(JAVELIN_HAL_SRC + javelin/hal/ble.cc + javelin/hal/bootloader.cc + javelin/hal/connection.cc + javelin/hal/display.cc + javelin/hal/gpio.cc + javelin/hal/infrared.cc + javelin/hal/midi.cc + javelin/hal/mouse.cc + javelin/hal/power.cc + javelin/hal/rgb.cc + javelin/hal/rtc.cc + javelin/hal/serial_port.cc + javelin/hal/sound.cc + javelin/hal/usb_status.cc + ) + + # Font sources (needed by display/script subsystem) + set(JAVELIN_FONT_SRC + javelin/font/monochrome/data/default.cc + javelin/font/monochrome/data/dos.cc + javelin/font/monochrome/data/huge_digits.cc + javelin/font/monochrome/data/large_digits.cc + javelin/font/monochrome/data/large.cc + javelin/font/monochrome/data/medium_digits.cc + javelin/font/monochrome/data/small_digits.cc + javelin/font/monochrome/font.cc + ) + + # Split sources (ZMK handles split transport, but Javelin split types + # are referenced by other code) + set(JAVELIN_SPLIT_SRC + javelin/split/split_console.cc + javelin/split/split_key_state.cc + javelin/split/split_midi.cc + javelin/split/split_power_override.cc + javelin/split/split_serial_buffer.cc + javelin/split/split_usb_status.cc + javelin/split/split_version.cc + javelin/split/split.cc + ) + + # Platform shims (our ZMK implementations) + set(ZMK_SHIM_SRC + src/platform/zmk_key_shim.cc + src/platform/zmk_clock_shim.cc + src/platform/zmk_flash_shim.cc + src/platform/zmk_connection_shim.cc + src/platform/zmk_console_shim.cc + ) + + # ZMK behavior driver (C, not C++) + set(ZMK_BEHAVIOR_SRC + src/behaviors/behavior_javelin_steno.c + ) + + # Engine initialization + set(ZMK_INIT_SRC + src/engine_init.cc + ) + + # Combine all C++ sources + set(ALL_CXX_SRC + ${JAVELIN_CORE_SRC} + ${JAVELIN_DICT_SRC} + ${JAVELIN_PROC_SRC} + ${JAVELIN_HAL_SRC} + ${JAVELIN_FONT_SRC} + ${JAVELIN_SPLIT_SRC} + ${ZMK_SHIM_SRC} + ${ZMK_INIT_SRC} + ) + + # Set C++ compile flags for all Javelin + shim sources + set_source_files_properties(${ALL_CXX_SRC} PROPERTIES + COMPILE_FLAGS "-std=gnu++23 -fno-exceptions -fno-rtti -fno-threadsafe-statics" + ) + + # Compile definitions for Javelin + set_source_files_properties(${ALL_CXX_SRC} PROPERTIES + COMPILE_DEFINITIONS "JAVELIN_USE_EMBEDDED_STENO=1;JAVELIN_USE_USER_DICTIONARY=1;JAVELIN_PLATFORM_ZEPHYR=1" + ) + + target_sources(app PRIVATE + ${ALL_CXX_SRC} + ${ZMK_BEHAVIOR_SRC} + ) + + endif() # central role check + +endif() # CONFIG_ZMK_JAVELIN_STENO diff --git a/Kconfig b/Kconfig new file mode 100644 index 0000000..6789932 --- /dev/null +++ b/Kconfig @@ -0,0 +1,48 @@ +menu "Javelin Steno Engine" + +config ZMK_JAVELIN_STENO + bool "Enable Javelin steno engine" + default n + select CPLUSPLUS + select LIB_CPLUSPLUS + select NEWLIB_LIBC + +config ZMK_JAVELIN_STENO_HEAP_SIZE + int "Heap size for Javelin engine (bytes)" + default 32768 + depends on ZMK_JAVELIN_STENO + +config ZMK_JAVELIN_STENO_USER_DICT + bool "Enable user dictionary (writable flash)" + default y + depends on ZMK_JAVELIN_STENO + +config ZMK_JAVELIN_STENO_USER_DICT_SIZE + int "User dictionary flash size (bytes)" + default 16384 + depends on ZMK_JAVELIN_STENO_USER_DICT + +config ZMK_JAVELIN_STENO_ORTHOGRAPHY_CACHE + bool "Enable orthography lookup cache (~32KB RAM)" + default n + depends on ZMK_JAVELIN_STENO + +config ZMK_JAVELIN_STENO_CONSOLE + bool "Enable steno console commands via USB" + default n + depends on ZMK_JAVELIN_STENO + +choice ZMK_JAVELIN_STENO_TRIGGER + prompt "Stroke trigger mode" + default ZMK_JAVELIN_STENO_TRIGGER_ALL_UP + depends on ZMK_JAVELIN_STENO + +config ZMK_JAVELIN_STENO_TRIGGER_ALL_UP + bool "All keys up (standard)" + +config ZMK_JAVELIN_STENO_TRIGGER_FIRST_UP + bool "First key up (faster, less forgiving)" + +endchoice + +endmenu diff --git a/dts/bindings/behaviors/zmk,behavior-javelin-steno.yaml b/dts/bindings/behaviors/zmk,behavior-javelin-steno.yaml new file mode 100644 index 0000000..1c4e344 --- /dev/null +++ b/dts/bindings/behaviors/zmk,behavior-javelin-steno.yaml @@ -0,0 +1,11 @@ +description: | + Javelin steno behavior — maps key positions to steno keys and feeds + them into the on-board Javelin steno engine for chord-based text input. + +compatible: "zmk,behavior-javelin-steno" + +include: one_param.yaml + +properties: + "#binding-cells": + const: 1 diff --git a/include/zmk_javelin_steno/zmk_platform_shim.h b/include/zmk_javelin_steno/zmk_platform_shim.h new file mode 100644 index 0000000..7c2aecc --- /dev/null +++ b/include/zmk_javelin_steno/zmk_platform_shim.h @@ -0,0 +1,12 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +void zmk_javelin_steno_init(void); +void zmk_javelin_steno_process_key(int steno_key_index, bool is_press); + +#ifdef __cplusplus +} +#endif diff --git a/javelin b/javelin new file mode 160000 index 0000000..2272667 --- /dev/null +++ b/javelin @@ -0,0 +1 @@ +Subproject commit 22726675b2dcf3b8b6ba1e20898f25e324b2260a diff --git a/src/behaviors/behavior_javelin_steno.c b/src/behaviors/behavior_javelin_steno.c new file mode 100644 index 0000000..1a18287 --- /dev/null +++ b/src/behaviors/behavior_javelin_steno.c @@ -0,0 +1,48 @@ +#include +#include +#include + +#include +#include +#include + +#include "zmk_javelin_steno/zmk_platform_shim.h" + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +struct behavior_javelin_steno_config {}; +struct behavior_javelin_steno_data {}; + +static int on_keymap_binding_pressed(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + int steno_key = binding->param1; + zmk_javelin_steno_process_key(steno_key, true); + return 0; +} + +static int on_keymap_binding_released(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + int steno_key = binding->param1; + zmk_javelin_steno_process_key(steno_key, false); + return 0; +} + +static int behavior_javelin_steno_init(const struct device *dev) { + zmk_javelin_steno_init(); + return 0; +} + +static const struct behavior_driver_api behavior_javelin_steno_driver_api = { + .binding_pressed = on_keymap_binding_pressed, + .binding_released = on_keymap_binding_released, + .locality = BEHAVIOR_LOCALITY_CENTRAL, +}; + +static struct behavior_javelin_steno_data behavior_javelin_steno_data; +static struct behavior_javelin_steno_config behavior_javelin_steno_config; + +BEHAVIOR_DT_INST_DEFINE(0, behavior_javelin_steno_init, NULL, + &behavior_javelin_steno_data, + &behavior_javelin_steno_config, + POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, + &behavior_javelin_steno_driver_api); diff --git a/src/engine_init.cc b/src/engine_init.cc new file mode 100644 index 0000000..362c058 --- /dev/null +++ b/src/engine_init.cc @@ -0,0 +1,38 @@ +#include "zmk_javelin_steno/zmk_platform_shim.h" + +#include "engine.h" +#include "processor/all_up.h" +#include "processor/processor.h" +#include "steno_key_state.h" + +// Static storage for the processor pipeline. +// Pipeline: StenoProcessor → StenoAllUp → StenoEngine +static StenoAllUp *allUp = nullptr; +static StenoProcessor *processor = nullptr; + +extern "C" { + +void zmk_javelin_steno_init(void) { + // TODO Phase 2: Initialize with real dictionary and orthography. + // For now this is a skeleton — engine construction requires a + // StenoDictionaryCollection loaded from flash (Phase 3). + // + // Full init will look like: + // 1. Read dictionary collection from flash partition + // 2. Validate magic (0x3443534a = 'JSC4') + // 3. Build dictionary list from collection + // 4. Load compiled orthography + // 5. Construct StenoEngine with dict + ortho + // 6. Build processor pipeline: AllUp → Engine + // 7. Wrap in StenoProcessor for key input +} + +void zmk_javelin_steno_process_key(int steno_key_index, bool is_press) { + if (!processor) { + return; + } + StenoKey key = static_cast(steno_key_index); + processor->Process(key, is_press); +} + +} // extern "C" diff --git a/src/platform/zmk_clock_shim.cc b/src/platform/zmk_clock_shim.cc new file mode 100644 index 0000000..ca2203e --- /dev/null +++ b/src/platform/zmk_clock_shim.cc @@ -0,0 +1,19 @@ +#include "clock.h" + +// TODO Phase 2: Replace with Zephyr kernel time functions. +// #include +// uint32_t Clock::GetMilliseconds() { return k_uptime_get_32(); } +// uint32_t Clock::GetMicroseconds() { return k_cyc_to_us_floor32(k_cycle_get_32()); } +// void Clock::Sleep(uint32_t ms) { k_msleep(ms); } + +uint32_t Clock::GetMilliseconds() { + return 0; +} + +uint32_t Clock::GetMicroseconds() { + return 0; +} + +void Clock::Sleep(uint32_t milliseconds) { + (void)milliseconds; +} diff --git a/src/platform/zmk_connection_shim.cc b/src/platform/zmk_connection_shim.cc new file mode 100644 index 0000000..fd52370 --- /dev/null +++ b/src/platform/zmk_connection_shim.cc @@ -0,0 +1,10 @@ +#include "hal/connection.h" +#include "hal/ble.h" +#include "hal/usb_status.h" + +// Connection::IsConnected and friends are already implemented in +// hal/connection.cc using the weak Ble:: and UsbStatus:: defaults. +// This file exists for future ZMK-specific connection status overrides. + +// TODO Phase 2: Optionally override Ble::IsConnected() with +// zmk_ble_active_profile_is_connected() for accurate BLE status. diff --git a/src/platform/zmk_console_shim.cc b/src/platform/zmk_console_shim.cc new file mode 100644 index 0000000..a109099 --- /dev/null +++ b/src/platform/zmk_console_shim.cc @@ -0,0 +1,9 @@ +#include "console.h" + +// ConsoleWriter::Write is the main output path for Javelin's console system. +// For Phase 1, stub it out. Phase 4 will route to USB CDC or ZMK logging. + +void ConsoleWriter::Write(const char *data, size_t length) { + (void)data; + (void)length; +} diff --git a/src/platform/zmk_flash_shim.cc b/src/platform/zmk_flash_shim.cc new file mode 100644 index 0000000..316283a --- /dev/null +++ b/src/platform/zmk_flash_shim.cc @@ -0,0 +1,19 @@ +#include "flash.h" + +// TODO Phase 3: Replace with Zephyr flash API. +// #include +// #include + +Flash Flash::instance; + +void Flash::EraseBlockInternal(const void *target, size_t size) { + (void)target; + (void)size; +} + +void Flash::WriteBlockInternal(const void *target, const void *data, + size_t size) { + (void)target; + (void)data; + (void)size; +} diff --git a/src/platform/zmk_key_shim.cc b/src/platform/zmk_key_shim.cc new file mode 100644 index 0000000..43bf848 --- /dev/null +++ b/src/platform/zmk_key_shim.cc @@ -0,0 +1,20 @@ +#include "key.h" +#include "key_code.h" + +// TODO Phase 2: Replace stubs with ZMK HID event emission. +// Key::Press/Release are called by Javelin's output pipeline to send +// translated text to the host. These need to map to +// raise_zmk_keycode_state_changed() with correct usage pages. + +bool Key::historyEnabled = false; + +void Key::Press(KeyCode key) { + (void)key; +} + +void Key::Release(KeyCode key) { + (void)key; +} + +void Key::Flush() { +} diff --git a/zephyr/module.yml b/zephyr/module.yml new file mode 100644 index 0000000..219b2cf --- /dev/null +++ b/zephyr/module.yml @@ -0,0 +1,5 @@ +build: + cmake: . + kconfig: Kconfig + settings: + dts_root: .