Files
adamksmith a911b9e4cb Initial port of stecman/hw-boot-selection to ESP32-S3
ESPHome external_component that presents a synthetic 64 KB FAT12 disk
over the ESP32-S3's native USB-OTG, containing a one-byte switch_position
file and a switch_position_grub.cfg snippet.  The disk's volume serial
is fixed at 0x55AA6922 to stay drop-in compatible with the upstream
/etc/grub.d hook.

Components:
- fat12 / msc_disk: portable FAT12 sector synthesiser ported from
  reference/src/fat.c and the boot-sector / FAT / dir-entry generation
  in reference/src/usb.c.
- usb_descriptors / tusb_glue: TinyUSB MSC class with our own callbacks
  delegating to MscDisk::read_lba (linker --allow-multiple-definition
  picks ours over esp_tinyusb's storage-backed defaults).
- grub_boot_switch: top-level Component, NVS persistence on target
  changes, exposes targets table to sub-platforms.
- select: HA-facing select platform reading parent's targets map.

Validated end-to-end on an ESP32-S3-DevKitC-1: USB enumerates as
26ba:8003, /dev/sdb mounts FAT12, mtype reads back the live target,
and POST /select/boot_target/set?option=Windows updates both files
on the next host read.
2026-05-04 18:25:41 +00:00

117 lines
3.7 KiB
C++

#include "grub_boot_switch.h"
#include "usb_descriptors.h"
#include "tinyusb.h"
namespace esphome {
namespace grub_boot_switch {
static const char *const TAG = "grub_boot_switch";
extern GrubBootSwitch *g_active_component; // defined in tusb_glue.cpp
static UsbDescriptorBundle s_usb_descriptors;
std::string GrubBootSwitch::label_for_digit(uint8_t digit) const {
for (const auto &t : targets_) {
if (t.first == digit)
return t.second;
}
return std::string();
}
bool GrubBootSwitch::digit_for_label(const std::string &label, uint8_t *out) const {
for (const auto &t : targets_) {
if (t.second == label) {
if (out)
*out = t.first;
return true;
}
}
return false;
}
void GrubBootSwitch::set_target(uint8_t t) {
if (current_target_.load() == t)
return;
current_target_.store(t);
pref_.save(&t);
for (auto &cb : callbacks_)
cb(t);
}
void GrubBootSwitch::install_virtual_files_() {
// File 1: bare ASCII digit, one byte. Mirrors readSwitch() in the reference.
disk_.add_file("switch_position", "SWITCH~1", " ", 1,
[this](uint8_t *out) { out[0] = '0' + (current_target_.load() % 10); });
// File 2: GRUB snippet. Length and final-byte position match the reference's
// grubConfigStr ("set os_hw_switch=N\n") so existing /etc/grub.d hooks parse
// it identically.
static const char kPrefix[] = "set os_hw_switch=";
static const size_t kPrefixLen = sizeof(kPrefix) - 1;
static const size_t kFileLen = kPrefixLen + 2; // digit + newline
disk_.add_file("switch_position_grub.cfg", "SWITCH~1", "CFG", kFileLen,
[this](uint8_t *out) {
memcpy(out, kPrefix, kPrefixLen);
out[kPrefixLen] = '0' + (current_target_.load() % 10);
out[kPrefixLen + 1] = '\n';
});
}
void GrubBootSwitch::start_usb_() {
g_active_component = this;
usb_descriptors_build(&s_usb_descriptors, usb_vid_, usb_pid_,
usb_manufacturer_.c_str(), usb_product_.c_str(), nullptr);
tinyusb_config_t cfg = {};
cfg.device_descriptor =
reinterpret_cast<const tusb_desc_device_t *>(s_usb_descriptors.device);
cfg.string_descriptor = s_usb_descriptors.strings;
cfg.string_descriptor_count =
sizeof(s_usb_descriptors.strings) / sizeof(s_usb_descriptors.strings[0]);
cfg.external_phy = false;
cfg.configuration_descriptor = s_usb_descriptors.config;
cfg.self_powered = false;
esp_err_t err = tinyusb_driver_install(&cfg);
if (err != ESP_OK) {
ESP_LOGE(TAG, "tinyusb_driver_install failed: %d", err);
this->mark_failed();
return;
}
ESP_LOGI(TAG, "TinyUSB MSC installed (VID=%04X PID=%04X serial=%s)", usb_vid_,
usb_pid_, s_usb_descriptors.serial_buf);
}
void GrubBootSwitch::setup() {
ESP_LOGCONFIG(TAG, "Setting up GRUB boot switch");
// NVS-backed persistence: hash is "grub_boot_switch::target" + a fixed key
// so updates stay tied to this component instance.
pref_ = global_preferences->make_preference<uint8_t>(0x47425354U /* "GBST" */, true);
uint8_t saved = current_target_.load();
if (pref_.load(&saved)) {
current_target_.store(saved);
ESP_LOGCONFIG(TAG, "Restored target from NVS: %u", saved);
}
install_virtual_files_();
start_usb_();
}
void GrubBootSwitch::dump_config() {
ESP_LOGCONFIG(TAG, "GRUB boot switch:");
ESP_LOGCONFIG(TAG, " USB VID/PID: %04X:%04X", usb_vid_, usb_pid_);
ESP_LOGCONFIG(TAG, " Current target: %u", current_target_.load());
if (!targets_.empty()) {
ESP_LOGCONFIG(TAG, " Targets:");
for (const auto &t : targets_)
ESP_LOGCONFIG(TAG, " %u: %s", t.first, t.second.c_str());
}
}
} // namespace grub_boot_switch
} // namespace esphome