NESynth — NES on FM-1 / SMK37 — v2.0
SMK pad-light release 971: stable colors during pad presses and Fader 2 brightness changes, validated on SMK Pro. Test evidence. FM-1 firmware and original-firmware restore are unchanged.
Now with Bluetooth and Multi-game storage.
- Install the core once, then add games over Bluetooth or USB.
- Keep multiple games installed across power cycles, select them on the device, and see remaining space.
- Manage the same game library from either connection, including play and delete.
- Separate Install firmware and Add games tabs, with automatic firmware selection and drag-and-drop games.
The public release name is v2.0; internal USB firmware build numbers remain unchanged for device compatibility. Cartridge save RAM remains volatile.
Shared InfoNES core for a local browser player and dual-core FM-1/SMK37 firmware. Supports NTSC, mapper 0 (NROM), 1 (MMC1), 2 (UxROM), 3 (CNROM), 4 (MMC3), 9 (MMC2) and 11 (Color Dreams), one controller. Includes From Below, CodeMaster, Diamond Chase, and an original diagnostic generated by tools/diagnostic.py. Homebrew licenses and pinned sources are in web/homebrew/ and the website credits; no commercial ROMs are included.
The prototype fits the existing FM-1 memory map. The website plays NES/ZIP cartridges, browses a user-selected ROM folder, simulates FM-1 and SMK37 controls, adds games to the device library over Bluetooth or USB, and installs or restores FM-1 and SMK-37 Pro/Elite firmware over USB MIDI. FM-1 picture, controls, both cores, and update/restart have been exercised. Punch-Out reached approximately 60 emulated frames/sec with audio-prioritized adaptive drawing; screen refresh is lower and other games remain experimental. SMK-37 Pro installation and gameplay have been exercised with the owner build. The calibration-free public SMK core and Elite hardware remain unvalidated.
Live site: https://chancethemaker.github.io/NESynth-FM1-SMK/
Mapper 7 / AxROM (SMK969 branch build)
The shared emulator now supports standard iNES AxROM with 32-256 KiB PRG ROM, 8 KiB fixed CHR RAM, 32 KiB bank switching and mapper-selected single-screen mirroring. It follows the conflict-free iNES convention documented by NESdev; NES 2.0 variants and oversized boards remain unsupported. The SMK969 library firmware is required for mapper 7 uploads. Existing public firmware templates predate this mapper and retain their explicit compatibility checks.
The supplied Who Framed Roger Rabbit ZIP completed 3,600 local WASM frames with audio and an inspected gameplay image. Full, skipped and adaptive rendering produced identical audio and presented frames. Host and sanitizer tests exercise all bank register values at every supported ROM size, both VRAM pages, CHR RAM persistence, reset and invalid boards. ROMs and private calibration packages remain local.
SMK969 is installed on the owner's SMK-37 Pro with its three-game catalog preserved and 76 KiB free. Application size is 216,004 of 216,800 bytes (796 spare); shared storage remains 392 KiB. Roger Rabbit needs 132 KiB including sector rounding, so hardware gameplay awaits sufficient free space. No existing game was deleted. Evidence: experiments/bluetooth/smk969-hardware.json.
USB and Bluetooth game library (SMK-37 Pro, branch build 969)
The shared renderer now uses the game's backdrop palette entry for hidden background pixels, fixing a gray left-edge strip when games disable the first eight background pixels. Sprite clipping independently suppresses sprite composition instead of erasing the background. Regression coverage exercises all background/sprite enable, left-edge mask and sprite-priority combinations with multiple backdrop colors. Sanitizer and WASM checks pass. The SMK update is installed with both existing games preserved; Tecmo Baseball runs at approximately 60 emulated frames/sec with CPU1 active and no display errors. The owner confirmed the SMK gray bar is gone. The FM-1 update is also installed, preserving Tennis and Three Stooges. Bluetooth discovery/connection and exact library readback pass; Three Stooges launches with its stored checksum intact at approximately 60 emulated frames/sec with CPU1 active. Physical FM-1 screen confirmation is pending.
SMK969 includes an original NESynth pixel-art loading screen and an on-device game library. The library has 392 KiB of shared game storage and up to 32 entries, reclaimed from unused application flash. Each cartridge is still limited to 262,672 bytes (256 KiB plus header/trainer); the limit comes from the shared RAM staging buffer. Space is based on extracted NES data, rounded up to 4 KiB flash sectors, not ZIP size. A new upload adds a game; an identical length/CRC reuses the existing entry.
At startup, use the existing directional keys/pads to select a game and REC / Start (or A) to launch it. Hold PLAY + REC during gameplay to return to the library. The screen shows remaining space. Press PLAY / Select in the library to enter Bluetooth loading; press REC / Start in the idle Bluetooth screen to return. Fader 1 controls volume. On SMK, Fader 2 adjusts pad brightness from off to full (0-255); startup stays at 150 until the fader moves. Held pads go dark and relight on release. Bluetooth remains off during gameplay, library browsing and USB game loading.
Open the main page’s Add games tab in Chrome or Edge through HTTPS or a localhost web server. web/bluetooth.html remains available and uses the same loader controller. Choose USB cable or Bluetooth in the connection selector; both use the same game library, file picker, space indicator and Play/Delete controls. For USB, attach a data cable, click Connect device and allow MIDI/SysEx access. The page opens USB loading automatically, with the radio off; the device says USB READY. For Bluetooth, press PLAY (Select) in the library, wait for SEND A GAME, and connect NESynth SMK. Drop a .nes or .zip and choose Send and play. This sends the cartridge only, verifies it, restarts to save it, and launches the game. Idle connection checks pause while importing or dragging and never close a live connection after a transient failure. File drops during a check are accepted; page-wide drop protection prevents accidental navigation. USB game loading requires SMK968; older builds retain their Bluetooth behavior.
The USB fallback uses bounded, sequenced MIDI SysEx messages to carry the same cartridge/library commands as BLE. Requests are rejected outside exclusive USB loading mode, so USB cannot write into gameplay or active-radio buffers. Add, duplicate detection, CRC checks, storage allocation and flash publication are shared. It does not invoke the firmware updater. A failed cable connection leaves existing stored games intact; reconnect and retry the game upload. The loader build omits the obsolete radio-diagnostic-only USB backup/status commands to make room, while retaining private RF validation and normal USB recovery.
python tools/build_bluetooth_probe.py --loader
node tools/package_bluetooth_loader.mjs
python -m http.server 8766 --bind 127.0.0.1 --directory web
# Open http://127.0.0.1:8766/bluetooth.html
The build requires the pinned local Hortator/AC79 dependencies described below. This owner's unit also needs the verified private calibration backup options; --loader accepts --private-calibration-backups and --private-unit-backups pairs. These local artifacts stay under ignored build/bluetooth-loader/; PRIVATE firmware must not be published. The main page’s Install firmware tab automatically downloads and packages the NESynth core for the selected model. Private calibration is not bundled with the website. For this owner’s validated SMK core, start the loopback preview with python tools/serve.py --port 8817 --owner-firmware; it serves only the exact verified local application through an allowlisted route. The older SMK963 template remains a packaging/test fixture. USB firmware replacement clears the library; keep your original games on the computer. Battery save RAM remains volatile.
The final application uses 215,768 of 216,800 bytes before the library boundary, leaving 1,032 bytes. Catalogs occupy 0x39000..0x3afff; game data uses 0x3b000..0x9cfff (401,408 bytes). Boot code, VM/music storage and factory data remain outside the writable region. Additions write only unused sectors, read back the complete game, then publish a CRC-protected catalog in the alternate metadata bank, committing its marker last. Deletion publishes a new catalog without erasing other games. Interrupted operations retain the previous valid catalog or the fully committed new one. Deleted holes are reused; there is no automatic compaction, so the largest available contiguous slot can be smaller than total free space. The browser reports this when a game will not fit.
Uploads retain the SMK966 speed improvements: bounded bursts of eight packets, cumulative progress acknowledgments, missing-data replay with a strict retry limit, 251-byte Data Length Extension and a requested 15-30 ms connection. Link tuning waits until application access so Windows service discovery can finish. Control messages remain acknowledged and complete cartridge CRC verification remains mandatory. The pinned SDK is unchanged; only a generated configuration copy is adapted. The historical SMK966 Mario transfer measured 8.95-10.99 seconds versus 62.2 seconds on SMK965; see experiments/bluetooth/smk966-hardware.json. SMK967 hardware checks saved Super Pitfall and Mario together, reported 216 KiB free, and launched Mario from the stored library at 60.2 emulated frames/sec with CPU1 active and no new audio underruns. Mario uploaded in 10.11 seconds with zero retries. See experiments/bluetooth/smk967-hardware.json for the precise scope; on-device deletion remains untested, while simulated deletion/recovery tests pass.
SMK968 hardware verified (2026-10-16): a complete 163,856-byte Xenophobe USB transfer verified in 4.31 seconds with the SMK radio off, then launched with its stored checksum intact. The existing two-game catalog and free space were unchanged. This duplicate-game test exercises the USB transfer/verification path without creating another copy. Native browser hardware transfer is not automated; the actual page handlers are tested against simulated Web MIDI, with Bluetooth absent. Details: experiments/bluetooth/smk968-hardware.json.
For local verification, tools/upload_usb_game.py GAME.zip uploads over USB; omit the game to list the shared library. tools/upload_bluetooth_game.py GAME.zip uploads over Bluetooth. tools/manage_bluetooth_library.py lists entries; --play INDEX or --delete INDEX acts on a freshly read entry. tools/inspect_bluetooth_loader.py --library reads count/free space over USB; --open-menu, --open-loader (Bluetooth) or --open-usb changes modes. The new storage tests inject failure at every flash I/O during addition and deletion, verify protected boundaries and readback corruption, and exercise duplicate detection, hole reuse, full catalogs and malformed metadata. Actual page handlers are tested for radio loss and reconnect without using hardware.
FM-1 library candidate and transport lights
The feature branch now also builds FM1969 with the same game menu, USB/Bluetooth uploads, eight supported mappers and dual-core player. The FM-1 library has 360 KiB across up to 32 entries, with the same 262,672-byte individual cartridge limit. FM-1 uses its own flash layout and original two-stage USB updater; SMK packages are not interchangeable.
FM-1 Bluetooth verified on hardware (2026-10-16): discovery, GATT connection, reconnection and a 262,160-byte cartridge upload passed. The transfer took 10.5 seconds with no retries; restart retained the cartridge checksum and existing library, then ran the game at approximately 60 emulated frames/sec with CPU1 active. The fix initializes SDK transmitter power before controller startup, following the working fm1-ctl peripheral; our port does not run the SDK's normal configuration initialization. The controller and SDK also share the same address buffer. Details: experiments/bluetooth/fm1969-hardware.json. SEND A GAME alone is not proof of an over-the-air connection.
python tools/build_bluetooth_probe.py --board fm1 --loader
python tools/package_fm1.py --bluetooth-loader --label library
The commands above produce build/fm1-bluetooth-loader/nesynth-fm1-969-library.fwsc. It reads the preserved FM-1 RF calibration snapshot at 0xe8000 and refuses invalid records; it never embeds the SMK calibration fallback. Host update/restore simulation, calibration parsing, storage fault injection and the shared browser USB flow pass. The installed and hardware-verified transmitter-power fix is the local nesynth-fm1-969-radio-txpower-preserved.fwsc package, which preserves the owner's existing library. Its application matches a complete rebuild byte-for-byte. Bluetooth hardware testing used the CLI; native browser chooser interaction remains a user check.
Both models follow NES order from left to right: B then A, and Select then Start. The left PLAY button is NES Select; the right REC button is NES Start. Hold PLAY + REC for the library shortcut. A/B use the final two white keys with no unused white key between them: FM-1 F5 = B and G5 = A; SMK B5 = B and C6 = A. The instrument simulators use the same positions. FM-1 retains its existing illuminated PLAY/REC keys. SMK now lights the green PLAY and red record indicators through the existing scanner, using outputs 17 and 19 (output 18 is PLAY's alternate red light). The scanner test verifies unchanged keyboard/analog sampling with these lights enabled; physical light placement still needs checking on the new build. The lights stay on in gameplay, library and upload modes.
The shared bluetooth.html uploader and USB command-line tools accept both models. Select NESynth FM1 in the Bluetooth chooser for FM-1. FM-1 volume uses its master knob; SMK uses Fader 1. Before a firmware update, use USB loading to stop Bluetooth; a firmware-upgrade request while the radio is active first reboots to radio-off USB mode and must be retried. Firmware replacement clears the game library unless a separately verified preservation package is used. Game uploads and power cycles retain the catalog.
Try it
python tools/build.py web
python tools/serve.py --port 8816
Open http://127.0.0.1:8816/. The diagnostic starts silently. Loading a cartridge, pressing a game control, or choosing Enable sound starts audio. Audio on confirms the browser audio context is running; a suspended context can be enabled again. Move the diamond with arrows/WASD; A changes pitch and B changes background color. Enter/X = A, Space/Z = B, P = Start, Shift = Select. On-screen controls also work. Input taps are held at least 40 ms; focus loss releases all controls.
Drop a .nes or .zip file onto Drop your game here in step 1, or click the same area to choose one. This single picker feeds both the browser player and installer. Load one file at a time. ZIPs containing one .nes file open automatically; archives containing several show a cartridge picker. Standard stored and DEFLATE ZIPs are supported, including cartridges in subfolders and uppercase .NES extensions. Mac metadata files are ignored. Extraction and checksum validation happen locally; nothing is uploaded. The browser limits archives to 64 MiB and extracted cartridges to 1 MiB, and rejects encrypted, split, ZIP64, damaged, or unsupported-compression archives. DEFLATE extraction requires a browser with DecompressionStream('deflate-raw') support.
Legacy DiskDude! headers are repaired in memory when imported. The checksum-identified Ms. Pac-Man (Tengen) overdump also has its excess graphics removed and horizontal mirroring restored, using the verified PRG/CHR and board metadata in MAME (mspacmnt). Unknown oversized dumps are still rejected; this does not enable NES 2.0 or special consoles. The original NES/ZIP file is never rewritten. The browser player, firmware builder, and Python cartridge reader use the same repair.
Accepted cartridges have a standard iNES 1.0 header. Mapper 0 accepts 16/32 KiB PRG and zero/8 KiB CHR; mapper 9 accepts power-of-two PRG sizes from 32 to 128 KiB and CHR ROM from 8 to 128 KiB. Mapper 1 supports standard MMC1B boards with power-of-two 16?256 KiB PRG, zero/8?128 KiB CHR, and at most 8 KiB PRG RAM. Expanded SxROM RAM/512 KiB PRG boards and MMC1A variants are not supported. An optional 512-byte trainer is supported. NES 2.0, PAL, other mappers, special console variants, and unsupported RAM sizes are rejected before entering the core. ZIP support does not change cartridge compatibility. A valid header does not guarantee accurate gameplay.
Mapper 2 supports standard UxROM with 32–256 KiB PRG and 8 KiB CHR RAM. Mapper 3 supports CNROM with 16/32 KiB PRG and 8/16/32 KiB CHR ROM. Mapper 4 supports standard MMC3 banking with 32–512 KiB PRG, zero/8–256 KiB CHR, four-screen mirroring and PRG-RAM protection. Bank sizes must be powers of two. The browser can load larger MMC3 games than the instrument: device firmware reserves 524,816 bytes (512 KiB plus header and trainer). Oversized device games get a separate error and remain playable in the browser. UxROM/CNROM use conflict-free register writes; MMC3 IRQs are a scanline approximation, with MMC3A/MMC6 and clone-specific behavior unimplemented. These are experimental compatibility limits, not a guarantee for every game.
The browser's default game simulator is a CRT television with a console and playable controller beneath it. The NESynth logo is on its gray enclosure. Instrument simulators match the current NES-order firmware controls. Drag the TV channel readout or upper tuning knob to discover 12 animated demoscene channels (04–15). Channel 03 or Escape returns to the loaded game without replacing it; emulation and its audio stop while a demo is showing. Click the left/right halves of either TV dial to lower/raise its setting; dragging still works. The lower TV volume knob and browser volume slider stay in sync. Fullscreen opens a movable retro clicker with channel and volume buttons, a CH 03 game button, and a close button. Reopen it with TV remote. Channel and volume controls also support arrow keys, Home and End.
FM-1_960 adds these three mappers and the larger cartridge slot. The application occupies 569,800 of 581,564 bytes with the original diagnostic embedded. Native, WASM, sanitizer, bank/IRQ/RAM protection, package round-trip and forward/reverse loader tests pass. User-supplied TetrisT, Super Pitfall, Super Shinobi and Super C ZIPs ran 3,600 frames with audio and inspected pictures; the two MMC3 games also passed full-versus-adaptive rendering/audio comparisons. These local games are not distributed. Physical validation of this new build remains pending.
FM-1_961 additionally supports mapper 11 (Color Dreams): power-of-two 32–128 KiB PRG and 8–128 KiB CHR ROM. A checksum-specific import repair clears only reserved header bytes 11–15 in the reported Simpsons dump; unknown payloads remain rejected. Simpsons and Shockwave each completed a 3,600-frame local playback run with audio and firmware packing. Simpsons still shows lower-screen rendering corruption; accepting its header does not establish accurate gameplay. The SMK source now includes a bounded RAM-only USB updater and compiles with the 512 KiB cartridge slot; the SMK browser installer is now available as the experimental SMK-37_963 candidate described below.
Battery RAM persists in browser storage, keyed by cartridge SHA-256, with .sav import/export. Only cartridges with the battery flag expose saves. Device save RAM remains volatile. A reset preserves battery RAM; loading a cartridge starts a fresh core session before browser save restoration.
Build and verify
Requires Python 3, Node, and Zig. Instrument compilation uses WSL and the JieLi pi32v2 toolchain. Defaults match the local GameDude setup; override with ZIG, JIELI_TOOLCHAIN, --zig, or --toolchain.
npm test
python tools/build.py sanitize
python tools/build.py fm1
python tools/build.py smk37
sanitize requires g++ in WSL. It runs the host suite with AddressSanitizer and UndefinedBehaviorSanitizer. Normal builds use no downloaded packages and do not access any connected device.
Run npm run firmware to regenerate the verified browser template (requires the local pinned AC79 SDK and restore fixture configured in tools/package_fm1.py). The template contains only our original diagnostic.
Hardware outputs are under build/fm1/ and build/smk37/, with ELF symbols, section reports, manifests, and nes-NOT-FOR-INSTALL.bin. These are raw link products, not .fwsc packages. tools/package_fm1.py creates and validates .fwsc packages with forward/reverse loader simulations. The experimental source identity is FM-1_961. Do not feed raw link binaries to an updater.
--rom path.nes or --rom path.zip embeds a private cartridge. For a ZIP containing several cartridges, add --rom-entry "folder/game.nes". --rom-capacity changes the reserved file slot. Build artifacts are ignored. The default 64 KiB slot accommodates NROM; MMC2 cartridges may need a larger slot (for example, --rom-capacity 262672). Enlarging it does not enable more mappers.
Measured build footprint
2026-10-13, optimized pi32v2 build with a 393,744-byte cartridge slot:
| Allocation | FM-1 | SMK37 | Existing budget |
|---|---|---|---|
| Application, including ROM slot | 436,516 bytes | 427,264 bytes | 581,564 bytes |
Regular RAM, including .data | 44,124 bytes | 42,560 bytes | 73,728 bytes |
| Large buffers | 356,448 bytes | 353,760 bytes | 368,640 bytes, shared with CPU1 stacks |
| CPU1 stacks | 8,192 bytes | 8,192 bytes | Included in the pool budget above |
| Remaining pool space | 4,000 bytes | 6,688 bytes | — |
The generated manifests are authoritative for a particular build; later changes can alter these figures. CPU0 stacks, SRAM code, and boot metadata have their own retained linker regions.
The Windows host executed the diagnostic at roughly 12,000–13,000 unthrottled frames/sec; Node WebAssembly was roughly 7,000–8,000. These are desktop diagnostic measurements, not FM-1 estimates or game compatibility results. Real-time Chrome playback was observed near 60 fps. Latest reports are build/host-benchmark.json and build/wasm-benchmark.json.
Architecture
- CPU0 owns NES CPU/PPU/APU emulation, controller state, audio interrupts, USB, and LCD DMA.
- Rendering uses a 256-word scratch scanline, then a 61,440-byte indexed frame.
- CPU1 scales an immutable indexed-frame snapshot into a 240×225 RGB565 buffer. The image is centered at LCD y=7; both axes use a 15/16 scale.
- Hardware adaptively rasterizes every third to sixth emulated frame, targeting 10?20 pictures/sec while running CPU/APU and mapper fetches on every frame. Drawing backs off when the audio queue falls below 2,200 samples. The browser rasterizes every frame. CPU1 compares RGB565 rows against the last submitted image; LCD DMA transfers only the changed row range. Duplicate raster frames are not resubmitted. Worker timeout/fault retirement and local scaling fallback are retained, with a full refresh after a fault.
- MMC1/MMC2 decode only fetched tile rows and caches them by immutable CHR-ROM address. The row cache resets when a cartridge resets or loads. Hot CPU, PPU, mapper and scaler code runs from executable SRAM on the instruments.
- The 44.1 kHz mono ring feeds both output channels. Browser audio scheduling drives emulation while sound is active; a monotonic timer drives silent playback. The rational sample count targets approximately 60.0988 NTSC frames/sec.
- The original APU is an approximate linear mix with a DC-blocking filter. No claim of cycle-accurate PPU/APU timing, exact sprite behavior, emphasis-color emulation, or broad NROM compatibility is made.
The retained gp_* and some gb_* names are the GameDude platform interface, not a second Game Boy core.
Verification performed
- 1,800-frame diagnostic runs in native host and WebAssembly; matching frame hash across both targets.
- ROM rejection, short files, mapper/region/format checks, 16/32 KiB PRG, CHR RAM, and trainer placement.
- Controller-driven sprite movement and background color; audible-sample generation and A-button pitch change; volume mute.
- Pulse mute/resume, constant-volume zero, envelope expiry/retrigger, triangle linear-counter release and disabled DMC buffer clearing. APU writes use an elapsed CPU clock rather than its scanline remainder.
- Exact RGB channels in browser clamped image buffers; Web Audio activation/retry, PCM routing and continuous scheduling.
- Pause/reset, audio backpressure, exact long-run sample totals, battery RAM dirty tracking and reset preservation.
- Full output comparison and guard words for the LCD scaler; corrupt source rejection and late-completion acceptance.
- Dirty-row bounds for first/last/single changed rows, unchanged pictures and dropped source rows. Rendering strides 1/2/3/6 and adaptive cadence transitions produce identical audio samples and identical presented frames across 3,600-frame diagnostic and private Punch-Out and Ninja Gaiden runs.
- 256 seeded header mutations and 64 seeded random NROM programs in WebAssembly. This is limited robustness coverage, not an exhaustive security audit.
- Host diagnostic under address/undefined-behavior sanitizers.
- FM-1 and SMK37 cross-compilation, linker memory bounds, FM-1 USB updater symbol retention.
- Chrome visual preview, audio activation, keyboard movement, pause/resume.
- ZIP stored/DEFLATE extraction, streamed data descriptors, subfolders, multiple-cartridge selection, CRC verification, size bounds, malformed archives, and extracted-cartridge playback in WebAssembly; Python build-input extraction tests.
- MMC2 PRG/CHR bank selection, bounded register values, exact latch addresses, mirroring, reset, and post-fetch latch switching for background and sprite rendering.
- A user-provided Punch-Out ZIP ran 3,600 frames in WebAssembly with audio output, reaching the Glass Joe match. Screenshots were inspected; this is a limited smoke test, not a complete game or timing validation.
node tools/smoke_rom.mjs path.ziprepeats the local check without publishing cartridge data.
FM-1_957 (Punch-Out) measured 60.09 emulated frames/sec with zero new audio underruns during the last sample window. The underrun counter stayed at its post-startup value of 18 between uptime 30.6 and 363.3 seconds, including user gameplay. CPU1 remained online with no faults/fallbacks. The user reported substantially better playback. Reports: build/fm1/diagnostics-punchout-957.json and build/fm1/diagnostics-punchout-957-playing.json. Display transfers vary with motion because unchanged rows are omitted; the internal 60 fps rate is not the LCD refresh rate. This is a limited game/device trial, not a broad compatibility claim.
MMC1 was subsequently added in FM-1_958. Ninja Gaiden reached Stage 1-1 in a 3,600-frame browser-core smoke test; adaptive/full rendering produced identical audio and presented frames. Its ZIP also passed the exact FM-1 firmware-packing checks and SMK37 cross-compilation. Physical Ninja Gaiden playback has not yet been verified.
The current browser templates reserve 524,816 bytes for cartridge data, header and trainer. npm test checks NROM/MMC1/MMC2 packing, exact cartridge bytes, unchanged updater and loader, corrupt template rejection, install handoffs, interrupted transfers, active-loader resume, exact restart validation, official restore verification, MIDI discovery and progress accounting. C loader simulations check flash write boundaries and reverse restore. SMK37 includes the RAM USB updater; model-specific packages, simulated transfers and protected flash boundaries are tested.
Website device workflow
The main page has two keyboard-accessible tabs beside the browser player (above it on small screens):
- Install firmware: select FM-1, SMK-37 Pro or SMK-37 Elite, connect by USB, then press Install NESynth. The page automatically fetches the versioned NESynth core, verifies its SHA-256 and size, and builds the model-specific package from the pinned official base. There is no firmware picker or game prerequisite. Changing models clears the prepared package. Factory restore, connection help, logs, explicit retry and exact restart checks remain. Setup installs a core with an empty library. The public cores contain no owner calibration; the loopback owner preview automatically uses the existing private Pro core without copying it to
web/. The newly built calibration-free SMK core passes packaging and browser simulation checks; this exact binary has not been tested on hardware. Elite hardware remains unvalidated. - Add games: drop a
.nesor.zipon the picker, TV or console, or choose a homebrew. Browser playback and device loading share this selection. Connect by Bluetooth or USB, then use Send and play to add the cartridge. The tab shows saved games, Play/Delete actions and free space. Selecting a game never starts a transfer or disconnects an existing connection. Both transports use the same device library and shared loader implementation asbluetooth.html.
Disconnect the game loader before using firmware tools. Actual transfers lock competing file imports and device operations; tab switching keeps connection and file state. Firmware setup and game selection are independent. Use Chrome/Edge over HTTPS or localhost. For firmware updates, restart NESynth with Bluetooth off and connect only the target instrument by USB. Generic ota-FM-1 loaders retain physical-model confirmation because some SMK loaders use that identity.
Run npm run test:ui with Playwright and Chrome available for isolated desktop/mobile checks of both flows. PLAYWRIGHT_MODULE can point to an existing installation. These tests simulate all device access. Firmware UI tests need the pinned stock fixtures at build/smk37/stock-pro.fwsc and build/smk37/stock-elite.fwsc. node tests/nesynth_firmware.mjs checks automatic downloads, package assembly and rejection of corrupt or failed downloads; optional Pro and Elite stock paths exercise both wrappers. Existing transport/package tests continue to cover interrupted transfers, immutable images and exact restart identity.
The FM-1/SMK-37 Pro/SMK-37 Elite simulator views use the shared key/pad mappings. Click highlighted controls or enlarge the live screen. The classic NES controller remains available. Choose the exact model in the installer separately. Changing models clears prepared firmware and requires a fresh connection check; it keeps the selected cartridge.
On Windows, tools/serve.py provides a same-origin native USB service for SMK Pro when Node, the pinned Pro stock fixture and build/python-deps/mido are present. The existing Connect, Install and Restore controls use it automatically on localhost. Each USB stage runs in a short-lived process, with exact SMK identities, verified packages, bounded requests and an OS lock shared across preview ports. The browser displays progress without requesting WebMIDI. Only fixed core/restore actions are accepted; requests cannot upload arbitrary firmware, run commands or choose filesystem paths. Mutating requests require a matching loopback Host and Origin. --owner-firmware uses the existing private Pro core; generated packages stay under ignored build/native-smk/.
Hardware check on 2026-10-16: the default Chrome SMK stock-to-loader transition froze twice, including with a three-second delay (removed). The native service completed NESynth → stock Pro 016 → NESynth 969 through the same page in default Chrome, with requestMIDIAccess deliberately blocked in the test. Restart identities and UI responsiveness were verified; the owner confirmed normal Chrome remained responsive after clearing its old frozen session. That verification originally covered only the local preview. The hosted Windows Pro path now uses the same service when launched with --allow-hosted; see Windows SMK USB helper below. FM-1 and Elite retain their existing browser transports. python tests/native_smk.py checks model isolation, package integrity, both transfer formats, request bounds, interruptions and exact restarts with simulated USB only.
SMK-37 Pro and Elite installer
Select SMK-37 Pro (or SMK-37 Elite) in Install firmware, connect by USB, and choose Install NESynth. The page automatically prepares the version 969 library core; use Add games afterward. Controls follow NES order: PLAY is Select, REC is Start, and the final two white keys are B then A. Fader 1 controls game volume: down = mute, up = full. The owner SMK Pro build has been exercised on hardware, including dual-core gameplay and the local native USB installer. The exact calibration-free public core has packaging and browser simulation coverage; hardware validation remains pending for that binary and for Elite.
First installation accepts official M-VAVE 016 for the selected model. The browser downloads that model's pinned official package from the SMK documentation mirror, checks its exact SHA-256, and creates the NESynth package locally. The current page automatically assembles the NESynth library core; the raw application is not sent directly to the device. Updater-enabled Felucca/Sloop/Melodee builds with SMK identity 922 or newer, Doom identity 007 or newer, and NESynth use the portable RAM-loader route. Older firmware without that updater requires adapter migration.
Return to the original M-VAVE synth restores the exact selected model's 016 application. The RAM updater checks the supported flash chip, factory boot CRC and per-device calibration before writing. It preflights all sectors, writes only 0x4000..0x9d000, verifies each sector and preserves boot/calibration. It requires an explicit retry after interruption and an exact restart identity before success. Power loss during programming may require adapter recovery. Generic ota-FM-1 identity is accepted for Elite only after explicit physical-model confirmation.
SMK-37_962 sends LCD pixel data asynchronously, allowing NES emulation and audio production to continue during DMA. The shared framebuffer remains reserved until completion; a 250 ms transfer timeout disables the display while leaving emulation and USB recovery available. LCD DMA is stopped before entering the RAM updater. CPU1 scaling, adaptive rendering and Fader 1 volume remain enabled. On the owner's Pro running Super Mario Bros. (JU), SMK-37_962 measured 41.96 emulated frames/sec, 88.20 new audio underruns/sec and no CPU1 faults/fallbacks or LCD timeouts. The scanner consumed 33.86% of elapsed CPU time and completed about 12,545 full scans/sec (about 200,718 interrupts/sec). LCD updates were 3.62/sec during this sample; unchanged rows/frames are omitted, so that value depends on scene motion.
SMK-37_963 addresses that measured scanner load by changing only SPI2's scan divider from 5 to 95, before the first transfer. The 16-row latch sequence, ADC settling/completion pipeline, keys, pads and Fader 1 mapping remain intact. Expected full keyboard scan rate is around 1 kHz; actual input responsiveness, analog sweep rate and game speed require a new physical measurement. The vendored scanner now accepts SMK37_SCAN_BAUD, retaining its original default of 5; NESynth's local configuration supplies 95. This does not change CPU, audio or LCD clocks.
Build the diagnostic-only template with python tools/public_smk_template.py. Download the two files from the pinned URLs in web/vendor/smk37-stock.js to the ignored build/smk37/stock-pro.fwsc and stock-elite.fwsc, then run:
npm run test:smk -- build/smk37/stock-pro.fwsc build/smk37/stock-elite.fwsc
npm run test:ui
The checks cover exact ROM packing, both model wrappers, immutable update data, official/custom/active-loader handoffs, interrupted transfers, incorrect restart rejection, factory restore and the single browser installer. The C updater simulation also consumes each actual generated application packet with a clearly synthetic calibration fixture, exercising flash limits and readback. CI runs these package and loader checks before publishing. Synthetic fixtures are not device backups or hardware validation.
Updater sources and browser packaging are adapted from Doom's SMK updater, retaining the Felucca/Sloop credits and GPL notices. Pinned source hashes and adaptations are in vendor/smk-updater/PROVENANCE.json and BROWSER_PROVENANCE.json.
Remaining hardware work
The feat/bluetooth-game-loader branch investigates a resident game loader and
wireless cartridge uploads. Reconciliation evidence
compares DEADACTIVE's Hortator Bluetooth implementation
with the existing Felucca peripheral diagnostics. Hortator's five Bluetooth host
test groups and startup-check self-test pass locally. Its cooperative scheduler
and stored-calibration approach are useful references; its central MIDI role is
different from a phone-facing cartridge receiver. That initial branch did not
enable a Bluetooth runtime. The follow-up diagnostic is described below; reliable
file transfer and device performance remain open. The published SMK963 build
remains unchanged. The owner reports substantially improved SMK963 playback;
post-change FPS and complete control validation have not been measured.
The follow-up feat/bluetooth-loader-prototype branch contains a non-installable
target link probe and a bounded cartridge receiver. Measured fit evidence
records the real NESynth + pinned Hortator central stack + receiver link:
| Cartridge allocation | Application bytes | Free application bytes |
|---|---|---|
| 256 KiB + 528-byte header/trainer allowance | 439,264 | 142,300 |
| 384 KiB + 528-byte header/trainer allowance | 570,336 | 11,228 |
Both use 56,288 of 73,728 bytes of regular RAM and 361,952 of 368,640 bytes
of buffer/CPU1-stack RAM. Bluetooth's heap and cooperative task records share
the inactive 61,440-byte video_source buffer. This measures the storage layout;
it does not implement the required gameplay/loader ownership transition.
Gameplay and Bluetooth must never use this buffer together. A future loader
must quiesce CPU1, display DMA and emulation before radio startup, then use
verified teardown or a reboot before gameplay. The 256 KiB option leaves more
space for that integration; the 384 KiB result is not a promised final capacity.
The existing 512 KiB public template is unchanged.
The receiver has BEGIN/DATA/COMMIT/ABORT operations, 512-byte chunks, bounded relative offsets, chunk and full-image CRCs, exact duplicate acknowledgment, and a publish gate after readback and backend ROM validation. Host tests exercise corruption, gaps, retries, interruption and backend failures under ASan/UBSan. There is no physical flash backend, cartridge wire protocol adapter or frontend yet. The separate diagnostic GATT service below only echoes RAM data. The callback backend must invalidate staging before writes and publish only a fully validated image; power-loss recovery needs separate hardware implementation and tests. No old-game preservation is implied.
tools/inspect_ble_calibration.py only reads private full-flash backups and
prints candidate offsets/counts. Two byte-identical stock reads from the owner's
SMK Pro contain two distinct checksum-valid VM item 187 candidates. Record
selection was initially unverified; the follow-up below resolves the candidate
under the pinned SDK's bank rules. Radio acceptance still needs a device test. No private backup,
calibration payload or Bluetooth address is included in the repository.
To repeat the link measurement on Windows with WSL and the JieLi toolchain
configured for tools/build.py, place these references under ignored build/references/:
hortator: commit7e3415971c5800b4a3ad0146a7d95615883d7d96fromhttps://github.com/deadactive/hortator.ac79-ble-sdk: commit64afb30b805af5f5d3c2e6471e970c3fd0ba40ad, tagAC79NN_SDK_V1.2.13_2026-04-20, fromhttps://gitee.com/Jieli-Tech/fw-AC79_AIoT_SDK.git. Includecpu/wl82/liba,include_lib,apps/common/config,apps/common/includeandapps/common/ble/includeif using sparse checkout.
python tools/build_bluetooth_probe.py --rom-capacity 262672
# Or test the larger allocation:
python tools/build_bluetooth_probe.py --rom-capacity 393744
python tests/ble_calibration.py
wsl -e cc -O1 -g '-fsanitize=address,undefined' -fno-sanitize-recover=all tests/rom_transfer.c experiments/bluetooth/rom_transfer.c -o build/rom-transfer-test
wsl -e ./build/rom-transfer-test
The script verifies Hortator source and SDK library/config hashes. Only the two
pinned text config files are normalized to LF for hashing; libraries are checked
byte-for-byte. Generated adaptations retain the original notices and stay under
build/bluetooth-probe/. Each run replaces that directory's ELF/map/report and
the ordinary ignored build/smk37/ baseline outputs. The Bluetooth probe emits
no installable firmware: hooks are deliberately inert, calibration reads
fail closed, and radio startup is never called. Do not convert its ELF into a
device package. Neither the target link nor the host checks prove radio operation,
transfer throughput, task-stack sufficiency or gameplay performance on hardware.
Local SMK Pro Bluetooth diagnostic (964)
python tools/build_bluetooth_probe.py --diagnostic builds a separate diagnostic,
not a game-playing release. --peripheral instead measures NESynth plus this GATT
profile with inert hooks. Default invocation remains the original central fit probe.
Diagnostic evidence records the
local build, calibration selection and tests. The public site remains SMK963. The owner's
private candidate passed hardware advertising, connection, an eight-byte RAM
echo with count increment, and re-advertising after disconnect on 2026-10-16.
The controller reached stage 7 with no fatal or allocation errors. Game upload,
longer stability testing and post-radio USB restoration are still pending.
The validated Pro016 flash directory places VM at 0x9d000 with length 0x23000.
The pinned SDK cfg_tool.a caps sector-aligned VM at 128 KiB and divides it into
two banks. The reader uses bank headers at 0x9d000 and 0xad000, walks records
sequentially, checks every record CRC, and requires RF trim, crystal and PA data.
It refuses ambiguous/transitional banks, malformed records and failed reads;
it never runs SDK VM initialization, defrag or writes. On the owner's paired stock
backup it selects RF record 0x9d015 after validating 14 records. The second
candidate 0xbe015 is outside the SDK-managed range and is not used. This reader
targets the audited SMK Pro layout; it does not claim to handle every VM revision.
The diagnostic starts with CPU1, emulation, audio and Bluetooth inactive. The LCD shows PRESS PLAY and USB restoration is available. A fresh PLAY press starts the radio using the device's own calibration. Expected subsequent states are STARTING, ADVERTISING, then CONNECTED. It advertises NESynth SMK Test. The custom service has an eight-byte RAM echo; there are no cartridge writes or bonding. Startup uses the cooperative app task, actual ICFG interrupt detection, bounded callback lengths and duplicate-event guards from the reconciled references. The build verifies RAM code cannot reach XIP and that application flash-writing routines compile to immediate failure. The separate existing RAM updater retains its bounded application-write capability for installation/restoration.
python tools/build_bluetooth_probe.py --diagnostic
node tools/package_bluetooth_diagnostic.mjs build/smk37/stock-pro.fwsc
python -m pip install --target build/bluetooth-python-deps bleak==3.0.2
# After installing the local diagnostic and pressing PLAY:
python tools/inspect_bluetooth_diagnostic.py # USB status; no radio start or flash write
python tools/test_bluetooth_diagnostic.py
Local outputs are in build/bluetooth-diagnostic/: the model-specific
NESynth-SMK-Pro-964-Bluetooth-Test.fwsc, its .smkupdate RAM-loader packet and
JSON reports. The raw .bin is not an installer. The package round-trip verifies
exact application bytes and protected boot; the actual update packet also passes
the C RAM-loader simulation with a synthetic calibration fixture and preserved tail.
No vendor firmware, device calibration or game ROM is committed or published.
Hardware testing on 2026-10-16 found that the owner's live VM region had
already been repurposed by Melodee: 0x9d000 was erased and 0xad000 held
its MSP2 object header. The default reader correctly reported error 1 before
starting the controller. USB metadata is available through the inspector above;
it contains state, parser/read results and bank markers, never RF payload bytes.
For this case only, --diagnostic --private-calibration-backups FIRST SECOND
accepts two identical full stock backups and validates their records with the
same C parser. It embeds only RF/crystal/PA records and the protected 8 KiB
factory region in an ignored local image. Before using the saved records, the
device must match the validated 556-byte usrtrim record at 0xc1000 byte for
byte, the same record the SMK updater and Melodee use as the unit fingerprint.
A mismatch or failed read refuses the saved RF data. The surrounding 8 KiB is
compared separately to report changed-sector offsets without exposing bytes. This does not restore or write VM/music storage. Packages are prefixed
PRIVATE-; neither the generated header nor firmware may be committed or
published. This fallback is device-specific, available to the diagnostic and SMK965
loader, and is not a calibration migration. When the owner confirms the stock RF backup belongs
to this same unit but its current factory data has changed, two current private
reads can be collected with tools/backup_bluetooth_factory.py --output-dir build/bluetooth-diagnostic/live-factory while the radio is off. Optional
--private-unit-backups FIRST SECOND uses that matching pair for the installation
guard while retaining the validated stock RF records as the radio source. The
source relationship must be confirmed by the owner; matching current reads alone
does not prove an old RF backup belongs to that device.
The test client uses Bleak discovery
and GATT reads/writes, matches
both the diagnostic name and UUID, requires exactly one match, writes a random
eight-byte nonce and checks its echo and counter before disconnecting. Service UUID:
f86c4cf1-cf8b-4bc4-bc4c-d6f47eabe7e0; value UUID ends in e7e1.
Power-cycle before USB restoration and leave PLAY untouched. A USB update request after Bluetooth starts reboots to radio-off mode; retry the update after that reboot. This avoids running the RAM updater while controller tasks/IRQs remain active. The SMK964 diagnostic was physically installed and its radio startup, Bluetooth connection, RAM echo and re-advertising were verified on this SMK Pro; this section describes the earlier diagnostic, separate from the integrated SMK965 build above.
Improve LCD refresh and broader game/audio compatibility, and validate physical SMK37 installation, playback and recovery before promoting its experimental installer to a hardware-validated release. More mapper support needs separate compatibility work. The original diagnostic is the included NES cartridge; GameDude's Game Boy homebrew games are not NES-compatible.
Read-only diagnostic SysEx requests use 7D 4E 53 version (7D 'N' 'S'), separate from GameDude. Version 3 appends five uint32 fields to the retained 35-field platform record: last/max/total emulator ticks, measured frame count, and maximum observed LCD completion time. Values use five 7-bit little-endian chunks; timer frequency is 24 MHz. Total ticks wrap at 32 bits. LCD time includes delayed CPU polling and therefore bounds observed completion latency, not exact wire time.
Version 4 retains those 40 fields and appends cumulative CPU, PPU, APU and scan-IRQ ticks. IRQ time overlaps the other measurements. python tools/diagnose_device.py --samples 4 --interval 2 collects read-only measurements and computes frame rates and per-frame timings.
Sources and modifications
- InfoNES by Jay Kumogata, pinned at
fe3295c0a86bf5bbecc22aeab3b3af11f6f47908. Its retained upstream license is Apache-2.0. Source credits to pNesX, NesterJ and DarcNES are preserved. Mapper 0 uses the upstream implementation; MMC1 is implemented insrc/mapper_mmc1.cppusing the NESdev register reference; MMC2 is integrated insrc/mapper_mmc2.cppwith post-fetch PPU callbacks. - The newer Pico fork was evaluated, but the portable original avoids its Pico SDK/C++ platform dependencies for this first port. Its older emulation behavior is a compatibility limitation.
- GameDude platform files came from
../arcade-fm1-smk; imported hashes are invendor/provenance.json. Felucca HAL, linker, USB and recovery notices remain invendor/felucca. Integration is GPL-3.0-only; seeLICENSE.
vendor/infones remains pristine. tools/prepare_core.py creates patched copies under build/infones: fixed-width integers, sequenced 6502 operand reads, scanline scratch rendering, buffer placement, bounded audio event queue, corrected APU register indexing, ROM-write protection, palette masking, sprite-pointer bounds, signed envelope/sweep phases, reset cleanup, four-screen selection and approximate NTSC scanline pacing. These patches do not make the core cycle-accurate.
tools/import_sources.py and tools/adapt_platform.py record the one-time bootstrap and refuse to overwrite the completed port. They are not normal build steps. No files in the reference project were changed.
Website transport, packaging, progress and simulator adaptations are recorded in vendor/web-provenance.json. The website distributes experimental firmware templates; hardware validation status is recorded separately for each model.
FM-1_959 shares the browser core's bounded vblank/NMI scheduling fix. This lets foreground status polling coexist with an NMI handler that also reads PPUSTATUS. The known legacy Ms. Pac-Man ZIP reaches gameplay with audio in a 3,600-frame browser-core test; full/adaptive modes agree. Native/WASM tests, sanitizers, firmware packaging, and SMK37 compilation pass. This revision has not been physically tested on an instrument.
SMK-37_962 supports diagnostic versions 1–5. Version 5 retains the first 44 fields and adds LCD busy, LCD timeout count, LCD failure flag, cumulative USB-IRQ ticks, and raw clock divider/system registers. python tools/diagnose_device.py --board smk37 --samples 4 --interval 2 collects measurements from the installed candidate; optional --port restricts discovery. The host needs mido and python-rtmidi. Reports include emulation/display rates, CPU1 online/fallback state, audio underruns and interrupt time. IRQ totals can overlap under interrupt nesting; raw clock registers are not decoded frequencies. The asynchronous display and diagnostic packet host tests cover buffer ownership, completion, timer wrap, timeout shutdown and all response versions.
Automatic library release assets are staged with python tools/public_library_firmware.py after building SMK with python tools/build_bluetooth_probe.py --loader --output-dir build/public-bluetooth-loader. Staging rejects private-calibration reports and checks binary sizes/hashes. FM-1 uses its existing build/fm1-bluetooth-loader core.
Windows SMK USB helper
The live website uses a local USB helper for SMK-37 Pro firmware installation on Windows. Chrome's Windows MIDI backend can freeze when the stock SMK disconnects and reappears as its updater. Closing a Web MIDI port or adding a delay did not fix that transition. The helper performs each USB stage in a separate process while the same live website displays progress. FM-1 and non-Windows browser installation are unchanged; Elite does not yet have the native helper path.
Install Python 3.12 and Node.js 22 or newer, download or clone this repository, then run from its folder:
python tools/start_windows_usb.py
The first run installs pinned MIDI dependencies inside build/python-deps and downloads the SHA-256-verified official Pro 016 base if missing. Leave the helper running, use the normal live NESynth site, select SMK-37 Pro, and click Connect or Install. Allow Chrome's local-network permission if prompted. No Chrome flag changes or different browser are needed. Stop the helper with Ctrl+C when finished; start it again for later installs. Games still use the existing Bluetooth / USB uploader.
--allow-hosted explicitly enables the official GitHub Pages origin on a loopback-only listener. Other origins and Host headers are rejected. Hosted mutations require a per-process token obtained through the allowed capability request, JSON requests and fixed actions; callers cannot provide firmware bytes, shell commands or filesystem paths. The page fails before requesting Web MIDI when the Windows Pro helper is absent, incompatible or permission is denied. A failed status request does not retry programming automatically. The existing local service still works without hosted access enabled.
SMK flash compatibility release
The SMK update and stock-restore path now accepts the reported EB6014 flash chip alongside 856014 and displays detailed loader failures. Update older Windows USB-helper checkouts before installing this release. See release scope, evidence and recovery limits.
