Skip to content
3 changes: 2 additions & 1 deletion pistomp/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@
"type": "integer"
},
"disable": {
"type": "boolean"
"type": "boolean",
"description": "Disable this footswitch entirely or per-pedalboard (disabled=True)"
},
"gpio_input": {
"type": "integer"
Expand Down
2 changes: 1 addition & 1 deletion pistomp/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def __init__(self, midi_channel, midi_CC):
def to_json(self):
return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4)

def set_value(self, value):
def set_value(self, bypass_value: float):
logging.error("Controller subclass hasn't overriden the set_value method")


37 changes: 22 additions & 15 deletions pistomp/footswitch.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def __init__(self, id, led_pin, pixel, midi_CC, midi_channel, midiout, refresh_c
super(Footswitch, self).__init__(midi_channel, midi_CC)
self.id = id
self.display_label = None
self.enabled = False
self.toggled = False
self.led = None
self.midiout = midiout
self.refresh_callback = refresh_callback
Expand All @@ -100,6 +100,7 @@ def __init__(self, id, led_pin, pixel, midi_CC, midi_channel, midiout, refresh_c
self.category = None
self.pixel = pixel
self.longpress_groups = []
self.disabled = False
self.taptempo = taptempo

if adc_input and gpio_input:
Expand Down Expand Up @@ -137,9 +138,9 @@ def set_midi_CC(self, midi_CC):
def set_midi_channel(self, midi_channel):
self.midi_channel = midi_channel

def set_value(self, value):
self.enabled = (value < 1)
self._set_led(self.enabled)
def set_value(self, bypass_value: float):
self.toggled = (bypass_value < 1)
self._set_led(self.toggled)
self.refresh_callback(footswitch=self)

def _set_led(self, enabled):
Expand All @@ -160,12 +161,15 @@ def _set_led(self, enabled):
def set_category(self, category):
self.category = category
if self.pixel:
self.pixel.set_color_by_category(category, self.enabled)
self.pixel.set_color_by_category(category, self.toggled)

def set_lcd_color(self, color):
self.lcd_color = color

def set_longpress_groups(self, groups):
if groups is None:
self.longpress_groups = []
return
if isinstance(groups, str):
groups = groups.split()
if isinstance(groups, list):
Expand All @@ -176,6 +180,8 @@ def set_longpress_groups(self, groups):
info.number_in_group += 1

def poll(self):
if self.disabled:
return
if self.adc_switch:
self.adc_switch.refresh()
elif self.gpio_switch:
Expand All @@ -199,21 +205,21 @@ def pressed(self, state):
# The footswitch will only "toggle" if it's associated with a relay
# (in which case it will toggle with the relay) or with a Midi message
#
new_enabled = not self.enabled
new_toggled = not self.toggled

# First handle Longpress Events
if state is switchstate.Value.LONGPRESSED:
# Update Relay (if relay is associated with this footswitch)
if len(self.relay_list) > 0:
# Pin kept low (long press)
# toggle the relay and LED, exit this method
self.enabled = new_enabled
self.toggled = new_toggled
for r in self.relay_list:
if self.enabled:
if self.toggled:
r.enable()
else:
r.disable()
self._set_led(self.enabled)
self._set_led(self.toggled)
self.refresh_callback(True) # True means this is a bypass change only
else:
# TODO consider case where relay and longpress are specified
Expand All @@ -227,7 +233,7 @@ def pressed(self, state):

# If mapped to preset change
elif self.preset_callback is not None:
# Change the preset and exit this method. Don't flip "enabled" since
# Change the preset and exit this method. Don't flip "toggled" since
# there is no "toggle" action associated with a preset
if self.preset_callback_arg is None:
self.preset_callback()
Expand All @@ -237,16 +243,16 @@ def pressed(self, state):

# Send midi
elif self.midi_CC is not None:
self.enabled = new_enabled
self.toggled = new_toggled
# Update LED
self._set_led(self.enabled)
cc = [self.midi_channel | CONTROL_CHANGE, self.midi_CC, 127 if self.enabled else 0]
self._set_led(self.toggled)
cc = [self.midi_channel | CONTROL_CHANGE, self.midi_CC, 127 if self.toggled else 0]
logging.debug("Sending CC event: %d" % self.midi_CC)
self.midiout.send_message(cc)

# Update plugin parameter if any
if self.parameter is not None:
self.parameter.value = not self.enabled # TODO assumes mapped parameter is :bypass
self.parameter.value = not self.toggled # TODO assumes mapped parameter is :bypass

# Update LCD
self.refresh_callback(footswitch=self)
Expand All @@ -266,7 +272,8 @@ def add_preset(self, callback, callback_arg=None):
self.preset_callback_arg = callback_arg

def clear_pedalboard_info(self):
self.enabled = False
self.toggled = False
self.disabled = False
self.display_label = None
self.set_category(None)
self.preset_callback = None
Expand Down
29 changes: 28 additions & 1 deletion pistomp/hardware.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import pistomp.encoder as Encoder
import pistomp.encodermidicontrol as EncoderMidiControl
import pistomp.footswitch as Footswitch
import pistomp.gpioswitch as gpioswitch
import pistomp.taptempo as taptempo

from abc import ABC, abstractmethod
Expand Down Expand Up @@ -58,6 +59,7 @@ def __init__(self, default_config, handler, midiout, refresh_callback):
self.controllers: dict[str, Controller] = {}
self.footswitches: list[Footswitch.Footswitch] = []
self.encoder_switches = []
self.encoder_switch_map: dict[int, gpioswitch.GpioSwitch] = {}
self.indicators = []
self.debounce_map = None
self.ledstrip = None
Expand Down Expand Up @@ -116,8 +118,9 @@ def reinit(self, cfg):
# Global footswitch init (callbacks and groups)
Footswitch.Footswitch.init(self.handler.callbacks)

# Footswitch configuration
# Apply defaults
self.__init_footswitches(self.cfg)
self.__init_encoders(self.cfg)

# Analog control configuration
for ac in self.analog_controls:
Expand All @@ -130,6 +133,7 @@ def reinit(self, cfg):
if cfg is not None:
self.__init_midi(cfg)
self.__init_footswitches(cfg)
self.__init_encoders(cfg)

@abstractmethod
def init_analog_controls(self):
Expand Down Expand Up @@ -397,6 +401,12 @@ def __init_footswitches(self, cfg):
fs.add_preset(callback=self.handler.preset_set_and_change, callback_arg=preset_value)
fs.set_display_label(str(preset_value))

# Suppress (per-pedalboard disable without removing the object)
if Util.DICT_GET(f, Token.DISABLE) is True:
fs.disabled = True
idx += 1
continue

# LCD/LED attributes
if Token.COLOR in f:
fs.set_lcd_color(f[Token.COLOR])
Expand All @@ -406,3 +416,20 @@ def __init_footswitches(self, cfg):
fs.set_longpress_groups(Util.DICT_GET(f, Token.LONGPRESS))

idx += 1

def __init_encoders(self, cfg: dict | None) -> None:
if cfg is None or Token.HARDWARE not in cfg:
return
cfg_encs = Util.DICT_GET(cfg[Token.HARDWARE], Token.ENCODERS)
if not cfg_encs:
return
for enc_cfg in cfg_encs:
enc_id = Util.DICT_GET(enc_cfg, Token.ID)
if enc_id is None:
continue
sw = self.encoder_switch_map.get(enc_id)
if sw is None:
continue
if Token.LONGPRESS in enc_cfg:
lp_name = enc_cfg[Token.LONGPRESS]
sw.longpress_callback = self.handler.get_callback(lp_name) if lp_name else None
2 changes: 1 addition & 1 deletion pistomp/lcd320x240.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,7 @@ def draw_unbound_footswitches(self):
def update_footswitch(self, footswitch):
for wfs in self.w_footswitches:
if wfs.object == footswitch:
wfs.toggle(footswitch.enabled == False)
wfs.toggle(footswitch.toggled == False)
label = footswitch.get_display_label()
if label:
wfs.label = label
Expand Down
2 changes: 1 addition & 1 deletion pistomp/lcdbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ def base_draw_bound_plugins(self, zone, plugins, footswitches):
continue
f = fss[fs_id]
color = Category.valid_color(f.lcd_color)
if self.color_plugin_bypassed is not None and not f.enabled:
if self.color_plugin_bypassed is not None and not f.toggled:
color = self.color_plugin_bypassed
label = "" if f.display_label is None else f.display_label
x = self.footswitch_pitch[len(fss)] * fs_id
Expand Down
18 changes: 10 additions & 8 deletions pistomp/pistompcore.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@
import pistomp.hardware as hardware
import pistomp.relay as Relay

#import pistomp.lcdili9341 as Lcd # pistompcore UI
import pistomp.lcd320x240 as Lcd # New UI
#import pistomp.lcd128x64 as Lcd
#import pistomp.lcd135x240 as Lcd
#import pistomp.lcdsy7789 as Lcd
# import pistomp.lcdili9341 as Lcd # pistompcore UI
import pistomp.lcd320x240 as Lcd # New UI
# import pistomp.lcd128x64 as Lcd
# import pistomp.lcd135x240 as Lcd
# import pistomp.lcdsy7789 as Lcd

# Pins (Unless the hardware has been changed, these should not be altered)
TOP_ENC_PIN_D = 17
Expand Down Expand Up @@ -70,17 +70,19 @@ def __init__(self, cfg, mod, midiout, refresh_callback):

self.init_analog_controls()

#self.reinit(None)
# self.reinit(None)

def init_lcd(self):
self.mod.add_lcd(Lcd.Lcd(self.mod.homedir, self.mod, flip=True))

def init_encoders(self):
top_enc = Encoder.Encoder(TOP_ENC_PIN_D, TOP_ENC_PIN_CLK, callback=self.mod.universal_encoder_select)
self.encoders.append(top_enc)
enc_sw = gpioswitch.GpioSwitch(1, None, None, callback=self.mod.universal_encoder_sw,
longpress_callback=self.mod.universal_encoder_sw)
enc_sw = gpioswitch.GpioSwitch(
1, None, None, callback=self.mod.universal_encoder_sw, longpress_callback=self.mod.universal_encoder_sw
)
self.encoder_switches.append(enc_sw)
# XXX: user-added encoders via config are not supported here yet (see v3).

def init_relays(self):
self.relay = Relay.Relay(RELAY_SET_PIN, RELAY_RESET_PIN)
Expand Down
2 changes: 2 additions & 0 deletions pistomp/pistomptre.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ def add_encoder(self, id, type, callback, longpress_callback, midi_channel, midi
enc_sw = gpioswitch.GpioSwitch(sw_pin, None, None, callback=self.handler.universal_encoder_sw,
longpress_callback=longpress)
self.encoder_switches.append(enc_sw)
if id is not None:
self.encoder_switch_map[id] = enc_sw

return enc

Expand Down
2 changes: 1 addition & 1 deletion pistomp/testhost.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ def _disp_title(self, line, data):
def _disp_footswitches(self, line, data):
disp = ""
for sw in self.hardware.footswitches:
if sw.enabled:
if sw.toggled:
disp += "ON "
else:
disp += "OFF "
Expand Down
12 changes: 6 additions & 6 deletions tests/test_footswitch.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,11 @@ class TestPressed:
def test_short_press_toggles_enabled_and_sends_midi(self):
midiout = MagicMock()
with _make_footswitch(midi_CC=42, midi_channel=0, midiout=midiout) as fs:
assert fs.enabled is False
assert fs.toggled is False

fs.pressed(switchstate.Value.RELEASED)

assert fs.enabled is True
assert fs.toggled is True
midiout.send_message.assert_called_once()
cc_msg = midiout.send_message.call_args[0][0]
assert cc_msg[1] == 42
Expand All @@ -90,7 +90,7 @@ def test_short_press_twice_toggles_back(self):
fs.pressed(switchstate.Value.RELEASED)
fs.pressed(switchstate.Value.RELEASED)

assert fs.enabled is False
assert fs.toggled is False
assert midiout.send_message.call_count == 2

def test_preset_callback_called_on_press(self):
Expand All @@ -101,7 +101,7 @@ def test_preset_callback_called_on_press(self):
fs.pressed(switchstate.Value.RELEASED)

cb.assert_called_once_with("bank_a")
assert fs.enabled is False # preset press does not toggle enabled
assert fs.toggled is False # preset press does not toggle enabled

def test_longpress_logs_timestamp(self):
with _make_footswitch() as fs:
Expand All @@ -128,13 +128,13 @@ def test_relay_toggle_on_longpress(self):
class TestClearPedalboardInfo:
def test_clears_state(self):
with _make_footswitch() as fs:
fs.enabled = True
fs.toggled = True
fs.display_label = "Reverb"
pixel = MagicMock()
fs.pixel = pixel

fs.clear_pedalboard_info()

assert fs.enabled is False
assert fs.toggled is False
assert fs.display_label is None
assert fs.preset_callback is None
12 changes: 6 additions & 6 deletions tests/test_lcd320x240.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def setup_main_ui(instance):
"exp:pedal": {Token.ID: 0, Token.TYPE: Token.EXPRESSION, Token.COLOR: "Red", Token.NAME: "Wah"},
},
)
mock_footswitches = [MockObject(id=i, enabled=False, get_display_label=lambda: "") for i in range(4)]
mock_footswitches = [MockObject(id=i, toggled=False, get_display_label=lambda: "") for i in range(4)]
instance.link_data(pedalboards=[mock_pedalboard], current=mock_current, footswitches=mock_footswitches)
instance.draw_main_panel()

Expand Down Expand Up @@ -135,26 +135,26 @@ def test_parameter_dialog_snapshot(lcd, snapshot):

def test_update_footswitch_off_snapshot(lcd, snapshot):
instance, _ = lcd
mock_fs = MockObject(id=0, enabled=True, get_display_label=lambda: "Dist", color="Red")
mock_fs = MockObject(id=0, toggled=True, get_display_label=lambda: "Dist", color="Red")
mock_current = MockObject(
pedalboard=MockObject(title="PB", plugins=[]), presets={0: "Clean"}, preset_index=0, analog_controllers={}
)
instance.link_data(pedalboards=[], current=mock_current, footswitches=[mock_fs])
instance.draw_main_panel()
mock_fs.enabled = False # pyright: ignore[reportAttributeAccessIssue]
mock_fs.toggled = False # pyright: ignore[reportAttributeAccessIssue]
instance.update_footswitch(mock_fs)
snapshot()


def test_update_footswitch_on_snapshot(lcd, snapshot):
instance, _ = lcd
mock_fs = MockObject(id=1, enabled=False, get_display_label=lambda: "Drive", color="Orange")
mock_fs = MockObject(id=1, toggled=False, get_display_label=lambda: "Drive", color="Orange")
mock_current = MockObject(
pedalboard=MockObject(title="PB", plugins=[]), presets={0: "Clean"}, preset_index=0, analog_controllers={}
)
instance.link_data(pedalboards=[], current=mock_current, footswitches=[mock_fs])
instance.draw_main_panel()
mock_fs.enabled = True # pyright: ignore[reportAttributeAccessIssue]
mock_fs.toggled = True # pyright: ignore[reportAttributeAccessIssue]
instance.update_footswitch(mock_fs)
snapshot()

Expand Down Expand Up @@ -195,7 +195,7 @@ def test_update_wifi_noop_when_path_unchanged(lcd, mock_handler):

def test_tap_tempo_snapshot(lcd, snapshot):
instance, _ = lcd
mock_fs = MockObject(id=2, enabled=True, get_display_label=lambda: "120")
mock_fs = MockObject(id=2, toggled=True, get_display_label=lambda: "120")
mock_current = MockObject(
pedalboard=MockObject(title="BPM Test", plugins=[]), presets={0: "Clean"}, preset_index=0, analog_controllers={}
)
Expand Down
Loading