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,957 @@
1
+ """
2
+ Typed message protocol for fbuild daemon operations.
3
+
4
+ This module defines typed dataclasses for all client-daemon communication,
5
+ ensuring type safety and validation.
6
+
7
+ Supports:
8
+ - Build operations (compilation and linking)
9
+ - Deploy operations (firmware upload)
10
+ - Monitor operations (serial monitoring)
11
+ - Status updates and progress tracking
12
+ - Lock management (acquire, release, query)
13
+ - Firmware queries (check if firmware is current)
14
+ - Serial session management (attach, detach, write)
15
+ """
16
+
17
+ import time
18
+ from dataclasses import asdict, dataclass, field
19
+ from enum import Enum
20
+ from typing import Any
21
+
22
+
23
+ class DaemonState(Enum):
24
+ """Daemon state enumeration."""
25
+
26
+ IDLE = "idle"
27
+ DEPLOYING = "deploying"
28
+ MONITORING = "monitoring"
29
+ BUILDING = "building"
30
+ COMPLETED = "completed"
31
+ FAILED = "failed"
32
+ UNKNOWN = "unknown"
33
+
34
+ @classmethod
35
+ def from_string(cls, value: str) -> "DaemonState":
36
+ """Convert string to DaemonState, defaulting to UNKNOWN if invalid."""
37
+ try:
38
+ return cls(value)
39
+ except ValueError:
40
+ return cls.UNKNOWN
41
+
42
+
43
+ class OperationType(Enum):
44
+ """Type of operation being performed."""
45
+
46
+ BUILD = "build"
47
+ DEPLOY = "deploy"
48
+ MONITOR = "monitor"
49
+ BUILD_AND_DEPLOY = "build_and_deploy"
50
+
51
+ @classmethod
52
+ def from_string(cls, value: str) -> "OperationType":
53
+ """Convert string to OperationType."""
54
+ return cls(value)
55
+
56
+
57
+ @dataclass
58
+ class DeployRequest:
59
+ """Client → Daemon: Deploy request message.
60
+
61
+ Attributes:
62
+ project_dir: Absolute path to project directory
63
+ environment: Build environment name
64
+ port: Serial port for deployment (optional, auto-detect if None)
65
+ clean_build: Whether to perform clean build
66
+ monitor_after: Whether to start monitor after deploy
67
+ monitor_timeout: Timeout for monitor in seconds (if monitor_after=True)
68
+ monitor_halt_on_error: Pattern to halt on error (if monitor_after=True)
69
+ monitor_halt_on_success: Pattern to halt on success (if monitor_after=True)
70
+ monitor_expect: Expected pattern to check at timeout/success (if monitor_after=True)
71
+ monitor_show_timestamp: Whether to prefix monitor output lines with elapsed time
72
+ caller_pid: Process ID of requesting client
73
+ caller_cwd: Working directory of requesting client
74
+ timestamp: Unix timestamp when request was created
75
+ request_id: Unique identifier for this request
76
+ """
77
+
78
+ project_dir: str
79
+ environment: str
80
+ port: str | None
81
+ clean_build: bool
82
+ monitor_after: bool
83
+ monitor_timeout: float | None
84
+ monitor_halt_on_error: str | None
85
+ monitor_halt_on_success: str | None
86
+ monitor_expect: str | None
87
+ caller_pid: int
88
+ caller_cwd: str
89
+ monitor_show_timestamp: bool = False
90
+ timestamp: float = field(default_factory=time.time)
91
+ request_id: str = field(default_factory=lambda: f"deploy_{int(time.time() * 1000)}")
92
+
93
+ def to_dict(self) -> dict[str, Any]:
94
+ """Convert to dictionary for JSON serialization."""
95
+ return asdict(self)
96
+
97
+ @classmethod
98
+ def from_dict(cls, data: dict[str, Any]) -> "DeployRequest":
99
+ """Create DeployRequest from dictionary."""
100
+ return cls(
101
+ project_dir=data["project_dir"],
102
+ environment=data["environment"],
103
+ port=data.get("port"),
104
+ clean_build=data.get("clean_build", False),
105
+ monitor_after=data.get("monitor_after", False),
106
+ monitor_timeout=data.get("monitor_timeout"),
107
+ monitor_halt_on_error=data.get("monitor_halt_on_error"),
108
+ monitor_halt_on_success=data.get("monitor_halt_on_success"),
109
+ monitor_expect=data.get("monitor_expect"),
110
+ caller_pid=data["caller_pid"],
111
+ caller_cwd=data["caller_cwd"],
112
+ monitor_show_timestamp=data.get("monitor_show_timestamp", False),
113
+ timestamp=data.get("timestamp", time.time()),
114
+ request_id=data.get("request_id", f"deploy_{int(time.time() * 1000)}"),
115
+ )
116
+
117
+
118
+ @dataclass
119
+ class MonitorRequest:
120
+ """Client → Daemon: Monitor request message.
121
+
122
+ Attributes:
123
+ project_dir: Absolute path to project directory
124
+ environment: Build environment name
125
+ port: Serial port for monitoring (optional, auto-detect if None)
126
+ baud_rate: Serial baud rate (optional, use config default if None)
127
+ halt_on_error: Pattern to halt on (error detection)
128
+ halt_on_success: Pattern to halt on (success detection)
129
+ expect: Expected pattern to check at timeout/success
130
+ timeout: Maximum monitoring time in seconds
131
+ caller_pid: Process ID of requesting client
132
+ caller_cwd: Working directory of requesting client
133
+ show_timestamp: Whether to prefix output lines with elapsed time (SS.HH format)
134
+ timestamp: Unix timestamp when request was created
135
+ request_id: Unique identifier for this request
136
+ """
137
+
138
+ project_dir: str
139
+ environment: str
140
+ port: str | None
141
+ baud_rate: int | None
142
+ halt_on_error: str | None
143
+ halt_on_success: str | None
144
+ expect: str | None
145
+ timeout: float | None
146
+ caller_pid: int
147
+ caller_cwd: str
148
+ show_timestamp: bool = False
149
+ timestamp: float = field(default_factory=time.time)
150
+ request_id: str = field(default_factory=lambda: f"monitor_{int(time.time() * 1000)}")
151
+
152
+ def to_dict(self) -> dict[str, Any]:
153
+ """Convert to dictionary for JSON serialization."""
154
+ return asdict(self)
155
+
156
+ @classmethod
157
+ def from_dict(cls, data: dict[str, Any]) -> "MonitorRequest":
158
+ """Create MonitorRequest from dictionary."""
159
+ return cls(
160
+ project_dir=data["project_dir"],
161
+ environment=data["environment"],
162
+ port=data.get("port"),
163
+ baud_rate=data.get("baud_rate"),
164
+ halt_on_error=data.get("halt_on_error"),
165
+ halt_on_success=data.get("halt_on_success"),
166
+ expect=data.get("expect"),
167
+ timeout=data.get("timeout"),
168
+ caller_pid=data["caller_pid"],
169
+ caller_cwd=data["caller_cwd"],
170
+ show_timestamp=data.get("show_timestamp", False),
171
+ timestamp=data.get("timestamp", time.time()),
172
+ request_id=data.get("request_id", f"monitor_{int(time.time() * 1000)}"),
173
+ )
174
+
175
+
176
+ @dataclass
177
+ class BuildRequest:
178
+ """Client → Daemon: Build request message.
179
+
180
+ Attributes:
181
+ project_dir: Absolute path to project directory
182
+ environment: Build environment name
183
+ clean_build: Whether to perform clean build
184
+ verbose: Enable verbose build output
185
+ caller_pid: Process ID of requesting client
186
+ caller_cwd: Working directory of requesting client
187
+ timestamp: Unix timestamp when request was created
188
+ request_id: Unique identifier for this request
189
+ """
190
+
191
+ project_dir: str
192
+ environment: str
193
+ clean_build: bool
194
+ verbose: bool
195
+ caller_pid: int
196
+ caller_cwd: str
197
+ timestamp: float = field(default_factory=time.time)
198
+ request_id: str = field(default_factory=lambda: f"build_{int(time.time() * 1000)}")
199
+
200
+ def to_dict(self) -> dict[str, Any]:
201
+ """Convert to dictionary for JSON serialization."""
202
+ return asdict(self)
203
+
204
+ @classmethod
205
+ def from_dict(cls, data: dict[str, Any]) -> "BuildRequest":
206
+ """Create BuildRequest from dictionary."""
207
+ return cls(
208
+ project_dir=data["project_dir"],
209
+ environment=data["environment"],
210
+ clean_build=data.get("clean_build", False),
211
+ verbose=data.get("verbose", False),
212
+ caller_pid=data["caller_pid"],
213
+ caller_cwd=data["caller_cwd"],
214
+ timestamp=data.get("timestamp", time.time()),
215
+ request_id=data.get("request_id", f"build_{int(time.time() * 1000)}"),
216
+ )
217
+
218
+
219
+ @dataclass
220
+ class InstallDependenciesRequest:
221
+ """Client → Daemon: Install dependencies request message.
222
+
223
+ This request downloads and caches all dependencies (toolchain, platform,
224
+ framework, libraries) without performing a build. Useful for:
225
+ - Pre-warming the cache before builds
226
+ - Ensuring dependencies are available offline
227
+ - Separating dependency installation from compilation
228
+
229
+ Attributes:
230
+ project_dir: Absolute path to project directory
231
+ environment: Build environment name
232
+ verbose: Enable verbose output
233
+ caller_pid: Process ID of requesting client
234
+ caller_cwd: Working directory of requesting client
235
+ timestamp: Unix timestamp when request was created
236
+ request_id: Unique identifier for this request
237
+ """
238
+
239
+ project_dir: str
240
+ environment: str
241
+ verbose: bool
242
+ caller_pid: int
243
+ caller_cwd: str
244
+ timestamp: float = field(default_factory=time.time)
245
+ request_id: str = field(default_factory=lambda: f"install_deps_{int(time.time() * 1000)}")
246
+
247
+ def to_dict(self) -> dict[str, Any]:
248
+ """Convert to dictionary for JSON serialization."""
249
+ return asdict(self)
250
+
251
+ @classmethod
252
+ def from_dict(cls, data: dict[str, Any]) -> "InstallDependenciesRequest":
253
+ """Create InstallDependenciesRequest from dictionary."""
254
+ return cls(
255
+ project_dir=data["project_dir"],
256
+ environment=data["environment"],
257
+ verbose=data.get("verbose", False),
258
+ caller_pid=data["caller_pid"],
259
+ caller_cwd=data["caller_cwd"],
260
+ timestamp=data.get("timestamp", time.time()),
261
+ request_id=data.get("request_id", f"install_deps_{int(time.time() * 1000)}"),
262
+ )
263
+
264
+
265
+ @dataclass
266
+ class DaemonStatus:
267
+ """Daemon → Client: Status update message.
268
+
269
+ Attributes:
270
+ state: Current daemon state
271
+ message: Human-readable status message
272
+ updated_at: Unix timestamp of last status update
273
+ operation_in_progress: Whether an operation is actively running
274
+ daemon_pid: Process ID of the daemon
275
+ daemon_started_at: Unix timestamp when daemon started
276
+ caller_pid: Process ID of client whose request is being processed
277
+ caller_cwd: Working directory of client whose request is being processed
278
+ request_id: ID of the request currently being processed
279
+ request_started_at: Unix timestamp when current request started
280
+ environment: Environment being processed
281
+ project_dir: Project directory for current operation
282
+ current_operation: Detailed description of current operation
283
+ operation_type: Type of operation (deploy/monitor)
284
+ output_lines: Recent output lines from the operation
285
+ exit_code: Process exit code (None if still running)
286
+ port: Serial port being used
287
+ ports: Dictionary of active ports with their state information
288
+ locks: Dictionary of lock state information (port_locks, project_locks)
289
+ """
290
+
291
+ state: DaemonState
292
+ message: str
293
+ updated_at: float
294
+ operation_in_progress: bool = False
295
+ daemon_pid: int | None = None
296
+ daemon_started_at: float | None = None
297
+ caller_pid: int | None = None
298
+ caller_cwd: str | None = None
299
+ request_id: str | None = None
300
+ request_started_at: float | None = None
301
+ environment: str | None = None
302
+ project_dir: str | None = None
303
+ current_operation: str | None = None
304
+ operation_type: OperationType | None = None
305
+ output_lines: list[str] = field(default_factory=list)
306
+ exit_code: int | None = None
307
+ port: str | None = None
308
+ ports: dict[str, Any] = field(default_factory=dict)
309
+ locks: dict[str, Any] = field(default_factory=dict)
310
+
311
+ def to_dict(self) -> dict[str, Any]:
312
+ """Convert to dictionary for JSON serialization."""
313
+ result = asdict(self)
314
+ # Convert enums to string values
315
+ result["state"] = self.state.value
316
+ if self.operation_type:
317
+ result["operation_type"] = self.operation_type.value
318
+ else:
319
+ result["operation_type"] = None
320
+ return result
321
+
322
+ @classmethod
323
+ def from_dict(cls, data: dict[str, Any]) -> "DaemonStatus":
324
+ """Create DaemonStatus from dictionary."""
325
+ # Convert state string to enum
326
+ state_str = data.get("state", "unknown")
327
+ state = DaemonState.from_string(state_str)
328
+
329
+ # Convert operation_type string to enum
330
+ operation_type = None
331
+ if data.get("operation_type"):
332
+ operation_type = OperationType.from_string(data["operation_type"])
333
+
334
+ return cls(
335
+ state=state,
336
+ message=data.get("message", ""),
337
+ updated_at=data.get("updated_at", time.time()),
338
+ operation_in_progress=data.get("operation_in_progress", False),
339
+ daemon_pid=data.get("daemon_pid"),
340
+ daemon_started_at=data.get("daemon_started_at"),
341
+ caller_pid=data.get("caller_pid"),
342
+ caller_cwd=data.get("caller_cwd"),
343
+ request_id=data.get("request_id"),
344
+ request_started_at=data.get("request_started_at"),
345
+ environment=data.get("environment"),
346
+ project_dir=data.get("project_dir"),
347
+ current_operation=data.get("current_operation"),
348
+ operation_type=operation_type,
349
+ output_lines=data.get("output_lines", []),
350
+ exit_code=data.get("exit_code"),
351
+ port=data.get("port"),
352
+ ports=data.get("ports", {}),
353
+ locks=data.get("locks", {}),
354
+ )
355
+
356
+ def is_stale(self, timeout_seconds: float = 30.0) -> bool:
357
+ """Check if status hasn't been updated recently."""
358
+ return (time.time() - self.updated_at) > timeout_seconds
359
+
360
+ def get_age_seconds(self) -> float:
361
+ """Get age of this status update in seconds."""
362
+ return time.time() - self.updated_at
363
+
364
+
365
+ # =============================================================================
366
+ # Lock Management Messages (Iteration 2)
367
+ # =============================================================================
368
+
369
+
370
+ class LockType(Enum):
371
+ """Type of lock to acquire."""
372
+
373
+ EXCLUSIVE = "exclusive"
374
+ SHARED_READ = "shared_read"
375
+
376
+
377
+ @dataclass
378
+ class LockAcquireRequest:
379
+ """Client → Daemon: Request to acquire a configuration lock.
380
+
381
+ Attributes:
382
+ client_id: Unique identifier for the requesting client
383
+ project_dir: Absolute path to project directory
384
+ environment: Build environment name
385
+ port: Serial port for the configuration
386
+ lock_type: Type of lock to acquire (exclusive or shared_read)
387
+ description: Human-readable description of the operation
388
+ timeout: Maximum time in seconds to wait for the lock
389
+ timestamp: Unix timestamp when request was created
390
+ """
391
+
392
+ client_id: str
393
+ project_dir: str
394
+ environment: str
395
+ port: str
396
+ lock_type: LockType
397
+ description: str = ""
398
+ timeout: float = 300.0 # 5 minutes default
399
+ timestamp: float = field(default_factory=time.time)
400
+
401
+ def to_dict(self) -> dict[str, Any]:
402
+ """Convert to dictionary for JSON serialization."""
403
+ result = asdict(self)
404
+ result["lock_type"] = self.lock_type.value
405
+ return result
406
+
407
+ @classmethod
408
+ def from_dict(cls, data: dict[str, Any]) -> "LockAcquireRequest":
409
+ """Create LockAcquireRequest from dictionary."""
410
+ return cls(
411
+ client_id=data["client_id"],
412
+ project_dir=data["project_dir"],
413
+ environment=data["environment"],
414
+ port=data["port"],
415
+ lock_type=LockType(data["lock_type"]),
416
+ description=data.get("description", ""),
417
+ timeout=data.get("timeout", 300.0),
418
+ timestamp=data.get("timestamp", time.time()),
419
+ )
420
+
421
+
422
+ @dataclass
423
+ class LockReleaseRequest:
424
+ """Client → Daemon: Request to release a configuration lock.
425
+
426
+ Attributes:
427
+ client_id: Unique identifier for the client releasing the lock
428
+ project_dir: Absolute path to project directory
429
+ environment: Build environment name
430
+ port: Serial port for the configuration
431
+ timestamp: Unix timestamp when request was created
432
+ """
433
+
434
+ client_id: str
435
+ project_dir: str
436
+ environment: str
437
+ port: str
438
+ timestamp: float = field(default_factory=time.time)
439
+
440
+ def to_dict(self) -> dict[str, Any]:
441
+ """Convert to dictionary for JSON serialization."""
442
+ return asdict(self)
443
+
444
+ @classmethod
445
+ def from_dict(cls, data: dict[str, Any]) -> "LockReleaseRequest":
446
+ """Create LockReleaseRequest from dictionary."""
447
+ return cls(
448
+ client_id=data["client_id"],
449
+ project_dir=data["project_dir"],
450
+ environment=data["environment"],
451
+ port=data["port"],
452
+ timestamp=data.get("timestamp", time.time()),
453
+ )
454
+
455
+
456
+ @dataclass
457
+ class LockStatusRequest:
458
+ """Client → Daemon: Request status of a configuration lock.
459
+
460
+ Attributes:
461
+ project_dir: Absolute path to project directory
462
+ environment: Build environment name
463
+ port: Serial port for the configuration
464
+ timestamp: Unix timestamp when request was created
465
+ """
466
+
467
+ project_dir: str
468
+ environment: str
469
+ port: str
470
+ timestamp: float = field(default_factory=time.time)
471
+
472
+ def to_dict(self) -> dict[str, Any]:
473
+ """Convert to dictionary for JSON serialization."""
474
+ return asdict(self)
475
+
476
+ @classmethod
477
+ def from_dict(cls, data: dict[str, Any]) -> "LockStatusRequest":
478
+ """Create LockStatusRequest from dictionary."""
479
+ return cls(
480
+ project_dir=data["project_dir"],
481
+ environment=data["environment"],
482
+ port=data["port"],
483
+ timestamp=data.get("timestamp", time.time()),
484
+ )
485
+
486
+
487
+ @dataclass
488
+ class LockResponse:
489
+ """Daemon → Client: Response to a lock request.
490
+
491
+ Attributes:
492
+ success: Whether the operation succeeded
493
+ message: Human-readable status message
494
+ lock_state: Current state of the lock ("unlocked", "locked_exclusive", "locked_shared_read")
495
+ holder_count: Number of clients holding the lock
496
+ waiting_count: Number of clients waiting for the lock
497
+ timestamp: Unix timestamp of the response
498
+ """
499
+
500
+ success: bool
501
+ message: str
502
+ lock_state: str = "unlocked"
503
+ holder_count: int = 0
504
+ waiting_count: int = 0
505
+ timestamp: float = field(default_factory=time.time)
506
+
507
+ def to_dict(self) -> dict[str, Any]:
508
+ """Convert to dictionary for JSON serialization."""
509
+ return asdict(self)
510
+
511
+ @classmethod
512
+ def from_dict(cls, data: dict[str, Any]) -> "LockResponse":
513
+ """Create LockResponse from dictionary."""
514
+ return cls(
515
+ success=data["success"],
516
+ message=data["message"],
517
+ lock_state=data.get("lock_state", "unlocked"),
518
+ holder_count=data.get("holder_count", 0),
519
+ waiting_count=data.get("waiting_count", 0),
520
+ timestamp=data.get("timestamp", time.time()),
521
+ )
522
+
523
+
524
+ # =============================================================================
525
+ # Firmware Ledger Messages (Iteration 2)
526
+ # =============================================================================
527
+
528
+
529
+ @dataclass
530
+ class FirmwareQueryRequest:
531
+ """Client → Daemon: Query if firmware is current on a device.
532
+
533
+ Used to check if a redeploy is needed or if the device already has
534
+ the expected firmware loaded.
535
+
536
+ Attributes:
537
+ port: Serial port of the device
538
+ source_hash: Hash of the source files
539
+ build_flags_hash: Hash of the build flags (optional)
540
+ timestamp: Unix timestamp when request was created
541
+ """
542
+
543
+ port: str
544
+ source_hash: str
545
+ build_flags_hash: str | None = None
546
+ timestamp: float = field(default_factory=time.time)
547
+
548
+ def to_dict(self) -> dict[str, Any]:
549
+ """Convert to dictionary for JSON serialization."""
550
+ return asdict(self)
551
+
552
+ @classmethod
553
+ def from_dict(cls, data: dict[str, Any]) -> "FirmwareQueryRequest":
554
+ """Create FirmwareQueryRequest from dictionary."""
555
+ return cls(
556
+ port=data["port"],
557
+ source_hash=data["source_hash"],
558
+ build_flags_hash=data.get("build_flags_hash"),
559
+ timestamp=data.get("timestamp", time.time()),
560
+ )
561
+
562
+
563
+ @dataclass
564
+ class FirmwareQueryResponse:
565
+ """Daemon → Client: Response to firmware query.
566
+
567
+ Attributes:
568
+ is_current: True if firmware matches what's deployed (no redeploy needed)
569
+ needs_redeploy: True if source or build flags have changed
570
+ firmware_hash: Hash of the currently deployed firmware (if known)
571
+ project_dir: Project directory of the deployed firmware
572
+ environment: Environment of the deployed firmware
573
+ upload_timestamp: When the firmware was last uploaded
574
+ message: Human-readable message
575
+ timestamp: Unix timestamp of the response
576
+ """
577
+
578
+ is_current: bool
579
+ needs_redeploy: bool
580
+ firmware_hash: str | None = None
581
+ project_dir: str | None = None
582
+ environment: str | None = None
583
+ upload_timestamp: float | None = None
584
+ message: str = ""
585
+ timestamp: float = field(default_factory=time.time)
586
+
587
+ def to_dict(self) -> dict[str, Any]:
588
+ """Convert to dictionary for JSON serialization."""
589
+ return asdict(self)
590
+
591
+ @classmethod
592
+ def from_dict(cls, data: dict[str, Any]) -> "FirmwareQueryResponse":
593
+ """Create FirmwareQueryResponse from dictionary."""
594
+ return cls(
595
+ is_current=data["is_current"],
596
+ needs_redeploy=data["needs_redeploy"],
597
+ firmware_hash=data.get("firmware_hash"),
598
+ project_dir=data.get("project_dir"),
599
+ environment=data.get("environment"),
600
+ upload_timestamp=data.get("upload_timestamp"),
601
+ message=data.get("message", ""),
602
+ timestamp=data.get("timestamp", time.time()),
603
+ )
604
+
605
+
606
+ @dataclass
607
+ class FirmwareRecordRequest:
608
+ """Client → Daemon: Record a firmware deployment.
609
+
610
+ Sent after a successful upload to update the firmware ledger.
611
+
612
+ Attributes:
613
+ port: Serial port of the device
614
+ firmware_hash: Hash of the firmware file
615
+ source_hash: Hash of the source files
616
+ project_dir: Absolute path to project directory
617
+ environment: Build environment name
618
+ build_flags_hash: Hash of build flags (optional)
619
+ timestamp: Unix timestamp when request was created
620
+ """
621
+
622
+ port: str
623
+ firmware_hash: str
624
+ source_hash: str
625
+ project_dir: str
626
+ environment: str
627
+ build_flags_hash: str | None = None
628
+ timestamp: float = field(default_factory=time.time)
629
+
630
+ def to_dict(self) -> dict[str, Any]:
631
+ """Convert to dictionary for JSON serialization."""
632
+ return asdict(self)
633
+
634
+ @classmethod
635
+ def from_dict(cls, data: dict[str, Any]) -> "FirmwareRecordRequest":
636
+ """Create FirmwareRecordRequest from dictionary."""
637
+ return cls(
638
+ port=data["port"],
639
+ firmware_hash=data["firmware_hash"],
640
+ source_hash=data["source_hash"],
641
+ project_dir=data["project_dir"],
642
+ environment=data["environment"],
643
+ build_flags_hash=data.get("build_flags_hash"),
644
+ timestamp=data.get("timestamp", time.time()),
645
+ )
646
+
647
+
648
+ # =============================================================================
649
+ # Serial Session Messages (Iteration 2)
650
+ # =============================================================================
651
+
652
+
653
+ @dataclass
654
+ class SerialAttachRequest:
655
+ """Client → Daemon: Request to attach to a serial session.
656
+
657
+ Attributes:
658
+ client_id: Unique identifier for the client
659
+ port: Serial port to attach to
660
+ baud_rate: Baud rate for the connection
661
+ as_reader: Whether to attach as a reader (True) or open the port (False)
662
+ timestamp: Unix timestamp when request was created
663
+ """
664
+
665
+ client_id: str
666
+ port: str
667
+ baud_rate: int = 115200
668
+ as_reader: bool = True
669
+ timestamp: float = field(default_factory=time.time)
670
+
671
+ def to_dict(self) -> dict[str, Any]:
672
+ """Convert to dictionary for JSON serialization."""
673
+ return asdict(self)
674
+
675
+ @classmethod
676
+ def from_dict(cls, data: dict[str, Any]) -> "SerialAttachRequest":
677
+ """Create SerialAttachRequest from dictionary."""
678
+ return cls(
679
+ client_id=data["client_id"],
680
+ port=data["port"],
681
+ baud_rate=data.get("baud_rate", 115200),
682
+ as_reader=data.get("as_reader", True),
683
+ timestamp=data.get("timestamp", time.time()),
684
+ )
685
+
686
+
687
+ @dataclass
688
+ class SerialDetachRequest:
689
+ """Client → Daemon: Request to detach from a serial session.
690
+
691
+ Attributes:
692
+ client_id: Unique identifier for the client
693
+ port: Serial port to detach from
694
+ close_port: Whether to close the port if this is the last reader
695
+ timestamp: Unix timestamp when request was created
696
+ """
697
+
698
+ client_id: str
699
+ port: str
700
+ close_port: bool = False
701
+ timestamp: float = field(default_factory=time.time)
702
+
703
+ def to_dict(self) -> dict[str, Any]:
704
+ """Convert to dictionary for JSON serialization."""
705
+ return asdict(self)
706
+
707
+ @classmethod
708
+ def from_dict(cls, data: dict[str, Any]) -> "SerialDetachRequest":
709
+ """Create SerialDetachRequest from dictionary."""
710
+ return cls(
711
+ client_id=data["client_id"],
712
+ port=data["port"],
713
+ close_port=data.get("close_port", False),
714
+ timestamp=data.get("timestamp", time.time()),
715
+ )
716
+
717
+
718
+ @dataclass
719
+ class SerialWriteRequest:
720
+ """Client → Daemon: Request to write data to a serial port.
721
+
722
+ The client must have acquired writer access first.
723
+
724
+ Attributes:
725
+ client_id: Unique identifier for the client
726
+ port: Serial port to write to
727
+ data: Base64-encoded data to write
728
+ acquire_writer: Whether to automatically acquire writer access if not held
729
+ timestamp: Unix timestamp when request was created
730
+ """
731
+
732
+ client_id: str
733
+ port: str
734
+ data: str # Base64-encoded bytes
735
+ acquire_writer: bool = True
736
+ timestamp: float = field(default_factory=time.time)
737
+
738
+ def to_dict(self) -> dict[str, Any]:
739
+ """Convert to dictionary for JSON serialization."""
740
+ return asdict(self)
741
+
742
+ @classmethod
743
+ def from_dict(cls, data: dict[str, Any]) -> "SerialWriteRequest":
744
+ """Create SerialWriteRequest from dictionary."""
745
+ return cls(
746
+ client_id=data["client_id"],
747
+ port=data["port"],
748
+ data=data["data"],
749
+ acquire_writer=data.get("acquire_writer", True),
750
+ timestamp=data.get("timestamp", time.time()),
751
+ )
752
+
753
+
754
+ @dataclass
755
+ class SerialBufferRequest:
756
+ """Client → Daemon: Request to read buffered serial output.
757
+
758
+ Attributes:
759
+ client_id: Unique identifier for the client
760
+ port: Serial port to read from
761
+ max_lines: Maximum number of lines to return
762
+ timestamp: Unix timestamp when request was created
763
+ """
764
+
765
+ client_id: str
766
+ port: str
767
+ max_lines: int = 100
768
+ timestamp: float = field(default_factory=time.time)
769
+
770
+ def to_dict(self) -> dict[str, Any]:
771
+ """Convert to dictionary for JSON serialization."""
772
+ return asdict(self)
773
+
774
+ @classmethod
775
+ def from_dict(cls, data: dict[str, Any]) -> "SerialBufferRequest":
776
+ """Create SerialBufferRequest from dictionary."""
777
+ return cls(
778
+ client_id=data["client_id"],
779
+ port=data["port"],
780
+ max_lines=data.get("max_lines", 100),
781
+ timestamp=data.get("timestamp", time.time()),
782
+ )
783
+
784
+
785
+ @dataclass
786
+ class SerialSessionResponse:
787
+ """Daemon → Client: Response to serial session operations.
788
+
789
+ Attributes:
790
+ success: Whether the operation succeeded
791
+ message: Human-readable status message
792
+ is_open: Whether the port is currently open
793
+ reader_count: Number of clients attached as readers
794
+ has_writer: Whether a client has write access
795
+ buffer_size: Number of lines in the output buffer
796
+ lines: Output lines (for buffer requests)
797
+ bytes_written: Number of bytes written (for write requests)
798
+ timestamp: Unix timestamp of the response
799
+ """
800
+
801
+ success: bool
802
+ message: str
803
+ is_open: bool = False
804
+ reader_count: int = 0
805
+ has_writer: bool = False
806
+ buffer_size: int = 0
807
+ lines: list[str] = field(default_factory=list)
808
+ bytes_written: int = 0
809
+ timestamp: float = field(default_factory=time.time)
810
+
811
+ def to_dict(self) -> dict[str, Any]:
812
+ """Convert to dictionary for JSON serialization."""
813
+ return asdict(self)
814
+
815
+ @classmethod
816
+ def from_dict(cls, data: dict[str, Any]) -> "SerialSessionResponse":
817
+ """Create SerialSessionResponse from dictionary."""
818
+ return cls(
819
+ success=data["success"],
820
+ message=data["message"],
821
+ is_open=data.get("is_open", False),
822
+ reader_count=data.get("reader_count", 0),
823
+ has_writer=data.get("has_writer", False),
824
+ buffer_size=data.get("buffer_size", 0),
825
+ lines=data.get("lines", []),
826
+ bytes_written=data.get("bytes_written", 0),
827
+ timestamp=data.get("timestamp", time.time()),
828
+ )
829
+
830
+
831
+ # =============================================================================
832
+ # Client Connection Messages (Iteration 2)
833
+ # =============================================================================
834
+
835
+
836
+ @dataclass
837
+ class ClientConnectRequest:
838
+ """Client → Daemon: Register a new client connection.
839
+
840
+ Attributes:
841
+ client_id: Unique identifier for the client (UUID)
842
+ pid: Process ID of the client
843
+ hostname: Hostname of the client machine
844
+ version: Version of the client software
845
+ timestamp: Unix timestamp when request was created
846
+ """
847
+
848
+ client_id: str
849
+ pid: int
850
+ hostname: str = ""
851
+ version: str = ""
852
+ timestamp: float = field(default_factory=time.time)
853
+
854
+ def to_dict(self) -> dict[str, Any]:
855
+ """Convert to dictionary for JSON serialization."""
856
+ return asdict(self)
857
+
858
+ @classmethod
859
+ def from_dict(cls, data: dict[str, Any]) -> "ClientConnectRequest":
860
+ """Create ClientConnectRequest from dictionary."""
861
+ return cls(
862
+ client_id=data["client_id"],
863
+ pid=data["pid"],
864
+ hostname=data.get("hostname", ""),
865
+ version=data.get("version", ""),
866
+ timestamp=data.get("timestamp", time.time()),
867
+ )
868
+
869
+
870
+ @dataclass
871
+ class ClientHeartbeatRequest:
872
+ """Client → Daemon: Periodic heartbeat to indicate client is alive.
873
+
874
+ Attributes:
875
+ client_id: Unique identifier for the client
876
+ timestamp: Unix timestamp when heartbeat was sent
877
+ """
878
+
879
+ client_id: str
880
+ timestamp: float = field(default_factory=time.time)
881
+
882
+ def to_dict(self) -> dict[str, Any]:
883
+ """Convert to dictionary for JSON serialization."""
884
+ return asdict(self)
885
+
886
+ @classmethod
887
+ def from_dict(cls, data: dict[str, Any]) -> "ClientHeartbeatRequest":
888
+ """Create ClientHeartbeatRequest from dictionary."""
889
+ return cls(
890
+ client_id=data["client_id"],
891
+ timestamp=data.get("timestamp", time.time()),
892
+ )
893
+
894
+
895
+ @dataclass
896
+ class ClientDisconnectRequest:
897
+ """Client → Daemon: Graceful disconnect notification.
898
+
899
+ Attributes:
900
+ client_id: Unique identifier for the client
901
+ reason: Optional reason for disconnection
902
+ timestamp: Unix timestamp when disconnect was initiated
903
+ """
904
+
905
+ client_id: str
906
+ reason: str = ""
907
+ timestamp: float = field(default_factory=time.time)
908
+
909
+ def to_dict(self) -> dict[str, Any]:
910
+ """Convert to dictionary for JSON serialization."""
911
+ return asdict(self)
912
+
913
+ @classmethod
914
+ def from_dict(cls, data: dict[str, Any]) -> "ClientDisconnectRequest":
915
+ """Create ClientDisconnectRequest from dictionary."""
916
+ return cls(
917
+ client_id=data["client_id"],
918
+ reason=data.get("reason", ""),
919
+ timestamp=data.get("timestamp", time.time()),
920
+ )
921
+
922
+
923
+ @dataclass
924
+ class ClientResponse:
925
+ """Daemon → Client: Response to client connection operations.
926
+
927
+ Attributes:
928
+ success: Whether the operation succeeded
929
+ message: Human-readable status message
930
+ client_id: Client ID (may be assigned by daemon)
931
+ is_registered: Whether the client is currently registered
932
+ total_clients: Total number of connected clients
933
+ timestamp: Unix timestamp of the response
934
+ """
935
+
936
+ success: bool
937
+ message: str
938
+ client_id: str = ""
939
+ is_registered: bool = False
940
+ total_clients: int = 0
941
+ timestamp: float = field(default_factory=time.time)
942
+
943
+ def to_dict(self) -> dict[str, Any]:
944
+ """Convert to dictionary for JSON serialization."""
945
+ return asdict(self)
946
+
947
+ @classmethod
948
+ def from_dict(cls, data: dict[str, Any]) -> "ClientResponse":
949
+ """Create ClientResponse from dictionary."""
950
+ return cls(
951
+ success=data["success"],
952
+ message=data["message"],
953
+ client_id=data.get("client_id", ""),
954
+ is_registered=data.get("is_registered", False),
955
+ total_clients=data.get("total_clients", 0),
956
+ timestamp=data.get("timestamp", time.time()),
957
+ )