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,84 @@ 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 (
107+ 'fast-flash: reflashing with references for %d/%d binaries' ,
108+ sum (1 for d in diff_args if d != 'skip' ),
109+ len (diff_args ),
110+ )
111+ else :
112+ diff_args = []
113+ logging .info ('fast-flash: no references found, performing full flash' )
114+ else :
115+ logging .info ('fast-flash: disabled, performing full flash' )
116+
117+ diff_with = ['--diff-with' , * diff_args ] if diff_args else []
118+
51119 try :
52120 esptool .main (
53121 [
54122 '--chip' ,
55123 self .app .target ,
56124 'write-flash' ,
57- '0x0' , # Merged binary is flashed at offset 0
58- self .app .binary_file ,
125+ * addr_file_pairs ,
59126 * flash_settings ,
127+ * diff_with ,
60128 ],
61129 esp = self .esp ,
62130 )
63131 except Exception :
64132 raise
133+ else :
134+ # Save copies of each binary as *_flashed.bin references so the
135+ # next invocation of flash() can pass them to --diff-with and only
136+ # write the 4 KB sectors that actually changed.
137+ if self .fast_flash :
138+ for _ , binary in self .app .flash_files :
139+ ref = self ._ref_path (binary )
140+ try :
141+ if Path (binary ).exists ():
142+ shutil .copy2 (binary , ref )
143+ except OSError as e :
144+ logging .warning (
145+ 'fast-flash: could not save reference for %s (%s)' ,
146+ Path (binary ).name ,
147+ e ,
148+ )
149+
150+ def erase_flash (self , force : bool = False ) -> None :
151+ """
152+ Erase the complete flash and invalidate all fast-flash reference binaries.
153+ """
154+ super ().erase_flash (force = force )
155+ if self .fast_flash :
156+ for ref in self ._ref_binaries :
157+ if ref .exists ():
158+ try :
159+ ref .unlink ()
160+ logging .debug ('fast-flash: removed reference %s after erase' , ref .name )
161+ except OSError as e :
162+ logging .warning ('fast-flash: could not remove reference %s (%s)' , ref .name , e )
0 commit comments