mirror of
https://github.com/esphome/esphome.git
synced 2026-09-18 02:28:42 +00:00
[tinyusb] Add on_mount/on_unmount triggers and is_mounted condition (#19067)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
588ad529e0
commit
328077c4f8
@@ -1,4 +1,4 @@
|
||||
from esphome import final_validate as fv
|
||||
from esphome import automation, final_validate as fv, pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32
|
||||
from esphome.components.esp32 import (
|
||||
@@ -12,17 +12,22 @@ from esphome.components.esp32 import (
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_HARDWARE_UART, CONF_ID
|
||||
from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@kbx81"]
|
||||
CONFLICTS_WITH = ["usb_host"]
|
||||
|
||||
CONF_ON_MOUNT = "on_mount"
|
||||
CONF_ON_UNMOUNT = "on_unmount"
|
||||
CONF_USB_LANG_ID = "usb_lang_id"
|
||||
CONF_USB_MANUFACTURER_STR = "usb_manufacturer_str"
|
||||
CONF_USB_PRODUCT_ID = "usb_product_id"
|
||||
CONF_USB_PRODUCT_STR = "usb_product_str"
|
||||
CONF_USB_SERIAL_STR = "usb_serial_str"
|
||||
CONF_USB_VENDOR_ID = "usb_vendor_id"
|
||||
CONF_VBUS_MONITOR_PIN = "vbus_monitor_pin"
|
||||
|
||||
# Components that provide a USB device class (CDC, HID, MSC, ...) on top of
|
||||
# tinyusb. Configuring `tinyusb:` without any of these triggers a 5s hang in
|
||||
@@ -33,6 +38,20 @@ _USB_CLASS_COMPONENTS = ("usb_cdc_acm",)
|
||||
|
||||
tinyusb_ns = cg.esphome_ns.namespace("tinyusb")
|
||||
TinyUSB = tinyusb_ns.class_("TinyUSB", cg.Component)
|
||||
IsMountedCondition = tinyusb_ns.class_("IsMountedCondition", automation.Condition)
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_MOUNT,
|
||||
"add_on_mount_state_callback",
|
||||
forwarder=automation.TriggerOnTrueForwarder,
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_UNMOUNT,
|
||||
"add_on_mount_state_callback",
|
||||
forwarder=automation.TriggerOnFalseForwarder,
|
||||
),
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
@@ -44,6 +63,18 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_USB_MANUFACTURER_STR, default="ESPHome"): cv.string,
|
||||
cv.Optional(CONF_USB_PRODUCT_STR, default="ESPHome"): cv.string,
|
||||
cv.Optional(CONF_USB_SERIAL_STR, default=""): cv.string,
|
||||
# esp_tinyusb monitors VBUS on the S31 through a GPIO interrupt and needs
|
||||
# the GPIO ISR service installed first, which would collide with the esp32
|
||||
# platform's own lazy install and disable other interrupts. The other
|
||||
# variants watch the pin in the OTG hardware.
|
||||
cv.Optional(CONF_VBUS_MONITOR_PIN): cv.All(
|
||||
pins.internal_gpio_input_pin_number,
|
||||
esp32.only_on_variant(
|
||||
unsupported=[VARIANT_ESP32S31], msg_prefix=CONF_VBUS_MONITOR_PIN
|
||||
),
|
||||
),
|
||||
cv.Optional(CONF_ON_MOUNT): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_UNMOUNT): automation.validate_automation({}),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
esp32.only_on_variant(
|
||||
@@ -93,9 +124,28 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(var.set_usb_desc_product(config[CONF_USB_PRODUCT_STR]))
|
||||
if config[CONF_USB_SERIAL_STR]:
|
||||
cg.add(var.set_usb_desc_serial(config[CONF_USB_SERIAL_STR]))
|
||||
if (vbus_pin := config.get(CONF_VBUS_MONITOR_PIN)) is not None:
|
||||
cg.add(var.set_vbus_monitor_pin(vbus_pin))
|
||||
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
add_idf_component(name="espressif/esp_tinyusb", ref="2.2.1")
|
||||
|
||||
add_idf_sdkconfig_option("CONFIG_TINYUSB_DESC_USE_ESPRESSIF_VID", False)
|
||||
add_idf_sdkconfig_option("CONFIG_TINYUSB_DESC_USE_DEFAULT_PID", False)
|
||||
add_idf_sdkconfig_option("CONFIG_TINYUSB_DESC_BCD_DEVICE", 0x0100)
|
||||
|
||||
|
||||
@automation.register_condition(
|
||||
"tinyusb.is_mounted",
|
||||
IsMountedCondition,
|
||||
cv.Schema({cv.GenerateID(): cv.use_id(TinyUSB)}),
|
||||
)
|
||||
async def tinyusb_is_mounted_to_code(
|
||||
config: ConfigType,
|
||||
condition_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(condition_id, template_arg, paren)
|
||||
|
||||
@@ -9,6 +9,14 @@ namespace esphome::tinyusb {
|
||||
|
||||
static const char *const TAG = "tinyusb";
|
||||
|
||||
// Runs on the TinyUSB task: only wake the main loop, which reads the state and runs
|
||||
// the automations.
|
||||
static void tinyusb_event_cb(tinyusb_event_t *event, void *arg) {
|
||||
if (event->id == TINYUSB_EVENT_ATTACHED || event->id == TINYUSB_EVENT_DETACHED) {
|
||||
static_cast<TinyUSB *>(arg)->enable_loop_soon_any_context();
|
||||
}
|
||||
}
|
||||
|
||||
void TinyUSB::setup() {
|
||||
// Use the device's MAC address as its serial number if no serial number is defined
|
||||
if (this->string_descriptor_[SERIAL_NUMBER] == nullptr) {
|
||||
@@ -21,6 +29,12 @@ void TinyUSB::setup() {
|
||||
this->tusb_cfg_ = TINYUSB_DEFAULT_CONFIG();
|
||||
this->tusb_cfg_.port = TINYUSB_PORT_FULL_SPEED_0;
|
||||
this->tusb_cfg_.phy.skip_setup = false;
|
||||
// Without VBUS monitoring the OTG core only sees a cable pull as the bus going idle
|
||||
// (a suspend), so TinyUSB never reports a detach and stays "mounted".
|
||||
if (this->vbus_monitor_pin_ >= 0) {
|
||||
this->tusb_cfg_.phy.self_powered = true;
|
||||
this->tusb_cfg_.phy.vbus_monitor_io = this->vbus_monitor_pin_;
|
||||
}
|
||||
this->tusb_cfg_.descriptor = {
|
||||
.device = &this->usb_descriptor_,
|
||||
.string = this->string_descriptor_,
|
||||
@@ -42,11 +56,26 @@ void TinyUSB::setup() {
|
||||
}
|
||||
#endif
|
||||
|
||||
this->tusb_cfg_.event_cb = tinyusb_event_cb;
|
||||
this->tusb_cfg_.event_arg = this;
|
||||
esp_err_t result = tinyusb_driver_install(&this->tusb_cfg_);
|
||||
if (result != ESP_OK) {
|
||||
ESP_LOGE(TAG, "tinyusb_driver_install failed: %s", esp_err_to_name(result));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
// loop() only reports mount changes; the mount hooks wake it when one happens.
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
void TinyUSB::loop() {
|
||||
const bool mounted = tud_mounted();
|
||||
if (mounted != this->last_reported_mounted_) {
|
||||
this->last_reported_mounted_ = mounted;
|
||||
ESP_LOGD(TAG, "USB host %s", mounted ? LOG_STR_LITERAL("mounted") : LOG_STR_LITERAL("unmounted"));
|
||||
this->mount_state_callback_.call(mounted);
|
||||
}
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
void TinyUSB::dump_config() {
|
||||
@@ -56,9 +85,12 @@ void TinyUSB::dump_config() {
|
||||
" Vendor ID: 0x%04X\n"
|
||||
" Manufacturer: '%s'\n"
|
||||
" Product: '%s'\n"
|
||||
" Serial: '%s'\n",
|
||||
" Serial: '%s'",
|
||||
this->usb_descriptor_.idProduct, this->usb_descriptor_.idVendor, this->string_descriptor_[MANUFACTURER],
|
||||
this->string_descriptor_[PRODUCT], this->string_descriptor_[SERIAL_NUMBER]);
|
||||
if (this->vbus_monitor_pin_ >= 0) {
|
||||
ESP_LOGCONFIG(TAG, " VBUS Monitor Pin: GPIO%d", this->vbus_monitor_pin_);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::tinyusb
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
#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/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <utility>
|
||||
#include "tinyusb.h"
|
||||
#include "tusb.h"
|
||||
|
||||
@@ -23,9 +26,17 @@ static const char *const DEFAULT_USB_STR = "ESPHome";
|
||||
class TinyUSB final : public Component {
|
||||
public:
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::BUS; }
|
||||
|
||||
/// True while a USB host has enumerated and configured the device.
|
||||
bool is_mounted() const { return tud_mounted(); }
|
||||
/// Called with the new mount state whenever a host mounts or unmounts the device.
|
||||
template<typename F> void add_on_mount_state_callback(F &&callback) {
|
||||
this->mount_state_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
void set_usb_desc_product_id(uint16_t product_id) { this->usb_descriptor_.idProduct = product_id; }
|
||||
void set_usb_desc_vendor_id(uint16_t vendor_id) { this->usb_descriptor_.idVendor = vendor_id; }
|
||||
void set_usb_desc_lang_id(uint16_t lang_id) {
|
||||
@@ -37,6 +48,8 @@ class TinyUSB final : public Component {
|
||||
}
|
||||
void set_usb_desc_product(const char *usb_desc_product) { this->string_descriptor_[PRODUCT] = usb_desc_product; }
|
||||
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); }
|
||||
|
||||
protected:
|
||||
char usb_desc_lang_id_[2] = {0x09, 0x04}; // defaults to english
|
||||
@@ -50,6 +63,11 @@ class TinyUSB final : public Component {
|
||||
nullptr, // 5: Terminator
|
||||
};
|
||||
|
||||
LazyCallbackManager<void(bool)> mount_state_callback_;
|
||||
// Edge-detection baseline for loop(); is_mounted() reads the live state instead.
|
||||
bool last_reported_mounted_{false};
|
||||
int8_t vbus_monitor_pin_{-1};
|
||||
|
||||
tinyusb_config_t tusb_cfg_{};
|
||||
tusb_desc_device_t usb_descriptor_{
|
||||
.bLength = sizeof(tusb_desc_device_t),
|
||||
@@ -69,6 +87,15 @@ class TinyUSB final : public Component {
|
||||
};
|
||||
};
|
||||
|
||||
template<typename... Ts> class IsMountedCondition final : public Condition<Ts...> {
|
||||
public:
|
||||
explicit IsMountedCondition(TinyUSB *parent) : parent_(parent) {}
|
||||
bool check(const Ts &...) override { return this->parent_->is_mounted(); }
|
||||
|
||||
protected:
|
||||
TinyUSB *parent_;
|
||||
};
|
||||
|
||||
} // namespace esphome::tinyusb
|
||||
#endif // USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 ||
|
||||
// USE_ESP32_VARIANT_ESP32S31 || USE_ESP32_VARIANT_ESP32H4
|
||||
|
||||
@@ -6,6 +6,15 @@ tinyusb:
|
||||
usb_product_str: ESPHomeTestProduct
|
||||
usb_serial_str: ESPHomeTestSerialNumber
|
||||
usb_vendor_id: 0x2345
|
||||
on_mount:
|
||||
- logger.log: USB host mounted
|
||||
- if:
|
||||
condition:
|
||||
tinyusb.is_mounted:
|
||||
then:
|
||||
- logger.log: USB host is mounted
|
||||
on_unmount:
|
||||
- logger.log: USB host unmounted
|
||||
|
||||
# tinyusb requires at least one USB class companion; usb_cdc_acm satisfies that.
|
||||
usb_cdc_acm:
|
||||
|
||||
@@ -1 +1,7 @@
|
||||
<<: !include common.yaml
|
||||
packages:
|
||||
tinyusb: !include common.yaml
|
||||
|
||||
# VBUS monitoring is per variant: the OTG hardware watches the pin here, while the
|
||||
# S31 would need the GPIO ISR path and rejects the key.
|
||||
tinyusb:
|
||||
vbus_monitor_pin: 4
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
<<: !include common.yaml
|
||||
packages:
|
||||
tinyusb: !include common.yaml
|
||||
|
||||
# VBUS monitoring is per variant: the OTG hardware watches the pin here, while the
|
||||
# S31 would need the GPIO ISR path and rejects the key.
|
||||
tinyusb:
|
||||
vbus_monitor_pin: 4
|
||||
|
||||
# S2 defaults logger to USB_CDC, which conflicts with tinyusb on the shared
|
||||
# USB OTG peripheral; route the logger to UART0 so the fixture builds.
|
||||
|
||||
@@ -1 +1,7 @@
|
||||
<<: !include common.yaml
|
||||
packages:
|
||||
tinyusb: !include common.yaml
|
||||
|
||||
# VBUS monitoring is per variant: the OTG hardware watches the pin here, while the
|
||||
# S31 would need the GPIO ISR path and rejects the key.
|
||||
tinyusb:
|
||||
vbus_monitor_pin: 4
|
||||
|
||||
Reference in New Issue
Block a user