1- """Main setup screen: chip selector, file picker, port selector, start button ."""
1+ """Main setup screen: chip selector, firmware (auto-download or local), port selector ."""
22
33from __future__ import annotations
44
1818 Checkbox ,
1919)
2020
21+ from defib .firmware import has_firmware , download_firmware , get_cached_path
2122from defib .profiles .loader import list_all_chips
2223
2324
2425def _get_serial_ports () -> list [tuple [str , str ]]:
2526 """Get available serial ports as (label, value) tuples."""
2627 try :
2728 from serial .tools .list_ports import comports
28- # Filter out ghost/placeholder ports (no USB vendor ID = not real)
2929 ports = sorted (
3030 [p for p in comports () if p .vid is not None ],
3131 key = lambda p : p .device ,
@@ -47,9 +47,9 @@ class MainScreen(Screen[None]):
4747 }
4848
4949 #form-container {
50- width: 70 ;
50+ width: 74 ;
5151 height: auto;
52- max-height: 30 ;
52+ max-height: 34 ;
5353 border: thick $accent;
5454 padding: 1 2;
5555 background: $panel;
@@ -72,6 +72,18 @@ class MainScreen(Screen[None]):
7272 width: 100%;
7373 }
7474
75+ #fw-status {
76+ height: 1;
77+ color: $success;
78+ margin-top: 0;
79+ }
80+
81+ #fw-hint {
82+ height: 1;
83+ color: $text-muted;
84+ text-style: italic;
85+ }
86+
7587 #button-row {
7688 margin-top: 1;
7789 align: center middle;
@@ -82,8 +94,8 @@ class MainScreen(Screen[None]):
8294 min-width: 20;
8395 }
8496
85- #chip-input {
86- width: 100% ;
97+ #download-btn {
98+ min- width: 30 ;
8799 }
88100
89101 #file-input {
@@ -110,9 +122,17 @@ def compose(self) -> ComposeResult:
110122 allow_blank = True ,
111123 )
112124
113- yield Label ("Firmware File:" )
125+ yield Label ("Firmware:" )
126+ yield Button (
127+ "Select a chip first" ,
128+ variant = "default" ,
129+ id = "download-btn" ,
130+ disabled = True ,
131+ )
132+ yield Static ("" , id = "fw-status" )
133+ yield Static ("" , id = "fw-hint" )
114134 yield Input (
115- placeholder = "/ path/to/u-boot.bin " ,
135+ placeholder = "Or enter path to local firmware file " ,
116136 id = "file-input" ,
117137 )
118138
@@ -125,34 +145,136 @@ def compose(self) -> ComposeResult:
125145 value = port_options [0 ][1 ] if port_options else "" ,
126146 )
127147
128- yield Checkbox ("Send Ctrl-C after upload (enter U-Boot console)" , id = "break-check" )
148+ yield Checkbox (
149+ "Send Ctrl-C after upload (enter U-Boot console)" ,
150+ id = "break-check" ,
151+ )
129152
130153 with Horizontal (id = "button-row" ):
131154 yield Button ("Start Recovery" , variant = "primary" , id = "start-btn" )
132155
133156 yield Footer ()
134157
158+ def _get_chip (self ) -> str :
159+ sel = self .query_one ("#chip-select" , Select )
160+ return str (sel .value ) if sel .value != Select .BLANK else ""
161+
162+ def on_select_changed (self , event : Select .Changed ) -> None :
163+ if event .select .id == "chip-select" :
164+ self ._on_chip_changed ()
165+
166+ def on_input_changed (self , event : Input .Changed ) -> None :
167+ if event .input .id == "file-input" :
168+ self ._update_start_button ()
169+
170+ def _on_chip_changed (self ) -> None :
171+ chip = self ._get_chip ()
172+ dl_btn = self .query_one ("#download-btn" , Button )
173+ hint = self .query_one ("#fw-hint" , Static )
174+ status = self .query_one ("#fw-status" , Static )
175+
176+ if not chip :
177+ dl_btn .label = "Select a chip first"
178+ dl_btn .disabled = True
179+ dl_btn .variant = "default"
180+ hint .update ("" )
181+ status .update ("" )
182+ elif has_firmware (chip ):
183+ cached = get_cached_path (chip )
184+ if cached :
185+ dl_btn .label = f"Re-download U-Boot for { chip } "
186+ dl_btn .disabled = False
187+ dl_btn .variant = "default"
188+ status .update (f"✓ Cached: { cached .name } ({ cached .stat ().st_size // 1024 } KB)" )
189+ else :
190+ dl_btn .label = f"Download U-Boot for { chip } "
191+ dl_btn .disabled = False
192+ dl_btn .variant = "success"
193+ status .update ("" )
194+ hint .update ("Or enter a local file path below for custom builds." )
195+ else :
196+ dl_btn .label = "No OpenIPC build available"
197+ dl_btn .disabled = True
198+ dl_btn .variant = "default"
199+ hint .update ("Enter a local firmware file path below." )
200+ status .update ("" )
201+
202+ self ._update_start_button ()
203+
135204 def on_button_pressed (self , event : Button .Pressed ) -> None :
136205 if event .button .id == "start-btn" :
137206 self ._start_recovery ()
207+ elif event .button .id == "download-btn" :
208+ self ._download_firmware ()
209+
210+ def _download_firmware (self ) -> None :
211+ chip = self ._get_chip ()
212+ if not chip or not has_firmware (chip ):
213+ return
214+
215+ dl_btn = self .query_one ("#download-btn" , Button )
216+ status = self .query_one ("#fw-status" , Static )
217+
218+ dl_btn .label = "Downloading..."
219+ dl_btn .disabled = True
220+ status .update ("" )
221+
222+ try :
223+ path = download_firmware (chip )
224+ status .update (f"✓ { path .name } ({ path .stat ().st_size // 1024 } KB)" )
225+ dl_btn .label = f"Re-download U-Boot for { chip } "
226+ dl_btn .disabled = False
227+ dl_btn .variant = "default"
228+ self .notify (
229+ f"Downloaded { path .name } " ,
230+ severity = "information" ,
231+ title = "Firmware Ready" ,
232+ )
233+ except (ValueError , ConnectionError ) as e :
234+ status .update ("" )
235+ dl_btn .label = "Download failed — retry?"
236+ dl_btn .disabled = False
237+ dl_btn .variant = "error"
238+ self .notify (str (e ), severity = "error" , title = "Download Failed" )
239+
240+ self ._update_start_button ()
241+
242+ def _get_firmware_path (self ) -> str :
243+ """Get firmware path: local file input takes priority, then cached download."""
244+ local = str (self .query_one ("#file-input" , Input ).value ).strip ()
245+ if local :
246+ return local
247+
248+ chip = self ._get_chip ()
249+ if chip :
250+ cached = get_cached_path (chip )
251+ if cached :
252+ return str (cached )
253+
254+ return ""
255+
256+ def _update_start_button (self ) -> None :
257+ chip = self ._get_chip ()
258+ firmware = self ._get_firmware_path ()
259+ port_sel = self .query_one ("#port-select" , Select )
260+ port = str (port_sel .value ) if port_sel .value != Select .BLANK else ""
261+
262+ self .query_one ("#start-btn" , Button ).disabled = not (chip and firmware and port )
138263
139264 def _start_recovery (self ) -> None :
140- chip_select = self .query_one ("#chip-select" , Select )
141- file_input = self .query_one ("#file-input" , Input )
142- port_select = self .query_one ("#port-select" , Select )
265+ chip = self ._get_chip ()
266+ firmware_path = self ._get_firmware_path ()
267+ port_sel = self .query_one ("#port-select" , Select )
268+ port = str (port_sel .value ) if port_sel .value != Select .BLANK else ""
143269 break_check = self .query_one ("#break-check" , Checkbox )
144-
145- chip = str (chip_select .value ) if chip_select .value != Select .BLANK else ""
146- firmware_path = file_input .value .strip ()
147- port = str (port_select .value ) if port_select .value != Select .BLANK else ""
148270 send_break = break_check .value
149271
150272 # Validation
151273 errors : list [str ] = []
152274 if not chip :
153275 errors .append ("Select a chip model" )
154276 if not firmware_path :
155- errors .append ("Enter a firmware file path" )
277+ errors .append ("Download firmware or enter a file path" )
156278 elif not Path (firmware_path ).is_file ():
157279 errors .append (f"File not found: { firmware_path } " )
158280 if not port :
@@ -162,7 +284,6 @@ def _start_recovery(self) -> None:
162284 self .notify ("\n " .join (errors ), severity = "error" , title = "Validation Error" )
163285 return
164286
165- # Start recovery via the app
166287 from defib .tui .app import DefibApp
167288 app = self .app
168289 if isinstance (app , DefibApp ):
0 commit comments