Skip to content
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht

### Added

* **`generate` runs until stopped when no `--count` is given, `cangen`-style (#477).** `canarchy generate <interface>` with no `--count` previously sent a single frame and exited, unlike the reference `cangen` tool from can-utils, which streams continuously by default. `--count` now defaults to unbounded: omitting it generates and transmits frames continuously, spaced by `--gap`, until interrupted (Ctrl-C / SIGINT); `--count N` keeps its existing exact-`N`-frames behaviour. Frame generation streams one frame at a time (`iter_generated_frames`, `LocalTransport.generate_stream_events`) rather than materializing an unbounded list, and the CLI dispatches an unbounded live run to a streaming emitter — mirroring the existing `capture` / `gateway --text` pattern — so both text and JSON/JSONL output stream per-frame and SIGINT exits cleanly (exit 0). `--dry-run` without `--count` plans a single frame rather than requiring or attempting an unbounded plan. Entry points that cannot deliver Ctrl-C, such as the MCP server, get a structured `MISSING_COUNT` error instead of hanging on an unbounded live run. Closes #477.

* **SEO and link-preview metadata for the published site (#479, #480, #483).** The homepage now ships Open Graph and Twitter Card tags with a generated 1200×630 brand card (`src/homepage/og-card.png`), a canonical URL, an inline SVG favicon, a `SoftwareApplication` JSON-LD block, and a keyword-forward `<title>`. The Pages build (`scripts/build_pages_site.sh`) additionally emits a root `robots.txt` referencing both the root and docs sitemaps and a root `sitemap.xml` covering the homepage, and publishes the social card. PyPI keywords in `pyproject.toml` expanded from 6 to 19 discovery terms. Closes #479, #480, #483.

* **`canarchy tui` is now a full-screen Textual app that streams the CAN bus live.** The TUI was a line-oriented shell that reprinted six panes after each command finished — it had no live/async streaming (`/capture` ran to completion then dumped a batch), no interactivity (no pane focus, filter, sort, row navigation, or scrollback), and hard-coded tiny backlogs. It is now a full-screen [Textual](https://textual.textualize.io/) application whose headline feature is watching the bus in real time. The engine/view split is preserved: the deterministic fold layer in `src/canarchy/tui.py` (`TuiState`, `_update_state`, and per-pane row extractors) stays as the tested model, a new `src/canarchy/tui_app.py` consumes it as the view, and a new `src/canarchy/tui_capture.py` `CaptureSession` streams frames through the existing `LocalTransport.capture_stream_events` generator on a daemon thread + queue (mirroring the transport gateway pattern), drained on the UI loop so one frame at a time flows through the same folding logic batch results use. Panes (Bus Status, Live Traffic, Decoded Signals, J1939 summary ribbon + table, UDS, an append-only Alerts log) are interactive Textual widgets: `/capture <iface>` starts a background live capture and `/stop` (or the `x` key) ends it; `/filter <pane> [text]` and `/sort <pane> [column]` filter and sort a pane; arrow keys navigate rows; `[`/`]` resize the operator-controlled backlog; `space` pauses the live feed; `c` clears; `ctrl+f` maximizes the focused pane. The command entry still runs real CANarchy commands and slash hotkeys through the shared parser, and the nested-front-end guard (`TUI_COMMAND_UNSUPPORTED`) is retained. `textual` is now a core dependency. **Breaking:** the interactive-only rewrite retires the text-mode shell and the one-shot `tui --command` mode; the TUI requires an interactive terminal and a non-TTY invocation prints guidance and exits non-zero — use the individual commands for scripted/automation runs. New Pilot-based app tests (`tests/test_tui_app.py`) and `CaptureSession` unit tests (`tests/test_tui_capture.py`) drive the scaffold backend's deterministic frame stream; the fold-layer snapshot tests are unchanged. A remaining follow-up: a finite-timeout `capture_stream` loop so live capture stops instantly on real hardware.
Expand Down
5 changes: 4 additions & 1 deletion docs/command_spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,12 +225,15 @@ Examples:
canarchy generate can0 --id 0x123 --dlc 4 --data 11223344 --count 2 --gap 100 --json
canarchy generate --id 0x123 --dlc 4 --data 11223344 --count 2 --dry-run --json
canarchy generate can0 --data I --count 4 --text
canarchy generate can0 # runs until Ctrl-C, cangen-style
```

Notes:

* `--id`, `--dlc`, and `--data` accept `R` for random generation, and `--data I` enables a deterministic incrementing payload pattern
* `--dry-run` emits the planned generated frame events without requiring an interface or opening a transport
* omitting `--count` generates and transmits frames continuously, spaced by `--gap`, until interrupted (Ctrl-C) — matching `cangen` from can-utils; passing `--count N` generates exactly `N` frames and exits
* `--dry-run` emits the planned generated frame events without requiring an interface or opening a transport; without `--count` a dry run plans a single frame
* invocations that cannot receive Ctrl-C (e.g. the MCP server) must pass `--count` explicitly for a live (non-dry-run) run, or a structured `MISSING_COUNT` error is returned
* `--ack-active` requests an interactive `YES` confirmation before generated frames are transmitted
* when active acknowledgement is required by configuration, omitting `--ack-active` returns a structured `ACTIVE_ACK_REQUIRED` error
* generated JSON output includes an active-transmit alert followed by generated frame events
Expand Down
8 changes: 6 additions & 2 deletions docs/design/generate-command.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ Operators need a quick active-traffic workflow for lab validation, replay suppor
|----|------|-------------|
| `REQ-GENERATE-01` | Ubiquitous | The system shall provide a `canarchy generate <interface>` command for active CAN frame generation. |
| `REQ-GENERATE-02` | Event-driven | When `generate` is invoked, the system shall support fixed, random (`R`), and incrementing (`I`) generation modes for identifiers and payloads as specified by `--id`, `--dlc`, and `--data`. |
| `REQ-GENERATE-03` | Event-driven | When `generate` is invoked, the system shall bound output by `--count` and space events by `--gap` milliseconds. |
| `REQ-GENERATE-03` | Event-driven | When `generate` is invoked with an explicit `--count`, the system shall bound output to that many frames and space events by `--gap` milliseconds. |
| `REQ-GENERATE-12` | Event-driven | When `generate` is invoked without `--count` from an interactive terminal, the system shall generate and transmit frames continuously, spaced by `--gap` milliseconds, until interrupted (Ctrl-C / SIGINT), matching `cangen`. |
| `REQ-GENERATE-13` | Unwanted behaviour | If `--dry-run` is supplied without `--count`, the system shall plan a single frame rather than requiring or attempting an unbounded plan. |
| `REQ-GENERATE-14` | Unwanted behaviour | If `generate` is invoked without `--count` and without `--dry-run` through a non-interactive entry point that cannot deliver Ctrl-C (e.g. the MCP server), the system shall return a structured error with code `MISSING_COUNT` and exit code 1 instead of generating indefinitely. |
| `REQ-GENERATE-04` | Event-driven | When `generate` is invoked and validation succeeds, the system shall emit a preflight active-transmit warning to `stderr`, emit a leading active-transmit alert event, and serialise the generated frame events. |
| `REQ-GENERATE-10` | Optional feature | Where `--ack-active` is supplied for `generate`, the system shall require a confirmation response of `YES` before generated frames are transmitted. |
| `REQ-GENERATE-11` | Optional feature | Where active acknowledgement is required, the system shall require `--ack-active` before generated frames are transmitted. |
Expand All @@ -48,7 +51,7 @@ canarchy generate <interface> [--id <hex|R>] [--dlc <0-8|R>] [--data <hex|R|I>]
| `--id` | `R` | Arbitration ID as hex or `R` for random |
| `--dlc` | `R` | Data length `0-8` or `R` for random |
| `--data` | `R` | Payload as hex, `R` for random bytes, or `I` for incrementing |
| `--count` | `1` | Number of frames to generate |
| `--count` | continuous | Number of frames to generate; omit to generate continuously until interrupted (Ctrl-C), matching `cangen`. `--dry-run` without `--count` plans a single frame. Invocations that cannot receive Ctrl-C (e.g. the MCP server) must pass `--count` explicitly. |
| `--gap` | `200` | Inter-frame gap in milliseconds |
| `--extended` | off | Force 29-bit extended arbitration IDs |
| `--ack-active` | off | Request an interactive confirmation prompt before generated frames are transmitted |
Expand Down Expand Up @@ -114,6 +117,7 @@ frames: 3
| `INVALID_FRAME_DATA` | `--data` is not valid hex, `R`, or `I` | 1 |
| `INVALID_COUNT` | `--count` is less than `1` | 1 |
| `INVALID_GAP` | `--gap` is less than `0` | 1 |
| `MISSING_COUNT` | `--count` was omitted for a live run reached through a non-interactive entry point (e.g. the MCP server) that cannot deliver Ctrl-C | 1 |
| `ACTIVE_ACK_REQUIRED` | active acknowledgement is required but `--ack-active` was omitted | 1 |
| `ACTIVE_CONFIRMATION_DECLINED` | `--ack-active` was supplied but the confirmation response was not `YES` | 1 |

Expand Down
43 changes: 43 additions & 0 deletions docs/tests/generate-command.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ Validate that `generate` produces deterministic frame sequences, emits active-tr
| `REQ-GENERATE-07` | `TEST-GENERATE-06` |
| `REQ-GENERATE-08` | `TEST-GENERATE-07` |
| `REQ-GENERATE-09` | `TEST-GENERATE-08` |
| `REQ-GENERATE-12` | `TEST-GENERATE-09` |
| `REQ-GENERATE-13` | `TEST-GENERATE-10` |
| `REQ-GENERATE-14` | `TEST-GENERATE-11` |

## Representative Test Cases

Expand Down Expand Up @@ -143,6 +146,46 @@ And `errors[0].code` shall equal `"INVALID_GAP"`

---

### `TEST-GENERATE-09` — Continuous generation without `--count`

```gherkin
Given the scaffold transport backend is active
When the operator runs `canarchy generate can0 --json` with no `--count`
Then the command shall stream generated frame events continuously
And the run shall stop cleanly with exit code `0` when interrupted (Ctrl-C / SIGINT)
```

**Fixture:** scaffold backend; `time.sleep` mocked to raise `KeyboardInterrupt` after a fixed number of frames.

---

### `TEST-GENERATE-10` — Dry run without `--count` plans a single frame

```gherkin
Given the scaffold transport backend is active
When the operator runs `canarchy generate can0 --dry-run --json` with no `--count`
Then the command shall succeed
And `data.frame_count` shall equal `1`
And no transport transmission shall occur
```

**Fixture:** scaffold backend (no file required).

---

### `TEST-GENERATE-11` — Live generation without `--count` through a non-interactive entry point

```gherkin
Given the MCP server invokes `generate` directly via `execute_command`, bypassing the interactive CLI's Ctrl-C dispatch
When the call requests a live run (`dry_run=false`) without `count`
Then the command shall exit with code `1`
And `errors[0].code` shall equal `"MISSING_COUNT"`
```

**Fixture:** none required.

---

## Fixtures And Environment

No dedicated fixture files are required. Tests exercise the command through the deterministic scaffold backend and CLI unit coverage.
Expand Down
10 changes: 9 additions & 1 deletion docs/tutorials/generate_and_capture.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ Terminal 1 prints:
### Extended (29-bit) IDs

```bash
canarchy generate 239.0.0.1 --id 0x18FEEE31 --dlc 8 --data R --extended
canarchy generate 239.0.0.1 --id 0x18FEEE31 --dlc 8 --data R --extended --count 1
```

### Control the gap
Expand All @@ -115,6 +115,14 @@ canarchy generate 239.0.0.1 --id 0x18FEEE31 --dlc 8 --data R --extended
canarchy generate 239.0.0.1 --count 10 --gap 50
```

### Run continuously, `cangen`-style

Omit `--count` to generate and transmit frames continuously, spaced by `--gap`, until you stop it with Ctrl-C:

```bash
canarchy generate 239.0.0.1 --gap 50
```

## Get Structured Output

Use `--json` instead of `--candump` to get machine-readable output:
Expand Down
89 changes: 87 additions & 2 deletions src/canarchy/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -604,7 +604,12 @@ def build_parser() -> CanarchyArgumentParser:
generate.add_argument(
"--data", default="R", help="payload hex, R for random, I for incrementing"
)
generate.add_argument("--count", type=int, default=1, help="number of frames to generate")
generate.add_argument(
"--count",
type=int,
default=None,
help="number of frames to generate; omit to generate continuously until interrupted (Ctrl-C)",
)
generate.add_argument(
"--gap", type=float, default=200.0, help="inter-frame gap in milliseconds"
)
Expand Down Expand Up @@ -3054,7 +3059,7 @@ def validate_args(args: argparse.Namespace) -> None:
data={"data": args.data},
)

if args.count < 1:
if args.count is not None and args.count < 1:
raise CommandError(
command=args.command,
exit_code=EXIT_USER_ERROR,
Expand All @@ -3068,6 +3073,12 @@ def validate_args(args: argparse.Namespace) -> None:
data={"count": args.count},
)

if args.count is None and getattr(args, "dry_run", False):
# Dry-run plans a fixed set of frames to display, so an unbounded
# generation request (the default with no `--count`) assumes a
# single-frame plan rather than requiring `--count` explicitly.
args.count = 1

if args.gap < 0:
raise CommandError(
command=args.command,
Expand Down Expand Up @@ -4577,6 +4588,26 @@ def transport_payload(
[],
)
if args.command == "generate":
if args.count is None:
# Continuous generation (no `--count`) streams frame-by-frame until
# Ctrl-C from `main()`'s CLI dispatch; callers that reach this
# batch/JSON-envelope path directly (e.g. the MCP server) have no
# way to interrupt an unbounded run, so require an explicit count.
raise CommandError(
command=args.command,
exit_code=EXIT_USER_ERROR,
errors=[
ErrorDetail(
code="MISSING_COUNT",
message="An explicit `--count` is required for this invocation.",
hint=(
"Pass `--count N`, or run `canarchy generate` from an "
"interactive terminal to stream continuously until Ctrl-C."
),
)
],
data={"count": None},
)
frames = generate_frames(
args.interface,
id_spec=args.id,
Expand Down Expand Up @@ -11425,6 +11456,47 @@ def emit_live_gateway(args: argparse.Namespace) -> int:
return EXIT_OK


def emit_live_generate(args: argparse.Namespace, output_format: str) -> int:
"""Stream generated frames until Ctrl+C — used when `generate` has no `--count`."""
transport = LocalTransport()
enforce_active_transmit_safety(args)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve structured acknowledgement failures

When --count is omitted, main() calls this emitter outside execute_command()'s exception wrapper. If active acknowledgement is required, or if --ack-active receives anything other than YES, enforce_active_transmit_safety() raises CommandError here before the try block and it escapes as a traceback instead of the documented ACTIVE_ACK_REQUIRED or ACTIVE_CONFIRMATION_DECLINED result envelope. Catch and emit this error at the streaming boundary so active-safety failures remain actionable and machine-readable.

AGENTS.md reference: AGENTS.md:L359-L370

Useful? React with 👍 / 👎.

text_mode = output_format == "text"
try:
for event in transport.generate_stream_events(
args.interface,
id_spec=args.id,
dlc_spec=args.dlc,
data_spec=args.data,
count=args.count,
gap_ms=args.gap,
extended=args.extended,
):
if text_mode:
if event.get("event_type") != "frame":
continue
frame = event["payload"]["frame"]
interface = frame["interface"] or args.interface
timestamp = event.get("timestamp")
timestamp_text = (
f"({timestamp:0.6f})" if isinstance(timestamp, (int, float)) else "(0.000000)"
)
print(f"{timestamp_text} {interface} {format_candump_frame(frame)}")
else:
print(json.dumps(event, sort_keys=True))
except TransportError as exc:
emit_result(
error_result(
"generate",
errors=[ErrorDetail(code=exc.code, message=exc.message, hint=exc.hint)],
),
output_format,
)
return EXIT_TRANSPORT_ERROR
except KeyboardInterrupt:
return EXIT_OK
return EXIT_OK


def emit_dataset_stream(args: argparse.Namespace) -> int:
from canarchy.dataset_convert import ConversionError, stream_file

Expand Down Expand Up @@ -12294,6 +12366,19 @@ def main(argv: Sequence[str] | None = None) -> int:
and not getattr(args, "dry_run", False)
):
return emit_live_gateway(args)
if args.command == "generate" and args.count is None and not getattr(args, "dry_run", False):
# This dispatch bypasses `execute_command`'s batch path (and its
# `validate_args` call), so validate here to catch a bad `--id`,
# `--dlc`, `--data`, or `--gap` before an unbounded stream starts.
try:
validate_args(args)
except CommandError as exc:
emit_result(
error_result(exc.command, errors=exc.errors, data=exc.data, warnings=exc.warnings),
output_format,
)
return exc.exit_code
return emit_live_generate(args, output_format)
if args.command == "datasets stream" and not args.json:
return emit_dataset_stream(args)
if (
Expand Down
6 changes: 5 additions & 1 deletion src/canarchy/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,11 @@
},
"count": {
"type": "integer",
"description": "Number of frames to generate",
"description": (
"Number of frames to generate. Defaults to 1 for a dry-run plan; "
"required (and must be finite) when `dry_run=false`, since this "
Comment on lines 100 to +104

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Document the changed MCP generate contract

Update both AGENTS.md and docs/agents.md for this changed MCP workflow: a live generate call without count now returns MISSING_COUNT, whereas the existing agent guide only says active tools default to dry-run and does not tell agents that count becomes mandatory when they opt into dry_run=false. This command-surface and MCP-tool change does not meet the repository's required agent-guide acceptance gate.

AGENTS.md reference: AGENTS.md:L82-L90

Useful? React with 👍 / 👎.

"tool cannot interrupt an unbounded live run."
),
"default": 1,
},
"gap": {
Expand Down
Loading
Loading