Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ Notable user visible changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [v3.3.1] - 2026-08-14
### Fixed
- Pedalboards list no longer shows deleted pedalboards after they are removed from MOD-UI
- Fixed a race condition where Restore would re-write `last.json`, triggering a crash when the pedalboard it refers to does not yet exist when it is re-scanned

## [v3.3.0] - 2026-08-14
### Added
- Welcome screen on startup
Expand Down
18 changes: 18 additions & 0 deletions modalapi/modhandler.py
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ def __init__(self, audiocard: Audiocard, homedir, data_dir="/home/pistomp/data")
# Backup
self.backup_file = "pistomp_backup.zip"
self.data_dir = data_dir
self._restoring = False

# Banks
self.banks_file = os.path.join(self.data_dir, "banks.json")
Expand Down Expand Up @@ -970,6 +971,11 @@ def poll_modui_changes(self):
# reads next_pedalboard_preset_index this tick. No-op if already drained.
self.poll_ws_messages()

# unzip rewrites last.json/banks.json/snapshots.json
# don't poll again until we restart the service
if self._restoring:
return

# Check for pedalboard change via last.json
if self.last_json_monitor.check_for_change():
self._is_pedalboard_loading = True
Expand All @@ -980,6 +986,15 @@ def poll_modui_changes(self):

if mod_bundle not in self.pedalboards:
self.load_pedalboards()
if mod_bundle not in self.pedalboards:
# MOD-UI owns this relationship; if its own list still lacks
# the bundle we have nothing to load and no business picking
# a substitute mid-session. Keep the board we have.
logging.warning("last.json names a pedalboard MOD-UI does not list: %s", mod_bundle)
self._is_pedalboard_loading = False
self.lcd.link_data(self.pedalboard_list, self.current, self.hardware.footswitches)
self.lcd.draw_main_panel()
return

pb = self.reload_pedalboard(mod_bundle)
self.set_current_pedalboard(pb)
Expand Down Expand Up @@ -1053,6 +1068,8 @@ def load_pedalboards(self):
sys.exit()

pbs = json.loads(resp.text)
self.pedalboards = {}
self.pedalboard_list = []
for pb in pbs:
bundle = pb[Token.BUNDLE]
title = pb[Token.TITLE]
Expand Down Expand Up @@ -1683,6 +1700,7 @@ def _do_restore_data(self, backup_dir: str, on_success=None):
logging.info("Restoring data backup...")
cmd = os.path.join(self.homedir, "util", "data-restore.sh")
job = ArchiveJob.restore(cmd, self.username, os.path.join(backup_dir, self.backup_file), self.data_dir)
self._restoring = True
self.lcd.pstack.push_panel(
ArchiveProgressPanel(
title="Restoring",
Expand Down
73 changes: 70 additions & 3 deletions tests/v3/test_pedalboards.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,16 +86,15 @@ def get_side_effect(url, **kwargs):
snapshot()


def test_v3_pedalboard_selection_menu_shows_all_boards(
v3_system: SystemFixture, nav_handler, make_plugin, snapshot
):
def test_v3_pedalboard_selection_menu_shows_all_boards(v3_system: SystemFixture, nav_handler, make_plugin, snapshot):
"""Starting from a loaded pedalboard, open the pedalboard selection menu
showing 6 boards with distinct titles."""
handler = v3_system.handler
hw = v3_system.hw

titles = ["Blues Rig", "Doom Bass", "Shoegaze", "Ambient Pad", "Metal", "Jazz Clean"]
from unittest.mock import MagicMock

for i, title in enumerate(titles):
pb = MagicMock()
pb.title = title
Expand All @@ -116,6 +115,74 @@ def test_v3_pedalboard_selection_menu_shows_all_boards(
snapshot()


def _list_side_effect(boards: list[tuple[str, str]]):
"""mock_get side effect where pedalboard/list returns exactly `boards` as (title, bundle)."""

def side_effect(url, **_kwargs):
resp = MagicMock()
resp.status_code = 200
if "pedalboard/list" in url:
resp.text = json.dumps([{"title": t, "bundle": b} for t, b in boards])
elif "snapshot/list" in url:
resp.text = json.dumps({"0": "Default"})
elif "snapshot/name" in url:
resp.text = json.dumps({"name": "Default"})
else:
resp.text = "{}"
return resp

return side_effect


def test_v3_refetch_for_unknown_bundle_does_not_duplicate_list(v3_system: SystemFixture):
"""last.json naming a board we haven't cached refetches the list. The refetch
rebuilds it — a board known before must not appear twice afterwards."""
handler = v3_system.handler

before = [pb.bundle for pb in handler.pedalboard_list]
assert before == ["/path/to/rig.pedalboard", "/path/to/new.pedalboard"]

v3_system.mock_get.side_effect = _list_side_effect(
[
("Integration Rig", "/path/to/rig.pedalboard"),
("New Rig", "/path/to/new.pedalboard"),
("Restored Rig", "/path/to/restored.pedalboard"),
]
)

last_json = Path(handler.data_dir) / "last.json"
last_json.write_text(json.dumps({"pedalboard": "/path/to/restored.pedalboard"}))
os.utime(last_json, (9999, 9999))

handler.poll_modui_changes()

assert handler.current
assert handler.current.pedalboard.title == "Restored Rig"

bundles = [pb.bundle for pb in handler.pedalboard_list]
assert bundles == [
"/path/to/rig.pedalboard",
"/path/to/new.pedalboard",
"/path/to/restored.pedalboard",
]
assert len(handler.pedalboards) == 3


def test_v3_refetch_drops_boards_modui_no_longer_lists(v3_system: SystemFixture):
"""A board deleted in MOD-UI leaves both the dict and the nav list on refetch."""
handler = v3_system.handler

v3_system.mock_get.side_effect = _list_side_effect(
[
("Integration Rig", "/path/to/rig.pedalboard"),
]
)

handler.load_pedalboards()

assert "/path/to/new.pedalboard" not in handler.pedalboards
assert [pb.bundle for pb in handler.pedalboard_list] == ["/path/to/rig.pedalboard"]


def test_v3_outbound_ws_suppressed_during_pedalboard_change(v3_system: SystemFixture, make_plugin):
"""While a pedalboard change is in flight, outbound param_set messages are dropped."""
Expand Down
Loading