Metadata-Version: 2.5
Name: sim-bridge
Version: 0.1.2
Summary: Run AVR (simavr) and STM32 (Renode) firmware in simulation with UART, GDB debugging, and VCD waveform recording
Author: grbl-multi contributors
License: MIT
Keywords: avr,debugging,embedded,firmware,gdb,gpio,gtkwave,renode,simavr,simulation,simulator,stm32,vcd,waveform
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Embedded Systems
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.12
Requires-Dist: pyvcd>=0.5.0
Requires-Dist: typer>=0.15.0
Requires-Dist: vcdvcd>=2.6.0
Description-Content-Type: text/markdown

# sim-bridge

**Run AVR and STM32 firmware in simulation — with UART, GDB debugging and VCD waveform recording exposed through one small CLI toolkit.**

sim-bridge wraps two mature open-source MCU simulators — [simavr](https://github.com/buserror/simavr) (AVR) and [Renode](https://renode.io/) (STM32) — and turns "start a simulator and wire up its debug interfaces" into a small set of repeatable commands. It adds the plumbing the raw simulators leave to you:

- **UART I/O** — the firmware console is published as a PTY (AVR) or TCP socket (STM32), auto-allocated and written to a port file, so any terminal, socket client or test script can talk to the firmware as if it were a serial port.
- **GDB server** — every host exposes a GDB stub, so you can set breakpoints and step through firmware from your editor instead of printf-debugging.
- **VCD recording** — signal transitions (AVR modules and STM32 GPIO pins, discovered at runtime) are recorded into [VCD (Value Change Dump)](https://en.wikipedia.org/wiki/Value_change_dump) files that GTKWave can open: a scope for timing questions that are hard to answer on real hardware.

The value is not in the simulators themselves (both are excellent and used unmodified) but in the harness around them: one consistent state machine, one control protocol, and one set of commands across two MCU families.

This is useful for embedded firmware developers who want to verify stepper and timer timing, PWM duty cycles, interrupt latency, or UART protocol behaviour without physical hardware. It works for any AVR or STM32 firmware — CNC G-code firmware is simply the case sim-bridge was extracted from, which is why G-code shows up in the examples.

## How it works

sim-bridge runs two cooperating processes:

```
 ┌─────────────┐         TCP (JSON control)        ┌─────────────┐
 │  *-vcd CLI  │  ◄──────────────────────────►  │  *-host CLI  │
 │  *-ctl CLI  │                                  │  (server)   │
 └─────────────┘                                  └──────┬──────┘
                                                        │
                                          ┌─────────────┼─────────────┐
                                          ▼             ▼             ▼
                                    ┌──────────┐ ┌──────────┐ ┌──────────┐
                                    │ UART     │ │ GDB      │ │ VCD      │
                                    │ socket/  │ │ server   │ │ hooks    │
                                    │ PTY      │ │          │ │          │
                                    └──────────┘ └──────────┘ └──────────┘
```

- **`*-host`** launches the simulator (simavr or Renode), loads the firmware ELF, exposes the firmware UART, opens a GDB server, and registers the signal hooks used for VCD recording.
- **`*-vcd`** is the VCD control client. It discovers the host via a port file in `/tmp/`, then issues `start` / `stop` / `status` / `list` commands to drive VCD recording.
- **`simavr-ctl`** (AVR) is the runtime control client: it drives input pins (`pin.set` — the AVR counterpart of Renode's `OnGPIO`), reads/writes MCU memory (`mem.read` / `mem.write`), waits for a memory location or pin to satisfy a condition without polling (`mem.watch` / `pin.watch`), and reports/controls the core itself (`sim.state` / `sim.reset`). The host owns the command table and advertises it via `caps`, so clients are stateless.

Per-platform differences are handled for you rather than exposed:

| | AVR (simavr) | STM32 (Renode) |
|---|---|---|
| UART | PTY at `/tmp/simavr-uart-pty` | TCP socket on `--uartport` |
| Signal discovery | `simavr-vcd list` — module based (`timer1.compa`) | `renode-vcd list` — GPIO based (`PA0`, `gpioPortA`) |
| Runtime control | `simavr-ctl pin` / `mem` / `sim` — drive pins, read/write data space & flash, wait on a condition without polling, inspect/reset the core | Renode Robot server — `OnGPIO` stimulus, `sysbus` register access |
| Hook mechanism | bundled C++ bridge, compiled on first run and cached | IronPython injected through Renode's Robot XML-RPC server |
| VCD writing | streamed to file while recording | buffered in memory, written on `stop -f` |
| Discovery file | `/tmp/simavr-bridge.port` | `/tmp/renode-bridge.port` |

`renode-host` itself is not STM32-specific: it injects `$bin` / `$gdbport` / `$uartport`
into any `.resc` and launches `renode-run`, so any Renode target works for UART + GDB.
`renode-vcd` additionally needs GPIO peripherals named `gpioPort<LETTER>` (the STM32 and
Silabs convention); targets without that naming still run, but have no recordable signals.

## CLI commands

| Command | Platform | Role | Description |
|---------|----------|------|-------------|
| `simavr-host` | AVR | Host | Runs an AVR firmware ELF on simavr: UART PTY + GDB server + control port |
| `simavr-vcd` | AVR | VCD control | Discovers signals and records VCD on a running `simavr-host` |
| `simavr-ctl` | AVR | Runtime control | Drives input pins (`pin set`), reads/writes memory (`mem read`/`write`), waits for conditions (`mem watch`/`pin watch`), and reports/resets the core (`sim state`/`reset`) on a running `simavr-host` |
| `renode-host` | Renode targets | Host | Runs a firmware ELF on Renode: UART socket + GDB server + Robot server |
| `renode-vcd` | Renode targets with `gpioPort*` | VCD control | Discovers GPIO pins and records VCD on a running `renode-host` |
| `sim-bridge-skill` | — | Skill installer | Installs the bundled AI-agent skill into an agent skills directory |

> **Note:** `simavr-host` / `simavr-vcd` / `simavr-ctl` only support Linux (POSIX PTY + socket APIs). `renode-host` / `renode-vcd` work on Linux and macOS.

## Installation

### From PyPI

```bash
pip install sim-bridge
```

### System dependencies

**For `simavr-host` (AVR simulation):**

```bash
# Debian/Ubuntu
sudo apt install build-essential libsimavr-dev rapidjson-dev
```

- `g++` — compiles the bundled C++ bridge on first run (cached in `/tmp/sim_bridge/`)
- `libsimavr-dev` — provides `libsimavr`, `libsimavrparts`, and headers (compatible with simavr 1.6 and 1.8)
- `rapidjson-dev` — C++ JSON parser for the control protocol

**For `renode-host` (STM32 simulation):**

```bash
uv tool install git+https://github.com/antmicro/renode-run.git
```

`renode-run` manages the Renode binary; sim-bridge calls it automatically.

## Quickstart

### AVR (simavr)

You need an AVR firmware ELF file (e.g. built with `cmake -B build -DMCU=atmega328p . && cmake --build build`).

```bash
# Terminal 1: launch simulation host (omit --gdbport when you do not need a debugger)
simavr-host build/atmega328p_GRBL_Debug.elf --gdbport 1234
# → UART PTY:   /tmp/simavr-uart-pty
# → GDB server: localhost:1234 (only when --gdbport is passed)
# → VCD control port: auto (written to /tmp/simavr-bridge.port)

# Terminal 2: record waveforms
simavr-vcd list                        # discover available signals
simavr-vcd start -s timer1 -f out.vcd  # start recording (atomic)
# ... drive the firmware through /tmp/simavr-uart-pty ...
simavr-vcd stop                        # stop → out.vcd written

# Terminal 3 (optional): drive inputs / inspect memory while it runs
simavr-ctl caps                        # protocol, MCU, memory spaces, commands
simavr-ctl pin set portb.pin1 0        # close a limit switch (virtual switch, level latched)
simavr-ctl pin get portb.pin1          # sampled level + PIN/DDR/PORT registers
simavr-ctl mem read 0x100 --size 32    # hex dump of the data space
simavr-ctl mem read 0x100 --hex        # script-friendly hex string
simavr-ctl mem write 0x100 0a0b0c

# View waveform
gtkwave out.vcd
```

A driven pin behaves like a real switch: the level **latches** (through the
firmware's own `PORT`/`DDR` writes and across resets) and every change delivers a
pin-change interrupt, so falling *and* rising edges are seen — exactly what
limit-switch, probe and safety-door tests need.

**Signal names** follow simavr's module convention: `<module>.<signal>` (e.g. `timer1.compa`, `uart0.rxd`, `portb.pb3`).

**Signal groups** (shorthand): use the module name as a group. Common groups on ATmega328P: `portb`, `portc`, `portd`, `timer0`, `timer1`, `timer2`, `uart0`, `adc`, `spi`, `twi`, `all`.

### STM32 (Renode)

You need a firmware ELF file and a Renode `.resc` platform script. the `.resc` Must follow[resc-template.md](src/sim_bridge/skills/sim-bridge-firmware/references/resc-template.md)

```bash
# Terminal 1: launch simulation host
renode-host \
    --resc platform.resc \
    --firmware build/stm32f103_GRBL_Debug.elf \
    --gdbport 3333
# → UART socket:  localhost:12345 (talk to the firmware here)
# → GDB server:   localhost:3333
# → Robot server: auto (written to /tmp/renode-bridge.port)

# Terminal 2: record waveforms
renode-vcd list                         # discover GPIO pins
renode-vcd start -s PA0 -s PA1 -s PA3   # start recording specific pins
# ... drive the firmware at localhost:12345 ...
renode-vcd stop -f out.vcd              # stop → out.vcd written

# View waveform
gtkwave out.vcd
```

**Signal names** use STM32 convention: `PA0`, `PB5`, `PC15`, or entire ports `gpioPortA`, `gpioPortB`, etc. Use `all` (default) for all pins.

## Talking to the firmware over UART

The UART is a plain byte channel, so it accepts whatever protocol your firmware speaks — text commands, binary framing, or G-code as in this example. On STM32 it is a TCP socket; on AVR it is a PTY.

**Interactive (PuTTY / gtkterm):** connect to `127.0.0.1:12345` (Raw mode).

**Command line:**
```bash
echo -e '$X\nG1 X100 Y100 F1000\n' | nc -q10 127.0.0.1 12345
```

**Python script:**
```python
import socket, time

s = socket.create_connection(('127.0.0.1', 12345))
time.sleep(2); s.recv(4096)              # wait for boot

s.sendall(b'$X\n'); time.sleep(1); s.recv(4096)    # unlock
s.sendall(b'G1 X100 Y100 F1000\n')                  # move
time.sleep(10); s.recv(4096)

s.close()
```

## VCD recording details

**State machine** (host-authoritative):

```
idle ──start──► recording ──stop──► idle
```

- `start` is atomic: registers hooks + begins simulation in one step. Returns error if already recording.
- `stop` requires `--file`: pauses simulation, drains transitions, writes VCD, returns to idle.
- `status`: query current state and transition count.
- `list`: enumerate recordable signals on the running firmware, so you never guess a signal name.

## Command reference

### `simavr-host`

```
simavr-host FIRMWARE [--gdbport PORT] [--uart INDEX] [--freq HZ] [--mcu NAME]
```

Runs the given AVR firmware ELF on simavr. The bundled C++ bridge (`simavr_uart_bridge.cpp`) is compiled on first run and cached by source hash; it provides the UART PTY and the VCD signal hooks. `--gdbport` is optional: omit it and no GDB server is started (the bridge prints `GDB server disabled`) — what plain waveform capture and CI runs want.

### `simavr-vcd`

```
simavr-vcd list                          # list available signals
simavr-vcd start [-s SIGNAL]... [-f FILE] [--period US]
simavr-vcd status
simavr-vcd stop
```

### `simavr-ctl`

```
simavr-ctl caps                                          # discovery: protocol, MCU, spaces, commands
simavr-ctl pin set SIGNAL LEVEL                          # e.g. portb.pin1 0
simavr-ctl pin set --pins SIGNAL=LEVEL [--pins ...]      # drive several pins atomically
simavr-ctl pin get SIGNAL
simavr-ctl pin watch SIGNAL [--until change|eq|ne|lt|gt] [--value L] [--timeout-ms N]
simavr-ctl mem read ADDR [--size N] [--space data|flash] [--hex]
simavr-ctl mem write ADDR HEXBYTES [--space data]
simavr-ctl mem watch ADDR [--size N] [--space data|flash] [--until ...] [--value V] [--timeout-ms N]
simavr-ctl sim state                                     # CPU state, PC, cycle, SREG, interrupts
simavr-ctl sim reset                                     # the reset pin, from software
simavr-ctl raw '{"cmd": "..."}'                          # escape hatch for new commands
```

Runtime control for a running `simavr-host`, over the same JSON control port as
`simavr-vcd`. `pin set` mirrors Renode's `OnGPIO` stimulus: the level is latched
as the port's external drive (so the firmware's pull-up handling cannot revert it)
and the transition raises the pin-change interrupt. `mem` reaches the AVR data
space (`0x00`–`ramend`) and flash read-only; both are bounded and validated on the
host. `mem watch` / `pin watch` are the polling-free way to wait — the host
evaluates the condition between CPU slices (the CPU keeps running), so one request
replaces a poll loop and there is no sampling window; a timeout exits non-zero
*with the last value*, which is what lets a test report "stalled at N" in seconds
instead of burning a large test timeout. Only one watch can be pending at a time,
and other control clients are not blocked by it. Add a command family by
registering one handler in the bridge's `command_table()` plus a Typer sub-app
here — the transport never changes.

### `renode-host`

```
renode-host --resc FILE --firmware FILE [--gdbport PORT] [--uartport PORT] [--robot-port PORT]
```

Runs the given firmware ELF on Renode (through `renode-run`), using the supplied `.resc` platform script for the MCU and board description. Not limited to STM32: the tool itself only injects `$bin` / `$gdbport` / `$uartport` and passes the rest to Renode.

### `renode-vcd`

```
renode-vcd list [-g PORT_LETTER]
renode-vcd start [-s SIGNAL]...
renode-vcd status
renode-vcd stop -f FILE
```

Injects GPIO state-change hooks into the running Renode instance over its Robot XML-RPC server. Discovery is name-based (`gpioPort*`), so this works for any target whose platform description uses that convention — STM32 and Silabs families.

## Python API

The CLIs are the interactive surface; anything that needs to *own* the lifecycle
(a test suite, a CI job, a script, a notebook) uses the library, so it does not
re-implement process spawning, readiness probing, transports and teardown:

```python
from sim_bridge import Firmware, SimSession

fw = Firmware("build/firmware.elf")                        # cached symbol lookups
with SimSession.simavr(fw, log="run.log") as sim:          # or .renode(fw, "platform.resc")
    sim.boot(probe=r"READY", idle=sim.set_idle_inputs)     # wait for the firmware
    sim.console.request("VERSION")                         # send a line, read the reply
    sim.console.exchange(b"\x01", r"OK")                   # any payload, any reply pattern
    sim.machine.drive_input("B", 1, False)                 # drive a virtual input pin
    sim.machine.read_u32(fw.symbol("my_global"))
    sim.reset()                                            # fresh firmware, inputs idle
```

What the library owns, per backend:

| Concern | AVR (simavr) | Renode |
|---|---|---|
| Launch | `simavr-host` with no GDB server unless asked | `renode-host` including the `.resc`, plus a `--ready-marker` file to signal completion |
| Ready when | the UART PTY appears | the ready-marker file appears (no `LoadELF` race) |
| Reset | restart the process | `pause` → `LoadELF` → `reset` (in place) |
| Console | PTY | TCP socket |
| Control | JSON control port | Robot XML-RPC |
| Teardown | `killpg` + `SIGKILL` escalation, log kept | same |

| Symbol | Purpose |
|--------|---------|
| `Firmware(path)` | the ELF; `symbol(name)` / `u32(name, i)` with the right `nm` per architecture |
| `SimSession.simavr(...)` / `.renode(...)` | launch configuration (firmware, log, env) |
| `sim.boot(probe=…, idle=…, post_reset=…)` | spawn, prepare platform, open console, start, wait for the firmware |
| `sim.console` | `exchange` / `request` / `expect` / `flush` / `send` / `send_line` / `send_bytes` |
| `sim.machine` | `read_u8` / `read_u32` / `write` / `drive_input` / `drive_inputs` |
| `sim.add_input(name, port, pin, triggered_level)` | register a virtual input for `set_idle_inputs` / `set_input` |
| `sim.reset()` | fresh firmware between tests, inputs back to idle |
| `sim.diagnose()` / `sim.log_tail()` | attach host output to a failure message |

`post_reset` runs after the firmware is up, because restoring stored settings needs a
listening console.

The library is firmware- and project-agnostic: it moves bytes, pin levels and words in and
out of a simulated target and knows nothing about any one firmware's protocol, board or
test strategy. The console's request/response primitive is:

```python
match, text = console.exchange(payload, pattern, timeout)   # send this, wait for that
```

`payload` is `str` / `bytes` / a single-byte `int`; `pattern` is a regex; `request(line)`
is just `exchange(..., line=True)` with an optional default reply pattern. There is no
`status()`, no default query byte and no default report pattern in the library — a
firmware with a status protocol (or any other semantic operation) layers it on top:

```python
class MyConsole(Console):
    def status(self, timeout=3.0):
        match, _ = self.exchange(b"?", r"<[^>\r\n]*>", timeout)
        return match.group(0)
```

Raw `send` / `expect` / `flush` / `text` need no configuration at all. For a worked
harness built on this API, see
`grbl-multi/test/e2e` (a consumer project, not part of sim-bridge).


## AI agent skill (Claude Code, CodeArts, Cline, ...)

sim-bridge ships with an **AI agent skill** so a coding agent can drive the whole
build → simulate → record → inspect loop for you. The skill is bundled inside the
Python package (`sim_bridge/skills/sim-bridge-firmware/`) and installs into your
agent's skills directory with one command — no repository clone needed:

```bash
sim-bridge-skill list        # show the bundled skill, its files, and source path
sim-bridge-skill install     # auto-detect the agent skills directory
```

Explicit targets:

```bash
sim-bridge-skill install claude          # ~/.claude/skills
sim-bridge-skill install codeartsdoer    # ~/.codeartsdoer/skills
sim-bridge-skill install cline           # ~/.cline/skills
sim-bridge-skill install copilot         # ~/.copilot/skills
sim-bridge-skill install dsh             # ~/.dsh/skills
sim-bridge-skill install --dest ./.claude/skills   # any other directory
```

Options: `--force` replaces an existing copy, `--dry-run` previews without writing,
and `--symlink` links to the bundled copy instead of copying (handy in a source
checkout). Reload or restart your agent session afterwards so the skill is picked up.

Once installed, the agent knows:

- how to build Debug firmware for AVR (simavr) and STM32 (Renode) and launch the host;
- how to record GPIO / timer / UART signals to VCD and analyse them in GTKWave;
- how to set up VS Code `tasks.json` / `launch.json` for breakpoint debugging over GDB;
- how to build automated E2E tests that boot the ELF on Renode, stimulate inputs and
  assert console replies plus pin registers;
- the platform pitfalls: `-DUSE_USB=OFF`, `-DHAL_SERIAL_DMA=OFF`, `renode-run` only,
  flash overflow in Debug builds, `renode-vcd stop -f`, and the one-shot state machine.

Skill contents (also readable directly in the repository):

| File | Contents |
|------|----------|
| `src/sim_bridge/skills/sim-bridge-firmware/SKILL.md` | Entry point: workflow, platform choice, hard constraints |
| `references/avr-simavr.md` | AVR build, `simavr-host`, signal groups, UART PTY |
| `references/stm32-renode.md` | STM32 build, `renode-host`, GPIO recording, UART socket |
| `references/resc-template.md` | Renode `.resc` rules and a full template |
| `references/vscode-config.md` | VS Code debug configuration templates |
| `references/e2e-testing.md` | End-to-end firmware tests in CI: `renode-host` + `unittest`, `OnGPIO` stimulus, VCD waveform assertions |
| `references/adapting-to-your-project.md` | Adopting sim-bridge in any firmware project |
| `references/troubleshooting.md` | Build, port, state-machine, and empty-VCD errors |

## License

MIT
