Add firmware nanopb protocol core

This commit is contained in:
2026-04-11 11:56:45 +08:00
parent f753a7f883
commit 1b2fe79b5d
6 changed files with 503 additions and 5 deletions

View File

@@ -17,6 +17,9 @@ find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(new_kbd) project(new_kbd)
list(APPEND CMAKE_MODULE_PATH ${ZEPHYR_BASE}/modules/nanopb)
include(nanopb)
zephyr_include_directories(${CMAKE_CURRENT_SOURCE_DIR}/inc) zephyr_include_directories(${CMAKE_CURRENT_SOURCE_DIR}/inc)
zephyr_include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src/events) zephyr_include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src/events)
zephyr_include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src/ui) zephyr_include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src/ui)
@@ -33,11 +36,17 @@ target_compile_definitions(app PRIVATE
APP_HID_KEYMAP_DEF_PATH=\"hid_keymap_def.h\" APP_HID_KEYMAP_DEF_PATH=\"hid_keymap_def.h\"
) )
zephyr_nanopb_sources(app
KeyBorad/proto/keyboard.proto
)
target_sources(app PRIVATE target_sources(app PRIVATE
src/main.c src/main.c
src/events/battery_status_event.c src/events/battery_status_event.c
src/events/config_event.c src/events/config_event.c
src/events/display_theme_event.c src/events/display_theme_event.c
src/events/function_bitmap_event.c
src/events/function_key_event.c
src/events/hid_boot_event.c src/events/hid_boot_event.c
src/events/hid_host_ack_event.c src/events/hid_host_ack_event.c
src/events/hid_host_command_error_event.c src/events/hid_host_command_error_event.c
@@ -59,13 +68,16 @@ target_sources(app PRIVATE
src/modules/display_module.c src/modules/display_module.c
src/modules/hid_host_command_module.c src/modules/hid_host_command_module.c
src/modules/hid_tx_manager_module.c src/modules/hid_tx_manager_module.c
src/modules/keyboard_proto.c
src/modules/keyboard_module.c src/modules/keyboard_module.c
src/modules/led_state_module.c src/modules/led_state_module.c
src/modules/mode_switch_module.c src/modules/mode_switch_module.c
src/modules/qdec_module.c src/modules/qdec_module.c
src/modules/time_manager_module.c src/modules/time_manager_module.c
src/modules/usb_cdc_proto_module.c
src/modules/usb_hid_module.c src/modules/usb_hid_module.c
src/modules/ble_hid_module.c src/modules/ble_hid_module.c
src/modules/ble_gatt_proto_module.c
src/ui/display_ui.c src/ui/display_ui.c
src/ui/fonts/ui_font_keyboard_small_18.c src/ui/fonts/ui_font_keyboard_small_18.c
src/ui/fonts/ui_font_keyboard_time_48.c src/ui/fonts/ui_font_keyboard_time_48.c

View File

@@ -36,6 +36,13 @@
}; };
}; };
&zephyr_udc0 {
cdc_acm_uart0: cdc_acm_uart0 {
compatible = "zephyr,cdc-acm-uart";
label = "new_kbd CDC ACM";
};
};
&gpregret1 { &gpregret1 {
status = "okay"; status = "okay";

View File

@@ -32,11 +32,6 @@ Design notes:
## Planned next nodes ## Planned next nodes
- keyboard split logic uses `function_bitmap_event`
- CDC transport module
- GATT transport module
- nanopb integration and generated protocol code
### Node 2: keyboard split point ### Node 2: keyboard split point
Files updated in this step: Files updated in this step:
@@ -59,3 +54,43 @@ Implemented behavior:
- for consumer usages marked as function keys: - for consumer usages marked as function keys:
- stop normal HID reporting - stop normal HID reporting
- emit `function_key_event` - emit `function_key_event`
### Node 3: nanopb build integration and protocol core
Files updated in this step:
- `CMakeLists.txt`
- `prj.conf`
- `app.overlay`
- `inc/keyboard_proto.h`
- `src/modules/keyboard_proto.c`
Design notes:
- use NCS built-in nanopb instead of a hand-written protobuf runtime
- generate protocol code from the shared `.proto`
- keep one firmware-side protocol helper that:
- encodes and decodes `CdcPacketBody`
- encodes and decodes `CdcFrame`
- validates checksum
- routes host commands back into existing modules
Implemented behavior:
- enable CDC ACM class in Zephyr USB device-next stack
- add a CDC ACM UART node in devicetree
- wire `zephyr_nanopb_sources()` into the build
- add protocol helper functions for:
- body encode/decode
- CDC frame encode
- CDC frame stream extraction
- hello response build
- ack/error build
- function key event build
- LED state build
- host command dispatch
## Planned next nodes
- CDC transport module
- GATT transport module

64
inc/keyboard_proto.h Normal file
View File

@@ -0,0 +1,64 @@
#ifndef KEYBOARD_PROTO_H__
#define KEYBOARD_PROTO_H__
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <pb_decode.h>
#include <pb_encode.h>
#include "KeyBorad/proto/keyboard.pb.h"
#define KEYBOARD_PROTO_MAX_BODY_SIZE 64U
#define KEYBOARD_PROTO_MAX_FRAME_SIZE 128U
#define KEYBOARD_PROTO_FUNCTION_BITMAP_SIZE 29U
enum keyboard_proto_transport {
KEYBOARD_PROTO_TRANSPORT_CDC = 0,
KEYBOARD_PROTO_TRANSPORT_GATT,
};
typedef bool (*keyboard_proto_send_body_fn)(
const struct keyboard_cdc_CdcPacketBody *body,
void *user_data);
bool keyboard_proto_encode_body(
const struct keyboard_cdc_CdcPacketBody *body,
uint8_t *buffer,
size_t buffer_size,
size_t *encoded_size);
bool keyboard_proto_decode_body(
const uint8_t *buffer,
size_t buffer_size,
struct keyboard_cdc_CdcPacketBody *body);
bool keyboard_proto_encode_cdc_frame(
uint32_t packet_type,
const struct keyboard_cdc_CdcPacketBody *body,
uint8_t *buffer,
size_t buffer_size,
size_t *encoded_size);
bool keyboard_proto_try_take_cdc_frame(
uint8_t *buffer,
size_t *buffer_size,
struct keyboard_cdc_CdcFrame *frame);
bool keyboard_proto_build_function_key_event_body(
uint16_t usage,
bool pressed,
struct keyboard_cdc_CdcPacketBody *body);
bool keyboard_proto_build_led_state_body(
uint8_t led_mask,
struct keyboard_cdc_CdcPacketBody *body);
bool keyboard_proto_handle_host_body(
const struct keyboard_cdc_CdcPacketBody *body,
enum keyboard_proto_transport transport,
keyboard_proto_send_body_fn send_fn,
void *user_data);
#endif /* KEYBOARD_PROTO_H__ */

View File

@@ -68,6 +68,11 @@ CONFIG_BT_DIS_PNP_PID=0x0001
CONFIG_USB_DEVICE_STACK_NEXT=y CONFIG_USB_DEVICE_STACK_NEXT=y
CONFIG_USBD_HID_SUPPORT=y CONFIG_USBD_HID_SUPPORT=y
CONFIG_SERIAL=y
CONFIG_UART_INTERRUPT_DRIVEN=y
CONFIG_UART_LINE_CTRL=y
CONFIG_USBD_CDC_ACM_CLASS=y
CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT=n
CONFIG_UDC_BUF_POOL_SIZE=8192 CONFIG_UDC_BUF_POOL_SIZE=8192
CONFIG_UDC_BUF_COUNT=32 CONFIG_UDC_BUF_COUNT=32
CONFIG_USBD_MAX_UDC_MSG=20 CONFIG_USBD_MAX_UDC_MSG=20

View File

@@ -0,0 +1,375 @@
#include "keyboard_proto.h"
#include <string.h>
#include <zephyr/sys/byteorder.h>
#include "display_theme_event.h"
#include "function_bitmap_event.h"
#include "time_manager.h"
#include "time_sync_event.h"
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(keyboard_proto, LOG_LEVEL_INF);
#define KB_PROTO_BODY_TAG_HELLO_REQ 1U
#define KB_PROTO_BODY_TAG_HELLO_RSP 2U
#define KB_PROTO_BODY_TAG_BITMAP 3U
#define KB_PROTO_BODY_TAG_FUNCTION_KEY_EVENT 4U
#define KB_PROTO_BODY_TAG_LED_STATE 5U
#define KB_PROTO_BODY_TAG_TIME_SYNC 6U
#define KB_PROTO_BODY_TAG_THEME_RGB 7U
#define KB_PROTO_BODY_TAG_ACK 8U
#define KB_PROTO_BODY_TAG_ERROR 9U
#define KB_PROTO_PACKET_HELLO_REQ 0x01U
#define KB_PROTO_PACKET_HELLO_RSP 0x02U
#define KB_PROTO_PACKET_BITMAP 0x10U
#define KB_PROTO_PACKET_FUNCTION_KEY_EVENT 0x20U
#define KB_PROTO_PACKET_LED_STATE 0x21U
#define KB_PROTO_PACKET_TIME_SYNC 0x30U
#define KB_PROTO_PACKET_THEME_RGB 0x31U
#define KB_PROTO_PACKET_ACK 0x7EU
#define KB_PROTO_PACKET_ERROR 0x7FU
#define KB_PROTO_ERROR_UNKNOWN_TYPE 0x01U
#define KB_PROTO_ERROR_INVALID_LENGTH 0x02U
#define KB_PROTO_ERROR_INVALID_PARAM 0x03U
#define KB_PROTO_ERROR_NOT_READY 0x04U
static uint8_t keyboard_proto_calc_checksum(const uint8_t *data, size_t len)
{
uint8_t checksum = 0U;
for (size_t i = 0; i < len; ++i) {
checksum ^= data[i];
}
return checksum;
}
static bool keyboard_proto_fill_payload(
pb_bytes_array_t *dest,
size_t max_size,
const uint8_t *src,
size_t src_len)
{
if ((dest == NULL) || (src == NULL) || (src_len > max_size)) {
return false;
}
dest->size = src_len;
memcpy(dest->bytes, src, src_len);
return true;
}
bool keyboard_proto_encode_body(
const struct keyboard_cdc_CdcPacketBody *body,
uint8_t *buffer,
size_t buffer_size,
size_t *encoded_size)
{
pb_ostream_t stream;
if ((body == NULL) || (buffer == NULL) || (encoded_size == NULL)) {
return false;
}
stream = pb_ostream_from_buffer(buffer, buffer_size);
if (!pb_encode(&stream, keyboard_cdc_CdcPacketBody_fields, body)) {
LOG_WRN("pb_encode body failed: %s", PB_GET_ERROR(&stream));
return false;
}
*encoded_size = stream.bytes_written;
return true;
}
bool keyboard_proto_decode_body(
const uint8_t *buffer,
size_t buffer_size,
struct keyboard_cdc_CdcPacketBody *body)
{
pb_istream_t stream;
if ((buffer == NULL) || (body == NULL)) {
return false;
}
*body = keyboard_cdc_CdcPacketBody_init_zero;
stream = pb_istream_from_buffer(buffer, buffer_size);
if (!pb_decode(&stream, keyboard_cdc_CdcPacketBody_fields, body)) {
return false;
}
return stream.bytes_left == 0U;
}
bool keyboard_proto_encode_cdc_frame(
uint32_t packet_type,
const struct keyboard_cdc_CdcPacketBody *body,
uint8_t *buffer,
size_t buffer_size,
size_t *encoded_size)
{
struct keyboard_cdc_CdcFrame frame = keyboard_cdc_CdcFrame_init_zero;
uint8_t payload[KEYBOARD_PROTO_MAX_BODY_SIZE];
size_t payload_size = 0U;
pb_ostream_t stream;
uint8_t checksum_bytes[4U + KEYBOARD_PROTO_MAX_BODY_SIZE];
if ((buffer == NULL) || (encoded_size == NULL) || (body == NULL)) {
return false;
}
if (!keyboard_proto_encode_body(body, payload, sizeof(payload), &payload_size)) {
return false;
}
frame.head1 = 0xAAU;
frame.head2 = 0x55U;
frame.payload_length = payload_size;
frame.type = (keyboard_cdc_CdcPacketType)packet_type;
if (!keyboard_proto_fill_payload(&frame.payload, KEYBOARD_PROTO_MAX_BODY_SIZE,
payload, payload_size)) {
return false;
}
checksum_bytes[0] = (uint8_t)frame.head1;
checksum_bytes[1] = (uint8_t)frame.head2;
checksum_bytes[2] = (uint8_t)frame.payload_length;
checksum_bytes[3] = (uint8_t)packet_type;
memcpy(&checksum_bytes[4], payload, payload_size);
frame.checksum = keyboard_proto_calc_checksum(checksum_bytes,
4U + payload_size);
stream = pb_ostream_from_buffer(buffer, buffer_size);
if (!pb_encode(&stream, keyboard_cdc_CdcFrame_fields, &frame)) {
LOG_WRN("pb_encode frame failed: %s", PB_GET_ERROR(&stream));
return false;
}
*encoded_size = stream.bytes_written;
return true;
}
static bool keyboard_proto_validate_frame(const struct keyboard_cdc_CdcFrame *frame)
{
uint8_t checksum_bytes[4U + KEYBOARD_PROTO_MAX_BODY_SIZE];
uint8_t checksum;
if ((frame == NULL) ||
(frame->head1 != 0xAAU) ||
(frame->head2 != 0x55U) ||
(frame->payload.size != frame->payload_length) ||
(frame->payload.size > KEYBOARD_PROTO_MAX_BODY_SIZE)) {
return false;
}
checksum_bytes[0] = (uint8_t)frame->head1;
checksum_bytes[1] = (uint8_t)frame->head2;
checksum_bytes[2] = (uint8_t)frame->payload_length;
checksum_bytes[3] = (uint8_t)frame->type;
memcpy(&checksum_bytes[4], frame->payload.bytes, frame->payload.size);
checksum = keyboard_proto_calc_checksum(checksum_bytes,
4U + frame->payload.size);
return checksum == (uint8_t)frame->checksum;
}
bool keyboard_proto_try_take_cdc_frame(
uint8_t *buffer,
size_t *buffer_size,
struct keyboard_cdc_CdcFrame *frame)
{
for (size_t candidate_len = 1U; candidate_len <= *buffer_size; ++candidate_len) {
struct keyboard_cdc_CdcFrame candidate = keyboard_cdc_CdcFrame_init_zero;
pb_istream_t stream = pb_istream_from_buffer(buffer, candidate_len);
if (!pb_decode(&stream, keyboard_cdc_CdcFrame_fields, &candidate)) {
continue;
}
if ((stream.bytes_left != 0U) ||
!keyboard_proto_validate_frame(&candidate)) {
continue;
}
*frame = candidate;
memmove(buffer, buffer + candidate_len, *buffer_size - candidate_len);
*buffer_size -= candidate_len;
return true;
}
return false;
}
static bool keyboard_proto_build_hello_rsp_body(
struct keyboard_cdc_CdcPacketBody *body)
{
*body = keyboard_cdc_CdcPacketBody_init_zero;
body->which_body = KB_PROTO_BODY_TAG_HELLO_RSP;
body->body.hello_rsp.protocol_version = 1U;
body->body.hello_rsp.vendor_id = 0x1209U;
body->body.hello_rsp.product_id = 0x0001U;
body->body.hello_rsp.firmware_major = 1U;
body->body.hello_rsp.firmware_minor = 0U;
body->body.hello_rsp.capability_flags = BIT(0) | BIT(1) | BIT(2) | BIT(3);
return true;
}
static bool keyboard_proto_build_ack_body(
uint32_t acked_type,
struct keyboard_cdc_CdcPacketBody *body)
{
*body = keyboard_cdc_CdcPacketBody_init_zero;
body->which_body = KB_PROTO_BODY_TAG_ACK;
body->body.ack.acked_type = acked_type;
return true;
}
static bool keyboard_proto_build_error_body(
uint32_t error_type,
uint32_t error_code,
struct keyboard_cdc_CdcPacketBody *body)
{
*body = keyboard_cdc_CdcPacketBody_init_zero;
body->which_body = KB_PROTO_BODY_TAG_ERROR;
body->body.error.error_type = error_type;
body->body.error.error_code = (keyboard_cdc_ErrorCode)error_code;
return true;
}
bool keyboard_proto_build_function_key_event_body(
uint16_t usage,
bool pressed,
struct keyboard_cdc_CdcPacketBody *body)
{
if (body == NULL) {
return false;
}
*body = keyboard_cdc_CdcPacketBody_init_zero;
body->which_body = KB_PROTO_BODY_TAG_FUNCTION_KEY_EVENT;
body->body.function_key_event.usage = usage;
body->body.function_key_event.action = pressed ? 1U : 0U;
return true;
}
bool keyboard_proto_build_led_state_body(
uint8_t led_mask,
struct keyboard_cdc_CdcPacketBody *body)
{
if (body == NULL) {
return false;
}
*body = keyboard_cdc_CdcPacketBody_init_zero;
body->which_body = KB_PROTO_BODY_TAG_LED_STATE;
body->body.led_state.led_mask = led_mask;
return true;
}
static bool keyboard_proto_handle_bitmap(
const struct keyboard_cdc_CdcPacketBody *body,
keyboard_proto_send_body_fn send_fn,
void *user_data)
{
struct keyboard_cdc_CdcPacketBody response;
if (body->body.bitmap.usage_bitmap.size != KEYBOARD_PROTO_FUNCTION_BITMAP_SIZE) {
keyboard_proto_build_error_body(KB_PROTO_PACKET_BITMAP,
KB_PROTO_ERROR_INVALID_LENGTH,
&response);
return send_fn(&response, user_data);
}
function_bitmap_event_submit(body->body.bitmap.usage_bitmap.bytes,
body->body.bitmap.usage_bitmap.size);
keyboard_proto_build_ack_body(KB_PROTO_PACKET_BITMAP, &response);
return send_fn(&response, user_data);
}
static bool keyboard_proto_handle_time_sync(
const struct keyboard_cdc_CdcPacketBody *body,
enum keyboard_proto_transport transport,
keyboard_proto_send_body_fn send_fn,
void *user_data)
{
struct time_sync_update update;
struct keyboard_cdc_CdcPacketBody response;
if (!time_manager_is_ready()) {
keyboard_proto_build_error_body(KB_PROTO_PACKET_TIME_SYNC,
KB_PROTO_ERROR_NOT_READY,
&response);
return send_fn(&response, user_data);
}
if (body->body.time_sync.utc_ms == 0U) {
keyboard_proto_build_error_body(KB_PROTO_PACKET_TIME_SYNC,
KB_PROTO_ERROR_INVALID_PARAM,
&response);
return send_fn(&response, user_data);
}
update.utc_ms = body->body.time_sync.utc_ms;
update.timezone_min = (int16_t)body->body.time_sync.timezone_min;
update.accuracy_ms = body->body.time_sync.accuracy_ms;
update.source = (transport == KEYBOARD_PROTO_TRANSPORT_CDC) ?
TIME_SYNC_SOURCE_USB : TIME_SYNC_SOURCE_BLE;
time_sync_event_submit(&update);
keyboard_proto_build_ack_body(KB_PROTO_PACKET_TIME_SYNC, &response);
return send_fn(&response, user_data);
}
static bool keyboard_proto_handle_theme_rgb(
const struct keyboard_cdc_CdcPacketBody *body,
keyboard_proto_send_body_fn send_fn,
void *user_data)
{
struct keyboard_cdc_CdcPacketBody response;
display_theme_event_submit((uint8_t)body->body.theme_rgb.red,
(uint8_t)body->body.theme_rgb.green,
(uint8_t)body->body.theme_rgb.blue);
keyboard_proto_build_ack_body(KB_PROTO_PACKET_THEME_RGB, &response);
return send_fn(&response, user_data);
}
bool keyboard_proto_handle_host_body(
const struct keyboard_cdc_CdcPacketBody *body,
enum keyboard_proto_transport transport,
keyboard_proto_send_body_fn send_fn,
void *user_data)
{
struct keyboard_cdc_CdcPacketBody response;
if ((body == NULL) || (send_fn == NULL)) {
return false;
}
switch (body->which_body) {
case KB_PROTO_BODY_TAG_HELLO_REQ:
keyboard_proto_build_hello_rsp_body(&response);
return send_fn(&response, user_data);
case KB_PROTO_BODY_TAG_BITMAP:
return keyboard_proto_handle_bitmap(body, send_fn, user_data);
case KB_PROTO_BODY_TAG_TIME_SYNC:
return keyboard_proto_handle_time_sync(body, transport, send_fn, user_data);
case KB_PROTO_BODY_TAG_THEME_RGB:
return keyboard_proto_handle_theme_rgb(body, send_fn, user_data);
default:
keyboard_proto_build_error_body(KB_PROTO_PACKET_ERROR,
KB_PROTO_ERROR_UNKNOWN_TYPE,
&response);
return send_fn(&response, user_data);
}
}