Backup RFP Infinity controller state before Resolume changes
This commit is contained in:
@@ -17,8 +17,10 @@ from pathlib import Path
|
||||
|
||||
NODE_ENV = "rfp_esp32s3_wroom1_n16r8_3x106"
|
||||
MASTER_ENV = "rfp_esp32s3_wroom1_n16r8_master"
|
||||
NODE_FIRMWARE = Path(".pio/build") / NODE_ENV / "firmware.bin"
|
||||
MASTER_FIRMWARE = Path(".pio/build") / MASTER_ENV / "firmware.bin"
|
||||
NODE_RELEASE = "RFP_N16R8_NODE3x106_V20260511E"
|
||||
MASTER_RELEASE = "RFP_N16R8_MASTER_V20260511E"
|
||||
NODE_FIRMWARE_FALLBACK = Path(".pio/build") / NODE_ENV / "firmware.bin"
|
||||
MASTER_FIRMWARE_FALLBACK = Path(".pio/build") / MASTER_ENV / "firmware.bin"
|
||||
DEFAULT_SUBNET = "192.168.178.0/24"
|
||||
NODE_HOSTS = range(11, 17)
|
||||
MASTER_HOST = 10
|
||||
@@ -68,26 +70,107 @@ def build(root: Path, env_name: str, env: dict[str, str], dry_run: bool) -> None
|
||||
run([python_executable(root), "-m", "platformio", "run", "-e", env_name], root, env, dry_run)
|
||||
|
||||
|
||||
def flash(root: Path, targets: list[str], firmware: Path, env: dict[str, str], dry_run: bool) -> None:
|
||||
def release_firmware(root: Path, expected_release: str, fallback: Path) -> Path:
|
||||
release_dir = root / "build_output/release"
|
||||
matches = sorted(release_dir.glob(f"WLEDMM_*_{expected_release}.bin"))
|
||||
if len(matches) == 1:
|
||||
return matches[0].relative_to(root)
|
||||
if len(matches) > 1:
|
||||
names = ", ".join(str(path.relative_to(root)) for path in matches)
|
||||
raise RuntimeError(f"Multiple release firmware files match {expected_release}: {names}")
|
||||
return fallback
|
||||
|
||||
|
||||
def flash_one(
|
||||
root: Path,
|
||||
target: str,
|
||||
firmware: Path,
|
||||
expected_release: str,
|
||||
env: dict[str, str],
|
||||
args: argparse.Namespace,
|
||||
) -> None:
|
||||
firmware_path = root / firmware
|
||||
if not args.dry_run and not firmware_path.exists():
|
||||
raise FileNotFoundError(f"Firmware does not exist: {firmware_path}")
|
||||
|
||||
cmd = [
|
||||
python_executable(root),
|
||||
"tools/rfp_network_flash.py",
|
||||
"flash",
|
||||
"--targets",
|
||||
target,
|
||||
"--firmware",
|
||||
str(firmware),
|
||||
"--expect-release",
|
||||
expected_release,
|
||||
]
|
||||
if args.preflight_only:
|
||||
cmd.append("--preflight-only")
|
||||
if args.skip_validation and not args.preflight_only:
|
||||
cmd.append("--skip-validation")
|
||||
if args.force_reboot_after_upload:
|
||||
cmd.append("--force-reboot-after-upload")
|
||||
if args.ignore_preflight_warnings:
|
||||
cmd.append("--ignore-preflight-warnings")
|
||||
if args.skip_current_release and not args.preflight_only:
|
||||
cmd.append("--skip-if-release-matches")
|
||||
if args.upload_timeout is not None:
|
||||
cmd += ["--upload-timeout", str(args.upload_timeout)]
|
||||
run(cmd, root, env, args.dry_run)
|
||||
|
||||
|
||||
def flash(
|
||||
root: Path,
|
||||
targets: list[str],
|
||||
firmware: Path,
|
||||
expected_release: str,
|
||||
env: dict[str, str],
|
||||
args: argparse.Namespace,
|
||||
) -> None:
|
||||
if not targets:
|
||||
return
|
||||
firmware_path = root / firmware
|
||||
if not dry_run and not firmware_path.exists():
|
||||
raise FileNotFoundError(f"Firmware does not exist: {firmware_path}")
|
||||
run(
|
||||
[
|
||||
python_executable(root),
|
||||
"tools/rfp_network_flash.py",
|
||||
"flash",
|
||||
"--targets",
|
||||
",".join(targets),
|
||||
"--firmware",
|
||||
str(firmware),
|
||||
],
|
||||
root,
|
||||
env,
|
||||
dry_run,
|
||||
)
|
||||
for target in targets:
|
||||
try:
|
||||
flash_one(root, target, firmware, expected_release, env, args)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
print()
|
||||
print(f"OTA failed at {target}; stopping before continuing to the remaining devices.")
|
||||
print("After fixing the cause, resume with:")
|
||||
print(f" {python_executable(root)} tools/rfp_update_all_ota.py --no-build --start-from {target}")
|
||||
raise exc
|
||||
|
||||
|
||||
def flash_nodes_via_master_usb(
|
||||
root: Path,
|
||||
targets: list[str],
|
||||
firmware: Path,
|
||||
expected_release: str,
|
||||
env: dict[str, str],
|
||||
args: argparse.Namespace,
|
||||
) -> None:
|
||||
if not targets:
|
||||
return
|
||||
cmd = [
|
||||
python_executable(root),
|
||||
"tools/rfp_master_usb_relay.py",
|
||||
"--port",
|
||||
args.port,
|
||||
"--firmware",
|
||||
str(firmware),
|
||||
"--expect-release",
|
||||
expected_release,
|
||||
"--targets",
|
||||
",".join(targets),
|
||||
"--baud",
|
||||
str(args.relay_baud),
|
||||
"--startup-delay",
|
||||
str(args.relay_startup_delay),
|
||||
]
|
||||
if args.start_from:
|
||||
cmd += ["--start-from", args.start_from]
|
||||
if not args.skip_current_release:
|
||||
cmd.append("--force-current-release")
|
||||
run(cmd, root, env, args.dry_run)
|
||||
|
||||
|
||||
def filtered_plan(nodes: list[str], master: str, args: argparse.Namespace) -> list[tuple[str, str]]:
|
||||
@@ -115,6 +198,17 @@ def parse_args() -> argparse.Namespace:
|
||||
parser.add_argument("--subnet", default=DEFAULT_SUBNET, help=f"Show subnet used to derive .10-.16 targets (default: {DEFAULT_SUBNET})")
|
||||
parser.add_argument("--no-build", action="store_true", help="Skip PlatformIO builds and flash existing firmware.bin files")
|
||||
parser.add_argument("--dry-run", action="store_true", help="Print build and flash steps without executing them")
|
||||
parser.add_argument("--preflight-only", action="store_true", help="Only run per-device OTA diagnostics, do not upload")
|
||||
parser.add_argument("--via-master-usb", action="store_true", help="Update nodes through the USB-connected master instead of direct PC-to-node HTTP")
|
||||
parser.add_argument("--port", default="/dev/ttyACM0", help="Master serial port for --via-master-usb")
|
||||
parser.add_argument("--relay-baud", type=int, default=921600, help="Master USB relay baud rate (default: 921600)")
|
||||
parser.add_argument("--relay-startup-delay", type=float, default=4.0, help="Seconds to wait after opening master USB serial (default: 4)")
|
||||
parser.add_argument("--ignore-preflight-warnings", action="store_true", help="Try OTA even if /update preflight looks blocked")
|
||||
parser.add_argument("--force-reboot-after-upload", action="store_true", help="Request a reboot if upload response is uncertain and no reboot is observed")
|
||||
parser.add_argument("--force-current-release", dest="skip_current_release", action="store_false", help="Reflash devices even when their current release already matches the selected firmware")
|
||||
parser.add_argument("--upload-timeout", type=float, default=180.0, help="Per-device OTA upload timeout in seconds (default: 180)")
|
||||
parser.add_argument("--no-skip-validation", dest="skip_validation", action="store_false", help="Do not send WLED skipValidation=1 during upload")
|
||||
parser.set_defaults(skip_validation=True, skip_current_release=True)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
@@ -144,12 +238,36 @@ def main() -> int:
|
||||
print(f"Building master firmware: {MASTER_ENV}")
|
||||
build(root, MASTER_ENV, env, args.dry_run)
|
||||
|
||||
node_firmware = release_firmware(root, NODE_RELEASE, NODE_FIRMWARE_FALLBACK)
|
||||
master_firmware = release_firmware(root, MASTER_RELEASE, MASTER_FIRMWARE_FALLBACK)
|
||||
print(f"Node firmware: {node_firmware} (expect release {NODE_RELEASE})")
|
||||
print(f"Master firmware: {master_firmware} (expect release {MASTER_RELEASE})")
|
||||
if args.skip_validation and not args.preflight_only:
|
||||
print("WLED OTA release validation: skipValidation=1 for controlled RFP release-name migration")
|
||||
if args.skip_current_release and not args.preflight_only:
|
||||
print("Already matching devices: skipped by default; use --force-current-release to reflash them")
|
||||
print()
|
||||
|
||||
if node_targets:
|
||||
print("Flashing nodes sequentially...")
|
||||
flash(root, node_targets, NODE_FIRMWARE, env, args.dry_run)
|
||||
if args.via_master_usb:
|
||||
if args.preflight_only:
|
||||
print("USB master relay preflight is not separate; run without --preflight-only to query through the master.")
|
||||
else:
|
||||
print("Flashing nodes sequentially via USB-connected master...")
|
||||
flash_nodes_via_master_usb(root, node_targets, node_firmware, NODE_RELEASE, env, args)
|
||||
else:
|
||||
print(("Preflighting" if args.preflight_only else "Flashing") + " nodes sequentially...")
|
||||
flash(root, node_targets, node_firmware, NODE_RELEASE, env, args)
|
||||
if master_targets:
|
||||
print("Flashing master last...")
|
||||
flash(root, master_targets, MASTER_FIRMWARE, env, args.dry_run)
|
||||
if args.via_master_usb:
|
||||
print(
|
||||
"Skipping master firmware in --via-master-usb mode. "
|
||||
"For full updates, use tools/rfp_update_master_usb_then_nodes.py "
|
||||
"to flash the master by USB first and then relay node OTA."
|
||||
)
|
||||
else:
|
||||
print(("Preflighting" if args.preflight_only else "Flashing") + " master last...")
|
||||
flash(root, master_targets, master_firmware, MASTER_RELEASE, env, args)
|
||||
|
||||
print("OTA update plan completed.")
|
||||
return 0
|
||||
|
||||
Reference in New Issue
Block a user