diff --git a/common/token.py b/common/token.py
index f9bd96e23..1e2b09d06 100755
--- a/common/token.py
+++ b/common/token.py
@@ -16,6 +16,7 @@
ACTION = 'action'
ADC_INPUT = 'adc_input'
ANALOG_CONTROLLERS = 'analog_controllers'
+AUTOSYNC = 'autosync'
BANK = 'bank'
BUNDLE = 'bundle'
BYPASS = 'bypass'
diff --git a/modalapi/modhandler.py b/modalapi/modhandler.py
index 2eb0429c6..e677724ab 100755
--- a/modalapi/modhandler.py
+++ b/modalapi/modhandler.py
@@ -348,6 +348,9 @@ def set_current_pedalboard(self, pedalboard):
cfg = yaml.load(ymlfile, Loader=yaml.SafeLoader)
self.hardware.reinit(cfg)
+ # Sync current state of analog controls (expression pedals, etc.)
+ self.hardware.sync_analog_controls()
+
# Initialize the data and draw on LCD
self.bind_current_pedalboard()
self.load_current_presets()
diff --git a/pistomp/analogcontrol.py b/pistomp/analogcontrol.py
index 19a4b972e..5632a084a 100755
--- a/pistomp/analogcontrol.py
+++ b/pistomp/analogcontrol.py
@@ -37,4 +37,9 @@ def readChannel(self):
return data
def refresh(self):
+ """Read current value from hardware and potentially take action."""
logging.error("AnalogControl subclass hasn't overriden the refresh method")
+
+ def initialize(self):
+ """Called when the pedalboard has been loaded, e.g. to sync current value."""
+ logging.error("AnalogControl subclass hasn't implemented initialize method")
\ No newline at end of file
diff --git a/pistomp/analogmidicontrol.py b/pistomp/analogmidicontrol.py
index 9dab36b19..1fe7c04c9 100755
--- a/pistomp/analogmidicontrol.py
+++ b/pistomp/analogmidicontrol.py
@@ -12,6 +12,7 @@
#
# You should have received a copy of the GNU General Public License
# along with pi-stomp. If not, see .
+from typing import override
import adafruit_mcp3xxx.mcp3008 as MCP
from adafruit_mcp3xxx.analog_in import AnalogIn
@@ -26,18 +27,23 @@
import logging
-class AnalogMidiControl(analogcontrol.AnalogControl):
+def as_midi_value(adc_value: int):
+ """Convert a 10-bit ADC value (0-1023) to a MIDI value (0-127)."""
+ return util.renormalize(adc_value, 0, 1023, 0, 127)
+
- def __init__(self, spi, adc_channel, tolerance, midi_CC, midi_channel, midiout, type, id=None, cfg={}):
+class AnalogMidiControl(analogcontrol.AnalogControl):
+ def __init__(self, spi, adc_channel, tolerance, midi_CC, midi_channel, midiout, type, id=None, cfg={}, autosync=False):
super(AnalogMidiControl, self).__init__(spi, adc_channel, tolerance)
self.midi_CC = midi_CC
self.midiout = midiout
self.midi_channel = midi_channel
+ self.autosync = autosync
# Parent member overrides
self.type = type
self.id = id
- self.last_read = 0 # this keeps track of the last potentiometer value
+ self.last_read = 0 # this keeps track of the last potentiometer value
self.value = None
self.cfg = cfg
@@ -47,18 +53,33 @@ def set_midi_channel(self, midi_channel):
def set_value(self, value):
self.value = value
- # Override of base class method
+ @override
+ def initialize(self):
+ if not self.autosync:
+ return
+
+ # read the analog pin
+ value = self.readChannel()
+ set_volume = as_midi_value(value)
+
+ cc = [self.midi_channel | CONTROL_CHANGE, self.midi_CC, set_volume]
+ logging.debug("AnalogControl force-sending CC event %s" % cc)
+ self.midiout.send_message(cc)
+
+ # save the reading to prevent duplicate sends on next poll
+ self.last_read = value
+
+ @override
def refresh(self):
# read the analog pin
value = self.readChannel()
# how much has it changed since the last read?
pot_adjust = abs(value - self.last_read)
- value_changed = (pot_adjust > self.tolerance)
+ value_changed = pot_adjust > self.tolerance
if value_changed:
- # convert 16bit adc0 (0-65535) trim pot read into 0-100 volume level
- set_volume = util.renormalize(value, 0, 1023, 0, 127)
+ set_volume = as_midi_value(value)
cc = [self.midi_channel | CONTROL_CHANGE, self.midi_CC, set_volume]
logging.debug("AnalogControl Sending CC event %s" % cc)
diff --git a/pistomp/analogswitch.py b/pistomp/analogswitch.py
index 813062ae2..df9c97d95 100755
--- a/pistomp/analogswitch.py
+++ b/pistomp/analogswitch.py
@@ -12,27 +12,28 @@
#
# You should have received a copy of the GNU General Public License
# along with pi-stomp. If not, see .
+from typing import override
import time
import pistomp.analogcontrol as analogcontrol
import pistomp.switchstate as switchstate
-import pistomp.taptempo as taptempo
+from pistomp.taptempo import TapTempo
LONG_PRESS_TIME = 0.5 # Hold seconds which defines a long press
FALLING_THRESHOLD = 800 # ASSUMES 10-bit ADC, can be changed for debounce handling
class AnalogSwitch(analogcontrol.AnalogControl):
- def __init__(self, spi, adc_channel, tolerance, callback, taptempo=None):
+ def __init__(self, spi, adc_channel, tolerance, callback, taptempo: TapTempo | None = None):
super(AnalogSwitch, self).__init__(spi, adc_channel, tolerance)
#self.value = None # this keeps track of the last value, do we still need this?
self.callback = callback
self.state = switchstate.Value.RELEASED
self.start_time = 0
self.duration = 0
- self.taptempo = taptempo
+ self.taptempo: TapTempo | None = taptempo
- # Override of base class method
+ @override
def refresh(self):
# read the analog channel
new_value = self.readChannel()
@@ -57,3 +58,9 @@ def refresh(self):
self.callback(switchstate.Value.RELEASED)
elif self.state is switchstate.Value.LONGPRESSED:
self.state = switchstate.Value.RELEASED
+
+ @override
+ def initialize(self):
+ # no-op for stateless switches
+ pass
+
\ No newline at end of file
diff --git a/pistomp/hardware.py b/pistomp/hardware.py
index 27cb73eb5..bcc62a1d9 100755
--- a/pistomp/hardware.py
+++ b/pistomp/hardware.py
@@ -20,6 +20,7 @@
import common.token as Token
import common.util as Util
+from pistomp.analogcontrol import AnalogControl
import pistomp.analogmidicontrol as AnalogMidiControl
import pistomp.footswitch as Footswitch
import pistomp.taptempo as taptempo
@@ -46,7 +47,7 @@ def __init__(self, default_config, handler, midiout, refresh_callback):
# Standard hardware objects (not required to exist)
self.relay = None
- self.analog_controls = []
+ self.analog_controls: list[AnalogControl] = []
self.encoders = []
self.controllers = {}
self.footswitches = []
@@ -104,13 +105,15 @@ def reinit(self, cfg):
# reinit hardware as specified by the new cfg context (after pedalboard change, etc.)
self.cfg = self.default_cfg.copy()
- self.__init_midi_default()
-
# Global footswitch init (callbacks and groups)
Footswitch.Footswitch.init(self.handler.callbacks)
- # Footswitch configuration
- self.__init_footswitches(self.cfg)
+ # Analog control configuration
+ for ac in self.analog_controls:
+ try:
+ ac.initialize()
+ except Exception as e:
+ logging.warning(f"Failed to initialize analog control {ac}: {e}")
# Pedalboard specific config
if cfg is not None:
@@ -239,6 +242,7 @@ def create_analog_controls(self, cfg):
midi_cc = Util.DICT_GET(c, Token.MIDI_CC)
threshold = Util.DICT_GET(c, Token.THRESHOLD)
control_type = Util.DICT_GET(c, Token.TYPE)
+ autosync = Util.DICT_GET(c, Token.AUTOSYNC)
if adc_input is None:
logging.error("Config file error. Analog control specified without %s" % Token.ADC_INPUT)
@@ -248,9 +252,11 @@ def create_analog_controls(self, cfg):
continue
if threshold is None:
threshold = 16 # Default, 1024 is full scale
+ if autosync is None:
+ autosync = False # Default to False
control = AnalogMidiControl.AnalogMidiControl(self.spi, adc_input, threshold, midi_cc, midi_channel,
- self.midiout, control_type, id, c)
+ self.midiout, control_type, id, c, autosync)
self.analog_controls.append(control)
key = format("%d:%d" % (midi_channel, midi_cc))
self.controllers[key] = control
@@ -299,9 +305,6 @@ def get_real_midi_channel(self, cfg):
pass
return chan
- def __init_midi_default(self):
- self.__init_midi(self.cfg)
-
def __init_midi(self, cfg):
self.midi_channel = self.get_real_midi_channel(cfg)
# TODO could iterate thru all objects here instead of handling in __init_footswitches
diff --git a/setup/config_templates/default_config.yml b/setup/config_templates/default_config.yml
index 6f12f2d86..e0e836ef5 100755
--- a/setup/config_templates/default_config.yml
+++ b/setup/config_templates/default_config.yml
@@ -54,12 +54,14 @@ hardware:
# id: The id and position on the screen (starting with 0 on the left)
# type: The control type, used to represent the control on the screen (optional)
# midi_CC: The MIDI CC message to be sent when the control is adjusted (optional)
+ # autosync: Whether to send current value on pedalboard load (optional, default: false)
#
#analog_controllers:
# - adc_input: 5
# id: 0
# type: EXPRESSION
# midi_CC: 75
+ # autosync: true
# encoders:
# Each encoder definition is a list which starts with the id
diff --git a/setup/config_templates/default_config_3fs_2knob.yml b/setup/config_templates/default_config_3fs_2knob.yml
index 1e58c23b7..daf6c7421 100755
--- a/setup/config_templates/default_config_3fs_2knob.yml
+++ b/setup/config_templates/default_config_3fs_2knob.yml
@@ -48,12 +48,14 @@ hardware:
# id: The id and position on the screen (starting with 0 on the left)
# type: The control type, used to represent the control on the screen (optional)
# midi_CC: The MIDI CC message to be sent when the control is adjusted (optional)
+ # autosync: Whether to send current value on pedalboard load (optional, default: false)
#
analog_controllers:
#- adc_input: 7
# id: 0
# midi_CC: 77
# type: EXPRESSION
+ # autosync: true
- adc_input: 0
id: 1
midi_CC: 70
diff --git a/setup/config_templates/default_config_3fs_2knob_exp.yml b/setup/config_templates/default_config_3fs_2knob_exp.yml
index c7d6ddd2f..e6c95ad8a 100755
--- a/setup/config_templates/default_config_3fs_2knob_exp.yml
+++ b/setup/config_templates/default_config_3fs_2knob_exp.yml
@@ -48,12 +48,14 @@ hardware:
# id: The id and position on the screen (starting with 0 on the left)
# type: The control type, used to represent the control on the screen (optional)
# midi_CC: The MIDI CC message to be sent when the control is adjusted (optional)
+ # autosync: Whether to send current value on pedalboard load (optional, default: false)
#
analog_controllers:
- adc_input: 7
id: 0
midi_CC: 77
type: EXPRESSION
+ autosync: true
- adc_input: 0
id: 1
midi_CC: 70
diff --git a/setup/config_templates/default_config_pistompcore.yml b/setup/config_templates/default_config_pistompcore.yml
index 16eafcc02..fba746d71 100755
--- a/setup/config_templates/default_config_pistompcore.yml
+++ b/setup/config_templates/default_config_pistompcore.yml
@@ -48,12 +48,14 @@ hardware:
# id: The id and position on the screen (starting with 0 on the left)
# type: The control type, used to represent the control on the screen (optional)
# midi_CC: The MIDI CC message to be sent when the control is adjusted (optional)
+ # autosync: Whether to send current value on pedalboard load (optional, default: false)
#
# analog_controllers:
# - adc_input: 7
# id: 0
# midi_CC: 77
# type: EXPRESSION
+# autosync: true
# - adc_input: 0
# id: 1
# midi_CC: 70
diff --git a/setup/config_templates/default_config_pistomptre.yml b/setup/config_templates/default_config_pistomptre.yml
index 6f12f2d86..e0e836ef5 100644
--- a/setup/config_templates/default_config_pistomptre.yml
+++ b/setup/config_templates/default_config_pistomptre.yml
@@ -54,12 +54,14 @@ hardware:
# id: The id and position on the screen (starting with 0 on the left)
# type: The control type, used to represent the control on the screen (optional)
# midi_CC: The MIDI CC message to be sent when the control is adjusted (optional)
+ # autosync: Whether to send current value on pedalboard load (optional, default: false)
#
#analog_controllers:
# - adc_input: 5
# id: 0
# type: EXPRESSION
# midi_CC: 75
+ # autosync: true
# encoders:
# Each encoder definition is a list which starts with the id