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.
This commit is contained in:
116
components/grub_boot_switch/grub_boot_switch.cpp
Normal file
116
components/grub_boot_switch/grub_boot_switch.cpp
Normal file
@@ -0,0 +1,116 @@
|
||||
#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
|
||||
Reference in New Issue
Block a user