11import logging
2+ import shutil
3+ from pathlib import Path
24
35import esptool
46from pytest_embedded_serial_esp .serial import EspSerial
57
68from .app import ArduinoApp
79
10+ _ALWAYS_FLASH = {'boot_app0.bin' }
11+ """Binaries that must always be fully flashed (never receive a --diff-with ref).
12+
13+ boot_app0.bin selects the active OTA partition slot. If the user performed an
14+ OTA update since the last flash, the on-chip copy will differ from the reference
15+ even though our local copy has not changed.
16+ """
17+
818
919class ArduinoSerial (EspSerial ):
1020 """
@@ -19,14 +29,26 @@ def __init__(
1929 self ,
2030 app : ArduinoApp ,
2131 target : str | None = None ,
32+ fast_flash : bool = True ,
2233 ** kwargs ,
2334 ) -> None :
2435 self .app = app
36+ self .fast_flash = fast_flash
2537 super ().__init__ (
2638 target = target or self .app .target ,
2739 ** kwargs ,
2840 )
2941
42+ def _ref_path (self , binary : str ) -> Path :
43+ """Return the ``*_flashed.bin`` reference path for *binary* inside the build dir."""
44+ p = Path (binary )
45+ return Path (self .app .binary_path ) / (p .stem + '_flashed' + p .suffix )
46+
47+ @property
48+ def _ref_binaries (self ) -> list [Path ]:
49+ """All potential reference files for the current flash_files list."""
50+ return [self ._ref_path (path ) for _ , path in self .app .flash_files ]
51+
3052 def _start (self ):
3153 if self .skip_autoflash :
3254 logging .info ('Skipping auto flash...' )
@@ -37,7 +59,16 @@ def _start(self):
3759 @EspSerial .use_esptool ()
3860 def flash (self ) -> None :
3961 """
40- Flash the binary files to the board.
62+ Flash individual binary files to the board.
63+
64+ Uses esptool's ``--diff-with`` for fast reflashing when reference binaries
65+ from the previous successful flash are available, writing only changed
66+ 4 KB sectors. References are saved after each successful flash and
67+ invalidated by :meth:`erase_flash`.
68+
69+ Unlike the merged-binary approach, individual binaries do not overlap
70+ with writable flash regions (NVS, OTA data, etc.), so the post-flash
71+ MD5 verification succeeds and ``--diff-with`` works correctly.
4172 """
4273
4374 flash_settings = []
@@ -48,17 +79,78 @@ def flash(self) -> None:
4879 if self .esp_flash_force :
4980 flash_settings .append ('--force' )
5081
82+ addr_file_pairs : list [str ] = []
83+ diff_args : list [str ] = []
84+ have_any_ref = False
85+
86+ for addr , binary in self .app .flash_files :
87+ addr_file_pairs .extend ([addr , binary ])
88+
89+ if not self .fast_flash :
90+ continue
91+
92+ name = Path (binary ).name
93+ if name in _ALWAYS_FLASH :
94+ diff_args .append ('skip' )
95+ continue
96+
97+ ref = self ._ref_path (binary )
98+ if ref .exists ():
99+ diff_args .append (str (ref ))
100+ have_any_ref = True
101+ else :
102+ diff_args .append ('skip' )
103+
104+ if self .fast_flash :
105+ if have_any_ref :
106+ logging .info ('fast-flash: reflashing with references for %d/%d binaries' ,
107+ sum (1 for d in diff_args if d != 'skip' ), len (diff_args ))
108+ else :
109+ diff_args = []
110+ logging .info ('fast-flash: no references found, performing full flash' )
111+ else :
112+ logging .info ('fast-flash: disabled, performing full flash' )
113+
114+ diff_with = ['--diff-with' , * diff_args ] if diff_args else []
115+
51116 try :
52117 esptool .main (
53118 [
54119 '--chip' ,
55120 self .app .target ,
56121 'write-flash' ,
57- '0x0' , # Merged binary is flashed at offset 0
58- self .app .binary_file ,
122+ * addr_file_pairs ,
59123 * flash_settings ,
124+ * diff_with ,
60125 ],
61126 esp = self .esp ,
62127 )
63128 except Exception :
64129 raise
130+ else :
131+ if self .fast_flash :
132+ for _ , binary in self .app .flash_files :
133+ ref = self ._ref_path (binary )
134+ try :
135+ if Path (binary ).exists ():
136+ shutil .copy2 (binary , ref )
137+ except OSError as e :
138+ logging .warning (
139+ 'fast-flash: could not save reference for %s (%s)' ,
140+ Path (binary ).name ,
141+ e ,
142+ )
143+
144+ def erase_flash (self , force : bool = False ) -> None :
145+ """
146+ Erase the complete flash and invalidate all fast-flash reference binaries.
147+ """
148+ super ().erase_flash (force = force )
149+ if self .fast_flash :
150+ for ref in self ._ref_binaries :
151+ if ref .exists ():
152+ try :
153+ ref .unlink ()
154+ logging .debug ('fast-flash: removed reference %s after erase' , ref .name )
155+ except OSError as e :
156+ logging .warning ('fast-flash: could not remove reference %s (%s)' , ref .name , e )
0 commit comments