prettyprint

2023年4月11日 星期二

[Raspberry Pi Pico W] BTstack: Ep 3. HID device, Convert a USB Cabled Keyboard to a Bluetooth Wireless Keyboard

本文章介紹在Raspberry Pi Pico W上使用tinyusb HID host與BTstack HID device將有線的USB keyboard轉成Bluetooth無線的keyborad。在desktop PC與cell phone上測試藍芽無線鍵盤輸入。

參考文件:

  1. Device Class Definition for Human Interface Devices (HID)
  2. HID Usage Tables FOR Universal Serial Bus (USB)
  3. HUMAN INTERFACE DEVICE PROFILE 1.1
  4. https://usb.org/
根據文件說明,由HID device輸入字後轉成HID input report輸出。
HID host 再將HID input report轉成字元輸出。

流程如上圖所示:

鍵盤輸入字元時,產生HID input report到Pico W usb host port。再將HID input report透過 Pico W bluetooth HID device輸出到 PC or cell phone HID host。

HID input report主要有8 bytes: {modifier, 0, keycode1, keycode2, keycode3, keycode4, keycode5, keycode6}。以wireshark擷取封包,如下圖所示。

輸入大寫W, HID input report為 {0x02, 0x00, 0x1a, 0x00, 0x00, 0x00, 0x00, 0x00}

測試結果請參閱下列成果影片。

程式碼主要修改於tinyusb hid_app.c example與btstack  hid_keyboard_demo.c。詳細程式碼請參閱文末說明,動態說明請參閱成果影片。

成果影片:


程式碼:
  • pico_bt_hid_keyboard.c
#include <stdio.h>
#include "pico/stdlib.h"

#include "bsp/board.h"
#include "tusb.h"
#include "pico/cyw43_arch.h"
#include "pico/util/queue.h"

queue_t hid_keyboard_report_queue;

extern void btstack_hid_kb_init();


int main()
{
    stdio_init_all();
    if (cyw43_arch_init()) {
        printf("cyw43_init_error\n");
        return 0;
    }

    btstack_hid_kb_init();

    queue_init_with_spinlock(&hid_keyboard_report_queue, sizeof(hid_keyboard_report_t), 10, spin_lock_claim_unused(true));
    board_init();
    if (!tuh_init(BOARD_TUH_RHPORT)) { 
        printf("tuh init\n");
        return 0;
    }  else {
        printf("tinyusb init successfully\n");
    }

    

    while(1) {
        // tinyusb host task
        tuh_task();
    }

    

    return 0;
}
  • hid_app.c -- 只有修改rocess_kbd_report function(tusb_hid.c)
 static void process_kbd_report(hid_keyboard_report_t const *report)
{
     queue_try_add(&hid_keyboard_report_queue, report);
     return;
}
  • btstack_hid_kb.c
 /* This file was modified from btstack hid_keyboard_demo.c example*/

/*
 * Copyright (C) 2014 BlueKitchen GmbH
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the copyright holders nor the names of
 *    contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 * 4. Any redistribution, use, or modification is done solely for
 *    personal benefit and not for any commercial purpose or for
 *    monetary gain.
 *
 * THIS SOFTWARE IS PROVIDED BY BLUEKITCHEN GMBH AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BLUEKITCHEN
 * GMBH OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
 * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
 * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 * Please inquire about commercial licensing options at 
 * contact@bluekitchen-gmbh.com
 *
 */

 
// *****************************************************************************
/* EXAMPLE_START(hid_keyboard_demo): HID Keyboard Classic
 *
 * @text This HID Device example demonstrates how to implement
 * an HID keyboard. Without a HAVE_BTSTACK_STDIN, a fixed demo text is sent
 * If HAVE_BTSTACK_STDIN is defined, you can type from the terminal
 */
// *****************************************************************************


#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>

#include "btstack.h"


#include "pico/util/queue.h"
extern queue_t hid_keyboard_report_queue;

// When not set to 0xffff, sniff and sniff subrating are enabled
static uint16_t host_max_latency = 1600;
static uint16_t host_min_timeout = 3200;

#define REPORT_ID 0x01

// close to USB HID Specification 1.1, Appendix B.1
const uint8_t hid_descriptor_keyboard[] = {

    0x05, 0x01,                    // Usage Page (Generic Desktop)
    0x09, 0x06,                    // Usage (Keyboard)
    0xa1, 0x01,                    // Collection (Application)

    // Report ID

    0x85, REPORT_ID,               // Report ID

    // Modifier byte (input)

    0x05, 0x07,                    //   Usage Page (Key Codes)
    0x75, 0x01,                    //   Report Size (1)
    0x95, 0x08,                    //   Report Count (8)
    0x05, 0x07,                    //   Usage Page (Key codes)
    0x19, 0xe0,                    //   Usage Minimum (Keyboard LeftControl)
    0x29, 0xe7,                    //   Usage Maxium (Keyboard Right GUI)
    0x15, 0x00,                    //   Logical Minimum (0)
    0x25, 0x01,                    //   Logical Maximum (1)
    0x81, 0x02,                    //   Input (Data, Variable, Absolute)

    // Reserved byte (input)

    0x75, 0x01,                    //   Report Size (1)
    0x95, 0x08,                    //   Report Count (8)
    0x81, 0x03,                    //   Input (Constant, Variable, Absolute)

    // LED report + padding (output)

    0x95, 0x05,                    //   Report Count (5)
    0x75, 0x01,                    //   Report Size (1)
    0x05, 0x08,                    //   Usage Page (LEDs)
    0x19, 0x01,                    //   Usage Minimum (Num Lock)
    0x29, 0x05,                    //   Usage Maxium (Kana)
    0x91, 0x02,                    //   Output (Data, Variable, Absolute)

    0x95, 0x01,                    //   Report Count (1)
    0x75, 0x03,                    //   Report Size (3)
    0x91, 0x03,                    //   Output (Constant, Variable, Absolute)

    // Keycodes (input)

    0x95, 0x06,                    //   Report Count (6)
    0x75, 0x08,                    //   Report Size (8)
    0x15, 0x00,                    //   Logical Minimum (0)
    0x25, 0xff,                    //   Logical Maximum (1)
    0x05, 0x07,                    //   Usage Page (Key codes)
    0x19, 0x00,                    //   Usage Minimum (Reserved (no event indicated))
    0x29, 0xff,                    //   Usage Maxium (Reserved)
    0x81, 0x00,                    //   Input (Data, Array)

    0xc0,                          // End collection  
};



// STATE

static uint8_t hid_service_buffer[300];
static uint8_t device_id_sdp_service_buffer[100];
static const char hid_device_name[] = "BTstack HID Keyboard";
static btstack_packet_callback_registration_t hci_event_callback_registration;
static uint16_t hid_cid;
static uint8_t hid_boot_device = 0;

// HID Report sending

static uint8_t                send_modifier;
static uint8_t                send_keycode;

static enum {
    APP_BOOTING,
    APP_NOT_CONNECTED,
    APP_CONNECTING,
    APP_CONNECTED
} app_state = APP_BOOTING;


//=======Code Modified   Begin=================
static void send_report(int modifier, int keycode){
    uint8_t report[] = { 0xa1, REPORT_ID, modifier, 0, keycode, 0, 0, 0, 0, 0};

    hid_device_send_interrupt_message(hid_cid, &report[0], sizeof(report));
}
#define TEXT_PERIOD_MS 20
static btstack_timer_source_t text_timer;



static void text_timer_handler(btstack_timer_source_t * ts){
    struct 
    {
        uint8_t modifier;   /**< Keyboard modifier (KEYBOARD_MODIFIER_* masks). */
        uint8_t reserved;   /**< Reserved for OEM use, always set to 0. */
        uint8_t keycode[6]; /**< Key codes of the currently pressed keys. */
    } report_q;

    if (queue_try_remove(&hid_keyboard_report_queue, &report_q)) {
        uint8_t report[] = { 0xa1, REPORT_ID, report_q.modifier, 0, 
                    report_q.keycode[0],report_q.keycode[1],report_q.keycode[2],report_q.keycode[3],report_q.keycode[4],report_q.keycode[5]};
        hid_device_send_interrupt_message(hid_cid, &report[0], sizeof(report));
    }

    hid_device_request_can_send_now_event(hid_cid);  
 }
//=======Code Modified   End===============
static void packet_handler(uint8_t packet_type, uint16_t channel, uint8_t * packet, uint16_t packet_size){ UNUSED(channel); UNUSED(packet_size); uint8_t status; switch (packet_type){ case HCI_EVENT_PACKET: switch (hci_event_packet_get_type(packet)){ case BTSTACK_EVENT_STATE: if (btstack_event_state_get_state(packet) != HCI_STATE_WORKING) return; app_state = APP_NOT_CONNECTED; break; case HCI_EVENT_USER_CONFIRMATION_REQUEST: // ssp: inform about user confirmation request log_info("SSP User Confirmation Request with numeric value '%06"PRIu32"'\n", hci_event_user_confirmation_request_get_numeric_value(packet)); log_info("SSP User Confirmation Auto accept\n"); break; case HCI_EVENT_HID_META: switch (hci_event_hid_meta_get_subevent_code(packet)){ case HID_SUBEVENT_CONNECTION_OPENED: status = hid_subevent_connection_opened_get_status(packet); if (status != ERROR_CODE_SUCCESS) { // outgoing connection failed printf("Connection failed, status 0x%x\n", status); app_state = APP_NOT_CONNECTED; hid_cid = 0; return; } app_state = APP_CONNECTED; hid_cid = hid_subevent_connection_opened_get_hid_cid(packet); btstack_run_loop_set_timer_handler(&text_timer, text_timer_handler); btstack_run_loop_set_timer(&text_timer, TEXT_PERIOD_MS); btstack_run_loop_add_timer(&text_timer); break; case HID_SUBEVENT_CONNECTION_CLOSED: printf("HID Disconnected\n"); app_state = APP_NOT_CONNECTED; hid_cid = 0; break; case HID_SUBEVENT_CAN_SEND_NOW: //send_report(send_modifier, send_keycode); btstack_run_loop_set_timer_handler(&text_timer, text_timer_handler); btstack_run_loop_set_timer(&text_timer, TEXT_PERIOD_MS); btstack_run_loop_add_timer(&text_timer); break; default: break; } break; default: break; } break; default: break; } } void btstack_hid_kb_init() { // allow to get found by inquiry gap_discoverable_control(1); // use Limited Discoverable Mode; Peripheral; Keyboard as CoD gap_set_class_of_device(0x2540); // allow for role switch in general and sniff mode gap_set_default_link_policy_settings( LM_LINK_POLICY_ENABLE_ROLE_SWITCH | LM_LINK_POLICY_ENABLE_SNIFF_MODE ); // allow for role switch on outgoing connections - this allow HID Host to become master when we re-connect to it gap_set_allow_role_switch(true); gap_set_local_name(hid_device_name); // L2CAP l2cap_init(); // SDP Server sdp_init(); memset(hid_service_buffer, 0, sizeof(hid_service_buffer)); uint8_t hid_virtual_cable = 0; uint8_t hid_remote_wake = 1; uint8_t hid_reconnect_initiate = 1; uint8_t hid_normally_connectable = 1; hid_sdp_record_t hid_params = { // hid sevice subclass 2540 Keyboard, hid counntry code 33 US 0x2540, 33, hid_virtual_cable, hid_remote_wake, hid_reconnect_initiate, hid_normally_connectable, hid_boot_device, host_max_latency, host_min_timeout, 3200, hid_descriptor_keyboard, sizeof(hid_descriptor_keyboard), hid_device_name }; hid_create_sdp_record(hid_service_buffer, 0x10001, &hid_params); printf("HID service record size: %u\n", de_get_len( hid_service_buffer)); sdp_register_service(hid_service_buffer); // See https://www.bluetooth.com/specifications/assigned-numbers/company-identifiers if you don't have a USB Vendor ID and need a Bluetooth Vendor ID // device info: BlueKitchen GmbH, product 1, version 1 device_id_create_sdp_record(device_id_sdp_service_buffer, 0x10003, DEVICE_ID_VENDOR_ID_SOURCE_BLUETOOTH, BLUETOOTH_COMPANY_ID_BLUEKITCHEN_GMBH, 1, 1); printf("Device ID SDP service record size: %u\n", de_get_len((uint8_t*)device_id_sdp_service_buffer)); sdp_register_service(device_id_sdp_service_buffer); // HID Device hid_device_init(hid_boot_device, sizeof(hid_descriptor_keyboard), hid_descriptor_keyboard); // register for HCI events hci_event_callback_registration.callback = &packet_handler; hci_add_event_handler(&hci_event_callback_registration); // register for HID events hid_device_register_packet_handler(&packet_handler); // turn on! hci_power_control(HCI_POWER_ON); } /* LISTING_END */ /* EXAMPLE_END */

  • CMakeLists.txt


# Generated Cmake Pico project file

cmake_minimum_required(VERSION 3.13)

set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)

# Initialise pico_sdk from installed location
# (note this can come from environment, CMake cache etc)
set(PICO_SDK_PATH "/home/duser/pico/pico-sdk")

set(PICO_BOARD pico_w CACHE STRING "Board type")

# Pull in Raspberry Pi Pico SDK (must be before project)
include(pico_sdk_import.cmake)

if (PICO_SDK_VERSION_STRING VERSION_LESS "1.4.0")
  message(FATAL_ERROR "Raspberry Pi Pico SDK version 1.4.0 (or later) required. Your version is ${PICO_SDK_VERSION_STRING}")
endif()

project(picow_bt_hid_keyboard C CXX ASM)

# Initialise the Raspberry Pi Pico SDK
pico_sdk_init()

# Add executable. Default name is the project name, version 0.1

add_executable(picow_bt_hid_keyboard 
      picow_bt_hid_keyboard.c 
      tusb_hid.c
      btstack_hid_kb.c)

pico_set_program_name(picow_bt_hid_keyboard "picow_bt_hid_keyboard")
pico_set_program_version(picow_bt_hid_keyboard "0.1")

pico_enable_stdio_uart(picow_bt_hid_keyboard 1)
pico_enable_stdio_usb(picow_bt_hid_keyboard 0)

# Add the standard library to the build
target_link_libraries(picow_bt_hid_keyboard
        pico_stdlib
        pico_util
        pico_cyw43_arch_none
        pico_btstack_cyw43
        pico_btstack_classic)

# Add the standard include files to the build
target_include_directories(picow_bt_hid_keyboard PRIVATE
  ${CMAKE_CURRENT_LIST_DIR}
  ${CMAKE_CURRENT_LIST_DIR}/.. # for our common lwipopts or any other standard includes, if required
)

# Add any user requested libraries   ----------
target_link_libraries(picow_bt_hid_keyboard
        tinyusb_host 
        tinyusb_board
        )

pico_add_extra_outputs(picow_bt_hid_keyboard)

沒有留言:

張貼留言