From d4dc836b524be2f6fa89fa8b77d68cf633681435 Mon Sep 17 00:00:00 2001 From: afiqzudinhadi Date: Mon, 22 Jun 2026 21:02:14 +0800 Subject: [PATCH] Implement Phase 2: real platform shims + behavior driver + steno key defines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - zmk_key_shim: Key::Press/Release → raise_zmk_keycode_state_changed() Maps Javelin KeyCode to ZMK HID events (keyboard + consumer pages) - zmk_clock_shim: Clock:: → Zephyr k_uptime_get/k_cyc_to_us/k_msleep - zmk_flash_shim: Flash::EraseBlock/WriteBlock → Zephyr flash API - behavior_javelin_steno.c: proper DT_DRV_COMPAT, DT_INST_FOREACH_STATUS_OKAY - engine_init.cc: processor pipeline wiring (AllUp → JeffModifiers → Engine) - javelin_steno.dtsi: default &javsteno behavior instance - dt-bindings/zmk/javelin_steno.h: STENO_S1..STENO_ZR key defines for keymaps - Exclude thread.cc (pthreads), key.cc, clock.cc (replaced by shims) - Fix CMake: combine COMPILE_FLAGS + COMPILE_DEFINITIONS in single call Engine init deferred until dictionary loaded (Phase 3). --- CMakeLists.txt | 9 +-- dts/behaviors/javelin_steno.dtsi | 8 ++ include/dt-bindings/zmk/javelin_steno.h | 41 ++++++++++ include/zmk_javelin_steno/zmk_platform_shim.h | 8 ++ src/behaviors/behavior_javelin_steno.c | 31 ++++---- src/engine_init.cc | 75 +++++++++++++++---- src/platform/zmk_clock_shim.cc | 18 ++--- src/platform/zmk_flash_shim.cc | 45 ++++++++--- src/platform/zmk_key_shim.cc | 51 ++++++++++--- 9 files changed, 213 insertions(+), 73 deletions(-) create mode 100644 dts/behaviors/javelin_steno.dtsi create mode 100644 include/dt-bindings/zmk/javelin_steno.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 0f103d4..73004a2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,7 +14,6 @@ if(CONFIG_ZMK_JAVELIN_STENO) 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 @@ -31,7 +30,6 @@ if(CONFIG_ZMK_JAVELIN_STENO) 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 @@ -55,7 +53,6 @@ if(CONFIG_ZMK_JAVELIN_STENO) 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 @@ -182,13 +179,9 @@ if(CONFIG_ZMK_JAVELIN_STENO) ${ZMK_INIT_SRC} ) - # Set C++ compile flags for all Javelin + shim sources + # C++ flags + Javelin compile definitions 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" ) diff --git a/dts/behaviors/javelin_steno.dtsi b/dts/behaviors/javelin_steno.dtsi new file mode 100644 index 0000000..e017a68 --- /dev/null +++ b/dts/behaviors/javelin_steno.dtsi @@ -0,0 +1,8 @@ +/ { + behaviors { + /omit-if-no-ref/ javsteno: javelin_steno { + compatible = "zmk,behavior-javelin-steno"; + #binding-cells = <1>; + }; + }; +}; diff --git a/include/dt-bindings/zmk/javelin_steno.h b/include/dt-bindings/zmk/javelin_steno.h new file mode 100644 index 0000000..0583d7d --- /dev/null +++ b/include/dt-bindings/zmk/javelin_steno.h @@ -0,0 +1,41 @@ +// Steno key indices matching Javelin's StenoKey enum. +// Use in keymap: &javsteno STENO_S1, &javsteno STENO_TL, etc. + +#define STENO_S1 0 +#define STENO_S2 1 +#define STENO_TL 2 +#define STENO_KL 3 +#define STENO_PL 4 +#define STENO_WL 5 +#define STENO_HL 6 +#define STENO_RL 7 +#define STENO_A 8 +#define STENO_O 9 +#define STENO_STAR1 10 +#define STENO_STAR2 11 +#define STENO_STAR3 12 +#define STENO_STAR4 13 +#define STENO_E 14 +#define STENO_U 15 +#define STENO_FR 16 +#define STENO_RR 17 +#define STENO_PR 18 +#define STENO_BR 19 +#define STENO_LR 20 +#define STENO_GR 21 +#define STENO_TR 22 +#define STENO_SR 23 +#define STENO_DR 24 +#define STENO_ZR 25 +#define STENO_NUM1 26 +#define STENO_NUM2 27 +#define STENO_NUM3 28 +#define STENO_NUM4 29 +#define STENO_NUM5 30 +#define STENO_NUM6 31 +#define STENO_NUM7 32 +#define STENO_NUM8 33 +#define STENO_NUM9 34 +#define STENO_NUM10 35 +#define STENO_NUM11 36 +#define STENO_NUM12 37 diff --git a/include/zmk_javelin_steno/zmk_platform_shim.h b/include/zmk_javelin_steno/zmk_platform_shim.h index 7c2aecc..b3adb79 100644 --- a/include/zmk_javelin_steno/zmk_platform_shim.h +++ b/include/zmk_javelin_steno/zmk_platform_shim.h @@ -9,4 +9,12 @@ void zmk_javelin_steno_process_key(int steno_key_index, bool is_press); #ifdef __cplusplus } + +class StenoDictionary; +struct StenoOrthography; + +// Complete engine initialization with loaded dictionary. +// Called from dictionary loading code once flash data is validated. +void zmk_javelin_steno_init_engine(StenoDictionary &dictionary, + const StenoOrthography &orthography); #endif diff --git a/src/behaviors/behavior_javelin_steno.c b/src/behaviors/behavior_javelin_steno.c index 1a18287..b319422 100644 --- a/src/behaviors/behavior_javelin_steno.c +++ b/src/behaviors/behavior_javelin_steno.c @@ -1,30 +1,26 @@ +#define DT_DRV_COMPAT zmk_behavior_javelin_steno + #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; + zmk_javelin_steno_process_key((int)binding->param1, true); + return ZMK_BEHAVIOR_OPAQUE; } 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; + zmk_javelin_steno_process_key((int)binding->param1, false); + return ZMK_BEHAVIOR_OPAQUE; } static int behavior_javelin_steno_init(const struct device *dev) { @@ -33,16 +29,15 @@ static int behavior_javelin_steno_init(const struct device *dev) { } static const struct behavior_driver_api behavior_javelin_steno_driver_api = { + .locality = BEHAVIOR_LOCALITY_CENTRAL, .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; +#define JAVSTENO_INST(n) \ + BEHAVIOR_DT_INST_DEFINE(n, behavior_javelin_steno_init, NULL, \ + NULL, NULL, POST_KERNEL, \ + CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, \ + &behavior_javelin_steno_driver_api); -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); +DT_INST_FOREACH_STATUS_OKAY(JAVSTENO_INST) diff --git a/src/engine_init.cc b/src/engine_init.cc index 362c058..82c8db0 100644 --- a/src/engine_init.cc +++ b/src/engine_init.cc @@ -1,38 +1,81 @@ #include "zmk_javelin_steno/zmk_platform_shim.h" +#include "dictionary/dictionary_definition.h" +#include "dictionary/dictionary_list.h" +#include "dictionary/invalid_dictionary.h" #include "engine.h" +#include "orthography.h" #include "processor/all_up.h" +#include "processor/first_up.h" +#include "processor/jeff_modifiers.h" #include "processor/processor.h" +#include "processor/repeat.h" #include "steno_key_state.h" +#include "static_allocate.h" +#include "stroke.h" -// Static storage for the processor pipeline. -// Pipeline: StenoProcessor → StenoAllUp → StenoEngine -static StenoAllUp *allUp = nullptr; +// Pipeline: StenoProcessor → StenoRepeat → StenoAllUp → StenoJeffModifiers → StenoEngine +// +// StenoProcessor converts raw StenoKey press/release → StenoKeyState. +// StenoRepeat handles stroke repetition on held keys. +// StenoAllUp triggers when all keys released (standard steno behavior). +// StenoJeffModifiers handles modifier key combos within steno. +// StenoEngine does dictionary lookup → text output via Key::Press/Release. + +static JavelinStaticAllocate compiledOrthography; +static JavelinStaticAllocate jeffModifiers; +static JavelinStaticAllocate repeat; +static JavelinStaticAllocate allUp; static StenoProcessor *processor = nullptr; +static bool initialized = false; + 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). + if (initialized) { + return; + } + + // Use empty orthography until a dictionary collection is loaded (Phase 3). + // The empty orthography has no rules — suffix folding won't work, but + // basic dictionary lookups will function once a dict is in flash. + new (compiledOrthography) + StenoCompiledOrthography(StenoOrthography::emptyOrthography); + + // TODO Phase 3: Load StenoDictionaryCollection from flash partition. + // 1. Get pointer to steno_dict_partition start address + // 2. Validate magic == 0x3443534a ('JSC4') + // 3. Call collection->AddDictionariesToList() to populate dict list + // 4. Construct engine with real dictionary // - // 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 + // For now, engine construction is deferred until dictionary is available. + // The processor pipeline is NOT built yet — process_key will early-return. + + initialized = true; } 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); + processor->Process(static_cast(steno_key_index), is_press); } } // extern "C" + +// Called once dictionary is loaded (Phase 3) to complete initialization. +void zmk_javelin_steno_init_engine(StenoDictionary &dictionary, + const StenoOrthography &orthography) { + new (compiledOrthography) StenoCompiledOrthography(orthography); + + new (StenoEngine::container) + StenoEngine(dictionary, nullptr, compiledOrthography.value); + + new (jeffModifiers) StenoJeffModifiers(StenoEngine::container.value); + new (allUp) StenoAllUp(jeffModifiers.value); + new (repeat) StenoRepeat(allUp.value); + + static StenoProcessor processorInstance(repeat.value); + processor = &processorInstance; +} diff --git a/src/platform/zmk_clock_shim.cc b/src/platform/zmk_clock_shim.cc index ca2203e..c4a30c5 100644 --- a/src/platform/zmk_clock_shim.cc +++ b/src/platform/zmk_clock_shim.cc @@ -1,19 +1,13 @@ #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; +extern "C" { +#include } +uint32_t Clock::GetMilliseconds() { return k_uptime_get_32(); } + uint32_t Clock::GetMicroseconds() { - return 0; + return k_cyc_to_us_floor32(k_cycle_get_32()); } -void Clock::Sleep(uint32_t milliseconds) { - (void)milliseconds; -} +void Clock::Sleep(uint32_t milliseconds) { k_msleep(milliseconds); } diff --git a/src/platform/zmk_flash_shim.cc b/src/platform/zmk_flash_shim.cc index 316283a..6860b0d 100644 --- a/src/platform/zmk_flash_shim.cc +++ b/src/platform/zmk_flash_shim.cc @@ -1,19 +1,46 @@ #include "flash.h" +#include "mem.h" -// TODO Phase 3: Replace with Zephyr flash API. -// #include -// #include +extern "C" { +#include +#include +#include +} -Flash Flash::instance; +// Flash partition IDs — must match DTS overlay. +// steno_dict_partition: compiled dictionary (read-only at runtime) +// steno_user_partition: user dictionary (read-write) +// +// Javelin's Flash class uses raw pointers into XIP-mapped flash. +// On nRF52840, internal flash is memory-mapped at 0x00000000. +// We convert between XIP pointers and partition offsets. + +#ifndef FIXED_PARTITION_ID +#define FIXED_PARTITION_ID(label) FLASH_AREA_ID(label) +#endif + +// Flash::instance is defined in javelin/flash.cc (high-level methods). +// We only provide the platform-specific EraseBlockInternal/WriteBlockInternal. + +// Convert XIP address to flash device offset. +// nRF52840 internal flash is at 0x00000000, so pointer == offset. +static off_t xip_to_offset(const void *ptr) { + return (off_t)(uintptr_t)ptr; +} void Flash::EraseBlockInternal(const void *target, size_t size) { - (void)target; - (void)size; + const struct device *dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_flash)); + if (!device_is_ready(dev)) { + return; + } + flash_erase(dev, xip_to_offset(target), size); } void Flash::WriteBlockInternal(const void *target, const void *data, size_t size) { - (void)target; - (void)data; - (void)size; + const struct device *dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_flash)); + if (!device_is_ready(dev)) { + return; + } + flash_write(dev, xip_to_offset(target), data, size); } diff --git a/src/platform/zmk_key_shim.cc b/src/platform/zmk_key_shim.cc index 43bf848..79e18f7 100644 --- a/src/platform/zmk_key_shim.cc +++ b/src/platform/zmk_key_shim.cc @@ -1,20 +1,51 @@ #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. +extern "C" { +#include +#include +#include +} + +// Javelin KeyCode layout: +// 0x04-0xE7 → USB HID Keyboard page (0x07) +// 0xE0-0xE7 → Modifier keys (subset of keyboard page) +// 0x10000+ → Consumer page (0x0C), usage = value & 0xFFFF + +static constexpr uint16_t HID_USAGE_PAGE_KEYBOARD = 0x07; +static constexpr uint16_t HID_USAGE_PAGE_CONSUMER = 0x0C; + +static void emit_keycode(KeyCode key, bool pressed) { + uint32_t v = key.value; + uint16_t usage_page; + uint32_t keycode; + + if (v >= 0x10000) { + usage_page = HID_USAGE_PAGE_CONSUMER; + keycode = v & 0xFFFF; + } else { + usage_page = HID_USAGE_PAGE_KEYBOARD; + keycode = v; + } + + raise_zmk_keycode_state_changed( + (struct zmk_keycode_state_changed){ + .usage_page = usage_page, + .keycode = keycode, + .implicit_modifiers = 0, + .explicit_modifiers = 0, + .state = pressed, + .timestamp = k_uptime_get(), + }); +} bool Key::historyEnabled = false; -void Key::Press(KeyCode key) { - (void)key; -} +void Key::Press(KeyCode key) { emit_keycode(key, true); } -void Key::Release(KeyCode key) { - (void)key; -} +void Key::Release(KeyCode key) { emit_keycode(key, false); } void Key::Flush() { + // ZMK processes events asynchronously via its event queue. + // Flush is a no-op — events are dispatched as they're raised. }