diff --git a/CMakeLists.txt b/CMakeLists.txt index 5f9e57e..7d13eb2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,6 +17,9 @@ find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) 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}/src/events) 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\" ) +zephyr_nanopb_sources(app + KeyBorad/proto/keyboard.proto +) + target_sources(app PRIVATE src/main.c src/events/battery_status_event.c src/events/config_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_host_ack_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/hid_host_command_module.c src/modules/hid_tx_manager_module.c + src/modules/keyboard_proto.c src/modules/keyboard_module.c src/modules/led_state_module.c src/modules/mode_switch_module.c src/modules/qdec_module.c src/modules/time_manager_module.c + src/modules/usb_cdc_proto_module.c src/modules/usb_hid_module.c src/modules/ble_hid_module.c + src/modules/ble_gatt_proto_module.c src/ui/display_ui.c src/ui/fonts/ui_font_keyboard_small_18.c src/ui/fonts/ui_font_keyboard_time_48.c diff --git a/app.overlay b/app.overlay index 8b7a48f..5063ee8 100644 --- a/app.overlay +++ b/app.overlay @@ -36,6 +36,13 @@ }; }; +&zephyr_udc0 { + cdc_acm_uart0: cdc_acm_uart0 { + compatible = "zephyr,cdc-acm-uart"; + label = "new_kbd CDC ACM"; + }; +}; + &gpregret1 { status = "okay"; diff --git a/docs/firmware_proto_transport.md b/docs/firmware_proto_transport.md index 8fd7b6f..1d94684 100644 --- a/docs/firmware_proto_transport.md +++ b/docs/firmware_proto_transport.md @@ -32,11 +32,6 @@ Design notes: ## 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 Files updated in this step: @@ -59,3 +54,43 @@ Implemented behavior: - for consumer usages marked as function keys: - stop normal HID reporting - 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 diff --git a/inc/keyboard_proto.h b/inc/keyboard_proto.h new file mode 100644 index 0000000..b93aaf5 --- /dev/null +++ b/inc/keyboard_proto.h @@ -0,0 +1,64 @@ +#ifndef KEYBOARD_PROTO_H__ +#define KEYBOARD_PROTO_H__ + +#include +#include +#include + +#include +#include + +#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__ */ diff --git a/prj.conf b/prj.conf index cf0536c..510f71a 100644 --- a/prj.conf +++ b/prj.conf @@ -68,6 +68,11 @@ CONFIG_BT_DIS_PNP_PID=0x0001 CONFIG_USB_DEVICE_STACK_NEXT=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_COUNT=32 CONFIG_USBD_MAX_UDC_MSG=20 diff --git a/src/modules/keyboard_proto.c b/src/modules/keyboard_proto.c new file mode 100644 index 0000000..8f98b10 --- /dev/null +++ b/src/modules/keyboard_proto.c @@ -0,0 +1,375 @@ +#include "keyboard_proto.h" + +#include + +#include + +#include "display_theme_event.h" +#include "function_bitmap_event.h" +#include "time_manager.h" +#include "time_sync_event.h" + +#include +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); + } +}