mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 10:51:36 -04:00
Browser-based ESP32/ESP8266 flasher (esptool-js + Web Serial); nothing is uploaded to WROLPi, the flash is written straight to the device over USB. Firmware sources (themed, tabbed): a saved set, the WROLPi media directory, or a local file. Local files can be dropped anywhere on the page, which jumps to the "Add from computer" dropzone. The media picker searches .bin files by path and can detect the connected chip to filter firmware to matching images. - Indexed file_group.suffix + path filter for fast .bin search (alembic migration backfills via split_path_stem_and_suffix) - modules/flasher: chip detection from ESP image headers, /search endpoint, and flasher.yaml saved configurations (/configs CRUD) - Flash progress modal with ETA, connect timeout, boot-mode hint, auto- scrolling logs Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
86 lines
3.1 KiB
Python
86 lines
3.1 KiB
Python
"""A YAML config storing the user's saved firmware flashing configurations.
|
|
|
|
Each saved configuration is a named set of firmware files (media-relative paths) and their flash offsets, plus
|
|
whether to erase the flash first. This lets a user configure a multi-part flash once (e.g. a Meshtastic T-Deck:
|
|
firmware at 0x0, littlefs at 0xc90000) and re-load it later with one click.
|
|
"""
|
|
from dataclasses import dataclass, field
|
|
from typing import List
|
|
|
|
from wrolpi.common import ConfigFile, logger
|
|
|
|
logger = logger.getChild(__name__)
|
|
|
|
|
|
@dataclass
|
|
class FlasherConfigValidator:
|
|
version: int = None
|
|
configurations: list = field(default_factory=list)
|
|
|
|
|
|
class FlasherConfig(ConfigFile):
|
|
file_name = 'flasher.yaml'
|
|
default_config = dict(
|
|
version=0,
|
|
configurations=[],
|
|
)
|
|
validator = FlasherConfigValidator
|
|
|
|
def import_config(self, file=None, send_events=False):
|
|
super().import_config(file, send_events)
|
|
# YAML-only config (no database); a successful read is a successful import.
|
|
self.successful_import = True
|
|
|
|
@property
|
|
def configurations(self) -> List[dict]:
|
|
return list(self._config.get('configurations', []))
|
|
|
|
@configurations.setter
|
|
def configurations(self, value: List[dict]):
|
|
# ConfigFile.update() validates, stores, and triggers a background save.
|
|
self.update({'configurations': value})
|
|
|
|
def save_configuration(self, name: str, files: List[dict], erase_all: bool = False) -> dict:
|
|
"""Add or replace (by name) a saved configuration. Returns the stored configuration."""
|
|
name = (name or '').strip()
|
|
if not name:
|
|
raise ValueError('Configuration name is required')
|
|
# Keep only the fields we persist for each file.
|
|
stored_files = [
|
|
dict(
|
|
path=f.get('path'),
|
|
address=f.get('address'),
|
|
name=f.get('name'),
|
|
size=f.get('size'),
|
|
)
|
|
for f in files
|
|
]
|
|
configuration = dict(name=name, erase_all=bool(erase_all), files=stored_files)
|
|
# Replace any existing configuration with the same name, then keep the list sorted by name.
|
|
configurations = [c for c in self.configurations if c.get('name') != name]
|
|
configurations.append(configuration)
|
|
configurations.sort(key=lambda c: (c.get('name') or '').lower())
|
|
self.configurations = configurations
|
|
return configuration
|
|
|
|
def delete_configuration(self, name: str) -> bool:
|
|
"""Delete a saved configuration by name. Returns True if one was removed."""
|
|
configurations = self.configurations
|
|
remaining = [c for c in configurations if c.get('name') != name]
|
|
if len(remaining) == len(configurations):
|
|
return False
|
|
self.configurations = remaining
|
|
return True
|
|
|
|
|
|
FLASHER_CONFIG: FlasherConfig = FlasherConfig()
|
|
|
|
# Test override (see get_flasher_config).
|
|
TEST_FLASHER_CONFIG = None
|
|
|
|
|
|
def get_flasher_config() -> FlasherConfig:
|
|
global TEST_FLASHER_CONFIG
|
|
if isinstance(TEST_FLASHER_CONFIG, ConfigFile):
|
|
return TEST_FLASHER_CONFIG
|
|
return FLASHER_CONFIG
|