firmwareloop 0.0.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.
- firmwareloop-0.0.8.dist-info/METADATA +268 -0
- firmwareloop-0.0.8.dist-info/RECORD +26 -0
- firmwareloop-0.0.8.dist-info/WHEEL +5 -0
- firmwareloop-0.0.8.dist-info/entry_points.txt +5 -0
- firmwareloop-0.0.8.dist-info/licenses/LICENSE +21 -0
- firmwareloop-0.0.8.dist-info/top_level.txt +1 -0
- tools/__init__.py +5 -0
- tools/acceptance-scenario.ps1 +455 -0
- tools/build.ps1 +208 -0
- tools/can.ps1 +130 -0
- tools/check-qoder-mcp.ps1 +111 -0
- tools/common/build-backends.psm1 +294 -0
- tools/common/fw.psm1 +311 -0
- tools/common/uart_probe.py +100 -0
- tools/doctor.ps1 +148 -0
- tools/flash.ps1 +181 -0
- tools/fw_mcp_server.py +1272 -0
- tools/instrument_cli.py +417 -0
- tools/lib/__init__.py +3 -0
- tools/lib/instruments.py +181 -0
- tools/logic_capture.ps1 +261 -0
- tools/logic_decode.ps1 +307 -0
- tools/reset.ps1 +141 -0
- tools/setup-agent-mcp.ps1 +147 -0
- tools/test-backends.ps1 +100 -0
- tools/test.ps1 +194 -0
tools/fw_mcp_server.py
ADDED
|
@@ -0,0 +1,1272 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
fw_mcp_server.py - FirmwareLoop High-Level Workflow & Device MCP Server (v0.0.8).
|
|
4
|
+
|
|
5
|
+
Exposes high-level firmware engineering and hardware management tools to AI Coding
|
|
6
|
+
Agents (Antigravity CLI, Claude Code CLI, Qoder IDE, Cursor) via the Model Context
|
|
7
|
+
Protocol (JSON-RPC 2.0 stdio).
|
|
8
|
+
|
|
9
|
+
Tools provided:
|
|
10
|
+
- fw_doctor: Environment & toolchain diagnostic (doctor.ps1)
|
|
11
|
+
- fw_build: Multi-backend firmware compilation & diagnostics (build.ps1)
|
|
12
|
+
- fw_flash: Flash firmware image to target MCU (flash.ps1 / agentic-hil)
|
|
13
|
+
- fw_reset: Reset physical or simulated target MCU (reset.ps1)
|
|
14
|
+
- fw_run_hil_test: Hardware-in-the-loop pytest suite (test.ps1)
|
|
15
|
+
- fw_acceptance_scenario: End-to-end acceptance scenario (acceptance-scenario.ps1)
|
|
16
|
+
- fw_measure: Controlled PyVISA/SCPI instrument measurement (instrument_cli.py)
|
|
17
|
+
- fw_logic_capture: Logic analyzer protocol capture (logic_capture.ps1)
|
|
18
|
+
- fw_logic_decode: Logic analyzer protocol decoding & assertion (logic_decode.ps1)
|
|
19
|
+
- fw_get_evidence: Read structured audit run evidence (artifacts/runs/)
|
|
20
|
+
- fw_configure_lab: Interactively configure project, build backend, chip & ports (lab.yaml)
|
|
21
|
+
- fw_scan_hardware: Auto-detect connected ST-LINK / J-Link probes, MCU targets & COM ports
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import json
|
|
27
|
+
import os
|
|
28
|
+
import shutil
|
|
29
|
+
import subprocess
|
|
30
|
+
import sys
|
|
31
|
+
import traceback
|
|
32
|
+
from typing import Any, Dict, List, Optional
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
import yaml
|
|
36
|
+
except ImportError:
|
|
37
|
+
yaml = None
|
|
38
|
+
|
|
39
|
+
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
40
|
+
PYTHON_EXE = sys.executable
|
|
41
|
+
AHIL_EXE = os.path.join(REPO_ROOT, ".venv", "Scripts", "agentic-hil.exe")
|
|
42
|
+
PYOCD_EXE = os.path.join(REPO_ROOT, ".venv", "Scripts", "pyocd.exe")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def find_powershell() -> str:
|
|
46
|
+
for candidate in ["pwsh", "powershell"]:
|
|
47
|
+
path = shutil.which(candidate)
|
|
48
|
+
if path:
|
|
49
|
+
return path
|
|
50
|
+
return "powershell.exe"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
PWSH_EXE = find_powershell()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def run_process(cmd: List[str], timeout: int = 300) -> Dict[str, Any]:
|
|
57
|
+
try:
|
|
58
|
+
proc = subprocess.run(
|
|
59
|
+
cmd,
|
|
60
|
+
cwd=REPO_ROOT,
|
|
61
|
+
stdout=subprocess.PIPE,
|
|
62
|
+
stderr=subprocess.PIPE,
|
|
63
|
+
text=True,
|
|
64
|
+
encoding="utf-8",
|
|
65
|
+
errors="replace",
|
|
66
|
+
timeout=timeout,
|
|
67
|
+
)
|
|
68
|
+
stdout_raw = proc.stdout.strip()
|
|
69
|
+
stderr_raw = proc.stderr.strip()
|
|
70
|
+
|
|
71
|
+
# Try to parse JSON from stdout
|
|
72
|
+
parsed = None
|
|
73
|
+
for line in reversed(stdout_raw.splitlines()):
|
|
74
|
+
line_str = line.strip()
|
|
75
|
+
if line_str.startswith("{") and line_str.endswith("}"):
|
|
76
|
+
try:
|
|
77
|
+
parsed = json.loads(line_str)
|
|
78
|
+
break
|
|
79
|
+
except Exception:
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
if parsed is None and stdout_raw.startswith("{") and stdout_raw.endswith("}"):
|
|
83
|
+
try:
|
|
84
|
+
parsed = json.loads(stdout_raw)
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
"exit_code": proc.returncode,
|
|
90
|
+
"success": proc.returncode == 0,
|
|
91
|
+
"stdout": stdout_raw,
|
|
92
|
+
"stderr": stderr_raw,
|
|
93
|
+
"data": parsed,
|
|
94
|
+
}
|
|
95
|
+
except subprocess.TimeoutExpired:
|
|
96
|
+
return {
|
|
97
|
+
"exit_code": -1,
|
|
98
|
+
"success": False,
|
|
99
|
+
"error_class": "TIMEOUT",
|
|
100
|
+
"error": f"Command timed out after {timeout} seconds",
|
|
101
|
+
"stdout": "",
|
|
102
|
+
"stderr": "",
|
|
103
|
+
}
|
|
104
|
+
except Exception as e:
|
|
105
|
+
return {
|
|
106
|
+
"exit_code": -1,
|
|
107
|
+
"success": False,
|
|
108
|
+
"error_class": "EXECUTION_ERROR",
|
|
109
|
+
"error": str(e),
|
|
110
|
+
"traceback": traceback.format_exc(),
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# ===========================================================================
|
|
115
|
+
# Tool Implementations
|
|
116
|
+
# ===========================================================================
|
|
117
|
+
|
|
118
|
+
def handle_fw_doctor(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
119
|
+
script = os.path.join(REPO_ROOT, "tools", "doctor.ps1")
|
|
120
|
+
cmd = [PWSH_EXE, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", script, "-Json"]
|
|
121
|
+
res = run_process(cmd, timeout=60)
|
|
122
|
+
if res.get("data"):
|
|
123
|
+
return res["data"]
|
|
124
|
+
return res
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def handle_fw_build(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
128
|
+
script = os.path.join(REPO_ROOT, "tools", "build.ps1")
|
|
129
|
+
cmd = [PWSH_EXE, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", script, "-Json"]
|
|
130
|
+
|
|
131
|
+
backend = args.get("backend")
|
|
132
|
+
if backend and backend != "auto":
|
|
133
|
+
cmd.extend(["-Backend", backend])
|
|
134
|
+
|
|
135
|
+
config = args.get("configuration", "Debug")
|
|
136
|
+
if config:
|
|
137
|
+
cmd.extend(["-Configuration", config])
|
|
138
|
+
|
|
139
|
+
if args.get("clean", True):
|
|
140
|
+
cmd.append("-Clean")
|
|
141
|
+
|
|
142
|
+
source_dir = args.get("source_dir")
|
|
143
|
+
if source_dir:
|
|
144
|
+
cmd.extend(["-SourceDir", source_dir])
|
|
145
|
+
|
|
146
|
+
if args.get("dry_run", False):
|
|
147
|
+
cmd.append("-DryRun")
|
|
148
|
+
|
|
149
|
+
timeout_ms = args.get("timeout_ms", 300000)
|
|
150
|
+
res = run_process(cmd, timeout=int(timeout_ms / 1000) + 10)
|
|
151
|
+
if res.get("data"):
|
|
152
|
+
return res["data"]
|
|
153
|
+
return res
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def handle_fw_flash(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
157
|
+
script = os.path.join(REPO_ROOT, "tools", "flash.ps1")
|
|
158
|
+
backend = args.get("backend", "simulator")
|
|
159
|
+
artifact = args.get("artifact_path", "artifacts/build/firmware.elf")
|
|
160
|
+
|
|
161
|
+
cmd = [
|
|
162
|
+
PWSH_EXE, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass",
|
|
163
|
+
"-File", script, "-Backend", backend, "-Artifact", artifact, "-Json"
|
|
164
|
+
]
|
|
165
|
+
res = run_process(cmd, timeout=120)
|
|
166
|
+
if res.get("data"):
|
|
167
|
+
return res["data"]
|
|
168
|
+
return res
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def handle_fw_reset(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
172
|
+
script = os.path.join(REPO_ROOT, "tools", "reset.ps1")
|
|
173
|
+
backend = args.get("backend", "simulator")
|
|
174
|
+
expected = args.get("expected_target")
|
|
175
|
+
|
|
176
|
+
cmd = [
|
|
177
|
+
PWSH_EXE, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass",
|
|
178
|
+
"-File", script, "-Backend", backend, "-Json"
|
|
179
|
+
]
|
|
180
|
+
if expected:
|
|
181
|
+
cmd.extend(["-ExpectedTarget", expected])
|
|
182
|
+
|
|
183
|
+
res = run_process(cmd, timeout=30)
|
|
184
|
+
if res.get("data"):
|
|
185
|
+
return res["data"]
|
|
186
|
+
return res
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def handle_fw_run_hil_test(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
190
|
+
script = os.path.join(REPO_ROOT, "tools", "test.ps1")
|
|
191
|
+
cmd = [PWSH_EXE, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", script, "-Json"]
|
|
192
|
+
|
|
193
|
+
gate = args.get("gate", "simulator")
|
|
194
|
+
if gate:
|
|
195
|
+
cmd.extend(["-Gate", gate])
|
|
196
|
+
|
|
197
|
+
test_filter = args.get("test_filter")
|
|
198
|
+
if test_filter:
|
|
199
|
+
cmd.extend(["-Filter", test_filter])
|
|
200
|
+
|
|
201
|
+
res = run_process(cmd, timeout=120)
|
|
202
|
+
if res.get("data"):
|
|
203
|
+
return res["data"]
|
|
204
|
+
return res
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def handle_fw_acceptance_scenario(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
208
|
+
script = os.path.join(REPO_ROOT, "tools", "acceptance-scenario.ps1")
|
|
209
|
+
cmd = [PWSH_EXE, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", script, "-Json"]
|
|
210
|
+
|
|
211
|
+
mode = args.get("mode", "simulator")
|
|
212
|
+
if mode:
|
|
213
|
+
cmd.extend(["-Mode", mode])
|
|
214
|
+
|
|
215
|
+
hardware = args.get("hardware")
|
|
216
|
+
if hardware:
|
|
217
|
+
cmd.extend(["-Hardware", hardware])
|
|
218
|
+
|
|
219
|
+
res = run_process(cmd, timeout=180)
|
|
220
|
+
if res.get("data"):
|
|
221
|
+
return res["data"]
|
|
222
|
+
return res
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def handle_fw_measure(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
226
|
+
cli_script = os.path.join(REPO_ROOT, "tools", "instrument_cli.py")
|
|
227
|
+
instr_type = args.get("instrument_type", "scope")
|
|
228
|
+
subcommand = args.get("command", "measure-frequency")
|
|
229
|
+
instr_name = args.get("instrument_name", "scope1" if instr_type == "scope" else "psu1")
|
|
230
|
+
channel = args.get("channel", "CH1")
|
|
231
|
+
backend = args.get("backend", "simulator")
|
|
232
|
+
|
|
233
|
+
cmd = [PYTHON_EXE, cli_script, instr_type, subcommand, "--instrument", instr_name, "--backend", backend]
|
|
234
|
+
if instr_type == "scope" and channel:
|
|
235
|
+
cmd.extend(["--channel", channel])
|
|
236
|
+
|
|
237
|
+
res = run_process(cmd, timeout=30)
|
|
238
|
+
if res.get("data"):
|
|
239
|
+
return res["data"]
|
|
240
|
+
return res
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def handle_fw_logic_capture(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
244
|
+
script = os.path.join(REPO_ROOT, "tools", "logic_capture.ps1")
|
|
245
|
+
cmd = [PWSH_EXE, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", script, "-Json"]
|
|
246
|
+
|
|
247
|
+
protocol = args.get("protocol", "spi")
|
|
248
|
+
cmd.extend(["-Protocol", protocol])
|
|
249
|
+
|
|
250
|
+
sample_rate = args.get("sample_rate")
|
|
251
|
+
if sample_rate:
|
|
252
|
+
cmd.extend(["-SampleRate", str(sample_rate)])
|
|
253
|
+
|
|
254
|
+
duration_ms = args.get("duration_ms")
|
|
255
|
+
if duration_ms:
|
|
256
|
+
cmd.extend(["-DurationMs", str(duration_ms)])
|
|
257
|
+
|
|
258
|
+
output_file = args.get("output_file")
|
|
259
|
+
if output_file:
|
|
260
|
+
cmd.extend(["-OutputFile", output_file])
|
|
261
|
+
|
|
262
|
+
res = run_process(cmd, timeout=30)
|
|
263
|
+
if res.get("data"):
|
|
264
|
+
return res["data"]
|
|
265
|
+
return res
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def handle_fw_logic_decode(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
269
|
+
script = os.path.join(REPO_ROOT, "tools", "logic_decode.ps1")
|
|
270
|
+
cmd = [PWSH_EXE, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", script, "-Json"]
|
|
271
|
+
|
|
272
|
+
capture_file = args.get("capture_file")
|
|
273
|
+
if capture_file:
|
|
274
|
+
cmd.extend(["-CaptureFile", capture_file])
|
|
275
|
+
|
|
276
|
+
protocol = args.get("protocol", "spi")
|
|
277
|
+
cmd.extend(["-Protocol", protocol])
|
|
278
|
+
|
|
279
|
+
frequency = args.get("frequency_hz")
|
|
280
|
+
if frequency:
|
|
281
|
+
cmd.extend(["-FrequencyHz", str(frequency)])
|
|
282
|
+
|
|
283
|
+
res = run_process(cmd, timeout=30)
|
|
284
|
+
if res.get("data"):
|
|
285
|
+
return res["data"]
|
|
286
|
+
return res
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def handle_fw_get_evidence(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
290
|
+
run_id = args.get("run_id", "latest")
|
|
291
|
+
runs_dir = os.path.join(REPO_ROOT, "artifacts", "runs")
|
|
292
|
+
|
|
293
|
+
if not os.path.exists(runs_dir):
|
|
294
|
+
return {"ok": False, "error_class": "ARTIFACT_NOT_FOUND", "message": "artifacts/runs directory does not exist"}
|
|
295
|
+
|
|
296
|
+
target_dir = None
|
|
297
|
+
if run_id == "latest":
|
|
298
|
+
entries = [os.path.join(runs_dir, d) for d in os.listdir(runs_dir) if os.path.isdir(os.path.join(runs_dir, d))]
|
|
299
|
+
if not entries:
|
|
300
|
+
return {"ok": False, "error_class": "ARTIFACT_NOT_FOUND", "message": "No runs found in artifacts/runs"}
|
|
301
|
+
target_dir = max(entries, key=os.path.getmtime)
|
|
302
|
+
else:
|
|
303
|
+
candidate = os.path.join(runs_dir, run_id)
|
|
304
|
+
if os.path.exists(candidate):
|
|
305
|
+
target_dir = candidate
|
|
306
|
+
else:
|
|
307
|
+
return {"ok": False, "error_class": "ARTIFACT_NOT_FOUND", "message": f"Run '{run_id}' not found"}
|
|
308
|
+
|
|
309
|
+
summary_file = os.path.join(target_dir, "summary.json")
|
|
310
|
+
summary_data = None
|
|
311
|
+
if os.path.exists(summary_file):
|
|
312
|
+
try:
|
|
313
|
+
with open(summary_file, "r", encoding="utf-8") as f:
|
|
314
|
+
summary_data = json.load(f)
|
|
315
|
+
except Exception:
|
|
316
|
+
pass
|
|
317
|
+
|
|
318
|
+
report_file = os.path.join(target_dir, "final-report.json")
|
|
319
|
+
report_data = None
|
|
320
|
+
if os.path.exists(report_file):
|
|
321
|
+
try:
|
|
322
|
+
with open(report_file, "r", encoding="utf-8") as f:
|
|
323
|
+
report_data = json.load(f)
|
|
324
|
+
except Exception:
|
|
325
|
+
pass
|
|
326
|
+
|
|
327
|
+
files = os.listdir(target_dir)
|
|
328
|
+
return {
|
|
329
|
+
"ok": True,
|
|
330
|
+
"run_id": os.path.basename(target_dir),
|
|
331
|
+
"directory": target_dir,
|
|
332
|
+
"files": files,
|
|
333
|
+
"summary": summary_data,
|
|
334
|
+
"final_report": report_data,
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def handle_fw_init_project(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
339
|
+
"""Scaffold multi-agent instruction files (AGENTS.md, CLAUDE.md, GEMINI.md), lab.yaml, and .mcp.json."""
|
|
340
|
+
target_dir = args.get("target_dir", ".")
|
|
341
|
+
target_dir = os.path.abspath(target_dir)
|
|
342
|
+
os.makedirs(target_dir, exist_ok=True)
|
|
343
|
+
target_chip = args.get("target_chip", "STM32F103C8")
|
|
344
|
+
build_backend = args.get("build_backend", "keil")
|
|
345
|
+
overwrite = args.get("overwrite", False)
|
|
346
|
+
|
|
347
|
+
created_files = []
|
|
348
|
+
|
|
349
|
+
# 1. AGENTS.md
|
|
350
|
+
agents_path = os.path.join(target_dir, "AGENTS.md")
|
|
351
|
+
agents_content = """# AGENTS.md — Firmware Engineering & Lab Automation Guidelines
|
|
352
|
+
|
|
353
|
+
This repository is configured with **FirmwareLoop (`fwloop`)** for AI Agent-driven firmware development, building, flashing, and hardware-in-the-loop (HIL) testing.
|
|
354
|
+
|
|
355
|
+
## Dual-Tier MCP Tools Available
|
|
356
|
+
- **Upper-Tier (`fwloop`)**:
|
|
357
|
+
- `fw_doctor()`: Check compiler, Python, COM ports, and probe connectivity.
|
|
358
|
+
- `fw_configure_lab(project_name, build_backend, target_chip, uart_port)`: Interactively configure project settings in `lab/lab.yaml`.
|
|
359
|
+
- `fw_scan_hardware(adopt=false)`: Scan connected ST-LINK / J-Link / CMSIS-DAP probes and COM ports.
|
|
360
|
+
- `fw_build(backend="auto", configuration="Debug", clean=true)`: Build firmware across Keil5, CMake, Make, IAR, PlatformIO, etc.
|
|
361
|
+
- `fw_flash(backend="auto", artifact_path)`: Flash compiled firmware image into target MCU via ST-LINK or J-Link.
|
|
362
|
+
- `fw_reset(backend="auto")`: Hardware/software reset target MCU.
|
|
363
|
+
- `fw_run_hil_test()`: Run pytest automated hardware-in-the-loop test suite.
|
|
364
|
+
- `fw_measure(instrument_type, command)`: Safe PyVISA instrument measurement.
|
|
365
|
+
- `fw_logic_capture(protocol="i2c|spi|uart")` / `fw_logic_decode()`: Logic analyzer protocol capture & decode.
|
|
366
|
+
- `fw_get_evidence()`: Retrieve audit run artifacts and test evidence.
|
|
367
|
+
- `fw_init_project()`: Scaffold multi-agent instruction files and lab configurations.
|
|
368
|
+
|
|
369
|
+
- **Lower-Tier (`agentic-hil`)**:
|
|
370
|
+
- `probe_target()`, `flash_firmware()`, `reset_target()`, `com_session_*()`, `debug_*()`.
|
|
371
|
+
|
|
372
|
+
## Core Agent Rules & Principles
|
|
373
|
+
1. **Real-Hardware-First & Zero Fake Results**: Never fake, simulate, or mock results during real firmware development. If toolchains (Keil5/GCC), debug probes (ST-LINK/J-Link), target MCU, or instruments are missing or disconnected, immediately fail closed with explicit error classes (`TOOLCHAIN_NOT_FOUND`, `PROBE_NOT_FOUND`, `TARGET_UNREACHABLE`) and actionable setup guidance for the user. Never claim success on incomplete conditions!
|
|
374
|
+
2. **Evidence-Driven**: Never judge success by `exit code == 0` alone. Inspect Build Evidence + Runtime Evidence + Measurement Evidence + Assertion.
|
|
375
|
+
3. **Safety First**: Never bypass `lab/limits.yaml` safety boundaries.
|
|
376
|
+
4. **Iteration Limit**: Maximum 3 automated fix attempts before requesting human guidance.
|
|
377
|
+
"""
|
|
378
|
+
if not os.path.exists(agents_path) or overwrite:
|
|
379
|
+
with open(agents_path, "w", encoding="utf-8") as fh:
|
|
380
|
+
fh.write(agents_content)
|
|
381
|
+
created_files.append("AGENTS.md")
|
|
382
|
+
|
|
383
|
+
# 2. CLAUDE.md
|
|
384
|
+
claude_path = os.path.join(target_dir, "CLAUDE.md")
|
|
385
|
+
claude_content = f"# CLAUDE.md — Claude Code Guidelines\n\n{agents_content}"
|
|
386
|
+
if not os.path.exists(claude_path) or overwrite:
|
|
387
|
+
with open(claude_path, "w", encoding="utf-8") as fh:
|
|
388
|
+
fh.write(claude_content)
|
|
389
|
+
created_files.append("CLAUDE.md")
|
|
390
|
+
|
|
391
|
+
# 3. GEMINI.md
|
|
392
|
+
gemini_path = os.path.join(target_dir, "GEMINI.md")
|
|
393
|
+
gemini_content = "# GEMINI.md — Antigravity Agent Guidelines\n\nSee `AGENTS.md` for full project guidelines and Dual-Tier MCP tools.\n"
|
|
394
|
+
if not os.path.exists(gemini_path) or overwrite:
|
|
395
|
+
with open(gemini_path, "w", encoding="utf-8") as fh:
|
|
396
|
+
fh.write(gemini_content)
|
|
397
|
+
created_files.append("GEMINI.md")
|
|
398
|
+
|
|
399
|
+
# 4. lab/lab.yaml
|
|
400
|
+
lab_dir = os.path.join(target_dir, "lab")
|
|
401
|
+
os.makedirs(lab_dir, exist_ok=True)
|
|
402
|
+
lab_yaml_path = os.path.join(lab_dir, "lab.yaml")
|
|
403
|
+
lab_yaml_content = f"""# FirmwareLoop Project & Hardware Bench Configuration
|
|
404
|
+
schema: "firmwareloop-lab-config/v1"
|
|
405
|
+
|
|
406
|
+
project:
|
|
407
|
+
name: "{os.path.basename(target_dir)}"
|
|
408
|
+
target_chip: "{target_chip}"
|
|
409
|
+
build_backend: "{build_backend}"
|
|
410
|
+
source_dir: "."
|
|
411
|
+
|
|
412
|
+
hardware:
|
|
413
|
+
debugger_probe: "stlink"
|
|
414
|
+
probe_serial: "auto"
|
|
415
|
+
uart:
|
|
416
|
+
port: "COM3"
|
|
417
|
+
baudrate: 115200
|
|
418
|
+
power_supply:
|
|
419
|
+
default_voltage: 3.3
|
|
420
|
+
"""
|
|
421
|
+
if not os.path.exists(lab_yaml_path) or overwrite:
|
|
422
|
+
with open(lab_yaml_path, "w", encoding="utf-8") as fh:
|
|
423
|
+
fh.write(lab_yaml_content)
|
|
424
|
+
created_files.append("lab/lab.yaml")
|
|
425
|
+
|
|
426
|
+
# 5. .mcp.json
|
|
427
|
+
mcp_json_path = os.path.join(target_dir, ".mcp.json")
|
|
428
|
+
mcp_json_content = """{
|
|
429
|
+
"mcpServers": {
|
|
430
|
+
"fwloop": {
|
|
431
|
+
"command": "fwloop"
|
|
432
|
+
},
|
|
433
|
+
"agentic-hil": {
|
|
434
|
+
"command": "agentic-hil",
|
|
435
|
+
"args": ["mcp-stdio"]
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
"""
|
|
440
|
+
if not os.path.exists(mcp_json_path) or overwrite:
|
|
441
|
+
with open(mcp_json_path, "w", encoding="utf-8") as fh:
|
|
442
|
+
fh.write(mcp_json_content)
|
|
443
|
+
created_files.append(".mcp.json")
|
|
444
|
+
|
|
445
|
+
# 6. skills/firmwareloop/SKILL.md
|
|
446
|
+
skill_dir = os.path.join(target_dir, "skills", "firmwareloop")
|
|
447
|
+
os.makedirs(skill_dir, exist_ok=True)
|
|
448
|
+
skill_path = os.path.join(skill_dir, "SKILL.md")
|
|
449
|
+
skill_content = """---
|
|
450
|
+
name: firmwareloop
|
|
451
|
+
description: FirmwareLoop workflow skill for orchestrating firmware builds (Keil/CMake/Make/etc.), probe detection (ST-LINK/J-Link), interactive lab configuration, pytest HIL testing, I2C/SPI logic analyzer captures, PyVISA instrument measurements, and end-to-end hardware acceptance.
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
# FirmwareLoop Skill
|
|
455
|
+
|
|
456
|
+
Use this skill when developing, building, testing, or diagnosing embedded firmware within the FirmwareLoop repository.
|
|
457
|
+
|
|
458
|
+
## Capabilities & Tools
|
|
459
|
+
|
|
460
|
+
### Upper-Tier MCP Tools (`firmwareloop`)
|
|
461
|
+
- `fw_doctor`: Run environmental health check.
|
|
462
|
+
- `fw_configure_lab`: Interactively configure or update project parameters, target chip (e.g. STM32F103C8), build backend (e.g. keil), source directory, COM port, and debugger probe in `lab/lab.yaml`.
|
|
463
|
+
- `fw_scan_hardware`: Scan and identify attached hardware debuggers (ST-LINK, J-Link, CMSIS-DAP) and COM ports.
|
|
464
|
+
- `fw_build`: Compile firmware across 7 backends (Keil, CMake, Make, IAR, PlatformIO, Zephyr, ESP-IDF).
|
|
465
|
+
- `fw_flash`: Flash compiled firmware image into target MCU.
|
|
466
|
+
- `fw_reset`: Hardware or software reset of the target MCU.
|
|
467
|
+
- `fw_run_hil_test`: Run 12-item pytest HIL automated testing.
|
|
468
|
+
- `fw_acceptance_scenario`: Execute end-to-end acceptance scenario.
|
|
469
|
+
- `fw_measure`: Query PyVISA/SCPI instrument readings (frequency, duty cycle, Vpp, voltage, current).
|
|
470
|
+
- `fw_logic_capture` / `fw_logic_decode`: Digital protocol capture and verification (I2C, SPI, UART).
|
|
471
|
+
- `fw_get_evidence`: Retrieve audit run artifacts and reports.
|
|
472
|
+
- `fw_init_project`: Scaffold multi-agent instruction files and lab configurations.
|
|
473
|
+
|
|
474
|
+
## Mandatory Rules
|
|
475
|
+
1. **Real-Hardware-First & Zero Fake Data**: Never fake or mock hardware results. When compilers, debuggers, or MCU targets are missing, fail closed immediately and output explicit error diagnostics and user setup guidance.
|
|
476
|
+
2. **Max 3 Code Iterations**: Never loop infinitely fixing code.
|
|
477
|
+
3. **Fail Closed on Safety**: Never bypass `limits.yaml`.
|
|
478
|
+
4. **Verify Evidence**: Ensure all evidence is captured in `artifacts/runs/<run_id>/`.
|
|
479
|
+
"""
|
|
480
|
+
if not os.path.exists(skill_path) or overwrite:
|
|
481
|
+
with open(skill_path, "w", encoding="utf-8") as fh:
|
|
482
|
+
fh.write(skill_content)
|
|
483
|
+
created_files.append("skills/firmwareloop/SKILL.md")
|
|
484
|
+
|
|
485
|
+
return {
|
|
486
|
+
"ok": True,
|
|
487
|
+
"target_dir": target_dir,
|
|
488
|
+
"created_files": created_files,
|
|
489
|
+
"message": f"Successfully initialized FirmwareLoop multi-agent files in {target_dir}"
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def handle_fw_configure_lab(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
494
|
+
lab_yaml_path = os.path.join(REPO_ROOT, "lab", "lab.yaml")
|
|
495
|
+
example_yaml_path = os.path.join(REPO_ROOT, "lab", "lab.example.yaml")
|
|
496
|
+
|
|
497
|
+
config: Dict[str, Any] = {}
|
|
498
|
+
if os.path.exists(lab_yaml_path):
|
|
499
|
+
try:
|
|
500
|
+
if yaml:
|
|
501
|
+
with open(lab_yaml_path, "r", encoding="utf-8") as f:
|
|
502
|
+
config = yaml.safe_load(f) or {}
|
|
503
|
+
else:
|
|
504
|
+
with open(lab_yaml_path, "r", encoding="utf-8") as f:
|
|
505
|
+
config = json.load(f) or {}
|
|
506
|
+
except Exception:
|
|
507
|
+
config = {}
|
|
508
|
+
elif os.path.exists(example_yaml_path) and yaml:
|
|
509
|
+
try:
|
|
510
|
+
with open(example_yaml_path, "r", encoding="utf-8") as f:
|
|
511
|
+
config = yaml.safe_load(f) or {}
|
|
512
|
+
except Exception:
|
|
513
|
+
config = {}
|
|
514
|
+
|
|
515
|
+
if "project" not in config:
|
|
516
|
+
config["project"] = {}
|
|
517
|
+
if "dut" not in config:
|
|
518
|
+
config["dut"] = {"uart": {}}
|
|
519
|
+
if "hardware" not in config:
|
|
520
|
+
config["hardware"] = {}
|
|
521
|
+
|
|
522
|
+
updated = []
|
|
523
|
+
|
|
524
|
+
if "project_name" in args and args["project_name"] is not None:
|
|
525
|
+
config["project"]["name"] = args["project_name"]
|
|
526
|
+
updated.append("project.name")
|
|
527
|
+
|
|
528
|
+
if "build_backend" in args and args["build_backend"] is not None:
|
|
529
|
+
config["project"]["build_backend"] = args["build_backend"]
|
|
530
|
+
updated.append("project.build_backend")
|
|
531
|
+
|
|
532
|
+
if "source_dir" in args and args["source_dir"] is not None:
|
|
533
|
+
config["project"]["source_dir"] = args["source_dir"]
|
|
534
|
+
updated.append("project.source_dir")
|
|
535
|
+
|
|
536
|
+
if "target_chip" in args and args["target_chip"] is not None:
|
|
537
|
+
config["project"]["target_chip"] = args["target_chip"]
|
|
538
|
+
config["dut"]["expected_target"] = args["target_chip"]
|
|
539
|
+
updated.append("project.target_chip")
|
|
540
|
+
|
|
541
|
+
if "uart_port" in args and args["uart_port"] is not None:
|
|
542
|
+
if "uart" not in config["dut"]:
|
|
543
|
+
config["dut"]["uart"] = {}
|
|
544
|
+
config["dut"]["uart"]["port"] = args["uart_port"]
|
|
545
|
+
updated.append("dut.uart.port")
|
|
546
|
+
|
|
547
|
+
if "uart_baudrate" in args and args["uart_baudrate"] is not None:
|
|
548
|
+
if "uart" not in config["dut"]:
|
|
549
|
+
config["dut"]["uart"] = {}
|
|
550
|
+
config["dut"]["uart"]["baudrate"] = int(args["uart_baudrate"])
|
|
551
|
+
updated.append("dut.uart.baudrate")
|
|
552
|
+
|
|
553
|
+
if "debugger_backend" in args and args["debugger_backend"] is not None:
|
|
554
|
+
config["hardware"]["debugger_backend"] = args["debugger_backend"]
|
|
555
|
+
updated.append("hardware.debugger_backend")
|
|
556
|
+
|
|
557
|
+
if "debugger_probe_id" in args and args["debugger_probe_id"] is not None:
|
|
558
|
+
config["hardware"]["probe_id"] = args["debugger_probe_id"]
|
|
559
|
+
updated.append("hardware.probe_id")
|
|
560
|
+
|
|
561
|
+
os.makedirs(os.path.dirname(lab_yaml_path), exist_ok=True)
|
|
562
|
+
if yaml:
|
|
563
|
+
with open(lab_yaml_path, "w", encoding="utf-8") as f:
|
|
564
|
+
yaml.dump(config, f, allow_unicode=True, default_flow_style=False)
|
|
565
|
+
else:
|
|
566
|
+
with open(lab_yaml_path, "w", encoding="utf-8") as f:
|
|
567
|
+
json.dump(config, f, ensure_ascii=False, indent=2)
|
|
568
|
+
|
|
569
|
+
return {
|
|
570
|
+
"ok": True,
|
|
571
|
+
"message": f"Successfully updated lab/lab.yaml ({len(updated)} fields modified)",
|
|
572
|
+
"updated_fields": updated,
|
|
573
|
+
"config_file": lab_yaml_path,
|
|
574
|
+
"config": config,
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def handle_fw_scan_hardware(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
579
|
+
probes = []
|
|
580
|
+
com_ports = []
|
|
581
|
+
messages = []
|
|
582
|
+
|
|
583
|
+
# 1. Probe detection via pyocd
|
|
584
|
+
if os.path.exists(PYOCD_EXE):
|
|
585
|
+
res = run_process([PYOCD_EXE, "list"], timeout=15)
|
|
586
|
+
raw_output = res.get("stdout", "")
|
|
587
|
+
if "No available debug probes" not in raw_output:
|
|
588
|
+
for line in raw_output.splitlines():
|
|
589
|
+
if line.strip() and not line.startswith("#") and not line.startswith("usage"):
|
|
590
|
+
probes.append({"raw": line.strip(), "source": "pyocd"})
|
|
591
|
+
|
|
592
|
+
# 2. Probe & COM detection via agentic-hil
|
|
593
|
+
if os.path.exists(AHIL_EXE):
|
|
594
|
+
# COM Ports
|
|
595
|
+
com_res = run_process([AHIL_EXE, "com-ports"], timeout=10)
|
|
596
|
+
if com_res.get("data") and "ports" in com_res["data"]:
|
|
597
|
+
for p in com_res["data"]["ports"]:
|
|
598
|
+
com_ports.append(p)
|
|
599
|
+
elif com_res.get("stdout"):
|
|
600
|
+
for line in com_res["stdout"].splitlines():
|
|
601
|
+
if line.strip():
|
|
602
|
+
com_ports.append({"raw": line.strip()})
|
|
603
|
+
|
|
604
|
+
# Debugger Probes
|
|
605
|
+
probe_res = run_process([AHIL_EXE, "debugger-probes"], timeout=15)
|
|
606
|
+
if probe_res.get("stdout") and "No" not in probe_res["stdout"]:
|
|
607
|
+
messages.append(probe_res["stdout"])
|
|
608
|
+
|
|
609
|
+
# Auto adopt if requested
|
|
610
|
+
if args.get("adopt", False):
|
|
611
|
+
adopt_res = run_process([AHIL_EXE, "adopt-hardware"], timeout=20)
|
|
612
|
+
messages.append(f"adopt-hardware: {adopt_res.get('stdout', '')}")
|
|
613
|
+
|
|
614
|
+
return {
|
|
615
|
+
"ok": True,
|
|
616
|
+
"probes": probes,
|
|
617
|
+
"probes_count": len(probes),
|
|
618
|
+
"com_ports": com_ports,
|
|
619
|
+
"com_ports_count": len(com_ports),
|
|
620
|
+
"details": messages,
|
|
621
|
+
"recommendation": (
|
|
622
|
+
"No physical probes detected. Connect an ST-LINK or J-Link debugger and retry."
|
|
623
|
+
if len(probes) == 0 else
|
|
624
|
+
f"Detected {len(probes)} probe(s) and {len(com_ports)} COM port(s)."
|
|
625
|
+
),
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
# ===========================================================================
|
|
630
|
+
# Tool Definitions & Schemas
|
|
631
|
+
# ===========================================================================
|
|
632
|
+
|
|
633
|
+
TOOLS_REGISTRY = {
|
|
634
|
+
"fw_doctor": {
|
|
635
|
+
"description": "Run diagnostic check on environment, toolchains, Python modules, COM ports, and probe/instrument readiness.",
|
|
636
|
+
"inputSchema": {
|
|
637
|
+
"type": "object",
|
|
638
|
+
"properties": {
|
|
639
|
+
"include_ports": {
|
|
640
|
+
"type": "boolean",
|
|
641
|
+
"description": "Whether to query active COM ports on the host",
|
|
642
|
+
"default": True,
|
|
643
|
+
}
|
|
644
|
+
},
|
|
645
|
+
"additionalProperties": False,
|
|
646
|
+
},
|
|
647
|
+
"handler": handle_fw_doctor,
|
|
648
|
+
},
|
|
649
|
+
"fw_configure_lab": {
|
|
650
|
+
"description": "Interactively configure or update project parameters, target chip (e.g. STM32F103C8), build backend (e.g. keil), source directory, COM port, and debugger probe in lab/lab.yaml.",
|
|
651
|
+
"inputSchema": {
|
|
652
|
+
"type": "object",
|
|
653
|
+
"properties": {
|
|
654
|
+
"project_name": {
|
|
655
|
+
"type": "string",
|
|
656
|
+
"description": "Project identifier name",
|
|
657
|
+
},
|
|
658
|
+
"build_backend": {
|
|
659
|
+
"type": "string",
|
|
660
|
+
"enum": ["keil", "cmake", "make", "iar", "zephyr", "esp-idf", "platformio"],
|
|
661
|
+
"description": "Build toolchain backend",
|
|
662
|
+
},
|
|
663
|
+
"source_dir": {
|
|
664
|
+
"type": "string",
|
|
665
|
+
"description": "Source or project directory containing .uvprojx, Makefile, or CMakeLists.txt",
|
|
666
|
+
},
|
|
667
|
+
"target_chip": {
|
|
668
|
+
"type": "string",
|
|
669
|
+
"description": "Target MCU model (e.g. STM32F103C8, STM32F407ZG)",
|
|
670
|
+
},
|
|
671
|
+
"debugger_backend": {
|
|
672
|
+
"type": "string",
|
|
673
|
+
"enum": ["stlink", "jlink", "pyocd", "openocd", "daplink"],
|
|
674
|
+
"description": "Hardware debugger probe backend",
|
|
675
|
+
},
|
|
676
|
+
"debugger_probe_id": {
|
|
677
|
+
"type": "string",
|
|
678
|
+
"description": "Serial number or ID of the hardware probe",
|
|
679
|
+
},
|
|
680
|
+
"uart_port": {
|
|
681
|
+
"type": "string",
|
|
682
|
+
"description": "DUT serial communication COM port (e.g. COM5)",
|
|
683
|
+
},
|
|
684
|
+
"uart_baudrate": {
|
|
685
|
+
"type": "integer",
|
|
686
|
+
"description": "UART baud rate (e.g. 115200)",
|
|
687
|
+
"default": 115200,
|
|
688
|
+
},
|
|
689
|
+
},
|
|
690
|
+
"additionalProperties": False,
|
|
691
|
+
},
|
|
692
|
+
"handler": handle_fw_configure_lab,
|
|
693
|
+
},
|
|
694
|
+
"fw_scan_hardware": {
|
|
695
|
+
"description": "Scan and identify all attached hardware debuggers (ST-LINK, J-Link, CMSIS-DAP), target MCU identity, and active COM ports.",
|
|
696
|
+
"inputSchema": {
|
|
697
|
+
"type": "object",
|
|
698
|
+
"properties": {
|
|
699
|
+
"adopt": {
|
|
700
|
+
"type": "boolean",
|
|
701
|
+
"description": "Whether to carry detected hardware parameters into configuration automatically",
|
|
702
|
+
"default": False,
|
|
703
|
+
}
|
|
704
|
+
},
|
|
705
|
+
"additionalProperties": False,
|
|
706
|
+
},
|
|
707
|
+
"handler": handle_fw_scan_hardware,
|
|
708
|
+
},
|
|
709
|
+
"fw_build": {
|
|
710
|
+
"description": "Compile firmware using the repository's build system (Keil, CMake, Make, IAR, PlatformIO, Zephyr, ESP-IDF). Returns structured artifact SHA256 and compiler diagnostics (file, line, col, message).",
|
|
711
|
+
"inputSchema": {
|
|
712
|
+
"type": "object",
|
|
713
|
+
"properties": {
|
|
714
|
+
"backend": {
|
|
715
|
+
"type": "string",
|
|
716
|
+
"enum": ["auto", "keil", "cmake", "make", "platformio", "iar", "zephyr", "esp-idf"],
|
|
717
|
+
"description": "Build system backend (defaults to auto detection or lab.yaml)",
|
|
718
|
+
"default": "auto",
|
|
719
|
+
},
|
|
720
|
+
"configuration": {
|
|
721
|
+
"type": "string",
|
|
722
|
+
"enum": ["Debug", "Release"],
|
|
723
|
+
"description": "Build configuration",
|
|
724
|
+
"default": "Debug",
|
|
725
|
+
},
|
|
726
|
+
"clean": {
|
|
727
|
+
"type": "boolean",
|
|
728
|
+
"description": "Perform full clean rebuild before compiling",
|
|
729
|
+
"default": True,
|
|
730
|
+
},
|
|
731
|
+
"source_dir": {
|
|
732
|
+
"type": "string",
|
|
733
|
+
"description": "Custom source directory (optional)",
|
|
734
|
+
},
|
|
735
|
+
"dry_run": {
|
|
736
|
+
"type": "boolean",
|
|
737
|
+
"description": "Only construct and return the build command without executing",
|
|
738
|
+
"default": False,
|
|
739
|
+
},
|
|
740
|
+
},
|
|
741
|
+
"additionalProperties": False,
|
|
742
|
+
},
|
|
743
|
+
"handler": handle_fw_build,
|
|
744
|
+
},
|
|
745
|
+
"fw_flash": {
|
|
746
|
+
"description": "Flash compiled firmware image (.elf / .hex / .bin / .axf) into target MCU via ST-LINK, J-Link, pyOCD, or simulator.",
|
|
747
|
+
"inputSchema": {
|
|
748
|
+
"type": "object",
|
|
749
|
+
"properties": {
|
|
750
|
+
"backend": {
|
|
751
|
+
"type": "string",
|
|
752
|
+
"enum": ["simulator", "agentic-hil", "openocd", "stm32cubeprogrammer", "jlink"],
|
|
753
|
+
"description": "Flashing backend driver",
|
|
754
|
+
"default": "simulator",
|
|
755
|
+
},
|
|
756
|
+
"artifact_path": {
|
|
757
|
+
"type": "string",
|
|
758
|
+
"description": "Relative or absolute path to the binary artifact to flash",
|
|
759
|
+
"default": "artifacts/build/firmware.elf",
|
|
760
|
+
},
|
|
761
|
+
},
|
|
762
|
+
"additionalProperties": False,
|
|
763
|
+
},
|
|
764
|
+
"handler": handle_fw_flash,
|
|
765
|
+
},
|
|
766
|
+
"fw_reset": {
|
|
767
|
+
"description": "Reset physical or simulated target MCU via debug probe or hardware reset line.",
|
|
768
|
+
"inputSchema": {
|
|
769
|
+
"type": "object",
|
|
770
|
+
"properties": {
|
|
771
|
+
"backend": {
|
|
772
|
+
"type": "string",
|
|
773
|
+
"enum": ["simulator", "agentic-hil", "openocd"],
|
|
774
|
+
"description": "Reset backend driver",
|
|
775
|
+
"default": "simulator",
|
|
776
|
+
},
|
|
777
|
+
"expected_target": {
|
|
778
|
+
"type": "string",
|
|
779
|
+
"description": "Expected MCU identity to verify before issuing reset",
|
|
780
|
+
},
|
|
781
|
+
},
|
|
782
|
+
"additionalProperties": False,
|
|
783
|
+
},
|
|
784
|
+
"handler": handle_fw_reset,
|
|
785
|
+
},
|
|
786
|
+
"fw_run_hil_test": {
|
|
787
|
+
"description": "Execute pytest hardware-in-the-loop (HIL) automated test suite (boot, UART, protocol, power, signal, logic analyzer). Returns structured JUnit & evidence.",
|
|
788
|
+
"inputSchema": {
|
|
789
|
+
"type": "object",
|
|
790
|
+
"properties": {
|
|
791
|
+
"gate": {
|
|
792
|
+
"type": "string",
|
|
793
|
+
"enum": ["simulator", "agentic-hil", "direct-serial"],
|
|
794
|
+
"description": "UART/Hardware gate mode",
|
|
795
|
+
"default": "simulator",
|
|
796
|
+
},
|
|
797
|
+
"test_filter": {
|
|
798
|
+
"type": "string",
|
|
799
|
+
"description": "pytest filter expression (-k filter)",
|
|
800
|
+
},
|
|
801
|
+
},
|
|
802
|
+
"additionalProperties": False,
|
|
803
|
+
},
|
|
804
|
+
"handler": handle_fw_run_hil_test,
|
|
805
|
+
},
|
|
806
|
+
"fw_acceptance_scenario": {
|
|
807
|
+
"description": "Execute complete end-to-end acceptance scenario (Build -> Flash -> Reset -> UART Observation -> Logic Analyzer -> Scope -> pytest -> Final Report).",
|
|
808
|
+
"inputSchema": {
|
|
809
|
+
"type": "object",
|
|
810
|
+
"properties": {
|
|
811
|
+
"mode": {
|
|
812
|
+
"type": "string",
|
|
813
|
+
"enum": ["simulator", "real"],
|
|
814
|
+
"description": "Execution mode (simulator for host CI/offline, real for physical bench)",
|
|
815
|
+
"default": "simulator",
|
|
816
|
+
},
|
|
817
|
+
"hardware": {
|
|
818
|
+
"type": "string",
|
|
819
|
+
"enum": ["agentic-hil"],
|
|
820
|
+
"description": "Hardware adapter when running in real mode",
|
|
821
|
+
"default": "agentic-hil",
|
|
822
|
+
},
|
|
823
|
+
},
|
|
824
|
+
"additionalProperties": False,
|
|
825
|
+
},
|
|
826
|
+
"handler": handle_fw_acceptance_scenario,
|
|
827
|
+
},
|
|
828
|
+
"fw_measure": {
|
|
829
|
+
"description": "Perform controlled, safe physical or simulated instrument measurements (Scope, PSU, DMM, AWG) via PyVISA/SCPI, enforced by safety limits.",
|
|
830
|
+
"inputSchema": {
|
|
831
|
+
"type": "object",
|
|
832
|
+
"properties": {
|
|
833
|
+
"instrument_type": {
|
|
834
|
+
"type": "string",
|
|
835
|
+
"enum": ["scope", "psu", "dmm"],
|
|
836
|
+
"description": "Category of instrument",
|
|
837
|
+
"default": "scope",
|
|
838
|
+
},
|
|
839
|
+
"command": {
|
|
840
|
+
"type": "string",
|
|
841
|
+
"enum": [
|
|
842
|
+
"measure-frequency",
|
|
843
|
+
"measure-duty",
|
|
844
|
+
"measure-vpp",
|
|
845
|
+
"measure-rms",
|
|
846
|
+
"measure-rise-time",
|
|
847
|
+
"measure-voltage",
|
|
848
|
+
"measure-current",
|
|
849
|
+
"measure-power",
|
|
850
|
+
"measure-resistance",
|
|
851
|
+
],
|
|
852
|
+
"description": "Measurement command",
|
|
853
|
+
"default": "measure-frequency",
|
|
854
|
+
},
|
|
855
|
+
"instrument_name": {
|
|
856
|
+
"type": "string",
|
|
857
|
+
"description": "Instrument identifier configured in lab.yaml (e.g., scope1, psu1)",
|
|
858
|
+
"default": "scope1",
|
|
859
|
+
},
|
|
860
|
+
"channel": {
|
|
861
|
+
"type": "string",
|
|
862
|
+
"description": "Scope channel (CH1, CH2, etc.)",
|
|
863
|
+
"default": "CH1",
|
|
864
|
+
},
|
|
865
|
+
"backend": {
|
|
866
|
+
"type": "string",
|
|
867
|
+
"enum": ["simulator", "visa"],
|
|
868
|
+
"description": "Driver backend (simulator or real VISA)",
|
|
869
|
+
"default": "simulator",
|
|
870
|
+
},
|
|
871
|
+
},
|
|
872
|
+
"required": ["instrument_type", "command"],
|
|
873
|
+
"additionalProperties": False,
|
|
874
|
+
},
|
|
875
|
+
"handler": handle_fw_measure,
|
|
876
|
+
},
|
|
877
|
+
"fw_logic_capture": {
|
|
878
|
+
"description": "Capture digital protocol waveforms (I2C, SPI, UART) via Saleae / sigrok or deterministic simulator.",
|
|
879
|
+
"inputSchema": {
|
|
880
|
+
"type": "object",
|
|
881
|
+
"properties": {
|
|
882
|
+
"protocol": {
|
|
883
|
+
"type": "string",
|
|
884
|
+
"enum": ["i2c", "spi", "uart"],
|
|
885
|
+
"description": "Target communication protocol (e.g. i2c, spi, uart)",
|
|
886
|
+
"default": "i2c",
|
|
887
|
+
},
|
|
888
|
+
"sample_rate": {
|
|
889
|
+
"type": "integer",
|
|
890
|
+
"description": "Sample rate in Hz",
|
|
891
|
+
"default": 10000000,
|
|
892
|
+
},
|
|
893
|
+
"duration_ms": {
|
|
894
|
+
"type": "integer",
|
|
895
|
+
"description": "Capture duration in milliseconds",
|
|
896
|
+
"default": 50,
|
|
897
|
+
},
|
|
898
|
+
"output_file": {
|
|
899
|
+
"type": "string",
|
|
900
|
+
"description": "Path to save raw capture file (optional)",
|
|
901
|
+
},
|
|
902
|
+
},
|
|
903
|
+
"additionalProperties": False,
|
|
904
|
+
},
|
|
905
|
+
"handler": handle_fw_logic_capture,
|
|
906
|
+
},
|
|
907
|
+
"fw_logic_decode": {
|
|
908
|
+
"description": "Decode captured digital waveforms into protocol packets/frames (I2C address/ACK, SPI bytes, UART data) and verify assertions.",
|
|
909
|
+
"inputSchema": {
|
|
910
|
+
"type": "object",
|
|
911
|
+
"properties": {
|
|
912
|
+
"capture_file": {
|
|
913
|
+
"type": "string",
|
|
914
|
+
"description": "Path to capture file (defaults to latest capture in artifacts/captures/)",
|
|
915
|
+
},
|
|
916
|
+
"protocol": {
|
|
917
|
+
"type": "string",
|
|
918
|
+
"enum": ["i2c", "spi", "uart"],
|
|
919
|
+
"description": "Protocol decoder",
|
|
920
|
+
"default": "i2c",
|
|
921
|
+
},
|
|
922
|
+
"frequency_hz": {
|
|
923
|
+
"type": "integer",
|
|
924
|
+
"description": "Clock frequency for decoding (Hz)",
|
|
925
|
+
"default": 400000,
|
|
926
|
+
},
|
|
927
|
+
},
|
|
928
|
+
"additionalProperties": False,
|
|
929
|
+
},
|
|
930
|
+
"handler": handle_fw_logic_decode,
|
|
931
|
+
},
|
|
932
|
+
"fw_get_evidence": {
|
|
933
|
+
"description": "Retrieve audit evidence, summary, and artifacts from recent test and acceptance runs.",
|
|
934
|
+
"inputSchema": {
|
|
935
|
+
"type": "object",
|
|
936
|
+
"properties": {
|
|
937
|
+
"run_id": {
|
|
938
|
+
"type": "string",
|
|
939
|
+
"description": "Specific run ID or 'latest' for the most recent run",
|
|
940
|
+
"default": "latest",
|
|
941
|
+
}
|
|
942
|
+
},
|
|
943
|
+
"additionalProperties": False,
|
|
944
|
+
},
|
|
945
|
+
"handler": handle_fw_get_evidence,
|
|
946
|
+
},
|
|
947
|
+
"fw_init_project": {
|
|
948
|
+
"description": "Scaffold multi-agent instruction files (AGENTS.md, CLAUDE.md, GEMINI.md), lab.yaml bench config, and .mcp.json in the current or specified firmware project directory.",
|
|
949
|
+
"inputSchema": {
|
|
950
|
+
"type": "object",
|
|
951
|
+
"properties": {
|
|
952
|
+
"target_dir": {
|
|
953
|
+
"type": "string",
|
|
954
|
+
"description": "Directory path of the firmware project to initialize (defaults to current directory '.')",
|
|
955
|
+
"default": ".",
|
|
956
|
+
},
|
|
957
|
+
"target_chip": {
|
|
958
|
+
"type": "string",
|
|
959
|
+
"description": "Target MCU chip model (e.g. STM32F103C8, STM32F407ZG)",
|
|
960
|
+
"default": "STM32F103C8",
|
|
961
|
+
},
|
|
962
|
+
"build_backend": {
|
|
963
|
+
"type": "string",
|
|
964
|
+
"enum": ["keil", "cmake", "make", "platformio", "iar", "zephyr", "esp-idf"],
|
|
965
|
+
"description": "Build system backend",
|
|
966
|
+
"default": "keil",
|
|
967
|
+
},
|
|
968
|
+
"overwrite": {
|
|
969
|
+
"type": "boolean",
|
|
970
|
+
"description": "Whether to overwrite existing instruction files if present",
|
|
971
|
+
"default": False,
|
|
972
|
+
},
|
|
973
|
+
},
|
|
974
|
+
"additionalProperties": False,
|
|
975
|
+
},
|
|
976
|
+
"handler": handle_fw_init_project,
|
|
977
|
+
},
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
# ===========================================================================
|
|
982
|
+
# JSON-RPC 2.0 MCP Protocol Processor
|
|
983
|
+
# ===========================================================================
|
|
984
|
+
|
|
985
|
+
def build_tools_list() -> List[Dict[str, Any]]:
|
|
986
|
+
tools = []
|
|
987
|
+
for name, info in TOOLS_REGISTRY.items():
|
|
988
|
+
tools.append({
|
|
989
|
+
"name": name,
|
|
990
|
+
"description": info["description"],
|
|
991
|
+
"inputSchema": info["inputSchema"],
|
|
992
|
+
})
|
|
993
|
+
return tools
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
def process_request(req: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
997
|
+
req_id = req.get("id")
|
|
998
|
+
method = req.get("method")
|
|
999
|
+
params = req.get("params", {})
|
|
1000
|
+
|
|
1001
|
+
if method == "initialize":
|
|
1002
|
+
return {
|
|
1003
|
+
"jsonrpc": "2.0",
|
|
1004
|
+
"id": req_id,
|
|
1005
|
+
"result": {
|
|
1006
|
+
"protocolVersion": "2024-11-05",
|
|
1007
|
+
"capabilities": {
|
|
1008
|
+
"tools": {
|
|
1009
|
+
"listChanged": False
|
|
1010
|
+
}
|
|
1011
|
+
},
|
|
1012
|
+
"serverInfo": {
|
|
1013
|
+
"name": "firmwareloop",
|
|
1014
|
+
"version": "0.0.8"
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
if method == "notifications/initialized":
|
|
1020
|
+
return None
|
|
1021
|
+
|
|
1022
|
+
if method == "ping":
|
|
1023
|
+
return {
|
|
1024
|
+
"jsonrpc": "2.0",
|
|
1025
|
+
"id": req_id,
|
|
1026
|
+
"result": {}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
if method == "tools/list":
|
|
1030
|
+
return {
|
|
1031
|
+
"jsonrpc": "2.0",
|
|
1032
|
+
"id": req_id,
|
|
1033
|
+
"result": {
|
|
1034
|
+
"tools": build_tools_list()
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
if method == "tools/call":
|
|
1039
|
+
tool_name = params.get("name")
|
|
1040
|
+
arguments = params.get("arguments", {})
|
|
1041
|
+
|
|
1042
|
+
if tool_name not in TOOLS_REGISTRY:
|
|
1043
|
+
return {
|
|
1044
|
+
"jsonrpc": "2.0",
|
|
1045
|
+
"id": req_id,
|
|
1046
|
+
"error": {
|
|
1047
|
+
"code": -32601,
|
|
1048
|
+
"message": f"Tool '{tool_name}' not found"
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
handler = TOOLS_REGISTRY[tool_name]["handler"]
|
|
1053
|
+
try:
|
|
1054
|
+
result_data = handler(arguments)
|
|
1055
|
+
result_text = json.dumps(result_data, ensure_ascii=False, indent=2)
|
|
1056
|
+
return {
|
|
1057
|
+
"jsonrpc": "2.0",
|
|
1058
|
+
"id": req_id,
|
|
1059
|
+
"result": {
|
|
1060
|
+
"content": [
|
|
1061
|
+
{
|
|
1062
|
+
"type": "text",
|
|
1063
|
+
"text": result_text
|
|
1064
|
+
}
|
|
1065
|
+
],
|
|
1066
|
+
"isError": not result_data.get("ok", result_data.get("success", True))
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
except Exception as e:
|
|
1070
|
+
err_body = {
|
|
1071
|
+
"ok": False,
|
|
1072
|
+
"error_class": "TOOL_EXECUTION_EXCEPTION",
|
|
1073
|
+
"error": str(e),
|
|
1074
|
+
"traceback": traceback.format_exc()
|
|
1075
|
+
}
|
|
1076
|
+
return {
|
|
1077
|
+
"jsonrpc": "2.0",
|
|
1078
|
+
"id": req_id,
|
|
1079
|
+
"result": {
|
|
1080
|
+
"content": [
|
|
1081
|
+
{
|
|
1082
|
+
"type": "text",
|
|
1083
|
+
"text": json.dumps(err_body, ensure_ascii=False, indent=2)
|
|
1084
|
+
}
|
|
1085
|
+
],
|
|
1086
|
+
"isError": True
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
# Unknown method
|
|
1091
|
+
if req_id is not None:
|
|
1092
|
+
return {
|
|
1093
|
+
"jsonrpc": "2.0",
|
|
1094
|
+
"id": req_id,
|
|
1095
|
+
"error": {
|
|
1096
|
+
"code": -32601,
|
|
1097
|
+
"message": f"Method '{method}' not implemented"
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
return None
|
|
1101
|
+
|
|
1102
|
+
|
|
1103
|
+
def handle_cli_update() -> int:
|
|
1104
|
+
"""Handle `firmwareloop update` command."""
|
|
1105
|
+
print("============================================================")
|
|
1106
|
+
print(" FirmwareLoop Auto-Updater (v0.0.8)")
|
|
1107
|
+
print("============================================================")
|
|
1108
|
+
|
|
1109
|
+
is_git_repo = os.path.exists(os.path.join(REPO_ROOT, ".git"))
|
|
1110
|
+
if is_git_repo:
|
|
1111
|
+
print(f"[*] Local Git repository detected at: {REPO_ROOT}")
|
|
1112
|
+
print("[*] Pulling latest updates from GitHub remote...")
|
|
1113
|
+
try:
|
|
1114
|
+
res_pull = subprocess.run(["git", "pull"], cwd=REPO_ROOT, text=True, capture_output=True)
|
|
1115
|
+
print(res_pull.stdout.strip() if res_pull.stdout else "")
|
|
1116
|
+
if res_pull.returncode != 0:
|
|
1117
|
+
print(f"[-] Git pull failed: {res_pull.stderr.strip()}")
|
|
1118
|
+
return 1
|
|
1119
|
+
print("[+] Git pull completed successfully.")
|
|
1120
|
+
except Exception as e:
|
|
1121
|
+
print(f"[-] Error executing git pull: {e}")
|
|
1122
|
+
return 1
|
|
1123
|
+
|
|
1124
|
+
print("[*] Reinstalling & updating package dependencies...")
|
|
1125
|
+
uv_path = shutil.which("uv")
|
|
1126
|
+
if uv_path:
|
|
1127
|
+
cmd = [uv_path, "pip", "install", "-e", "."]
|
|
1128
|
+
else:
|
|
1129
|
+
cmd = [sys.executable, "-m", "pip", "install", "-e", "."]
|
|
1130
|
+
|
|
1131
|
+
try:
|
|
1132
|
+
res_install = subprocess.run(cmd, cwd=REPO_ROOT, text=True, capture_output=True)
|
|
1133
|
+
if res_install.returncode == 0:
|
|
1134
|
+
print("[+] Dependencies and CLI entrypoints updated successfully.")
|
|
1135
|
+
else:
|
|
1136
|
+
print(f"[-] Dependency update warning: {res_install.stderr.strip()}")
|
|
1137
|
+
except Exception as e:
|
|
1138
|
+
print(f"[-] Error installing dependencies: {e}")
|
|
1139
|
+
|
|
1140
|
+
print("============================================================")
|
|
1141
|
+
print("[+] FirmwareLoop is now up to date!")
|
|
1142
|
+
print("============================================================")
|
|
1143
|
+
return 0
|
|
1144
|
+
else:
|
|
1145
|
+
print("[*] Global / uv-managed installation detected.")
|
|
1146
|
+
uv_path = shutil.which("uv")
|
|
1147
|
+
if uv_path:
|
|
1148
|
+
print("[*] Refreshing uv cache for firmwareloop...")
|
|
1149
|
+
try:
|
|
1150
|
+
subprocess.run([uv_path, "cache", "clean", "firmwareloop"], capture_output=True)
|
|
1151
|
+
print("[+] uv cache refreshed! The next run will automatically fetch the latest release from GitHub.")
|
|
1152
|
+
return 0
|
|
1153
|
+
except Exception as e:
|
|
1154
|
+
print(f"[-] Error refreshing uv cache: {e}")
|
|
1155
|
+
return 1
|
|
1156
|
+
else:
|
|
1157
|
+
print("[*] Upgrading via pip...")
|
|
1158
|
+
res = subprocess.run([sys.executable, "-m", "pip", "install", "--upgrade", "firmwareloop"])
|
|
1159
|
+
return res.returncode
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
def handle_cli_doctor() -> int:
|
|
1163
|
+
"""Handle `firmwareloop doctor` command."""
|
|
1164
|
+
print("[*] Running FirmwareLoop environment diagnostics...")
|
|
1165
|
+
cmd = [PWSH_EXE, "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", os.path.join(REPO_ROOT, "tools", "doctor.ps1")]
|
|
1166
|
+
res = subprocess.run(cmd, cwd=REPO_ROOT)
|
|
1167
|
+
return res.returncode
|
|
1168
|
+
|
|
1169
|
+
|
|
1170
|
+
def handle_cli_setup() -> int:
|
|
1171
|
+
"""Handle `firmwareloop setup` command."""
|
|
1172
|
+
cmd = [PWSH_EXE, "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", os.path.join(REPO_ROOT, "tools", "setup-agent-mcp.ps1")]
|
|
1173
|
+
res = subprocess.run(cmd, cwd=REPO_ROOT)
|
|
1174
|
+
return res.returncode
|
|
1175
|
+
|
|
1176
|
+
|
|
1177
|
+
def handle_cli_init() -> int:
|
|
1178
|
+
"""Handle `firmwareloop init` command."""
|
|
1179
|
+
print("============================================================")
|
|
1180
|
+
print(" FirmwareLoop Multi-Agent Project Initializer (v0.0.8)")
|
|
1181
|
+
print("============================================================")
|
|
1182
|
+
cwd = os.getcwd()
|
|
1183
|
+
print(f"[*] Initializing multi-agent guidelines & bench config in:\n {cwd}\n")
|
|
1184
|
+
res = handle_fw_init_project({"target_dir": cwd, "overwrite": False})
|
|
1185
|
+
if res.get("ok"):
|
|
1186
|
+
for f in res.get("created_files", []):
|
|
1187
|
+
print(f" [+] Created: {f}")
|
|
1188
|
+
if not res.get("created_files"):
|
|
1189
|
+
print(" [*] All agent instruction files (AGENTS.md, CLAUDE.md, GEMINI.md, lab.yaml, .mcp.json) already exist.")
|
|
1190
|
+
print("")
|
|
1191
|
+
print("============================================================")
|
|
1192
|
+
print("[+] Done! All AI Coding Agents (Claude Code, Qoder, Antigravity, Cursor) are now ready to operate in this repository.")
|
|
1193
|
+
print("============================================================")
|
|
1194
|
+
return 0
|
|
1195
|
+
else:
|
|
1196
|
+
print(f"[-] Initialization failed: {res.get('message')}")
|
|
1197
|
+
return 1
|
|
1198
|
+
|
|
1199
|
+
|
|
1200
|
+
def print_cli_help() -> None:
|
|
1201
|
+
print("""FirmwareLoop (fwloop) — AI Agent Firmware Engineering & Lab Automation Platform (v0.0.8)
|
|
1202
|
+
|
|
1203
|
+
Usage:
|
|
1204
|
+
fwloop [command] (or: firmwareloop [command])
|
|
1205
|
+
|
|
1206
|
+
Commands:
|
|
1207
|
+
init Initialize multi-agent rules (AGENTS.md, CLAUDE.md, GEMINI.md) & lab.yaml in current project
|
|
1208
|
+
update Check and update FirmwareLoop to the latest version
|
|
1209
|
+
doctor Run environment & toolchain diagnostics
|
|
1210
|
+
setup Print or generate AI Agent MCP registration commands
|
|
1211
|
+
version, -v Show current version
|
|
1212
|
+
help, -h Show this help message
|
|
1213
|
+
|
|
1214
|
+
Default behavior (no arguments):
|
|
1215
|
+
Starts the Model Context Protocol (MCP) JSON-RPC 2.0 stdio server.
|
|
1216
|
+
""")
|
|
1217
|
+
|
|
1218
|
+
|
|
1219
|
+
def main() -> None:
|
|
1220
|
+
args = sys.argv[1:]
|
|
1221
|
+
if args:
|
|
1222
|
+
cmd = args[0].lower().strip()
|
|
1223
|
+
if cmd in ["init", "scaffold"]:
|
|
1224
|
+
sys.exit(handle_cli_init())
|
|
1225
|
+
elif cmd in ["update", "upgrade"]:
|
|
1226
|
+
sys.exit(handle_cli_update())
|
|
1227
|
+
elif cmd in ["doctor", "check"]:
|
|
1228
|
+
sys.exit(handle_cli_doctor())
|
|
1229
|
+
elif cmd in ["setup", "register"]:
|
|
1230
|
+
sys.exit(handle_cli_setup())
|
|
1231
|
+
elif cmd in ["version", "-v", "--version"]:
|
|
1232
|
+
print("FirmwareLoop v0.0.8")
|
|
1233
|
+
sys.exit(0)
|
|
1234
|
+
elif cmd in ["help", "-h", "--help"]:
|
|
1235
|
+
print_cli_help()
|
|
1236
|
+
sys.exit(0)
|
|
1237
|
+
elif cmd in ["mcp", "mcp-stdio", "stdio"]:
|
|
1238
|
+
pass # Fall through to stdio server loop
|
|
1239
|
+
|
|
1240
|
+
# MCP stdio JSON-RPC loop
|
|
1241
|
+
if sys.platform == "win32":
|
|
1242
|
+
import msvcrt
|
|
1243
|
+
msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
|
|
1244
|
+
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
|
|
1245
|
+
|
|
1246
|
+
reader = sys.stdin.buffer
|
|
1247
|
+
writer = sys.stdout.buffer
|
|
1248
|
+
|
|
1249
|
+
while True:
|
|
1250
|
+
line = reader.readline()
|
|
1251
|
+
if not line:
|
|
1252
|
+
break
|
|
1253
|
+
|
|
1254
|
+
line_str = line.decode("utf-8", errors="replace").strip()
|
|
1255
|
+
if not line_str:
|
|
1256
|
+
continue
|
|
1257
|
+
|
|
1258
|
+
try:
|
|
1259
|
+
req = json.loads(line_str)
|
|
1260
|
+
except Exception:
|
|
1261
|
+
continue
|
|
1262
|
+
|
|
1263
|
+
res = process_request(req)
|
|
1264
|
+
if res is not None:
|
|
1265
|
+
res_bytes = (json.dumps(res, ensure_ascii=False) + "\n").encode("utf-8")
|
|
1266
|
+
writer.write(res_bytes)
|
|
1267
|
+
writer.flush()
|
|
1268
|
+
|
|
1269
|
+
|
|
1270
|
+
if __name__ == "__main__":
|
|
1271
|
+
main()
|
|
1272
|
+
|