Hello everyone!
I want to create an HID proxy based on an Arduino Nano 33 IoT board. The idea is to connect to a keyboard via Classic Bluetooth, receive its HID data, and send that data over USB—essentially mimicking a regular USB keyboard (rather than a Bluetooth keyboard).
I found out that the u-blox NINA-W102 module, by default, operates in Bluetooth Low Energy mode, and Arduino IDE development only supports that mode. However, I need Classic Bluetooth in HID Host mode so that the module can connect to the keyboard and receive data directly.
I studied how to flash the NINA-W102 module (found information in this topic) so I can use it at a low level via the ESP-IDF framework. I managed to write a test firmware where NINA connects to a Classic Bluetooth keyboard and receives data.
Demo example for work with classic Blutooth
#include <inttypes.h>
#include "nvs.h"
#include "nvs_flash.h"
#include "esp_system.h"
#include "esp_log.h"
#include "esp_bt.h"
#include "esp_bt_main.h"
#include "esp_bt_device.h"
#include "esp_gap_bt_api.h"
#include "esp_hidh_api.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#define GAP_TAG "GAP"
#define HID_TAG "HID"
static char *bda2str(esp_bd_addr_t bda, char *str, size_t size)
{
if (bda == NULL || str == NULL || size < 18)
{
return NULL;
}
uint8_t *p = bda;
sprintf(str, "%02x:%02x:%02x:%02x:%02x:%02x",
p[0], p[1], p[2], p[3], p[4], p[5]);
return str;
}
static void bt_app_gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *param)
{
switch (event)
{
case ESP_BT_GAP_DISC_RES_EVT:
{
char bda_str[18];
ESP_LOGI(GAP_TAG, "Device found: %s", bda2str(param->disc_res.bda, bda_str, 18));
esp_bt_gap_cancel_discovery();
esp_bt_hid_host_connect(param->disc_res.bda);
break;
}
case ESP_BT_GAP_ACL_CONN_CMPL_STAT_EVT:
{
ESP_LOGI(GAP_TAG, "Connected");
break;
}
case ESP_BT_GAP_ACL_DISCONN_CMPL_STAT_EVT:
{
ESP_LOGI(GAP_TAG, "Disconnect....");
break;
}
case ESP_BT_GAP_AUTH_CMPL_EVT:
{
if (param->auth_cmpl.stat == ESP_BT_STATUS_SUCCESS)
{
ESP_LOGI(GAP_TAG, "Pairing success, paired device: %s", param->auth_cmpl.device_name);
}
else
{
ESP_LOGE(GAP_TAG, "Pairing failed, status=0x%x", param->auth_cmpl.stat);
}
break;
}
default:
{
ESP_LOGI(GAP_TAG, "event: %d", event);
break;
}
}
}
static void hidh_callback(esp_hidh_cb_event_t event, esp_hidh_cb_param_t *param)
{
switch (event)
{
case ESP_HIDH_OPEN_EVT:
{
ESP_LOGI(HID_TAG, "Keyboard connected: %x", param->open.conn_status);
break;
}
case ESP_HIDH_CLOSE_EVT:
{
ESP_LOGW(HID_TAG, "Keyboard disconnected");
break;
}
case ESP_HIDH_DATA_IND_EVT:
{
// need pass this data to SAMD21
ESP_LOGI(HID_TAG, "HID data (len=%d):", param->data_ind.len);
ESP_LOG_BUFFER_HEX(HID_TAG, param->data_ind.data, param->data_ind.len);
break;
}
default:
ESP_LOGI(HID_TAG, "Unhandled HIDH event: %d", event);
break;
}
}
static void bt_app_gap_start_up(void)
{
esp_bt_gap_register_callback(bt_app_gap_cb);
esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_NON_DISCOVERABLE);
esp_bt_gap_start_discovery(ESP_BT_INQ_MODE_LIMITED_INQUIRY, 10, 0);
}
void app_main(void)
{
// need for test pairing
nvs_flash_erase();
char bda_str[18] = {0};
/* Initialize NVS — it is used to store PHY calibration data and save key-value pairs in flash memory*/
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
{
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_BLE));
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
if ((ret = esp_bt_controller_init(&bt_cfg)) != ESP_OK)
{
ESP_LOGE(GAP_TAG, "%s initialize controller failed: %s", __func__, esp_err_to_name(ret));
return;
}
if ((ret = esp_bt_controller_enable(ESP_BT_MODE_CLASSIC_BT)) != ESP_OK)
{
ESP_LOGE(GAP_TAG, "%s enable controller failed: %s", __func__, esp_err_to_name(ret));
return;
}
esp_bluedroid_config_t bluedroid_cfg = BT_BLUEDROID_INIT_CONFIG_DEFAULT();
if ((ret = esp_bluedroid_init_with_cfg(&bluedroid_cfg)) != ESP_OK)
{
ESP_LOGE(GAP_TAG, "%s initialize bluedroid failed: %s", __func__, esp_err_to_name(ret));
return;
}
if ((ret = esp_bluedroid_enable()) != ESP_OK)
{
ESP_LOGE(GAP_TAG, "%s enable bluedroid failed: %s", __func__, esp_err_to_name(ret));
return;
}
if ((ret = esp_bt_hid_host_init()) != ESP_OK)
{
ESP_LOGE(GAP_TAG, "%s init hid host failed: %s", __func__, esp_err_to_name(ret));
return;
}
ESP_LOGI(GAP_TAG, "Own address:[%s]", bda2str((uint8_t *)esp_bt_dev_get_address(), bda_str, sizeof(bda_str)));
bt_app_gap_start_up();
esp_bt_hid_host_register_callback(hidh_callback);
}
Now, I need to transfer that data from NINA (ESP32) to the SAMD21 microcontroller on the board, and then forward it via USB.
My question: how can I properly implement this on the ESP-IDF side?
I see something like SerialNina, but I’m not sure how to send data to SerialNina in ESP-IDF code. Are there other ways? Which direction should I be looking into, and what resources or documentation should I read?
P.S. This is my first experience with Arduino and embedded development in general, so I may be mixing up some terminology or missing details.