167 lines
6.9 KiB
Markdown
167 lines
6.9 KiB
Markdown
# Magikarp Topper Serial Key and Emulator
|
|
|
|
## Scope
|
|
|
|
Target:
|
|
|
|
- `/games/pokemon_pro/game`
|
|
|
|
Generated artifacts:
|
|
|
|
- `analysis/topper-emulator/topper_emulator.py`
|
|
- `analysis/topper-emulator/README.md`
|
|
|
|
Static caveat: this page is based on static rootfs files and targeted Ghidra decompilation. It does not include live cabinet or logic-analyzer validation.
|
|
|
|
## Transport Finding
|
|
|
|
The Magikarp topper path uses the generic SPIKE node-board bus.
|
|
|
|
Evidence:
|
|
|
|
- `/games/pokemon_pro/spike3/netbridge/netbridge.elf` contains debug/source strings including `uart_node.c`, `NODEBUS_Poll`, `NODEUART_Write`, `NODEUART_WritePoll`, and `NODEUART_WriteAddress`.
|
|
- `/games/pokemon_pro/game` function `FUN_00a21a20` constructs addressed node frames and calls lower transport helper `FUN_00a4c4c0`.
|
|
- `/games/pokemon_pro/game` functions `FUN_0090aff0`, `FUN_00a23450`, `FUN_00a26690`, and `FUN_00a26700` implement discovery/address-management around node addresses, not USB or cloud registration.
|
|
|
|
Inference: the MicroPython emulator should target a UART-style nodebus interface first. The rootfs does load `i2c-dev`, but the topper-specific evidence reviewed here points to nodebus UART. Exact pins, voltage, baud, and half-duplex behavior still need hardware validation.
|
|
|
|
## Frame Format Recovered From `FUN_00a21a20`
|
|
|
|
For addressed commands, higher-level helpers pass an application frame shaped like:
|
|
|
|
```text
|
|
byte 0: node_address | 0x80
|
|
byte 1: application argument count
|
|
byte 2..N: command and arguments
|
|
last: response payload length
|
|
```
|
|
|
|
`FUN_00a21a20` converts that to this wire frame:
|
|
|
|
```text
|
|
byte 0: node_address | 0x80
|
|
byte 1: application argument count + 1
|
|
byte 2..N: command and arguments
|
|
next: subtractive checksum over bytes before the checksum
|
|
last: response payload length + 2
|
|
```
|
|
|
|
The response is expected as:
|
|
|
|
```text
|
|
bytes 0..payload_len-1: payload
|
|
next: subtractive checksum over payload
|
|
last: status bits; status bits 0x0c cause the game to reject the response
|
|
```
|
|
|
|
Discovery is special: `FUN_00a23450` sends a single zero byte and expects one raw response byte containing a discovered node address.
|
|
|
|
## Serial Number Key Algorithm
|
|
|
|
The strongest recovered key path is:
|
|
|
|
- `FUN_006a9620` gets logical node device `10` with `FUN_007cbc80(10)`.
|
|
- It verifies the node is available with `FUN_007cbfb0(10, 1)`.
|
|
- It passes the configured node address byte from device entry offset `0x1a` to `FUN_00a28a00(address, out)`.
|
|
- `FUN_00a28a00` sends command `0xfc` with expected response length `0x10`.
|
|
- `FUN_006a9620` treats the 16 returned bytes as four little-endian 32-bit words and computes:
|
|
|
|
```c
|
|
key = (word0 ^ word1 ^ word2 ^ word3) & 0x7fffffff;
|
|
```
|
|
|
|
This is the recovered algorithm for the Magikarp topper serial-number key material read from the control board.
|
|
|
|
Important namespace note:
|
|
|
|
- The public adjustment name table places `AD_MAGIKARP_TOPPER_SERIAL_NUMBER_KEY` at index `0x1a3`. Nearby entries are:
|
|
- `0x19b`: `AD_MAGIKARP_MECH_DISABLED`
|
|
- `0x19c`: `AD_MAGIKARP_HEAD_HI_LIMIT`
|
|
- `0x19d`: `AD_MAGIKARP_HEAD_LOW_LIMIT`
|
|
- `0x19e`: `AD_MAGIKARP_TAIL_HI_LIMIT`
|
|
- `0x19f`: `AD_MAGIKARP_TAIL_LOW_LIMIT`
|
|
- `0x1a0`: `AD_MAGIKARP_WATER_CENTER`
|
|
- `0x1a1`: `AD_MAGIKARP_ATTRACT_BEHAVIOR`
|
|
- `0x1a2`: `AD_MAGIKARP_ATTRACT_FREQUENCY`
|
|
- `0x1a3`: `AD_MAGIKARP_TOPPER_SERIAL_NUMBER_KEY`
|
|
- The Magikarp runtime functions use constants such as `0xa6..0xaa` and `0xad` with `FUN_007d3330` / `FUN_007d39c0`. Static table dumping showed those are not direct indexes into the public `AD_*` name-pointer table, so they should be treated as a separate adjustment-access namespace or translated runtime IDs.
|
|
|
|
## Persistence and Calibration Path
|
|
|
|
`FUN_006a9570` compares the recovered key from object offset `+0x0c` against `FUN_007d3330(0xad)`. If the stored value is zero or mismatched, and the calibration/config payload is present and valid, it stores the recovered key with `FUN_007d39c0(0xad, key)` and marks the local state as synchronized.
|
|
|
|
Calibration validity is checked by `FUN_006a8f50`:
|
|
|
|
```text
|
|
byte 0: 0x01
|
|
byte 1..3: ASCII "-15"
|
|
byte 4..8: five Magikarp calibration bytes
|
|
byte 9..10: little-endian ((sum(bytes 4..8) ^ 0x93d3) & 0xffff)
|
|
```
|
|
|
|
`FUN_006a9920` constructs the same payload from five calibration adjustment reads. Static dump of the pointer used as the three-byte magic resolves to target address `0x0268ab40`, whose first bytes are ASCII `-15`.
|
|
|
|
`FUN_0090c290` and `FUN_0090c310` copy a 64-byte node config/cache block from the generic node record. They treat bytes `0x3c..0x3f` equal to `0x3254789a` as a stored-valid marker.
|
|
|
|
## Emulator
|
|
|
|
The generated emulator is `analysis/topper-emulator/topper_emulator.py`.
|
|
|
|
It implements:
|
|
|
|
- Discovery request `0x00`.
|
|
- Address-management command `0xf0` as a no-op ACK path.
|
|
- Config read command `0xf2` in 8-byte chunks.
|
|
- Serial-key command `0xfc`, returning the 16-byte key material.
|
|
- Adjacent serial/cache helper command `0xfd`.
|
|
- Identity command `0xfe`.
|
|
- Status command `0xff`.
|
|
- Selected provisioning/runtime commands as ACK/no-op paths: `0x14`, `0x40`, `0x41`, `0x42`, `0x43`, `0x44`, `0x45`, `0x46`, `0x48`, `0x4d`, `0x72`, `0xf4`, `0xf8`.
|
|
|
|
Host-side validation performed:
|
|
|
|
```text
|
|
python3 -m py_compile analysis/topper-emulator/topper_emulator.py
|
|
```
|
|
|
|
Frame self-test:
|
|
|
|
```text
|
|
request: 8a02fc7812
|
|
response length: 18
|
|
response payload matched serial material: True
|
|
response status: 0
|
|
```
|
|
|
|
The sample request is command `0xfc` for node address `10`, using the recovered wire-frame checksum and expected response length.
|
|
|
|
## Open Hardware Variables
|
|
|
|
These values must be matched against a real board or a known-good capture:
|
|
|
|
- `NODE_ADDRESS`: logical device `10` resolves to a configured node address at runtime via `FUN_007cbc80(10)`, device-entry offset `0x1a`. This value was not recovered as a fixed static literal.
|
|
- UART ID, pins, baud, electrical level, and bus direction control.
|
|
- `IDENTITY_BOARD_PART_ID`: command `0xfe` returns an 11-byte identity payload. The game compares its part ID against the expected node-device table. This part ID was not confidently recovered from static analysis.
|
|
- Exact serial/version fields expected by diagnostics.
|
|
- Whether live netbridge behavior requires breaks, XON/XOFF, timing gaps, or address arbitration before the game accepts a MicroPython device on the physical bus.
|
|
|
|
## Confidence
|
|
|
|
High confidence:
|
|
|
|
- Command `0xfc` supplies 16 bytes used for the key calculation.
|
|
- The key calculation is XOR of four little-endian dwords masked with `0x7fffffff`.
|
|
- Logical topper node device ID is `10`.
|
|
- Calibration block predicate and checksum are recovered.
|
|
- The implemented node response checksum/status shape matches `FUN_00a21a20`.
|
|
|
|
Moderate confidence:
|
|
|
|
- The implemented no-op handling is enough for startup/provisioning if identity fields match.
|
|
- Returning zero in the second status word for command `0xff` avoids the generic unsigned/problem bit paths.
|
|
|
|
Low confidence until hardware-tested:
|
|
|
|
- Physical-layer UART settings and direction handling.
|
|
- Correct `IDENTITY_BOARD_PART_ID` for a live Magikarp topper board.
|