Skip to content

Add local emulators for v1/v2/v3 hardware#142

Merged
sastraxi merged 41 commits into
TreeFallSound:pistomp-v3from
sastraxi:feat/v1v2-emulator
Jun 3, 2026
Merged

Add local emulators for v1/v2/v3 hardware#142
sastraxi merged 41 commits into
TreeFallSound:pistomp-v3from
sastraxi:feat/v1v2-emulator

Conversation

@sastraxi

@sastraxi sastraxi commented May 10, 2026

Copy link
Copy Markdown
Collaborator

pi-stomp Emulator

Run locally against a MOD Desktop instance.

./run_emulator.sh [v1|v2|v3]   # default: v3

Requires MOD Desktop running at http://127.0.0.1:18181/ as well as lilv. See the updated README.md for more details.

Please note that a lot of this code and the PR description were assisted by Claude; normally I take more time cleaning up its code, but here I had a lighter touch because 99% of this code is out of the production path.

Hardware versions

Version Host flag Handler class
v3 (Pistomptre) emulator_v3 EmulatorModhandler + EmulatorHardwareV3
v2 (Pistompcore) emulator_v2 EmulatorModhandler + EmulatorHardwareV2
v1 (Pistomp) emulator_v1 EmulatorMod + EmulatorHardwareV1

Key classes

Class File Role
EmulatorHardwareBase emulator/hardware_base.py Shared base — LCD, footswitches, analog controls
EmulatorHardwareV1/V2/V3 emulator/hardware_v*.py Version-specific encoder/footswitch layout
EmulatorModhandler emulator/modhandler.py Modhandler subclass — owns VirtualAudiocard, StubWifiManager, StubRelay; drives render loop
EmulatorMod emulator/mod.py v1-only Mod subclass
EmulatorWindow emulator/window.py pygame window — scaled LCD left, clickable controls panel right
LcdPygame emulator/lcd_pygame.py LcdBase implementation — receives PIL frames, blits to pygame Surface
VirtualAudiocard emulator/stubs.py In-memory EQ/volume/bypass state; no ALSA
StubWifiManager emulator/stubs.py No-op wifi

Render loop

poll_controls runs every ~10 ms:

  1. EmulatorWindow.process_events() — drains pygame events, fires hardware callbacks
  2. super().poll_controls() — polls encoders/footswitches
  3. lcd.poll_updates() — flushes dirty PIL→pygame blit
  4. EmulatorWindow.render() — scales LCD surface and blits to screen

Notes

  • pygame has an unfixed circular import that specifically triggers in Python 3.14; by using the pygame._freetype C extension directly we can get around it
    *lilv is installed outside the venv, so there's a run_emulator.sh shim that uses pkg-config / Homebrew to find the prefix and exports DYLD_LIBRARY_PATH/PYTHONPATH for it
  • to avoid loading slightly-different system versions of fonts, emulator/__init__.py patches ImageFont.truetype to resolve bare names from fonts/
  • SPI bandwidth bottlenecking is emulated by time.sleep based on the number of pixels blitted

Images

image
image

sastraxi and others added 27 commits April 22, 2026 13:22
Account for blit elapsed time when computing SPI sleep deficit, and skip
sleeps below macOS's ~1 ms timer granularity.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
# Conflicts:
#	pyproject.toml
#	uv.lock
sastraxi and others added 9 commits May 11, 2026 20:30
# Conflicts:
#	modalapi/modhandler.py
#	uv.lock
Extracted from release/patch ef3d349 ("Patch to make everything work
together"). The Modhandler/Mod base classes spin up an AsyncWebSocketBridge
pointed at :80 (assumed for on-device MOD-UI); on the emulator MOD Desktop
listens at :18181. Replace the bridge after super().__init__() so emulator
runs talk to the right port.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
# Conflicts:
#	modalapi/modhandler.py
The new WifiManager (now on pistomp-v3 via PR TreeFallSound#140) is command-queue-based:
callers invoke the queue with Cmd objects, poll() drains via on_status_change
callback, saved profiles surface through get_cached_saved(), and
connect_scanned/saved gained extra args (security, wait, reconnect).
StubWifiManager still used the pre-queue interface and crashed the emulator
on first wifi tick. Reuse the real CommandQueue against the stub (its run()
methods match the new shape), add the missing methods, and drop the
orphaned configure_wifi path which no caller invokes anymore.

Relocated from release/patch bridge 54c590a now that feat/multi-wifi has
landed on base.
Settings hardcoded /home/pistomp/data/config — on the emulator, set_setting() either silently lost the write or scribbled at a non-emulator path, so LCD speed and other persistent choices never survived restart.

Make Settings accept a data_dir parameter (default unchanged) and pass the emulator's data dir from EmulatorModhandler, alongside banks.json and last.json. Also mkdir -p the emu data dir so first-run writes succeed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
# Conflicts:
#	pyproject.toml
#	uv.lock

@rreichenbach rreichenbach left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I finally got this to work.

On a Mac, I had to do this to create the correct env:
uv venv --python 3.13
uv sync --extra emulator

Could be good to document that in README.md

Then I had to make these changes. Probably due to not being updated to include the recent Footswitch change from enabled to toggled.

--- a/emulator/controls.py
+++ b/emulator/controls.py
@@ -101,11 +101,11 @@ class MockFootswitch(footswitch.Footswitch):
pass

 def press(self):
  •    self.enabled = not self.enabled
    
  •    self.toggled = not self.toggled
       if self.midiout and self.midi_CC is not None and _rtmidi_available:
           self.midiout.send_message(
               [CONTROL_CHANGE | (self.midi_channel & 0x0F),
    
  •             self.midi_CC, 127 if self.enabled else 0])
    
  •             self.midi_CC, 127 if self.toggled else 0])
       self.refresh_callback(footswitch=self)
    

diff --git a/emulator/window.py b/emulator/window.py
index 3db8e2c..788bb3a 100644
--- a/emulator/window.py
+++ b/emulator/window.py
@@ -252,7 +252,7 @@ class EmulatorWindow:
# Footswitch state colours
for btn, idx in self._fs_btns:
fs = self.hw.footswitches[idx]

  •        color = FS_ON if fs.enabled else FS_OFF
    
  •        color = FS_ON if fs.toggled else FS_OFF
    

@sastraxi

Copy link
Copy Markdown
Collaborator Author

@rreichenbach nice catch -- I suppose it would be a good idea to add some minimal tests for the emulator.

Also, any particular reason for 3.13? If it doesn't work with 3.14 I can take a look.

@sastraxi sastraxi requested a review from rreichenbach May 29, 2026 04:04
@sastraxi

Copy link
Copy Markdown
Collaborator Author

@rreichenbach Ready for re-review. I added some tests, fixed the bug you found, and got rid of the --extra emulator stuff; it just installs its deps with uv sync (in group dev, which is default).

@rreichenbach rreichenbach left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good.

@sastraxi sastraxi merged commit ed4f280 into TreeFallSound:pistomp-v3 Jun 3, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants