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

74 lines
2.5 KiB
C++

#pragma once
#include "fat12.h"
#include <stdint.h>
#include <stddef.h>
#include <functional>
#include <vector>
#include <string>
namespace esphome {
namespace grub_boot_switch {
constexpr uint32_t SECTOR_COUNT = 128;
constexpr uint32_t SECTOR_SIZE = 512;
constexpr uint32_t SECTORS_PER_CLUSTER = 1;
constexpr uint32_t RESERVED_SECTORS = 1;
constexpr uint32_t FAT_COPIES = 2;
constexpr uint32_t ROOT_ENTRIES = 512;
constexpr uint32_t ROOT_ENTRY_LENGTH = 32;
constexpr uint32_t FILEDATA_START_CLUSTER = 3;
constexpr uint32_t DATA_REGION_SECTOR =
RESERVED_SECTORS + FAT_COPIES + (ROOT_ENTRIES * ROOT_ENTRY_LENGTH) / SECTOR_SIZE;
constexpr uint32_t FILEDATA_START_SECTOR =
DATA_REGION_SECTOR + (FILEDATA_START_CLUSTER - 2) * SECTORS_PER_CLUSTER;
using FileReadCallback = std::function<void(uint8_t *output)>;
struct VirtualFile {
std::string long_name;
FatDirEntry dir;
FileReadCallback read;
};
class MscDisk {
public:
MscDisk();
// Volume serial baked into the FAT boot sector.
// Default 0x55AA6922 matches the upstream stecman/hw-boot-selection firmware
// so existing GRUB hooks (search --fs-uuid 55AA-6922) keep working unchanged.
void set_volume_serial(uint32_t serial) { volume_serial_ = serial; }
void set_volume_label(const char *label); // 11 chars, space-padded
// Date stamped on every directory entry. GRUB 2.06 rejects 0-valued dates.
void set_file_date(uint16_t year, uint8_t month, uint8_t day);
// Register a file. Short name must be 8.3 with space padding handled by caller
// (use add_file() helper below for typical cases). size is the file's logical
// size in bytes; content_cb is called once per host read of the file's sector.
void add_file(const std::string &long_name, const std::string &short_name,
const std::string &short_ext, uint32_t size, FileReadCallback content_cb);
// Synthesise a single 512-byte sector for LBA `lba` into `out`.
// out is zeroed first. Returns 0 on success.
int read_lba(uint32_t lba, uint8_t *out);
uint32_t sector_count() const { return SECTOR_COUNT; }
uint32_t sector_size() const { return SECTOR_SIZE; }
protected:
void write_boot_sector(uint8_t *out);
void write_fat_sector(uint8_t *out);
void write_dir_sector(uint8_t *out);
uint32_t volume_serial_ = 0x55AA6922u;
char volume_label_[11] = {'S', 'W', 'I', 'T', 'C', 'H', ' ', ' ', ' ', ' ', ' '};
uint16_t file_date_ = 0;
std::vector<VirtualFile> files_;
};
} // namespace grub_boot_switch
} // namespace esphome