flashgate 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flashgate/__init__.py +8 -0
- flashgate/__main__.py +5 -0
- flashgate/board.py +120 -0
- flashgate/cli.py +466 -0
- flashgate/flasher.py +132 -0
- flashgate/gatestate.py +81 -0
- flashgate/mcp_server.py +221 -0
- flashgate/probes.py +171 -0
- flashgate/serialmon.py +106 -0
- flashgate/sttools.py +56 -0
- flashgate/swdsig.py +83 -0
- flashgate-0.4.0.dist-info/METADATA +203 -0
- flashgate-0.4.0.dist-info/RECORD +17 -0
- flashgate-0.4.0.dist-info/WHEEL +5 -0
- flashgate-0.4.0.dist-info/entry_points.txt +3 -0
- flashgate-0.4.0.dist-info/licenses/LICENSE +21 -0
- flashgate-0.4.0.dist-info/top_level.txt +1 -0
flashgate/serialmon.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Console serial: resolve the USB-TTL adapter, wait for the boot banner."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
import serial
|
|
12
|
+
from serial.tools import list_ports
|
|
13
|
+
|
|
14
|
+
ENV_PORT = "FLASHGATE_SERIAL_PORT"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class BannerResult:
|
|
19
|
+
matched: bool
|
|
20
|
+
groups: dict[str, str] | None
|
|
21
|
+
transcript: str
|
|
22
|
+
error_hit: str | None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def resolve_console_port(
|
|
26
|
+
explicit_port: str,
|
|
27
|
+
vid: int,
|
|
28
|
+
pids: tuple[int, ...],
|
|
29
|
+
) -> tuple[str | None, str]:
|
|
30
|
+
"""Layered port resolution. Returns (device, why) so doctor can explain.
|
|
31
|
+
|
|
32
|
+
1. explicit port (yaml `serial.port` or env FLASHGATE_SERIAL_PORT)
|
|
33
|
+
2. VID/PID hint — matches common USB-TTL bridges (CH340/CP210x/FT232...)
|
|
34
|
+
3. sole serial port on the machine
|
|
35
|
+
The banner regex remains the final identity proof in every case: a wrong
|
|
36
|
+
port simply never matches and verify times out with a clear error.
|
|
37
|
+
"""
|
|
38
|
+
explicit = (os.environ.get(ENV_PORT) or explicit_port or "").strip()
|
|
39
|
+
if explicit:
|
|
40
|
+
return explicit, "explicit (config/env override)"
|
|
41
|
+
|
|
42
|
+
ports = list_ports.comports()
|
|
43
|
+
for p in ports:
|
|
44
|
+
if p.vid == vid and (not pids or p.pid in pids):
|
|
45
|
+
return p.device, f"VID/PID hint {p.vid:04X}:{p.pid:04X} ({p.description})"
|
|
46
|
+
if len(ports) == 1:
|
|
47
|
+
return ports[0].device, f"sole serial port ({ports[0].description})"
|
|
48
|
+
|
|
49
|
+
if not ports:
|
|
50
|
+
return None, "no serial ports on this machine — plug in the USB-TTL adapter"
|
|
51
|
+
names = ", ".join(f"{p.device} ({p.description})" for p in ports)
|
|
52
|
+
return None, f"ambiguous: multiple serial ports [{names}] — set serial.port or {ENV_PORT}"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def open_flush(device: str, baudrate: int) -> serial.Serial:
|
|
56
|
+
"""Open and drain stale output (a previous firmware's banner).
|
|
57
|
+
|
|
58
|
+
verify() keeps this connection open across the flash step on purpose:
|
|
59
|
+
the banner emitted right at the `--start` reset then sits in the OS
|
|
60
|
+
buffer instead of being lost to a close/reopen race.
|
|
61
|
+
"""
|
|
62
|
+
conn = serial.Serial(device, baudrate, timeout=0.2)
|
|
63
|
+
conn.reset_input_buffer()
|
|
64
|
+
conn.reset_output_buffer()
|
|
65
|
+
return conn
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def wait_on(
|
|
69
|
+
conn: serial.Serial,
|
|
70
|
+
banner_regex: str,
|
|
71
|
+
error_patterns: tuple[str, ...],
|
|
72
|
+
timeout_s: float,
|
|
73
|
+
echo: bool = True,
|
|
74
|
+
) -> BannerResult:
|
|
75
|
+
"""Read from an open connection until banner / error pattern / timeout."""
|
|
76
|
+
from .probes import compile_pattern
|
|
77
|
+
|
|
78
|
+
pattern = compile_pattern(banner_regex, anchor=False)
|
|
79
|
+
deadline = time.monotonic() + timeout_s
|
|
80
|
+
transcript = ""
|
|
81
|
+
|
|
82
|
+
while time.monotonic() < deadline:
|
|
83
|
+
chunk = conn.read(512)
|
|
84
|
+
if chunk:
|
|
85
|
+
text = chunk.decode("utf-8", errors="replace")
|
|
86
|
+
transcript += text
|
|
87
|
+
if echo:
|
|
88
|
+
sys.stdout.write(text)
|
|
89
|
+
sys.stdout.flush()
|
|
90
|
+
m = pattern.search(transcript)
|
|
91
|
+
if m:
|
|
92
|
+
return BannerResult(True, m.groupdict(), transcript, None)
|
|
93
|
+
for err in error_patterns:
|
|
94
|
+
if err in transcript:
|
|
95
|
+
return BannerResult(False, None, transcript, err)
|
|
96
|
+
|
|
97
|
+
return BannerResult(False, None, transcript, None)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def console_forever(device: str, baudrate: int) -> None:
|
|
101
|
+
with serial.Serial(device, baudrate, timeout=0.2) as conn:
|
|
102
|
+
while True:
|
|
103
|
+
chunk = conn.read(512)
|
|
104
|
+
if chunk:
|
|
105
|
+
sys.stdout.write(chunk.decode("utf-8", errors="replace"))
|
|
106
|
+
sys.stdout.flush()
|
flashgate/sttools.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""STM32Cube bundle discovery: toolchain + CubeProgrammer live under
|
|
2
|
+
%LOCALAPPDATA%/stm32cube/bundles/<tool>/<version>/bin on Windows when the
|
|
3
|
+
STM32Cube CLT / VSCode extension installed them."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _bundle_root() -> Path:
|
|
13
|
+
local = os.environ.get("LOCALAPPDATA") or str(Path.home() / "AppData" / "Local")
|
|
14
|
+
return Path(local) / "stm32cube" / "bundles"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def bundle_bin_dirs() -> list[Path]:
|
|
18
|
+
"""All bundle bin dirs (cmake/ninja/gcc/programmer...), newest version
|
|
19
|
+
of each tool first so PATH lookups win with the newest."""
|
|
20
|
+
root = _bundle_root()
|
|
21
|
+
if not root.is_dir():
|
|
22
|
+
return []
|
|
23
|
+
|
|
24
|
+
def version_key(p: Path) -> tuple:
|
|
25
|
+
raw = p.parent.name # the <version> directory
|
|
26
|
+
return tuple(int(x) if x.isdigit() else 0 for x in re.split(r"[+.-]", raw))
|
|
27
|
+
|
|
28
|
+
dirs = [p for p in root.glob("*/*/bin") if p.is_dir()]
|
|
29
|
+
return sorted(dirs, key=lambda p: (p.parts[-3], tuple(-x for x in version_key(p))))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def augmented_env() -> dict[str, str]:
|
|
33
|
+
"""subprocess env with all ST bundle bins prepended to PATH."""
|
|
34
|
+
env = os.environ.copy()
|
|
35
|
+
dirs = [str(p) for p in bundle_bin_dirs()]
|
|
36
|
+
if dirs:
|
|
37
|
+
env["PATH"] = os.pathsep.join(dirs + [env.get("PATH", "")])
|
|
38
|
+
return env
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def find_cubeprogrammer() -> Path | None:
|
|
42
|
+
"""Newest STM32_Programmer_CLI.exe under the bundles, or PATH lookup."""
|
|
43
|
+
candidates = list((_bundle_root() / "programmer").glob("*/bin/STM32_Programmer_CLI.exe"))
|
|
44
|
+
|
|
45
|
+
def version_key(p: Path) -> tuple:
|
|
46
|
+
raw = p.parent.parent.name
|
|
47
|
+
return tuple(int(x) if x.isdigit() else 0 for x in re.split(r"[+.-]", raw))
|
|
48
|
+
|
|
49
|
+
if candidates:
|
|
50
|
+
return max(candidates, key=version_key)
|
|
51
|
+
|
|
52
|
+
for entry in os.environ.get("PATH", "").split(os.pathsep):
|
|
53
|
+
guess = Path(entry) / "STM32_Programmer_CLI.exe"
|
|
54
|
+
if guess.is_file():
|
|
55
|
+
return guess
|
|
56
|
+
return None
|
flashgate/swdsig.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""SWD signature evidence: read the firmware's fixed-address boot identity
|
|
2
|
+
through the ST-Link alone — no serial cable required.
|
|
3
|
+
|
|
4
|
+
The firmware publishes a 64-byte struct at SIGRAM (end of AXI SRAM):
|
|
5
|
+
magic written LAST, CRC32 over the first 48 bytes. A debugger read that
|
|
6
|
+
races the boot-time writes fails the CRC and is simply retried.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import struct
|
|
12
|
+
import tempfile
|
|
13
|
+
import time
|
|
14
|
+
import zlib
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from .flasher import FLASH_ATTEMPTS # reuse attempt count semantics
|
|
18
|
+
from .sttools import augmented_env, find_cubeprogrammer
|
|
19
|
+
|
|
20
|
+
SIG_MAGIC = 0xF1A5C0DE
|
|
21
|
+
READ_TIMEOUT_S = 30
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SwdError(Exception):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def read_ram(connect: str, address: int, size: int) -> bytes | None:
|
|
29
|
+
"""One CubeProgrammer memory read (HotPlug: does not halt the core)."""
|
|
30
|
+
cli = find_cubeprogrammer()
|
|
31
|
+
if cli is None:
|
|
32
|
+
raise SwdError("STM32CubeProgrammer CLI not found")
|
|
33
|
+
import subprocess
|
|
34
|
+
|
|
35
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
36
|
+
out = Path(tmp) / "sig.bin"
|
|
37
|
+
cmd = [str(cli), "--connect", connect, "--read", hex(address), str(size), str(out)]
|
|
38
|
+
try:
|
|
39
|
+
proc = subprocess.run(
|
|
40
|
+
cmd, capture_output=True, text=True, timeout=READ_TIMEOUT_S,
|
|
41
|
+
env=augmented_env(), encoding="utf-8", errors="replace",
|
|
42
|
+
)
|
|
43
|
+
except subprocess.TimeoutExpired as exc:
|
|
44
|
+
raise SwdError("CubeProgrammer read timed out") from exc
|
|
45
|
+
if proc.returncode != 0 or not out.is_file():
|
|
46
|
+
output = (proc.stdout or "") + (proc.stderr or "")
|
|
47
|
+
raise SwdError(f"RAM read failed: {output.strip()[-300:]}")
|
|
48
|
+
return out.read_bytes()[:size]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def parse_signature(buf: bytes) -> dict | None:
|
|
52
|
+
"""Validate magic + CRC and decode the identity. None = not (yet) valid."""
|
|
53
|
+
if len(buf) < 64:
|
|
54
|
+
return None
|
|
55
|
+
magic, version, flags = struct.unpack_from("<IHH", buf, 0)
|
|
56
|
+
if magic != SIG_MAGIC:
|
|
57
|
+
return None
|
|
58
|
+
crc_field = struct.unpack_from("<I", buf, 0x30)[0]
|
|
59
|
+
if zlib.crc32(buf[:0x30]) & 0xFFFFFFFF != crc_field:
|
|
60
|
+
return None # raced the boot-time write: retry
|
|
61
|
+
git = buf[0x08:0x18].split(b"\x00", 1)[0].decode("ascii", errors="replace")
|
|
62
|
+
build = buf[0x18:0x30].split(b"\x00", 1)[0].decode("ascii", errors="replace")
|
|
63
|
+
return {"version": version, "flags": flags, "git": git, "build": build}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def wait_for_signature(
|
|
67
|
+
connect: str, address: int, size: int = 64, timeout_s: float = 15.0
|
|
68
|
+
) -> tuple[dict | None, str]:
|
|
69
|
+
"""Poll the signature until valid or timeout. Returns (info, last_error)."""
|
|
70
|
+
deadline = time.monotonic() + timeout_s
|
|
71
|
+
last = "no valid signature yet"
|
|
72
|
+
while time.monotonic() < deadline:
|
|
73
|
+
try:
|
|
74
|
+
buf = read_ram(connect, address, size)
|
|
75
|
+
except SwdError as exc:
|
|
76
|
+
last = str(exc)
|
|
77
|
+
time.sleep(1.0)
|
|
78
|
+
continue
|
|
79
|
+
info = parse_signature(buf or b"")
|
|
80
|
+
if info is not None:
|
|
81
|
+
return info, ""
|
|
82
|
+
time.sleep(0.5)
|
|
83
|
+
return None, last
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flashgate
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Hardware-in-the-loop verification gate: your agent can't claim the firmware works until the board says so.
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: pyserial>=3.5
|
|
10
|
+
Requires-Dist: PyYAML>=6.0
|
|
11
|
+
Provides-Extra: mcp
|
|
12
|
+
Requires-Dist: mcp>=1.2; extra == "mcp"
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# flashgate
|
|
18
|
+
|
|
19
|
+
flashgate answers one specific question: the firmware you just built — does
|
|
20
|
+
it actually run on the board?
|
|
21
|
+
|
|
22
|
+
It automates the whole chain: compile, flash over ST-Link, wait for the
|
|
23
|
+
board to report its own identity, then drive the actual feature over the
|
|
24
|
+
console and assert on hardware register readbacks. Any broken step ends
|
|
25
|
+
with a non-zero exit code and a reason.
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
build → flash over ST-Link → board boots → evidence → probes → exit code
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
[中文说明](#中文说明) | [使用说明(推荐先读这份)](docs/GUIDE.md)
|
|
32
|
+
|
|
33
|
+
## How the board testifies
|
|
34
|
+
|
|
35
|
+
Two evidence channels, picked per bench (`evidence.mode`: uart / swd / auto):
|
|
36
|
+
|
|
37
|
+
The firmware prints its identity over the console UART, and also publishes
|
|
38
|
+
a 64-byte signature (magic + git sha + build time + CRC32) at a fixed RAM
|
|
39
|
+
address that the host reads through the ST-Link alone — no serial cable
|
|
40
|
+
needed for boot verification:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
FLASHGATE-BOOT board=apollo-h743 git=2c58bd3 build=2026-08-28T07:26:35Z rtos=FreeRTOS
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Both carry the git sha plus `-dirty` when the tree differs from HEAD, so a
|
|
47
|
+
passing verify proves the board is running exactly the code you're looking
|
|
48
|
+
at. Functional probes then send real commands and assert on the answers,
|
|
49
|
+
including register readbacks (TIM3 CCR), not firmware self-reports:
|
|
50
|
+
|
|
51
|
+
```yaml
|
|
52
|
+
- send: "led0?"
|
|
53
|
+
expect: "OK led0 state={state} ccr={ccr:d}" # mirror of the firmware's printf
|
|
54
|
+
assert: "state == BREATH and ccr <= 1000"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Quick start
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install -e . # Python 3.11+
|
|
61
|
+
pip install -e ".[mcp]" # optional MCP server
|
|
62
|
+
|
|
63
|
+
flashgate doctor # ST-Link / serial / toolchain sanity
|
|
64
|
+
flashgate verify --all-probes # build → flash → evidence → sha → probes
|
|
65
|
+
echo $? # 0 = the board confirms it works
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Requires an ST-Link; a USB-TTL adapter on the console UART adds probes.
|
|
69
|
+
The repo ships a complete buildable example for the ALIENTEK Apollo
|
|
70
|
+
STM32H743 (`examples/apollo-h743/`) with the Stop hook pre-wired, so a
|
|
71
|
+
fresh clone verifies out of the box once wired up.
|
|
72
|
+
|
|
73
|
+
## Exit codes
|
|
74
|
+
|
|
75
|
+
| code | meaning |
|
|
76
|
+
|---|---|
|
|
77
|
+
| 0 | verified |
|
|
78
|
+
| 1 | build failed |
|
|
79
|
+
| 2 | flash failed |
|
|
80
|
+
| 3 | board stayed silent (no banner / no signature within timeout) |
|
|
81
|
+
| 4 | error string seen on serial (HardFault, assertion) |
|
|
82
|
+
| 5 | on-board identity ≠ repo state |
|
|
83
|
+
| 6 | environment error (no ST-Link / serial / tools) |
|
|
84
|
+
| 7 | functional probe failed |
|
|
85
|
+
|
|
86
|
+
## The Stop hook
|
|
87
|
+
|
|
88
|
+
`hooks/flashgate_stop.py` is a Claude Code Stop hook (any harness
|
|
89
|
+
implementing the same hook contract works). When the agent tries to finish after
|
|
90
|
+
touching watched firmware files, the hook fingerprints the tree and allows
|
|
91
|
+
instantly if that exact state already passed hardware verify (~0.7 s
|
|
92
|
+
cached). Otherwise it runs the full verify on the real board and blocks
|
|
93
|
+
the stop on failure, feeding the agent the board's testimony:
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
[flashgate] BLOCKED (attempt 1/2): firmware changes are not verified on hardware (verify rc=7).
|
|
97
|
+
[flashgate] last verify output:
|
|
98
|
+
step 1: led-demo> led0 breath
|
|
99
|
+
board: OK led0 state=OFF ← readback exposes the silent no-op
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The same broken tree is blocked at most twice, then released with a loud
|
|
103
|
+
warning — the session can never wedge, and a failure is never silently
|
|
104
|
+
swallowed.
|
|
105
|
+
|
|
106
|
+
## Install as a Claude Code plugin
|
|
107
|
+
|
|
108
|
+
The repo doubles as a Claude Code plugin: the Stop hook, an MCP server,
|
|
109
|
+
a board-integration skill and /flashgate commands, all at once:
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
pip install git+https://github.com/Lion-1209/flashgate # CLI + hook runtime
|
|
113
|
+
pip install "flashgate[mcp] @ git+https://github.com/Lion-1209/flashgate" # + MCP
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Then inside Claude Code:
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
/plugin marketplace add Lion-1209/flashgate
|
|
120
|
+
/plugin install flashgate@flashgate
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The hook needs to know your board profile: set `FLASHGATE_BOARD` to your
|
|
124
|
+
board yaml (or a repo `boards/` default is used when present). Without a
|
|
125
|
+
profile the gate stays idle and says so.
|
|
126
|
+
|
|
127
|
+
## MCP server
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{ "mcpServers": { "flashgate": {
|
|
131
|
+
"command": "flashgate-mcp",
|
|
132
|
+
"args": ["--board", "/path/to/boards/apollo-h743.yaml"] } } }
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
board_info, doctor, build, flash, verify, probe, console_send,
|
|
136
|
+
console_read. Any MCP-capable agent can drive the board directly. mcp 1.x
|
|
137
|
+
and 2.x supported.
|
|
138
|
+
|
|
139
|
+
## Demos
|
|
140
|
+
|
|
141
|
+
Real-hardware recordings, indexed in [demo/](demo/README.md): doctor,
|
|
142
|
+
green-path verify, boot-timeout catch, probe catching a silent no-op,
|
|
143
|
+
Stop-hook escalation — plus the full session of a real Claude Code agent
|
|
144
|
+
getting blocked, diagnosing the firmware↔profile contract, fixing both
|
|
145
|
+
sides, and passing on hardware ([24 MB GIF, release asset](https://github.com/Lion-1209/flashgate/releases/download/v0.3.0/5-agent-blocked.gif)).
|
|
146
|
+
|
|
147
|
+
## Board profiles
|
|
148
|
+
|
|
149
|
+
One yaml per board (`boards/`): build command, artifact, flash address,
|
|
150
|
+
serial adapter hints, banner template, probes, watch globs. The console-side
|
|
151
|
+
USB adapter is a property of your bench, not the board — port resolution
|
|
152
|
+
goes explicit `serial.port` / `FLASHGATE_SERIAL_PORT`, then VID/PID hint,
|
|
153
|
+
then the sole serial port, with the banner match as the final identity
|
|
154
|
+
proof. See the [guide](docs/GUIDE.md#6-固件怎么对接-flashgate) for the
|
|
155
|
+
firmware-side integration recipe (three levels, with code) and the
|
|
156
|
+
full profile field reference.
|
|
157
|
+
|
|
158
|
+
## Status
|
|
159
|
+
|
|
160
|
+
Boot gate, probe gate, Stop hook, MCP server, SWD signature channel — all
|
|
161
|
+
implemented and validated on real hardware. Windows-first; Linux/macOS
|
|
162
|
+
untested.
|
|
163
|
+
|
|
164
|
+
## License
|
|
165
|
+
|
|
166
|
+
MIT
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
# 中文说明
|
|
171
|
+
|
|
172
|
+
flashgate 回答一个很具体的问题:刚编译出来的固件,烧到板子上到底能
|
|
173
|
+
不能跑。它把整条链路自动化:编译、ST-Link 烧录、等板子报告身份、经
|
|
174
|
+
串口实际调用功能并断言,任何一步断了就以非零退出码结束。
|
|
175
|
+
|
|
176
|
+
完整的使用说明在 [docs/GUIDE.md](docs/GUIDE.md):接线、安装、第一次
|
|
177
|
+
验证、给自己的板子写档案、探针写法、Stop hook 和 MCP 的配置、排错。
|
|
178
|
+
|
|
179
|
+
要点:
|
|
180
|
+
|
|
181
|
+
- 板子有两种方式自证:串口 banner,或者在固定 RAM 地址发布 64 字节
|
|
182
|
+
签名(后者只靠 ST-Link 就能验证,不用串口线)
|
|
183
|
+
- 版本身份带 `-dirty` 语义,验证通过意味着板上跑的就是当前工作区
|
|
184
|
+
- 探针下真命令、断言硬件寄存器读回值;响应报的是实际状态不是回声,
|
|
185
|
+
静默失效的设置第一步就会露馅
|
|
186
|
+
- Stop hook 挂进 Claude Code:agent 改了固件没过真机验证就说"完成",
|
|
187
|
+
会被拦下并收到板子的失败证词;同一棵坏树最多拦两次,之后放行但
|
|
188
|
+
打警告,会话不会被卡死
|
|
189
|
+
- 仓库带完整的示例固件(examples/apollo-h743),接好线 clone 下来
|
|
190
|
+
就能跑通第一次验证
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
pip install -e ".[mcp]"
|
|
194
|
+
flashgate doctor # 体检
|
|
195
|
+
flashgate verify --all-probes # 完整验证
|
|
196
|
+
echo $?
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
六个真机演示 GIF 在 [demo/](demo/),包括一段完整的 Claude Code 会话:
|
|
200
|
+
agent 改固件、被拦、自己定位到固件与板卡档案的契约不一致、两侧改齐、
|
|
201
|
+
真机通过后放行。
|
|
202
|
+
|
|
203
|
+
MIT License
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
flashgate/__init__.py,sha256=Uz8PQ4_6aJyawXxFLxITQea35LHE6isOBD0WE-fIkBM,284
|
|
2
|
+
flashgate/__main__.py,sha256=E6Gls0DNz8GQK2K-kOUIx8cYhgANW_CH54VKrfCfs14,52
|
|
3
|
+
flashgate/board.py,sha256=g16zvmJcsoaaEUs6WdBplRZODEORPMZZn5kz5QhSgl4,4109
|
|
4
|
+
flashgate/cli.py,sha256=3fmUQl0xFPXRSYuSRm4IN7Gco_LsReffjbPWr3yq0Qk,18235
|
|
5
|
+
flashgate/flasher.py,sha256=7rezPunWcLWDWy7V6B308PZwNOOKCT0EKg6o_77oN2U,4619
|
|
6
|
+
flashgate/gatestate.py,sha256=nLVV_E20yaNd-TTjGV3At3dYKwWL73zAip6irN4dXw8,2733
|
|
7
|
+
flashgate/mcp_server.py,sha256=-YlNunexA8IC3lyeCVTJT5H2iaCK-Tvs7UpBLXQeqs8,7396
|
|
8
|
+
flashgate/probes.py,sha256=i0cld8iBIvgUb7uUa5nXvzDW4gQgoB_rZIN03_TKpfg,6358
|
|
9
|
+
flashgate/serialmon.py,sha256=cBNSVcH1jmBw0rhibin36l7fTtQqDLNwxJYll1JjWoA,3477
|
|
10
|
+
flashgate/sttools.py,sha256=dCP-25JYIZvKd1WtMdY-pO5qd-RBLLT_NqFLB_WakHo,1927
|
|
11
|
+
flashgate/swdsig.py,sha256=KqTKHeIREJLFfocSJn34fDTNx45VnrMf-KNYUQw3LaE,3036
|
|
12
|
+
flashgate-0.4.0.dist-info/licenses/LICENSE,sha256=M4DdcSR3Z1VwwpIziutxKWK7AIYkUuj9R_raDA-Uy5s,1073
|
|
13
|
+
flashgate-0.4.0.dist-info/METADATA,sha256=99gm-vtw1L64_bG_bmGa0iUiEUdkYIRESwxsMZiSLjQ,7498
|
|
14
|
+
flashgate-0.4.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
15
|
+
flashgate-0.4.0.dist-info/entry_points.txt,sha256=j7rNuE8toLqBj7qXuwfXhAI_xDy6Ta5_8FX2UKxS8CE,91
|
|
16
|
+
flashgate-0.4.0.dist-info/top_level.txt,sha256=yO2i60mrqfucJB7D8yIlLA27b4tNUaU0jGBGGBgFEU4,10
|
|
17
|
+
flashgate-0.4.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lion (Lion-1209)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
flashgate
|