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,285 @@
1
+ """
2
+ Monitor Request Processor - Handles serial monitoring operations.
3
+
4
+ This module implements the MonitorRequestProcessor which executes serial
5
+ monitoring operations for Arduino/ESP32 devices. It captures serial output,
6
+ performs pattern matching, and handles halt conditions.
7
+
8
+ Enhanced in Iteration 2 with:
9
+ - SharedSerialManager integration for centralized serial port management
10
+ - Support for multiple reader clients (broadcast output)
11
+ - ConfigurationLockManager for centralized locking
12
+ """
13
+
14
+ import logging
15
+ import sys
16
+ import uuid
17
+ from pathlib import Path
18
+ from typing import TYPE_CHECKING
19
+
20
+ from fbuild.daemon.messages import DaemonState, OperationType
21
+ from fbuild.daemon.port_state_manager import PortState
22
+ from fbuild.daemon.request_processor import RequestProcessor
23
+
24
+ if TYPE_CHECKING:
25
+ from fbuild.daemon.daemon_context import DaemonContext
26
+ from fbuild.daemon.messages import MonitorRequest
27
+
28
+
29
+ class MonitorRequestProcessor(RequestProcessor):
30
+ """Processor for monitor requests.
31
+
32
+ This processor handles serial monitoring of Arduino/ESP32 devices. It:
33
+ 1. Connects to the specified serial port
34
+ 2. Captures and streams output to a file
35
+ 3. Performs pattern matching on the output
36
+ 4. Handles halt conditions (error/success patterns)
37
+ 5. Times out if specified
38
+
39
+ The monitor runs until:
40
+ - A halt pattern is matched (halt_on_error or halt_on_success)
41
+ - The timeout is reached
42
+ - The user interrupts it (Ctrl+C)
43
+ - An error occurs
44
+
45
+ Example:
46
+ >>> processor = MonitorRequestProcessor()
47
+ >>> success = processor.process_request(monitor_request, daemon_context)
48
+ """
49
+
50
+ def get_operation_type(self) -> OperationType:
51
+ """Return MONITOR operation type."""
52
+ return OperationType.MONITOR
53
+
54
+ def get_required_locks(self, request: "MonitorRequest", context: "DaemonContext") -> dict[str, str]:
55
+ """Monitor operations require only a port lock.
56
+
57
+ Args:
58
+ request: The monitor request
59
+ context: The daemon context
60
+
61
+ Returns:
62
+ Dictionary with port lock requirement
63
+ """
64
+ return {"port": request.port} if request.port else {}
65
+
66
+ def validate_request(self, request: "MonitorRequest", context: "DaemonContext") -> bool:
67
+ """Validate that the monitor request has a port specified.
68
+
69
+ Args:
70
+ request: The monitor request
71
+ context: The daemon context
72
+
73
+ Returns:
74
+ True if request is valid (has port), False otherwise
75
+ """
76
+ if not request.port:
77
+ logging.error("Monitor requires port to be specified")
78
+ return False
79
+ return True
80
+
81
+ def get_starting_state(self) -> DaemonState:
82
+ """Monitor starts in MONITORING state."""
83
+ return DaemonState.MONITORING
84
+
85
+ def get_starting_message(self, request: "MonitorRequest") -> str:
86
+ """Get the starting status message."""
87
+ return f"Monitoring {request.environment} on {request.port}"
88
+
89
+ def get_success_message(self, request: "MonitorRequest") -> str:
90
+ """Get the success status message."""
91
+ return "Monitor completed"
92
+
93
+ def get_failure_message(self, request: "MonitorRequest") -> str:
94
+ """Get the failure status message."""
95
+ return "Monitor failed"
96
+
97
+ def execute_operation(self, request: "MonitorRequest", context: "DaemonContext") -> bool:
98
+ """Execute the serial monitoring operation.
99
+
100
+ This is the core monitor logic extracted from the original
101
+ process_monitor_request function. All boilerplate (locks, status
102
+ updates, error handling) is handled by the base RequestProcessor.
103
+
104
+ Enhanced in Iteration 2:
105
+ - If SharedSerialManager has the port open, we can attach as a reader
106
+ to get shared access (multiple clients can monitor the same port)
107
+ - Falls back to direct serial access for exclusive monitoring
108
+
109
+ Args:
110
+ request: The monitor request containing port, baud_rate, etc.
111
+ context: The daemon context with all subsystems
112
+
113
+ Returns:
114
+ True if monitoring completed successfully, False otherwise
115
+ """
116
+ logging.info(f"Starting monitor on {request.port}")
117
+
118
+ # Track port state as MONITORING
119
+ port = request.port
120
+ if port:
121
+ context.port_state_manager.acquire_port(
122
+ port=port,
123
+ state=PortState.MONITORING,
124
+ client_pid=request.caller_pid,
125
+ project_dir=request.project_dir,
126
+ environment=request.environment,
127
+ operation_id=request.request_id,
128
+ )
129
+
130
+ # Generate a client ID for shared serial manager
131
+ client_id = f"monitor_{request.request_id}_{uuid.uuid4().hex[:8]}"
132
+
133
+ # Check if port is already managed by SharedSerialManager
134
+ shared_session = context.shared_serial_manager.get_session_info(port) if port else None
135
+
136
+ try:
137
+ if shared_session and shared_session.get("is_open"):
138
+ # Port is already open - attach as a reader for shared access
139
+ assert port is not None # Guaranteed by shared_session check above
140
+ return self._monitor_shared(request, port, client_id, context)
141
+ else:
142
+ # Port not managed - use direct serial access
143
+ return self._monitor_direct(request, port, context)
144
+
145
+ finally:
146
+ # Always clean up
147
+ if port:
148
+ # Detach from shared session if we were attached
149
+ if shared_session and shared_session.get("is_open"):
150
+ context.shared_serial_manager.detach_reader(port, client_id)
151
+ # Release port state
152
+ context.port_state_manager.release_port(port)
153
+
154
+ def _monitor_shared(
155
+ self,
156
+ request: "MonitorRequest",
157
+ port: str,
158
+ client_id: str,
159
+ context: "DaemonContext",
160
+ ) -> bool:
161
+ """Monitor using SharedSerialManager for shared access.
162
+
163
+ This allows multiple clients to monitor the same port simultaneously,
164
+ receiving broadcast output from the daemon-managed serial session.
165
+
166
+ Args:
167
+ request: The monitor request
168
+ port: Serial port to monitor
169
+ client_id: Client ID for the shared session
170
+ context: The daemon context
171
+
172
+ Returns:
173
+ True if monitoring completed successfully
174
+ """
175
+ import time
176
+
177
+ logging.info(f"Using shared serial access for {port}")
178
+
179
+ # Attach as a reader
180
+ if not context.shared_serial_manager.attach_reader(port, client_id):
181
+ logging.error(f"Failed to attach as reader to {port}")
182
+ return False
183
+
184
+ # Create output file path for streaming
185
+ output_file = Path(request.project_dir) / ".fbuild" / "monitor_output.txt"
186
+ output_file.parent.mkdir(parents=True, exist_ok=True)
187
+ output_file.write_text("", encoding="utf-8")
188
+
189
+ # Monitor loop - poll the buffer and check for patterns
190
+ start_time = time.time()
191
+ timeout = request.timeout if request.timeout else float("inf")
192
+ last_buffer_size = 0
193
+
194
+ try:
195
+ with output_file.open("a", encoding="utf-8") as f:
196
+ while True:
197
+ # Check timeout
198
+ elapsed = time.time() - start_time
199
+ if elapsed >= timeout:
200
+ logging.info(f"Monitor timeout after {elapsed:.1f}s")
201
+ return True
202
+
203
+ # Read buffered output
204
+ lines = context.shared_serial_manager.read_buffer(port, client_id, max_lines=1000)
205
+
206
+ # Process new lines
207
+ if len(lines) > last_buffer_size:
208
+ new_lines = lines[last_buffer_size:]
209
+ for line in new_lines:
210
+ # Write to output file
211
+ f.write(line + "\n")
212
+ f.flush()
213
+
214
+ # Check halt patterns
215
+ if request.halt_on_error and request.halt_on_error in line:
216
+ logging.error(f"Halt on error pattern matched: {line}")
217
+ return False
218
+ if request.halt_on_success and request.halt_on_success in line:
219
+ logging.info(f"Halt on success pattern matched: {line}")
220
+ return True
221
+
222
+ last_buffer_size = len(lines)
223
+
224
+ # Brief sleep to avoid busy-waiting
225
+ time.sleep(0.1)
226
+
227
+ except KeyboardInterrupt: # noqa: KBI002
228
+ logging.info("Monitor interrupted by user")
229
+ return True
230
+
231
+ def _monitor_direct(self, request: "MonitorRequest", port: str | None, context: "DaemonContext") -> bool:
232
+ """Monitor using direct serial access (exclusive).
233
+
234
+ This is the original monitoring approach using SerialMonitor.
235
+
236
+ Args:
237
+ request: The monitor request
238
+ port: Serial port to monitor
239
+ context: The daemon context (unused but passed for consistency)
240
+
241
+ Returns:
242
+ True if monitoring completed successfully
243
+ """
244
+ # Create output file path for streaming
245
+ output_file = Path(request.project_dir) / ".fbuild" / "monitor_output.txt"
246
+ output_file.parent.mkdir(parents=True, exist_ok=True)
247
+ # Clear/truncate output file before starting
248
+ output_file.write_text("", encoding="utf-8")
249
+
250
+ # Create summary file path
251
+ summary_file = Path(request.project_dir) / ".fbuild" / "monitor_summary.json"
252
+ # Clear old summary file
253
+ if summary_file.exists():
254
+ summary_file.unlink()
255
+
256
+ try:
257
+ # Get fresh monitor class after module reload
258
+ # Using direct import would use cached version
259
+ monitor_class = getattr(sys.modules["fbuild.deploy.monitor"], "SerialMonitor")
260
+ except (KeyError, AttributeError) as e:
261
+ logging.error(f"Failed to get SerialMonitor class: {e}")
262
+ return False
263
+
264
+ # Create monitor and execute
265
+ monitor = monitor_class(verbose=False)
266
+ exit_code = monitor.monitor(
267
+ project_dir=Path(request.project_dir),
268
+ env_name=request.environment,
269
+ port=request.port,
270
+ baud=request.baud_rate if request.baud_rate else 115200,
271
+ timeout=int(request.timeout) if request.timeout is not None else None,
272
+ halt_on_error=request.halt_on_error,
273
+ halt_on_success=request.halt_on_success,
274
+ expect=request.expect,
275
+ output_file=output_file,
276
+ summary_file=summary_file,
277
+ timestamp=request.show_timestamp,
278
+ )
279
+
280
+ if exit_code == 0:
281
+ logging.info("Monitor completed successfully")
282
+ return True
283
+ else:
284
+ logging.error(f"Monitor failed with exit code {exit_code}")
285
+ return False