Add Skylanders Portal of Power support#20
Draft
pathawks wants to merge 1 commit into
Draft
Conversation
Read NFC figures placed on the Skylanders portal over WebHID and back each one up as its raw 1024-byte tag dump, following the same scanner-style pattern as the Disney Infinity base. The portal firmware performs the MIFARE Classic authentication itself, so the driver only issues per-block QUERY reads — no keys, no decryption. Encrypted application blocks are saved verbatim; only the plaintext identity (NUID, figure id, variant id) is parsed, for display and the dump filename. No character database ships. Works in Chrome on macOS and Windows. Linux is unsupported: the portal only accepts host writes via SET_REPORT Output, which Chrome on Linux can't issue; the device description notes this.
There was a problem hiding this comment.
Pull request overview
Adds first-class support for the Skylanders “Portal of Power” WebHID device, enabling nabu to auto-detect figure placement, read full 1K MIFARE dumps via the portal’s block query command, and save raw .bin tag dumps through a dedicated scanner-style UI (similar to the existing Disney Infinity flow).
Changes:
- Introduces a new Portal of Power HID protocol layer + driver with status-delta slot events and full figure block dumping.
- Adds a new
PortalScannerwizard UI for per-slot read state, LED feedback, and.binsaving. - Wires the new device into the device registry, connection registry, and App scanner routing.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 15 comments.
Show a summary per file
| File | Description |
|---|---|
| src/lib/drivers/portal-of-power/portal-figure-file.ts | Parses plaintext identity fields and builds dump filenames. |
| src/lib/drivers/portal-of-power/portal-figure-file.test.ts | Unit tests for identity parsing, dump-size detection, and filename format. |
| src/lib/drivers/portal-of-power/portal-driver.ts | Implements the Portal of Power WebHID driver, command serialization, status handling, and block/figure reads. |
| src/lib/drivers/portal-of-power/portal-commands.ts | Defines opcodes/constants and status bitmap decode + diff logic. |
| src/lib/drivers/portal-of-power/portal-commands.test.ts | Unit tests for status decoding/diffing and slot index encoding. |
| src/lib/core/devices.ts | Adds the Portal of Power to the canonical supported device list (with Linux limitation note). |
| src/lib/core/connection-registry.ts | Registers the new transport filters and driver constructor for connection flow. |
| src/components/wizard/portal-scanner.tsx | Adds the scanner UI for reading figures from slots and saving .bin dumps with LED state feedback. |
| src/App.tsx | Routes connected “skylanders” scanner devices to PortalScanner and excludes them from generic auto-detect flow. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // null prior yields an `added` for every currently-occupied slot. | ||
| const status = driver.currentStatus; | ||
| if (status) for (const ev of diffStatus(null, status)) handleEvent(ev); | ||
| void setLed(LED_OFF); |
Comment on lines
+67
to
+68
| const readGen = new Map<number, number>(); | ||
| let lastLed: Rgb | null = null; |
|
|
||
| const readSlot = async (slot: number, gen: number) => { | ||
| try { | ||
| const data = await driver.readFigure(slot); |
Comment on lines
+130
to
+132
| return () => { | ||
| driver.onTagEvent(null); | ||
| }; |
Comment on lines
+217
to
+222
| if (resp.length < 3 + BLOCK_SIZE) { | ||
| throw new Error( | ||
| `Portal returned a short block (${resp.length} bytes) for slot ${slot}, block ${block}`, | ||
| ); | ||
| } | ||
| return resp.slice(3, 3 + BLOCK_SIZE); |
| // null prior yields an `added` for every currently-occupied slot. | ||
| const status = driver.currentStatus; | ||
| if (status) for (const ev of diffStatus(null, status)) handleEvent(ev); | ||
| void setLed(LED_OFF); |
Comment on lines
+67
to
+68
| const readGen = new Map<number, number>(); | ||
| let lastLed: Rgb | null = null; |
|
|
||
| const readSlot = async (slot: number, gen: number) => { | ||
| try { | ||
| const data = await driver.readFigure(slot); |
Comment on lines
+130
to
+132
| return () => { | ||
| driver.onTagEvent(null); | ||
| }; |
Comment on lines
+217
to
+222
| if (resp.length < 3 + BLOCK_SIZE) { | ||
| throw new Error( | ||
| `Portal returned a short block (${resp.length} bytes) for slot ${slot}, block ${block}`, | ||
| ); | ||
| } | ||
| return resp.slice(3, 3 + BLOCK_SIZE); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds support for the Skylanders "Portal of Power" — the USB NFC base — so nabu can read a figure placed on the portal and back it up as a raw tag dump. It follows the same WebHID, scanner-style flow as the existing Disney Infinity base: connect, place a figure, it's read automatically, and you save a
.bin.How it works
The portal is a fixed 32-byte HID device. The driver activates the base, watches its ~10 Hz status stream to learn which slots are occupied, and reads each present figure block-by-block (64 × 16 B = 1024 B) through the portal's
QUERY_BLOCKcommand.The host needs no MIFARE keys and performs no decryption. The portal firmware does the MIFARE Classic authentication itself, so the driver simply requests blocks and writes back exactly what the licensed reader returns. A figure's encrypted application blocks are saved verbatim; only the plaintext identity (NUID, figure id, variant id) is parsed — for the on-screen row and the dump filename. No character/name database is bundled.
Platform support
Works in Chrome on macOS and Windows. Linux is not supported: the portal only accepts host writes via the
SET_REPORTOutput control request, which Chrome on Linux can't issue — it routes WebHID writes throughhidrawto the firmware's broken interrupt-OUT endpoint, and no udev rule changes that. The device description states the limitation so Linux users aren't surprised by a connect failure.Note
The macOS/Windows path hasn't yet been exercised end-to-end in a browser. That's the remaining validation step before this leaves draft.
Files
src/lib/drivers/portal-of-power/—portal-commands.ts(wire format, opcode tables, status bitmap decode/diff),portal-driver.ts(PortalOfPowerDriver),portal-figure-file.ts(identity parse + filename), plus unit tests.src/components/wizard/portal-scanner.tsx— the scanner UI (per-slot read state, LED feedback, save).devices.ts,connection-registry.ts, andApp.tsx;use-connection.tsis untouched since it's already data-driven over the device registry.Licensing
No third-party code or data is vendored. The protocol is referenced from Dolphin's
Skylander.cpp(GPL-2.0-or-later, compatible with this project's GPL-3.0) in a header comment only — the same way the Disney Infinity driver references Dolphin — soTHIRD-PARTY-LICENSESis unchanged.Testing
npm run lint,tsc -b, andvitestpass (176 tests, including new coverage for the status bitmap decode/diff and the figure identity parse).