mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 10:12:52 +00:00
[tinyusb] Add tinyusb_keyboard component (#16079)
Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
co-authored by
J. Nick Koston
J. Nick Koston
parent
d08d324a83
commit
1e13b7f973
@@ -605,6 +605,7 @@ esphome/components/thermopro_ble/* @sittner
|
||||
esphome/components/thermostat/* @kbx81
|
||||
esphome/components/time/* @esphome/core
|
||||
esphome/components/tinyusb/* @kbx81
|
||||
esphome/components/tinyusb_keyboard/* @frederikbolding
|
||||
esphome/components/tlc5947/* @rnauber
|
||||
esphome/components/tlc5971/* @IJIJI
|
||||
esphome/components/tm1621/* @Philippe12
|
||||
|
||||
@@ -34,7 +34,7 @@ CONF_VBUS_MONITOR_PIN = "vbus_monitor_pin"
|
||||
# esp_tinyusb's driver install (descriptors_set fails with no class and no
|
||||
# user-provided full_speed_config), which trips the task watchdog before
|
||||
# loop() ever runs.
|
||||
_USB_CLASS_COMPONENTS = ("usb_cdc_acm",)
|
||||
_USB_CLASS_COMPONENTS = ("usb_cdc_acm", "tinyusb_keyboard")
|
||||
|
||||
tinyusb_ns = cg.esphome_ns.namespace("tinyusb")
|
||||
TinyUSB = tinyusb_ns.class_("TinyUSB", cg.Component)
|
||||
|
||||
@@ -26,6 +26,8 @@ void TinyUSB::setup() {
|
||||
}
|
||||
|
||||
// Start from esp_tinyusb defaults to keep required task settings valid across esp_tinyusb updates.
|
||||
// Setters write into tusb_cfg_ before setup(), so anything they set has to survive the reset.
|
||||
const uint8_t *full_speed_config = this->tusb_cfg_.descriptor.full_speed_config;
|
||||
this->tusb_cfg_ = TINYUSB_DEFAULT_CONFIG();
|
||||
this->tusb_cfg_.port = TINYUSB_PORT_FULL_SPEED_0;
|
||||
this->tusb_cfg_.phy.skip_setup = false;
|
||||
@@ -39,6 +41,7 @@ void TinyUSB::setup() {
|
||||
.device = &this->usb_descriptor_,
|
||||
.string = this->string_descriptor_,
|
||||
.string_count = SIZE,
|
||||
.full_speed_config = full_speed_config,
|
||||
};
|
||||
|
||||
// Defense-in-depth: esp_tinyusb's tinyusb_descriptors_set() fails with
|
||||
|
||||
@@ -49,6 +49,8 @@ class TinyUSB final : public Component {
|
||||
void set_usb_desc_serial(const char *usb_desc_serial) { this->string_descriptor_[SERIAL_NUMBER] = usb_desc_serial; }
|
||||
/// Self-powered device: watch VBUS on this GPIO so a cable pull becomes a detach.
|
||||
void set_vbus_monitor_pin(int pin) { this->vbus_monitor_pin_ = static_cast<int8_t>(pin); }
|
||||
/// Configuration descriptor from a class component that esp_tinyusb has no built-in default for (HID).
|
||||
void set_full_speed_config(const uint8_t *descriptor) { this->tusb_cfg_.descriptor.full_speed_config = descriptor; }
|
||||
|
||||
protected:
|
||||
char usb_desc_lang_id_[2] = {0x09, 0x04}; // defaults to english
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import string
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32
|
||||
from esphome.components.esp32 import (
|
||||
VARIANT_ESP32H4,
|
||||
VARIANT_ESP32P4,
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
VARIANT_ESP32S31,
|
||||
add_idf_sdkconfig_option,
|
||||
)
|
||||
from esphome.components.tinyusb import TinyUSB
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_KEY
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@frederikbolding"]
|
||||
# The keyboard supplies the whole configuration descriptor, with no room for the CDC interfaces
|
||||
CONFLICTS_WITH = ["usb_cdc_acm"]
|
||||
DEPENDENCIES = ["tinyusb"]
|
||||
DOMAIN = "tinyusb_keyboard"
|
||||
|
||||
CONF_MODIFIERS = "modifiers"
|
||||
CONF_TINYUSB_ID = "tinyusb_id"
|
||||
CONF_USAGE = "usage"
|
||||
|
||||
# Keyboard page usage ids run without gaps from HID_KEY_A (0x04) through a..z, 1..9 and 0
|
||||
HID_KEY_A = 0x04
|
||||
KEY_CODES = {
|
||||
char: HID_KEY_A + index
|
||||
for index, char in enumerate(string.ascii_lowercase + "1234567890")
|
||||
}
|
||||
|
||||
tinyusb_keyboard_ns = cg.esphome_ns.namespace("tinyusb_keyboard")
|
||||
TinyUSBKeyboard = tinyusb_keyboard_ns.class_("TinyUSBKeyboard", cg.Component)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(TinyUSBKeyboard),
|
||||
cv.GenerateID(CONF_TINYUSB_ID): cv.use_id(TinyUSB),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
esp32.only_on_variant(
|
||||
supported=[
|
||||
VARIANT_ESP32H4,
|
||||
VARIANT_ESP32P4,
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
VARIANT_ESP32S31,
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
_KEY_CODE = cv.Any(cv.enum(KEY_CODES), cv.int_range(min=10, max=255))
|
||||
|
||||
|
||||
def key_code(value: object) -> int:
|
||||
"""A single lowercase letter or digit, or a keyboard usage id.
|
||||
|
||||
Usage ids 0 to 9 are never typed keys (error codes and the letters a to f), so an unquoted
|
||||
digit is read as that digit.
|
||||
"""
|
||||
if isinstance(value, int) and 0 <= value <= 9:
|
||||
value = str(value)
|
||||
return _KEY_CODE(value)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
tinyusb = await cg.get_variable(config[CONF_TINYUSB_ID])
|
||||
cg.add(tinyusb.set_full_speed_config(tinyusb_keyboard_ns.CONFIGURATION_DESCRIPTOR))
|
||||
add_idf_sdkconfig_option("CONFIG_TINYUSB_HID_COUNT", 1)
|
||||
|
||||
|
||||
KEYBOARD_ID_SCHEMA = {cv.GenerateID(): cv.use_id(TinyUSBKeyboard)}
|
||||
|
||||
automation.register_apply_action(
|
||||
"tinyusb_keyboard.press",
|
||||
automation.maybe_simple_id(
|
||||
{
|
||||
**KEYBOARD_ID_SCHEMA,
|
||||
cv.Required(CONF_KEY): cv.templatable(key_code),
|
||||
cv.Optional(CONF_MODIFIERS, default=0): cv.templatable(cv.uint8_t),
|
||||
}
|
||||
),
|
||||
automation.ApplyCall(
|
||||
"press_key({}, {})", ((CONF_KEY, cg.uint8), (CONF_MODIFIERS, cg.uint8))
|
||||
),
|
||||
)
|
||||
automation.register_apply_action(
|
||||
"tinyusb_keyboard.release",
|
||||
automation.maybe_simple_id(KEYBOARD_ID_SCHEMA),
|
||||
automation.ApplyCall("release_keys()"),
|
||||
)
|
||||
automation.register_apply_action(
|
||||
"tinyusb_keyboard.media_press",
|
||||
automation.maybe_simple_id(
|
||||
{**KEYBOARD_ID_SCHEMA, cv.Required(CONF_USAGE): cv.templatable(cv.uint16_t)}
|
||||
),
|
||||
automation.ApplyCall("press_media({})", ((CONF_USAGE, cg.uint16),)),
|
||||
)
|
||||
automation.register_apply_action(
|
||||
"tinyusb_keyboard.media_release",
|
||||
automation.maybe_simple_id(KEYBOARD_ID_SCHEMA),
|
||||
automation.ApplyCall("release_media()"),
|
||||
)
|
||||
@@ -0,0 +1,90 @@
|
||||
#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32S31) || defined(USE_ESP32_VARIANT_ESP32H4)
|
||||
|
||||
#include "keyboard.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::tinyusb_keyboard {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "tinyusb_keyboard");
|
||||
|
||||
void TinyUSBKeyboard::dump_config() { ESP_LOGCONFIG(TAG, "TinyUSB Keyboard"); }
|
||||
|
||||
void TinyUSBKeyboard::set_keyboard_(uint8_t keycode, uint8_t modifiers) {
|
||||
this->keycode_ = keycode;
|
||||
this->modifiers_ = modifiers;
|
||||
this->keyboard_pending_ = true;
|
||||
this->flush_();
|
||||
}
|
||||
|
||||
void TinyUSBKeyboard::set_consumer_(uint16_t usage) {
|
||||
this->usage_ = usage;
|
||||
this->consumer_pending_ = true;
|
||||
this->flush_();
|
||||
}
|
||||
|
||||
void TinyUSBKeyboard::flush_() {
|
||||
if (!tud_mounted()) {
|
||||
// No host is listening, and a key held from before it attached would be wrong, so drop the state
|
||||
ESP_LOGV(TAG, "No host, dropping report");
|
||||
this->keyboard_pending_ = false;
|
||||
this->consumer_pending_ = false;
|
||||
this->wakeup_requested_ = false;
|
||||
this->disable_loop();
|
||||
return;
|
||||
}
|
||||
if (tud_suspended()) {
|
||||
// A key press wakes a sleeping host, like a real keyboard; the report goes out once the bus resumes.
|
||||
// Signal once per suspend: a host that did not arm remote wakeup will not answer, and one that did
|
||||
// must not be signalled on every pass.
|
||||
if (!this->wakeup_requested_) {
|
||||
this->wakeup_requested_ = true;
|
||||
if (!tud_remote_wakeup()) {
|
||||
ESP_LOGV(TAG, "Host did not allow remote wakeup");
|
||||
}
|
||||
}
|
||||
this->enable_loop();
|
||||
return;
|
||||
}
|
||||
this->wakeup_requested_ = false;
|
||||
// tud_hid_*_report() returns false while the endpoint still holds the previous report, which is
|
||||
// what happens for a press and a release in one action list. Keep retrying so the release gets out.
|
||||
if (this->keyboard_pending_) {
|
||||
uint8_t keycodes[6] = {this->keycode_};
|
||||
if (tud_hid_keyboard_report(REPORT_ID_KEYBOARD, this->modifiers_, keycodes)) {
|
||||
this->keyboard_pending_ = false;
|
||||
}
|
||||
}
|
||||
if (this->consumer_pending_ && !this->keyboard_pending_ &&
|
||||
tud_hid_report(REPORT_ID_CONSUMER, &this->usage_, sizeof(this->usage_))) {
|
||||
this->consumer_pending_ = false;
|
||||
}
|
||||
if (this->keyboard_pending_ || this->consumer_pending_) {
|
||||
this->enable_loop();
|
||||
} else {
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
void TinyUSBKeyboard::loop() { this->flush_(); }
|
||||
|
||||
} // namespace esphome::tinyusb_keyboard
|
||||
|
||||
// TinyUSB resolves these at link time when HID is enabled
|
||||
extern "C" {
|
||||
const uint8_t *tud_hid_descriptor_report_cb(uint8_t /*instance*/) {
|
||||
return esphome::tinyusb_keyboard::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;
|
||||
}
|
||||
|
||||
// Keyboard LED state from the host; nothing consumes it yet
|
||||
void tud_hid_set_report_cb(uint8_t /*instance*/, uint8_t /*report_id*/, hid_report_type_t /*report_type*/,
|
||||
const uint8_t * /*buffer*/, uint16_t /*bufsize*/) {}
|
||||
}
|
||||
|
||||
#endif // USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 ||
|
||||
// USE_ESP32_VARIANT_ESP32S31 || USE_ESP32_VARIANT_ESP32H4
|
||||
@@ -0,0 +1,66 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32S31) || defined(USE_ESP32_VARIANT_ESP32H4)
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
#include "tusb.h"
|
||||
// tusb.h only pulls the HID class in when CFG_TUD_HID is set, which the static analysis build does not do
|
||||
#include "class/hid/hid_device.h"
|
||||
|
||||
namespace esphome::tinyusb_keyboard {
|
||||
|
||||
static constexpr uint8_t REPORT_ID_KEYBOARD = 1;
|
||||
static constexpr uint8_t REPORT_ID_CONSUMER = 2;
|
||||
static constexpr uint8_t ENDPOINT_IN = 0x81;
|
||||
static constexpr uint8_t POLL_INTERVAL_MS = 10;
|
||||
|
||||
// One HID interface carrying a keyboard and a consumer control (media keys), told apart by report id.
|
||||
// Report ids rule out the boot protocol, so the interface declares no boot protocol, as TinyUSB's
|
||||
// composite example does.
|
||||
inline constexpr uint8_t HID_REPORT_DESCRIPTOR[] = {
|
||||
TUD_HID_REPORT_DESC_KEYBOARD(HID_REPORT_ID(REPORT_ID_KEYBOARD)),
|
||||
TUD_HID_REPORT_DESC_CONSUMER(HID_REPORT_ID(REPORT_ID_CONSUMER)),
|
||||
};
|
||||
|
||||
// esp_tinyusb ships default configuration descriptors for CDC, MSC and NCM only, so the keyboard supplies its own
|
||||
inline constexpr uint8_t CONFIGURATION_DESCRIPTOR[] = {
|
||||
TUD_CONFIG_DESCRIPTOR(1, 1, 0, TUD_CONFIG_DESC_LEN + TUD_HID_DESC_LEN, TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100),
|
||||
TUD_HID_DESCRIPTOR(0, 0, HID_ITF_PROTOCOL_NONE, sizeof(HID_REPORT_DESCRIPTOR), ENDPOINT_IN, CFG_TUD_HID_EP_BUFSIZE,
|
||||
POLL_INTERVAL_MS),
|
||||
};
|
||||
|
||||
class TinyUSBKeyboard final : public Component {
|
||||
public:
|
||||
void setup() override { this->disable_loop(); }
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
|
||||
/// Press one key (HID usage id) with the given modifier bitmask; every other key is reported released.
|
||||
void press_key(uint8_t keycode, uint8_t modifiers) { this->set_keyboard_(keycode, modifiers); }
|
||||
void release_keys() { this->set_keyboard_(0, 0); }
|
||||
/// Press one consumer control usage, for example 0xE9 for volume up.
|
||||
void press_media(uint16_t usage) { this->set_consumer_(usage); }
|
||||
void release_media() { this->set_consumer_(0); }
|
||||
|
||||
protected:
|
||||
void set_keyboard_(uint8_t keycode, uint8_t modifiers);
|
||||
void set_consumer_(uint16_t usage);
|
||||
/// Sends whatever changed; a report the endpoint cannot take yet is retried from loop()
|
||||
void flush_();
|
||||
|
||||
// The wanted state of each report, resent until the endpoint accepts it so a release is never lost.
|
||||
// Only the latest state is kept, so consecutive key actions need a short delay between them.
|
||||
uint16_t usage_{0};
|
||||
uint8_t keycode_{0};
|
||||
uint8_t modifiers_{0};
|
||||
bool keyboard_pending_{false};
|
||||
bool consumer_pending_{false};
|
||||
bool wakeup_requested_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::tinyusb_keyboard
|
||||
|
||||
#endif // USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 ||
|
||||
// USE_ESP32_VARIANT_ESP32S31 || USE_ESP32_VARIANT_ESP32H4
|
||||
@@ -0,0 +1,43 @@
|
||||
tinyusb:
|
||||
|
||||
tinyusb_keyboard:
|
||||
id: kb
|
||||
|
||||
binary_sensor:
|
||||
- platform: template
|
||||
id: kb_letter
|
||||
on_press:
|
||||
- tinyusb_keyboard.press:
|
||||
id: kb
|
||||
key: a
|
||||
on_release:
|
||||
- tinyusb_keyboard.release: kb
|
||||
|
||||
- platform: template
|
||||
id: kb_shift_digit
|
||||
on_press:
|
||||
- tinyusb_keyboard.press:
|
||||
id: kb
|
||||
key: "1"
|
||||
modifiers: 0x02 # Left Shift
|
||||
on_release:
|
||||
- tinyusb_keyboard.release: kb
|
||||
|
||||
- platform: template
|
||||
id: kb_raw_code
|
||||
on_press:
|
||||
- tinyusb_keyboard.press:
|
||||
id: kb
|
||||
key: 0x28 # Enter
|
||||
modifiers: !lambda return 0;
|
||||
on_release:
|
||||
- tinyusb_keyboard.release: kb
|
||||
|
||||
- platform: template
|
||||
id: kb_volume_up
|
||||
on_press:
|
||||
- tinyusb_keyboard.media_press:
|
||||
id: kb
|
||||
usage: 0xE9 # Volume Up
|
||||
on_release:
|
||||
- tinyusb_keyboard.media_release: kb
|
||||
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
tinyusb_keyboard: !include common.yaml
|
||||
Reference in New Issue
Block a user