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.
- fbuild/__init__.py +390 -0
- fbuild/assets/example.txt +1 -0
- fbuild/build/__init__.py +117 -0
- fbuild/build/archive_creator.py +186 -0
- fbuild/build/binary_generator.py +444 -0
- fbuild/build/build_component_factory.py +131 -0
- fbuild/build/build_info_generator.py +624 -0
- fbuild/build/build_state.py +325 -0
- fbuild/build/build_utils.py +93 -0
- fbuild/build/compilation_executor.py +422 -0
- fbuild/build/compiler.py +165 -0
- fbuild/build/compiler_avr.py +574 -0
- fbuild/build/configurable_compiler.py +664 -0
- fbuild/build/configurable_linker.py +637 -0
- fbuild/build/flag_builder.py +214 -0
- fbuild/build/library_dependency_processor.py +185 -0
- fbuild/build/linker.py +708 -0
- fbuild/build/orchestrator.py +67 -0
- fbuild/build/orchestrator_avr.py +651 -0
- fbuild/build/orchestrator_esp32.py +878 -0
- fbuild/build/orchestrator_rp2040.py +719 -0
- fbuild/build/orchestrator_stm32.py +696 -0
- fbuild/build/orchestrator_teensy.py +580 -0
- fbuild/build/source_compilation_orchestrator.py +218 -0
- fbuild/build/source_scanner.py +516 -0
- fbuild/cli.py +717 -0
- fbuild/cli_utils.py +314 -0
- fbuild/config/__init__.py +16 -0
- fbuild/config/board_config.py +542 -0
- fbuild/config/board_loader.py +92 -0
- fbuild/config/ini_parser.py +369 -0
- fbuild/config/mcu_specs.py +88 -0
- fbuild/daemon/__init__.py +42 -0
- fbuild/daemon/async_client.py +531 -0
- fbuild/daemon/client.py +1505 -0
- fbuild/daemon/compilation_queue.py +293 -0
- fbuild/daemon/configuration_lock.py +865 -0
- fbuild/daemon/daemon.py +585 -0
- fbuild/daemon/daemon_context.py +293 -0
- fbuild/daemon/error_collector.py +263 -0
- fbuild/daemon/file_cache.py +332 -0
- fbuild/daemon/firmware_ledger.py +546 -0
- fbuild/daemon/lock_manager.py +508 -0
- fbuild/daemon/logging_utils.py +149 -0
- fbuild/daemon/messages.py +957 -0
- fbuild/daemon/operation_registry.py +288 -0
- fbuild/daemon/port_state_manager.py +249 -0
- fbuild/daemon/process_tracker.py +366 -0
- fbuild/daemon/processors/__init__.py +18 -0
- fbuild/daemon/processors/build_processor.py +248 -0
- fbuild/daemon/processors/deploy_processor.py +664 -0
- fbuild/daemon/processors/install_deps_processor.py +431 -0
- fbuild/daemon/processors/locking_processor.py +777 -0
- fbuild/daemon/processors/monitor_processor.py +285 -0
- fbuild/daemon/request_processor.py +457 -0
- fbuild/daemon/shared_serial.py +819 -0
- fbuild/daemon/status_manager.py +238 -0
- fbuild/daemon/subprocess_manager.py +316 -0
- fbuild/deploy/__init__.py +21 -0
- fbuild/deploy/deployer.py +67 -0
- fbuild/deploy/deployer_esp32.py +310 -0
- fbuild/deploy/docker_utils.py +315 -0
- fbuild/deploy/monitor.py +519 -0
- fbuild/deploy/qemu_runner.py +603 -0
- fbuild/interrupt_utils.py +34 -0
- fbuild/ledger/__init__.py +52 -0
- fbuild/ledger/board_ledger.py +560 -0
- fbuild/output.py +352 -0
- fbuild/packages/__init__.py +66 -0
- fbuild/packages/archive_utils.py +1098 -0
- fbuild/packages/arduino_core.py +412 -0
- fbuild/packages/cache.py +256 -0
- fbuild/packages/concurrent_manager.py +510 -0
- fbuild/packages/downloader.py +518 -0
- fbuild/packages/fingerprint.py +423 -0
- fbuild/packages/framework_esp32.py +538 -0
- fbuild/packages/framework_rp2040.py +349 -0
- fbuild/packages/framework_stm32.py +459 -0
- fbuild/packages/framework_teensy.py +346 -0
- fbuild/packages/github_utils.py +96 -0
- fbuild/packages/header_trampoline_cache.py +394 -0
- fbuild/packages/library_compiler.py +203 -0
- fbuild/packages/library_manager.py +549 -0
- fbuild/packages/library_manager_esp32.py +725 -0
- fbuild/packages/package.py +163 -0
- fbuild/packages/platform_esp32.py +383 -0
- fbuild/packages/platform_rp2040.py +400 -0
- fbuild/packages/platform_stm32.py +581 -0
- fbuild/packages/platform_teensy.py +312 -0
- fbuild/packages/platform_utils.py +131 -0
- fbuild/packages/platformio_registry.py +369 -0
- fbuild/packages/sdk_utils.py +231 -0
- fbuild/packages/toolchain.py +436 -0
- fbuild/packages/toolchain_binaries.py +196 -0
- fbuild/packages/toolchain_esp32.py +489 -0
- fbuild/packages/toolchain_metadata.py +185 -0
- fbuild/packages/toolchain_rp2040.py +436 -0
- fbuild/packages/toolchain_stm32.py +417 -0
- fbuild/packages/toolchain_teensy.py +404 -0
- fbuild/platform_configs/esp32.json +150 -0
- fbuild/platform_configs/esp32c2.json +144 -0
- fbuild/platform_configs/esp32c3.json +143 -0
- fbuild/platform_configs/esp32c5.json +151 -0
- fbuild/platform_configs/esp32c6.json +151 -0
- fbuild/platform_configs/esp32p4.json +149 -0
- fbuild/platform_configs/esp32s3.json +151 -0
- fbuild/platform_configs/imxrt1062.json +56 -0
- fbuild/platform_configs/rp2040.json +70 -0
- fbuild/platform_configs/rp2350.json +76 -0
- fbuild/platform_configs/stm32f1.json +59 -0
- fbuild/platform_configs/stm32f4.json +63 -0
- fbuild/py.typed +0 -0
- fbuild-1.2.8.dist-info/METADATA +468 -0
- fbuild-1.2.8.dist-info/RECORD +121 -0
- fbuild-1.2.8.dist-info/WHEEL +5 -0
- fbuild-1.2.8.dist-info/entry_points.txt +5 -0
- fbuild-1.2.8.dist-info/licenses/LICENSE +21 -0
- fbuild-1.2.8.dist-info/top_level.txt +2 -0
- fbuild_lint/__init__.py +0 -0
- fbuild_lint/ruff_plugins/__init__.py +0 -0
- fbuild_lint/ruff_plugins/keyboard_interrupt_checker.py +158 -0
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Board Ledger - Track attached chip/port mappings.
|
|
3
|
+
|
|
4
|
+
This module provides a simple ledger to cache chip type detections for serial ports.
|
|
5
|
+
The cache is stored in ~/.fbuild/board_ledger.json and uses file locking for
|
|
6
|
+
thread-safe access.
|
|
7
|
+
|
|
8
|
+
Features:
|
|
9
|
+
- Port to chip type mapping with timestamps
|
|
10
|
+
- Automatic stale entry expiration (24 hours)
|
|
11
|
+
- Thread-safe file access with file locking
|
|
12
|
+
- Chip type validation against known ESP32 variants
|
|
13
|
+
- Integration with esptool for chip detection
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import subprocess
|
|
19
|
+
import sys
|
|
20
|
+
import threading
|
|
21
|
+
import time
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
# Stale entry threshold: 24 hours
|
|
27
|
+
STALE_THRESHOLD_SECONDS = 24 * 60 * 60
|
|
28
|
+
|
|
29
|
+
# Known chip types and their corresponding environment names
|
|
30
|
+
CHIP_TO_ENVIRONMENT: dict[str, str] = {
|
|
31
|
+
"ESP32": "esp32dev",
|
|
32
|
+
"ESP32-S2": "esp32s2",
|
|
33
|
+
"ESP32-S3": "esp32s3",
|
|
34
|
+
"ESP32-C2": "esp32c2",
|
|
35
|
+
"ESP32-C3": "esp32c3",
|
|
36
|
+
"ESP32-C6": "esp32c6",
|
|
37
|
+
"ESP32-H2": "esp32h2",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Valid chip types (for validation)
|
|
41
|
+
VALID_CHIP_TYPES = set(CHIP_TO_ENVIRONMENT.keys())
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class BoardLedgerError(Exception):
|
|
45
|
+
"""Raised when board ledger operations fail."""
|
|
46
|
+
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ChipDetectionError(BoardLedgerError):
|
|
51
|
+
"""Raised when chip detection fails."""
|
|
52
|
+
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class LedgerEntry:
|
|
58
|
+
"""A single entry in the board ledger.
|
|
59
|
+
|
|
60
|
+
Attributes:
|
|
61
|
+
chip_type: The detected chip type (e.g., "ESP32-S3")
|
|
62
|
+
timestamp: Unix timestamp when the entry was created/updated
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
chip_type: str
|
|
66
|
+
timestamp: float
|
|
67
|
+
|
|
68
|
+
def is_stale(self, threshold: float = STALE_THRESHOLD_SECONDS) -> bool:
|
|
69
|
+
"""Check if this entry is stale (older than threshold).
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
threshold: Maximum age in seconds before entry is considered stale
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
True if entry is older than threshold
|
|
76
|
+
"""
|
|
77
|
+
return (time.time() - self.timestamp) > threshold
|
|
78
|
+
|
|
79
|
+
def to_dict(self) -> dict[str, Any]:
|
|
80
|
+
"""Convert to dictionary for JSON serialization."""
|
|
81
|
+
return {
|
|
82
|
+
"chip_type": self.chip_type,
|
|
83
|
+
"timestamp": self.timestamp,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def from_dict(cls, data: dict[str, Any]) -> "LedgerEntry":
|
|
88
|
+
"""Create entry from dictionary."""
|
|
89
|
+
return cls(
|
|
90
|
+
chip_type=data["chip_type"],
|
|
91
|
+
timestamp=data["timestamp"],
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class BoardLedger:
|
|
96
|
+
"""Manages port to chip type mappings with persistent storage.
|
|
97
|
+
|
|
98
|
+
The ledger stores mappings in ~/.fbuild/board_ledger.json and provides
|
|
99
|
+
thread-safe access through file locking.
|
|
100
|
+
|
|
101
|
+
Example:
|
|
102
|
+
>>> ledger = BoardLedger()
|
|
103
|
+
>>> ledger.set_chip("COM3", "ESP32-S3")
|
|
104
|
+
>>> chip = ledger.get_chip("COM3")
|
|
105
|
+
>>> print(chip) # "ESP32-S3"
|
|
106
|
+
>>> ledger.clear("COM3")
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
def __init__(self, ledger_path: Path | None = None):
|
|
110
|
+
"""Initialize the board ledger.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
ledger_path: Optional custom path for ledger file.
|
|
114
|
+
Defaults to ~/.fbuild/board_ledger.json
|
|
115
|
+
"""
|
|
116
|
+
if ledger_path is None:
|
|
117
|
+
self._ledger_path = Path.home() / ".fbuild" / "board_ledger.json"
|
|
118
|
+
else:
|
|
119
|
+
self._ledger_path = ledger_path
|
|
120
|
+
|
|
121
|
+
# Thread lock for in-process synchronization
|
|
122
|
+
self._lock = threading.Lock()
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def ledger_path(self) -> Path:
|
|
126
|
+
"""Get the path to the ledger file."""
|
|
127
|
+
return self._ledger_path
|
|
128
|
+
|
|
129
|
+
def _ensure_directory(self) -> None:
|
|
130
|
+
"""Ensure the parent directory exists."""
|
|
131
|
+
self._ledger_path.parent.mkdir(parents=True, exist_ok=True)
|
|
132
|
+
|
|
133
|
+
def _read_ledger(self) -> dict[str, dict[str, Any]]:
|
|
134
|
+
"""Read the ledger file.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Dictionary mapping port names to entry dictionaries
|
|
138
|
+
"""
|
|
139
|
+
if not self._ledger_path.exists():
|
|
140
|
+
return {}
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
with open(self._ledger_path, encoding="utf-8") as f:
|
|
144
|
+
data = json.load(f)
|
|
145
|
+
if not isinstance(data, dict):
|
|
146
|
+
return {}
|
|
147
|
+
return data
|
|
148
|
+
except (json.JSONDecodeError, OSError):
|
|
149
|
+
return {}
|
|
150
|
+
|
|
151
|
+
def _write_ledger(self, data: dict[str, dict[str, Any]]) -> None:
|
|
152
|
+
"""Write the ledger file.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
data: Dictionary mapping port names to entry dictionaries
|
|
156
|
+
"""
|
|
157
|
+
self._ensure_directory()
|
|
158
|
+
try:
|
|
159
|
+
with open(self._ledger_path, "w", encoding="utf-8") as f:
|
|
160
|
+
json.dump(data, f, indent=2)
|
|
161
|
+
except OSError as e:
|
|
162
|
+
raise BoardLedgerError(f"Failed to write ledger: {e}") from e
|
|
163
|
+
|
|
164
|
+
def _acquire_file_lock(self) -> Any:
|
|
165
|
+
"""Acquire a file lock for cross-process synchronization.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Lock file handle (or None on platforms without locking support)
|
|
169
|
+
"""
|
|
170
|
+
self._ensure_directory()
|
|
171
|
+
lock_path = self._ledger_path.with_suffix(".lock")
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
# Open lock file
|
|
175
|
+
lock_file = open(lock_path, "w", encoding="utf-8")
|
|
176
|
+
|
|
177
|
+
# Platform-specific locking
|
|
178
|
+
if sys.platform == "win32":
|
|
179
|
+
import msvcrt
|
|
180
|
+
|
|
181
|
+
msvcrt.locking(lock_file.fileno(), msvcrt.LK_NBLCK, 1)
|
|
182
|
+
else: # pragma: no cover - Unix only
|
|
183
|
+
import fcntl # type: ignore[import-not-found]
|
|
184
|
+
|
|
185
|
+
fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX)
|
|
186
|
+
|
|
187
|
+
return lock_file
|
|
188
|
+
except (ImportError, OSError):
|
|
189
|
+
# Locking not available or failed - continue without lock
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
def _release_file_lock(self, lock_file: Any) -> None:
|
|
193
|
+
"""Release a file lock.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
lock_file: Lock file handle from _acquire_file_lock
|
|
197
|
+
"""
|
|
198
|
+
if lock_file is None:
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
if sys.platform == "win32":
|
|
203
|
+
import msvcrt
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
msvcrt.locking(lock_file.fileno(), msvcrt.LK_UNLCK, 1)
|
|
207
|
+
except OSError:
|
|
208
|
+
pass
|
|
209
|
+
else: # pragma: no cover - Unix only
|
|
210
|
+
import fcntl # type: ignore[import-not-found]
|
|
211
|
+
|
|
212
|
+
fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
|
|
213
|
+
|
|
214
|
+
lock_file.close()
|
|
215
|
+
except (ImportError, OSError):
|
|
216
|
+
pass
|
|
217
|
+
|
|
218
|
+
def get_chip(self, port: str) -> str | None:
|
|
219
|
+
"""Get the cached chip type for a port.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
port: Serial port name (e.g., "COM3", "/dev/ttyUSB0")
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Chip type string (e.g., "ESP32-S3") or None if not found/stale
|
|
226
|
+
"""
|
|
227
|
+
with self._lock:
|
|
228
|
+
lock_file = self._acquire_file_lock()
|
|
229
|
+
try:
|
|
230
|
+
data = self._read_ledger()
|
|
231
|
+
entry_data = data.get(port)
|
|
232
|
+
if entry_data is None:
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
entry = LedgerEntry.from_dict(entry_data)
|
|
236
|
+
if entry.is_stale():
|
|
237
|
+
return None
|
|
238
|
+
|
|
239
|
+
return entry.chip_type
|
|
240
|
+
finally:
|
|
241
|
+
self._release_file_lock(lock_file)
|
|
242
|
+
|
|
243
|
+
def set_chip(self, port: str, chip_type: str) -> None:
|
|
244
|
+
"""Set the chip type for a port.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
port: Serial port name (e.g., "COM3", "/dev/ttyUSB0")
|
|
248
|
+
chip_type: Chip type (e.g., "ESP32-S3", "ESP32-C6")
|
|
249
|
+
|
|
250
|
+
Raises:
|
|
251
|
+
BoardLedgerError: If chip_type is not valid
|
|
252
|
+
"""
|
|
253
|
+
# Normalize chip type (handle lowercase/variant formats)
|
|
254
|
+
normalized = self._normalize_chip_type(chip_type)
|
|
255
|
+
if normalized not in VALID_CHIP_TYPES:
|
|
256
|
+
raise BoardLedgerError(f"Invalid chip type: {chip_type}. Valid types: {', '.join(sorted(VALID_CHIP_TYPES))}")
|
|
257
|
+
|
|
258
|
+
entry = LedgerEntry(chip_type=normalized, timestamp=time.time())
|
|
259
|
+
|
|
260
|
+
with self._lock:
|
|
261
|
+
lock_file = self._acquire_file_lock()
|
|
262
|
+
try:
|
|
263
|
+
data = self._read_ledger()
|
|
264
|
+
data[port] = entry.to_dict()
|
|
265
|
+
self._write_ledger(data)
|
|
266
|
+
finally:
|
|
267
|
+
self._release_file_lock(lock_file)
|
|
268
|
+
|
|
269
|
+
def clear(self, port: str) -> bool:
|
|
270
|
+
"""Clear the cached chip type for a port.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
port: Serial port name to clear
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
True if entry was cleared, False if not found
|
|
277
|
+
"""
|
|
278
|
+
with self._lock:
|
|
279
|
+
lock_file = self._acquire_file_lock()
|
|
280
|
+
try:
|
|
281
|
+
data = self._read_ledger()
|
|
282
|
+
if port in data:
|
|
283
|
+
del data[port]
|
|
284
|
+
self._write_ledger(data)
|
|
285
|
+
return True
|
|
286
|
+
return False
|
|
287
|
+
finally:
|
|
288
|
+
self._release_file_lock(lock_file)
|
|
289
|
+
|
|
290
|
+
def clear_all(self) -> int:
|
|
291
|
+
"""Clear all entries from the ledger.
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
Number of entries cleared
|
|
295
|
+
"""
|
|
296
|
+
with self._lock:
|
|
297
|
+
lock_file = self._acquire_file_lock()
|
|
298
|
+
try:
|
|
299
|
+
data = self._read_ledger()
|
|
300
|
+
count = len(data)
|
|
301
|
+
self._write_ledger({})
|
|
302
|
+
return count
|
|
303
|
+
finally:
|
|
304
|
+
self._release_file_lock(lock_file)
|
|
305
|
+
|
|
306
|
+
def clear_stale(self, threshold: float = STALE_THRESHOLD_SECONDS) -> int:
|
|
307
|
+
"""Remove all stale entries from the ledger.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
threshold: Maximum age in seconds before entry is considered stale
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
Number of entries removed
|
|
314
|
+
"""
|
|
315
|
+
with self._lock:
|
|
316
|
+
lock_file = self._acquire_file_lock()
|
|
317
|
+
try:
|
|
318
|
+
data = self._read_ledger()
|
|
319
|
+
original_count = len(data)
|
|
320
|
+
|
|
321
|
+
# Filter out stale entries
|
|
322
|
+
fresh_data = {}
|
|
323
|
+
for port, entry_data in data.items():
|
|
324
|
+
entry = LedgerEntry.from_dict(entry_data)
|
|
325
|
+
if not entry.is_stale(threshold):
|
|
326
|
+
fresh_data[port] = entry_data
|
|
327
|
+
|
|
328
|
+
self._write_ledger(fresh_data)
|
|
329
|
+
return original_count - len(fresh_data)
|
|
330
|
+
finally:
|
|
331
|
+
self._release_file_lock(lock_file)
|
|
332
|
+
|
|
333
|
+
def get_all(self) -> dict[str, LedgerEntry]:
|
|
334
|
+
"""Get all non-stale entries in the ledger.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
Dictionary mapping port names to LedgerEntry objects
|
|
338
|
+
"""
|
|
339
|
+
with self._lock:
|
|
340
|
+
lock_file = self._acquire_file_lock()
|
|
341
|
+
try:
|
|
342
|
+
data = self._read_ledger()
|
|
343
|
+
result = {}
|
|
344
|
+
for port, entry_data in data.items():
|
|
345
|
+
entry = LedgerEntry.from_dict(entry_data)
|
|
346
|
+
if not entry.is_stale():
|
|
347
|
+
result[port] = entry
|
|
348
|
+
return result
|
|
349
|
+
finally:
|
|
350
|
+
self._release_file_lock(lock_file)
|
|
351
|
+
|
|
352
|
+
def get_environment(self, port: str) -> str | None:
|
|
353
|
+
"""Get the environment name for a cached port.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
port: Serial port name
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
Environment name (e.g., "esp32s3") or None if not found
|
|
360
|
+
"""
|
|
361
|
+
chip_type = self.get_chip(port)
|
|
362
|
+
if chip_type is None:
|
|
363
|
+
return None
|
|
364
|
+
return CHIP_TO_ENVIRONMENT.get(chip_type)
|
|
365
|
+
|
|
366
|
+
@staticmethod
|
|
367
|
+
def _normalize_chip_type(chip_type: str) -> str:
|
|
368
|
+
"""Normalize chip type to standard format.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
chip_type: Raw chip type string (e.g., "esp32s3", "ESP32-S3")
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
Normalized chip type (e.g., "ESP32-S3")
|
|
375
|
+
"""
|
|
376
|
+
# Map common variations to standard format
|
|
377
|
+
upper = chip_type.upper().replace("_", "-")
|
|
378
|
+
|
|
379
|
+
# Handle formats without hyphen (esp32s3 -> ESP32-S3)
|
|
380
|
+
mappings = {
|
|
381
|
+
"ESP32S2": "ESP32-S2",
|
|
382
|
+
"ESP32S3": "ESP32-S3",
|
|
383
|
+
"ESP32C2": "ESP32-C2",
|
|
384
|
+
"ESP32C3": "ESP32-C3",
|
|
385
|
+
"ESP32C6": "ESP32-C6",
|
|
386
|
+
"ESP32H2": "ESP32-H2",
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return mappings.get(upper, upper)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
@dataclass
|
|
393
|
+
class DetectionResult:
|
|
394
|
+
"""Result of chip detection.
|
|
395
|
+
|
|
396
|
+
Attributes:
|
|
397
|
+
chip_type: The detected chip type (e.g., "ESP32-S3")
|
|
398
|
+
environment: The environment name (e.g., "esp32s3")
|
|
399
|
+
was_cached: Whether the result came from cache
|
|
400
|
+
"""
|
|
401
|
+
|
|
402
|
+
chip_type: str
|
|
403
|
+
environment: str
|
|
404
|
+
was_cached: bool
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def detect_chip_with_esptool(port: str, verbose: bool = False) -> str:
|
|
408
|
+
"""Detect chip type using esptool.
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
port: Serial port to detect chip on
|
|
412
|
+
verbose: Whether to show verbose output
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
Chip type string (e.g., "ESP32-S3")
|
|
416
|
+
|
|
417
|
+
Raises:
|
|
418
|
+
ChipDetectionError: If detection fails
|
|
419
|
+
"""
|
|
420
|
+
try:
|
|
421
|
+
# Build esptool command
|
|
422
|
+
cmd = [
|
|
423
|
+
sys.executable,
|
|
424
|
+
"-m",
|
|
425
|
+
"esptool",
|
|
426
|
+
"--port",
|
|
427
|
+
port,
|
|
428
|
+
"chip_id",
|
|
429
|
+
]
|
|
430
|
+
|
|
431
|
+
if verbose:
|
|
432
|
+
print(f"Running: {' '.join(cmd)}")
|
|
433
|
+
|
|
434
|
+
# Set up environment (strip MSYS paths on Windows)
|
|
435
|
+
env = os.environ.copy()
|
|
436
|
+
if sys.platform == "win32" and "PATH" in env:
|
|
437
|
+
paths = env["PATH"].split(os.pathsep)
|
|
438
|
+
filtered_paths = [p for p in paths if "msys" not in p.lower()]
|
|
439
|
+
env["PATH"] = os.pathsep.join(filtered_paths)
|
|
440
|
+
|
|
441
|
+
result = subprocess.run(
|
|
442
|
+
cmd,
|
|
443
|
+
capture_output=True,
|
|
444
|
+
text=True,
|
|
445
|
+
timeout=30,
|
|
446
|
+
env=env,
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
if result.returncode != 0:
|
|
450
|
+
error_msg = result.stderr.strip() if result.stderr else "Unknown error"
|
|
451
|
+
raise ChipDetectionError(f"esptool chip_id failed: {error_msg}")
|
|
452
|
+
|
|
453
|
+
# Parse output to find chip type
|
|
454
|
+
# Example output: "Chip is ESP32-S3 (revision v0.2)"
|
|
455
|
+
output = result.stdout
|
|
456
|
+
for line in output.splitlines():
|
|
457
|
+
if "Chip is" in line:
|
|
458
|
+
# Extract chip name
|
|
459
|
+
# Format: "Chip is ESP32-S3 (revision ...)"
|
|
460
|
+
parts = line.split("Chip is")
|
|
461
|
+
if len(parts) >= 2:
|
|
462
|
+
chip_part = parts[1].strip()
|
|
463
|
+
# Remove revision info if present
|
|
464
|
+
if "(" in chip_part:
|
|
465
|
+
chip_part = chip_part.split("(")[0].strip()
|
|
466
|
+
return BoardLedger._normalize_chip_type(chip_part)
|
|
467
|
+
|
|
468
|
+
raise ChipDetectionError(f"Could not parse chip type from esptool output: {output}")
|
|
469
|
+
|
|
470
|
+
except subprocess.TimeoutExpired:
|
|
471
|
+
raise ChipDetectionError(f"Chip detection timed out on port {port}")
|
|
472
|
+
except KeyboardInterrupt as ke:
|
|
473
|
+
from fbuild.interrupt_utils import handle_keyboard_interrupt_properly
|
|
474
|
+
|
|
475
|
+
handle_keyboard_interrupt_properly(ke)
|
|
476
|
+
except ChipDetectionError:
|
|
477
|
+
raise
|
|
478
|
+
except Exception as e:
|
|
479
|
+
raise ChipDetectionError(f"Chip detection failed: {e}") from e
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def detect_and_cache(
|
|
483
|
+
port: str,
|
|
484
|
+
ledger: BoardLedger | None = None,
|
|
485
|
+
force_detect: bool = False,
|
|
486
|
+
verbose: bool = False,
|
|
487
|
+
) -> DetectionResult:
|
|
488
|
+
"""Detect chip type, using cache when available.
|
|
489
|
+
|
|
490
|
+
This function first checks the ledger for a cached chip type. If not found
|
|
491
|
+
or stale (or force_detect is True), it calls esptool to detect the chip
|
|
492
|
+
and caches the result.
|
|
493
|
+
|
|
494
|
+
Args:
|
|
495
|
+
port: Serial port to detect chip on
|
|
496
|
+
ledger: BoardLedger instance (creates default if None)
|
|
497
|
+
force_detect: If True, always call esptool even if cache exists
|
|
498
|
+
verbose: Whether to show verbose output
|
|
499
|
+
|
|
500
|
+
Returns:
|
|
501
|
+
DetectionResult with chip_type, environment, and was_cached flag
|
|
502
|
+
|
|
503
|
+
Raises:
|
|
504
|
+
ChipDetectionError: If detection fails
|
|
505
|
+
BoardLedgerError: If caching fails
|
|
506
|
+
"""
|
|
507
|
+
if ledger is None:
|
|
508
|
+
ledger = BoardLedger()
|
|
509
|
+
|
|
510
|
+
# Check cache first (unless force_detect)
|
|
511
|
+
if not force_detect:
|
|
512
|
+
cached_chip = ledger.get_chip(port)
|
|
513
|
+
if cached_chip is not None:
|
|
514
|
+
environment = CHIP_TO_ENVIRONMENT.get(cached_chip)
|
|
515
|
+
if environment is not None:
|
|
516
|
+
if verbose:
|
|
517
|
+
print(f"Using cached chip type: {cached_chip} -> {environment}")
|
|
518
|
+
return DetectionResult(
|
|
519
|
+
chip_type=cached_chip,
|
|
520
|
+
environment=environment,
|
|
521
|
+
was_cached=True,
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
# Detect with esptool
|
|
525
|
+
if verbose:
|
|
526
|
+
print(f"Detecting chip on port {port}...")
|
|
527
|
+
|
|
528
|
+
chip_type = detect_chip_with_esptool(port, verbose=verbose)
|
|
529
|
+
environment = CHIP_TO_ENVIRONMENT.get(chip_type)
|
|
530
|
+
|
|
531
|
+
if environment is None:
|
|
532
|
+
raise ChipDetectionError(f"Unknown chip type: {chip_type}. Supported chips: {', '.join(sorted(VALID_CHIP_TYPES))}")
|
|
533
|
+
|
|
534
|
+
# Cache the result
|
|
535
|
+
try:
|
|
536
|
+
ledger.set_chip(port, chip_type)
|
|
537
|
+
if verbose:
|
|
538
|
+
print(f"Cached chip detection: {port} -> {chip_type} ({environment})")
|
|
539
|
+
except BoardLedgerError as e:
|
|
540
|
+
if verbose:
|
|
541
|
+
print(f"Warning: Failed to cache chip detection: {e}")
|
|
542
|
+
|
|
543
|
+
return DetectionResult(
|
|
544
|
+
chip_type=chip_type,
|
|
545
|
+
environment=environment,
|
|
546
|
+
was_cached=False,
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def get_environment_for_chip(chip_type: str) -> str | None:
|
|
551
|
+
"""Get the environment name for a chip type.
|
|
552
|
+
|
|
553
|
+
Args:
|
|
554
|
+
chip_type: Chip type string (e.g., "ESP32-S3", "esp32s3")
|
|
555
|
+
|
|
556
|
+
Returns:
|
|
557
|
+
Environment name (e.g., "esp32s3") or None if unknown
|
|
558
|
+
"""
|
|
559
|
+
normalized = BoardLedger._normalize_chip_type(chip_type)
|
|
560
|
+
return CHIP_TO_ENVIRONMENT.get(normalized)
|