Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Pages

on:
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages
cancel-in-progress: true

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Build Pages bundle
working-directory: apps/web
run: bun run build:pages

- name: Configure Pages
uses: actions/configure-pages@v5

- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: apps/web/.pages

deploy:
if: github.ref == 'refs/heads/main'
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ node_modules/
apps/cli/bin/
apps/cli/dist/
apps/web/dist/
apps/web/.pages/
playwright-report/
test-results/

# Capture files (large binary data)
captures/*.sal
Expand Down
20 changes: 13 additions & 7 deletions apps/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export interface GlobalFlags {
json: boolean;
quiet: boolean;
yes: boolean;
debug: boolean;
}

export interface ParsedArgs {
Expand Down Expand Up @@ -60,7 +61,7 @@ function isNegativeNumberToken(arg: string): boolean {
}

export function parseArgs(argv: string[]): ParsedArgs {
const global: GlobalFlags = { json: false, quiet: false, yes: false };
const global: GlobalFlags = { json: false, quiet: false, yes: false, debug: false };
const flags: Record<string, string | boolean> = {};
const positional: string[] = [];
let command: string | null = null;
Expand Down Expand Up @@ -100,6 +101,7 @@ export function parseArgs(argv: string[]): ParsedArgs {
if (name === "json") global.json = true;
else if (name === "quiet") global.quiet = true;
else if (name === "yes") global.yes = true;
else if (name === "debug") global.debug = true;
else flags[name] = value;
} else if (arg.startsWith("-") && arg.length > 1) {
// short flags: -y, -h, etc
Expand Down Expand Up @@ -196,7 +198,8 @@ export async function main(argv: string[]): Promise<number> {
if (parsed.flags.svo) format = "svo";
else if (parsed.flags.hex) format = "hex";
else if (parsed.flags.json || parsed.global.json) format = "json";
return await runRead(parsed.global, { format });
const debug = parsed.flags.debug === true;
return await runRead(parsed.global, { format, debug });
}

case "write": {
Expand Down Expand Up @@ -338,11 +341,14 @@ function printCommandHelp(command: string): void {
process.stdout.write(
`axon read — read the current config from the servo\n\n` +
`FLAGS:\n` +
` (default) Human-readable summary (named parameters not yet shown)\n` +
` --json Machine-readable JSON with raw bytes\n` +
` (default) Human-readable summary of decoded parameters\n` +
` --json Machine-readable JSON with decoded parameters\n` +
` --svo Raw 95-byte dump to stdout (vendor .svo compatible)\n` +
` --hex Annotated hex dump (for debugging)\n\n` +
`Example:\n` +
` --hex Annotated hex dump (for debugging)\n` +
` --debug Include raw block metadata in normal / --json output\n\n` +
`Examples:\n` +
` axon read\n` +
` axon read --json\n` +
` axon read --svo > backup.svo\n`,
);
break;
Expand All @@ -361,7 +367,7 @@ function printCommandHelp(command: string): void {
);
break;
case "get":
printGetHelp({ json: false, quiet: false, yes: false });
printGetHelp({ json: false, quiet: false, yes: false, debug: false });
break;
case "set":
process.stdout.write(
Expand Down
62 changes: 58 additions & 4 deletions apps/cli/src/commands/mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,7 @@ async function runModeSet(
await flashFirmware(handle, decrypted, {
expectedModelId: modelId ?? undefined,
onProgress: makeProgressSink(global),
onWireDebug: makeWireDebugSink(global),
cmdSleepMs: Number.isFinite(cmdSleepOverride) ? cmdSleepOverride : undefined,
});

Expand Down Expand Up @@ -589,18 +590,55 @@ function friendlyModeName(mode: ServoMode): string {
}

/** Returns true when AXON_DEBUG=1 is set. */
function isDebug(): boolean {
return process.env.AXON_DEBUG === "1";
function isDebug(global?: GlobalFlags): boolean {
return global?.debug === true || process.env.AXON_DEBUG === "1";
}

function timestampLabel(date: Date): string {
const core = date.toLocaleTimeString([], {
hour: "numeric",
minute: "2-digit",
second: "2-digit",
});
return `${core}.${date.getMilliseconds().toString().padStart(3, "0")}`;
}

function elapsedLabel(ms: number): string {
const seconds = (ms / 1000).toFixed(3);
return `+${seconds}s`;
}

function formatProgressDebugLine(event: FlashProgressEvent): string {
const parts = [`Flash ${event.phase}`];
if (typeof event.recordsSent === "number" && typeof event.recordsTotal === "number") {
parts.push(`${event.recordsSent}/${event.recordsTotal} records`);
}
if (typeof event.bytesSent === "number" && typeof event.bytesTotal === "number") {
parts.push(`${event.bytesSent}/${event.bytesTotal} bytes`);
}
if (event.message) {
parts.push(event.message);
}
return parts.join(" - ");
}

function makeProgressSink(global: GlobalFlags): FlashProgressFn {
if (global.json || global.quiet) return () => {};

const isTTY = process.stderr.isTTY === true;
const debug = isDebug(global);
let firstEventAt: number | null = null;
let started = false;
return (e: FlashProgressEvent) => {
if (isDebug()) {
process.stderr.write(` [debug] ${e.phase}: ${e.message ?? ""}\n`);
if (debug) {
const now = new Date();
const nowMs = now.getTime();
if (firstEventAt === null) {
firstEventAt = nowMs;
}
process.stderr.write(
`[${timestampLabel(now)} ${elapsedLabel(nowMs - firstEventAt)}] ${formatProgressDebugLine(e)}\n`,
);
return;
}
if (e.bytesSent !== undefined && e.bytesTotal !== undefined && e.bytesTotal > 0) {
Expand All @@ -615,5 +653,21 @@ function makeProgressSink(global: GlobalFlags): FlashProgressFn {
};
}

function makeWireDebugSink(global: GlobalFlags): ((message: string) => void) | undefined {
if (!isDebug(global)) return undefined;

let firstEventAt: number | null = null;
return (message: string) => {
const now = new Date();
const nowMs = now.getTime();
if (firstEventAt === null) {
firstEventAt = nowMs;
}
process.stderr.write(
`[${timestampLabel(now)} ${elapsedLabel(nowMs - firstEventAt)}] [wire] ${message}\n`,
);
};
}

export type { IdentifyReply, ServoModel };
export { loadCatalog, loadServoModes, parseModelId };
Loading
Loading