fbuild 1.2.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. fbuild/__init__.py +390 -0
  2. fbuild/assets/example.txt +1 -0
  3. fbuild/build/__init__.py +117 -0
  4. fbuild/build/archive_creator.py +186 -0
  5. fbuild/build/binary_generator.py +444 -0
  6. fbuild/build/build_component_factory.py +131 -0
  7. fbuild/build/build_info_generator.py +624 -0
  8. fbuild/build/build_state.py +325 -0
  9. fbuild/build/build_utils.py +93 -0
  10. fbuild/build/compilation_executor.py +422 -0
  11. fbuild/build/compiler.py +165 -0
  12. fbuild/build/compiler_avr.py +574 -0
  13. fbuild/build/configurable_compiler.py +664 -0
  14. fbuild/build/configurable_linker.py +637 -0
  15. fbuild/build/flag_builder.py +214 -0
  16. fbuild/build/library_dependency_processor.py +185 -0
  17. fbuild/build/linker.py +708 -0
  18. fbuild/build/orchestrator.py +67 -0
  19. fbuild/build/orchestrator_avr.py +651 -0
  20. fbuild/build/orchestrator_esp32.py +878 -0
  21. fbuild/build/orchestrator_rp2040.py +719 -0
  22. fbuild/build/orchestrator_stm32.py +696 -0
  23. fbuild/build/orchestrator_teensy.py +580 -0
  24. fbuild/build/source_compilation_orchestrator.py +218 -0
  25. fbuild/build/source_scanner.py +516 -0
  26. fbuild/cli.py +717 -0
  27. fbuild/cli_utils.py +314 -0
  28. fbuild/config/__init__.py +16 -0
  29. fbuild/config/board_config.py +542 -0
  30. fbuild/config/board_loader.py +92 -0
  31. fbuild/config/ini_parser.py +369 -0
  32. fbuild/config/mcu_specs.py +88 -0
  33. fbuild/daemon/__init__.py +42 -0
  34. fbuild/daemon/async_client.py +531 -0
  35. fbuild/daemon/client.py +1505 -0
  36. fbuild/daemon/compilation_queue.py +293 -0
  37. fbuild/daemon/configuration_lock.py +865 -0
  38. fbuild/daemon/daemon.py +585 -0
  39. fbuild/daemon/daemon_context.py +293 -0
  40. fbuild/daemon/error_collector.py +263 -0
  41. fbuild/daemon/file_cache.py +332 -0
  42. fbuild/daemon/firmware_ledger.py +546 -0
  43. fbuild/daemon/lock_manager.py +508 -0
  44. fbuild/daemon/logging_utils.py +149 -0
  45. fbuild/daemon/messages.py +957 -0
  46. fbuild/daemon/operation_registry.py +288 -0
  47. fbuild/daemon/port_state_manager.py +249 -0
  48. fbuild/daemon/process_tracker.py +366 -0
  49. fbuild/daemon/processors/__init__.py +18 -0
  50. fbuild/daemon/processors/build_processor.py +248 -0
  51. fbuild/daemon/processors/deploy_processor.py +664 -0
  52. fbuild/daemon/processors/install_deps_processor.py +431 -0
  53. fbuild/daemon/processors/locking_processor.py +777 -0
  54. fbuild/daemon/processors/monitor_processor.py +285 -0
  55. fbuild/daemon/request_processor.py +457 -0
  56. fbuild/daemon/shared_serial.py +819 -0
  57. fbuild/daemon/status_manager.py +238 -0
  58. fbuild/daemon/subprocess_manager.py +316 -0
  59. fbuild/deploy/__init__.py +21 -0
  60. fbuild/deploy/deployer.py +67 -0
  61. fbuild/deploy/deployer_esp32.py +310 -0
  62. fbuild/deploy/docker_utils.py +315 -0
  63. fbuild/deploy/monitor.py +519 -0
  64. fbuild/deploy/qemu_runner.py +603 -0
  65. fbuild/interrupt_utils.py +34 -0
  66. fbuild/ledger/__init__.py +52 -0
  67. fbuild/ledger/board_ledger.py +560 -0
  68. fbuild/output.py +352 -0
  69. fbuild/packages/__init__.py +66 -0
  70. fbuild/packages/archive_utils.py +1098 -0
  71. fbuild/packages/arduino_core.py +412 -0
  72. fbuild/packages/cache.py +256 -0
  73. fbuild/packages/concurrent_manager.py +510 -0
  74. fbuild/packages/downloader.py +518 -0
  75. fbuild/packages/fingerprint.py +423 -0
  76. fbuild/packages/framework_esp32.py +538 -0
  77. fbuild/packages/framework_rp2040.py +349 -0
  78. fbuild/packages/framework_stm32.py +459 -0
  79. fbuild/packages/framework_teensy.py +346 -0
  80. fbuild/packages/github_utils.py +96 -0
  81. fbuild/packages/header_trampoline_cache.py +394 -0
  82. fbuild/packages/library_compiler.py +203 -0
  83. fbuild/packages/library_manager.py +549 -0
  84. fbuild/packages/library_manager_esp32.py +725 -0
  85. fbuild/packages/package.py +163 -0
  86. fbuild/packages/platform_esp32.py +383 -0
  87. fbuild/packages/platform_rp2040.py +400 -0
  88. fbuild/packages/platform_stm32.py +581 -0
  89. fbuild/packages/platform_teensy.py +312 -0
  90. fbuild/packages/platform_utils.py +131 -0
  91. fbuild/packages/platformio_registry.py +369 -0
  92. fbuild/packages/sdk_utils.py +231 -0
  93. fbuild/packages/toolchain.py +436 -0
  94. fbuild/packages/toolchain_binaries.py +196 -0
  95. fbuild/packages/toolchain_esp32.py +489 -0
  96. fbuild/packages/toolchain_metadata.py +185 -0
  97. fbuild/packages/toolchain_rp2040.py +436 -0
  98. fbuild/packages/toolchain_stm32.py +417 -0
  99. fbuild/packages/toolchain_teensy.py +404 -0
  100. fbuild/platform_configs/esp32.json +150 -0
  101. fbuild/platform_configs/esp32c2.json +144 -0
  102. fbuild/platform_configs/esp32c3.json +143 -0
  103. fbuild/platform_configs/esp32c5.json +151 -0
  104. fbuild/platform_configs/esp32c6.json +151 -0
  105. fbuild/platform_configs/esp32p4.json +149 -0
  106. fbuild/platform_configs/esp32s3.json +151 -0
  107. fbuild/platform_configs/imxrt1062.json +56 -0
  108. fbuild/platform_configs/rp2040.json +70 -0
  109. fbuild/platform_configs/rp2350.json +76 -0
  110. fbuild/platform_configs/stm32f1.json +59 -0
  111. fbuild/platform_configs/stm32f4.json +63 -0
  112. fbuild/py.typed +0 -0
  113. fbuild-1.2.8.dist-info/METADATA +468 -0
  114. fbuild-1.2.8.dist-info/RECORD +121 -0
  115. fbuild-1.2.8.dist-info/WHEEL +5 -0
  116. fbuild-1.2.8.dist-info/entry_points.txt +5 -0
  117. fbuild-1.2.8.dist-info/licenses/LICENSE +21 -0
  118. fbuild-1.2.8.dist-info/top_level.txt +2 -0
  119. fbuild_lint/__init__.py +0 -0
  120. fbuild_lint/ruff_plugins/__init__.py +0 -0
  121. fbuild_lint/ruff_plugins/keyboard_interrupt_checker.py +158 -0
@@ -0,0 +1,519 @@
1
+ """
2
+ Serial monitor module for embedded devices.
3
+
4
+ This module provides serial monitoring capabilities with optional halt conditions.
5
+ """
6
+
7
+ import _thread
8
+ import json
9
+ import re
10
+ import sys
11
+ import time
12
+ from pathlib import Path
13
+ from typing import Optional
14
+
15
+ from fbuild.cli_utils import safe_print
16
+ from fbuild.config import PlatformIOConfig
17
+
18
+
19
+ class MonitorError(Exception):
20
+ """Raised when monitor operations fail."""
21
+
22
+ pass
23
+
24
+
25
+ class SerialMonitor:
26
+ """Serial monitor for embedded devices."""
27
+
28
+ def __init__(self, verbose: bool = False):
29
+ """Initialize serial monitor.
30
+
31
+ Args:
32
+ verbose: Whether to show verbose output
33
+ """
34
+ self.verbose = verbose
35
+
36
+ def _write_summary(
37
+ self,
38
+ summary_file: Optional[Path],
39
+ expect: Optional[str],
40
+ expect_found: bool,
41
+ halt_on_error: Optional[str],
42
+ halt_on_error_found: bool,
43
+ halt_on_success: Optional[str],
44
+ halt_on_success_found: bool,
45
+ lines_processed: int,
46
+ elapsed_time: float,
47
+ exit_reason: str,
48
+ ) -> None:
49
+ """Write monitoring summary to JSON file.
50
+
51
+ Args:
52
+ summary_file: Path to write summary JSON
53
+ expect: Expected pattern (or None)
54
+ expect_found: Whether expect pattern was found
55
+ halt_on_error: Error pattern (or None)
56
+ halt_on_error_found: Whether error pattern was found
57
+ halt_on_success: Success pattern (or None)
58
+ halt_on_success_found: Whether success pattern was found
59
+ lines_processed: Total lines read from serial
60
+ elapsed_time: Time elapsed in seconds
61
+ exit_reason: Reason for exit (timeout/expect_found/halt_error/halt_success/interrupted/error)
62
+ """
63
+ if not summary_file:
64
+ return
65
+
66
+ summary = {
67
+ "expect_pattern": expect,
68
+ "expect_found": expect_found,
69
+ "halt_on_error_pattern": halt_on_error,
70
+ "halt_on_error_found": halt_on_error_found,
71
+ "halt_on_success_pattern": halt_on_success,
72
+ "halt_on_success_found": halt_on_success_found,
73
+ "lines_processed": lines_processed,
74
+ "elapsed_time": round(elapsed_time, 2),
75
+ "exit_reason": exit_reason,
76
+ }
77
+
78
+ try:
79
+ summary_file.parent.mkdir(parents=True, exist_ok=True)
80
+ with open(summary_file, "w", encoding="utf-8") as f:
81
+ json.dump(summary, f, indent=2)
82
+ except KeyboardInterrupt: # noqa: KBI002
83
+ raise
84
+ except Exception as e:
85
+ # Silently fail - don't disrupt the monitor operation
86
+ if self.verbose:
87
+ print(f"Warning: Could not write summary file: {e}")
88
+
89
+ def _format_timestamp(self, start_time: float) -> str:
90
+ """Format elapsed time as timestamp prefix.
91
+
92
+ Args:
93
+ start_time: Start time from time.time()
94
+
95
+ Returns:
96
+ Formatted timestamp string in SS.HH format (seconds.hundredths)
97
+ """
98
+ elapsed = time.time() - start_time
99
+ seconds = int(elapsed)
100
+ hundredths = int((elapsed - seconds) * 100)
101
+ return f"{seconds:02d}.{hundredths:02d}"
102
+
103
+ def monitor(
104
+ self,
105
+ project_dir: Path,
106
+ env_name: str,
107
+ port: Optional[str] = None,
108
+ baud: int = 115200,
109
+ timeout: Optional[int] = None,
110
+ halt_on_error: Optional[str] = None,
111
+ halt_on_success: Optional[str] = None,
112
+ expect: Optional[str] = None,
113
+ output_file: Optional[Path] = None,
114
+ summary_file: Optional[Path] = None,
115
+ timestamp: bool = False,
116
+ ) -> int:
117
+ """Monitor serial output from device.
118
+
119
+ Args:
120
+ project_dir: Path to project directory
121
+ env_name: Environment name
122
+ port: Serial port to use (auto-detect if None)
123
+ baud: Baud rate (default: 115200)
124
+ timeout: Timeout in seconds (None for infinite)
125
+ halt_on_error: String pattern that triggers error exit
126
+ halt_on_success: String pattern that triggers success exit
127
+ expect: Expected pattern - checked at timeout/success for exit code
128
+ output_file: Optional file to write serial output to (for client streaming)
129
+ summary_file: Optional file to write summary JSON to (for client display)
130
+ timestamp: Whether to prefix each line with elapsed time (SS.HH format)
131
+
132
+ Returns:
133
+ Exit code (0 for success, 1 for error)
134
+ """
135
+ try:
136
+ import serial
137
+ except ImportError:
138
+ print("Error: pyserial not installed. Install with: pip install pyserial")
139
+ return 1
140
+
141
+ # Load platformio.ini to get board config
142
+ ini_path = project_dir / "platformio.ini"
143
+ if not ini_path.exists():
144
+ print(f"Error: platformio.ini not found in {project_dir}")
145
+ return 1
146
+
147
+ config = PlatformIOConfig(ini_path)
148
+
149
+ try:
150
+ env_config = config.get_env_config(env_name)
151
+ except KeyboardInterrupt as ke:
152
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
153
+
154
+ handle_keyboard_interrupt_properly(ke)
155
+ except Exception as e:
156
+ print(f"Error: {e}")
157
+ return 1
158
+
159
+ # Get monitor baud rate from config if specified
160
+ monitor_speed = env_config.get("monitor_speed")
161
+ if monitor_speed:
162
+ try:
163
+ baud = int(monitor_speed)
164
+ except ValueError:
165
+ pass
166
+
167
+ # Auto-detect port if not specified
168
+ if not port:
169
+ port = self._detect_serial_port()
170
+ if not port:
171
+ print("Error: No serial port specified and auto-detection failed. " + "Use --port to specify a port.")
172
+ return 1
173
+
174
+ print(f"Opening serial port {port} at {baud} baud...")
175
+
176
+ ser = None
177
+ output_fp = None
178
+ try:
179
+ # Open serial port
180
+ ser = serial.Serial(
181
+ port,
182
+ baud,
183
+ timeout=0.1, # Short timeout for readline
184
+ )
185
+
186
+ # Reset the device to ensure we catch all output from the start
187
+ # This is necessary because the device may have already booted
188
+ # between esptool finishing and the monitor starting
189
+ ser.setDTR(False) # type: ignore[attr-defined]
190
+ ser.setRTS(True) # type: ignore[attr-defined]
191
+ time.sleep(0.1)
192
+ ser.setRTS(False) # type: ignore[attr-defined]
193
+ time.sleep(0.1)
194
+ ser.setDTR(True) # type: ignore[attr-defined]
195
+
196
+ print(f"Connected to {port}")
197
+ print("--- Serial Monitor (Ctrl+C to exit) ---")
198
+ print()
199
+
200
+ # Give device a moment to start booting after reset
201
+ time.sleep(0.2)
202
+
203
+ # Open output file for streaming (if specified)
204
+ if output_file:
205
+ try:
206
+ output_file.parent.mkdir(parents=True, exist_ok=True)
207
+ output_fp = open(output_file, "w", encoding="utf-8", errors="replace")
208
+ except KeyboardInterrupt: # noqa: KBI002
209
+ raise
210
+ except Exception as e:
211
+ print(f"Warning: Could not open output file {output_file}: {e}")
212
+
213
+ start_time = time.time()
214
+
215
+ # Track statistics
216
+ expect_found = False
217
+ halt_on_error_found = False
218
+ halt_on_success_found = False
219
+ lines_processed = 0
220
+
221
+ while True:
222
+ # Check timeout
223
+ if timeout and (time.time() - start_time) > timeout:
224
+ elapsed_time = time.time() - start_time
225
+ print()
226
+ print(f"--- Monitor timeout after {timeout} seconds ---")
227
+
228
+ # Print statistics
229
+ if expect or halt_on_error or halt_on_success:
230
+ safe_print("\n--- Test Results ---")
231
+ if expect:
232
+ if expect_found:
233
+ safe_print(f"✓ Expected pattern found: '{expect}'")
234
+ else:
235
+ safe_print(f"✗ Expected pattern NOT found: '{expect}'")
236
+ if halt_on_error:
237
+ if halt_on_error_found:
238
+ safe_print(f"✗ Error pattern found: '{halt_on_error}'")
239
+ else:
240
+ safe_print(f"✓ Error pattern not found: '{halt_on_error}'")
241
+ if halt_on_success:
242
+ if halt_on_success_found:
243
+ safe_print(f"✓ Success pattern found: '{halt_on_success}'")
244
+ else:
245
+ safe_print(f"✗ Success pattern NOT found: '{halt_on_success}'")
246
+
247
+ ser.close()
248
+ if output_fp:
249
+ output_fp.close()
250
+
251
+ # Write summary
252
+ self._write_summary(
253
+ summary_file,
254
+ expect,
255
+ expect_found,
256
+ halt_on_error,
257
+ halt_on_error_found,
258
+ halt_on_success,
259
+ halt_on_success_found,
260
+ lines_processed,
261
+ elapsed_time,
262
+ "timeout",
263
+ )
264
+
265
+ # Check expect keyword for exit code
266
+ if expect:
267
+ return 0 if expect_found else 1
268
+ else:
269
+ # Legacy behavior when no expect is specified
270
+ if halt_on_error or halt_on_success:
271
+ return 1 # Error: pattern was expected but not found
272
+ else:
273
+ return 0 # Success: just a timed monitoring session
274
+
275
+ # Read line from serial
276
+ try:
277
+ if ser.in_waiting:
278
+ line = ser.readline()
279
+ try:
280
+ text = line.decode("utf-8", errors="replace").rstrip()
281
+ except KeyboardInterrupt as ke:
282
+ from fbuild.interrupt_utils import (
283
+ handle_keyboard_interrupt_properly,
284
+ )
285
+
286
+ handle_keyboard_interrupt_properly(ke)
287
+ except Exception:
288
+ text = str(line)
289
+
290
+ # Print the line with optional timestamp prefix
291
+ if timestamp:
292
+ ts_prefix = self._format_timestamp(start_time)
293
+ safe_print(f"{ts_prefix} {text}")
294
+ else:
295
+ safe_print(text)
296
+ sys.stdout.flush()
297
+
298
+ # Write to output file if specified (with timestamp if enabled)
299
+ if output_fp:
300
+ try:
301
+ if timestamp:
302
+ ts_prefix = self._format_timestamp(start_time)
303
+ output_fp.write(f"{ts_prefix} {text}\n")
304
+ else:
305
+ output_fp.write(text + "\n")
306
+ output_fp.flush()
307
+ except KeyboardInterrupt: # noqa: KBI002
308
+ raise
309
+ except Exception:
310
+ pass # Ignore write errors
311
+
312
+ # Increment line counter
313
+ lines_processed += 1
314
+
315
+ # Check for expect pattern (track but don't halt)
316
+ if expect and re.search(expect, text, re.IGNORECASE):
317
+ expect_found = True
318
+
319
+ # Check halt conditions
320
+ if halt_on_error and re.search(halt_on_error, text, re.IGNORECASE):
321
+ halt_on_error_found = True
322
+ elapsed_time = time.time() - start_time
323
+ print()
324
+ print(f"--- Found error pattern: '{halt_on_error}' ---")
325
+
326
+ # Print statistics
327
+ if expect or halt_on_success:
328
+ safe_print("\n--- Test Results ---")
329
+ if expect:
330
+ if expect_found:
331
+ safe_print(f"✓ Expected pattern found: '{expect}'")
332
+ else:
333
+ safe_print(f"✗ Expected pattern NOT found: '{expect}'")
334
+ if halt_on_success:
335
+ if halt_on_success_found:
336
+ safe_print(f"✓ Success pattern found: '{halt_on_success}'")
337
+ else:
338
+ safe_print(f"✗ Success pattern NOT found: '{halt_on_success}'")
339
+ safe_print(f"✗ Error pattern found: '{halt_on_error}'")
340
+
341
+ ser.close()
342
+ if output_fp:
343
+ output_fp.close()
344
+
345
+ # Write summary
346
+ self._write_summary(
347
+ summary_file,
348
+ expect,
349
+ expect_found,
350
+ halt_on_error,
351
+ halt_on_error_found,
352
+ halt_on_success,
353
+ halt_on_success_found,
354
+ lines_processed,
355
+ elapsed_time,
356
+ "halt_error",
357
+ )
358
+
359
+ return 1
360
+
361
+ if halt_on_success and re.search(halt_on_success, text, re.IGNORECASE):
362
+ halt_on_success_found = True
363
+ elapsed_time = time.time() - start_time
364
+ print()
365
+ print(f"--- Found success pattern: '{halt_on_success}' ---")
366
+
367
+ # Print statistics
368
+ if expect or halt_on_error:
369
+ safe_print("\n--- Test Results ---")
370
+ if expect:
371
+ if expect_found:
372
+ safe_print(f"✓ Expected pattern found: '{expect}'")
373
+ else:
374
+ safe_print(f"✗ Expected pattern NOT found: '{expect}'")
375
+ safe_print(f"✓ Success pattern found: '{halt_on_success}'")
376
+ if halt_on_error:
377
+ if halt_on_error_found:
378
+ safe_print(f"✗ Error pattern found: '{halt_on_error}'")
379
+ else:
380
+ safe_print(f"✓ Error pattern not found: '{halt_on_error}'")
381
+
382
+ ser.close()
383
+ if output_fp:
384
+ output_fp.close()
385
+
386
+ # Write summary
387
+ exit_reason = "expect_found" if (expect and expect_found) else "halt_success"
388
+ self._write_summary(
389
+ summary_file,
390
+ expect,
391
+ expect_found,
392
+ halt_on_error,
393
+ halt_on_error_found,
394
+ halt_on_success,
395
+ halt_on_success_found,
396
+ lines_processed,
397
+ elapsed_time,
398
+ exit_reason,
399
+ )
400
+
401
+ # Check expect keyword for exit code
402
+ if expect:
403
+ return 0 if expect_found else 1
404
+ else:
405
+ return 0
406
+ else:
407
+ time.sleep(0.01)
408
+
409
+ except serial.SerialException as e:
410
+ elapsed_time = time.time() - start_time
411
+ print(f"\nError reading from serial port: {e}")
412
+ ser.close()
413
+ if output_fp:
414
+ output_fp.close()
415
+
416
+ # Write summary
417
+ self._write_summary(
418
+ summary_file,
419
+ expect,
420
+ expect_found,
421
+ halt_on_error,
422
+ halt_on_error_found,
423
+ halt_on_success,
424
+ halt_on_success_found,
425
+ lines_processed,
426
+ elapsed_time,
427
+ "error",
428
+ )
429
+
430
+ return 1
431
+
432
+ except serial.SerialException as e:
433
+ print(f"Error opening serial port {port}: {e}")
434
+ if output_fp:
435
+ output_fp.close()
436
+
437
+ # Write summary (minimal - couldn't even start monitoring)
438
+ self._write_summary(
439
+ summary_file,
440
+ expect,
441
+ False,
442
+ halt_on_error,
443
+ False,
444
+ halt_on_success,
445
+ False,
446
+ 0,
447
+ 0.0,
448
+ "error",
449
+ )
450
+
451
+ return 1
452
+ except KeyboardInterrupt:
453
+ # Interrupt other threads
454
+ _thread.interrupt_main()
455
+
456
+ elapsed_time = time.time() - start_time if "start_time" in locals() else 0.0
457
+ lines = lines_processed if "lines_processed" in locals() else 0
458
+ exp_found = expect_found if "expect_found" in locals() else False
459
+ halt_err_found = halt_on_error_found if "halt_on_error_found" in locals() else False
460
+ halt_succ_found = halt_on_success_found if "halt_on_success_found" in locals() else False
461
+
462
+ print()
463
+ print("--- Monitor interrupted ---")
464
+ if ser is not None:
465
+ ser.close()
466
+ if output_fp:
467
+ output_fp.close()
468
+
469
+ # Write summary
470
+ self._write_summary(
471
+ summary_file,
472
+ expect,
473
+ exp_found,
474
+ halt_on_error,
475
+ halt_err_found,
476
+ halt_on_success,
477
+ halt_succ_found,
478
+ lines,
479
+ elapsed_time,
480
+ "interrupted",
481
+ )
482
+
483
+ return 0
484
+
485
+ def _detect_serial_port(self) -> Optional[str]:
486
+ """Auto-detect serial port for device.
487
+
488
+ Returns:
489
+ Serial port name or None if not found
490
+ """
491
+ try:
492
+ import serial.tools.list_ports
493
+
494
+ ports = list(serial.tools.list_ports.comports())
495
+
496
+ # Look for ESP32 or USB-SERIAL devices
497
+ for port in ports:
498
+ description = (port.description or "").lower()
499
+ manufacturer = (port.manufacturer or "").lower()
500
+
501
+ if any(x in description or x in manufacturer for x in ["cp210", "ch340", "usb-serial", "uart", "esp32"]):
502
+ return port.device
503
+
504
+ # If no specific match, return first port
505
+ if ports:
506
+ return ports[0].device
507
+
508
+ except ImportError:
509
+ if self.verbose:
510
+ print("pyserial not installed. Cannot auto-detect port.")
511
+ except KeyboardInterrupt as ke:
512
+ from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
513
+
514
+ handle_keyboard_interrupt_properly(ke)
515
+ except Exception as e:
516
+ if self.verbose:
517
+ print(f"Port detection failed: {e}")
518
+
519
+ return None