# Netbridge Reverse Engineering and Emulator

## Scope

Target artifacts:

- `/games/pokemon_pro/spike3/netbridge/netbridge.elf`
- `/games/pokemon_pro/spike3/netbridge/spike3-swd.cfg`
- `/games/pokemon_pro/spike3/netbridge/rp2040.cfg`
- `/games/pokemon_pro/spike3/netbridge/version.txt`
- `/games/pokemon_pro/spike3/powerdist/powerdist_ext.hex`

Generated artifacts:

- `analysis/netbridge-emulator/netbridge_emu.py`
- `analysis/netbridge-emulator/test_netbridge_emu.py`
- `analysis/netbridge-emulator/README.md`

Static caveat: this page is based on static rootfs evidence and Ghidra decompilation of the RP2040 firmware. It has not been validated against live cabinet hardware or a logic-analyzer capture.

## Firmware Identity

Confirmed facts:

- `file games/pokemon_pro/spike3/netbridge/netbridge.elf` reports a 32-bit little-endian ARM EABI5 executable, statically linked, with debug info, not stripped.
- SHA-256 of `/games/pokemon_pro/spike3/netbridge/netbridge.elf`: `05b7f7c329da79fb24078df69c8401c7a47be5c8a855b73fb937e052bce28d85`.
- `/games/pokemon_pro/spike3/netbridge/version.txt` contains `0.98.4`.
- Ghidra import path used for this pass: `/pokemon_spike3/netbridge.elf`.
- Ghidra auto-detected language `ARM:LE:32:v8`.
- `arm-none-eabi-readelf -l` maps executable flash at virtual address `0x10000000`, file offset `0x1000`, size `0x12b40`.
- `arm-none-eabi-nm -n` shows `__flash_binary_start` at `0x10000000`, `__logical_binary_start` at `0x10000100`, and `__flash_binary_end` at `0x10013270`.

Interpretation: the image is RP2040/Pico-SDK style firmware loaded at XIP flash base `0x10000000`. The host-side game programs or verifies it through OpenOCD before talking to it over UART.

## Host Programming Path

The game and SPIKE menu binaries both contain these strings:

```text
/dev/ttyAMA2
/dev/ttyAMA5
netbridge image okay, CRC32=0x%08x
netbridge image CRC mismatch, write new image
openocd -d0 -f %s -f %s -c "program %s preverify verify reset exit"
failed to init bridge thread
/games/spike3/netbridge/spike3-swd.cfg
/games/spike3/netbridge/rp2040.cfg
/games/spike3/netbridge/netbridge.elf
```

String offsets:

- `/games/pokemon_pro/game`: `/dev/ttyAMA2` at `0x25e9468`, `/dev/ttyAMA5` at `0x25e9478`, OpenOCD command format at `0x25e9998`.
- `/games/pokemon_pro/spike3/spike_menu/game`: `/dev/ttyAMA2` at `0x3845a8`, `/dev/ttyAMA5` at `0x3845b8`, OpenOCD command format at `0x384ae8`.

`spike3-swd.cfg` configures OpenOCD `bcm2835gpio` SWD:

- SWCLK: Raspberry Pi GPIO 11.
- SWDIO: Raspberry Pi GPIO 10.
- Adapter speed: 1000 kHz.

`rp2040.cfg` is the normal RP2040 dual-core OpenOCD target config with flash bank at `0x10000000`.

Inference: startup tries to verify the RP2040 flash image, rewrites it through Pi GPIO bit-banged SWD if the CRC does not match, resets the RP2040, then opens a Pi UART for bridge messages.

## Main Firmware Loop

Ghidra decompilation of `main` shows this initialization order:

1. Enable watchdog with 1000 ms timeout.
2. Initialize ADC/version sensing.
3. Initialize nodebus.
4. Initialize host UART.
5. Initialize status LED, backbox light, power-fail handling, LCD, LCD backlight, I2S, power distribution, WS2812, and DMD.
6. Send reset flags to host with command `0x22`.
7. Send version to host with command `0x20`.
8. Loop forever updating watchdog, status, backbox light, WS2812, LCD, power distribution, ADC, host command handling, and LCD again.

The decompiled `HOST_SendVersion` sends four bytes:

```text
00 62 04 <adc_version>
```

This matches version `0.98.4`; the fourth byte is the ADC-board/version-ladder result or `0xff` if unavailable.

## Pi-Facing Host UART Protocol

`HOSTUART_Init` configures:

- RP2040 GPIO 16 and GPIO 17 to function 2.
- UART base `0x40034000`, consistent with RP2040 `uart0`.
- Baud: `2000000`.
- Format: 8 data bits, 1 stop bit, no parity per decompiled arguments.
- RX/TX FIFOs: host RX `0x1000` bytes, host TX `0x400` bytes.

`HOSTUART_SendMessage` and `HOSTUART_GetMessage` implement byte-stuffed frames:

```text
0x02                       start
command                    escaped if 0x02, 0x03, or 0x1b
sequence                   escaped if needed
payload bytes              escaped if needed
checksum                   escaped if needed
0x03                       end
```

Escaping:

- Literal `0x02`, `0x03`, or `0x1b` is sent as `0x1b` followed by `value | 0x80`.
- RX reverses escaping by masking the byte after `0x1b` with `0x7f`.

Checksum:

- The checksum byte makes `sum(command, sequence, payload..., checksum) & 0xff == 0`.
- Equivalently, `checksum = -sum(command, sequence, payload) & 0xff`.

## Host Command Map

Ghidra decompilation of `HOST_Update` gives this first-pass command map:

| Command | Meaning | Response |
| --- | --- | --- |
| `0x10..0x1f` | Power-distribution passthrough on SPIKE 3; legacy voltage/frequency/power commands on non-SPIKE 3 path | Usually forwarded or returns one-byte/few-byte synthetic hardware state |
| `0x20` | Netbridge version | `00 62 04 <adc_version>` |
| `0x21` | Backbox light set/get | Optional one-byte brightness response |
| `0x22` | Reset flags | One byte |
| `0x23` | Bridge status, then clear | 4-byte little-endian status |
| `0x24` | VBB millivolts | 4-byte little-endian value |
| `0x25` | Nodebus poll/discovery | One byte node address, or `0` |
| `0x27` | LCD reset/status/write/backlight subcommands | Status response for query paths |
| `0x28` | DMD status/test/write | Status response for query path |
| `0x29` | WS2812 count/debug setup | No direct response in the decoded path |
| `0x2a` | WS2812 direct data path | No direct response in the decoded path |
| `0x2b` | WS2812 pattern path | No direct response in the decoded path |
| `0x2c` | I2S enable | No direct response |
| `0x2f` | WS2812 node LED path | No direct response in the decoded path |
| `0x30` | Netbridge firmware CRC | 4-byte CRC32 |
| `0x40` | Extended bridge command namespace | Subcommand-dependent response |
| `0x41` | Direct nodebus request/response path | Node response bytes plus bridge status |

Extended command `0x40` subcommands currently identified:

| Subcommand | Meaning |
| --- | --- |
| `0x00` | Nodebus poll/discovery |
| `0x03` | Version bytes `00 62 04` |
| `0x04` | Enter/exit node break mode |
| `0x05` | Bridge status, then clear |
| `0x06` | Set node response-time timeout in milliseconds |
| `0x09` | Backbox light helper |
| `0x0a` | Backbox light get helper |
| `0x0c` | Deliberate infinite loop / hang path |
| `0x0d` | Four-slot seeded hash/mixing helper |

The `0x30` CRC is computed in firmware as:

```c
CRC32(0, &__logical_binary_start, 0x12a40)
```

For this ELF, `__logical_binary_start` maps to file offset `0x1100`. A Python `binascii.crc32` over `games/pokemon_pro/spike3/netbridge/netbridge.elf[0x1100:0x1100+0x12a40]` gives `0x357f5327`.

## Power-Distribution Cabinet Inputs

Confirmed static facts:

- SHA-256 of `/games/pokemon_pro/spike3/powerdist/powerdist_ext.hex` is `f8eb96998a32ff86008663f7f07f2df76c32a9c86200f690c215dc36917391a9`; it is a 32,799-byte Intel HEX file whose decoded address range starts at `0x3800`.
- In `netbridge.elf`, `HOST_Update` at `0x10001210` calls `POWERDIST_IsSpike3`. For a SPIKE 3 controller, commands `0x10..0x1f` are passed to `POWERDIST_SendCmd` at `0x100020b4`; the legacy non-SPIKE-3 command `0x15` fallback is the unrelated one-byte `0xff` response.
- `POWERDIST_Update` at `0x10002174` parses controller frames and forwards their original command, sequence, and payload to the Pi with `HOSTUART_SendMessage`. These frames are not restricted to replies to a Pi request.
- Disassembling the decoded power-distribution image as Thumb code at `0x3800` shows command `0x15` dispatch at `0x427c`. It calls `0x475c` and returns exactly one payload byte.
- Function `0x46b4` composes that byte from raw GPIO P0_8, P0_9, and P0_11 as bits 0, 1, and 2. The installed cabinet map assigns Action to bit 0 and Start to bit 1; bit 2 is the coin-door power interlock state.
- Most importantly, the controller's input-change path at `0x407a` calls the frame sender at `0x3f9c` with command `0x15`, sequence `0`, a one-byte payload, and length `1`. A separate command handler at `0x428a` provides the same state for explicit polls.

The last point explains the VPX failure introduced while adding the first
power-distribution model. The emulator answered explicit `0x15` polls but did
not reproduce the controller's unsolicited change report, so a Start edge
could exist inside the emulator without ever reaching game code. The repaired
UART service queues every changed Start/Action state as command `0x15`,
sequence `0`, and drains those frames independently of incoming host traffic.
An integration test uses a socket pair to prove that a Start transition emits
`15 00 01` at the decoded protocol level without first sending a poll.

Reproduction commands used for the static controller pass:

```sh
arm-none-eabi-objcopy -I ihex -O binary \
  games/pokemon_pro/spike3/powerdist/powerdist_ext.hex /tmp/powerdist_ext.bin
arm-none-eabi-objdump -D -b binary -m arm -M force-thumb \
  --adjust-vma=0x3800 /tmp/powerdist_ext.bin
```

## Nodebus Layer

`NODE_Init` configures a mixed PIO/hardware-UART nodebus:

- PIO block base `0x50300000`, consistent with RP2040 PIO1.
- PIO state machine 0 uses `uart_tx_txen_program`.
- GPIO 20 and GPIO 19 are assigned PIO function 7 around the transmit/TX-enable path.
- GPIO 21 is assigned UART function 2 around the receive path.
- Hardware UART base `0x40038000`, consistent with RP2040 `uart1`.
- Decompiled baud literal is `0x70800`, decimal `460800`.

`NODEUART_WriteAddress(address)` resets node RX state and sends `address | 0x180` into the PIO TX FIFO. `NODEUART_Write(byte)` subtracts the byte from `nodeTxCs` and queues it. `NODEUART_SendEnd(count)` sets the expected receive count and response timing window.

`NODEBUS_Poll` performs a binary-search-style poll over node addresses up to `0x3f`, using `NODEUART_WritePoll(address)`. It returns the discovered address or `0`.

Direct host command `0x41` has special handling in `HOSTUART_GetMessage`: as the host frame is parsed, the payload is echoed to the physical nodebus. The payload shape inferred from the decompile and the earlier game-side node wrapper is:

```text
byte 0: node address, high bit accepted by higher layers
byte 1: node argument count
byte 2..N: node command and arguments
last: expected response length
```

The bridge then reads node response bytes and sends host command `0x41` back with the node response followed by bridge status. This is the lower layer used by the previously documented Magikarp/topper commands such as `0xfc`.

## Other Hardware Functions

Confirmed from Ghidra decompilation:

- ADC/version sensing uses GPIO 26 and GPIO 29 as ADC inputs.
- LCD helper initializes GPIO 3, 6, and 7.
- LCD backlight uses GPIO 14 PWM plus GPIO 15 enable/control.
- Backbox light uses GPIO 23 PWM.
- Status output uses GPIO 25.
- Power-fail output/input uses GPIO 24.
- Power-distribution UART uses PIO1 state machines 2 and 3, with GPIO 27 TX and GPIO 28 RX.
- WS2812 output uses PIO0 and three configured output pin slots from the firmware's `pins` table.
- DMD work is launched on RP2040 core 1.

Open validation:

- Exact board connector routing for each RP2040 GPIO.
- Nodebus electrical level, half-duplex direction details, and break timing on real hardware.
- Exact meaning of every `bridgeStatus` bit.
- Which of `/dev/ttyAMA2` and `/dev/ttyAMA5` is the Pi-facing host UART on the real image boot path.

## Emulator

`analysis/netbridge-emulator/netbridge_emu.py` implements a first host-side recreation in Python.

It currently provides:

- STX/ETX/ESC host frame codec.
- Host checksum encode/decode.
- Commands `0x20`, `0x22`, `0x23`, `0x24`, `0x25`, `0x27`, `0x28`, `0x30`, `0x40`, and `0x41`.
- Inert ACK/no-response handling for early LCD/DMD/WS2812/I2S paths.
- Firmware CRC response `0x357f5327`.
- PTY, serial-device, and Unix-socket serving modes for QEMU or host-side serial plumbing.
- Existing-node stub at address `0x0a` with Magikarp/topper-oriented commands `0xf2`, `0xfc`, `0xfd`, `0xfe`, and `0xff`.
- Power-distribution command `0x15` polling plus unsolicited sequence-0
  Start/Action change reports matching the installed controller firmware.

Run:

```sh
python3 analysis/netbridge-emulator/netbridge_emu.py --pty
```

Run as a QEMU socket backend:

```sh
python3 analysis/netbridge-emulator/netbridge_emu.py --unix-socket emulation/work/netbridge.sock
```

Test:

```sh
python3 -m unittest discover -s analysis/netbridge-emulator
```

Verification performed:

```text
python3 -m unittest discover -s analysis/netbridge-emulator
...................................................................
Ran 67 tests
OK

python3 -m py_compile analysis/netbridge-emulator/netbridge_emu.py analysis/netbridge-emulator/test_netbridge_emu.py
```

## Emulation Strategy

For QEMU:

1. Set `SPIKE3_NETBRIDGE_EMU=1` for the Docker/QEMU scaffold.
2. The container starts `netbridge_emu.py --unix-socket /workspace/emulation/work/netbridge.sock`.
3. QEMU attaches that socket as a second PL011 serial port, which appears as guest `/dev/ttyAMA1`.
4. Guest setup symlinks `/dev/ttyAMA5` to `/dev/ttyAMA1`, because current preload logs show the target opening `/dev/ttyAMA5`.
5. `launch-target.sh` exports `SPIKE3_REAL_NETBRIDGE_SERIAL=1`, causing the preload shim to stop faking `/dev/ttyAMA5`.
6. If the game still tries to run OpenOCD before the UART bridge thread is useful, either allow the CRC/status path to bypass programming or stub OpenOCD success until the UART bridge thread opens.
7. Route dashboard switch events into the emulator's node model, not directly into the game.

Docker command:

```sh
SPIKE3_NETBRIDGE_EMU=1 SPIKE3_AUTOSTART=game docker compose -f emulation/compose.yaml up --build
```

Current validation boundary: the Python emulator tests, shell syntax checks, and QEMU wiring edits were verified locally, but the full Docker boot path with `SPIKE3_NETBRIDGE_EMU=1` has not yet been run to confirm the game's bridge thread accepts the emulated serial device.

For a microcontroller recreation:

- RP2040 is the best exact physical target because the original uses RP2040 PIO for node TX/TX-enable, WS2812, and power-distribution UART.
- ESP32 can still emulate the Pi-facing protocol if it only replaces the bridge at the host UART layer, or if its UART/RMT/peripheral timing can satisfy the nodebus physical layer.
- The minimum viable firmware only needs host UART framing, command responses, and a nodebus model that returns `payload || checksum || status` for command `0x41`.

## Pico Firmware Recreation

`analysis/netbridge-pico-firmware/` contains a Pico SDK recreation of the same Pi-facing protocol layer.

Files:

- `CMakeLists.txt`
- `include/netbridge_protocol.h`
- `src/netbridge_protocol.c`
- `src/netbridge_model.c`
- `src/pico_main.c`
- `tests/host_protocol_test.c`
- `README.md`

Implemented behavior:

- UART0 on RP2040 GPIO 16/17 at 2,000,000 baud.
- Same STX/ETX/ESC host framing and checksum as the original netbridge.
- Same core command stubs as the Python emulator.
- Firmware CRC response `0x357f5327`.
- Stub nodebus device at address `0x0a`.
- Pico GPIO 25 heartbeat LED.

Portable host-core test:

```sh
cmake -S analysis/netbridge-pico-firmware -B /tmp/spike3-netbridge-host
cmake --build /tmp/spike3-netbridge-host --target netbridge_host_test
/tmp/spike3-netbridge-host/netbridge_host_test
```

Pico UF2 build, once the Pico SDK is available:

```sh
export PICO_SDK_PATH=/path/to/pico-sdk
cmake -S analysis/netbridge-pico-firmware -B /tmp/spike3-netbridge-pico
cmake --build /tmp/spike3-netbridge-pico --target spike3_netbridge_pico
```

Verification performed for this pass:

```text
cmake -S analysis/netbridge-pico-firmware -B /tmp/spike3-netbridge-host
cmake --build /tmp/spike3-netbridge-host --target netbridge_host_test
/tmp/spike3-netbridge-host/netbridge_host_test
netbridge host protocol tests passed
```

The UF2 was not built in this pass because `PICO_SDK_PATH` is not configured in the workspace environment.

## Confidence

High confidence:

- RP2040 firmware identity, version, OpenOCD programming path, and SWD GPIO mapping.
- Pi-facing UART frame format and checksum.
- Host command meanings for version, status, CRC, node poll, and direct node request.
- Host UART baud and RP2040 GPIO 16/17 assignment.
- Nodebus use of PIO plus UART1 and command `0x41` as the node request path.

Moderate confidence:

- The Python emulator or Pico firmware recreation is sufficient to clear early bridge-thread and netbridge CRC paths once connected to the correct guest UART.
- Default zero/success responses for LCD/DMD/WS2812/I2S are enough for early game boot.

Low confidence until hardware-tested:

- Physical nodebus timings, connector pins, and voltage levels.
- Full power-distribution behavior.
- Complete `bridgeStatus` bit semantics.
