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.
83 lines
2.7 KiB
C++
83 lines
2.7 KiB
C++
#pragma once
|
|
|
|
#include "esphome/core/component.h"
|
|
#include "esphome/core/log.h"
|
|
#include "esphome/core/preferences.h"
|
|
|
|
#include "msc_disk.h"
|
|
|
|
#include <atomic>
|
|
#include <functional>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace esphome {
|
|
namespace grub_boot_switch {
|
|
|
|
class GrubBootSwitch : public Component {
|
|
public:
|
|
void setup() override;
|
|
void dump_config() override;
|
|
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
|
|
|
void set_volume_serial(uint32_t s) { disk_.set_volume_serial(s); }
|
|
void set_volume_label(const std::string &l) { disk_.set_volume_label(l.c_str()); }
|
|
void set_usb_ids(uint16_t vid, uint16_t pid) {
|
|
usb_vid_ = vid;
|
|
usb_pid_ = pid;
|
|
}
|
|
void set_usb_strings(const std::string &manuf, const std::string &product) {
|
|
usb_manufacturer_ = manuf;
|
|
usb_product_ = product;
|
|
}
|
|
void set_initial_target(uint8_t t) { current_target_ = t; }
|
|
|
|
// Target table: digit (0-9) → human-readable label. Populated from YAML
|
|
// before setup() runs. Order matters only for select-platform option lists.
|
|
void add_target(uint8_t digit, const std::string &label) {
|
|
targets_.emplace_back(digit, label);
|
|
}
|
|
const std::vector<std::pair<uint8_t, std::string>> &get_targets() const { return targets_; }
|
|
|
|
// Look up the label for a digit. Returns empty string if no match.
|
|
std::string label_for_digit(uint8_t digit) const;
|
|
// Look up the digit for a label. Returns false on miss; *out unchanged.
|
|
bool digit_for_label(const std::string &label, uint8_t *out) const;
|
|
|
|
// Public API used by automations + sub-components. set_target() persists
|
|
// to NVS and notifies registered callbacks.
|
|
uint8_t get_target() const { return current_target_.load(); }
|
|
void set_target(uint8_t t);
|
|
|
|
// Registered by the select sub-platform so it can publish state changes
|
|
// when set_target() is called from REST/automation/etc.
|
|
void add_on_target_change(std::function<void(uint8_t)> &&cb) {
|
|
callbacks_.emplace_back(std::move(cb));
|
|
}
|
|
|
|
// Called by the TinyUSB MSC glue layer.
|
|
int read_lba(uint32_t lba, uint8_t *out) { return disk_.read_lba(lba, out); }
|
|
uint32_t sector_count() const { return disk_.sector_count(); }
|
|
uint32_t sector_size() const { return disk_.sector_size(); }
|
|
|
|
protected:
|
|
void install_virtual_files_();
|
|
void start_usb_();
|
|
|
|
MscDisk disk_;
|
|
std::atomic<uint8_t> current_target_{0};
|
|
std::vector<std::pair<uint8_t, std::string>> targets_;
|
|
std::vector<std::function<void(uint8_t)>> callbacks_;
|
|
|
|
ESPPreferenceObject pref_;
|
|
|
|
uint16_t usb_vid_ = 0x26BA;
|
|
uint16_t usb_pid_ = 0x8003;
|
|
std::string usb_manufacturer_;
|
|
std::string usb_product_;
|
|
};
|
|
|
|
} // namespace grub_boot_switch
|
|
} // namespace esphome
|