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
|
||||
|
||||
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
|
||||
include
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||
)
|
||||
|
||||
# ── Dictionary source resolution ──
|
||||
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)
|
||||
# ── Split-dict mode ──────────────────────────────────
|
||||
# Central: behavior engine + BLE dict client
|
||||
# Peripheral: dict embed + lookup engine + GATT server
|
||||
if(CONFIG_STENO_SPLIT_DICT)
|
||||
|
||||
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)
|
||||
if(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
|
||||
# Central side: behavior engine queries peripheral over BLE
|
||||
target_sources(app PRIVATE
|
||||
src/behavior_steno.c
|
||||
src/output.c
|
||||
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()
|
||||
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()
|
||||
|
||||
# Auto-download dict if needed (Plover/Lapwing only)
|
||||
if(DEFINED STENO_DICT_NAME AND NOT EXISTS ${STENO_DICT_SRC})
|
||||
# ── Dictionary compilation ───────────────────────────
|
||||
# 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...")
|
||||
execute_process(
|
||||
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)
|
||||
message(WARNING "Steno: dict download failed. Build may fail.")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# ── Dictionary compilation ──
|
||||
if(EXISTS ${STENO_DICT_SRC})
|
||||
# Compile dict binary
|
||||
if(EXISTS ${STENO_DICT_SRC})
|
||||
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)
|
||||
add_custom_command(
|
||||
OUTPUT ${STENO_DICT_SRC}.stamp
|
||||
|
|
@ -95,11 +137,9 @@ if(EXISTS ${STENO_DICT_SRC})
|
|||
endif()
|
||||
add_dependencies(app steno_dict_gen)
|
||||
|
||||
# Generate header with dict path for .incbin
|
||||
file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/steno_dict_path.h
|
||||
"#define STENO_DICT_BIN_PATH \"${STENO_DICT_BIN}\"\n")
|
||||
target_include_directories(app PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
|
||||
set_property(SOURCE src/dict_embed.S APPEND PROPERTY
|
||||
COMPILE_DEFINITIONS STENO_DICT_BIN_PATH="${STENO_DICT_BIN}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
endif() # central role
|
||||
endif() # CONFIG_STENO_ENGINE
|
||||
|
|
|
|||
34
Kconfig
34
Kconfig
|
|
@ -16,7 +16,7 @@ choice STENO_DICT
|
|||
config STENO_DICT_PLOVER
|
||||
bool "Plover main dictionary (MPHF compressed)"
|
||||
help
|
||||
Use Plover main.json via MPHF compression (~44K entries in 453KB).
|
||||
Use Plover main.json via MPHF compression.
|
||||
|
||||
config STENO_DICT_LAPWING
|
||||
bool "Lapwing dictionary (MPHF compressed)"
|
||||
|
|
@ -35,7 +35,6 @@ config STENO_DICT_MPHF
|
|||
help
|
||||
Use MPHF (minimal perfect hash) dictionary format.
|
||||
Selects ZLIB for block-compressed string table decompression.
|
||||
Auto-selected for Plover/Lapwing dicts.
|
||||
|
||||
config STENO_CUSTOM_KEYMAP
|
||||
bool "Custom steno keymap"
|
||||
|
|
@ -76,10 +75,35 @@ config STENO_MULTI_STROKE_TIMEOUT_MS
|
|||
|
||||
config STENO_DICT_MAX_SIZE
|
||||
int "Max dictionary binary size (bytes)"
|
||||
default 430080
|
||||
default 473088
|
||||
help
|
||||
Max compiled dict size. 430080 = 420KB.
|
||||
Leaves room for zlib decompressor + USB logging overhead.
|
||||
Max compiled dict size. 473088 = 462KB.
|
||||
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
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@
|
|||
#include "undo.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"
|
||||
#else
|
||||
#include "trie.h"
|
||||
|
|
@ -24,34 +26,18 @@
|
|||
|
||||
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_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;
|
||||
#endif
|
||||
|
||||
#define STENO_MAX_MULTI 8
|
||||
#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 {
|
||||
uint32_t current_chord;
|
||||
uint8_t keys_held;
|
||||
|
|
@ -68,6 +54,30 @@ static bool dict_ready;
|
|||
static void flush_strokes(void);
|
||||
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,
|
||||
const uint32_t *strokes, uint8_t stroke_count)
|
||||
{
|
||||
|
|
@ -114,15 +124,11 @@ static void process_chord(void)
|
|||
state.current_chord = 0;
|
||||
|
||||
if (!dict_ready) {
|
||||
LOG_WRN("steno dict not ready, flushing");
|
||||
flush_strokes();
|
||||
return;
|
||||
}
|
||||
|
||||
const char *translation = dict_lookup(
|
||||
state.pending_strokes, state.stroke_count);
|
||||
LOG_INF("steno lookup %u strokes → %s", state.stroke_count,
|
||||
translation ? translation : "(null)");
|
||||
const char *translation = do_lookup(state.pending_strokes, state.stroke_count);
|
||||
|
||||
if (translation) {
|
||||
emit_formatted(translation, state.pending_strokes, state.stroke_count);
|
||||
|
|
@ -130,7 +136,7 @@ static void process_chord(void)
|
|||
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_MSEC(STENO_MULTI_TIMEOUT_MS));
|
||||
return;
|
||||
|
|
@ -140,8 +146,7 @@ static void process_chord(void)
|
|||
uint32_t last = state.pending_strokes[state.stroke_count - 1];
|
||||
state.stroke_count--;
|
||||
|
||||
const char *partial = dict_lookup(
|
||||
state.pending_strokes, state.stroke_count);
|
||||
const char *partial = do_lookup(state.pending_strokes, state.stroke_count);
|
||||
if (partial) {
|
||||
emit_formatted(partial, state.pending_strokes, state.stroke_count);
|
||||
}
|
||||
|
|
@ -149,7 +154,7 @@ static void process_chord(void)
|
|||
state.pending_strokes[0] = last;
|
||||
state.stroke_count = 1;
|
||||
|
||||
const char *rest = dict_lookup(&last, 1);
|
||||
const char *rest = do_lookup(&last, 1);
|
||||
if (rest) {
|
||||
emit_formatted(rest, &last, 1);
|
||||
state.stroke_count = 0;
|
||||
|
|
@ -169,14 +174,10 @@ static void flush_strokes(void)
|
|||
static void multi_timeout_handler(struct k_work *work)
|
||||
{
|
||||
ARG_UNUSED(work);
|
||||
|
||||
if (state.stroke_count == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char *translation = dict_lookup(
|
||||
state.pending_strokes, state.stroke_count);
|
||||
|
||||
const char *translation = do_lookup(state.pending_strokes, state.stroke_count);
|
||||
if (translation) {
|
||||
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)
|
||||
{
|
||||
uint32_t key_index = binding->param1;
|
||||
|
||||
if (key_index > 35) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
state.current_chord |= (1U << key_index);
|
||||
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);
|
||||
|
||||
return ZMK_BEHAVIOR_OPAQUE;
|
||||
|
|
@ -207,14 +206,9 @@ static int on_steno_binding_released(struct zmk_behavior_binding *binding,
|
|||
if (state.keys_held > 0) {
|
||||
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) {
|
||||
LOG_INF("steno all-up → process chord 0x%06X", state.current_chord);
|
||||
process_chord();
|
||||
}
|
||||
|
||||
return ZMK_BEHAVIOR_OPAQUE;
|
||||
}
|
||||
|
||||
|
|
@ -230,6 +224,10 @@ static int behavior_steno_init(const struct device *dev)
|
|||
steno_undo_init(&undo_history);
|
||||
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;
|
||||
if (dict_size > 4) {
|
||||
int ret;
|
||||
|
|
@ -247,6 +245,7 @@ static int behavior_steno_init(const struct device *dev)
|
|||
} else {
|
||||
LOG_WRN("No steno dict embedded");
|
||||
}
|
||||
#endif
|
||||
|
||||
LOG_INF("Steno engine initialized");
|
||||
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