Files
esp32-grub-switch/components/grub_boot_switch/tusb_glue.cpp
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

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"