Merge split-storage: BLE GATT dict protocol + split-aware build
Combined optimize-dict (MPHF compression, block-compressed strings, formatter, undo, Unicode output) with split-storage (BLE GATT service, LRU cache, peripheral-side dict embed). Split-dict mode: central queries peripheral over BLE. Non-split mode: dict embedded on central, local lookup.
This commit is contained in:
commit
bc2e40caf4
7 changed files with 972 additions and 132 deletions
116
CMakeLists.txt
116
CMakeLists.txt
|
|
@ -2,45 +2,87 @@
|
||||||
# SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
|
# SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
|
||||||
|
|
||||||
if(CONFIG_STENO_ENGINE)
|
if(CONFIG_STENO_ENGINE)
|
||||||
if(NOT CONFIG_ZMK_SPLIT OR CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
|
|
||||||
|
|
||||||
target_sources(app PRIVATE
|
|
||||||
src/behavior_steno.c
|
|
||||||
src/output.c
|
|
||||||
src/formatter.c
|
|
||||||
src/undo.c
|
|
||||||
src/dict_embed.S
|
|
||||||
)
|
|
||||||
|
|
||||||
if(CONFIG_STENO_DICT_MPHF)
|
|
||||||
target_sources(app PRIVATE src/dict_mphf.c)
|
|
||||||
else()
|
|
||||||
target_sources(app PRIVATE src/trie.c)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
target_include_directories(app PRIVATE
|
target_include_directories(app PRIVATE
|
||||||
include
|
include
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src
|
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||||
)
|
)
|
||||||
|
|
||||||
# ── Dictionary source resolution ──
|
# ── Split-dict mode ──────────────────────────────────
|
||||||
find_package(Python3 REQUIRED COMPONENTS Interpreter)
|
# Central: behavior engine + BLE dict client
|
||||||
set(STENO_DICT_BIN ${CMAKE_CURRENT_BINARY_DIR}/steno_dict.bin)
|
# Peripheral: dict embed + lookup engine + GATT server
|
||||||
set(STENO_DICTS_DIR ${CMAKE_CURRENT_SOURCE_DIR}/dicts)
|
if(CONFIG_STENO_SPLIT_DICT)
|
||||||
set(STENO_FETCH ${CMAKE_CURRENT_SOURCE_DIR}/tools/fetch_dict.py)
|
|
||||||
|
|
||||||
if(CONFIG_STENO_DICT_PLOVER)
|
if(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
|
||||||
set(STENO_DICT_NAME "plover")
|
# Central side: behavior engine queries peripheral over BLE
|
||||||
set(STENO_DICT_SRC ${STENO_DICTS_DIR}/plover-main.json)
|
target_sources(app PRIVATE
|
||||||
elseif(CONFIG_STENO_DICT_LAPWING)
|
src/behavior_steno.c
|
||||||
set(STENO_DICT_NAME "lapwing")
|
src/output.c
|
||||||
set(STENO_DICT_SRC ${STENO_DICTS_DIR}/lapwing.json)
|
src/formatter.c
|
||||||
|
src/undo.c
|
||||||
|
src/split_dict.c
|
||||||
|
src/split_cache.c
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
# Peripheral side: dict embedded here, serves GATT queries
|
||||||
|
target_sources(app PRIVATE
|
||||||
|
src/dict_embed.S
|
||||||
|
)
|
||||||
|
if(CONFIG_STENO_DICT_MPHF)
|
||||||
|
target_sources(app PRIVATE src/dict_mphf.c)
|
||||||
|
else()
|
||||||
|
target_sources(app PRIVATE src/trie.c)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# ── Non-split mode ───────────────────────────────────
|
||||||
|
# Everything on one board (or central-only without split)
|
||||||
else()
|
else()
|
||||||
set(STENO_DICT_SRC ${STENO_DICTS_DIR}/test.json)
|
if(NOT CONFIG_ZMK_SPLIT OR CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
|
||||||
|
target_sources(app PRIVATE
|
||||||
|
src/behavior_steno.c
|
||||||
|
src/output.c
|
||||||
|
src/formatter.c
|
||||||
|
src/undo.c
|
||||||
|
src/dict_embed.S
|
||||||
|
)
|
||||||
|
if(CONFIG_STENO_DICT_MPHF)
|
||||||
|
target_sources(app PRIVATE src/dict_mphf.c)
|
||||||
|
else()
|
||||||
|
target_sources(app PRIVATE src/trie.c)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Auto-download dict if needed (Plover/Lapwing only)
|
# ── Dictionary compilation ───────────────────────────
|
||||||
if(DEFINED STENO_DICT_NAME AND NOT EXISTS ${STENO_DICT_SRC})
|
# Build dict binary when we embed it (non-split, or split peripheral)
|
||||||
|
set(STENO_NEED_DICT_EMBED FALSE)
|
||||||
|
if(CONFIG_STENO_SPLIT_DICT)
|
||||||
|
if(NOT CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
|
||||||
|
set(STENO_NEED_DICT_EMBED TRUE)
|
||||||
|
endif()
|
||||||
|
elseif(NOT CONFIG_ZMK_SPLIT OR CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
|
||||||
|
set(STENO_NEED_DICT_EMBED TRUE)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(STENO_NEED_DICT_EMBED)
|
||||||
|
find_package(Python3 REQUIRED COMPONENTS Interpreter)
|
||||||
|
set(STENO_DICT_BIN ${CMAKE_CURRENT_BINARY_DIR}/steno_dict.bin)
|
||||||
|
set(STENO_DICTS_DIR ${CMAKE_CURRENT_SOURCE_DIR}/dicts)
|
||||||
|
set(STENO_FETCH ${CMAKE_CURRENT_SOURCE_DIR}/tools/fetch_dict.py)
|
||||||
|
|
||||||
|
if(CONFIG_STENO_DICT_PLOVER)
|
||||||
|
set(STENO_DICT_NAME "plover")
|
||||||
|
set(STENO_DICT_SRC ${STENO_DICTS_DIR}/plover-main.json)
|
||||||
|
elseif(CONFIG_STENO_DICT_LAPWING)
|
||||||
|
set(STENO_DICT_NAME "lapwing")
|
||||||
|
set(STENO_DICT_SRC ${STENO_DICTS_DIR}/lapwing.json)
|
||||||
|
else()
|
||||||
|
set(STENO_DICT_SRC ${STENO_DICTS_DIR}/test.json)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Auto-download dict if needed (Plover/Lapwing only)
|
||||||
|
if(DEFINED STENO_DICT_NAME AND NOT EXISTS ${STENO_DICT_SRC})
|
||||||
message(STATUS "Steno: downloading ${STENO_DICT_NAME} dictionary...")
|
message(STATUS "Steno: downloading ${STENO_DICT_NAME} dictionary...")
|
||||||
execute_process(
|
execute_process(
|
||||||
COMMAND ${Python3_EXECUTABLE} ${STENO_FETCH}
|
COMMAND ${Python3_EXECUTABLE} ${STENO_FETCH}
|
||||||
|
|
@ -50,12 +92,12 @@ if(DEFINED STENO_DICT_NAME AND NOT EXISTS ${STENO_DICT_SRC})
|
||||||
if(NOT FETCH_RESULT EQUAL 0)
|
if(NOT FETCH_RESULT EQUAL 0)
|
||||||
message(WARNING "Steno: dict download failed. Build may fail.")
|
message(WARNING "Steno: dict download failed. Build may fail.")
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# ── Dictionary compilation ──
|
# Compile dict binary
|
||||||
if(EXISTS ${STENO_DICT_SRC})
|
if(EXISTS ${STENO_DICT_SRC})
|
||||||
if(CONFIG_STENO_DICT_MPHF)
|
if(CONFIG_STENO_DICT_MPHF)
|
||||||
# Fetch at build time if hash changed (re-run on rebuild)
|
# Fetch at build time if hash changed
|
||||||
if(DEFINED STENO_DICT_NAME)
|
if(DEFINED STENO_DICT_NAME)
|
||||||
add_custom_command(
|
add_custom_command(
|
||||||
OUTPUT ${STENO_DICT_SRC}.stamp
|
OUTPUT ${STENO_DICT_SRC}.stamp
|
||||||
|
|
@ -95,11 +137,9 @@ if(EXISTS ${STENO_DICT_SRC})
|
||||||
endif()
|
endif()
|
||||||
add_dependencies(app steno_dict_gen)
|
add_dependencies(app steno_dict_gen)
|
||||||
|
|
||||||
# Generate header with dict path for .incbin
|
set_property(SOURCE src/dict_embed.S APPEND PROPERTY
|
||||||
file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/steno_dict_path.h
|
COMPILE_DEFINITIONS STENO_DICT_BIN_PATH="${STENO_DICT_BIN}")
|
||||||
"#define STENO_DICT_BIN_PATH \"${STENO_DICT_BIN}\"\n")
|
endif()
|
||||||
target_include_directories(app PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
endif() # central role
|
|
||||||
endif() # CONFIG_STENO_ENGINE
|
endif() # CONFIG_STENO_ENGINE
|
||||||
|
|
|
||||||
34
Kconfig
34
Kconfig
|
|
@ -16,7 +16,7 @@ choice STENO_DICT
|
||||||
config STENO_DICT_PLOVER
|
config STENO_DICT_PLOVER
|
||||||
bool "Plover main dictionary (MPHF compressed)"
|
bool "Plover main dictionary (MPHF compressed)"
|
||||||
help
|
help
|
||||||
Use Plover main.json via MPHF compression (~44K entries in 453KB).
|
Use Plover main.json via MPHF compression.
|
||||||
|
|
||||||
config STENO_DICT_LAPWING
|
config STENO_DICT_LAPWING
|
||||||
bool "Lapwing dictionary (MPHF compressed)"
|
bool "Lapwing dictionary (MPHF compressed)"
|
||||||
|
|
@ -35,7 +35,6 @@ config STENO_DICT_MPHF
|
||||||
help
|
help
|
||||||
Use MPHF (minimal perfect hash) dictionary format.
|
Use MPHF (minimal perfect hash) dictionary format.
|
||||||
Selects ZLIB for block-compressed string table decompression.
|
Selects ZLIB for block-compressed string table decompression.
|
||||||
Auto-selected for Plover/Lapwing dicts.
|
|
||||||
|
|
||||||
config STENO_CUSTOM_KEYMAP
|
config STENO_CUSTOM_KEYMAP
|
||||||
bool "Custom steno keymap"
|
bool "Custom steno keymap"
|
||||||
|
|
@ -76,10 +75,35 @@ config STENO_MULTI_STROKE_TIMEOUT_MS
|
||||||
|
|
||||||
config STENO_DICT_MAX_SIZE
|
config STENO_DICT_MAX_SIZE
|
||||||
int "Max dictionary binary size (bytes)"
|
int "Max dictionary binary size (bytes)"
|
||||||
default 430080
|
default 473088
|
||||||
help
|
help
|
||||||
Max compiled dict size. 430080 = 420KB.
|
Max compiled dict size. 473088 = 462KB.
|
||||||
Leaves room for zlib decompressor + USB logging overhead.
|
|
||||||
The MPHF compiler auto-trims to fit.
|
The MPHF compiler auto-trims to fit.
|
||||||
|
|
||||||
|
menuconfig STENO_SPLIT_DICT
|
||||||
|
bool "Split dictionary storage on peripheral"
|
||||||
|
default n
|
||||||
|
help
|
||||||
|
Store the steno dictionary on the peripheral half and
|
||||||
|
perform lookups over BLE. This frees flash on the central
|
||||||
|
side at the cost of added lookup latency.
|
||||||
|
|
||||||
|
if STENO_SPLIT_DICT
|
||||||
|
|
||||||
|
config STENO_SPLIT_CACHE_SIZE
|
||||||
|
int "LRU cache entries on central side"
|
||||||
|
default 64
|
||||||
|
range 16 512
|
||||||
|
|
||||||
|
config STENO_SPLIT_PREFETCH
|
||||||
|
bool "Prefetch common follow-up strokes"
|
||||||
|
default y
|
||||||
|
|
||||||
|
config STENO_SPLIT_TIMEOUT_MS
|
||||||
|
int "BLE lookup timeout (ms)"
|
||||||
|
default 50
|
||||||
|
range 10 500
|
||||||
|
|
||||||
|
endif # STENO_SPLIT_DICT
|
||||||
|
|
||||||
endif # STENO_ENGINE
|
endif # STENO_ENGINE
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,9 @@
|
||||||
#include "undo.h"
|
#include "undo.h"
|
||||||
#include "formatter.h"
|
#include "formatter.h"
|
||||||
|
|
||||||
#if IS_ENABLED(CONFIG_STENO_DICT_MPHF)
|
#if IS_ENABLED(CONFIG_STENO_SPLIT_DICT)
|
||||||
|
#include "split_dict.h"
|
||||||
|
#elif IS_ENABLED(CONFIG_STENO_DICT_MPHF)
|
||||||
#include "dict_mphf.h"
|
#include "dict_mphf.h"
|
||||||
#else
|
#else
|
||||||
#include "trie.h"
|
#include "trie.h"
|
||||||
|
|
@ -24,34 +26,18 @@
|
||||||
|
|
||||||
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
|
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
|
||||||
|
|
||||||
|
#if !IS_ENABLED(CONFIG_STENO_SPLIT_DICT)
|
||||||
extern const uint8_t _steno_dict_start[];
|
extern const uint8_t _steno_dict_start[];
|
||||||
extern const uint8_t _steno_dict_end[];
|
extern const uint8_t _steno_dict_end[];
|
||||||
|
#endif
|
||||||
|
|
||||||
#if IS_ENABLED(CONFIG_STENO_DICT_MPHF)
|
#if IS_ENABLED(CONFIG_STENO_DICT_MPHF) && !IS_ENABLED(CONFIG_STENO_SPLIT_DICT)
|
||||||
static struct dict_mphf mphf_dict;
|
static struct dict_mphf mphf_dict;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#define STENO_MAX_MULTI 8
|
#define STENO_MAX_MULTI 8
|
||||||
#define STENO_MULTI_TIMEOUT_MS CONFIG_STENO_MULTI_STROKE_TIMEOUT_MS
|
#define STENO_MULTI_TIMEOUT_MS CONFIG_STENO_MULTI_STROKE_TIMEOUT_MS
|
||||||
|
|
||||||
static inline const char *dict_lookup(const uint32_t *strokes, uint8_t count)
|
|
||||||
{
|
|
||||||
#if IS_ENABLED(CONFIG_STENO_DICT_MPHF)
|
|
||||||
return dict_mphf_lookup(&mphf_dict, strokes, count);
|
|
||||||
#else
|
|
||||||
return steno_trie_lookup(strokes, count);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline bool dict_has_prefix(const uint32_t *strokes, uint8_t count)
|
|
||||||
{
|
|
||||||
#if IS_ENABLED(CONFIG_STENO_DICT_MPHF)
|
|
||||||
return (count == 1) ? dict_mphf_has_prefix(&mphf_dict, strokes[0]) : false;
|
|
||||||
#else
|
|
||||||
return steno_trie_has_prefix(strokes, count);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
struct steno_state {
|
struct steno_state {
|
||||||
uint32_t current_chord;
|
uint32_t current_chord;
|
||||||
uint8_t keys_held;
|
uint8_t keys_held;
|
||||||
|
|
@ -68,6 +54,30 @@ static bool dict_ready;
|
||||||
static void flush_strokes(void);
|
static void flush_strokes(void);
|
||||||
static void multi_timeout_handler(struct k_work *work);
|
static void multi_timeout_handler(struct k_work *work);
|
||||||
|
|
||||||
|
static const char *do_lookup(const uint32_t *strokes, uint8_t count)
|
||||||
|
{
|
||||||
|
#if IS_ENABLED(CONFIG_STENO_SPLIT_DICT)
|
||||||
|
static char split_buf[128];
|
||||||
|
int ret = split_dict_lookup(strokes, count, split_buf, sizeof(split_buf));
|
||||||
|
return (ret > 0) ? split_buf : NULL;
|
||||||
|
#elif IS_ENABLED(CONFIG_STENO_DICT_MPHF)
|
||||||
|
return dict_mphf_lookup(&mphf_dict, strokes, count);
|
||||||
|
#else
|
||||||
|
return steno_trie_lookup(strokes, count);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool do_has_prefix(const uint32_t *strokes, uint8_t count)
|
||||||
|
{
|
||||||
|
#if IS_ENABLED(CONFIG_STENO_SPLIT_DICT)
|
||||||
|
return split_dict_has_prefix(strokes, count);
|
||||||
|
#elif IS_ENABLED(CONFIG_STENO_DICT_MPHF)
|
||||||
|
return (count == 1) ? dict_mphf_has_prefix(&mphf_dict, strokes[0]) : false;
|
||||||
|
#else
|
||||||
|
return steno_trie_has_prefix(strokes, count);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
static void emit_formatted(const char *translation,
|
static void emit_formatted(const char *translation,
|
||||||
const uint32_t *strokes, uint8_t stroke_count)
|
const uint32_t *strokes, uint8_t stroke_count)
|
||||||
{
|
{
|
||||||
|
|
@ -114,15 +124,11 @@ static void process_chord(void)
|
||||||
state.current_chord = 0;
|
state.current_chord = 0;
|
||||||
|
|
||||||
if (!dict_ready) {
|
if (!dict_ready) {
|
||||||
LOG_WRN("steno dict not ready, flushing");
|
|
||||||
flush_strokes();
|
flush_strokes();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const char *translation = dict_lookup(
|
const char *translation = do_lookup(state.pending_strokes, state.stroke_count);
|
||||||
state.pending_strokes, state.stroke_count);
|
|
||||||
LOG_INF("steno lookup %u strokes → %s", state.stroke_count,
|
|
||||||
translation ? translation : "(null)");
|
|
||||||
|
|
||||||
if (translation) {
|
if (translation) {
|
||||||
emit_formatted(translation, state.pending_strokes, state.stroke_count);
|
emit_formatted(translation, state.pending_strokes, state.stroke_count);
|
||||||
|
|
@ -130,7 +136,7 @@ static void process_chord(void)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dict_has_prefix(state.pending_strokes, state.stroke_count)) {
|
if (do_has_prefix(state.pending_strokes, state.stroke_count)) {
|
||||||
k_work_schedule(&state.multi_timeout,
|
k_work_schedule(&state.multi_timeout,
|
||||||
K_MSEC(STENO_MULTI_TIMEOUT_MS));
|
K_MSEC(STENO_MULTI_TIMEOUT_MS));
|
||||||
return;
|
return;
|
||||||
|
|
@ -140,8 +146,7 @@ static void process_chord(void)
|
||||||
uint32_t last = state.pending_strokes[state.stroke_count - 1];
|
uint32_t last = state.pending_strokes[state.stroke_count - 1];
|
||||||
state.stroke_count--;
|
state.stroke_count--;
|
||||||
|
|
||||||
const char *partial = dict_lookup(
|
const char *partial = do_lookup(state.pending_strokes, state.stroke_count);
|
||||||
state.pending_strokes, state.stroke_count);
|
|
||||||
if (partial) {
|
if (partial) {
|
||||||
emit_formatted(partial, state.pending_strokes, state.stroke_count);
|
emit_formatted(partial, state.pending_strokes, state.stroke_count);
|
||||||
}
|
}
|
||||||
|
|
@ -149,7 +154,7 @@ static void process_chord(void)
|
||||||
state.pending_strokes[0] = last;
|
state.pending_strokes[0] = last;
|
||||||
state.stroke_count = 1;
|
state.stroke_count = 1;
|
||||||
|
|
||||||
const char *rest = dict_lookup(&last, 1);
|
const char *rest = do_lookup(&last, 1);
|
||||||
if (rest) {
|
if (rest) {
|
||||||
emit_formatted(rest, &last, 1);
|
emit_formatted(rest, &last, 1);
|
||||||
state.stroke_count = 0;
|
state.stroke_count = 0;
|
||||||
|
|
@ -169,14 +174,10 @@ static void flush_strokes(void)
|
||||||
static void multi_timeout_handler(struct k_work *work)
|
static void multi_timeout_handler(struct k_work *work)
|
||||||
{
|
{
|
||||||
ARG_UNUSED(work);
|
ARG_UNUSED(work);
|
||||||
|
|
||||||
if (state.stroke_count == 0) {
|
if (state.stroke_count == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const char *translation = do_lookup(state.pending_strokes, state.stroke_count);
|
||||||
const char *translation = dict_lookup(
|
|
||||||
state.pending_strokes, state.stroke_count);
|
|
||||||
|
|
||||||
if (translation) {
|
if (translation) {
|
||||||
emit_formatted(translation, state.pending_strokes, state.stroke_count);
|
emit_formatted(translation, state.pending_strokes, state.stroke_count);
|
||||||
}
|
}
|
||||||
|
|
@ -187,15 +188,13 @@ static int on_steno_binding_pressed(struct zmk_behavior_binding *binding,
|
||||||
struct zmk_behavior_binding_event event)
|
struct zmk_behavior_binding_event event)
|
||||||
{
|
{
|
||||||
uint32_t key_index = binding->param1;
|
uint32_t key_index = binding->param1;
|
||||||
|
|
||||||
if (key_index > 35) {
|
if (key_index > 35) {
|
||||||
return -EINVAL;
|
return -EINVAL;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.current_chord |= (1U << key_index);
|
state.current_chord |= (1U << key_index);
|
||||||
state.keys_held++;
|
state.keys_held++;
|
||||||
|
|
||||||
LOG_INF("steno press key=%u chord=0x%06X held=%u",
|
LOG_DBG("Key %u pressed, chord=0x%06X held=%u",
|
||||||
key_index, state.current_chord, state.keys_held);
|
key_index, state.current_chord, state.keys_held);
|
||||||
|
|
||||||
return ZMK_BEHAVIOR_OPAQUE;
|
return ZMK_BEHAVIOR_OPAQUE;
|
||||||
|
|
@ -207,14 +206,9 @@ static int on_steno_binding_released(struct zmk_behavior_binding *binding,
|
||||||
if (state.keys_held > 0) {
|
if (state.keys_held > 0) {
|
||||||
state.keys_held--;
|
state.keys_held--;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_INF("steno release held=%u chord=0x%06X", state.keys_held, state.current_chord);
|
|
||||||
|
|
||||||
if (state.keys_held == 0 && state.current_chord != 0) {
|
if (state.keys_held == 0 && state.current_chord != 0) {
|
||||||
LOG_INF("steno all-up → process chord 0x%06X", state.current_chord);
|
|
||||||
process_chord();
|
process_chord();
|
||||||
}
|
}
|
||||||
|
|
||||||
return ZMK_BEHAVIOR_OPAQUE;
|
return ZMK_BEHAVIOR_OPAQUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -230,6 +224,10 @@ static int behavior_steno_init(const struct device *dev)
|
||||||
steno_undo_init(&undo_history);
|
steno_undo_init(&undo_history);
|
||||||
k_work_init_delayable(&state.multi_timeout, multi_timeout_handler);
|
k_work_init_delayable(&state.multi_timeout, multi_timeout_handler);
|
||||||
|
|
||||||
|
#if IS_ENABLED(CONFIG_STENO_SPLIT_DICT)
|
||||||
|
split_dict_init();
|
||||||
|
dict_ready = true;
|
||||||
|
#else
|
||||||
size_t dict_size = _steno_dict_end - _steno_dict_start;
|
size_t dict_size = _steno_dict_end - _steno_dict_start;
|
||||||
if (dict_size > 4) {
|
if (dict_size > 4) {
|
||||||
int ret;
|
int ret;
|
||||||
|
|
@ -247,6 +245,7 @@ static int behavior_steno_init(const struct device *dev)
|
||||||
} else {
|
} else {
|
||||||
LOG_WRN("No steno dict embedded");
|
LOG_WRN("No steno dict embedded");
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
LOG_INF("Steno engine initialized");
|
LOG_INF("Steno engine initialized");
|
||||||
return 0;
|
return 0;
|
||||||
|
|
|
||||||
167
src/split_cache.c
Normal file
167
src/split_cache.c
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 zmk-steno-engine contributors
|
||||||
|
* SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
|
||||||
|
*
|
||||||
|
* Licensed under the PolyForm Noncommercial License 1.0.0;
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* https://polyformproject.org/licenses/noncommercial/1.0.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
#include "split_cache.h"
|
||||||
|
|
||||||
|
/* FNV-1a hash over stroke bytes */
|
||||||
|
static uint32_t hash_strokes(const uint32_t *strokes, uint8_t count)
|
||||||
|
{
|
||||||
|
uint32_t hash = 2166136261u; /* FNV offset basis */
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < count; i++) {
|
||||||
|
uint32_t s = strokes[i];
|
||||||
|
for (int b = 0; b < 4; b++) {
|
||||||
|
hash ^= (s & 0xFF);
|
||||||
|
hash *= 16777619u; /* FNV prime */
|
||||||
|
s >>= 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool strokes_match(const struct cache_entry *entry,
|
||||||
|
const uint32_t *strokes, uint8_t count)
|
||||||
|
{
|
||||||
|
if (entry->stroke_count != count) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return memcmp(entry->strokes, strokes, count * sizeof(uint32_t)) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void split_cache_init(struct split_cache *cache)
|
||||||
|
{
|
||||||
|
memset(cache->entries, 0,
|
||||||
|
sizeof(struct cache_entry) * CONFIG_STENO_SPLIT_CACHE_SIZE);
|
||||||
|
cache->access_counter = 0;
|
||||||
|
cache->hits = 0;
|
||||||
|
cache->misses = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool split_cache_lookup(struct split_cache *cache, const uint32_t *strokes,
|
||||||
|
uint8_t count, char *result, size_t result_size,
|
||||||
|
bool *has_prefix)
|
||||||
|
{
|
||||||
|
uint32_t h = hash_strokes(strokes, count);
|
||||||
|
|
||||||
|
for (int i = 0; i < CONFIG_STENO_SPLIT_CACHE_SIZE; i++) {
|
||||||
|
struct cache_entry *e = &cache->entries[i];
|
||||||
|
|
||||||
|
if (!e->valid) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e->key_hash == h && strokes_match(e, strokes, count)) {
|
||||||
|
/* Hit */
|
||||||
|
cache->access_counter++;
|
||||||
|
e->access_count = cache->access_counter;
|
||||||
|
cache->hits++;
|
||||||
|
|
||||||
|
if (has_prefix) {
|
||||||
|
*has_prefix = e->has_prefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result && result_size > 0) {
|
||||||
|
size_t len = strlen(e->translation);
|
||||||
|
if (len >= result_size) {
|
||||||
|
len = result_size - 1;
|
||||||
|
}
|
||||||
|
memcpy(result, e->translation, len);
|
||||||
|
result[len] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cache->misses++;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void split_cache_insert(struct split_cache *cache, const uint32_t *strokes,
|
||||||
|
uint8_t count, const char *translation, bool has_prefix)
|
||||||
|
{
|
||||||
|
if (count == 0 || count > 8) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t h = hash_strokes(strokes, count);
|
||||||
|
|
||||||
|
/* Check if already present → update */
|
||||||
|
for (int i = 0; i < CONFIG_STENO_SPLIT_CACHE_SIZE; i++) {
|
||||||
|
struct cache_entry *e = &cache->entries[i];
|
||||||
|
if (e->valid && e->key_hash == h && strokes_match(e, strokes, count)) {
|
||||||
|
/* Update existing entry */
|
||||||
|
if (translation) {
|
||||||
|
size_t len = strlen(translation);
|
||||||
|
if (len >= SPLIT_CACHE_VALUE_SIZE) {
|
||||||
|
len = SPLIT_CACHE_VALUE_SIZE - 1;
|
||||||
|
}
|
||||||
|
memcpy(e->translation, translation, len);
|
||||||
|
e->translation[len] = '\0';
|
||||||
|
}
|
||||||
|
e->has_prefix = has_prefix;
|
||||||
|
cache->access_counter++;
|
||||||
|
e->access_count = cache->access_counter;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Find empty slot or LRU victim */
|
||||||
|
int target = -1;
|
||||||
|
uint32_t min_access = UINT32_MAX;
|
||||||
|
|
||||||
|
for (int i = 0; i < CONFIG_STENO_SPLIT_CACHE_SIZE; i++) {
|
||||||
|
if (!cache->entries[i].valid) {
|
||||||
|
target = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (cache->entries[i].access_count < min_access) {
|
||||||
|
min_access = cache->entries[i].access_count;
|
||||||
|
target = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target < 0) {
|
||||||
|
target = 0; /* fallback: should never happen if cache size > 0 */
|
||||||
|
}
|
||||||
|
|
||||||
|
struct cache_entry *e = &cache->entries[target];
|
||||||
|
e->key_hash = h;
|
||||||
|
e->stroke_count = count;
|
||||||
|
memcpy(e->strokes, strokes, count * sizeof(uint32_t));
|
||||||
|
|
||||||
|
if (translation) {
|
||||||
|
size_t len = strlen(translation);
|
||||||
|
if (len >= SPLIT_CACHE_VALUE_SIZE) {
|
||||||
|
len = SPLIT_CACHE_VALUE_SIZE - 1;
|
||||||
|
}
|
||||||
|
memcpy(e->translation, translation, len);
|
||||||
|
e->translation[len] = '\0';
|
||||||
|
} else {
|
||||||
|
e->translation[0] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
e->has_prefix = has_prefix;
|
||||||
|
e->valid = true;
|
||||||
|
cache->access_counter++;
|
||||||
|
e->access_count = cache->access_counter;
|
||||||
|
}
|
||||||
|
|
||||||
|
void split_cache_invalidate(struct split_cache *cache)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < CONFIG_STENO_SPLIT_CACHE_SIZE; i++) {
|
||||||
|
cache->entries[i].valid = false;
|
||||||
|
}
|
||||||
|
cache->access_counter = 0;
|
||||||
|
cache->hits = 0;
|
||||||
|
cache->misses = 0;
|
||||||
|
}
|
||||||
46
src/split_cache.h
Normal file
46
src/split_cache.h
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 zmk-steno-engine contributors
|
||||||
|
* SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
|
||||||
|
*
|
||||||
|
* Licensed under the PolyForm Noncommercial License 1.0.0;
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* https://polyformproject.org/licenses/noncommercial/1.0.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef SPLIT_CACHE_H
|
||||||
|
#define SPLIT_CACHE_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#define SPLIT_CACHE_KEY_SIZE 24 /* max 8 strokes * 3 bytes */
|
||||||
|
#define SPLIT_CACHE_VALUE_SIZE 128 /* max translation length */
|
||||||
|
|
||||||
|
struct cache_entry {
|
||||||
|
uint32_t key_hash;
|
||||||
|
uint8_t stroke_count;
|
||||||
|
uint32_t strokes[8];
|
||||||
|
char translation[SPLIT_CACHE_VALUE_SIZE];
|
||||||
|
bool has_prefix;
|
||||||
|
bool valid;
|
||||||
|
uint32_t access_count;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct split_cache {
|
||||||
|
struct cache_entry entries[CONFIG_STENO_SPLIT_CACHE_SIZE];
|
||||||
|
uint32_t access_counter;
|
||||||
|
uint32_t hits;
|
||||||
|
uint32_t misses;
|
||||||
|
};
|
||||||
|
|
||||||
|
void split_cache_init(struct split_cache *cache);
|
||||||
|
bool split_cache_lookup(struct split_cache *cache, const uint32_t *strokes,
|
||||||
|
uint8_t count, char *result, size_t result_size,
|
||||||
|
bool *has_prefix);
|
||||||
|
void split_cache_insert(struct split_cache *cache, const uint32_t *strokes,
|
||||||
|
uint8_t count, const char *translation, bool has_prefix);
|
||||||
|
void split_cache_invalidate(struct split_cache *cache);
|
||||||
|
|
||||||
|
#endif /* SPLIT_CACHE_H */
|
||||||
465
src/split_dict.c
Normal file
465
src/split_dict.c
Normal file
|
|
@ -0,0 +1,465 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 zmk-steno-engine contributors
|
||||||
|
* SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
|
||||||
|
*
|
||||||
|
* Licensed under the PolyForm Noncommercial License 1.0.0;
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* https://polyformproject.org/licenses/noncommercial/1.0.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
#include <zephyr/kernel.h>
|
||||||
|
#include <zephyr/bluetooth/bluetooth.h>
|
||||||
|
#include <zephyr/bluetooth/gatt.h>
|
||||||
|
#include <zephyr/bluetooth/conn.h>
|
||||||
|
#include <zephyr/bluetooth/uuid.h>
|
||||||
|
#include <zephyr/logging/log.h>
|
||||||
|
|
||||||
|
#include "split_dict.h"
|
||||||
|
#include "split_cache.h"
|
||||||
|
|
||||||
|
LOG_MODULE_REGISTER(split_dict, CONFIG_STENO_SPLIT_LOG_LEVEL);
|
||||||
|
|
||||||
|
/* Semaphore for blocking on BLE response */
|
||||||
|
static K_SEM_DEFINE(response_sem, 0, 1);
|
||||||
|
|
||||||
|
/* Current pending response state */
|
||||||
|
static uint8_t pending_seq;
|
||||||
|
static uint8_t response_buf[256];
|
||||||
|
static uint16_t response_len;
|
||||||
|
static uint8_t seq_counter;
|
||||||
|
|
||||||
|
/* Cache instance */
|
||||||
|
static struct split_cache dict_cache;
|
||||||
|
|
||||||
|
/* External trie lookup (peripheral side) */
|
||||||
|
extern int trie_lookup(const uint32_t *strokes, uint8_t count,
|
||||||
|
char *result, size_t result_size);
|
||||||
|
extern bool trie_has_prefix(const uint32_t *strokes, uint8_t count);
|
||||||
|
|
||||||
|
/* --- Helpers --- */
|
||||||
|
|
||||||
|
static void encode_strokes(const uint32_t *strokes, uint8_t count, uint8_t *out)
|
||||||
|
{
|
||||||
|
for (uint8_t i = 0; i < count; i++) {
|
||||||
|
out[i * 3 + 0] = (strokes[i] >> 16) & 0xFF;
|
||||||
|
out[i * 3 + 1] = (strokes[i] >> 8) & 0xFF;
|
||||||
|
out[i * 3 + 2] = strokes[i] & 0xFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void decode_strokes(const uint8_t *in, uint8_t count, uint32_t *strokes)
|
||||||
|
{
|
||||||
|
for (uint8_t i = 0; i < count; i++) {
|
||||||
|
strokes[i] = ((uint32_t)in[i * 3 + 0] << 16) |
|
||||||
|
((uint32_t)in[i * 3 + 1] << 8) |
|
||||||
|
(uint32_t)in[i * 3 + 2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- GATT Write Callbacks (peripheral side handlers) --- */
|
||||||
|
|
||||||
|
static ssize_t dict_query_write_cb(struct bt_conn *conn,
|
||||||
|
const struct bt_gatt_attr *attr,
|
||||||
|
const void *buf, uint16_t len,
|
||||||
|
uint16_t offset, uint8_t flags)
|
||||||
|
{
|
||||||
|
const struct steno_query_pkt *pkt = buf;
|
||||||
|
|
||||||
|
if (len < sizeof(struct steno_query_pkt)) {
|
||||||
|
LOG_WRN("Query pkt too short: %u", len);
|
||||||
|
return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t stroke_count = pkt->stroke_count;
|
||||||
|
uint16_t expected = sizeof(struct steno_query_pkt) + stroke_count * 3;
|
||||||
|
|
||||||
|
if (len < expected) {
|
||||||
|
LOG_WRN("Query pkt truncated: got %u, need %u", len, expected);
|
||||||
|
return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t strokes[8];
|
||||||
|
if (stroke_count > 8) {
|
||||||
|
stroke_count = 8;
|
||||||
|
}
|
||||||
|
decode_strokes(pkt->strokes, stroke_count, strokes);
|
||||||
|
|
||||||
|
/* Build response */
|
||||||
|
struct steno_response_pkt *resp = (struct steno_response_pkt *)response_buf;
|
||||||
|
resp->msg_type = STENO_MSG_RESPONSE;
|
||||||
|
resp->seq = pkt->seq;
|
||||||
|
|
||||||
|
char translation[128];
|
||||||
|
int ret = trie_lookup(strokes, stroke_count, translation, sizeof(translation));
|
||||||
|
|
||||||
|
if (ret > 0) {
|
||||||
|
resp->status = STENO_STATUS_FOUND;
|
||||||
|
resp->data_len = (uint16_t)ret;
|
||||||
|
memcpy(resp->data, translation, ret);
|
||||||
|
response_len = sizeof(struct steno_response_pkt) + ret;
|
||||||
|
} else {
|
||||||
|
resp->status = STENO_STATUS_NOT_FOUND;
|
||||||
|
resp->data_len = 0;
|
||||||
|
response_len = sizeof(struct steno_response_pkt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Notify central with response */
|
||||||
|
bt_gatt_notify(conn, attr, response_buf, response_len);
|
||||||
|
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ssize_t dict_prefix_write_cb(struct bt_conn *conn,
|
||||||
|
const struct bt_gatt_attr *attr,
|
||||||
|
const void *buf, uint16_t len,
|
||||||
|
uint16_t offset, uint8_t flags)
|
||||||
|
{
|
||||||
|
const struct steno_query_pkt *pkt = buf;
|
||||||
|
|
||||||
|
if (len < sizeof(struct steno_query_pkt)) {
|
||||||
|
return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t stroke_count = pkt->stroke_count;
|
||||||
|
if (stroke_count > 8) {
|
||||||
|
stroke_count = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t strokes[8];
|
||||||
|
decode_strokes(pkt->strokes, stroke_count, strokes);
|
||||||
|
|
||||||
|
struct steno_response_pkt *resp = (struct steno_response_pkt *)response_buf;
|
||||||
|
resp->msg_type = STENO_MSG_RESPONSE;
|
||||||
|
resp->seq = pkt->seq;
|
||||||
|
resp->data_len = 0;
|
||||||
|
|
||||||
|
if (trie_has_prefix(strokes, stroke_count)) {
|
||||||
|
resp->status = STENO_STATUS_PREFIX_ONLY;
|
||||||
|
} else {
|
||||||
|
resp->status = STENO_STATUS_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
response_len = sizeof(struct steno_response_pkt);
|
||||||
|
bt_gatt_notify(conn, attr, response_buf, response_len);
|
||||||
|
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ssize_t dict_batch_write_cb(struct bt_conn *conn,
|
||||||
|
const struct bt_gatt_attr *attr,
|
||||||
|
const void *buf, uint16_t len,
|
||||||
|
uint16_t offset, uint8_t flags)
|
||||||
|
{
|
||||||
|
const struct steno_batch_query_pkt *pkt = buf;
|
||||||
|
|
||||||
|
if (len < sizeof(struct steno_batch_query_pkt)) {
|
||||||
|
return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("Batch query: %u queries", pkt->query_count);
|
||||||
|
|
||||||
|
/* Process each sub-query packed in queries[] */
|
||||||
|
uint16_t pos = 0;
|
||||||
|
const uint8_t *data = pkt->queries;
|
||||||
|
uint16_t data_len = len - sizeof(struct steno_batch_query_pkt);
|
||||||
|
|
||||||
|
for (uint8_t q = 0; q < pkt->query_count && pos < data_len; q++) {
|
||||||
|
if (pos >= data_len) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
uint8_t stroke_count = data[pos];
|
||||||
|
pos++;
|
||||||
|
|
||||||
|
if (stroke_count > 8) {
|
||||||
|
stroke_count = 8;
|
||||||
|
}
|
||||||
|
if (pos + stroke_count * 3 > data_len) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t strokes[8];
|
||||||
|
decode_strokes(&data[pos], stroke_count, strokes);
|
||||||
|
pos += stroke_count * 3;
|
||||||
|
|
||||||
|
/* Lookup and send individual response per query */
|
||||||
|
struct steno_response_pkt *resp = (struct steno_response_pkt *)response_buf;
|
||||||
|
resp->msg_type = STENO_MSG_RESPONSE;
|
||||||
|
resp->seq = pkt->seq;
|
||||||
|
|
||||||
|
char translation[128];
|
||||||
|
int ret = trie_lookup(strokes, stroke_count, translation, sizeof(translation));
|
||||||
|
|
||||||
|
if (ret > 0) {
|
||||||
|
resp->status = STENO_STATUS_FOUND;
|
||||||
|
resp->data_len = (uint16_t)ret;
|
||||||
|
memcpy(resp->data, translation, ret);
|
||||||
|
response_len = sizeof(struct steno_response_pkt) + ret;
|
||||||
|
} else {
|
||||||
|
resp->status = STENO_STATUS_NOT_FOUND;
|
||||||
|
resp->data_len = 0;
|
||||||
|
response_len = sizeof(struct steno_response_pkt);
|
||||||
|
}
|
||||||
|
|
||||||
|
bt_gatt_notify(conn, attr, response_buf, response_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Notification callback (central side) --- */
|
||||||
|
|
||||||
|
static uint8_t notify_cb(struct bt_conn *conn,
|
||||||
|
struct bt_gatt_subscribe_params *params,
|
||||||
|
const void *data, uint16_t length)
|
||||||
|
{
|
||||||
|
if (!data) {
|
||||||
|
LOG_DBG("Notification unsubscribed");
|
||||||
|
return BT_GATT_ITER_STOP;
|
||||||
|
}
|
||||||
|
|
||||||
|
const struct steno_response_pkt *resp = data;
|
||||||
|
|
||||||
|
if (length < sizeof(struct steno_response_pkt)) {
|
||||||
|
LOG_WRN("Response too short");
|
||||||
|
return BT_GATT_ITER_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resp->seq == pending_seq) {
|
||||||
|
memcpy(response_buf, data, length);
|
||||||
|
response_len = length;
|
||||||
|
k_sem_give(&response_sem);
|
||||||
|
}
|
||||||
|
|
||||||
|
return BT_GATT_ITER_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- GATT Service Definition --- */
|
||||||
|
|
||||||
|
BT_GATT_SERVICE_DEFINE(steno_dict_svc,
|
||||||
|
BT_GATT_PRIMARY_SERVICE(STENO_UUID_SERVICE),
|
||||||
|
|
||||||
|
/* Dict Query characteristic: write + notify */
|
||||||
|
BT_GATT_CHARACTERISTIC(STENO_UUID_DICT_QUERY,
|
||||||
|
BT_GATT_CHRC_WRITE | BT_GATT_CHRC_NOTIFY,
|
||||||
|
BT_GATT_PERM_WRITE,
|
||||||
|
NULL, dict_query_write_cb, NULL),
|
||||||
|
BT_GATT_CCC(NULL, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
|
||||||
|
|
||||||
|
/* Dict Prefix characteristic: write + notify */
|
||||||
|
BT_GATT_CHARACTERISTIC(STENO_UUID_DICT_PREFIX,
|
||||||
|
BT_GATT_CHRC_WRITE | BT_GATT_CHRC_NOTIFY,
|
||||||
|
BT_GATT_PERM_WRITE,
|
||||||
|
NULL, dict_prefix_write_cb, NULL),
|
||||||
|
BT_GATT_CCC(NULL, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
|
||||||
|
|
||||||
|
/* Dict Batch characteristic: write + notify */
|
||||||
|
BT_GATT_CHARACTERISTIC(STENO_UUID_DICT_BATCH,
|
||||||
|
BT_GATT_CHRC_WRITE | BT_GATT_CHRC_NOTIFY,
|
||||||
|
BT_GATT_PERM_WRITE,
|
||||||
|
NULL, dict_batch_write_cb, NULL),
|
||||||
|
BT_GATT_CCC(NULL, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
|
||||||
|
);
|
||||||
|
|
||||||
|
/* --- Central-side API --- */
|
||||||
|
|
||||||
|
/* Connection handle for GATT writes (set externally or via connection cb) */
|
||||||
|
static struct bt_conn *split_conn;
|
||||||
|
static struct bt_gatt_subscribe_params subscribe_params;
|
||||||
|
|
||||||
|
int split_dict_lookup(const uint32_t *strokes, uint8_t count,
|
||||||
|
char *result, size_t result_size)
|
||||||
|
{
|
||||||
|
if (count == 0 || count > 8) {
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Check cache first */
|
||||||
|
bool has_prefix;
|
||||||
|
if (split_cache_lookup(&dict_cache, strokes, count, result, result_size, &has_prefix)) {
|
||||||
|
LOG_DBG("Cache hit for %u strokes", count);
|
||||||
|
return strlen(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!split_conn) {
|
||||||
|
LOG_ERR("No split connection");
|
||||||
|
return -ENOTCONN;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Build query packet */
|
||||||
|
uint8_t pkt_buf[sizeof(struct steno_query_pkt) + 8 * 3];
|
||||||
|
struct steno_query_pkt *pkt = (struct steno_query_pkt *)pkt_buf;
|
||||||
|
|
||||||
|
pkt->msg_type = STENO_MSG_QUERY;
|
||||||
|
pkt->seq = seq_counter++;
|
||||||
|
pkt->stroke_count = count;
|
||||||
|
encode_strokes(strokes, count, pkt->strokes);
|
||||||
|
|
||||||
|
pending_seq = pkt->seq;
|
||||||
|
k_sem_reset(&response_sem);
|
||||||
|
|
||||||
|
uint16_t pkt_len = sizeof(struct steno_query_pkt) + count * 3;
|
||||||
|
|
||||||
|
/* Send via GATT write */
|
||||||
|
int err = bt_gatt_write_without_response(split_conn, 0, pkt_buf, pkt_len, false);
|
||||||
|
if (err) {
|
||||||
|
LOG_ERR("GATT write failed: %d", err);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wait for response */
|
||||||
|
err = k_sem_take(&response_sem, K_MSEC(CONFIG_STENO_SPLIT_TIMEOUT_MS));
|
||||||
|
if (err) {
|
||||||
|
LOG_WRN("Response timeout");
|
||||||
|
return -ETIMEDOUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Decode response */
|
||||||
|
const struct steno_response_pkt *resp = (const struct steno_response_pkt *)response_buf;
|
||||||
|
|
||||||
|
if (resp->status == STENO_STATUS_FOUND) {
|
||||||
|
uint16_t copy_len = resp->data_len;
|
||||||
|
if (copy_len >= result_size) {
|
||||||
|
copy_len = result_size - 1;
|
||||||
|
}
|
||||||
|
memcpy(result, resp->data, copy_len);
|
||||||
|
result[copy_len] = '\0';
|
||||||
|
|
||||||
|
/* Cache the result */
|
||||||
|
split_cache_insert(&dict_cache, strokes, count, result, false);
|
||||||
|
|
||||||
|
return copy_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -ENOENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool split_dict_has_prefix(const uint32_t *strokes, uint8_t count)
|
||||||
|
{
|
||||||
|
if (count == 0 || count > 8) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Check cache */
|
||||||
|
bool has_prefix;
|
||||||
|
char dummy[1];
|
||||||
|
if (split_cache_lookup(&dict_cache, strokes, count, dummy, sizeof(dummy), &has_prefix)) {
|
||||||
|
return has_prefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!split_conn) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t pkt_buf[sizeof(struct steno_query_pkt) + 8 * 3];
|
||||||
|
struct steno_query_pkt *pkt = (struct steno_query_pkt *)pkt_buf;
|
||||||
|
|
||||||
|
pkt->msg_type = STENO_MSG_PREFIX;
|
||||||
|
pkt->seq = seq_counter++;
|
||||||
|
pkt->stroke_count = count;
|
||||||
|
encode_strokes(strokes, count, pkt->strokes);
|
||||||
|
|
||||||
|
pending_seq = pkt->seq;
|
||||||
|
k_sem_reset(&response_sem);
|
||||||
|
|
||||||
|
uint16_t pkt_len = sizeof(struct steno_query_pkt) + count * 3;
|
||||||
|
|
||||||
|
int err = bt_gatt_write_without_response(split_conn, 0, pkt_buf, pkt_len, false);
|
||||||
|
if (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = k_sem_take(&response_sem, K_MSEC(CONFIG_STENO_SPLIT_TIMEOUT_MS));
|
||||||
|
if (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const struct steno_response_pkt *resp = (const struct steno_response_pkt *)response_buf;
|
||||||
|
return resp->status == STENO_STATUS_PREFIX_ONLY;
|
||||||
|
}
|
||||||
|
|
||||||
|
int split_dict_batch_lookup(const uint32_t **stroke_seqs, const uint8_t *counts,
|
||||||
|
uint8_t num_queries, struct steno_batch_result *results)
|
||||||
|
{
|
||||||
|
if (num_queries == 0 || !split_conn) {
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Build batch packet */
|
||||||
|
uint8_t pkt_buf[256];
|
||||||
|
struct steno_batch_query_pkt *pkt = (struct steno_batch_query_pkt *)pkt_buf;
|
||||||
|
|
||||||
|
pkt->msg_type = STENO_MSG_BATCH;
|
||||||
|
pkt->seq = seq_counter++;
|
||||||
|
pkt->query_count = num_queries;
|
||||||
|
|
||||||
|
uint16_t pos = 0;
|
||||||
|
for (uint8_t q = 0; q < num_queries; q++) {
|
||||||
|
uint8_t cnt = counts[q];
|
||||||
|
if (cnt > 8) {
|
||||||
|
cnt = 8;
|
||||||
|
}
|
||||||
|
pkt->queries[pos] = cnt;
|
||||||
|
pos++;
|
||||||
|
encode_strokes(stroke_seqs[q], cnt, &pkt->queries[pos]);
|
||||||
|
pos += cnt * 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t pkt_len = sizeof(struct steno_batch_query_pkt) + pos;
|
||||||
|
|
||||||
|
pending_seq = pkt->seq;
|
||||||
|
k_sem_reset(&response_sem);
|
||||||
|
|
||||||
|
int err = bt_gatt_write_without_response(split_conn, 0, pkt_buf, pkt_len, false);
|
||||||
|
if (err) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Collect responses for each query */
|
||||||
|
for (uint8_t q = 0; q < num_queries; q++) {
|
||||||
|
err = k_sem_take(&response_sem, K_MSEC(CONFIG_STENO_SPLIT_TIMEOUT_MS));
|
||||||
|
if (err) {
|
||||||
|
results[q].status = STENO_STATUS_ERROR;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const struct steno_response_pkt *resp =
|
||||||
|
(const struct steno_response_pkt *)response_buf;
|
||||||
|
|
||||||
|
results[q].status = resp->status;
|
||||||
|
results[q].has_prefix = (resp->status == STENO_STATUS_PREFIX_ONLY);
|
||||||
|
|
||||||
|
if (resp->status == STENO_STATUS_FOUND && resp->data_len > 0) {
|
||||||
|
uint16_t copy_len = resp->data_len;
|
||||||
|
if (copy_len >= sizeof(results[q].translation)) {
|
||||||
|
copy_len = sizeof(results[q].translation) - 1;
|
||||||
|
}
|
||||||
|
memcpy(results[q].translation, resp->data, copy_len);
|
||||||
|
results[q].translation[copy_len] = '\0';
|
||||||
|
results[q].translation_len = copy_len;
|
||||||
|
} else {
|
||||||
|
results[q].translation[0] = '\0';
|
||||||
|
results[q].translation_len = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int split_dict_init(void)
|
||||||
|
{
|
||||||
|
split_cache_init(&dict_cache);
|
||||||
|
seq_counter = 0;
|
||||||
|
split_conn = NULL;
|
||||||
|
|
||||||
|
LOG_INF("Split dict initialized");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int split_dict_gatt_register(void)
|
||||||
|
{
|
||||||
|
/* GATT service registered statically via BT_GATT_SERVICE_DEFINE */
|
||||||
|
LOG_INF("Split dict GATT service registered");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
99
src/split_dict.h
Normal file
99
src/split_dict.h
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 zmk-steno-engine contributors
|
||||||
|
* SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
|
||||||
|
*
|
||||||
|
* Licensed under the PolyForm Noncommercial License 1.0.0;
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* https://polyformproject.org/licenses/noncommercial/1.0.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef SPLIT_DICT_H
|
||||||
|
#define SPLIT_DICT_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <zephyr/bluetooth/uuid.h>
|
||||||
|
|
||||||
|
/* Custom 128-bit UUID base for steno GATT service
|
||||||
|
* Base: 7374656e-6f00-4000-8000-000000000000 */
|
||||||
|
#define STENO_UUID_BASE \
|
||||||
|
BT_UUID_DECLARE_128(BT_UUID_128_ENCODE( \
|
||||||
|
0x7374656e, 0x6f00, 0x4000, 0x8000, 0x000000000000))
|
||||||
|
|
||||||
|
#define STENO_UUID_SERVICE \
|
||||||
|
BT_UUID_DECLARE_128(BT_UUID_128_ENCODE( \
|
||||||
|
0x7374656e, 0x6f00, 0x4000, 0x8000, 0x000000000001))
|
||||||
|
|
||||||
|
#define STENO_UUID_DICT_QUERY \
|
||||||
|
BT_UUID_DECLARE_128(BT_UUID_128_ENCODE( \
|
||||||
|
0x7374656e, 0x6f00, 0x4000, 0x8000, 0x000000000002))
|
||||||
|
|
||||||
|
#define STENO_UUID_DICT_PREFIX \
|
||||||
|
BT_UUID_DECLARE_128(BT_UUID_128_ENCODE( \
|
||||||
|
0x7374656e, 0x6f00, 0x4000, 0x8000, 0x000000000003))
|
||||||
|
|
||||||
|
#define STENO_UUID_DICT_BATCH \
|
||||||
|
BT_UUID_DECLARE_128(BT_UUID_128_ENCODE( \
|
||||||
|
0x7374656e, 0x6f00, 0x4000, 0x8000, 0x000000000004))
|
||||||
|
|
||||||
|
/* Message types */
|
||||||
|
enum steno_msg_type {
|
||||||
|
STENO_MSG_QUERY = 0x01,
|
||||||
|
STENO_MSG_PREFIX = 0x02,
|
||||||
|
STENO_MSG_BATCH = 0x03,
|
||||||
|
STENO_MSG_RESPONSE = 0x80,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Status codes */
|
||||||
|
enum steno_status {
|
||||||
|
STENO_STATUS_FOUND = 0,
|
||||||
|
STENO_STATUS_NOT_FOUND = 1,
|
||||||
|
STENO_STATUS_PREFIX_ONLY = 2,
|
||||||
|
STENO_STATUS_ERROR = 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Packet structures */
|
||||||
|
struct steno_query_pkt {
|
||||||
|
uint8_t msg_type;
|
||||||
|
uint8_t seq;
|
||||||
|
uint8_t stroke_count;
|
||||||
|
uint8_t strokes[]; /* 3 bytes per stroke (24-bit packed) */
|
||||||
|
} __packed;
|
||||||
|
|
||||||
|
struct steno_response_pkt {
|
||||||
|
uint8_t msg_type;
|
||||||
|
uint8_t seq;
|
||||||
|
uint8_t status;
|
||||||
|
uint16_t data_len;
|
||||||
|
uint8_t data[]; /* translation string (UTF-8, not null-terminated) */
|
||||||
|
} __packed;
|
||||||
|
|
||||||
|
struct steno_batch_query_pkt {
|
||||||
|
uint8_t msg_type;
|
||||||
|
uint8_t seq;
|
||||||
|
uint8_t query_count;
|
||||||
|
uint8_t queries[]; /* packed steno_query_pkt entries (without msg_type/seq) */
|
||||||
|
} __packed;
|
||||||
|
|
||||||
|
/* Batch result entry */
|
||||||
|
struct steno_batch_result {
|
||||||
|
uint8_t status;
|
||||||
|
char translation[128];
|
||||||
|
uint16_t translation_len;
|
||||||
|
bool has_prefix;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* API */
|
||||||
|
int split_dict_init(void);
|
||||||
|
int split_dict_lookup(const uint32_t *strokes, uint8_t count,
|
||||||
|
char *result, size_t result_size);
|
||||||
|
bool split_dict_has_prefix(const uint32_t *strokes, uint8_t count);
|
||||||
|
int split_dict_batch_lookup(const uint32_t **stroke_seqs, const uint8_t *counts,
|
||||||
|
uint8_t num_queries, struct steno_batch_result *results);
|
||||||
|
|
||||||
|
/* GATT service registration */
|
||||||
|
int split_dict_gatt_register(void);
|
||||||
|
|
||||||
|
#endif /* SPLIT_DICT_H */
|
||||||
Loading…
Add table
Add a link
Reference in a new issue