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,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
|
+
)
|