diff --git a/src/inflate.c b/src/inflate.c index 1b886ce..86ced5d 100644 --- a/src/inflate.c +++ b/src/inflate.c @@ -191,7 +191,8 @@ static int dynamic_tables(struct bitstream *s, static const uint8_t order[19] = { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 }; - uint8_t lengths[MAX_CODES]; + /* Static to keep embedded stack use minimal (single-context). */ + static uint8_t lengths[MAX_CODES]; uint32_t hlit, hdist, hclen; if (bits(s, 5, &hlit) < 0 || bits(s, 5, &hdist) < 0 || @@ -214,7 +215,7 @@ static int dynamic_tables(struct bitstream *s, lengths[order[i]] = (uint8_t)v; } - struct huffman clcode; + static struct huffman clcode; if (construct(&clcode, lengths, 19) < 0) { return -1; } @@ -297,7 +298,11 @@ int steno_inflate(const uint8_t *src, size_t src_len, dst_len += len; s.pos += len; } else if (type == 1 || type == 2) { - struct huffman lencode, distcode; + /* Static: ~1.2 KB combined — too large for the caller's + * stack on embedded threads. Decoder is single-context + * (one worker on the peripheral; host tests are + * single-threaded). */ + static struct huffman lencode, distcode; int ret = (type == 1) ? fixed_tables(&lencode, &distcode) : dynamic_tables(&s, &lencode, &distcode); if (ret < 0) { diff --git a/src/split_dict.c b/src/split_dict.c index 8589527..9d52d24 100644 --- a/src/split_dict.c +++ b/src/split_dict.c @@ -38,8 +38,7 @@ static uint8_t rx_buf[sizeof(struct steno_response_pkt) + SPLIT_DICT_MAX_TEXT]; static uint16_t rx_len; static uint8_t seq_counter; -/* Notify buffer (peripheral side) */ -static uint8_t notify_buf[sizeof(struct steno_response_pkt) + SPLIT_DICT_MAX_TEXT]; + /* Cache instance */ static struct split_cache dict_cache; @@ -66,7 +65,36 @@ static void make_resolve_key(uint32_t slot, uint8_t dict, uint32_t key[2]) key[1] = slot; } -/* --- GATT Write Callbacks (peripheral side handlers) --- */ +/* --- Peripheral side: request worker --- + * + * Requests arrive in the BT RX thread, whose stack is far too small to + * inflate 16 KB string blocks (and blocking it starves the link). The + * write callback only copies the packet and hands it to a dedicated + * work queue; the worker resolves the request and notifies the reply. + */ + +#if !IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL) && \ + !defined(CONFIG_STENO_DEBUG_NO_GATT) +#define STENO_DICT_PERIPHERAL 1 +#endif + +#ifdef STENO_DICT_PERIPHERAL + +#define STENO_REQ_MAX 16 + +struct steno_req_slot { + struct bt_conn *conn; /* reference held until reply sent */ + const struct bt_gatt_attr *attr; /* static service attribute */ + uint8_t buf[STENO_REQ_MAX]; + uint16_t len; +}; + +static uint8_t notify_buf[sizeof(struct steno_response_pkt) + SPLIT_DICT_MAX_TEXT]; +static struct steno_req_slot req_slot; +static atomic_t req_busy; + +K_THREAD_STACK_DEFINE(steno_workq_stack, 4096); +static struct k_work_q steno_workq; static void send_response(struct bt_conn *conn, const struct bt_gatt_attr *attr, uint8_t seq, int ret, const char *text) @@ -134,28 +162,64 @@ static ssize_t handle_resolve(struct bt_conn *conn, return len; } +static void req_work_handler(struct k_work *work) +{ + ARG_UNUSED(work); + + struct bt_conn *conn = req_slot.conn; + const struct bt_gatt_attr *attr = req_slot.attr; + const uint8_t *bytes = req_slot.buf; + uint16_t len = req_slot.len; + + switch (bytes[0]) { + case STENO_MSG_GET_STRING: + handle_get_string(conn, attr, bytes, len); + break; + case STENO_MSG_RESOLVE: + handle_resolve(conn, attr, bytes, len); + break; + default: + LOG_WRN("Unknown msg type: 0x%02x", bytes[0]); + send_response(conn, attr, len >= 2 ? bytes[1] : 0, -EIO, NULL); + break; + } + + bt_conn_unref(conn); + req_slot.conn = NULL; + atomic_clear(&req_busy); +} + +static K_WORK_DEFINE(req_work, req_work_handler); + static ssize_t dict_req_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 uint8_t *bytes = buf; - - if (len < 1) { + if (len < 1 || len > STENO_REQ_MAX) { return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); } - switch (bytes[0]) { - case STENO_MSG_GET_STRING: - return handle_get_string(conn, attr, buf, len); - case STENO_MSG_RESOLVE: - return handle_resolve(conn, attr, buf, len); - default: - LOG_WRN("Unknown msg type: 0x%02x", bytes[0]); - return BT_GATT_ERR(BT_ATT_ERR_NOT_SUPPORTED); + /* One request in flight: the central blocks per request, so a + * second arrival means it already timed out — drop this one and + * let it retry. */ + if (atomic_set(&req_busy, 1) != 0) { + LOG_WRN("Request dropped: worker busy"); + return len; } + + req_slot.conn = bt_conn_ref(conn); + req_slot.attr = attr; + memcpy(req_slot.buf, buf, len); + req_slot.len = len; + + k_work_submit_to_queue(&steno_workq, &req_work); + + return len; } +#endif /* STENO_DICT_PERIPHERAL */ + /* --- Notification callback (central side) --- */ static uint8_t notify_cb(struct bt_conn *conn, @@ -188,7 +252,7 @@ static uint8_t notify_cb(struct bt_conn *conn, /* --- GATT Service Definition --- */ -#ifndef CONFIG_STENO_DEBUG_NO_GATT +#ifdef STENO_DICT_PERIPHERAL BT_GATT_SERVICE_DEFINE(steno_dict_svc, BT_GATT_PRIMARY_SERVICE(STENO_UUID_SERVICE), @@ -201,7 +265,7 @@ BT_GATT_SERVICE_DEFINE(steno_dict_svc, NULL, dict_req_write_cb, NULL), BT_GATT_CCC(NULL, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE), ); -#endif /* !CONFIG_STENO_DEBUG_NO_GATT */ +#endif /* STENO_DICT_PERIPHERAL */ /* --- Central-side client: connection tracking + GATT discovery --- */ @@ -500,6 +564,13 @@ int split_dict_init(void) split_cache_init(&dict_cache); seq_counter = 0; +#ifdef STENO_DICT_PERIPHERAL + k_work_queue_start(&steno_workq, steno_workq_stack, + K_THREAD_STACK_SIZEOF(steno_workq_stack), + K_PRIO_PREEMPT(10), NULL); + k_thread_name_set(&steno_workq.thread, "steno_dict"); +#endif + LOG_INF("Split dict initialized (protocol v4)"); return 0; }