Files
NerdMiner_v2/post_build_merge.py
bitmaker 698e6b7317 Add automatic firmware factory and update file generation
- Added post_build_merge.py script with ESP32 variant auto-detection
- Integrated post-build script to all platformio.ini environments
- Generates factory.bin (complete from 0x0) and firmware.bin (app only from 0x10000)
- Auto-detects ESP32/S2/S3/C3 by analyzing bootloader signature
- Organizes output by firmware version in firmware/{version}/ folders
2025-09-02 00:29:55 +02:00

202 lines
7.0 KiB
Python

#!/usr/bin/env python3
"""
Post-build script for NerdMiner_v2 firmware merging
Ported from merge_firmware_universal.js - detects ESP32 type from bootloader signature
Generates factory (0x0) and update (0x10000) files automatically after build
Usage: Add to platformio.ini environments:
extra_scripts =
pre:auto_firmware_version.py
post:post_build_merge.py
"""
import os
import subprocess
from pathlib import Path
Import("env")
def detect_esp32_type(bootloader_path):
"""Detect ESP32 type by analyzing bootloader signature - ported from JS version"""
try:
with open(bootloader_path, 'rb') as f:
bootloader_data = f.read()
if len(bootloader_data) < 13:
print("Warning: Bootloader too small, defaulting to ESP32")
return 'ESP32'
chip_id = bootloader_data[12] # Chip ID at byte 12
size = len(bootloader_data)
# Detect ESP32 type based on bootloader signature (from JS version)
if chip_id == 0x09 and size >= 15000:
return 'ESP32-S3'
elif chip_id == 0x05 and size >= 13000 and size < 14000:
return 'ESP32-C3'
elif chip_id == 0x02 and size >= 13000 and size < 15000:
return 'ESP32-S2'
elif chip_id == 0x00 and size >= 17000:
return 'ESP32'
else:
# Fallback: try to detect by size (from JS version)
if size >= 17000:
return 'ESP32'
elif size >= 15000:
return 'ESP32-S3'
elif size >= 13600:
return 'ESP32-S2'
elif size >= 13000:
return 'ESP32-C3'
else:
return 'ESP32' # Default fallback
except Exception as e:
print(f"Warning: Could not analyze bootloader ({e}), defaulting to ESP32")
return 'ESP32'
def get_memory_layout(esp_type):
"""Get memory addresses for each ESP32 variant - ported from JS version"""
if esp_type == 'ESP32-C3':
# ESP32-C3: Bootloader at 0x0000, no boot_app0
return {
'bootloader': 0x0000,
'partitions': 0x8000,
'firmware': 0x10000
}
elif esp_type == 'ESP32-S2':
# ESP32-S2: Bootloader at 0x1000, boot_app0 at 0xE000
return {
'bootloader': 0x1000,
'partitions': 0x8000,
'boot_app0': 0xE000,
'firmware': 0x10000
}
elif esp_type == 'ESP32-S3':
# ESP32-S3: Bootloader at 0x0000, no boot_app0
return {
'bootloader': 0x0000,
'partitions': 0x8000,
'firmware': 0x10000
}
else:
# ESP32 Classic: Bootloader at 0x1000, boot_app0 at 0xE000
return {
'bootloader': 0x1000,
'partitions': 0x8000,
'boot_app0': 0xE000,
'firmware': 0x10000
}
def get_firmware_version():
"""Get firmware version from git"""
try:
result = subprocess.run(["git", "describe", "--tags", "--dirty"],
stdout=subprocess.PIPE, text=True,
cwd=env.subst("$PROJECT_DIR"))
if result.returncode == 0:
version = result.stdout.strip()
# Clean up version string
version = version.replace('Release', '').replace('release', '')
return version
except:
pass
return "dev"
def create_merged_firmware(source, target, env):
"""Main function called after firmware build"""
# Get build info
project_dir = Path(env.subst("$PROJECT_DIR"))
build_dir = Path(env.subst("$BUILD_DIR"))
env_name = env.subst("$PIOENV")
version = get_firmware_version()
print(f"\n🔨 Building firmware files for {env_name}...")
# File paths in build directory
bootloader_file = build_dir / "bootloader.bin"
partitions_file = build_dir / "partitions.bin"
boot_app0_file = build_dir / "boot_app0.bin"
firmware_file = build_dir / "firmware.bin"
# Check if firmware exists
if not firmware_file.exists():
print(f"❌ Firmware file not found: {firmware_file}")
return
# Auto-detect ESP32 type
esp_type = detect_esp32_type(bootloader_file) if bootloader_file.exists() else 'ESP32'
addresses = get_memory_layout(esp_type)
print(f"📱 Detected: {esp_type} (bootloader at 0x{addresses['bootloader']:04X})")
# Output directory with version subfolder
version_dir = project_dir / "firmware" / version
version_dir.mkdir(parents=True, exist_ok=True)
# Output filenames (simplified names)
factory_file = version_dir / f"{env_name}_factory.bin"
update_file = version_dir / f"{env_name}_firmware.bin"
# 1. Create update file (just copy firmware.bin)
try:
import shutil
shutil.copy2(firmware_file, update_file)
print(f"✅ Firmware: {update_file.name}")
except Exception as e:
print(f"❌ Error creating firmware file: {e}")
return
# 2. Create factory file (merged)
try:
# Create merged binary - 4MB filled with 0xFF
merged_size = 0x400000 # 4MB
merged_data = bytearray([0xFF] * merged_size)
max_address = 0
# Files to merge
files_to_merge = {
'bootloader': bootloader_file,
'partitions': partitions_file,
'firmware': firmware_file
}
# Add boot_app0 for ESP32 Classic and S2
if 'boot_app0' in addresses:
files_to_merge['boot_app0'] = boot_app0_file
# Merge files at their respective addresses
for file_type, file_path in files_to_merge.items():
if file_path.exists():
address = addresses[file_type]
with open(file_path, 'rb') as f:
data = f.read()
print(f" 📄 {file_type} at 0x{address:06X}: {len(data)} bytes")
if address + len(data) <= merged_size:
merged_data[address:address+len(data)] = data
max_address = max(max_address, address + len(data))
else:
print(f"⚠️ Warning: {file_type} too large, truncating")
remaining = merged_size - address
merged_data[address:address+remaining] = data[:remaining]
max_address = merged_size
else:
print(f"⚠️ Warning: {file_type} not found: {file_path}")
# Find actual end of data (round up to 4K boundary)
actual_end = ((max_address + 4095) // 4096) * 4096
# Write factory file
with open(factory_file, 'wb') as f:
f.write(merged_data[:actual_end])
print(f"✅ Factory: {factory_file.name} ({actual_end} bytes)")
except Exception as e:
print(f"❌ Error creating factory file: {e}")
# Add post-build hook
env.AddPostAction("$BUILD_DIR/firmware.bin", create_merged_firmware)