Implement Phase 2: real platform shims + behavior driver + steno key defines

- 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).
This commit is contained in:
afiqzudinhadi 2026-06-22 21:02:14 +08:00
parent 206e5231e1
commit d4dc836b52
9 changed files with 213 additions and 73 deletions

View file

@ -14,7 +14,6 @@ if(CONFIG_ZMK_JAVELIN_STENO)
javelin/bit.cc javelin/bit.cc
javelin/button_script_manager.cc javelin/button_script_manager.cc
javelin/button_script.cc javelin/button_script.cc
javelin/clock.cc
javelin/console_input_buffer.cc javelin/console_input_buffer.cc
javelin/console.cc javelin/console.cc
javelin/container/cyclic_queue.cc javelin/container/cyclic_queue.cc
@ -31,7 +30,6 @@ if(CONFIG_ZMK_JAVELIN_STENO)
javelin/host_layout.cc javelin/host_layout.cc
javelin/key_code.cc javelin/key_code.cc
javelin/key_press_parser.cc javelin/key_press_parser.cc
javelin/key.cc
javelin/mem.cc javelin/mem.cc
javelin/orthography.cc javelin/orthography.cc
javelin/pattern_component.cc javelin/pattern_component.cc
@ -55,7 +53,6 @@ if(CONFIG_ZMK_JAVELIN_STENO)
javelin/stroke_list_parser.cc javelin/stroke_list_parser.cc
javelin/stroke.cc javelin/stroke.cc
javelin/system.cc javelin/system.cc
javelin/thread.cc
javelin/timer_manager.cc javelin/timer_manager.cc
javelin/unicode_script.cc javelin/unicode_script.cc
javelin/unicode.cc javelin/unicode.cc
@ -182,13 +179,9 @@ if(CONFIG_ZMK_JAVELIN_STENO)
${ZMK_INIT_SRC} ${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 set_source_files_properties(${ALL_CXX_SRC} PROPERTIES
COMPILE_FLAGS "-std=gnu++23 -fno-exceptions -fno-rtti -fno-threadsafe-statics" 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" COMPILE_DEFINITIONS "JAVELIN_USE_EMBEDDED_STENO=1;JAVELIN_USE_USER_DICTIONARY=1;JAVELIN_PLATFORM_ZEPHYR=1"
) )

View file

@ -0,0 +1,8 @@
/ {
behaviors {
/omit-if-no-ref/ javsteno: javelin_steno {
compatible = "zmk,behavior-javelin-steno";
#binding-cells = <1>;
};
};
};

View file

@ -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

View file

@ -9,4 +9,12 @@ void zmk_javelin_steno_process_key(int steno_key_index, bool is_press);
#ifdef __cplusplus #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 #endif

View file

@ -1,30 +1,26 @@
#define DT_DRV_COMPAT zmk_behavior_javelin_steno
#include <zephyr/device.h> #include <zephyr/device.h>
#include <zephyr/kernel.h> #include <zephyr/kernel.h>
#include <zephyr/logging/log.h> #include <zephyr/logging/log.h>
#include <drivers/behavior.h> #include <drivers/behavior.h>
#include <zmk/behavior.h> #include <zmk/behavior.h>
#include <zmk/keymap.h>
#include "zmk_javelin_steno/zmk_platform_shim.h" #include "zmk_javelin_steno/zmk_platform_shim.h"
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); 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, static int on_keymap_binding_pressed(struct zmk_behavior_binding *binding,
struct zmk_behavior_binding_event event) { struct zmk_behavior_binding_event event) {
int steno_key = binding->param1; zmk_javelin_steno_process_key((int)binding->param1, true);
zmk_javelin_steno_process_key(steno_key, true); return ZMK_BEHAVIOR_OPAQUE;
return 0;
} }
static int on_keymap_binding_released(struct zmk_behavior_binding *binding, static int on_keymap_binding_released(struct zmk_behavior_binding *binding,
struct zmk_behavior_binding_event event) { struct zmk_behavior_binding_event event) {
int steno_key = binding->param1; zmk_javelin_steno_process_key((int)binding->param1, false);
zmk_javelin_steno_process_key(steno_key, false); return ZMK_BEHAVIOR_OPAQUE;
return 0;
} }
static int behavior_javelin_steno_init(const struct device *dev) { 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 = { static const struct behavior_driver_api behavior_javelin_steno_driver_api = {
.locality = BEHAVIOR_LOCALITY_CENTRAL,
.binding_pressed = on_keymap_binding_pressed, .binding_pressed = on_keymap_binding_pressed,
.binding_released = on_keymap_binding_released, .binding_released = on_keymap_binding_released,
.locality = BEHAVIOR_LOCALITY_CENTRAL,
}; };
static struct behavior_javelin_steno_data behavior_javelin_steno_data; #define JAVSTENO_INST(n) \
static struct behavior_javelin_steno_config behavior_javelin_steno_config; 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, DT_INST_FOREACH_STATUS_OKAY(JAVSTENO_INST)
&behavior_javelin_steno_data,
&behavior_javelin_steno_config,
POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT,
&behavior_javelin_steno_driver_api);

View file

@ -1,38 +1,81 @@
#include "zmk_javelin_steno/zmk_platform_shim.h" #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 "engine.h"
#include "orthography.h"
#include "processor/all_up.h" #include "processor/all_up.h"
#include "processor/first_up.h"
#include "processor/jeff_modifiers.h"
#include "processor/processor.h" #include "processor/processor.h"
#include "processor/repeat.h"
#include "steno_key_state.h" #include "steno_key_state.h"
#include "static_allocate.h"
#include "stroke.h"
// Static storage for the processor pipeline. // Pipeline: StenoProcessor → StenoRepeat → StenoAllUp → StenoJeffModifiers → StenoEngine
// Pipeline: StenoProcessor → StenoAllUp → StenoEngine //
static StenoAllUp *allUp = nullptr; // 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<StenoCompiledOrthography> compiledOrthography;
static JavelinStaticAllocate<StenoJeffModifiers> jeffModifiers;
static JavelinStaticAllocate<StenoRepeat> repeat;
static JavelinStaticAllocate<StenoAllUp> allUp;
static StenoProcessor *processor = nullptr; static StenoProcessor *processor = nullptr;
static bool initialized = false;
extern "C" { extern "C" {
void zmk_javelin_steno_init(void) { void zmk_javelin_steno_init(void) {
// TODO Phase 2: Initialize with real dictionary and orthography. if (initialized) {
// For now this is a skeleton — engine construction requires a return;
// StenoDictionaryCollection loaded from flash (Phase 3). }
// 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: // For now, engine construction is deferred until dictionary is available.
// 1. Read dictionary collection from flash partition // The processor pipeline is NOT built yet — process_key will early-return.
// 2. Validate magic (0x3443534a = 'JSC4')
// 3. Build dictionary list from collection initialized = true;
// 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) { void zmk_javelin_steno_process_key(int steno_key_index, bool is_press) {
if (!processor) { if (!processor) {
return; return;
} }
StenoKey key = static_cast<StenoKey>(steno_key_index); processor->Process(static_cast<StenoKey>(steno_key_index), is_press);
processor->Process(key, is_press);
} }
} // extern "C" } // 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;
}

View file

@ -1,19 +1,13 @@
#include "clock.h" #include "clock.h"
// TODO Phase 2: Replace with Zephyr kernel time functions. extern "C" {
// #include <zephyr/kernel.h> #include <zephyr/kernel.h>
// 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::GetMilliseconds() { return k_uptime_get_32(); }
uint32_t Clock::GetMicroseconds() { uint32_t Clock::GetMicroseconds() {
return 0; return k_cyc_to_us_floor32(k_cycle_get_32());
} }
void Clock::Sleep(uint32_t milliseconds) { void Clock::Sleep(uint32_t milliseconds) { k_msleep(milliseconds); }
(void)milliseconds;
}

View file

@ -1,19 +1,46 @@
#include "flash.h" #include "flash.h"
#include "mem.h"
// TODO Phase 3: Replace with Zephyr flash API. extern "C" {
// #include <zephyr/drivers/flash.h> #include <zephyr/device.h>
// #include <zephyr/storage/flash_map.h> #include <zephyr/drivers/flash.h>
#include <zephyr/storage/flash_map.h>
}
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 Flash::EraseBlockInternal(const void *target, size_t size) {
(void)target; const struct device *dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_flash));
(void)size; if (!device_is_ready(dev)) {
return;
}
flash_erase(dev, xip_to_offset(target), size);
} }
void Flash::WriteBlockInternal(const void *target, const void *data, void Flash::WriteBlockInternal(const void *target, const void *data,
size_t size) { size_t size) {
(void)target; const struct device *dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_flash));
(void)data; if (!device_is_ready(dev)) {
(void)size; return;
}
flash_write(dev, xip_to_offset(target), data, size);
} }

View file

@ -1,20 +1,51 @@
#include "key.h" #include "key.h"
#include "key_code.h" #include "key_code.h"
// TODO Phase 2: Replace stubs with ZMK HID event emission. extern "C" {
// Key::Press/Release are called by Javelin's output pipeline to send #include <zephyr/kernel.h>
// translated text to the host. These need to map to #include <zmk/event_manager.h>
// raise_zmk_keycode_state_changed() with correct usage pages. #include <zmk/events/keycode_state_changed.h>
}
// 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; bool Key::historyEnabled = false;
void Key::Press(KeyCode key) { void Key::Press(KeyCode key) { emit_keycode(key, true); }
(void)key;
}
void Key::Release(KeyCode key) { void Key::Release(KeyCode key) { emit_keycode(key, false); }
(void)key;
}
void Key::Flush() { void Key::Flush() {
// ZMK processes events asynchronously via its event queue.
// Flush is a no-op — events are dispatched as they're raised.
} }