Bridge from NINA-W102 (ESP32) to SAMD21 for Classic Bluetooth HID data

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.

I managed to send data from NINA to SAMD21, but sending data from SAMD21 to NINA does not work.

I created a simple example to test communication between NINA and SAMD21 via UART.

SAMD21 Code
void setup() {
  // enable NINA
  digitalWrite(NINA_RESETN, HIGH);

  SerialHCI.begin(115200);
  Serial.begin(19200);
}

void loop() {
  // Read data from NINA
  while (SerialHCI.available()) {
    Serial.print(SerialHCI.readString());
  }

  // Write data to NINA
  while (Serial.available() && SerialHCI.availableForWrite()) {
    String inString = Serial.readString();
    int stringSize = inString.length();
    char inCharArray[stringSize + 1];
    inString.toCharArray(inCharArray, stringSize + 1);
    SerialHCI.write((uint8_t*)inCharArray, stringSize);

    // Serial.println(inCharArray);
  }
}
NINA Code
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/uart.h"

#define BUF_SIZE 1024

void uart_task(void *arg)
{
    const char *tick_loop_text = "Infinite loop tick\n";
    const char *data_received_text = "Data received\n";

    uint8_t *dtmp = (uint8_t *)malloc(BUF_SIZE + 1);

    while (1)
    {

        memset(dtmp, 0x0, BUF_SIZE + 1);
        int len = uart_read_bytes(UART_NUM_1, dtmp, BUF_SIZE, pdMS_TO_TICKS(100));

        if (len > 0)
        {
            // write static text to SAMD21 for indicate recived data
            uart_write_bytes(UART_NUM_1, data_received_text, strlen(data_received_text));

            // write recived text to SAMD21
            uart_write_bytes(UART_NUM_1, dtmp, sizeof(dtmp));
        }

        // write tick loop text to SAMD21
        uart_write_bytes(UART_NUM_1, tick_loop_text, strlen(tick_loop_text));

        vTaskDelay(pdMS_TO_TICKS(2000));
    }
}

void app_main(void)
{
    const uart_config_t uart_config = {
        .baud_rate = 115200,
        .data_bits = UART_DATA_8_BITS,
        .parity = UART_PARITY_DISABLE,
        .stop_bits = UART_STOP_BITS_1,
        .flow_ctrl = UART_HW_FLOWCTRL_DISABLE};

    uart_param_config(UART_NUM_1, &uart_config);
    uart_set_pin(UART_NUM_1, 23, 22, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
    uart_driver_install(UART_NUM_1, 1024, 1024, 0, NULL, 0);

    xTaskCreate(uart_task, "uart_task", 2048, NULL, 10, NULL);
}

I send a text string from SAMD21 to NINA and attempt to send it back to SAMD21 to verify that the connection is functioning.
For communication with NINA, I use SerialHCI, which, according to the datasheet, is configured for communication with NINA.

Can someone suggest where the problem might be?

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.