Phase 3: dictionary loading pipeline + binary embedding

- engine_init.cc: loads StenoDictionaryCollection from embedded binary,
  validates JSC4 magic + timestamp, constructs dict list + full processor
  pipeline (Repeat → AllUp → JeffModifiers → Engine)
- dict_embed.S: embeds compiled dict binary via .incbin directive.
  Falls back to null placeholder if no dict available
- CMakeLists.txt: dict binary discovery (dicts/steno_dict.bin or
  JAVELIN_DICT_BIN cmake var), assembly embedding
- tools/dict_compiler/compile_dict.py: stroke parser + skeleton compiler.
  Full JSC4 binary format not yet implemented — for now, use Javelin
  web tool (lim.au) to generate dict binary
- dicts/: directory for compiled dictionary binaries

Dict compiler is the remaining gap — JSC4 format requires exact XIP
pointer layout matching Javelin's C++ struct casting.
This commit is contained in:
afiqzudinhadi 2026-06-22 21:26:23 +08:00
parent d4dc836b52
commit 1751171667
6 changed files with 411 additions and 46 deletions

17
src/dict_embed.S Normal file
View file

@ -0,0 +1,17 @@
/* Embed compiled steno dictionary binary into firmware.
* JAVELIN_DICT_BIN_PATH is set by CMake to the path of the compiled
* dictionary file. If not set, a 4-byte null placeholder is used.
*/
.section .rodata.steno_dict,"a",%progbits
.global _javelin_dict_start
.global _javelin_dict_end
.balign 4
_javelin_dict_start:
#ifdef JAVELIN_DICT_BIN_PATH
.incbin JAVELIN_DICT_BIN_PATH
#else
.byte 0x00, 0x00, 0x00, 0x00
#endif
_javelin_dict_end:

View file

@ -1,8 +1,9 @@
#include "zmk_javelin_steno/zmk_platform_shim.h"
#include "container/list.h"
#include "dictionary/dictionary_definition.h"
#include "dictionary/dictionary_list.h"
#include "dictionary/invalid_dictionary.h"
#include "dictionary/user_dictionary.h"
#include "engine.h"
#include "orthography.h"
#include "processor/all_up.h"
@ -14,45 +15,86 @@
#include "static_allocate.h"
#include "stroke.h"
// 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.
extern "C" {
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
}
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
// Linker symbols for the embedded dictionary binary.
// Defined by the linker script or incbin directive in dict_embed.S
extern "C" const uint8_t _javelin_dict_start[];
extern "C" const uint8_t _javelin_dict_end[];
static JavelinStaticAllocate<StenoCompiledOrthography> compiledOrthography;
static JavelinStaticAllocate<StenoDictionaryList> dictionaryList;
static JavelinStaticAllocate<StenoJeffModifiers> jeffModifiers;
static JavelinStaticAllocate<StenoRepeat> repeat;
static JavelinStaticAllocate<StenoRepeat> stenoRepeat;
static JavelinStaticAllocate<StenoAllUp> allUp;
static StenoProcessor *processor = nullptr;
static bool initialized = false;
static bool load_dictionary_collection() {
const auto *collection =
reinterpret_cast<const StenoDictionaryCollection *>(_javelin_dict_start);
if (collection->magic != STENO_MAP_DICTIONARY_COLLECTION_MAGIC) {
LOG_ERR("Steno dict magic mismatch: 0x%08x (expected 0x%08x)",
collection->magic, STENO_MAP_DICTIONARY_COLLECTION_MAGIC);
return false;
}
if (!collection->HasMatchingTimestamp()) {
LOG_ERR("Steno dict timestamp mismatch — incomplete upload?");
return false;
}
List<StenoDictionaryListEntry> entries;
collection->AddDictionariesToList(entries);
if (entries.IsEmpty()) {
LOG_ERR("Steno dict collection has no dictionaries");
return false;
}
new (dictionaryList) StenoDictionaryList(static_cast<List<StenoDictionaryListEntry>&&>(entries));
const StenoOrthography *ortho = &StenoOrthography::emptyOrthography;
// TODO: load orthography from collection if present
new (compiledOrthography) StenoCompiledOrthography(*ortho);
new (StenoEngine::container)
StenoEngine(dictionaryList.value, nullptr, compiledOrthography.value);
LOG_INF("Steno engine initialized with %d dictionaries",
collection->dictionaryCount);
return true;
}
extern "C" {
void zmk_javelin_steno_init(void) {
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
//
// For now, engine construction is deferred until dictionary is available.
// The processor pipeline is NOT built yet — process_key will early-return.
initialized = true;
if (!load_dictionary_collection()) {
LOG_WRN("Steno engine not started — no valid dictionary found");
return;
}
// Build processor pipeline: Repeat → AllUp → JeffModifiers → Engine
new (jeffModifiers) StenoJeffModifiers(StenoEngine::container.value);
new (allUp) StenoAllUp(jeffModifiers.value);
new (stenoRepeat) StenoRepeat(allUp.value);
static StenoProcessor processorInstance(stenoRepeat.value);
processor = &processorInstance;
LOG_INF("Steno processor pipeline ready");
}
void zmk_javelin_steno_process_key(int steno_key_index, bool is_press) {
@ -63,19 +105,3 @@ void zmk_javelin_steno_process_key(int steno_key_index, bool 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;
}