fix: support device-pod flash-vector launch
This commit is contained in:
@@ -38,6 +38,12 @@ device pod, debug probe control, or I/O probe reads such as UART boot logs.
|
||||
- `device-host-cli` is expected to be self-contained on the user PC. Keil,
|
||||
serial-monitor, mklink, and other skills can be implementation references, but
|
||||
are not runtime dependencies of this HWLAB runner skill.
|
||||
- `device-host-cli` is not just a copied preinstall script. It is the hot
|
||||
development tool that HWLAB internal code agents, including DeepSeek-backed
|
||||
runners, may keep improving on the target Windows workspace when hardware,
|
||||
Keil, probe, UART, or filesystem operations are not smooth enough. Hot fixes
|
||||
must be validated through the real device-pod/gateway route first, then copied
|
||||
back into the HWLAB repo asset for the next CI/CD preinstall.
|
||||
|
||||
## Windows Host CLI Bootstrap
|
||||
|
||||
@@ -77,6 +83,26 @@ After that, `device-pod-cli` should use the profile's `hostCli`, normally
|
||||
`node tools\device-host-cli.mjs`, and should not depend on any Windows skill
|
||||
directory at runtime.
|
||||
|
||||
## Host CLI Hot Development
|
||||
|
||||
When a profiled operation is awkward or missing, prefer improving the Windows
|
||||
workspace copy of `tools\device-host-cli.mjs` instead of bypassing the device-pod
|
||||
model with arbitrary cmd snippets. The intended loop is:
|
||||
|
||||
```text
|
||||
edit tools\device-host-cli.mjs in the Windows device-pod workspace
|
||||
node tools\device-host-cli.mjs health
|
||||
node tools\device-host-cli.mjs <new named operation>
|
||||
node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs <devicePodId>:... <same operation>
|
||||
copy the validated change back to skills/device-pod-cli/assets/device-host-cli.mjs
|
||||
```
|
||||
|
||||
The host CLI should stay self-contained and named-operation oriented: add
|
||||
`debug-probe launch-flash` or `io-probe uart/1 read-after-launch-flash` style
|
||||
device semantics, not a generic shell escape. If gateway/cmd transfer itself is
|
||||
the friction, fix `tran.mjs` or the gateway transport separately instead of
|
||||
embedding transport workarounds into `device-host-cli`.
|
||||
|
||||
## Commands
|
||||
|
||||
All commands emit JSON and must remain short-running. Use `build start` /
|
||||
@@ -93,14 +119,17 @@ node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs device-pod-71-freq:wo
|
||||
node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs device-pod-71-freq:debug-probe chip-id
|
||||
node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs device-pod-71-freq:debug-probe download start --approved --reason "DEV smoke"
|
||||
node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs device-pod-71-freq:debug-probe download status <jobId>
|
||||
node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs device-pod-71-freq:debug-probe launch-flash --approved --reason "manual flash-vector launch"
|
||||
node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs device-pod-71-freq:io-probe:/uart/1 ports
|
||||
node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs device-pod-71-freq:io-probe:/uart/1 read
|
||||
node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs device-pod-71-freq:io-probe:/uart/1 read --duration-ms 5000
|
||||
node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs device-pod-71-freq:io-probe:/uart/1 read-after-launch-flash --approved --reason "boot log capture"
|
||||
```
|
||||
|
||||
## Safety
|
||||
|
||||
- Mutating operations such as workspace patching, download, reset, and I/O write
|
||||
require `--approved --reason <text>` before the CLI dispatches anything.
|
||||
- Mutating operations such as workspace patching, download, reset,
|
||||
`launch-flash`, `read-after-launch-flash`, and I/O write require
|
||||
`--approved --reason <text>` before the CLI dispatches anything.
|
||||
- `io-probe:/inner/...` is intentionally distinct from external physical I/O
|
||||
paths such as `io-probe:/uart/1`; do not use internal target state as evidence
|
||||
for external UART/GPIO observations.
|
||||
|
||||
@@ -555,6 +555,29 @@ async function pyocdArgs(profile, commandArgs) {
|
||||
return { pyocd: tools.pyocd.path, args };
|
||||
}
|
||||
|
||||
async function pyocdCommanderArgs(profile, commands, options = {}) {
|
||||
const tools = await toolInfo(profile);
|
||||
if (!tools.pyocd.found) throw new Error('pyOCD executable not found');
|
||||
const debug = profile.debugInterface || {};
|
||||
const args = ['commander'];
|
||||
if (debug.probeUid) args.push('-u', debug.probeUid);
|
||||
if (debug.pyocdTarget) args.push('-t', debug.pyocdTarget);
|
||||
const connectMode = options['connect-mode'] || options.connectMode || debug.launchConnectMode || 'attach';
|
||||
if (connectMode) args.push('-M', connectMode);
|
||||
if (debug.frequency) args.push('-f', debug.frequency);
|
||||
args.push('-W');
|
||||
for (const command of commands) args.push('-c', command);
|
||||
return { pyocd: tools.pyocd.path, args };
|
||||
}
|
||||
|
||||
function parseFlashVectors(text, flashBase) {
|
||||
const normalizedBase = String(flashBase || '').toLowerCase().replace(/^0x/u, '');
|
||||
const escapedBase = normalizedBase.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const match = String(text || '').toLowerCase().match(new RegExp(`${escapedBase}\\s*:\\s*([0-9a-f]{8})\\s+([0-9a-f]{8})`, 'u'));
|
||||
if (!match) throw new Error(`unable to parse flash vectors at ${flashBase}`);
|
||||
return { initialSp: `0x${match[1]}`, resetHandler: `0x${match[2]}` };
|
||||
}
|
||||
|
||||
async function debugStatus() {
|
||||
const profile = loadProfile();
|
||||
const tools = await toolInfo(profile);
|
||||
@@ -584,6 +607,45 @@ async function resetRun() {
|
||||
ok('debug-probe.reset', { exitCode: result.exitCode, success: result.exitCode === 0, stdout: result.stdout, stderr: result.stderr });
|
||||
}
|
||||
|
||||
async function launchFlash(options = {}) {
|
||||
const profile = loadProfile();
|
||||
const flashBase = String(options['flash-base'] || options.flashBase || profile.debugInterface?.flashBase || profile.target?.flashBase || '0x08000000');
|
||||
const skipHardwareReset = Boolean(options['skip-hardware-reset'] || options.skipHardwareReset);
|
||||
const prepareCommand = skipHardwareReset ? 'halt' : 'reset halt HARDWARE';
|
||||
const timeoutMs = Number(options['timeout-ms'] || options.timeoutMs || 60000);
|
||||
const read = await pyocdCommanderArgs(profile, [prepareCommand, `read32 ${flashBase} 8`], options);
|
||||
const vectorResult = await runCapture(read.pyocd, read.args, { timeoutMs });
|
||||
if (vectorResult.exitCode !== 0) throw new Error(`pyOCD vector read failed: ${shortTail(vectorResult.stdout || vectorResult.stderr)}`);
|
||||
const vectorText = `${vectorResult.stdout}\n${vectorResult.stderr}`;
|
||||
const vectors = parseFlashVectors(vectorText, flashBase);
|
||||
const launchCommands = [
|
||||
prepareCommand,
|
||||
`write32 0xE000ED08 ${flashBase}`,
|
||||
`wreg sp ${vectors.initialSp}`,
|
||||
`wreg pc ${vectors.resetHandler}`,
|
||||
'wreg xpsr 0x01000000',
|
||||
'wreg primask 0',
|
||||
'wreg faultmask 0',
|
||||
'wreg basepri 0',
|
||||
'go',
|
||||
];
|
||||
const launch = await pyocdCommanderArgs(profile, launchCommands, options);
|
||||
const launchResult = await runCapture(launch.pyocd, launch.args, { timeoutMs });
|
||||
ok('debug-probe.launch-flash', {
|
||||
success: launchResult.exitCode === 0,
|
||||
exitCode: launchResult.exitCode,
|
||||
flashBase,
|
||||
vectorTableRegister: '0xE000ED08',
|
||||
prepareCommand,
|
||||
skipHardwareReset,
|
||||
initialSp: vectors.initialSp,
|
||||
resetHandler: vectors.resetHandler,
|
||||
stdout: launchResult.stdout,
|
||||
stderr: launchResult.stderr,
|
||||
vectorRead: { exitCode: vectorResult.exitCode, stdout: vectorResult.stdout, stderr: vectorResult.stderr },
|
||||
});
|
||||
}
|
||||
|
||||
async function serialPorts() {
|
||||
const script = "Get-CimInstance Win32_SerialPort | Select-Object DeviceID,Name,PNPDeviceID | ConvertTo-Json -Compress";
|
||||
const result = await runPowerShell(script, 15000);
|
||||
@@ -627,6 +689,73 @@ async function serialRead(uartId, options) {
|
||||
ok('io-probe.uart.read', { uart, durationMs, exitCode: result.exitCode, content, bytes: Buffer.byteLength(content), logFile, stderr: result.stderr });
|
||||
}
|
||||
|
||||
async function serialReadAfterLaunchFlash(uartId, options) {
|
||||
const profile = loadProfile();
|
||||
const uart = resolveUart(profile, uartId);
|
||||
const tools = await toolInfo(profile);
|
||||
if (!tools.pyocd.found) throw new Error('pyOCD executable not found');
|
||||
const debug = profile.debugInterface || {};
|
||||
const durationMs = Number(options['duration-ms'] || options.durationMs || 5000);
|
||||
const flashBase = String(options['flash-base'] || options.flashBase || debug.flashBase || profile.target?.flashBase || '0x08000000');
|
||||
const skipHardwareReset = Boolean(options['skip-hardware-reset'] || options.skipHardwareReset);
|
||||
const prepareCommand = skipHardwareReset ? 'halt' : 'reset halt HARDWARE';
|
||||
const connectMode = options['connect-mode'] || options.connectMode || debug.launchConnectMode || 'attach';
|
||||
const script = [
|
||||
'$ErrorActionPreference = \'Stop\';',
|
||||
`$portName = ${psQuote(uart.port)};`,
|
||||
`$baud = ${Number(uart.baudRate || 115200)};`,
|
||||
`$durationMs = ${durationMs};`,
|
||||
`$pyocd = ${psQuote(tools.pyocd.path)};`,
|
||||
`$uid = ${psQuote(debug.probeUid || '')};`,
|
||||
`$target = ${psQuote(debug.pyocdTarget || '')};`,
|
||||
`$frequency = ${psQuote(debug.frequency || '')};`,
|
||||
`$connectMode = ${psQuote(connectMode || '')};`,
|
||||
`$flashBase = ${psQuote(flashBase)};`,
|
||||
`$prepareCommand = ${psQuote(prepareCommand)};`,
|
||||
'$common = @(\'commander\');',
|
||||
'if ($uid) { $common += @(\'-u\', $uid) }',
|
||||
'if ($target) { $common += @(\'-t\', $target) }',
|
||||
'if ($connectMode) { $common += @(\'-M\', $connectMode) }',
|
||||
'if ($frequency) { $common += @(\'-f\', $frequency) }',
|
||||
'$common += @(\'-W\');',
|
||||
'$port = [System.IO.Ports.SerialPort]::new($portName, $baud, [System.IO.Ports.Parity]::None, 8, [System.IO.Ports.StopBits]::One);',
|
||||
'$port.Encoding = [System.Text.UTF8Encoding]::new($false);',
|
||||
'$port.ReadTimeout = 200;',
|
||||
'try {',
|
||||
' $port.Open();',
|
||||
' $port.DiscardInBuffer();',
|
||||
' $port.Close();',
|
||||
' $vectorOutput = & $pyocd @common -c $prepareCommand -c "read32 $flashBase 8" 2>&1;',
|
||||
' $vectorExitCode = $LASTEXITCODE;',
|
||||
' $vectorText = ($vectorOutput | Out-String);',
|
||||
' if ($vectorExitCode -ne 0) { throw "pyOCD vector read failed: $vectorText" }',
|
||||
' $escapedBase = [regex]::Escape($flashBase.ToLower().Replace("0x", ""));',
|
||||
' $match = [regex]::Match($vectorText.ToLower(), "$escapedBase\\s*:\\s*([0-9a-f]{8})\\s+([0-9a-f]{8})");',
|
||||
' if (-not $match.Success) { throw "Unable to parse flash vectors from pyOCD output: $vectorText" }',
|
||||
' $initialSp = "0x" + $match.Groups[1].Value;',
|
||||
' $resetHandler = "0x" + $match.Groups[2].Value;',
|
||||
' $launchOutput = & $pyocd @common -c $prepareCommand -c "write32 0xE000ED08 $flashBase" -c "wreg sp $initialSp" -c "wreg pc $resetHandler" -c "wreg xpsr 0x01000000" -c "wreg primask 0" -c "wreg faultmask 0" -c "wreg basepri 0" -c "go" 2>&1;',
|
||||
' $launchExitCode = $LASTEXITCODE;',
|
||||
' $launchText = ($launchOutput | Out-String);',
|
||||
' if ($launchExitCode -ne 0) { throw "pyOCD flash-vector launch failed: $launchText" }',
|
||||
' $port.Open();',
|
||||
' Start-Sleep -Milliseconds $durationMs;',
|
||||
' $text = $port.ReadExisting();',
|
||||
' $bytes = [System.Text.Encoding]::UTF8.GetBytes($text);',
|
||||
' [pscustomobject]@{ contentB64 = [Convert]::ToBase64String($bytes); flashBase = $flashBase; vectorTableRegister = "0xE000ED08"; prepareCommand = $prepareCommand; initialSp = $initialSp; resetHandler = $resetHandler; vectorExitCode = $vectorExitCode; launchExitCode = $launchExitCode; vectorStdout = $vectorText; launchStdout = $launchText } | ConvertTo-Json -Depth 5 -Compress | Write-Output;',
|
||||
'} finally { if ($port.IsOpen) { $port.Close() }; $port.Dispose() }',
|
||||
].join(' ');
|
||||
const result = await runPowerShell(script, Math.max(15000, durationMs + 20000));
|
||||
if (result.exitCode !== 0) throw new Error(`uart read-after-launch-flash failed: ${shortTail(result.stdout || result.stderr)}`);
|
||||
const payload = JSON.parse(result.stdout.trim());
|
||||
const content = payload.contentB64 ? Buffer.from(payload.contentB64, 'base64').toString('utf8') : '';
|
||||
const logDir = path.join(stateRoot(profile.__workspaceRoot), 'serial');
|
||||
ensureDir(logDir);
|
||||
const logFile = path.join(logDir, `${uart.id.replace(/\//g, '_')}.jsonl`);
|
||||
fs.appendFileSync(logFile, `${JSON.stringify({ ts: nowIso(), direction: 'read-after-launch-flash', uartId: uart.id, port: uart.port, baudRate: uart.baudRate, content })}\n`, 'utf8');
|
||||
ok('io-probe.uart.read-after-launch-flash', { uart, durationMs, exitCode: result.exitCode, content, bytes: Buffer.byteLength(content), logFile, stderr: result.stderr, launch: { flashBase: payload.flashBase, vectorTableRegister: payload.vectorTableRegister, prepareCommand: payload.prepareCommand, initialSp: payload.initialSp, resetHandler: payload.resetHandler, vectorExitCode: payload.vectorExitCode, launchExitCode: payload.launchExitCode, vectorStdout: payload.vectorStdout, launchStdout: payload.launchStdout } });
|
||||
}
|
||||
|
||||
async function serialWrite(uartId, options) {
|
||||
const profile = loadProfile();
|
||||
const uart = resolveUart(profile, uartId);
|
||||
@@ -654,8 +783,8 @@ function usage() {
|
||||
'node tools/device-host-cli.mjs workspace rg <pattern> [path]',
|
||||
'node tools/device-host-cli.mjs workspace apply-patch [base] --patch-b64 <base64>',
|
||||
'node tools/device-host-cli.mjs workspace build start|status [jobId]',
|
||||
'node tools/device-host-cli.mjs debug-probe status|chip-id|bind|download|reset',
|
||||
'node tools/device-host-cli.mjs io-probe uart/1 ports|read|write [args]'
|
||||
'node tools/device-host-cli.mjs debug-probe status|chip-id|bind|download|reset|launch-flash',
|
||||
'node tools/device-host-cli.mjs io-probe uart/1 ports|read|read-after-launch-flash|write [args]'
|
||||
]
|
||||
});
|
||||
}
|
||||
@@ -688,12 +817,14 @@ async function main() {
|
||||
if (sub === 'status') return readJobStatus('keil-download', rest[1]);
|
||||
}
|
||||
if (command === 'reset') return await resetRun();
|
||||
if (command === 'launch-flash') return await launchFlash(parseArgs(rest));
|
||||
}
|
||||
if (group === 'io-probe') {
|
||||
const uartId = command;
|
||||
const sub = rest[0] || 'read';
|
||||
if (sub === 'ports') return await serialPorts();
|
||||
if (sub === 'read') return await serialRead(uartId, parseArgs(rest.slice(1)));
|
||||
if (sub === 'read-after-launch-flash') return await serialReadAfterLaunchFlash(uartId, parseArgs(rest.slice(1)));
|
||||
if (sub === 'write') return await serialWrite(uartId, parseArgs(rest.slice(1)));
|
||||
}
|
||||
throw new Error(`unsupported command: ${process.argv.slice(2).join(' ')}`);
|
||||
|
||||
Reference in New Issue
Block a user