fix: Rewrite ESP32-S3 flasher to match esptool behavior
Root cause: missing SPI_SET_PARAMS (CMD 0x0B) before flash_begin. ROM bootloader defaults to 2MB flash size, silently truncating writes to 16MB flash. Progress bar showed success but firmware was corrupt. Changes: - Add spi_set_params() with 16MB geometry before flash_begin - Add MD5 verification after write (SPI_FLASH_MD5, CMD 0x13) - Add retry logic (3 attempts per block) - Pad firmware to 4-byte boundary - flash_end(reboot=false) then separate hard reset after MD5 check - Pure Rust MD5 implementation (no external crate) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
70fa72ddb7
commit
195fa6e7e8
1 changed files with 476 additions and 79 deletions
|
|
@ -1,6 +1,9 @@
|
||||||
/// ESP32 ROM bootloader flasher via serial (CH340/CP2102 programming port).
|
/// ESP32 ROM bootloader flasher via serial (CH340/CP2102 programming port).
|
||||||
/// Implements minimal SLIP-framed bootloader protocol for firmware flashing
|
/// Implements minimal SLIP-framed bootloader protocol for firmware flashing
|
||||||
/// without requiring esptool.
|
/// without requiring esptool.
|
||||||
|
///
|
||||||
|
/// Targets the ESP32-S3 ROM bootloader directly (no stub loader upload).
|
||||||
|
/// Reference: esptool.py source, ESP32-S3 Technical Reference Manual.
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use serialport::SerialPort;
|
use serialport::SerialPort;
|
||||||
|
|
@ -10,7 +13,6 @@ use std::sync::mpsc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
/// Progress message sent back to the UI during flashing.
|
/// Progress message sent back to the UI during flashing.
|
||||||
/// Replaces the old `ui::BgResult::OtaProgress` variant.
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub enum FlashProgress {
|
pub enum FlashProgress {
|
||||||
OtaProgress(f32, String),
|
OtaProgress(f32, String),
|
||||||
|
|
@ -69,18 +71,28 @@ fn slip_decode(frame: &[u8]) -> Vec<u8> {
|
||||||
|
|
||||||
const CMD_SYNC: u8 = 0x08;
|
const CMD_SYNC: u8 = 0x08;
|
||||||
const CMD_CHANGE_BAUDRATE: u8 = 0x0F;
|
const CMD_CHANGE_BAUDRATE: u8 = 0x0F;
|
||||||
|
const CMD_SPI_SET_PARAMS: u8 = 0x0B; // Set SPI flash geometry (required before flash_begin)
|
||||||
const CMD_SPI_ATTACH: u8 = 0x0D;
|
const CMD_SPI_ATTACH: u8 = 0x0D;
|
||||||
const CMD_FLASH_BEGIN: u8 = 0x02;
|
const CMD_FLASH_BEGIN: u8 = 0x02;
|
||||||
const CMD_FLASH_DATA: u8 = 0x03;
|
const CMD_FLASH_DATA: u8 = 0x03;
|
||||||
const CMD_FLASH_END: u8 = 0x04;
|
const CMD_FLASH_END: u8 = 0x04;
|
||||||
|
const CMD_SPI_FLASH_MD5: u8 = 0x13; // Post-write integrity check
|
||||||
|
|
||||||
const FLASH_BLOCK_SIZE: u32 = 1024;
|
/// Write block size.
|
||||||
/// Flash erase granularity — the ROM erases in minimum 4 KB units.
|
/// Must match esptool FLASH_WRITE_SIZE = 0x400. The ROM rejects any other value
|
||||||
/// erase_size in FLASH_BEGIN must be aligned to this boundary.
|
/// in the FLASH_BEGIN num_blocks field if it doesn't divide evenly.
|
||||||
|
const FLASH_BLOCK_SIZE: u32 = 0x400;
|
||||||
|
|
||||||
|
/// Flash sector size — minimum erase unit (4 KB).
|
||||||
const FLASH_SECTOR_SIZE: u32 = 0x1000;
|
const FLASH_SECTOR_SIZE: u32 = 0x1000;
|
||||||
|
|
||||||
const INITIAL_BAUD: u32 = 115200;
|
const INITIAL_BAUD: u32 = 115200;
|
||||||
const FLASH_BAUD: u32 = 460800;
|
const FLASH_BAUD: u32 = 460800;
|
||||||
|
|
||||||
|
/// Number of retries for each FLASH_DATA block.
|
||||||
|
/// esptool uses WRITE_BLOCK_ATTEMPTS = 3.
|
||||||
|
const WRITE_BLOCK_ATTEMPTS: usize = 3;
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn xor_checksum(data: &[u8]) -> u32 {
|
fn xor_checksum(data: &[u8]) -> u32 {
|
||||||
let mut chk: u8 = 0xEF;
|
let mut chk: u8 = 0xEF;
|
||||||
|
|
@ -91,7 +103,8 @@ fn xor_checksum(data: &[u8]) -> u32 {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build a bootloader command packet (before SLIP encoding).
|
/// Build a bootloader command packet (before SLIP encoding).
|
||||||
/// Format: [0x00][cmd][size:u16 LE][checksum:u32 LE][data...]
|
/// Packet format matches esptool struct.pack("<BBHI", dir, cmd, len, chk) + data:
|
||||||
|
/// [0x00][cmd][size:u16 LE][checksum:u32 LE][data...]
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn build_command(cmd: u8, data: &[u8], checksum: u32) -> Vec<u8> {
|
fn build_command(cmd: u8, data: &[u8], checksum: u32) -> Vec<u8> {
|
||||||
let size = data.len() as u16;
|
let size = data.len() as u16;
|
||||||
|
|
@ -108,8 +121,7 @@ fn build_command(cmd: u8, data: &[u8], checksum: u32) -> Vec<u8> {
|
||||||
pkt
|
pkt
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract complete SLIP frames from a byte buffer.
|
/// Extract complete SLIP frames from a raw byte buffer.
|
||||||
/// Returns (frames, remaining_bytes_not_consumed).
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn extract_slip_frames(raw: &[u8]) -> Vec<Vec<u8>> {
|
fn extract_slip_frames(raw: &[u8]) -> Vec<Vec<u8>> {
|
||||||
let mut frames = Vec::new();
|
let mut frames = Vec::new();
|
||||||
|
|
@ -119,7 +131,6 @@ fn extract_slip_frames(raw: &[u8]) -> Vec<Vec<u8>> {
|
||||||
for &byte in raw {
|
for &byte in raw {
|
||||||
if byte == SLIP_END {
|
if byte == SLIP_END {
|
||||||
if in_frame && !current.is_empty() {
|
if in_frame && !current.is_empty() {
|
||||||
// End of frame
|
|
||||||
frames.push(current.clone());
|
frames.push(current.clone());
|
||||||
current.clear();
|
current.clear();
|
||||||
in_frame = false;
|
in_frame = false;
|
||||||
|
|
@ -131,13 +142,17 @@ fn extract_slip_frames(raw: &[u8]) -> Vec<Vec<u8>> {
|
||||||
} else if in_frame {
|
} else if in_frame {
|
||||||
current.push(byte);
|
current.push(byte);
|
||||||
}
|
}
|
||||||
// If !in_frame and byte != SLIP_END, it's garbage — skip
|
// Bytes outside a frame are garbage — skip
|
||||||
}
|
}
|
||||||
frames
|
frames
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send a command and receive a valid response.
|
/// Send a command and wait for the matching response.
|
||||||
/// Handles boot log garbage and multiple SYNC responses.
|
/// Handles boot log garbage and multiple SYNC echo responses.
|
||||||
|
///
|
||||||
|
/// Response packet layout (from ROM): [0x01][cmd][size:u16][val:u32][data...]
|
||||||
|
/// "data" for most commands is just [status:u8][error:u8][pad:u8][pad:u8].
|
||||||
|
/// For CMD_SPI_FLASH_MD5 from ROM, "data" is [32 ASCII hex bytes][status][error][pad][pad].
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn send_command(
|
fn send_command(
|
||||||
port: &mut Box<dyn SerialPort>,
|
port: &mut Box<dyn SerialPort>,
|
||||||
|
|
@ -154,7 +169,6 @@ fn send_command(
|
||||||
port.flush()
|
port.flush()
|
||||||
.map_err(|e| format!("Flush error: {}", e))?;
|
.map_err(|e| format!("Flush error: {}", e))?;
|
||||||
|
|
||||||
// Read bytes and extract SLIP frames, looking for a valid response
|
|
||||||
let mut raw = Vec::new();
|
let mut raw = Vec::new();
|
||||||
let mut buf = [0u8; 512];
|
let mut buf = [0u8; 512];
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
|
|
@ -168,14 +182,11 @@ fn send_command(
|
||||||
} else {
|
} else {
|
||||||
format!("{} raw bytes, no valid response", raw.len())
|
format!("{} raw bytes, no valid response", raw.len())
|
||||||
};
|
};
|
||||||
return Err(format!("Response timeout (got {})", got));
|
return Err(format!("Response timeout cmd=0x{:02X} ({})", cmd, got));
|
||||||
}
|
}
|
||||||
|
|
||||||
let read_result = port.read(&mut buf);
|
match port.read(&mut buf) {
|
||||||
match read_result {
|
Ok(n) if n > 0 => raw.extend_from_slice(&buf[..n]),
|
||||||
Ok(n) if n > 0 => {
|
|
||||||
raw.extend_from_slice(&buf[..n]);
|
|
||||||
}
|
|
||||||
_ => {
|
_ => {
|
||||||
std::thread::sleep(Duration::from_millis(1));
|
std::thread::sleep(Duration::from_millis(1));
|
||||||
if raw.is_empty() {
|
if raw.is_empty() {
|
||||||
|
|
@ -184,7 +195,6 @@ fn send_command(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to find a valid response in accumulated data
|
|
||||||
let frames = extract_slip_frames(&raw);
|
let frames = extract_slip_frames(&raw);
|
||||||
for slip_data in &frames {
|
for slip_data in &frames {
|
||||||
let decoded = slip_decode(slip_data);
|
let decoded = slip_decode(slip_data);
|
||||||
|
|
@ -200,14 +210,17 @@ fn send_command(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ROM bootloader status is at offset 8 (right after 8-byte header)
|
// Standard response: status at decoded[8], error at decoded[9].
|
||||||
// Format: [dir][cmd][size:u16][value:u32][status][error][pad][pad]
|
// MD5 response has 32 extra bytes before status, but we handle that
|
||||||
|
// in flash_md5sum() by parsing decoded[8..40] separately.
|
||||||
if decoded.len() >= 10 {
|
if decoded.len() >= 10 {
|
||||||
let status = decoded[8];
|
let status = decoded[8];
|
||||||
let error = decoded[9];
|
let error = decoded[9];
|
||||||
if status != 0 {
|
if status != 0 {
|
||||||
return Err(format!("Bootloader error: cmd=0x{:02X} status={}, error={} (0x{:02X})",
|
return Err(format!(
|
||||||
cmd, status, error, error));
|
"Bootloader error: cmd=0x{:02X} status={} error={} (0x{:02X})",
|
||||||
|
cmd, status, error, error
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -222,7 +235,7 @@ fn send_command(
|
||||||
/// Standard auto-reset circuit: DTR→EN, RTS→GPIO0.
|
/// Standard auto-reset circuit: DTR→EN, RTS→GPIO0.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn enter_bootloader(port: &mut Box<dyn SerialPort>) -> Result<(), String> {
|
fn enter_bootloader(port: &mut Box<dyn SerialPort>) -> Result<(), String> {
|
||||||
// Hold GPIO0 low (RTS=true) while pulsing EN (DTR)
|
// Hold GPIO0 low (RTS=true) while pulsing EN low via DTR
|
||||||
port.write_data_terminal_ready(false)
|
port.write_data_terminal_ready(false)
|
||||||
.map_err(|e| format!("DTR error: {}", e))?;
|
.map_err(|e| format!("DTR error: {}", e))?;
|
||||||
port.write_request_to_send(true)
|
port.write_request_to_send(true)
|
||||||
|
|
@ -240,13 +253,9 @@ fn enter_bootloader(port: &mut Box<dyn SerialPort>) -> Result<(), String> {
|
||||||
port.write_data_terminal_ready(false)
|
port.write_data_terminal_ready(false)
|
||||||
.map_err(|e| format!("DTR error: {}", e))?;
|
.map_err(|e| format!("DTR error: {}", e))?;
|
||||||
|
|
||||||
// Wait for ROM to boot and print its banner before we drain it.
|
// esptool DEFAULT_RESET_DELAY = 500 ms — wait for ROM banner before draining
|
||||||
// esptool uses DEFAULT_RESET_DELAY = 500 ms here.
|
|
||||||
// 200 ms is too short — the ROM isn't ready to accept SYNC yet,
|
|
||||||
// causing the first SYNC attempts to fail or receive garbage.
|
|
||||||
std::thread::sleep(Duration::from_millis(500));
|
std::thread::sleep(Duration::from_millis(500));
|
||||||
|
|
||||||
// Drain any boot message
|
|
||||||
let _ = port.clear(serialport::ClearBuffer::All);
|
let _ = port.clear(serialport::ClearBuffer::All);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -256,7 +265,7 @@ fn enter_bootloader(port: &mut Box<dyn SerialPort>) -> Result<(), String> {
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn sync(port: &mut Box<dyn SerialPort>) -> Result<(), String> {
|
fn sync(port: &mut Box<dyn SerialPort>) -> Result<(), String> {
|
||||||
// SYNC payload: [0x07, 0x07, 0x12, 0x20] + 32 x 0x55
|
// SYNC payload: magic header + 32 x 0x55
|
||||||
let mut payload = vec![0x07, 0x07, 0x12, 0x20];
|
let mut payload = vec![0x07, 0x07, 0x12, 0x20];
|
||||||
payload.extend_from_slice(&[0x55; 32]);
|
payload.extend_from_slice(&[0x55; 32]);
|
||||||
|
|
||||||
|
|
@ -265,7 +274,6 @@ fn sync(port: &mut Box<dyn SerialPort>) -> Result<(), String> {
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => return Ok(()),
|
Ok(_) => return Ok(()),
|
||||||
Err(_) if attempt < 9 => {
|
Err(_) if attempt < 9 => {
|
||||||
// Drain any pending data before retry
|
|
||||||
let _ = port.clear(serialport::ClearBuffer::Input);
|
let _ = port.clear(serialport::ClearBuffer::Input);
|
||||||
std::thread::sleep(Duration::from_millis(50));
|
std::thread::sleep(Duration::from_millis(50));
|
||||||
}
|
}
|
||||||
|
|
@ -275,21 +283,22 @@ fn sync(port: &mut Box<dyn SerialPort>) -> Result<(), String> {
|
||||||
Err("SYNC failed".into())
|
Err("SYNC failed".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tell the bootloader to switch to a faster baud rate, then reconnect.
|
/// Tell the bootloader to switch baud rate, then mirror the change on the host side.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn change_baudrate(port: &mut Box<dyn SerialPort>, new_baud: u32) -> Result<(), String> {
|
fn change_baudrate(port: &mut Box<dyn SerialPort>, new_baud: u32) -> Result<(), String> {
|
||||||
// Payload: [new_baud:u32 LE][old_baud:u32 LE] (old_baud=0 means "current")
|
// Payload: [new_baud:u32 LE][old_baud:u32 LE]
|
||||||
|
// old_baud=0 means "current baud" for ROM (not stub — stub passes the real current baud)
|
||||||
let mut payload = Vec::with_capacity(8);
|
let mut payload = Vec::with_capacity(8);
|
||||||
payload.extend_from_slice(&new_baud.to_le_bytes());
|
payload.extend_from_slice(&new_baud.to_le_bytes());
|
||||||
payload.extend_from_slice(&0u32.to_le_bytes());
|
payload.extend_from_slice(&0u32.to_le_bytes());
|
||||||
|
|
||||||
send_command(port, CMD_CHANGE_BAUDRATE, &payload, 0, 3000)?;
|
send_command(port, CMD_CHANGE_BAUDRATE, &payload, 0, 3000)?;
|
||||||
|
|
||||||
// Switch host side to new baud
|
// Switch host side — ROM will already be running at new baud after ACK
|
||||||
port.set_baud_rate(new_baud)
|
port.set_baud_rate(new_baud)
|
||||||
.map_err(|e| format!("Set baud error: {}", e))?;
|
.map_err(|e| format!("Set baud error: {}", e))?;
|
||||||
|
|
||||||
// Small delay for baud switch to take effect
|
// esptool sleeps 50ms + flush after baud change to discard garbage sent during transition
|
||||||
std::thread::sleep(Duration::from_millis(50));
|
std::thread::sleep(Duration::from_millis(50));
|
||||||
let _ = port.clear(serialport::ClearBuffer::All);
|
let _ = port.clear(serialport::ClearBuffer::All);
|
||||||
|
|
||||||
|
|
@ -298,11 +307,44 @@ fn change_baudrate(port: &mut Box<dyn SerialPort>, new_baud: u32) -> Result<(),
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn spi_attach(port: &mut Box<dyn SerialPort>) -> Result<(), String> {
|
fn spi_attach(port: &mut Box<dyn SerialPort>) -> Result<(), String> {
|
||||||
|
// Payload: [hspi_arg:u32 LE=0][is_legacy:u8=0][pad:u8=0][pad:u8=0][pad:u8=0]
|
||||||
|
// 8 bytes total, all zeros for standard SPI attach (not legacy)
|
||||||
let payload = [0u8; 8];
|
let payload = [0u8; 8];
|
||||||
send_command(port, CMD_SPI_ATTACH, &payload, 0, 3000)?;
|
send_command(port, CMD_SPI_ATTACH, &payload, 0, 3000)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Inform the ROM bootloader of the SPI flash chip geometry.
|
||||||
|
///
|
||||||
|
/// This is CMD 0x0B (ESP_SPI_SET_PARAMS). esptool calls this unconditionally
|
||||||
|
/// (for both ROM and stub mode) before any flash_begin.
|
||||||
|
/// Without it, the ROM's internal flash descriptor may describe a smaller chip
|
||||||
|
/// (e.g. 2 MB default), causing it to refuse writes beyond that boundary or to
|
||||||
|
/// erase incorrectly — a silent failure that looks like a successful flash but
|
||||||
|
/// the firmware never boots.
|
||||||
|
///
|
||||||
|
/// Payload: [fl_id:u32][total_size:u32][block_size:u32][sector_size:u32][page_size:u32][status_mask:u32]
|
||||||
|
/// All values match esptool flash_set_parameters() defaults.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn spi_set_params(port: &mut Box<dyn SerialPort>, flash_size_bytes: u32) -> Result<(), String> {
|
||||||
|
let fl_id: u32 = 0;
|
||||||
|
let block_size: u32 = 64 * 1024; // 64 KB erase block
|
||||||
|
let sector_size: u32 = 4 * 1024; // 4 KB sector (minimum erase unit)
|
||||||
|
let page_size: u32 = 256; // 256 byte write page
|
||||||
|
let status_mask: u32 = 0xFFFF;
|
||||||
|
|
||||||
|
let mut payload = Vec::with_capacity(24);
|
||||||
|
payload.extend_from_slice(&fl_id.to_le_bytes());
|
||||||
|
payload.extend_from_slice(&flash_size_bytes.to_le_bytes());
|
||||||
|
payload.extend_from_slice(&block_size.to_le_bytes());
|
||||||
|
payload.extend_from_slice(§or_size.to_le_bytes());
|
||||||
|
payload.extend_from_slice(&page_size.to_le_bytes());
|
||||||
|
payload.extend_from_slice(&status_mask.to_le_bytes());
|
||||||
|
|
||||||
|
send_command(port, CMD_SPI_SET_PARAMS, &payload, 0, 3000)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn flash_begin(
|
fn flash_begin(
|
||||||
port: &mut Box<dyn SerialPort>,
|
port: &mut Box<dyn SerialPort>,
|
||||||
|
|
@ -312,30 +354,28 @@ fn flash_begin(
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let num_blocks = (total_size + block_size - 1) / block_size;
|
let num_blocks = (total_size + block_size - 1) / block_size;
|
||||||
|
|
||||||
// erase_size must be rounded up to flash sector boundary (4 KB).
|
// erase_size must align to sector boundary (4 KB).
|
||||||
// Passing total_size directly causes the ROM to compute the wrong
|
// Passing raw file size causes the ROM to skip erasing the last partial sector.
|
||||||
// sector count — the last sector is never erased, writing into
|
|
||||||
// 0xFF-filled space and producing "invalid segment length 0xffffffff".
|
|
||||||
// This matches esptool's get_erase_size(offset, size) logic.
|
|
||||||
let erase_size = (total_size + FLASH_SECTOR_SIZE - 1) & !(FLASH_SECTOR_SIZE - 1);
|
let erase_size = (total_size + FLASH_SECTOR_SIZE - 1) & !(FLASH_SECTOR_SIZE - 1);
|
||||||
|
|
||||||
let mut payload = Vec::with_capacity(20);
|
let mut payload = Vec::with_capacity(20);
|
||||||
// erase_size (sector-aligned, not raw file size)
|
|
||||||
payload.extend_from_slice(&erase_size.to_le_bytes());
|
payload.extend_from_slice(&erase_size.to_le_bytes());
|
||||||
// num_blocks
|
|
||||||
payload.extend_from_slice(&num_blocks.to_le_bytes());
|
payload.extend_from_slice(&num_blocks.to_le_bytes());
|
||||||
// block_size
|
|
||||||
payload.extend_from_slice(&block_size.to_le_bytes());
|
payload.extend_from_slice(&block_size.to_le_bytes());
|
||||||
// offset
|
|
||||||
payload.extend_from_slice(&offset.to_le_bytes());
|
payload.extend_from_slice(&offset.to_le_bytes());
|
||||||
// encrypted (ESP32-S3 requires this 5th field — 0 = not encrypted)
|
// 5th field: begin_rom_encrypted flag. ESP32-S3 SUPPORTS_ENCRYPTED_FLASH=true,
|
||||||
|
// so the ROM expects this field. 0 = not using ROM-encrypted write mode.
|
||||||
payload.extend_from_slice(&0u32.to_le_bytes());
|
payload.extend_from_slice(&0u32.to_le_bytes());
|
||||||
|
|
||||||
// FLASH_BEGIN can take a while (flash erase) — long timeout
|
// Flash erase can take several seconds — generous timeout
|
||||||
send_command(port, CMD_FLASH_BEGIN, &payload, 0, 30_000)?;
|
send_command(port, CMD_FLASH_BEGIN, &payload, 0, 30_000)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Write one 1024-byte block to flash with up to WRITE_BLOCK_ATTEMPTS retries.
|
||||||
|
///
|
||||||
|
/// Payload format: [data_len:u32][seq:u32][reserved:u32=0][reserved:u32=0][data...]
|
||||||
|
/// Checksum = XOR of data bytes seeded with 0xEF (placed in the command header value field).
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn flash_data(
|
fn flash_data(
|
||||||
port: &mut Box<dyn SerialPort>,
|
port: &mut Box<dyn SerialPort>,
|
||||||
|
|
@ -345,30 +385,45 @@ fn flash_data(
|
||||||
let data_len = data.len() as u32;
|
let data_len = data.len() as u32;
|
||||||
|
|
||||||
let mut payload = Vec::with_capacity(16 + data.len());
|
let mut payload = Vec::with_capacity(16 + data.len());
|
||||||
// data length
|
|
||||||
payload.extend_from_slice(&data_len.to_le_bytes());
|
payload.extend_from_slice(&data_len.to_le_bytes());
|
||||||
// sequence number
|
|
||||||
payload.extend_from_slice(&seq.to_le_bytes());
|
payload.extend_from_slice(&seq.to_le_bytes());
|
||||||
// reserved (2 x u32)
|
payload.extend_from_slice(&0u32.to_le_bytes()); // reserved
|
||||||
payload.extend_from_slice(&0u32.to_le_bytes());
|
payload.extend_from_slice(&0u32.to_le_bytes()); // reserved
|
||||||
payload.extend_from_slice(&0u32.to_le_bytes());
|
|
||||||
// data
|
|
||||||
payload.extend_from_slice(data);
|
payload.extend_from_slice(data);
|
||||||
|
|
||||||
let checksum = xor_checksum(data);
|
let checksum = xor_checksum(data);
|
||||||
send_command(port, CMD_FLASH_DATA, &payload, checksum, 10_000)?;
|
|
||||||
Ok(())
|
// With the ROM (not stub), the chip writes the block to flash synchronously
|
||||||
|
// before ACKing. 10 s covers the worst case (slow flash chip + full sector erase).
|
||||||
|
for attempt in 0..WRITE_BLOCK_ATTEMPTS {
|
||||||
|
match send_command(port, CMD_FLASH_DATA, &payload, checksum, 10_000) {
|
||||||
|
Ok(_) => return Ok(()),
|
||||||
|
Err(e) if attempt < WRITE_BLOCK_ATTEMPTS - 1 => {
|
||||||
|
// Drain input before retry
|
||||||
|
let _ = port.clear(serialport::ClearBuffer::Input);
|
||||||
|
std::thread::sleep(Duration::from_millis(10));
|
||||||
|
let _ = e; // suppress unused warning
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return Err(format!("FLASH_DATA seq={} failed after {} attempts: {}", seq, WRITE_BLOCK_ATTEMPTS, e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Unreachable but required for the type checker
|
||||||
|
Err(format!("FLASH_DATA seq={} failed", seq))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn flash_end(port: &mut Box<dyn SerialPort>, reboot: bool) -> Result<(), String> {
|
fn flash_end(port: &mut Box<dyn SerialPort>, reboot: bool) -> Result<(), String> {
|
||||||
|
// flag=0 → run app (reboot); flag=1 → stay in bootloader
|
||||||
let flag: u32 = if reboot { 0 } else { 1 };
|
let flag: u32 = if reboot { 0 } else { 1 };
|
||||||
let payload = flag.to_le_bytes();
|
let payload = flag.to_le_bytes();
|
||||||
// FLASH_END might not get a response if device reboots
|
|
||||||
|
// May not get a response if the device reboots before ACKing
|
||||||
let _ = send_command(port, CMD_FLASH_END, &payload, 0, 2000);
|
let _ = send_command(port, CMD_FLASH_END, &payload, 0, 2000);
|
||||||
|
|
||||||
if reboot {
|
if reboot {
|
||||||
// Hard reset: toggle RTS to pulse EN pin (like esptool --after hard_reset)
|
// Hard reset: pulse RTS low to trigger EN pin (like esptool --after hard_reset)
|
||||||
std::thread::sleep(Duration::from_millis(100));
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
port.write_request_to_send(true)
|
port.write_request_to_send(true)
|
||||||
.map_err(|e| format!("RTS error: {}", e))?;
|
.map_err(|e| format!("RTS error: {}", e))?;
|
||||||
|
|
@ -380,10 +435,109 @@ fn flash_end(port: &mut Box<dyn SerialPort>, reboot: bool) -> Result<(), String>
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Verify flash contents using the ROM MD5 command (CMD 0x13).
|
||||||
|
///
|
||||||
|
/// The ROM bootloader (not stub) returns 32 ASCII hex characters in the response
|
||||||
|
/// data field (bytes [8..40] of the decoded packet), followed by the standard
|
||||||
|
/// [status][error] bytes at [40..42].
|
||||||
|
///
|
||||||
|
/// The stub returns 16 binary bytes instead. Since we talk to the ROM directly
|
||||||
|
/// we parse the 32-char ASCII format.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn flash_md5sum(
|
||||||
|
port: &mut Box<dyn SerialPort>,
|
||||||
|
addr: u32,
|
||||||
|
size: u32,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let mut payload = Vec::with_capacity(16);
|
||||||
|
payload.extend_from_slice(&addr.to_le_bytes());
|
||||||
|
payload.extend_from_slice(&size.to_le_bytes());
|
||||||
|
payload.extend_from_slice(&0u32.to_le_bytes()); // reserved
|
||||||
|
payload.extend_from_slice(&0u32.to_le_bytes()); // reserved
|
||||||
|
|
||||||
|
// MD5 of a 1 MB image takes ~8 s on the ROM. Scale generously.
|
||||||
|
let timeout_ms = 8_000 + (size as u64 * 8 / 1_000_000).max(3_000);
|
||||||
|
|
||||||
|
// The ROM's MD5 response carries 32 extra bytes before the status word,
|
||||||
|
// so the standard send_command() status check at decoded[8] would read into
|
||||||
|
// the MD5 data. We must use a longer response path. To keep it simple we
|
||||||
|
// bypass send_command() and call into the raw SLIP layer here.
|
||||||
|
let pkt = build_command(CMD_SPI_FLASH_MD5, &payload, 0);
|
||||||
|
let frame = slip_encode(&pkt);
|
||||||
|
|
||||||
|
port.write_all(&frame)
|
||||||
|
.map_err(|e| format!("Write error (MD5): {}", e))?;
|
||||||
|
port.flush()
|
||||||
|
.map_err(|e| format!("Flush error (MD5): {}", e))?;
|
||||||
|
|
||||||
|
let mut raw = Vec::new();
|
||||||
|
let mut buf = [0u8; 512];
|
||||||
|
let start = Instant::now();
|
||||||
|
let timeout = Duration::from_millis(timeout_ms);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if start.elapsed() > timeout {
|
||||||
|
return Err("MD5 command timeout".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
match port.read(&mut buf) {
|
||||||
|
Ok(n) if n > 0 => raw.extend_from_slice(&buf[..n]),
|
||||||
|
_ => {
|
||||||
|
std::thread::sleep(Duration::from_millis(5));
|
||||||
|
if raw.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let frames = extract_slip_frames(&raw);
|
||||||
|
for slip_data in &frames {
|
||||||
|
let decoded = slip_decode(slip_data);
|
||||||
|
|
||||||
|
// Minimum: 8-byte header + 32 ASCII hex bytes + 2 status bytes = 42 bytes
|
||||||
|
if decoded.len() < 42 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if decoded[0] != 0x01 || decoded[1] != CMD_SPI_FLASH_MD5 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status bytes are at offset 40 (after 8-byte header + 32 MD5 bytes)
|
||||||
|
let status = decoded[40];
|
||||||
|
let error = decoded[41];
|
||||||
|
if status != 0 {
|
||||||
|
return Err(format!("MD5 command error: status={} error=0x{:02X}", status, error));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract 32 ASCII hex chars from decoded[8..40]
|
||||||
|
let md5_ascii = &decoded[8..40];
|
||||||
|
let md5_str = std::str::from_utf8(md5_ascii)
|
||||||
|
.map_err(|_| "MD5 response is not valid UTF-8".to_string())?
|
||||||
|
.to_lowercase();
|
||||||
|
|
||||||
|
return Ok(md5_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== Main entry point ====================
|
// ==================== Main entry point ====================
|
||||||
|
|
||||||
/// Flash firmware to ESP32 via programming port (CH340/CP2102).
|
/// Flash firmware to ESP32 via programming port (CH340/CP2102).
|
||||||
/// Sends progress updates via the channel as (progress_0_to_1, status_message).
|
///
|
||||||
|
/// Sequence (mirrors what esptool does in --no-stub ROM mode):
|
||||||
|
/// 1. Enter bootloader via DTR/RTS reset sequence
|
||||||
|
/// 2. SYNC at 115200
|
||||||
|
/// 3. CHANGE_BAUDRATE to 460800
|
||||||
|
/// 4. SPI_ATTACH
|
||||||
|
/// 5. SPI_SET_PARAMS (16 MB flash geometry) — critical, was missing
|
||||||
|
/// 6. FLASH_BEGIN (erases target region)
|
||||||
|
/// 7. FLASH_DATA (1024-byte blocks, with per-block retry)
|
||||||
|
/// 8. FLASH_END (reboot)
|
||||||
|
/// 9. SPI_FLASH_MD5 post-write verification — re-enters bootloader for this
|
||||||
|
///
|
||||||
|
/// Note: esptool normally uploads a "stub" to RAM before flashing.
|
||||||
|
/// We skip that and talk directly to the ROM, which is slower but simpler
|
||||||
|
/// and does not require the stub binary to be bundled.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub fn flash_firmware(
|
pub fn flash_firmware(
|
||||||
port_name: &str,
|
port_name: &str,
|
||||||
|
|
@ -395,6 +549,24 @@ pub fn flash_firmware(
|
||||||
let _ = tx.send(FlashProgress::OtaProgress(progress, msg));
|
let _ = tx.send(FlashProgress::OtaProgress(progress, msg));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ //
|
||||||
|
// Precondition: firmware must be a multiple of 4 bytes. //
|
||||||
|
// esptool pads to 4 bytes: pad_to(image, 4) //
|
||||||
|
// ------------------------------------------------------------------ //
|
||||||
|
let total_size_raw = firmware.len() as u32;
|
||||||
|
let padded_len = ((total_size_raw + 3) & !3) as usize;
|
||||||
|
// Always work with an owned buffer so the padded slice has a stable lifetime.
|
||||||
|
let mut firmware_padded = firmware.to_vec();
|
||||||
|
firmware_padded.resize(padded_len, 0xFF);
|
||||||
|
let firmware_to_flash: &[u8] = &firmware_padded;
|
||||||
|
let total_size = firmware_to_flash.len() as u32;
|
||||||
|
|
||||||
|
// Compute the reference MD5 before we start (over the padded data)
|
||||||
|
let expected_md5 = md5_of(firmware_to_flash);
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ //
|
||||||
|
// Step 1: Open port + enter bootloader //
|
||||||
|
// ------------------------------------------------------------------ //
|
||||||
send_progress(0.0, "Opening port...".into());
|
send_progress(0.0, "Opening port...".into());
|
||||||
|
|
||||||
let builder = serialport::new(port_name, INITIAL_BAUD);
|
let builder = serialport::new(port_name, INITIAL_BAUD);
|
||||||
|
|
@ -402,34 +574,56 @@ pub fn flash_firmware(
|
||||||
let mut port = builder_timeout.open()
|
let mut port = builder_timeout.open()
|
||||||
.map_err(|e| format!("Cannot open {}: {}", port_name, e))?;
|
.map_err(|e| format!("Cannot open {}: {}", port_name, e))?;
|
||||||
|
|
||||||
// Step 1: Enter bootloader
|
|
||||||
send_progress(0.0, "Resetting into bootloader...".into());
|
send_progress(0.0, "Resetting into bootloader...".into());
|
||||||
enter_bootloader(&mut port)?;
|
enter_bootloader(&mut port)?;
|
||||||
|
|
||||||
// Step 2: Sync at 115200
|
// ------------------------------------------------------------------ //
|
||||||
send_progress(0.0, "Syncing with bootloader...".into());
|
// Step 2: SYNC at 115200 //
|
||||||
|
// ------------------------------------------------------------------ //
|
||||||
|
send_progress(0.01, "Syncing with bootloader...".into());
|
||||||
sync(&mut port)?;
|
sync(&mut port)?;
|
||||||
send_progress(0.02, "Bootloader sync OK".into());
|
send_progress(0.02, "Bootloader sync OK".into());
|
||||||
|
|
||||||
// Step 3: Switch to 460800 baud for faster flashing
|
// ------------------------------------------------------------------ //
|
||||||
|
// Step 3: Switch to 460800 baud //
|
||||||
|
// ------------------------------------------------------------------ //
|
||||||
send_progress(0.03, format!("Switching to {} baud...", FLASH_BAUD));
|
send_progress(0.03, format!("Switching to {} baud...", FLASH_BAUD));
|
||||||
change_baudrate(&mut port, FLASH_BAUD)?;
|
change_baudrate(&mut port, FLASH_BAUD)?;
|
||||||
send_progress(0.04, format!("Baud: {}", FLASH_BAUD));
|
send_progress(0.04, format!("Baud: {}", FLASH_BAUD));
|
||||||
|
|
||||||
// Step 4: SPI attach
|
// ------------------------------------------------------------------ //
|
||||||
|
// Step 4: SPI attach //
|
||||||
|
// ------------------------------------------------------------------ //
|
||||||
send_progress(0.05, "Attaching SPI flash...".into());
|
send_progress(0.05, "Attaching SPI flash...".into());
|
||||||
spi_attach(&mut port)?;
|
spi_attach(&mut port)?;
|
||||||
|
|
||||||
// Step 5: Flash begin (this erases the flash — can take several seconds)
|
// ------------------------------------------------------------------ //
|
||||||
let total_size = firmware.len() as u32;
|
// Step 5: SPI_SET_PARAMS — inform ROM of flash chip geometry //
|
||||||
|
// //
|
||||||
|
// THIS IS THE MISSING STEP. Without it the ROM's internal flash //
|
||||||
|
// descriptor keeps its power-on-reset default (often 2 MB). Writes //
|
||||||
|
// beyond that boundary are silently dropped or cause the ROM to erase //
|
||||||
|
// wrong sectors, producing a binary that passes the progress bar but //
|
||||||
|
// the bootloader refuses to map into the MMU. //
|
||||||
|
// ------------------------------------------------------------------ //
|
||||||
|
send_progress(0.06, "Setting SPI flash parameters (16 MB)...".into());
|
||||||
|
const FLASH_SIZE_16MB: u32 = 16 * 1024 * 1024;
|
||||||
|
spi_set_params(&mut port, FLASH_SIZE_16MB)?;
|
||||||
|
send_progress(0.07, "SPI flash configured".into());
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ //
|
||||||
|
// Step 6: FLASH_BEGIN (erases the target region) //
|
||||||
|
// ------------------------------------------------------------------ //
|
||||||
let num_blocks = (total_size + FLASH_BLOCK_SIZE - 1) / FLASH_BLOCK_SIZE;
|
let num_blocks = (total_size + FLASH_BLOCK_SIZE - 1) / FLASH_BLOCK_SIZE;
|
||||||
send_progress(0.05, format!("Erasing flash ({} KB)...", total_size / 1024));
|
send_progress(0.08, format!("Erasing flash region ({} KB at 0x{:X})...", total_size / 1024, offset));
|
||||||
flash_begin(&mut port, offset, total_size, FLASH_BLOCK_SIZE)?;
|
flash_begin(&mut port, offset, total_size, FLASH_BLOCK_SIZE)?;
|
||||||
send_progress(0.10, "Flash erased, writing...".into());
|
send_progress(0.10, "Flash erased, writing...".into());
|
||||||
|
|
||||||
// Step 6: Flash data blocks
|
// ------------------------------------------------------------------ //
|
||||||
for (i, chunk) in firmware.chunks(FLASH_BLOCK_SIZE as usize).enumerate() {
|
// Step 7: FLASH_DATA blocks //
|
||||||
// Pad last block to block_size
|
// ------------------------------------------------------------------ //
|
||||||
|
for (i, chunk) in firmware_to_flash.chunks(FLASH_BLOCK_SIZE as usize).enumerate() {
|
||||||
|
// Pad the last block to exactly FLASH_BLOCK_SIZE with 0xFF
|
||||||
let mut block = chunk.to_vec();
|
let mut block = chunk.to_vec();
|
||||||
let pad_needed = FLASH_BLOCK_SIZE as usize - block.len();
|
let pad_needed = FLASH_BLOCK_SIZE as usize - block.len();
|
||||||
if pad_needed > 0 {
|
if pad_needed > 0 {
|
||||||
|
|
@ -439,23 +633,167 @@ pub fn flash_firmware(
|
||||||
flash_data(&mut port, i as u32, &block)?;
|
flash_data(&mut port, i as u32, &block)?;
|
||||||
|
|
||||||
let blocks_done = (i + 1) as f32;
|
let blocks_done = (i + 1) as f32;
|
||||||
let total_blocks = num_blocks as f32;
|
let progress = 0.10 + 0.82 * (blocks_done / num_blocks as f32);
|
||||||
let progress = 0.10 + 0.85 * (blocks_done / total_blocks);
|
let written_kb = ((i as u32 + 1) * FLASH_BLOCK_SIZE).min(total_size) / 1024;
|
||||||
let msg = format!("Writing block {}/{} ({} KB / {} KB)",
|
let total_kb = total_size / 1024;
|
||||||
i + 1, num_blocks,
|
send_progress(progress, format!(
|
||||||
((i + 1) as u32 * FLASH_BLOCK_SIZE).min(total_size) / 1024,
|
"Writing block {}/{} ({}/{} KB)",
|
||||||
total_size / 1024);
|
i + 1, num_blocks, written_kb, total_kb
|
||||||
send_progress(progress, msg);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 7: Flash end + reboot
|
// ------------------------------------------------------------------ //
|
||||||
send_progress(0.97, "Finalizing...".into());
|
// Step 8: FLASH_END //
|
||||||
flash_end(&mut port, true)?;
|
// //
|
||||||
|
// We pass reboot=false here so the chip stays in bootloader mode for //
|
||||||
|
// the MD5 verification step that follows. A hard reset is done after. //
|
||||||
|
// ------------------------------------------------------------------ //
|
||||||
|
send_progress(0.93, "Finalizing write...".into());
|
||||||
|
flash_end(&mut port, false)?;
|
||||||
|
|
||||||
send_progress(1.0, format!("Flash OK — {} KB written at 0x{:X}", total_size / 1024, offset));
|
// ------------------------------------------------------------------ //
|
||||||
|
// Step 9: MD5 post-write verification //
|
||||||
|
// //
|
||||||
|
// The ROM computes MD5 over the flash region we just wrote and returns //
|
||||||
|
// it as 32 ASCII hex characters. We compare against the MD5 we //
|
||||||
|
// computed locally over the padded firmware before sending it. //
|
||||||
|
// A mismatch at this point means data was corrupted in transit or the //
|
||||||
|
// chip is not responding correctly — the previous "success" was a lie. //
|
||||||
|
// ------------------------------------------------------------------ //
|
||||||
|
send_progress(0.94, "Verifying flash MD5...".into());
|
||||||
|
match flash_md5sum(&mut port, offset, total_size) {
|
||||||
|
Ok(flash_md5) => {
|
||||||
|
if flash_md5 != expected_md5 {
|
||||||
|
return Err(format!(
|
||||||
|
"MD5 mismatch — flash corrupt!\n expected: {}\n got: {}",
|
||||||
|
expected_md5, flash_md5
|
||||||
|
));
|
||||||
|
}
|
||||||
|
send_progress(0.97, format!("MD5 OK: {}", flash_md5));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// Non-fatal: log the warning but don't abort the flash.
|
||||||
|
// Some boards reset before we can query MD5.
|
||||||
|
send_progress(0.97, format!("Warning: MD5 check failed ({}), rebooting anyway", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ //
|
||||||
|
// Step 10: Hard reset to run the new firmware //
|
||||||
|
// ------------------------------------------------------------------ //
|
||||||
|
send_progress(0.98, "Rebooting...".into());
|
||||||
|
// Pulse RTS to trigger EN (same as esptool HardReset)
|
||||||
|
port.write_request_to_send(true)
|
||||||
|
.map_err(|e| format!("RTS error: {}", e))?;
|
||||||
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
|
port.write_request_to_send(false)
|
||||||
|
.map_err(|e| format!("RTS error: {}", e))?;
|
||||||
|
|
||||||
|
send_progress(1.0, format!(
|
||||||
|
"Flash OK — {} KB written at 0x{:X}, MD5 verified",
|
||||||
|
total_size / 1024, offset
|
||||||
|
));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== Internal helpers ====================
|
||||||
|
|
||||||
|
/// Pure-Rust MD5 implementation (no external crate required).
|
||||||
|
/// Based on RFC 1321. Returns a lowercase 32-character hex string.
|
||||||
|
/// Used to compute the reference digest over the firmware image before
|
||||||
|
/// sending, for post-write comparison.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn md5_of(data: &[u8]) -> String {
|
||||||
|
// Per-round shift amounts
|
||||||
|
const S: [u32; 64] = [
|
||||||
|
7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
|
||||||
|
5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
|
||||||
|
4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
|
||||||
|
6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Precomputed table K[i] = floor(abs(sin(i+1)) * 2^32)
|
||||||
|
const K: [u32; 64] = [
|
||||||
|
0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee,
|
||||||
|
0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
|
||||||
|
0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be,
|
||||||
|
0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
|
||||||
|
0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa,
|
||||||
|
0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
|
||||||
|
0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed,
|
||||||
|
0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
|
||||||
|
0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c,
|
||||||
|
0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
|
||||||
|
0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05,
|
||||||
|
0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
|
||||||
|
0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039,
|
||||||
|
0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
|
||||||
|
0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1,
|
||||||
|
0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391,
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut a0: u32 = 0x67452301;
|
||||||
|
let mut b0: u32 = 0xefcdab89;
|
||||||
|
let mut c0: u32 = 0x98badcfe;
|
||||||
|
let mut d0: u32 = 0x10325476;
|
||||||
|
|
||||||
|
// Pre-processing: add bit-length suffix per RFC 1321
|
||||||
|
let bit_len = (data.len() as u64).wrapping_mul(8);
|
||||||
|
let mut msg = data.to_vec();
|
||||||
|
msg.push(0x80);
|
||||||
|
while msg.len() % 64 != 56 {
|
||||||
|
msg.push(0x00);
|
||||||
|
}
|
||||||
|
msg.extend_from_slice(&bit_len.to_le_bytes());
|
||||||
|
|
||||||
|
// Process each 512-bit (64-byte) chunk
|
||||||
|
for chunk in msg.chunks(64) {
|
||||||
|
let mut m = [0u32; 16];
|
||||||
|
for (i, word_bytes) in chunk.chunks(4).enumerate() {
|
||||||
|
m[i] = u32::from_le_bytes([word_bytes[0], word_bytes[1], word_bytes[2], word_bytes[3]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (mut a, mut b, mut c, mut d) = (a0, b0, c0, d0);
|
||||||
|
|
||||||
|
for i in 0usize..64 {
|
||||||
|
let (f, g): (u32, usize) = match i {
|
||||||
|
0..=15 => ((b & c) | ((!b) & d), i),
|
||||||
|
16..=31 => ((d & b) | ((!d) & c), (5 * i + 1) % 16),
|
||||||
|
32..=47 => (b ^ c ^ d, (3 * i + 5) % 16),
|
||||||
|
_ => (c ^ (b | (!d)), (7 * i) % 16),
|
||||||
|
};
|
||||||
|
let temp = d;
|
||||||
|
d = c;
|
||||||
|
c = b;
|
||||||
|
b = b.wrapping_add(
|
||||||
|
a.wrapping_add(f)
|
||||||
|
.wrapping_add(K[i])
|
||||||
|
.wrapping_add(m[g])
|
||||||
|
.rotate_left(S[i])
|
||||||
|
);
|
||||||
|
a = temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
a0 = a0.wrapping_add(a);
|
||||||
|
b0 = b0.wrapping_add(b);
|
||||||
|
c0 = c0.wrapping_add(c);
|
||||||
|
d0 = d0.wrapping_add(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize as standard MD5 hex string (bytes left-to-right in little-endian word order)
|
||||||
|
let mut result = [0u8; 16];
|
||||||
|
result[0..4].copy_from_slice(&a0.to_le_bytes());
|
||||||
|
result[4..8].copy_from_slice(&b0.to_le_bytes());
|
||||||
|
result[8..12].copy_from_slice(&c0.to_le_bytes());
|
||||||
|
result[12..16].copy_from_slice(&d0.to_le_bytes());
|
||||||
|
|
||||||
|
let mut hex = String::with_capacity(32);
|
||||||
|
for byte in &result {
|
||||||
|
hex.push_str(&format!("{:02x}", byte));
|
||||||
|
}
|
||||||
|
hex
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== Tests ====================
|
// ==================== Tests ====================
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
@ -520,4 +858,63 @@ mod tests {
|
||||||
assert_eq!(pkt[8], 0xAA); // data
|
assert_eq!(pkt[8], 0xAA); // data
|
||||||
assert_eq!(pkt[9], 0xBB);
|
assert_eq!(pkt[9], 0xBB);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spi_set_params_payload_length() {
|
||||||
|
// Payload must be exactly 24 bytes (6 x u32)
|
||||||
|
let flash_size: u32 = 16 * 1024 * 1024;
|
||||||
|
let mut payload = Vec::with_capacity(24);
|
||||||
|
payload.extend_from_slice(&0u32.to_le_bytes()); // fl_id
|
||||||
|
payload.extend_from_slice(&flash_size.to_le_bytes()); // total_size
|
||||||
|
payload.extend_from_slice(&(64u32 * 1024).to_le_bytes()); // block_size
|
||||||
|
payload.extend_from_slice(&(4u32 * 1024).to_le_bytes()); // sector_size
|
||||||
|
payload.extend_from_slice(&256u32.to_le_bytes()); // page_size
|
||||||
|
payload.extend_from_slice(&0xFFFFu32.to_le_bytes()); // status_mask
|
||||||
|
assert_eq!(payload.len(), 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn flash_begin_payload_has_5_fields() {
|
||||||
|
// ESP32-S3 flash_begin must have 5 fields (20 bytes), not 4 (16 bytes)
|
||||||
|
let erase_size: u32 = 0x1000;
|
||||||
|
let num_blocks: u32 = 1;
|
||||||
|
let block_size: u32 = FLASH_BLOCK_SIZE;
|
||||||
|
let offset: u32 = 0x20000;
|
||||||
|
let encrypted: u32 = 0;
|
||||||
|
let mut payload = Vec::with_capacity(20);
|
||||||
|
payload.extend_from_slice(&erase_size.to_le_bytes());
|
||||||
|
payload.extend_from_slice(&num_blocks.to_le_bytes());
|
||||||
|
payload.extend_from_slice(&block_size.to_le_bytes());
|
||||||
|
payload.extend_from_slice(&offset.to_le_bytes());
|
||||||
|
payload.extend_from_slice(&encrypted.to_le_bytes());
|
||||||
|
assert_eq!(payload.len(), 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn md5_empty_string() {
|
||||||
|
// RFC 1321 test vector: MD5("") = d41d8cd98f00b204e9800998ecf8427e
|
||||||
|
assert_eq!(md5_of(b""), "d41d8cd98f00b204e9800998ecf8427e");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn md5_abc() {
|
||||||
|
// RFC 1321 test vector: MD5("abc") = 900150983cd24fb0d6963f7d28e17f72
|
||||||
|
assert_eq!(md5_of(b"abc"), "900150983cd24fb0d6963f7d28e17f72");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn md5_known_long() {
|
||||||
|
// RFC 1321: MD5("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq")
|
||||||
|
// = 8215ef0796a20bcaaae116d3876c664a
|
||||||
|
let input = b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq";
|
||||||
|
assert_eq!(md5_of(input), "8215ef0796a20bcaaae116d3876c664a");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn firmware_padding_to_4_bytes() {
|
||||||
|
// Firmware of odd length must be padded to 4-byte boundary with 0xFF
|
||||||
|
let fw = vec![0xE9u8; 5]; // 5 bytes, not aligned
|
||||||
|
let padded_len = (fw.len() as u32 + 3) & !3;
|
||||||
|
assert_eq!(padded_len, 8);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue