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.
121 lines
3.7 KiB
C++
121 lines
3.7 KiB
C++
// TinyUSB MSC class callback implementations.
|
|
//
|
|
// These are weak symbols inside TinyUSB; defining them here overrides the
|
|
// stubs in esp_tinyusb's MSC wrapper so we control the SCSI / block-device
|
|
// surface entirely from our component, with no on-device storage.
|
|
|
|
#include "grub_boot_switch.h"
|
|
|
|
#include "tusb.h"
|
|
#include "class/msc/msc_device.h"
|
|
|
|
#include <string.h>
|
|
|
|
namespace esphome {
|
|
namespace grub_boot_switch {
|
|
|
|
// Set by GrubBootSwitch::start_usb_() before tinyusb_driver_install() runs.
|
|
// Single-instance assumption (MULTI_CONF=False in __init__.py).
|
|
GrubBootSwitch *g_active_component = nullptr;
|
|
|
|
} // namespace grub_boot_switch
|
|
} // namespace esphome
|
|
|
|
using esphome::grub_boot_switch::g_active_component;
|
|
|
|
extern "C" {
|
|
|
|
// SCSI Inquiry: vendor / product / revision strings. Mirrors the SCSI strings
|
|
// from reference/src/usb.c so the host sees the same identity as the STM32
|
|
// firmware.
|
|
void tud_msc_inquiry_cb(uint8_t lun, uint8_t vendor_id[8], uint8_t product_id[16],
|
|
uint8_t product_rev[4]) {
|
|
(void) lun;
|
|
static const char kVendor[] = "STECMAN ";
|
|
static const char kProduct[] = "BOOT SWITCH ";
|
|
static const char kRev[] = "V1 ";
|
|
memcpy(vendor_id, kVendor, 8);
|
|
memcpy(product_id, kProduct, 16);
|
|
memcpy(product_rev, kRev, 4);
|
|
}
|
|
|
|
// Unit ready: report ready once the component finished setup and we have a
|
|
// disk to serve.
|
|
bool tud_msc_test_unit_ready_cb(uint8_t lun) {
|
|
(void) lun;
|
|
return g_active_component != nullptr;
|
|
}
|
|
|
|
// Capacity in blocks.
|
|
void tud_msc_capacity_cb(uint8_t lun, uint32_t *block_count, uint16_t *block_size) {
|
|
(void) lun;
|
|
if (g_active_component != nullptr) {
|
|
*block_count = g_active_component->sector_count();
|
|
*block_size = static_cast<uint16_t>(g_active_component->sector_size());
|
|
} else {
|
|
*block_count = 0;
|
|
*block_size = 512;
|
|
}
|
|
}
|
|
|
|
// Start/stop unit: we have no media to load/eject, just acknowledge.
|
|
bool tud_msc_start_stop_cb(uint8_t lun, uint8_t power_condition, bool start, bool load_eject) {
|
|
(void) lun;
|
|
(void) power_condition;
|
|
(void) start;
|
|
(void) load_eject;
|
|
return true;
|
|
}
|
|
|
|
// Read10: TinyUSB asks for `bufsize` bytes starting at (lba, offset). Our
|
|
// disk is sector-granular (offset is always 0 for typical hosts), so we
|
|
// generate the full sector and copy out the requested slice.
|
|
int32_t tud_msc_read10_cb(uint8_t lun, uint32_t lba, uint32_t offset, void *buffer,
|
|
uint32_t bufsize) {
|
|
(void) lun;
|
|
if (g_active_component == nullptr)
|
|
return -1;
|
|
if (lba >= g_active_component->sector_count())
|
|
return -1;
|
|
|
|
uint8_t sector[512];
|
|
g_active_component->read_lba(lba, sector);
|
|
|
|
uint32_t to_copy = bufsize;
|
|
if (offset + to_copy > sizeof(sector))
|
|
to_copy = sizeof(sector) - offset;
|
|
memcpy(buffer, sector + offset, to_copy);
|
|
return static_cast<int32_t>(to_copy);
|
|
}
|
|
|
|
// Write10: silently accept and discard. Hosts (especially Windows) sometimes
|
|
// scribble FAT metadata back; failing the writes makes them retry forever.
|
|
int32_t tud_msc_write10_cb(uint8_t lun, uint32_t lba, uint32_t offset, uint8_t *buffer,
|
|
uint32_t bufsize) {
|
|
(void) lun;
|
|
(void) lba;
|
|
(void) offset;
|
|
(void) buffer;
|
|
return static_cast<int32_t>(bufsize);
|
|
}
|
|
|
|
// Report writability — false → host treats us as read-only media (the GRUB
|
|
// use case is read-only by design).
|
|
bool tud_msc_is_writable_cb(uint8_t lun) {
|
|
(void) lun;
|
|
return false;
|
|
}
|
|
|
|
// SCSI passthrough for unsupported commands. Returning -1 + sense data
|
|
// lets TinyUSB STALL the appropriate endpoint.
|
|
int32_t tud_msc_scsi_cb(uint8_t lun, uint8_t const scsi_cmd[16], void *buffer,
|
|
uint16_t bufsize) {
|
|
(void) lun;
|
|
(void) scsi_cmd;
|
|
(void) buffer;
|
|
(void) bufsize;
|
|
return -1;
|
|
}
|
|
|
|
} // extern "C"
|