# Connect an agent Source: https://docs.veecle.ai/agents/overview Connect a coding agent to Chiplab over MCP. Setup steps for the clients people use most, and what to do when yours is not listed. Chiplab exposes its simulation capabilities through the Model Context Protocol (MCP). Once your agent is connected, it can call tools like `ask` and `run` directly inside a chat or terminal session, no manual API calls, no context switching, no physical board required. ## How it works Chiplab acts as an MCP server; your coding agent acts as the MCP client. When you send a message that involves firmware simulation, your agent automatically discovers the available tools and calls them on your behalf. Authentication happens once via a browser-based OAuth flow; you never need to copy tokens or manage credentials manually. ## MCP server URL Chiplab's MCP server is available at `https://chiplab.veecle.ai/mcp`. This URL is public and requires no special handling. Authentication happens separately, through your browser session. ## Choose your agent These are the clients with setup steps. Open a card for that client's clicks and config. Every client connects to the same server and signs in once through your browser. Install with one click, or add the server to `mcp.json`. Add Chiplab as a remote server in OpenCode's config. Add Chiplab with one `claude mcp add` command. Add Chiplab as a custom connector. Install with one click, or add the server to `.vscode/mcp.json`. Add Chiplab with one `codex mcp add` command. ## Other clients Any client with remote HTTP MCP and OAuth uses `https://chiplab.veecle.ai/mcp`. A client that only speaks local stdio needs its remote-connector screen. Claude Desktop is that case: its JSON config does not accept this URL. If your client cannot connect and you need it supported, tell us which one on [Discord](https://discord.com/invite/F6GwZJ6ktP) or [open a request](https://github.com/veecle/chiplab/issues/new/choose). ## Managing connected sessions After you complete the authentication flow, your agent session appears on the [API Keys](/platform/api-keys) page of the Chiplab dashboard under **Connected agents**. From there you can: * See when the agent first connected and when it was last active. * **Revoke** any session instantly; the agent loses access immediately and must re-authenticate to reconnect. Revoking is useful when you rotate machines, hand off a project, or suspect a session has been exposed. # Changelog Source: https://docs.veecle.ai/changelog What's new in Chiplab. You can now test firmware with a TypeScript suite and get a pass or fail for each test. A failing test can fail the CI job, so the suite works as a gate on a pull request. The checks you can write are on [test](/tools/test), and the workflow edits are on [Test in CI](/use-cases/ci-on-every-pr). # Core concepts Source: https://docs.veecle.ai/concepts How Chiplab fits together: you compile the firmware, your agent runs it on a virtual board, and you read what the chip did. You compile the firmware. Chiplab runs that ELF on a virtual board and reports what the chip did. You do not drive the simulator. Your coding agent does. The board is not a generic CPU. It runs your exact binary against a model of the target's peripherals, memory map, and interrupt controller, so the firmware behaves the way it would on the real chip. There is no hardware on the desk. The simulator today is [Renode](https://renode.io). [QEMU](https://www.qemu.org) is planned. A session is a few objects the agent passes along. A project holds the work. An artifact is the ELF in that project. A run executes one artifact on a board. A test checks that same firmware and returns a pass or fail. ## The board A board is a board model plus the UARTs to capture. Only the UARTs you name are written into the output. Name the UART the firmware writes, or the capture comes back empty. Models and UART names are on [Boards](/hardware/boards). Framework coverage is on [Frameworks](/hardware/frameworks). Peripheral coverage keeps growing. If your firmware depends on a peripheral or timing behavior Chiplab doesn't model yet, ask Chiplab for the current coverage of your board. ## The pieces A project is where uploads and runs live. The next session can pick up the same work, and everyone with access to the project can see it. The calls that create, rename, and delete a project are on [run](/tools/run). An artifact is the compiled ELF your agent uploads into that project. Cross-compilation happens on your side. Chiplab runs the ELF as-is. A run is one execution of one artifact on one board. The captured output is `uart`. The full call is on [run](/tools/run). A test is a TypeScript suite that drives the same firmware on a virtual board. You get a pass or fail for each test. The full call is on [test](/tools/test). ## What comes back A run lasts up to 5 seconds of virtual time. `succeeded` means Chiplab completed the run. It does not mean your firmware behaved: a firmware that panics or faults still returns `succeeded`. Check the `uart` output for what you expect. Use a run when you want the printed output. Use a test when you need a pass or fail. For a test, `succeeded` means the suite ran, not that every test passed. Check `result.summary`. ## What Chiplab keeps Every run deposits observed behavior into the [knowledge corpus](/introduction#knowledge-corpus), indexed by chip family, failure pattern, and board configuration. Your firmware code and binaries are not part of it. [ask](/tools/ask) reads that corpus. Your agent uses it for board facts, gotchas, and how Chiplab itself works. ## How a session moves You describe the work in plain language. The agent picks the tool, waits, and reports what the virtual chip did. 1. [ask](/tools/ask), at the start of a session and whenever something is board-specific. For example, "Which UART does stm32f4\_discovery write to?" or "Why is my captured output empty?" 2. [run](/tools/run) uploads the ELF, executes it, and returns the UART you asked to capture. 3. [test](/tools/test), when you need a pass or fail for each test. 4. A pipeline repeats that loop. It authenticates with a [personal access token](/platform/api-keys), then asks, produces an ELF, uploads and runs it, and fails unless the captured UART matches what you expect. Setup is on [Test in CI](/use-cases/ci-on-every-pr). Run your first simulation in under five minutes. Look up boards, tools, and firmware patterns. Execute an ELF on a virtual board and read the UART. Get a pass or fail for each test. Repeat the loop from a pipeline with a token. STM32 and Nordic boards, and the UART each model can capture. # Boards Source: https://docs.veecle.ai/hardware/boards The STM32 and Nordic boards Chiplab simulates, and the UARTs each one exposes. Chiplab boots a virtual instance of each supported chip on open simulation platforms. Today that means [Renode](https://renode.io), with [QEMU](https://www.qemu.org) and more support planned as coverage grows. It captures the UARTs your agent asks for. `run` takes a `board` object: ```json theme={null} { "model": "stm32f4_discovery", "uarts": ["usart2"] } ``` The board model is the `model` value your agent passes in `board`. `uarts` is required. It is the list of peripherals to capture. Only those UARTs are written into the run's `uart` artifact. Requesting a UART the board does not expose is rejected. See which frameworks have an example for each board on [Frameworks](/hardware/frameworks).
| Board | Chip | UART | Available UARTs | | - | - | - | - | | STM32F4 Discovery | STM32F407 | `usart2` | `usart1`, `usart2`, `usart3`, `uart4`, `uart5` | | STM32F7 Discovery | STM32F746 | `usart1` `usart2` | `usart1`, `usart2`, `usart3`, `usart6` | | STM32F103 Blue Pill | STM32F103 | `usart2` | `usart1`, `usart2`, `usart3`, `usart4`, `usart5` | | STM32WBA52 Nucleo | STM32WBA52 | `usart1` | `usart1`, `usart2`, `lpuart1` | | STM32L073 Nucleo | STM32L073 | `usart2` | `usart1`, `usart2`, `usart4`, `usart5`, `lpuart1` | | STM32H745 Nucleo | STM32H745 | `usart3` | `usart1`, `usart2`, `usart3`, `uart4`, `uart5`, `usart6`, `uart7`, `uart8`, `lpuart1` | | nRF52840 DK | nRF52840 | `uart0` | `uart0`, `uart1` |
The UART column is the peripheral the public examples write. Pass that name in `uarts`. Any name in Available UARTs can go in `uarts`. ## What simulation covers Each board runs on a chip-accurate model of its microcontroller: the CPU, the on-chip peripherals (UART, GPIO, and the rest of the chip's peripheral registers) and the interrupt controller. Your firmware drives them exactly as it would on the real chip. Each run executes the ELF and captures the UARTs named in `board.uarts`. A run lasts up to 5 seconds of virtual time. Only the chip itself is modelled. External devices wired to it, such as sensors or I2C/SPI parts, are not simulated yet. Your firmware and tests observe the board through UART output. ## Don't see your chip? Next on the roadmap: NXP S32, Renesas RA, and Infineon AURIX. Please don't hesitate to ask. Requests really do shape what we add next. [Open a request](https://github.com/veecle/chiplab/issues/new/choose) on GitHub with your target MCU and use case, and our team will review it, or come say hi on [Discord](https://discord.com/invite/F6GwZJ6ktP). Call Chiplab's discovery/help tool (`ask`) to confirm the current, authoritative set of boards. It may be ahead of what's listed here. # Frameworks Source: https://docs.veecle.ai/hardware/frameworks The OS and framework combinations Chiplab runs, and which boards have an example. Chiplab runs the same firmware pattern across five OS/framework combinations: * **`bare-metal`** — vendor-HAL / direct-register Rust firmware (no async runtime). * **`embassy-rust`** — the same firmware on the [Embassy](https://embassy.dev) async runtime. * **`zephyr-os`** — the same firmware on the [Zephyr RTOS](https://zephyrproject.org) (C, built with `west`). * **`freertos`** — the same firmware on the [FreeRTOS](https://www.freertos.org) kernel (C, built with `make` + `arm-none-eabi-gcc`). * **`threadx`** — the same firmware on the [Eclipse ThreadX](https://github.com/eclipse-threadx/threadx) kernel (C, built with `make` + `arm-none-eabi-gcc`). ## Board matrix Every board Chiplab supports today, and which frameworks have an example. The board and the UARTs it can capture are on [Boards](/hardware/boards). A checkmark links to a ready-to-run example for that OS/framework (— = not available yet). The canonical copy of this framework matrix lives in [supported-boards.md](https://github.com/veecle/chiplab/blob/main/supported-boards.md); if the two ever diverge, that file is authoritative for which examples exist. The `model` values are what the simulator accepts; call `ask` if this page and the live platform ever disagree.
| Board | bare-metal | embassy-rust | zephyr-os | freertos | threadx | | - | - | - | - | - | - | | STM32F4 Discovery | [✓](https://github.com/veecle/chiplab/tree/main/examples/bare-metal/stm32f4-discovery) | [✓](https://github.com/veecle/chiplab/tree/main/examples/embassy-rust/stm32f4-discovery) | [✓](https://github.com/veecle/chiplab/tree/main/examples/zephyr-os/stm32f4-discovery) | [✓](https://github.com/veecle/chiplab/tree/main/examples/freertos/stm32f4-discovery) | [✓](https://github.com/veecle/chiplab/tree/main/examples/threadx/stm32f4-discovery) | | STM32F7 Discovery | [✓](https://github.com/veecle/chiplab/tree/main/examples/bare-metal/stm32f7-discovery) | [✓](https://github.com/veecle/chiplab/tree/main/examples/embassy-rust/stm32f7-discovery) | [✓](https://github.com/veecle/chiplab/tree/main/examples/zephyr-os/stm32f7-discovery) | [✓](https://github.com/veecle/chiplab/tree/main/examples/freertos/stm32f7-discovery) | — | | STM32F103 Blue Pill | [✓](https://github.com/veecle/chiplab/tree/main/examples/bare-metal/stm32f103-blue-pill) | [✓](https://github.com/veecle/chiplab/tree/main/examples/embassy-rust/stm32f103-blue-pill) | — | [✓](https://github.com/veecle/chiplab/tree/main/examples/freertos/stm32f103-blue-pill) | — | | STM32WBA52 Nucleo | — | [✓](https://github.com/veecle/chiplab/tree/main/examples/embassy-rust/stm32wba52-nucleo) | — | — | — | | STM32L073 Nucleo | [✓](https://github.com/veecle/chiplab/tree/main/examples/bare-metal/stm32l073-nucleo) | [✓](https://github.com/veecle/chiplab/tree/main/examples/embassy-rust/stm32l073-nucleo) | [✓](https://github.com/veecle/chiplab/tree/main/examples/zephyr-os/stm32l073-nucleo) | [✓](https://github.com/veecle/chiplab/tree/main/examples/freertos/stm32l073-nucleo) | — | | STM32H745 Nucleo | [✓](https://github.com/veecle/chiplab/tree/main/examples/bare-metal/stm32h745-nucleo) | — | [✓](https://github.com/veecle/chiplab/tree/main/examples/zephyr-os/stm32h745-nucleo) | [✓](https://github.com/veecle/chiplab/tree/main/examples/freertos/stm32h745-nucleo) | — | | nRF52840 DK | [✓](https://github.com/veecle/chiplab/tree/main/examples/bare-metal/nrf52840-dk) | [✓](https://github.com/veecle/chiplab/tree/main/examples/embassy-rust/nrf52840-dk) | [✓](https://github.com/veecle/chiplab/tree/main/examples/zephyr-os/nrf52840-dk) | [✓](https://github.com/veecle/chiplab/tree/main/examples/freertos/nrf52840-dk) | — |
Call Chiplab's discovery/help tool (`ask`) to confirm the current, authoritative set of boards — it may be ahead of what's listed here. # Introduction Source: https://docs.veecle.ai/introduction Chiplab is an MCP platform that gives AI coding agents direct access to chip-accurate firmware simulation, without any physical hardware. Built by Veecle. Chiplab is built for AI-assisted embedded development. Connect your coding agent once, and from then on you just describe what you want in plain language: "test this firmware on an STM32," "check if this build works on the nRF52840." Your agent handles the rest: uploading your binary, running it on a chip-accurate virtual board, and reporting back the results. ## What Chiplab does Chiplab exposes its capabilities to your agent over MCP. You don't call these directly; your agent discovers and uses them automatically based on what you ask it to do. Chiplab's tool surface is always evolving. Ask Chiplab what it can do right now for the current list of tools. Treat the summary below as illustrative, not a frozen contract. ### Available now * `ask`: Query Chiplab's knowledge base with any natural-language question about the platform, its tools, or firmware development patterns. * Projects: group uploads and runs under a server-minted `project_id`, so the work persists across sessions and is shared with everyone who has access. * `run`: Upload a compiled ELF and execute it on a virtual chip. The run starts in the background; your agent waits for it, then downloads the captured UART output. Today `run` covers single-chip execution and the UARTs you ask to capture, with more peripheral support and richer fault/panic traces on the way. How a session moves through them is on [Core concepts](/concepts). The full pages are [ask](/tools/ask), [run](/tools/run), and [test](/tools/test). A pipeline runs that same loop. The setup is on [Test in CI](/use-cases/ci-on-every-pr). ## Knowledge corpus Every simulation run deposits observed behavior into Chiplab's knowledge corpus, indexed by chip family, failure pattern, and board configuration. Your firmware code and binaries are not part of the corpus, only the chip-level behavior observed during the run. Your agent's answers draw on what Chiplab has learned about each chip, so every simulation gets more useful over time. ## Supported agents Setup steps for Cursor, OpenCode, Claude Code, Claude Desktop, VS Code, and Codex are on [Connect an agent](/agents/overview). A client that can add a remote HTTP MCP server and sign in through the browser uses the same URL. All of them connect to Chiplab over HTTP using the MCP. Authentication runs through a browser-based OAuth 2.0 flow, so there are no tokens to manage. Run your first simulation in under five minutes. Setup for Cursor, OpenCode, Claude Code, Claude Desktop, VS Code, and Codex. ## Use cases See what the firmware prints on a virtual board. Read the run status and UART, then ask about board gotchas. Fail the pipeline when the expected UART output is missing. # API keys Source: https://docs.veecle.ai/platform/api-keys Manage Chiplab API keys and connected agent sessions. The **API Keys** page is your control panel for authentication in Chiplab. From here you can see every AI agent that's authorized to use your account, check when each one connected and when it was last active, connect new agents, and revoke access you no longer want. ## Connected agents Every agent session you've authorized shows up in a list, with three columns: * **Name**, the agent's session identifier. * **Connected**, how long ago the session first authenticated. * **Last active**, when Chiplab last saw a request from that session. ## Connecting a new agent Click **Connect agent** on the API Keys page. Choose from the available cards, Cursor, OpenCode, Claude Code, and more added over time. Chiplab shows a ready-to-paste configuration snippet with the connection URL pre-filled. Add it to your agent, then complete the browser-based authentication. Once authenticated, the new session appears in your Connected agents list. ## Personal access tokens A pipeline has no browser, so CI authenticates with a personal access token instead of an agent session. Create one on this page, give it a name, and copy the value when Chiplab shows it. That value is shown once. Use it as a bearer token on the MCP server. The full snippet is on [Test in CI](/use-cases/ci-on-every-pr). Revoke a token from this list when you rotate it. Revoking is immediate. ## Revoking an agent session Find the session's row and click **Revoke**. Revoking access is immediate and permanent. There's no grace period, and it can't be undone. To use Chiplab from that agent again, you'll need to reconnect and re-authenticate from scratch. ## Next steps Configure Chiplab in Cursor's MCP settings and authenticate. Register Chiplab in your OpenCode config and run the auth command. Add Chiplab via the Claude CLI and authenticate with `/mcp`. # Usage Source: https://docs.veecle.ai/platform/usage Track Chiplab credit usage across your connected agents. The **Usage** dashboard shows how much your connected agents are consuming through Chiplab, so you can keep an eye on your credit balance. ## Beta Chiplab is in beta. During the beta, every account gets 1,000 free credits a day. The allowance resets every 24 hours, and unused credits do not roll over. No credit card is required. Tool calls are metered in credits, and one call can consume more than one credit, depending on the compute it triggers. Plans and prices after the beta are on [veecle.ai/pricing](https://veecle.ai/pricing). ## What you can track Cumulative activity across all connected agents since you started using Chiplab. A rolling recent-window view, useful for spotting a spike, confirming a run went through, or checking that nothing's stuck. Usage is tracked per account, across every agent you've connected, not per individual session. ## Where to find it Open the **Usage** section in the left sidebar. It loads your full request history for the current billing period, with aggregate totals at the top. Usage data only appears once your connected agents start making requests. If the dashboard looks empty, verify an agent is connected and has completed at least one tool call. Don't have an agent connected yet? Head to [API Keys](/platform/api-keys) to authenticate your first one. # Quickstart Source: https://docs.veecle.ai/quickstart Connect your AI coding agent to Chiplab and run your first firmware simulation. This guide walks you from a brand-new Chiplab account to a completed firmware simulation. By the end, your AI coding agent will be connected to Chiplab and you'll have seen real UART output from a virtual chip instance, no physical hardware required. Go to [chiplab.veecle.ai](https://chiplab.veecle.ai) and sign in with your account. It's self-serve: no credit card needed, and there's a free tier to get you started. In the [Chiplab dashboard](https://chiplab.veecle.ai), open **API keys** and choose **Connect agent**. Pick your client (Cursor, OpenCode, Claude Code, and others). The dashboard shows the exact command or config snippet to copy. Every client registers the same MCP server, `https://chiplab.veecle.ai/mcp`, so a client that isn't listed connects the same way. On first use your agent opens a browser to sign in. Working from a clone of the [Chiplab repo](https://github.com/veecle/chiplab)? Claude Code picks up [`.mcp.json`](https://github.com/veecle/chiplab/blob/main/.mcp.json) automatically and only asks you to trust it. Step-by-step guides for each client are on [Connect an agent](/agents/overview). With your agent connected, verify the connection by asking Chiplab what it can do right now. It should come back with the current list of tools. Then try a real simulation. Clone this repo: ```sh theme={null} git clone https://github.com/veecle/chiplab && cd chiplab ``` Then just tell your agent: ```text theme={null} Build and run examples/bare-metal/stm32f4-discovery on Chiplab. ``` Your agent installs what's needed, builds the binary, uploads the ELF, and runs it on a virtual STM32F4 Discovery board. The run is bounded to 5 seconds of virtual CPU time. Your agent waits for it to finish, then reads the captured UART. You'll see `Hello world!` when the agent asks for `usart2`, the peripheral that example writes to. This same produce-an-ELF → upload → run → read-output flow works for every board and framework this repo ships examples for; only the ELF path and board change. See [supported-boards.md](https://github.com/veecle/chiplab/blob/main/supported-boards.md) for the full board list. Your agent can also query Chiplab directly; you don't need to prompt this explicitly. It happens automatically whenever your agent needs context, most commonly the first time it uses Chiplab in a session. ## Connect your agent Configure Chiplab in Cursor's MCP settings and authenticate. Register Chiplab in your OpenCode config and run the auth command. Add Chiplab via the Claude CLI and authenticate with `/mcp`. Claude Desktop, VS Code, Codex, and clients that can add a remote HTTP MCP server. # ask Source: https://docs.veecle.ai/tools/ask Look up how Chiplab works: supported boards, the upload and run flow, credits, and chip-specific fixes learned from real runs. `ask` looks up how Chiplab works: which boards are supported, how to upload and run firmware, and how credits are used. It also returns chip-specific gotchas and validated fixes collected from real simulation runs. For general chip documentation, such as how a peripheral works, use the vendor reference manual. ## Ask your agent ```text theme={null} Which UART does stm32f4_discovery write to? ``` ```text theme={null} Why is my captured output empty? ``` ## What you get back A short answer in plain text. When the answer comes from the docs, it includes links to those pages. `ask` answers one natural-language question about Chiplab and returns plain text. ## Parameters A natural language question about Chiplab. ## Example call ```json theme={null} { "query": "Which UART does stm32f4_discovery write to?" } ``` ## Response A plain-text answer from the Chiplab assistant. When relevant, the answer includes links to the specific documentation pages it drew from. ## How it works Chiplab routes each question to an assistant with access to the most relevant sections of the [knowledge corpus](/introduction#knowledge-corpus) and composes a focused answer. The assistant is scoped to Chiplab usage topics, so every answer is grounded in platform documentation. # run Source: https://docs.veecle.ai/tools/run Run compiled firmware on a chip-accurate virtual board and read what it printed, with no hardware on your desk. Chiplab executes your firmware on a chip-accurate virtual board and returns the UART output you asked to capture, no physical hardware required. Cross-compilation happens on your side. Chiplab runs the ELF as-is. The simulation platform today is [Renode](https://renode.io), with [QEMU](https://www.qemu.org) and more planned. ## When to use it Use a run when you want the printed output. When you need a pass or fail for each test, use [test](/tools/test). ## Ask your agent ```text theme={null} Run this firmware on the STM32F4 Discovery and capture usart2. ``` Not sure which board fits your firmware? Ask Chiplab about individual board specs; board recommendation tooling is on the way. ## What you get back You get the captured UART output. `succeeded` means Chiplab completed the run. It does not mean your firmware behaved: a firmware that panics or faults still returns `succeeded`. Check the `uart` output for what you expect. A run lasts up to 5 seconds of virtual time. ## Chip accuracy Each virtual board runs your exact binary against a chip-accurate model of the target's peripherals, memory map, and interrupt controller. Peripheral coverage keeps growing. If your firmware depends on a peripheral or timing behavior Chiplab doesn't model yet, ask Chiplab for the current coverage of your board. ## Knowledge corpus Every run deposits observed behavior into Chiplab's [knowledge corpus](/introduction#knowledge-corpus). Your firmware code and binaries are not part of it. The agent workflow is a short sequence: resolve a project, upload the ELF, start the run, wait for the job, then download artifacts. Your agent's MCP client discovers the exact tool names and parameters live; the shapes below match the platform's current workflow. Cross-compilation happens on your side. Chiplab runs the ELF as-is. ## Project A project holds the uploads and runs, so the next session can continue the same work. Everyone with access to the project sees it. The id is server-minted. Your agent resolves it once, from context, from `list_projects`, or by calling `create_project`, and passes that same id on every later call. * `list_projects` returns each project's `project_id`, `name`, and `description`. * `create_project` takes a `name` and an optional `description`. * `update_project` changes the `name` or the `description`. Omit either field to leave it unchanged. The id stays the same. * `delete_project` removes that project for everyone who can access it. ## Upload `issue_upload_ticket` takes the `project_id` and returns: * `upload_url`: a presigned URL, valid for 10 minutes. Your agent `PUT`s the raw ELF to it. The URL needs no auth header. * `artifact_id`: the identifier to pass to `run`. The binary must be a compiled ELF for the target chip. Use an `artifact_id` with the same `project_id` it was issued under. ## Starting a run The project this run belongs to, from `create_project`. Ids look like `project_...`. The firmware to run, from `issue_upload_ticket`. The virtual board to boot. This is an object, not a string. `model` is one of the board slugs on [Boards](/hardware/boards). `uarts` is required: the list of UART names to capture, for example `["usart2"]`. Only those peripherals are written into the captured output. `run` returns as soon as the simulation has started. Identifier for this simulation run. Later calls refer to it as a `job_id`. ```json theme={null} { "project_id": "project_01...", "artifact_id": "artifact_01...", "board": { "model": "stm32f4_discovery", "uarts": ["usart2"] } } ``` ```json theme={null} { "run_id": "run_01..." } ``` ## Waiting for the job UART output is ready once the job finishes. Your agent calls `wait_for_job` with the `run_id` as `job_id`. The `run_id` returned by `run`, or a job id from an earlier listing. How many seconds to wait. The server caps this value. `running`, `succeeded`, or `failed`. `succeeded` means Chiplab completed the run. It does not mean your firmware behaved: a firmware that panics or faults still returns `succeeded`. Check the `uart` output for what you expect. The artifact and board the job was started with, so a run from an earlier session can be recognized without the call that started it. Present when `status` is `succeeded`. Includes an `artifacts` list naming the files the run produced, such as `uart`. Present when `status` is `failed`. What went wrong. True when `status` is still `running` because the wait ended before the job finished. A response with `status: "running"` and `timed_out: true` means the run is still going. `wait_for_job` is read-only, so calling it again on a finished run returns the same response immediately. ```json theme={null} { "status": "succeeded", "result": { "artifacts": ["uart"] } } ``` ## Reading the output To read an artifact, your agent calls `issue_download_ticket` with the `job_id` and an `artifact_name` from that `artifacts` list. The response is a presigned `download_url`. A `GET` of that URL returns the artifact bytes, with no auth header. `uart` is the captured UART output from the peripherals named in `board.uarts`. ## Earlier runs Earlier runs stay reachable in a later session. `list_jobs` takes the `project_id` and optionally: `run` to list only runs. Omit it to list every job, including test suite runs. An RFC 3339 timestamp. Only jobs started before this time are returned. When `more_available` is true, pass the `created_at` of the last job in the page to continue. Each entry has a `job_id`, a `status`, and a `created_at`, most recently started first. Pass the `job_id` to `wait_for_job` to read what that run produced. ## Limits A run lasts up to 5 seconds of virtual time. # test Source: https://docs.veecle.ai/tools/test Run a TypeScript test suite against your firmware on a virtual board, and get a pass or fail for each test. ## What it does `test` runs a TypeScript suite against your firmware on a virtual board and returns a pass or fail for each test. ## When to use it Use a suite when you need a pass or fail for each check, not just output. A firmware that stops printing fails the test. ## What you can test A suite advances virtual time and watches UART lines. Each example starts with `import { expect } from "jsr:@std/expect";`. **Boot check.** Expect `System ready` within 3 seconds. ```ts theme={null} Deno.test("boots", async () => { const outcome = await simulation.runUntilUartLine("System ready", 3000); expect(outcome.kind).toBe("matched"); }); ``` **Init order.** Expect several lines in order. Each call only matches output printed after it starts. ```ts theme={null} Deno.test("init order", async () => { expect((await simulation.runUntilUartLine("clocks on", 3000)).kind).toBe("matched"); expect((await simulation.runUntilUartLine("uart on", 3000)).kind).toBe("matched"); expect((await simulation.runUntilUartLine("System ready", 3000)).kind).toBe("matched"); }); ``` **Timing.** A 1 second tick should reach `tick 5` within 5.5 seconds. That relies on the chip-accurate interrupt timing. ```ts theme={null} Deno.test("one second tick", async () => { const outcome = await simulation.runUntilUartLine("tick 5", 5500); expect(outcome.kind).toBe("matched"); }); ``` **Long-run liveness.** `runForMillis` takes at most 60000 ms, so a longer run is 60 second chunks. Then expect `alive`. The line has to be printed after those chunks, because the next call only sees output from that point on. ```ts theme={null} Deno.test("stays alive", async () => { await simulation.runForMillis(60000); await simulation.runForMillis(60000); const outcome = await simulation.runUntilUartLine("alive", 3000); expect(outcome.kind).toBe("matched"); }); ``` **Logic with a compile-time stub.** Build the firmware with a fixed raw value. The suite only expects the UART line. It does not feed that value in. ```ts theme={null} Deno.test("stubbed temperature", async () => { const outcome = await simulation.runUntilUartLine("Temp: 23.5C", 3000); expect(outcome.kind).toBe("matched"); }); ``` Crash detection is the HardFault example in [Reading the result](#reading-the-result) on the Agents tab. ## Not supported yet A suite observes firmware through UART only. GPIO input and button stimulus, pin-state or register and memory reads, and external sensors or I2C/SPI devices are not supported yet. ## Ask your agent ```text theme={null} Write a test that checks this firmware prints Hello world! within 10 seconds on the STM32F4 Discovery, and run it. ``` ## What you get back You get a pass or fail for each test. `succeeded` means the suite ran, not that every test passed. Check `result.summary`. ## run\_test\_suite `run_test_suite` takes: The project this test belongs to, from `create_project`. Ids look like `project_...`. The firmware ELF, from `issue_upload_ticket`. Ids look like `artifact_...`. The TypeScript suite file, from a second `issue_upload_ticket`. Ids look like `artifact_...`. The virtual board. `model` is a board model such as `stm32f4_discovery`. `uarts` is the list of UART names to capture, for example `["usart2"]`. Your agent calls `issue_upload_ticket` twice, both with the same `project_id`. It PUTs the ELF to the first `upload_url` and the suite file to the second. `run_test_suite` returns `run_test_suite_id` as soon as the job starts. Pass that id to `wait_for_job` as `job_id`. ## How it works The suite drives the simulation. The board is paused until the suite advances time. Virtual time moves only inside the awaited calls. Between those calls the board stays frozen. `runUntilUartLine` only watches the UARTs named in `board.uarts`. ## Writing a suite A suite is one TypeScript file: ```ts theme={null} import { expect } from "jsr:@std/expect"; Deno.test("firmware greets", async () => { const outcome = await simulation.runUntilUartLine("Hello world!", 10000); expect(outcome.kind).toBe("matched"); }); ``` Tests use `Deno.test`, or `describe` and `it` from `jsr:@std/testing/bdd`. The only imports available are `jsr:@std/testing/bdd`, `jsr:@std/expect`, and `zod`. No network, npm packages, or imports of other files. Chiplab type-checks the suite before it runs. The server provides the `simulation` types itself. The suite must not reference `chiplab.d.ts`. Only the suite file is uploaded, and it is checked without network access, so a `/// ` to the definitions, by local path or URL, fails to resolve and the suite does not compile. For editor support, point the project's `deno.json` at the published definitions: ```json theme={null} { "compilerOptions": { "types": ["https://chiplab.veecle.ai/mcp/resources/schemas/chiplab.d.ts"] } } ``` A local `deno check` then needs `--allow-import` to fetch them. ## The simulation API The suite gets a global `simulation`. * `simulation.runForMillis(millis)` runs for `millis` of virtual time, then pauses. * `simulation.runUntilUartLine(expected, maxMillis)` runs until a line containing `expected` appears, or `maxMillis` elapses. It resolves to `{ kind: "matched" }` or `{ kind: "timed_out" }`. Both take 1 to 60000 ms. Two matching rules: * `runUntilUartLine` only matches output produced after the call starts. * `expected` must not contain control characters such as a newline. ## Reading the result `succeeded` means the suite ran, not that it passed. Check `result.summary`. | What happened | `wait_for_job` | | - | - | | The suite ran | `status` is `succeeded`. `result.summary` has `planned`, `passed`, and `failed`. | | The suite did not compile, or it threw before any test ran | `status` is `succeeded`. `result.summary` is `null`. Read `testsuite_stderr`. | | The job failed | `status` is `failed`. The response has an error string and no artifacts. | A suite that expects `Hello world!` on `stm32f4_discovery` / `usart2`, run against firmware that prints that line: ```json theme={null} { "input": { "board": { "model": "stm32f4_discovery", "uarts": ["usart2"] }, "firmware_artifact_id": "artifact_01m3ye77kfec79agpsjamg34nf", "suite_artifact_id": "artifact_01m3ye76e2fkrb99wrpwvae4xa" }, "job_id": "run_test_suite_01m3ye78dkf5na6977req4trz6", "project_id": "project_01m3ydfkccerpswp3zh14rpeqh", "result": { "artifacts": ["testsuite_report", "testsuite_stdout", "firmware_uart"], "summary": { "failed": 0, "passed": 1, "planned": 1 } }, "status": "succeeded", "timed_out": false } ``` `testsuite_report` for that run: ```json theme={null} { "planned": 1, "passed": 1, "failed": 0, "skipped": 0, "todo": 0, "bail_out": false, "tests": [{ "number": 1, "name": "firmware greets", "status": "passed" }], "diagnostics": ["# test.ts"] } ``` The same suite against firmware that prints `before fault` and then hits a HardFault. The wait times out, the assertion fails, and the job still succeeds: ```json theme={null} { "input": { "board": { "model": "stm32f4_discovery", "uarts": ["usart2"] }, "firmware_artifact_id": "artifact_01m3ye7hgaeyya3vwmaye3ep9g", "suite_artifact_id": "artifact_01m3ye76e2fkrb99wrpwvae4xa" }, "job_id": "run_test_suite_01m3ye7j9bfjhsv9spkmepxa5v", "project_id": "project_01m3ydfkccerpswp3zh14rpeqh", "result": { "artifacts": ["testsuite_report", "testsuite_stdout", "testsuite_stderr", "firmware_uart"], "summary": { "failed": 1, "passed": 0, "planned": 1 } }, "status": "succeeded", "timed_out": false } ``` `testsuite_report` for that run: ```json theme={null} { "planned": 1, "passed": 0, "failed": 1, "skipped": 0, "todo": 0, "bail_out": false, "tests": [{ "number": 1, "name": "firmware greets", "status": "failed" }], "diagnostics": ["# test.ts"] } ``` ## Artifacts `result.artifacts` lists what `issue_download_ticket` can download. Pass the `run_test_suite_id` as `job_id`. * `testsuite_report`: JSON with the counts, plus `skipped`, `todo`, `bail_out`, per-test `tests` (`number`, `name`, `status`), and `diagnostics`. * `testsuite_stdout` and `testsuite_stderr`: the suite's output, including `console.log`. * `firmware_uart`: the firmware's UART output. A [run](/tools/run) calls this artifact `uart`. * `simulator_stdout` and `simulator_stderr`: simulator logs. Empty artifacts are omitted, so a passing suite may have no `testsuite_stderr`. ## Limits The whole suite has 5 minutes of wall-clock time. Exceeding it fails the job with `internal server error` and produces no artifacts. Each `runForMillis` and `runUntilUartLine` call takes 1 to 60000 ms. Each `Deno.test` or top-level `describe` counts as one result. Steps and nested `it` calls are not reported individually. # Check pull requests on a virtual board Source: https://docs.veecle.ai/use-cases/ci-on-every-pr Run firmware on a virtual board from a pull request, and fail the job when the expected UART output is missing. You want a pull request checked on a virtual board, and the job failed when the captured UART is missing what you expect. ## What you'll do On [API keys](/platform/api-keys), create a personal access token and give it a name. Chiplab shows the token once. Copy it into a CI secret. The job sends it as a bearer token, `CHIPLAB_API_KEY`. ```json theme={null} { "mcpServers": { "chiplab": { "type": "http", "url": "https://chiplab.veecle.ai/mcp", "headers": { "Authorization": "Bearer ${CHIPLAB_API_KEY}" } } } } ``` `CHIPLAB_API_KEY` stays in the CI environment. The config file holds the `${CHIPLAB_API_KEY}` placeholder, so the token is expanded when the job connects. The examples repository ships [`.github/workflows/claude-chiplab-smoke.yml`](https://github.com/veecle/chiplab/blob/main/.github/workflows/claude-chiplab-smoke.yml). The template runs Claude Code in the job, which then uses Chiplab. It runs when someone comments `/smoke-test` on a pull request. It builds `examples/bare-metal/stm32f4-discovery` and checks the captured UART. It needs two secrets: `CHIPLAB_API_KEY` (a Chiplab personal access token, starts with `veecle_pat_`) and `ANTHROPIC_API_KEY` (for Claude Code). Treat it as a template: the trigger, the agent, and the example path are yours to change. Changing the `on:` trigger by itself is not enough. The job checks that the comment contains `/smoke-test`, and it checks out the pull request number from that comment. To run on every `pull_request`, change the trigger, the job-level comment check, and the checkout together. A `pull_request` event has no comment body, and `github.event.issue.number` is empty. ```diff theme={null} on: - issue_comment: - types: [created] + pull_request: ``` Delete the job-level `if`. It looks for `/smoke-test` in the comment. Deleting it also drops the check that the commenter is `OWNER`, `MEMBER`, or `COLLABORATOR`. ```diff theme={null} - if: | - github.event.issue.pull_request - && contains(github.event.comment.body, '/smoke-test') - && contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association) ``` Check out the pull request head: ```diff theme={null} - ref: refs/pull/${{ github.event.issue.number }}/head + ref: ${{ github.event.pull_request.head.sha }} ``` The fork-guard step calls `gh pr view` with that same number. Point it at the pull request: ```diff theme={null} - HEAD_REPO=$(gh pr view ${{ github.event.issue.number }} \ + HEAD_REPO=$(gh pr view ${{ github.event.pull_request.number }} \ ``` The job calls `ask` for the current boards, tools, and gotchas, produces an ELF, uploads it, and runs it on the target board with the UART the firmware writes. It then waits and downloads `uart`. Fail the pipeline unless `uart` contains what you expect. The public examples expect `Hello world!`. The check must look at the `uart` output, not the run status, because a crashing firmware still returns `succeeded`. To get a pass or fail per test, swap that UART check for `run_test_suite`. In your copy of the template, the prompt tells the agent to fail when the captured UART does not contain `Hello world!`. Change it so the agent uploads a suite, calls `run_test_suite`, and fails unless every test passed. `result.summary` has `planned`, `passed`, and `failed`. A null summary means the suite did not compile, or it threw before any test ran. ```diff theme={null} - Build the firmware in examples/bare-metal/stm32f4-discovery and test it on Chiplab, following the workflow in AGENTS.md. Diagnose and report any issues. - - If the firmware does not build, or the captured UART output does not contain "Hello world!", exit with a non-zero status so the workflow fails. + Build the firmware in examples/bare-metal/stm32f4-discovery, upload it with a suite, and call run_test_suite, following the workflow in AGENTS.md. Diagnose and report any issues. + + If the firmware does not build, result.summary is null, or result.summary.failed is not 0, exit with a non-zero status so the workflow fails. ``` ## Ask your agent ```text theme={null} Run this firmware on the STM32F4 Discovery, capture usart2, and fail the job if Hello world! is missing. ``` ## What you get back The job downloads `uart` and fails the pipeline unless that output contains what you expect. The public examples expect `Hello world!`. ## Next Get a pass or fail for each test. Execute an ELF on a virtual board and read the UART. # Find out why firmware crashes Source: https://docs.veecle.ai/use-cases/debug-a-crash Find where firmware stops by reading the UART output, and make crashes visible with handlers that print over the UART. The firmware panics on the virtual board. You read the `uart` output to see how far it got. ## What you'll do Your agent uploads the ELF and runs it on the board model, with the UARTs to capture. Only those UARTs are written into the output. `run` returns a `run_id` immediately. Your agent waits for the job and reads the status: `running`, `succeeded`, or `failed`. `uart` is the captured UART output. A crashing firmware still returns `succeeded`, so check the `uart` output. See [Core concepts](/concepts). Ask Chiplab about the gotchas for that board. `ask` takes a natural-language question and returns chip-specific gotchas and validated fixes from real runs. Change the firmware, build the ELF again, and run it on the same board. ## What you see when firmware crashes The run still returns `succeeded`, with no fault details. `wait_for_job` does not include an error string, fault registers, a PC, or a panic message. The `uart` output keeps everything printed before the crash, so the last line shows how far the firmware got. A firmware that prints `before fault` and then hits a HardFault returns status `succeeded` and a `uart` output of `before fault`. To see more, make the firmware report the crash over the UART: a panic handler that prints the panic message (instead of panic-halt), and a HardFault handler that prints the fault address and PC. Your agent can add these for you. A run lasts up to 5 seconds of virtual time. ## Catch crashes with a test A [test](/tools/test) that waits for an expected line fails when the firmware crashes before printing it. `runUntilUartLine` times out, so that test is a fail even though the job can still return `succeeded`. ## Ask your agent ```text theme={null} This firmware stops early on the STM32F4 Discovery. Add a panic handler and a HardFault handler that print to usart2, run it, and tell me the last line it printed and where it faulted. ``` ## What you get back You get the `uart` output up to the point where the firmware stopped. With a panic handler and a HardFault handler that print over the UART, that output also shows the panic message or the fault address. `ask` returns the gotchas for that board. ## Next Execute an ELF on a virtual board and read the UART. Look up boards, tools, and chip-specific gotchas. # Test firmware before the board arrives Source: https://docs.veecle.ai/use-cases/test-without-hardware See what the firmware prints on a virtual board before the hardware is on your desk. You can see what the firmware does on a virtual board before the hardware is on your desk. ## What you'll do Connect an agent once. A client that can add a remote HTTP MCP server signs in through the browser. Setup for each client is on [Connect an agent](/agents/overview). Build the ELF. You can compile it on your side, and Chiplab runs that binary as-is. Your agent uploads the ELF and runs it on the target board model, with the UARTs to capture. Only the UARTs you list are captured. The STM32F4 Discovery model is `stm32f4_discovery`, and its examples write `usart2`. The run returns a `run_id` immediately. Your agent waits for the job, then downloads `uart`, the captured UART output. A run lasts up to 5 seconds of virtual time. ## Ask your agent ```text theme={null} Run this ELF on the STM32F4 Discovery, capture usart2, and tell me what it printed. ``` ## What you get back You get a status (`running`, `succeeded`, or `failed`) and `uart` for the UARTs you captured. A crashing firmware still returns `succeeded`, so check the `uart` output. See [Core concepts](/concepts). On the STM32F4 Discovery examples, `usart2` prints `Hello world!`. ## Next Execute an ELF on a virtual board and read the UART. Get a pass or fail for each test.