|
| 1 | +# Lightpad Block M Firmware v1.1.0 — LittleFoot VM Notes |
| 2 | + |
| 3 | +Hardware testing session 2026-03-15. Devices: Lightpad Block M (fw 1.1.0), LUMI Keys Block (fw 1.3.9). |
| 4 | + |
| 5 | +## What Works |
| 6 | + |
| 7 | +- **Keep-alive**: serial request, topology, beginAPIMode, ping, packetACK — all work correctly |
| 8 | +- **Config sync**: factory sync, config request/set — all work |
| 9 | +- **SharedDataChange**: data change packets are received, ACK'd, and applied to device heap |
| 10 | +- **Program upload + execution**: LittleFoot programs uploaded via SharedDataChange DO execute |
| 11 | +- **fillPixel / makeARGB**: native functions work correctly at any (x,y) coordinate |
| 12 | +- **DNA bridge**: LUMI Keys forwards messages to Lightpad via DNA — requires beginAPIMode on LUMI Keys first |
| 13 | +- **Verified working program**: 225 unrolled fillPixel(makeARGB(255,0,255,0), x, y) calls → solid green fill ✓ |
| 14 | + |
| 15 | +## Opcode Compatibility |
| 16 | + |
| 17 | +The firmware's LittleFoot VM opcode table **differs** from the ROLI JUCE SDK source (`~/Downloads/roli-extracted/roli_blocks_basics/littlefoot/roli_LittleFootRunner.h`). |
| 18 | + |
| 19 | +### Known Working Opcodes |
| 20 | + |
| 21 | +| Byte | SDK Name | Operand | Verified | |
| 22 | +|------|----------|---------|----------| |
| 23 | +| 0x00 | halt | — | ✓ | |
| 24 | +| 0x01 | jump | int16 | ✓ (loop back-jumps work) | |
| 25 | +| 0x03 | jumpIfFalse | int16 | ✓ (loop conditionals work) | |
| 26 | +| 0x05 | retVoid | int8 | ✓ | |
| 27 | +| 0x07 | callNative | int16 | ✓ (makeARGB, fillPixel) | |
| 28 | +| 0x08 | drop | — | ✓ | |
| 29 | +| 0x0B | push0 | — | ✓ | |
| 30 | +| 0x0C | push1 | — | ✓ | |
| 31 | +| 0x0D | push8 | int8 | ✓ | |
| 32 | +| 0x0E | push16 | int16 | ✓ | |
| 33 | +| 0x10 | dup | — | ✓ | |
| 34 | + |
| 35 | +### Known Broken Opcodes |
| 36 | + |
| 37 | +| Byte | SDK Name | Result | |
| 38 | +|------|----------|--------| |
| 39 | +| 0x11 | dupOffset_01 | **ILLEGAL INSTRUCTION** — device logs error, program aborted | |
| 40 | +| 0x40 | getHeapBits | **ILLEGAL INSTRUCTION** — the BitmapLEDProgram uses this | |
| 41 | + |
| 42 | +### Uncertain / Behaves Differently |
| 43 | + |
| 44 | +| Byte | SDK Name | Observation | |
| 45 | +|------|----------|-------------| |
| 46 | +| 0x12 | dupOffset_02 | Works but produces wrong results — may be dupOffset(int8) consuming next byte as operand, OR dupOffset_02 but stack layout differs from simulation | |
| 47 | +| 0x18 | dupOffset(int8) | Produced blank screen — may not be dupOffset on this firmware | |
| 48 | +| 0x1C | dupFromGlobal | No illegal instruction but globals always returned 0 | |
| 49 | +| 0x1D | dropToGlobal | Same — silently fails | |
| 50 | +| 0x20 | add_int32 | Loop increment works in corner test context | |
| 51 | +| 0x24 | sub_int32 | Loop comparison works | |
| 52 | +| 0x36 | test_lt_int32 | Loop comparison works | |
| 53 | + |
| 54 | +### Untested but Likely Working (0x00-0x10 range) |
| 55 | + |
| 56 | +0x02 (jumpIfTrue), 0x04 (call), 0x06 (retValue), 0x09 (dropMultiple), 0x0A (pushMultiple0), 0x0F (push32) |
| 57 | + |
| 58 | +### Untested and Risky (0x11+ range) |
| 59 | + |
| 60 | +Everything from 0x11 onward might have a shifted opcode table. The safe opcodes above 0x10 that we verified (0x20 add, 0x24 sub, 0x36 test_lt) might work by coincidence or because the shift preserves their positions. |
| 61 | + |
| 62 | +## Key Bugs Fixed |
| 63 | + |
| 64 | +### 1. RemoteHeap `_expected_state()` — Full Heap Sync |
| 65 | + |
| 66 | +**Bug**: Unknown device bytes (uint16 0x100) were mapped to 0x00 in `_expected_state()`. When the target was also 0x00 (all bytes beyond the program), the diff engine skipped them. The device heap retained stale data from previous programs/firmware. |
| 67 | + |
| 68 | +**Fix**: Map unknown bytes to `target[i] ^ 0xFF`, guaranteeing they always differ from the target. Add `isAllZero` guard (matching ROLI source) to prevent flushing a blank heap. |
| 69 | + |
| 70 | +**Impact**: Without this fix, uploaded programs never executed — the device heap had corrupted data around the program. |
| 71 | + |
| 72 | +### 2. DNA Bridge Requires API Mode on All Devices |
| 73 | + |
| 74 | +**Bug**: When connecting through the LUMI Keys (USB) to the Lightpad (DNA), messages addressed to the Lightpad weren't delivered unless the LUMI Keys was also in API mode. |
| 75 | + |
| 76 | +**Fix**: The daemon already sends beginAPIMode to all devices via `_start_api_on_unconnected()`. The retry logic handles the DNA latency. No code change needed — just documented behavior. |
| 77 | + |
| 78 | +### 3. Initial TOS Value from FunctionExecutionContext |
| 79 | + |
| 80 | +**Observation**: The VM initializes `tos = 0` before calling `repaint()`. The first push operation flushes this value onto the stack, creating an extra entry that shifts all `dup_offset` references. This is by design in the VM but wasn't accounted for in our hand-assembled bytecode. |
| 81 | + |
| 82 | +**Status**: Worked around by using unrolled code. Needs proper handling if we implement loops. |
| 83 | + |
| 84 | +## Architecture for Per-Pixel LED Control |
| 85 | + |
| 86 | +The original plan: upload a BitmapLEDProgram that reads RGB565 pixel data from the heap, host writes pixels via SharedDataChange. This is how the ROLI SDK does it. |
| 87 | + |
| 88 | +**Problem**: BitmapLEDProgram requires `getHeapBits` (0x40) which is illegal on this firmware. |
| 89 | + |
| 90 | +### Alternative Approaches |
| 91 | + |
| 92 | +1. **getHeapByte (0x3E)** — Read whole bytes from heap, extract RGB565 fields with bit shifts. May work if 0x3E is a valid opcode. Needs testing. |
| 93 | + |
| 94 | +2. **Unrolled repaint with heap reads** — Instead of a loop, emit 225 hardcoded getHeapByte + fillPixel sequences. Program size: ~225 × 30 bytes = 6750 bytes (fits in 7200-byte heap, barely). But leaves almost no room for pixel data in the heap. |
| 95 | + |
| 96 | +3. **Multiple programs per frame** — Upload a new program for each frame that has hardcoded pixel colors. Extremely inefficient (full heap sync per frame). |
| 97 | + |
| 98 | +4. **Figure out the actual opcode table** — Systematically probe each opcode 0x11-0x42 to build the real opcode map. Then rewrite programs using the correct opcodes. This is the right long-term fix. |
| 99 | + |
| 100 | +5. **Firmware update protocol** — Implement firmware update in blocksd to flash newer firmware that matches the SDK. The ROLI protocol supports this (firmwareUpdatePacket 0x04). But ROLI confirmed v1.1.0 is the latest for Lightpad Block M. |
| 101 | + |
| 102 | +### Recommended Next Step |
| 103 | + |
| 104 | +**Probe the opcode table.** Write a small test harness that uploads programs using each opcode and checks for "Illegal instruction" log messages. Build the real opcode map, then rewrite BitmapLEDProgram accordingly. |
| 105 | + |
| 106 | +Key unknowns to resolve: |
| 107 | +- Is `getHeapByte` (0x3E) or `getHeapInt` (0x3F) available? |
| 108 | +- What is the actual opcode for `dupOffset(int8)`? |
| 109 | +- Do loops work if we use the correct opcodes? |
| 110 | +- What is the max ops per repaint call on this firmware? |
| 111 | + |
| 112 | +## Native Function IDs (Verified Working) |
| 113 | + |
| 114 | +| Signature | ID | Verified | |
| 115 | +|-----------|-----|----------| |
| 116 | +| makeARGB/iiiii | 0x3F83 (16259) | ✓ | |
| 117 | +| fillPixel/viii | 0xC20B (-15861) | ✓ | |
| 118 | +| repaint/v | 0x6F8D (28557) | ✓ (function ID lookup works) | |
| 119 | +| clearDisplay/v | unknown | untested (was in one test but unclear if it ran) | |
| 120 | + |
| 121 | +## Program Binary Format (Verified) |
| 122 | + |
| 123 | +``` |
| 124 | +Offset 0-1: uint16 LE checksum (matches ROLI algorithm) |
| 125 | +Offset 2-3: uint16 LE program size (bytes, including header) |
| 126 | +Offset 4-5: uint16 LE number of functions |
| 127 | +Offset 6-7: uint16 LE number of globals |
| 128 | +Offset 8-9: uint16 LE heap size |
| 129 | +Offset 10+: function table (4 bytes each: int16 func_id + uint16 code_offset) |
| 130 | +Remaining: bytecode |
| 131 | +``` |
| 132 | + |
| 133 | +Checksum algorithm: `n = programSize; for each byte from index 2: n = (3*n + byte) & 0xFFFF` |
| 134 | + |
| 135 | +Program checksum validation confirmed working — device only executes programs with matching checksums. |
| 136 | + |
| 137 | +## Test Files |
| 138 | + |
| 139 | +- `diag.py` — standalone diagnostic script for raw MIDI testing (not committed) |
| 140 | +- Corner test, green fill, and various loop programs were tested inline in `device_group.py` |
0 commit comments