fbuild 1.1.0__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.
Potentially problematic release.
This version of fbuild might be problematic. Click here for more details.
- fbuild/__init__.py +0 -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_state.py +325 -0
- fbuild/build/build_utils.py +98 -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 +612 -0
- fbuild/build/configurable_linker.py +637 -0
- fbuild/build/flag_builder.py +186 -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 +656 -0
- fbuild/build/orchestrator_esp32.py +797 -0
- fbuild/build/orchestrator_teensy.py +543 -0
- fbuild/build/source_compilation_orchestrator.py +220 -0
- fbuild/build/source_scanner.py +516 -0
- fbuild/cli.py +566 -0
- fbuild/cli_utils.py +312 -0
- fbuild/config/__init__.py +16 -0
- fbuild/config/board_config.py +457 -0
- fbuild/config/board_loader.py +92 -0
- fbuild/config/ini_parser.py +209 -0
- fbuild/config/mcu_specs.py +88 -0
- fbuild/daemon/__init__.py +34 -0
- fbuild/daemon/client.py +929 -0
- fbuild/daemon/compilation_queue.py +293 -0
- fbuild/daemon/daemon.py +474 -0
- fbuild/daemon/daemon_context.py +196 -0
- fbuild/daemon/error_collector.py +263 -0
- fbuild/daemon/file_cache.py +332 -0
- fbuild/daemon/lock_manager.py +270 -0
- fbuild/daemon/logging_utils.py +149 -0
- fbuild/daemon/messages.py +301 -0
- fbuild/daemon/operation_registry.py +288 -0
- fbuild/daemon/process_tracker.py +366 -0
- fbuild/daemon/processors/__init__.py +12 -0
- fbuild/daemon/processors/build_processor.py +157 -0
- fbuild/daemon/processors/deploy_processor.py +327 -0
- fbuild/daemon/processors/monitor_processor.py +146 -0
- fbuild/daemon/request_processor.py +401 -0
- fbuild/daemon/status_manager.py +216 -0
- fbuild/daemon/subprocess_manager.py +316 -0
- fbuild/deploy/__init__.py +17 -0
- fbuild/deploy/deployer.py +67 -0
- fbuild/deploy/deployer_esp32.py +314 -0
- fbuild/deploy/monitor.py +495 -0
- fbuild/interrupt_utils.py +34 -0
- fbuild/packages/__init__.py +53 -0
- fbuild/packages/archive_utils.py +1098 -0
- fbuild/packages/arduino_core.py +412 -0
- fbuild/packages/cache.py +249 -0
- fbuild/packages/downloader.py +366 -0
- fbuild/packages/framework_esp32.py +538 -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 +413 -0
- fbuild/packages/package.py +163 -0
- fbuild/packages/platform_esp32.py +383 -0
- fbuild/packages/platform_teensy.py +312 -0
- fbuild/packages/platform_utils.py +131 -0
- fbuild/packages/platformio_registry.py +325 -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 +484 -0
- fbuild/packages/toolchain_metadata.py +185 -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-1.1.0.dist-info/METADATA +447 -0
- fbuild-1.1.0.dist-info/RECORD +93 -0
- fbuild-1.1.0.dist-info/WHEEL +5 -0
- fbuild-1.1.0.dist-info/entry_points.txt +5 -0
- fbuild-1.1.0.dist-info/licenses/LICENSE +21 -0
- fbuild-1.1.0.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,316 @@
|
|
|
1
|
+
"""Centralized subprocess execution manager for daemon operations.
|
|
2
|
+
|
|
3
|
+
This module provides a unified interface for executing subprocesses with tracking,
|
|
4
|
+
logging, and statistics. All subprocess calls should go through this manager for
|
|
5
|
+
consistent error handling and monitoring.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import subprocess
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Optional
|
|
17
|
+
|
|
18
|
+
from ..interrupt_utils import handle_keyboard_interrupt_properly
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class SubprocessExecution:
|
|
25
|
+
"""Single subprocess execution with full tracking."""
|
|
26
|
+
|
|
27
|
+
execution_id: str
|
|
28
|
+
command: list[str]
|
|
29
|
+
cwd: Optional[Path]
|
|
30
|
+
env: Optional[dict[str, str]]
|
|
31
|
+
timeout: Optional[float]
|
|
32
|
+
returncode: Optional[int] = None
|
|
33
|
+
stdout: Optional[str] = None
|
|
34
|
+
stderr: Optional[str] = None
|
|
35
|
+
start_time: Optional[float] = None
|
|
36
|
+
end_time: Optional[float] = None
|
|
37
|
+
error: Optional[str] = None
|
|
38
|
+
|
|
39
|
+
def duration(self) -> Optional[float]:
|
|
40
|
+
"""Calculate execution duration in seconds."""
|
|
41
|
+
if self.start_time and self.end_time:
|
|
42
|
+
return self.end_time - self.start_time
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
def success(self) -> bool:
|
|
46
|
+
"""Check if execution was successful."""
|
|
47
|
+
return self.returncode == 0 and self.error is None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class SubprocessManager:
|
|
51
|
+
"""Centralized subprocess execution manager.
|
|
52
|
+
|
|
53
|
+
Provides tracking, logging, and statistics for all subprocess executions
|
|
54
|
+
in the daemon. Thread-safe for concurrent use.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(self, max_history: int = 1000):
|
|
58
|
+
"""Initialize subprocess manager.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
max_history: Maximum number of executions to keep in history
|
|
62
|
+
"""
|
|
63
|
+
self.executions: dict[str, SubprocessExecution] = {}
|
|
64
|
+
self.lock = threading.Lock()
|
|
65
|
+
self.max_history = max_history
|
|
66
|
+
self._execution_counter = 0
|
|
67
|
+
logger.info(f"SubprocessManager initialized successfully (max_history={max_history})")
|
|
68
|
+
|
|
69
|
+
def execute(
|
|
70
|
+
self,
|
|
71
|
+
command: list[str],
|
|
72
|
+
cwd: Optional[Path] = None,
|
|
73
|
+
env: Optional[dict[str, str]] = None,
|
|
74
|
+
timeout: Optional[float] = 60,
|
|
75
|
+
capture_output: bool = True,
|
|
76
|
+
check: bool = False,
|
|
77
|
+
) -> SubprocessExecution:
|
|
78
|
+
"""Execute subprocess with tracking.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
command: Command and arguments to execute
|
|
82
|
+
cwd: Working directory for subprocess
|
|
83
|
+
env: Environment variables
|
|
84
|
+
timeout: Timeout in seconds (None = no timeout)
|
|
85
|
+
capture_output: Whether to capture stdout/stderr
|
|
86
|
+
check: Whether to raise exception on non-zero exit code
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
SubprocessExecution with results and timing information
|
|
90
|
+
"""
|
|
91
|
+
# Generate unique execution ID
|
|
92
|
+
with self.lock:
|
|
93
|
+
self._execution_counter += 1
|
|
94
|
+
execution_id = f"subprocess_{int(time.time() * 1000000)}_{self._execution_counter}"
|
|
95
|
+
logger.debug(f"Subprocess command: {' '.join(str(c) for c in command)}")
|
|
96
|
+
logger.debug(f"Environment variables: {len(env) if env else 0} vars")
|
|
97
|
+
logger.debug(f"Capture output: {capture_output}")
|
|
98
|
+
|
|
99
|
+
execution = SubprocessExecution(
|
|
100
|
+
execution_id=execution_id,
|
|
101
|
+
command=command,
|
|
102
|
+
cwd=cwd,
|
|
103
|
+
env=env,
|
|
104
|
+
timeout=timeout,
|
|
105
|
+
start_time=time.time(),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Store execution
|
|
109
|
+
with self.lock:
|
|
110
|
+
self.executions[execution_id] = execution
|
|
111
|
+
logger.debug(f"Stored execution {execution_id} in history (total: {len(self.executions)})")
|
|
112
|
+
self._cleanup_old_executions()
|
|
113
|
+
|
|
114
|
+
# Log execution start
|
|
115
|
+
cmd_str = " ".join(str(c) for c in command[:3]) # First 3 args
|
|
116
|
+
if len(command) > 3:
|
|
117
|
+
cmd_str += "..."
|
|
118
|
+
logger.info(f"Starting subprocess {execution_id}: {cmd_str}")
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
# Execute subprocess
|
|
122
|
+
logger.debug(f"Executing subprocess.run() for {execution_id}")
|
|
123
|
+
result = subprocess.run(
|
|
124
|
+
command,
|
|
125
|
+
cwd=cwd,
|
|
126
|
+
env=env,
|
|
127
|
+
capture_output=capture_output,
|
|
128
|
+
text=True,
|
|
129
|
+
timeout=timeout,
|
|
130
|
+
check=check,
|
|
131
|
+
)
|
|
132
|
+
execution.returncode = result.returncode
|
|
133
|
+
execution.stdout = result.stdout if capture_output else None
|
|
134
|
+
execution.stderr = result.stderr if capture_output else None
|
|
135
|
+
execution.end_time = time.time()
|
|
136
|
+
|
|
137
|
+
# Log output details
|
|
138
|
+
if capture_output:
|
|
139
|
+
logger.debug(f"Subprocess {execution_id} stdout: {len(result.stdout) if result.stdout else 0} bytes")
|
|
140
|
+
logger.debug(f"Subprocess {execution_id} stderr: {len(result.stderr) if result.stderr else 0} bytes")
|
|
141
|
+
|
|
142
|
+
# Log result
|
|
143
|
+
duration = execution.duration()
|
|
144
|
+
if result.returncode == 0:
|
|
145
|
+
logger.info(f"Subprocess {execution_id}: SUCCESS in {duration:.2f}s")
|
|
146
|
+
else:
|
|
147
|
+
logger.warning(f"Subprocess {execution_id}: FAILED with code {result.returncode} in {duration:.2f}s")
|
|
148
|
+
if result.stderr and capture_output:
|
|
149
|
+
logger.debug(f"Subprocess {execution_id} stderr: {result.stderr[:200]}")
|
|
150
|
+
|
|
151
|
+
except subprocess.TimeoutExpired as e:
|
|
152
|
+
logger.error(f"Subprocess {execution_id}: TIMEOUT after {timeout}s")
|
|
153
|
+
execution.error = f"Timeout after {timeout}s"
|
|
154
|
+
execution.returncode = -1
|
|
155
|
+
execution.stderr = str(e)
|
|
156
|
+
execution.end_time = time.time()
|
|
157
|
+
|
|
158
|
+
except subprocess.CalledProcessError as e:
|
|
159
|
+
logger.error(f"Subprocess {execution_id}: CalledProcessError with exit code {e.returncode}")
|
|
160
|
+
execution.error = f"Process failed with exit code {e.returncode}"
|
|
161
|
+
execution.returncode = e.returncode
|
|
162
|
+
execution.stdout = e.stdout if capture_output else None
|
|
163
|
+
execution.stderr = e.stderr if capture_output else None
|
|
164
|
+
execution.end_time = time.time()
|
|
165
|
+
|
|
166
|
+
except KeyboardInterrupt as ke:
|
|
167
|
+
logger.warning(f"Subprocess {execution_id}: KeyboardInterrupt received")
|
|
168
|
+
handle_keyboard_interrupt_properly(ke)
|
|
169
|
+
|
|
170
|
+
except Exception as e:
|
|
171
|
+
logger.error(f"Subprocess {execution_id}: Unexpected exception: {e}", exc_info=True)
|
|
172
|
+
execution.error = str(e)
|
|
173
|
+
execution.returncode = -1
|
|
174
|
+
execution.end_time = time.time()
|
|
175
|
+
|
|
176
|
+
logger.debug(f"Returning execution result for {execution_id} (success={execution.success()})")
|
|
177
|
+
return execution
|
|
178
|
+
|
|
179
|
+
def get_execution(self, execution_id: str) -> Optional[SubprocessExecution]:
|
|
180
|
+
"""Get execution by ID.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
execution_id: Execution ID to retrieve
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
SubprocessExecution if found, None otherwise
|
|
187
|
+
"""
|
|
188
|
+
with self.lock:
|
|
189
|
+
execution = self.executions.get(execution_id)
|
|
190
|
+
if execution:
|
|
191
|
+
logger.debug(f"Found execution {execution_id} (success={execution.success()})")
|
|
192
|
+
else:
|
|
193
|
+
logger.debug(f"Execution {execution_id} not found")
|
|
194
|
+
return execution
|
|
195
|
+
|
|
196
|
+
def get_statistics(self) -> dict[str, Any]:
|
|
197
|
+
"""Get subprocess execution statistics.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Dictionary with execution counts and statistics
|
|
201
|
+
"""
|
|
202
|
+
with self.lock:
|
|
203
|
+
total = len(self.executions)
|
|
204
|
+
successful = sum(1 for e in self.executions.values() if e.success())
|
|
205
|
+
failed = sum(1 for e in self.executions.values() if not e.success())
|
|
206
|
+
|
|
207
|
+
# Calculate average duration for successful executions
|
|
208
|
+
successful_durations: list[float] = []
|
|
209
|
+
for e in self.executions.values():
|
|
210
|
+
if e.success():
|
|
211
|
+
duration = e.duration()
|
|
212
|
+
if duration is not None:
|
|
213
|
+
successful_durations.append(duration)
|
|
214
|
+
avg_duration = sum(successful_durations) / len(successful_durations) if successful_durations else 0.0
|
|
215
|
+
|
|
216
|
+
logger.debug(f"Average execution duration: {avg_duration:.3f}s (from {len(successful_durations)} successful executions)")
|
|
217
|
+
|
|
218
|
+
success_rate = (successful / total * 100) if total > 0 else 0.0
|
|
219
|
+
logger.info(f"Subprocess statistics: {successful}/{total} successful ({success_rate:.1f}% success rate)")
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
"total_executions": total,
|
|
223
|
+
"successful": successful,
|
|
224
|
+
"failed": failed,
|
|
225
|
+
"average_duration_seconds": round(avg_duration, 3),
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
def get_recent_failures(self, count: int = 10) -> list[SubprocessExecution]:
|
|
229
|
+
"""Get most recent failed executions.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
count: Maximum number of failures to return
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
List of failed SubprocessExecution objects
|
|
236
|
+
"""
|
|
237
|
+
with self.lock:
|
|
238
|
+
failures = [e for e in self.executions.values() if not e.success()]
|
|
239
|
+
logger.debug(f"Found {len(failures)} total failures in history")
|
|
240
|
+
# Sort by end_time descending (most recent first)
|
|
241
|
+
failures.sort(key=lambda e: e.end_time or 0, reverse=True)
|
|
242
|
+
result = failures[:count]
|
|
243
|
+
logger.debug(f"Returning {len(result)} most recent failures")
|
|
244
|
+
if result:
|
|
245
|
+
logger.info(f"Recent subprocess failures: {len(result)} failures found")
|
|
246
|
+
return result
|
|
247
|
+
|
|
248
|
+
def clear_history(self):
|
|
249
|
+
"""Clear all execution history."""
|
|
250
|
+
with self.lock:
|
|
251
|
+
count = len(self.executions)
|
|
252
|
+
self.executions.clear()
|
|
253
|
+
logger.info(f"Subprocess execution history cleared ({count} records removed)")
|
|
254
|
+
|
|
255
|
+
def _cleanup_old_executions(self):
|
|
256
|
+
"""Remove old executions beyond max_history limit.
|
|
257
|
+
|
|
258
|
+
Keeps successful executions to max_history, but always keeps all recent failures.
|
|
259
|
+
"""
|
|
260
|
+
current_count = len(self.executions)
|
|
261
|
+
if current_count <= self.max_history:
|
|
262
|
+
return
|
|
263
|
+
|
|
264
|
+
# Get all executions sorted by end time
|
|
265
|
+
all_executions = sorted(self.executions.values(), key=lambda e: e.end_time or 0)
|
|
266
|
+
logger.debug(f"Sorted {len(all_executions)} executions by end time")
|
|
267
|
+
|
|
268
|
+
# Keep all failures and recent successes
|
|
269
|
+
successes = [e for e in all_executions if e.success()]
|
|
270
|
+
failures = [e for e in all_executions if not e.success()]
|
|
271
|
+
logger.debug(f"Execution breakdown: {len(successes)} successes, {len(failures)} failures")
|
|
272
|
+
|
|
273
|
+
# Remove oldest successes if we're over limit
|
|
274
|
+
to_remove = len(self.executions) - self.max_history
|
|
275
|
+
if to_remove > 0 and len(successes) > to_remove:
|
|
276
|
+
for execution in successes[:to_remove]:
|
|
277
|
+
del self.executions[execution.execution_id]
|
|
278
|
+
|
|
279
|
+
logger.info(f"Cleaned up {to_remove} old successful subprocess executions (history now: {len(self.executions)})")
|
|
280
|
+
else:
|
|
281
|
+
logger.debug(f"Cannot remove {to_remove} executions (only {len(successes)} successes available)")
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# Global subprocess manager instance (initialized by daemon)
|
|
285
|
+
_subprocess_manager: Optional[SubprocessManager] = None
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def get_subprocess_manager() -> SubprocessManager:
|
|
289
|
+
"""Get global subprocess manager instance.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Global SubprocessManager instance
|
|
293
|
+
|
|
294
|
+
Raises:
|
|
295
|
+
RuntimeError: If subprocess manager not initialized
|
|
296
|
+
"""
|
|
297
|
+
global _subprocess_manager
|
|
298
|
+
if _subprocess_manager is None:
|
|
299
|
+
logger.error("SubprocessManager accessed before initialization")
|
|
300
|
+
raise RuntimeError("SubprocessManager not initialized. Call init_subprocess_manager() first.")
|
|
301
|
+
return _subprocess_manager
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def init_subprocess_manager(max_history: int = 1000) -> SubprocessManager:
|
|
305
|
+
"""Initialize global subprocess manager.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
max_history: Maximum number of executions to keep in history
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
Initialized SubprocessManager instance
|
|
312
|
+
"""
|
|
313
|
+
global _subprocess_manager
|
|
314
|
+
_subprocess_manager = SubprocessManager(max_history=max_history)
|
|
315
|
+
logger.info("Global SubprocessManager initialized successfully")
|
|
316
|
+
return _subprocess_manager
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Firmware deployment functionality for fbuild.
|
|
3
|
+
|
|
4
|
+
This module provides deployment capabilities for uploading firmware to devices.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .deployer import DeploymentError, DeploymentResult, IDeployer
|
|
8
|
+
from .deployer_esp32 import ESP32Deployer
|
|
9
|
+
from .monitor import SerialMonitor
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"IDeployer",
|
|
13
|
+
"ESP32Deployer",
|
|
14
|
+
"DeploymentResult",
|
|
15
|
+
"DeploymentError",
|
|
16
|
+
"SerialMonitor",
|
|
17
|
+
]
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Abstract base class for firmware deployers.
|
|
2
|
+
|
|
3
|
+
This module defines the interface for platform-specific deployers
|
|
4
|
+
to ensure consistent behavior across different platforms.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class DeploymentResult:
|
|
15
|
+
"""Result of a firmware deployment operation."""
|
|
16
|
+
|
|
17
|
+
success: bool
|
|
18
|
+
message: str
|
|
19
|
+
port: Optional[str] = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DeploymentError(Exception):
|
|
23
|
+
"""Base exception for deployment errors."""
|
|
24
|
+
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class IDeployer(ABC):
|
|
29
|
+
"""Interface for firmware deployers.
|
|
30
|
+
|
|
31
|
+
Deployers handle uploading firmware to embedded devices:
|
|
32
|
+
1. Locate firmware binaries
|
|
33
|
+
2. Detect or validate serial port
|
|
34
|
+
3. Flash firmware to device
|
|
35
|
+
4. Verify upload success
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def deploy(
|
|
40
|
+
self,
|
|
41
|
+
project_dir: Path,
|
|
42
|
+
env_name: str,
|
|
43
|
+
port: Optional[str] = None,
|
|
44
|
+
) -> DeploymentResult:
|
|
45
|
+
"""Deploy firmware to a device.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
project_dir: Path to project directory
|
|
49
|
+
env_name: Environment name to deploy
|
|
50
|
+
port: Serial port to use (auto-detect if None)
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
DeploymentResult with success status and message
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
DeploymentError: If deployment fails
|
|
57
|
+
"""
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
def _detect_serial_port(self) -> Optional[str]:
|
|
62
|
+
"""Auto-detect serial port for device.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Serial port name or None if not found
|
|
66
|
+
"""
|
|
67
|
+
pass
|