Need help emulating a keyboard exactly using ESP32 S2 mini.

haideralipunjabi
Posts: 2
Joined: Sun May 11, 2025 4:21 pm

Need help emulating a keyboard exactly using ESP32 S2 mini.

Postby haideralipunjabi » Sun May 11, 2025 4:29 pm

I have a KVM Switch that has a hotkey to switch between devices. The hotkey works when we press Ctrl then Ctrl and then 1 (or 2,3,4) on a keyboard.

I am not that experienced with ESP IDF but I was able to modify the examples in the repo to something that can press they keys like I want.

Here's my code:
https://gist.github.com/haideralipunjab ... 4a0be8c8c9

The issue is this is not able to trigger the hotkey. I suspect that the Switch doesn't think the connected device is a Keyboard. I did try to modify the Product ID / Vendor ID to one of the keyboards that worked, but that didn't work as well.

The keyboards that worked:
Logitech MX Keys
A random mini keyboard mouse combo (I used the Product ID / Vendor ID from this)
Flipper Zero's USB Keyboard app & Bad USB Script. (I am surprised this work).

I am unsure what am I doing wrong.

igi540
Espressif staff
Espressif staff
Posts: 7
Joined: Thu Jan 23, 2025 1:00 pm

Re: Need help emulating a keyboard exactly using ESP32 S2 mini.

Postby igi540 » Tue May 13, 2025 4:03 pm

Hi haideralipunjabi,
If your KVM switch doesn't respond to the hotkey from the ESP32, the issue is most likely how Ctrl is being sent – it must go in the modifier byte, not as a regular key. You also need to follow the correct press and release sequence.

Here's a working example for the hotkey Ctrl + Ctrl + 2 that should be recognized even by picky KVM switches:

Code: Select all

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "tinyusb.h"
#include "class/hid/hid_device.h"
#include "esp_log.h"

#define TAG "KVM_Trigger"

// --- HID Report descriptor ---
const uint8_t hid_report_descriptor[] = {
    TUD_HID_REPORT_DESC_KEYBOARD(HID_REPORT_ID(HID_ITF_PROTOCOL_KEYBOARD)),
};

// --- String descriptors ---
const char *hid_string_descriptor[] = {
    (char[]){0x09, 0x04}, // Language: English (0x0409)
    "ESP32-S3",           // Manufacturer
    "Virtual Keyboard",   // Product name
    "123456",             // Serial number
    "Keyboard Interface", // Interface description
};

// --- USB Configuration descriptor ---
#define TUSB_DESC_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_HID_DESC_LEN)
const uint8_t hid_configuration_descriptor[] = {
    TUD_CONFIG_DESCRIPTOR(1, 1, 0, TUSB_DESC_TOTAL_LEN, TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100),
    TUD_HID_DESCRIPTOR(0, 4, true, sizeof(hid_report_descriptor), 0x81, 8, 10),
};

// --- USB Device descriptor ---
const tusb_desc_device_t device_descriptor = {
    .bLength = sizeof(tusb_desc_device_t),
    .bDescriptorType = TUSB_DESC_DEVICE,
    .bcdUSB = 0x0200,
    .bDeviceClass = 0x00,
    .bDeviceSubClass = 0x00,
    .bDeviceProtocol = 0x00,
    .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
    .idVendor = 0xCafe,
    .idProduct = 0x4020,
    .bcdDevice = 0x0100,
    .iManufacturer = 0x01,
    .iProduct = 0x02,
    .iSerialNumber = 0x03,
    .bNumConfigurations = 0x01};

// --- TinyUSB HID callbacks ---
uint8_t const *tud_hid_descriptor_report_cb(uint8_t instance)
{
    return hid_report_descriptor;
}

uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t *buffer, uint16_t reqlen)
{
    return 0;
}

void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t const *buffer, uint16_t bufsize)
{
    // Not used
}

// --- Helper functions to send key combos ---
// Send only Ctrl (without any other key)
void send_ctrl_only()
{
    tud_hid_keyboard_report(HID_ITF_PROTOCOL_KEYBOARD, KEYBOARD_MODIFIER_LEFTCTRL, NULL);
    vTaskDelay(pdMS_TO_TICKS(100));
    tud_hid_keyboard_report(HID_ITF_PROTOCOL_KEYBOARD, 0, NULL);
    vTaskDelay(pdMS_TO_TICKS(100));
}

// Send Ctrl + a specific key
void send_ctrl_plus_key(uint8_t key)
{
    uint8_t keycode[6] = {0};
    keycode[0] = key;
    tud_hid_keyboard_report(HID_ITF_PROTOCOL_KEYBOARD, KEYBOARD_MODIFIER_LEFTCTRL, keycode);
    vTaskDelay(pdMS_TO_TICKS(100));
    tud_hid_keyboard_report(HID_ITF_PROTOCOL_KEYBOARD, 0, NULL);
    vTaskDelay(pdMS_TO_TICKS(100));
}

// --- Main application ---
void app_main(void)
{
    // Configure and install TinyUSB driver
    const tinyusb_config_t tusb_cfg = {
        .device_descriptor = &device_descriptor,
        .string_descriptor = hid_string_descriptor,
        .string_descriptor_count = sizeof(hid_string_descriptor) / sizeof(hid_string_descriptor[0]),
        .external_phy = false,
#if TUD_OPT_HIGH_SPEED
        .fs_configuration_descriptor = hid_configuration_descriptor,
        .hs_configuration_descriptor = hid_configuration_descriptor,
#else
        .configuration_descriptor = hid_configuration_descriptor,
#endif
    };
    tinyusb_driver_install(&tusb_cfg);
    ESP_LOGI(TAG, "USB HID keyboard ready");

    // Wait until the USB host has mounted the device
    while (!tud_mounted())
    {
        vTaskDelay(pdMS_TO_TICKS(100));
    }

    // Send KVM hotkey: Ctrl + Ctrl + 2
    send_ctrl_only();
    send_ctrl_only();
    send_ctrl_plus_key(HID_KEY_2);

    ESP_LOGI(TAG, "KVM hotkey sent");
}

Recommendations:

Make sure your descriptor has boot_protocol = true

Right after USB mounts, send tud_hid_keyboard_report(..., 0, NULL); as an "idle" state

If it still doesn't work, try another VID/PID, but the most important thing is comparing lsusb -v with a working keyboard

haideralipunjabi
Posts: 2
Joined: Sun May 11, 2025 4:21 pm

Re: Need help emulating a keyboard exactly using ESP32 S2 mini.

Postby haideralipunjabi » Wed May 14, 2025 1:12 pm

Thank you for your help. Your exact example didn't work, I had to modify it a bit but now I have working code.

Code: Select all

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "tinyusb.h"
#include "class/hid/hid_device.h"
#include "esp_log.h"
#define TAG "KVM_Trigger"

enum
{
    ITF_NUM_KEYBOARD,
    ITF_NUM_MOUSE,
    ITF_NUM_TOTAL
};

// --- HID Report descriptor ---
const uint8_t hid_report_descriptor[] = {
    TUD_HID_REPORT_DESC_KEYBOARD(HID_REPORT_ID(HID_ITF_PROTOCOL_KEYBOARD)),
};

// --- String descriptors ---
const char *hid_string_descriptor[] = {
    (char[]){0x09, 0x04}, // Language: English (0x0409)
    "Logitech",           // Manufacturer
    "Composite Device",   // Product
    "LOGI123456",         // Serial
    "Keyboard Interface", // HID interface
};

// --- USB Configuration descriptor ---
#define TUSB_DESC_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_HID_DESC_LEN)

const uint8_t hid_configuration_descriptor[] = {
    // Config number, interface count, string index, total length, attribute, power in mA
    TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, TUSB_DESC_TOTAL_LEN, TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100),
    // Interface number, string index, protocol, report descriptor len, EP In address, size & polling interval
    TUD_HID_DESCRIPTOR(ITF_NUM_KEYBOARD, 0, HID_ITF_PROTOCOL_KEYBOARD, sizeof(hid_report_descriptor), 0x81, CFG_TUD_HID_EP_BUFSIZE, 10),
};

// --- USB Device descriptor ---
const tusb_desc_device_t device_descriptor = {
    .bLength = sizeof(tusb_desc_device_t),
    .bDescriptorType = TUSB_DESC_DEVICE,
    .bcdUSB = 0x0200,
    .bDeviceClass = 0x00,
    .bDeviceSubClass = 0x00,
    .bDeviceProtocol = 0x00,
    .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
    .idVendor = 0x0513,  // Example: Logitech
    .idProduct = 0x0318, // Example: Known keyboard PID
    .bcdDevice = 0x0100,
    .iManufacturer = 0x01,
    .iProduct = 0x02,
    .iSerialNumber = 0x03,
    .bNumConfigurations = 0x01};

// --- TinyUSB HID callbacks ---
uint8_t const *tud_hid_descriptor_report_cb(uint8_t instance)
{
    return hid_report_descriptor;
}

uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t *buffer, uint16_t reqlen)
{
    return 0;
}

void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t const *buffer, uint16_t bufsize)
{
    // Not used
}

// --- Helper functions to send key combos ---
// Send only Ctrl (without any other key)
void send_ctrl_only()
{
    tud_hid_keyboard_report(HID_ITF_PROTOCOL_KEYBOARD, KEYBOARD_MODIFIER_LEFTCTRL, NULL);
    vTaskDelay(pdMS_TO_TICKS(100));
    tud_hid_keyboard_report(HID_ITF_PROTOCOL_KEYBOARD, 0, NULL);
    vTaskDelay(pdMS_TO_TICKS(100));
}

void send_key(uint8_t key)
{
    uint8_t keycode[6] = {0};
    keycode[0] = key;
    tud_hid_keyboard_report(HID_ITF_PROTOCOL_KEYBOARD, 0, keycode);
    vTaskDelay(pdMS_TO_TICKS(100));
    tud_hid_keyboard_report(HID_ITF_PROTOCOL_KEYBOARD, 0, NULL);
    vTaskDelay(pdMS_TO_TICKS(100));
}

// --- Main application ---
void app_main(void)
{
    // Configure and install TinyUSB driver
    const tinyusb_config_t tusb_cfg = {
        .device_descriptor = &device_descriptor,
        .string_descriptor = hid_string_descriptor,
        .string_descriptor_count = sizeof(hid_string_descriptor) / sizeof(hid_string_descriptor[0]),
        .external_phy = false,
#if TUD_OPT_HIGH_SPEED
        .fs_configuration_descriptor = hid_configuration_descriptor,
        .hs_configuration_descriptor = hid_configuration_descriptor,
#else
        .configuration_descriptor = hid_configuration_descriptor,
#endif
    };
    tinyusb_driver_install(&tusb_cfg);
    ESP_LOGI(TAG, "USB HID keyboard ready");

    // Wait until the USB host has mounted the device
    while (!tud_mounted())
    {
        vTaskDelay(pdMS_TO_TICKS(100));
    }
    tud_hid_keyboard_report(HID_ITF_PROTOCOL_KEYBOARD, 0, NULL);
    vTaskDelay(pdMS_TO_TICKS(100));

    // Send KVM hotkey: Ctrl + Ctrl + 2
    send_ctrl_only();
    send_ctrl_only();
    send_key(HID_KEY_2);

    ESP_LOGI(TAG, "KVM hotkey sent");
}
I plan to add MQTT to this and use it with Home Assistant. I think I can figure out that part on my own.

Thank you!

Who is online

Users browsing this forum: DuckDuckGo [Bot], Google [Bot] and 1 guest