From 9457a8f1b332a7a4aff91e3421d012704bc4a707 Mon Sep 17 00:00:00 2001
From: HLammers <62934625+HLammers@users.noreply.github.com>
Date: Thu, 14 Nov 2024 17:25:15 +0100
Subject: [PATCH 1/4] Revised knob and button behaviour
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Values changed by turning the VAL knob or via MIDI learn are no longer processed immediately, but only after pressing the SEL button (which is the same knob), which makes the GUI much more responsive – a changed but not yet confirmed value is indicated by a purple box around the value
* Changed behaviour of the PAGE button from press and hold to toggling on and off page change mode – this makes single hand operation easier
* Changed behaviour of the TRIGGER button from press and hold to select trigger to long-press to open trigger selection pop-up
* Added shortcut to select program via a pop-up which shows when long-pressing the PAGE button
* GUI efficiency improvement: avoiding title bar in some occasions to be drawn twice
* Memory saving improvements
---
src/button.py | 41 +++++-----
src/data.py | 4 +-
src/main_loops.py | 8 --
src/router.py | 6 +-
src/ui.py | 168 ++++++++++++++++++++++----------------
src/ui_blocks.py | 179 +++++++++++++++++++++--------------------
src/ui_page_input.py | 69 +++++++---------
src/ui_page_output.py | 55 +++++--------
src/ui_page_program.py | 89 ++++++++------------
src/ui_pages.py | 29 +++----
10 files changed, 307 insertions(+), 341 deletions(-)
diff --git a/src/button.py b/src/button.py
index 2421116..c403bf5 100644
--- a/src/button.py
+++ b/src/button.py
@@ -26,30 +26,29 @@
_IRQ_RISING_FALLING = Pin.IRQ_RISING | Pin.IRQ_FALLING
_DEBOUNCE_DELAY = const(50) # ms
-_KEPT_PRESSED_DELAY = const(200) # ms
+_LONG_PRESS_DELAY = const(200) # ms
_IRQ_LAST_STATE = const(0)
_IRQ_IDLE_STATE = const(1)
_IRQ_LAST_TIME = const(2)
-_IRQ_AVAILABLE = const(3)
+_IRQ_ONE_BUT_LAST_TIME = const(3)
+_IRQ_AVAILABLE = const(4)
-_BUTTON_EVENT_RELEASED = const(0)
-_BUTTON_EVENT_PRESSED = const(1)
-_BUTTON_EVENT_KEPT = const(2)
+_BUTTON_EVENT_PRESS = const(0)
+_BUTTON_EVENT_LONG_PRESS = const(1)
class Button():
'''button handling class; initiated by ui.__init__'''
- def __init__(self, pin_number: int, pull_up: bool = False, trigger_kept_pressed: bool = False) -> None:
+ def __init__(self, pin_number: int, pull_up: bool = False, long_press: bool = False) -> None:
self.pin_number = pin_number
- self.trigger_kept_pressed = trigger_kept_pressed
- self.irq_data = array('l', [_NONE, _NONE, 0, 0])
+ self.long_press = long_press
+ self.irq_data = array('l', [_NONE, _NONE, 0, _NONE, 0])
self.irq_data[_IRQ_IDLE_STATE] = int(pull_up)
_pin = Pin
pull_direction = _pin.PULL_UP if pull_up else _pin.PULL_DOWN
_pin = _pin(pin_number, _pin.IN, pull_direction)
self.pin = _pin
- self.last_pressed_time = _NONE
_pin.irq(self._callback, _IRQ_RISING_FALLING)
def close(self) -> None:
@@ -57,9 +56,9 @@ def close(self) -> None:
@micropython.viper
def value(self) -> int:
- '''return 1 if triggered and trigger_kept_pressed == False, 1 if short pressed and trigger_kept_pressed == True, 2 if
- kept pressed and trigger_kept_pressed == True, 0 if released and trigger_kept_ressed == True or _NONE if no new data is
- available (yet); called by ui.process_user_input'''
+ '''if long_press == False: return _BUTTON_EVENT_PRESS if pressed; if long_press == True: return _BUTTON_EVENT_PRESS if pressed
+ shorter then _LONG_PRESS_DELAY milliseconds or return _BUTTON_EVENT_LONG_PRESS if pressed longer (if kept pressed the trigger
+ happens after _LONG_PRESS_DELAY milliseconds); called by ui.process_user_input'''
irq_data = ptr32(self.irq_data) # type: ignore
if not irq_data[_IRQ_AVAILABLE]:
return _NONE
@@ -67,21 +66,18 @@ def value(self) -> int:
if int(time.ticks_diff(now, irq_data[_IRQ_LAST_TIME])) < _DEBOUNCE_DELAY:
return _NONE
pressed = irq_data[_IRQ_LAST_STATE] != irq_data[_IRQ_IDLE_STATE]
- if bool(self.trigger_kept_pressed):
+ if bool(self.long_press):
if pressed:
- if int(self.last_pressed_time) == _NONE:
- self.last_pressed_time = irq_data[_IRQ_LAST_TIME]
- difference = int(time.ticks_diff(now, int(self.last_pressed_time)))
- if difference < _KEPT_PRESSED_DELAY:
+ difference = int(time.ticks_diff(now, irq_data[_IRQ_LAST_TIME]))
+ if difference < _LONG_PRESS_DELAY:
return _NONE
irq_data[_IRQ_AVAILABLE] = 0
- return _BUTTON_EVENT_KEPT
- difference = int(time.ticks_diff(irq_data[_IRQ_LAST_TIME], int(self.last_pressed_time)))
- self.last_pressed_time = _NONE
+ return _BUTTON_EVENT_LONG_PRESS
+ difference = int(time.ticks_diff(irq_data[_IRQ_LAST_TIME], irq_data[_IRQ_ONE_BUT_LAST_TIME]))
irq_data[_IRQ_AVAILABLE] = 0
- return _BUTTON_EVENT_PRESSED if difference < _KEPT_PRESSED_DELAY else _BUTTON_EVENT_RELEASED
+ return _BUTTON_EVENT_PRESS if difference < _LONG_PRESS_DELAY else _NONE
irq_data[_IRQ_AVAILABLE] = 0
- return _BUTTON_EVENT_PRESSED if pressed else _NONE
+ return _BUTTON_EVENT_PRESS if pressed else _NONE
@micropython.viper
def _callback(self, pin):
@@ -92,4 +88,5 @@ def _callback(self, pin):
return
irq_data[_IRQ_LAST_STATE] = state
irq_data[_IRQ_AVAILABLE] = 1
+ irq_data[_IRQ_ONE_BUT_LAST_TIME] = irq_data[_IRQ_LAST_TIME]
irq_data[_IRQ_LAST_TIME] = int(time.ticks_ms())
\ No newline at end of file
diff --git a/src/data.py b/src/data.py
index 68289c4..eb53581 100644
--- a/src/data.py
+++ b/src/data.py
@@ -77,7 +77,7 @@ def load_program_json_file(self, id: int) -> dict|None:
def save_data_json_file(self, file: str = 'data.json') -> None:
'''save data set (self.data) to json file; called by self.save_back_up, router._save, router._save_program, Page*._save_*_settings
- or Page*.process_user_input and Page*.midi_learn'''
+ and Page*.process_user_input'''
data_file = open(f'/data_files/{file}', 'w')
json.dump(self.data, data_file)
data_file.close()
@@ -177,7 +177,7 @@ def load(self) -> None:
self.settings = self.data['settings']
self.programs_tuple = tuple((program[0] for program in programs))
-###### this might not be necessary if the set-up is changed to ID-based data mapping
+ ###### this might not be necessary if the set-up is changed to ID-based data mapping
def change_in_programs(self, field: str, old_value: str, new_value: str, condition_field: str = '', condition_value: str = '') -> None:
'''change an old value into a new value for a given field in all program data files; called by router.rename_device and
router.rename_preset'''
diff --git a/src/main_loops.py b/src/main_loops.py
index 3f5179d..a3a7336 100644
--- a/src/main_loops.py
+++ b/src/main_loops.py
@@ -67,16 +67,12 @@ def main():
monitor data'''
_ticks_diff = time.ticks_diff
_ticks_ms = time.ticks_ms
-######
- # _get_encoder_input = ui._get_encoder_input
_process_encoder_input = ui.process_encoder_input
_read_midi_learn_data = router.read_midi_learn_data
_process_midi_learn_data = ui.process_midi_learn_data
_process_user_input = ui.process_user_input
_process_monitor = ui.process_monitor
_process_program_change_break = router.process_program_change_break
-######
- # previous_encoder_input = (None, None)
previous_midi_learn_data = None
main_loop_time = time.ticks_ms()
while True:
@@ -87,10 +83,6 @@ def main():
# turn display off after prolonged inactivity
ui.check_sleep_time_out()
# process encoders
-######
- # encoder_input = _get_encoder_input()
- # if encoder_input != (None, None):
- # _process_encoder_input(encoder_input)
_process_encoder_input()
# process buttons and other input
_process_user_input()
diff --git a/src/router.py b/src/router.py
index 2a0d225..a0d5368 100644
--- a/src/router.py
+++ b/src/router.py
@@ -100,7 +100,8 @@ def update(self, reload_input_devices: bool = True, reload_output_devices: bool
reload_output_triggers: bool = True, reload_input_presets: bool = True, reload_output_presets: bool = True,
program_number: int = _NONE, already_waiting: bool = False) -> None:
'''reload data and call ui.program_change to triggers redraw; called by main_loops.py: init, self._save, self._save_program,
- Page*.process_user_input, Page*.midi_learn, Page*._save_*_settings, Page*._callback_menu, Page*._callback_select'''
+ ui._callback_confirm, ui._callback_select, Page*.process_user_input, Page*._save_*_settings, Page*._callback_menu,
+ Page*._callback_select'''
if not already_waiting:
# request second thread to wait (handshake)
with self.thread_lock:
@@ -416,7 +417,8 @@ def trigger(self) -> None:
self.ui_trigger = key_int
def save_active_program(self, replace: bool) -> None:
- '''save active program changes, either replacing or as a new program after the original; called by PageProgram._callback_confirm'''
+ '''save active program changes, either replacing or as a new program after the original; called by ui._callback_confirm and
+ PageProgram._callback_confirm'''
# request second thread to wait (handshake)
with self.thread_lock:
self.request_wait = True
diff --git a/src/ui.py b/src/ui.py
index d30f30b..a72e73d 100644
--- a/src/ui.py
+++ b/src/ui.py
@@ -10,9 +10,11 @@
You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/.'''
-from machine import Pin, SPI, Timer
+from machine import Pin, SPI
import time
+from data_types import ChainMapTuple
+
from display import ILI9225, Font
import font
@@ -47,9 +49,8 @@
_BUTTON_PAGE_CANCEL = const(2)
# _BUTTON_TRIGGER_CONFIRM = const(3)
-_BUTTON_EVENT_RELEASED = const(0)
-_BUTTON_EVENT_PRESSED = const(1)
-_BUTTON_EVENT_KEPT = const(2)
+_BUTTON_EVENT_PRESS = const(0)
+_BUTTON_EVENT_LONG_PRESS = const(1)
_DISPLAY_SPI = const(0)
_DISPLAY_PIN_DC = const(21)
@@ -72,7 +73,13 @@
_POP_UP_CONFIRM = const(3)
_POP_UP_ABOUT = const(4)
+_CONFIRM_SAVE = const(128) # needs to be higher than block ids
+_CONFIRM_REPLACE = const(129)
+
_SELECT_TRIGGER = const(0)
+_SELECT_PROGRAM = const(1)
+
+_ADD_NEW_LABEL = '[add new]'
_MARGIN = const(3)
_PAGES_W = const(16)
@@ -108,23 +115,23 @@
_SYS_ACTIVE_SENSING = const(0xFE)
_SYS_SYSTEM_RESET = const(0xFF)
-_TEXT_NOTE_OFF = 'NoteOff '
-_TEXT_NOTE_ON = 'NoteOn '
-_TEXT_POLYPHONIC_PRESSURE = 'PolyPres'
-_TEXT_CC = 'ContCtrl'
-_TEXT_PROGRAM_CHANGE = 'ProgChng'
-_TEXT_CHANNEL_PRESSURE = 'ChanPres'
-_TEXT_PITCH_BEND = 'PtchBend'
-_TEXT_QUARTER_FRAME = 'SC Qrtr Frme'
-_TEXT_SONG_POSITION = 'SC Song Pos '
-_TEXT_SONG_SELECT = 'SC Song Slct'
-_TEXT_TUNE_REQUEST = 'SC Tune Req '
-_TEXT_CLOCK = 'SR Clock '
-_TEXT_START = 'SR Start '
-_TEXT_CONTINUE = 'SR Continue '
-_TEXT_STOP = 'SR Stop '
-_TEXT_ACTIVE_SENSING = 'SR Act Sens '
-_TEXT_SYSTEM_RESET = 'SR Sys Reset'
+_TEXT_NOTE_OFF = b'NoteOff '
+_TEXT_NOTE_ON = b'NoteOn '
+_TEXT_POLYPHONIC_PRESSURE = b'PolyPres'
+_TEXT_CC = b'ContCtrl'
+_TEXT_PROGRAM_CHANGE = b'ProgChng'
+_TEXT_CHANNEL_PRESSURE = b'ChanPres'
+_TEXT_PITCH_BEND = b'PtchBend'
+_TEXT_QUARTER_FRAME = b'SC Qrtr Frme'
+_TEXT_SONG_POSITION = b'SC Song Pos '
+_TEXT_SONG_SELECT = b'SC Song Slct'
+_TEXT_TUNE_REQUEST = b'SC Tune Req '
+_TEXT_CLOCK = b'SR Clock '
+_TEXT_START = b'SR Start '
+_TEXT_CONTINUE = b'SR Continue '
+_TEXT_STOP = b'SR Stop '
+_TEXT_ACTIVE_SENSING = b'SR Act Sens '
+_TEXT_SYSTEM_RESET = b'SR Sys Reset'
ui = None
display = None
@@ -147,9 +154,9 @@ def __init__(self) -> None:
encoder_0 = Rotary(_ENCODER_0_PIN_A_CLK, _ENCODER_0_PIN_B_DT)
encoder_1 = Rotary(_ENCODER_1_PIN_A, _ENCODER_1_PIN_B)
self.buttons = [Button(pin, True) for pin in _BUTTON_PINS]
- self.buttons[-1].trigger_kept_pressed = True
- self.buttons[-2].trigger_kept_pressed = True
- self.page_pressed = False
+ self.buttons[-1].long_press = True # trigger/confirm
+ self.buttons[-2].long_press = True # page/cancel
+ self.page_select_mode = False
self.trigger_timer = None
self.frames = []
self.pages = []
@@ -201,8 +208,8 @@ def program_change(self) -> None:
frame.program_change()
def set_trigger(self, device: int = _NONE, preset: int = _NONE) -> None:
- '''set active trigger (triggered by the trigger button), call router.set_trigger and page.set_trigger; called by self._callback_select,
- PageProgram.process_user_input, PageProgram.midi_learn, PageInput.process_user_input and PageInput.midi_learn'''
+ '''set active trigger (triggered by the trigger button), call router.set_trigger and page.set_trigger; called by
+ self._callback_select, PageProgram.process_user_input and PageInput.process_user_input'''
router.set_trigger(device, preset) # type: ignore
for page in self.pages:
page.set_trigger()
@@ -219,10 +226,10 @@ def process_encoder_input(self) -> None:
if encoder_values[i] == _NONE:
continue
if self.active_pop_up is None:
- if i == 1 and self.page_pressed:
+ if i == 1 and self.page_select_mode:
self.frames[_FRAME_PAGE_SELECT].set_page(encoder_values[1])
else:
- self.frames[self.active_page].encoder(i, encoder_values[i], self.page_pressed)
+ self.frames[self.active_page].encoder(i, encoder_values[i], self.page_select_mode)
else: # pop-up visible
self.active_pop_up.encoder(i, encoder_values[i])
@@ -260,21 +267,24 @@ def process_user_input(self) -> None:
continue
if button_number == _BUTTON_PAGE_CANCEL:
if self.active_pop_up is None:
- if value == _BUTTON_EVENT_PRESSED: # short-press of page button
- continue
- kept_pressed = value == _BUTTON_EVENT_KEPT # long-press/release of page button
- self.page_pressed = kept_pressed
- self.frames[self.active_page].set_page_encoders(kept_pressed)
- self.frames[_FRAME_PAGE_SELECT].set_page_encoders(kept_pressed)
- elif value == _BUTTON_EVENT_PRESSED: # pop-up visible, short-press of page button
+ if value == _BUTTON_EVENT_PRESS: # short-press of page button
+ page_select_mode = not self.page_select_mode
+ self.page_select_mode = page_select_mode
+ self.frames[self.active_page].set_page_encoders(page_select_mode)
+ self.frames[_FRAME_PAGE_SELECT].set_page_encoders(page_select_mode)
+ elif not self.page_select_mode: # long-press of page button and not in page select mode
+ options = ChainMapTuple(data.programs_tuple, (_ADD_NEW_LABEL,)) # type: ignore
+ self.pop_ups[_POP_UP_SELECT].open(self.frames[self.active_page], _SELECT_PROGRAM, 'program', options,
+ _router.active_program_number, self._callback_select) # type: ignore
+ elif value == _BUTTON_EVENT_PRESS: # pop-up visible, short-press of page button
if self.active_pop_up.button_cancel():
self.frames[self.active_page].restore()
continue
# if button_number == _BUTTON_TRIGGER_CONFIRM
if self.active_pop_up is None:
- if value == _BUTTON_EVENT_PRESSED: # short-press of trigger button
+ if value == _BUTTON_EVENT_PRESS: # short-press of trigger button
_router.trigger() # type: ignore
- elif value == _BUTTON_EVENT_KEPT: # long-press of trigger button
+ elif value == _BUTTON_EVENT_LONG_PRESS: # long-press of trigger button
options = []
input_devices_tuple_assigned = _router.input_devices_tuple_assigned # type: ignore
input_presets_tuples = _router.input_presets_tuples # type: ignore
@@ -292,34 +302,26 @@ def process_user_input(self) -> None:
input_preset_name = input_presets_tuples[input_device_name][tmp >> 12]
options.append(f'{input_device_name}: {input_preset_name}')
self.pop_ups[_POP_UP_SELECT].open(self.frames[self.active_page], _SELECT_TRIGGER, 'input preset trigger:', options,
- _router.preset_trigger_option, True, self._callback_select) # type: ignore
+ _router.preset_trigger_option, self._callback_select) # type: ignore
else: # pop-up visible
- if value == _BUTTON_EVENT_PRESSED: # short-press of trigger button
+ if value == _BUTTON_EVENT_PRESS: # short-press of trigger button
if self.active_pop_up.button_confirm():
self.frames[self.active_page].restore()
- elif value == _BUTTON_EVENT_RELEASED: # long-press of trigger button
- _trigger_select = self.pop_ups[_POP_UP_SELECT]
- if self.active_pop_up == _trigger_select:
- _trigger_select.close()
- frame = self.frames[self.active_page]
- if len(frame.blocks) > 0:
- frame.blocks[frame.selected_block].update(True, False)
- self.frames[self.active_page].restore()
- def process_midi_learn_data(self, data) -> None:
+ def process_midi_learn_data(self, midi_learn_data) -> None:
'''process midi learn data (router.send_to_monitor > router.midi_learn_data > ui.process_midi_learn_data > Page.midi_learn); called by
main_loops.py: main'''
- port, channel, note, program, cc, cc_value, route_number = data
+ port, channel, note, program, cc, cc_value, route_number = midi_learn_data
self.frames[self.active_page].midi_learn(port, channel, note, program, cc, cc_value, route_number)
def process_monitor(self) -> None:
'''process monitor data (router.send_to_monitor > router.monitor_data > ui.process_monitor > PageMonitor.add_to_monitor); called by
main_loops.py: main'''
_router = router
- data = _router.read_monitor_data() # type: ignore
- if data is None:
+ monitor_data = _router.read_monitor_data() # type: ignore
+ if monitor_data is None:
return
- mode, port, channel, command, data_1, data_2, route_number = data
+ mode, port, channel, command, data_1, data_2, route_number = monitor_data
text_routing = ''
text_midi_in = ''
text_midi_out = ''
@@ -359,49 +361,49 @@ def process_monitor(self) -> None:
if command == _COMMAND_NOTE_OFF:
data_1_str = '' if data_1 == _NONE else mt.number_to_note(data_1)
data_2_str = '' if data_2 == _NONE else str(data_2)
- descriptive_str = f'P{port} C{channel:>2} {_TEXT_NOTE_OFF} {data_1_str:>3} {data_2_str:>3}'
+ descriptive_str = f'P{port} C{channel:>2} {_TEXT_NOTE_OFF.decode()} {data_1_str:>3} {data_2_str:>3}'
elif command == _COMMAND_NOTE_ON:
data_1_str = '' if data_1 == _NONE else mt.number_to_note(data_1)
data_2_str = '' if data_2 == _NONE else str(data_2)
- descriptive_str = f'P{port} C{channel:>2} {_TEXT_NOTE_ON} {data_1_str:>3} {data_2_str:>3}'
+ descriptive_str = f'P{port} C{channel:>2} {_TEXT_NOTE_ON.decode()} {data_1_str:>3} {data_2_str:>3}'
elif command == _COMMAND_POLYPHONIC_PRESSURE:
- descriptive_str = f'P{port} C{channel:>2} {_TEXT_POLYPHONIC_PRESSURE} {data_1:>3} {data_2:>3}'
+ descriptive_str = f'P{port} C{channel:>2} {_TEXT_POLYPHONIC_PRESSURE.decode()} {data_1:>3} {data_2:>3}'
elif command == _COMMAND_CC:
- descriptive_str = f'P{port} C{channel:>2} {_TEXT_CC} {data_1:>3} {data_2:>3}'
+ descriptive_str = f'P{port} C{channel:>2} {_TEXT_CC.decode()} {data_1:>3} {data_2:>3}'
elif command == _COMMAND_PROGRAM_CHANGE:
- descriptive_str = f'P{port} C{channel:>2} {_TEXT_PROGRAM_CHANGE} {data_1:>3} '
+ descriptive_str = f'P{port} C{channel:>2} {_TEXT_PROGRAM_CHANGE.decode()} {data_1:>3} '
elif command == _COMMAND_CHANNEL_PRESSURE:
- descriptive_str = f'P{port} C{channel:>2} {_TEXT_CHANNEL_PRESSURE} {data_1:>3} '
+ descriptive_str = f'P{port} C{channel:>2} {_TEXT_CHANNEL_PRESSURE.decode()} {data_1:>3} '
elif command == _COMMAND_PITCH_BEND:
value = (data_2 << 7) + data_1 - 0x2000
if value > 0:
value = f'+{value}'
- descriptive_str = f'P{port} C{channel:>2} {_TEXT_PITCH_BEND} {value:>7}'
+ descriptive_str = f'P{port} C{channel:>2} {_TEXT_PITCH_BEND.decode()} {value:>7}'
elif command == _SYS_SYSEX_START:
descriptive_str = '' # ignore
elif command == _SYS_QUARTER_FRAME:
- descriptive_str = f'P{port} {_TEXT_QUARTER_FRAME} {data_1:>3} '
+ descriptive_str = f'P{port} {_TEXT_QUARTER_FRAME.decode()} {data_1:>3} '
elif command == _SYS_SONG_POSITION:
value = (data_2 << 7) + data_1
- descriptive_str = f'P{port} {_TEXT_SONG_POSITION} {value:>7}'
+ descriptive_str = f'P{port} {_TEXT_SONG_POSITION.decode()} {value:>7}'
elif command == _SYS_SONG_SELECT:
- descriptive_str = f'P{port} {_TEXT_SONG_SELECT} {data_1:>3} '
+ descriptive_str = f'P{port} {_TEXT_SONG_SELECT.decode()} {data_1:>3} '
elif command == _SYS_TUNE_REQUEST:
- descriptive_str = f'P{port} {_TEXT_TUNE_REQUEST} '
+ descriptive_str = f'P{port} {_TEXT_TUNE_REQUEST.decode()} '
elif command == _SYS_SYSEX_END:
descriptive_str = '' # ignore
elif command == _SYS_CLOCK:
- descriptive_str = f'P{port} {_TEXT_CLOCK} '
+ descriptive_str = f'P{port} {_TEXT_CLOCK.decode()} '
elif command == _SYS_START:
- descriptive_str = f'P{port} {_TEXT_START} '
+ descriptive_str = f'P{port} {_TEXT_START.decode()} '
elif command == _SYS_CONTINUE:
- descriptive_str = f'P{port} {_TEXT_CONTINUE} '
+ descriptive_str = f'P{port} {_TEXT_CONTINUE.decode()} '
elif command == _SYS_STOP:
- descriptive_str = f'P{port} {_TEXT_STOP} '
+ descriptive_str = f'P{port} {_TEXT_STOP.decode()} '
elif command == _SYS_ACTIVE_SENSING:
- descriptive_str = f'P{port} {_TEXT_ACTIVE_SENSING} '
+ descriptive_str = f'P{port} {_TEXT_ACTIVE_SENSING.decode()} '
elif command == _SYS_SYSTEM_RESET:
- descriptive_str = f'P{port} {_TEXT_SYSTEM_RESET} '
+ descriptive_str = f'P{port} {_TEXT_SYSTEM_RESET.decode()} '
else:
descriptive_str = ''
if descriptive_str != '':
@@ -436,11 +438,29 @@ def _wake_up(self) -> None:
self.sleep_time = time.ticks_ms()
scr.set_display(True) # type: ignore
+ def _callback_confirm(self, caller_id: int, confirm: bool) -> None:
+ '''callback for confirm pop-up; called (passed on) by self._callback_confirm'''
+ _router = router
+ if caller_id == _CONFIRM_SAVE:
+ if confirm:
+ self.pop_ups[_POP_UP_CONFIRM].open(self, _CONFIRM_REPLACE,f'replace {_router.active_program_number + 1:0>3}?', # type: ignore
+ self._callback_confirm)
+ else:
+ next_program = self.next_program
+ if next_program != _NONE:
+ _router.update(False, False, False, False, False, False, next_program) # type: ignore
+ elif caller_id == _CONFIRM_REPLACE:
+ _router.save_active_program(confirm) # type: ignore
+ next_program = self.next_program
+ if next_program != _NONE:
+ if not confirm and next_program > _router.active_program_number: # type: ignore
+ next_program += 1
+ _router.update(False, False, False, False, False, False, next_program) # type: ignore
def _callback_select(self, caller_id: int, selection: int) -> None:
'''callback for select pop-up; called (passed on) by self.process_user_input'''
+ _router = router
if caller_id == _SELECT_TRIGGER:
- _router = router
if selection == _NONE or selection == _router.preset_trigger_option: # type: ignore
return
# (6) 12 12 2
@@ -454,6 +474,14 @@ def _callback_select(self, caller_id: int, selection: int) -> None:
input_device = tmp & 0xFFF
input_preset = tmp >> 12
self.set_trigger(input_device, input_preset)
+ elif caller_id == _SELECT_PROGRAM:
+ if selection == _NONE or selection == _router.active_program_number: # type: ignore
+ return
+ if _router.program_changed: # type: ignore
+ self.next_program = selection
+ self.pop_ups[_POP_UP_CONFIRM].open(self, _CONFIRM_SAVE, 'save changes?', self._callback_confirm)
+ else:
+ _router.update(False, False, False, False, False, False, selection) # type: ignore
class _Display():
'''display class; initiated once by ui.__init__'''
diff --git a/src/ui_blocks.py b/src/ui_blocks.py
index 8ff86e4..b188057 100644
--- a/src/ui_blocks.py
+++ b/src/ui_blocks.py
@@ -16,49 +16,50 @@
import ui
if __debug__: import screen_log
-_VERSION = '0.1.0'
-_YEAR = const(2024)
-
-_ALIGN_LEFT = const(0)
-_ALIGN_CENTRE = const(1)
-_ALIGN_RIGHT = const(2)
-
-_COLOR_TABS_DARK = const(0xAA29) # 0x29AA dark purple blue
-_COLOR_TABS_LIGHT = const(0xD095) # 0x95D0 green
-_COLOR_TITLE_BACK = const(0x06ED) # 0xED06 orange
-_COLOR_TITLE_FORE = const(0xAA29) # 0x29AA dark purple blue
-_COLOR_BLOCK_DARK = const(0xAA29) # 0x29AA dark purple blue
-_COLOR_BLOCK_LIGHT = const(0xD9CD) # 0xCDD9 light purple grey
-_COLOR_SELECTED_DARK = const(0x8E33) # 0x338E dark sea green
-_COLOR_SELECTED_LIGHT = const(0xFD97) # 0x97FD light sea green
-_COLOR_LINE = const(0x06ED) # 0xED06 orange
-_COLOR_POP_UP_DARK = const(0x8E33) # 0x338E dark sea green
-_COLOR_POP_UP_LIGHT = const(0xFD97) # 0x97FD light sea green
-
-_MARGIN = const(3)
-_MARGIN_LARGE = const(10)
-_TAB_H = const(36) # (h + len(_PAGE_LABELS)) // len(_PAGE_LABELS)
-_TITLE_BAR_W = const(204)
-_TITLE_BAR_H = const(14)
-_PROGRAM_NUMBER_W = const(25)
-_BLOCK_H = const(27)
-_BLOCK_W = const(102)
-_LABEL_H = const(10)
-_VALUE_H = const(17)
-_BUTTON_MARGIN_X = const(20)
-_BUTTON_MARGIN_Y = const(4)
-_TEXT_H = const(15)
-_CURSOR_H = const(10)
-_CHAR_W = const(13)
-_CHAR_H = const(11)
-_CONTROL_H = const(16)
-_SELECT_TRIGGER_W = const(160)
-_MENU_W = const(102)
-_CONFIRM_W = const(102)
-
-_TEXT_INPUT_CHARACTERS = '''ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#%^&*()_-+=[]\\{}|:;"',./<>?'''
-_CHARACTER_COLUMNS = const(13)
-_TEXT_INPUT_SPACE_BAR = 'space bar'
+_VERSION = b'0.2.0'
+_YEAR = const(2024)
+
+_ALIGN_LEFT = const(0)
+_ALIGN_CENTRE = const(1)
+_ALIGN_RIGHT = const(2)
+
+_COLOR_TABS_DARK = const(0xAA29) # 0x29AA dark purple blue
+_COLOR_TABS_LIGHT = const(0xD095) # 0x95D0 green
+_COLOR_TITLE_BACK = const(0x06ED) # 0xED06 orange
+_COLOR_TITLE_FORE = const(0xAA29) # 0x29AA dark purple blue
+_COLOR_BLOCK_DARK = const(0xAA29) # 0x29AA dark purple blue
+_COLOR_BLOCK_LIGHT = const(0xD9CD) # 0xCDD9 light purple grey
+_COLOR_SELECTED_DARK = const(0x8E33) # 0x338E dark sea green
+_COLOR_SELECTED_LIGHT = const(0xFD97) # 0x97FD light sea green
+_COLOR_SELECTED_CHANGED = const(0x6CA0) # 0xA06C purple
+_COLOR_LINE = const(0x06ED) # 0xED06 orange
+_COLOR_POP_UP_DARK = const(0x8E33) # 0x338E dark sea green
+_COLOR_POP_UP_LIGHT = const(0xFD97) # 0x97FD light sea green
+
+_MARGIN = const(3)
+_MARGIN_LARGE = const(10)
+_TAB_H = const(36) # (h + len(_PAGE_LABELS)) // len(_PAGE_LABELS)
+_TITLE_BAR_W = const(204)
+_TITLE_BAR_H = const(14)
+_PROGRAM_NUMBER_W = const(25)
+_BLOCK_H = const(27)
+_BLOCK_W = const(102)
+_LABEL_H = const(10)
+_VALUE_H = const(17)
+_BUTTON_MARGIN_X = const(20)
+_BUTTON_MARGIN_Y = const(4)
+_TEXT_H = const(15)
+_CURSOR_H = const(10)
+_CHAR_W = const(13)
+_CHAR_H = const(11)
+_CONTROL_H = const(16)
+_SELECT_TRIGGER_W = const(160)
+_MENU_W = const(102)
+_CONFIRM_W = const(102)
+
+_TEXT_INPUT_CHARACTERS = b'''ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#%^&*()_-+=[]\\{}|:;"',./<>?'''
+_CHARACTER_COLUMNS = const(13)
+_TEXT_INPUT_SPACE_BAR = b'space bar'
_bit_buffer = memoryview(bytearray(3048)) # large enough for text edit)
@@ -81,13 +82,13 @@ def __init__(self, id: int, row: int, col: int, cols: int, selected: bool, label
def update(self, selected: bool, redraw: bool = True, encoder_nr: int = 1) -> None:
'''update block, set selection state and redraw if necessary; called by PopUp.open, PopUp.close, ui.process_user_input,
- Page.set_visibility, Page.restore, Page.set_page_encoders, Page.encoder, Page._draw, Page._sub_page_selector
- and Page*.program_change'''
+ Page.set_visibility, Page.restore, Page.set_page_encoders, Page.encoder, Page._draw, Page._sub_page_selector and
+ Page*.program_change'''
pass
def draw(self, suppress_selected: bool, only_value: bool, frame_buffer) -> None:
'''draw whole block or only value part of block; called by PagesTabs.update, TitleBar.update, *Block.update, PagesTabs.encoder
- ButtonBlock.button_select, SelectBlock.set_label, SelectBlock.set_options, Page._draw, _Monitor.draw, Page.restore, Page*._load,
+ ButtonBlock.button_select, SelectBlock.set_label, SelectBlock.set_options, Page._draw, _Monitor.draw, Page.restore, Page*._load
and PageProgram._save_*_settings'''
if __debug__: screen_log.add_marker(f'add block {self.row},{self.col}, selected: {self.selected and not suppress_selected}')
if self.selected and not suppress_selected:
@@ -189,7 +190,7 @@ def __init__(self, title: str, page_number: int, number_of_pages: int) -> None:
self.font = ui.display.font
def update(self, selected, redraw: bool = True):
- '''update block, set selection state and redraw if necessary; called by Page._draw and Page._sub_page_selector'''
+ '''update block, set selection state and redraw if necessary; called by Page.set_page_encoders, Page._draw and Page._sub_page_selector'''
self.selected = selected
if selected:
ui.encoder_0.set(self.page_number - 1, 0, self.number_of_pages - 1)
@@ -306,15 +307,19 @@ def __init__(self, id, row: int, col: int, cols: int, selected: bool, label: str
super().__init__(id, row, col, cols, selected, label, add_line)
self.options = options
self.selection = selection
+ self.new_selection = selection
self.callback_func = callback_func
def update(self, selected: bool, redraw: bool = True, encoder_nr: int = 1) -> None:
'''update block, set selection state and redraw if necessary; called by PopUp.open, PopUp.close, ui.process_user_input,
Page.set_visibility, Page.set_page_encoders, Page.encoder and Page*.program_change and Page._draw'''
self.selected = selected
+ selection = self.selection
if selected:
encoder = ui.encoder_0 if encoder_nr == 0 else ui.encoder_1
- encoder.set(self.selection, 0, len(self.options) - 1)
+ encoder.set(selection, 0, len(self.options) - 1)
+ elif selection != self.new_selection:
+ self.new_selection = selection
if redraw:
self.draw()
@@ -333,15 +338,22 @@ def draw(self, suppress_selected: bool = False, only_value: bool = False) -> Non
_text(0, 0, w, _LABEL_H, self.label, 0, _ALIGN_CENTRE, _buffer)
_buffer.rect(0, _LABEL_H, w, _VALUE_H, False, True)
y = _LABEL_H + 1
- if self.selection < len(self.options):
- value = self.options[self.selection]
+ if self.new_selection < len(self.options):
+ value = self.options[self.new_selection]
else:
value = ''
w = self.w
_text(0, y, w, _VALUE_H - 2, value, 1, _ALIGN_CENTRE, _buffer)
super().draw(suppress_selected, only_value, _buffer)
+ x = self.x
if self.add_line:
- ui.scr.fill_rect(self.x, self.y + _BLOCK_H - 2, w, 2, _COLOR_LINE)
+ ui.scr.fill_rect(x, self.y + _BLOCK_H - 2, w, 2, _COLOR_LINE)
+ if self.selection != self.new_selection:
+ y = self.y + _LABEL_H
+ ui.scr.fill_rect(x, y, w, 2, _COLOR_SELECTED_CHANGED)
+ ui.scr.fill_rect(x, y + _VALUE_H - 2, w, 2, _COLOR_SELECTED_CHANGED)
+ ui.scr.fill_rect(x, y + 2, 2, _VALUE_H - 4, _COLOR_SELECTED_CHANGED)
+ ui.scr.fill_rect(x + w - 2, y + 2, 2, _VALUE_H - 4, _COLOR_SELECTED_CHANGED)
def set_label(self, label: str, redraw: bool = True):
'''set text for label part of block; called by PagesProgram._set_program_options'''
@@ -349,37 +361,44 @@ def set_label(self, label: str, redraw: bool = True):
if redraw:
self.draw()
- def set_options(self, options = None, selection: int = 0, redraw: bool = True) -> int:
- '''set list of options and current selection; called by self.encoder, Pages*._set_*_options and PageSettings.midi_learn'''
+ def set_options(self, options = None, selection: int = 0, redraw: bool = True) -> None:
+ '''set list of options and current selection; called Pages*._set_*_options'''
if options is None:
options = self.options
else:
if len(options) == 0:
options = ('',)
self.options = options
- if self.selected and not ui.ui.page_pressed:
+ if self.selected and not ui.ui.page_select_mode:
ui.encoder_1.set(selection, 0, len(options) - 1)
- if selection >= len(options):
- selection = len(options) - 1
self.selection = selection
- self.text = str(options[selection])
+ self.new_selection = selection
if redraw:
self.draw()
- return selection
+
+ def set_selection(self, selection: int) -> None:
+ '''set list of options and current selection; called by self.encoder and Page*.midi_learn'''
+ self.new_selection = selection
+ self.draw(only_value=True)
def encoder(self, value: int) -> None:
'''process value encoder input at block level (ui._callback_encoder_0/_callback_encoder_1 > global encoder_input_0/encoder_input_1 >
ui.process_encoder > Page/PopUp.encoder/PagesTab.set_page > Block: *.encoder); called by Pages.encoder'''
- if not self.selected or self.callback_func is None:
+ if not self.selected:
return
- value = self.set_options(selection=value)
- self.callback_func(self.id, value, self.options[value])
+ self.set_selection(value)
def button_select(self) -> None:
- '''process select button press at block level; called by Page.button_select'''
+ '''process select button press at block level storing changed selection if applicable, otherwise calling context menu/function;
+ called by Page.button_select'''
if not self.selected or self.callback_func is None:
return
- self.callback_func(self.id, button_encoder_1=True)
+ new_selection = self.new_selection
+ if self.selection == new_selection:
+ self.callback_func(self.id, button_encoder_1=True)
+ else:
+ self.selection = new_selection
+ self.callback_func(self.id, new_selection, self.options[new_selection])
def button_backspace(self) -> None:
'''process backspace button press at block level; called by Page.button_backspace'''
@@ -505,9 +524,9 @@ def draw(self) -> None:
for i, ch in enumerate(_TEXT_INPUT_CHARACTERS):
x = self.characters_left + (i % _CHARACTER_COLUMNS) * _CHAR_W
y = self.characters_top + (i // _CHARACTER_COLUMNS) * _CHAR_H
- _text(x, y, _CHAR_W, _CHAR_H, ch, 1, _ALIGN_CENTRE, _buffer)
+ _text(x, y, _CHAR_W, _CHAR_H, chr(ch), 1, _ALIGN_CENTRE, _buffer)
_text(self.characters_left, self.characters_top + self.character_rows * _CHAR_H, self.input_w, _CHAR_W,
- _TEXT_INPUT_SPACE_BAR, 1, _ALIGN_CENTRE, _buffer)
+ _TEXT_INPUT_SPACE_BAR.decode(), 1, _ALIGN_CENTRE, _buffer)
ui.scr.draw_frame_buffer(self.x, self.y, self.w, self.h, _COLOR_POP_UP_DARK, _COLOR_POP_UP_LIGHT, _buffer)
self._draw_input_text()
@@ -525,7 +544,7 @@ def button_select(self) -> None:
return
if self.selection_y < self.character_rows:
pos = self.selection_y * _CHARACTER_COLUMNS + self.selection_x
- self.text = self.text + _TEXT_INPUT_CHARACTERS[pos]
+ self.text = self.text + chr(_TEXT_INPUT_CHARACTERS[pos])
else:
self.text = self.text + ' '
self._draw_input_text()
@@ -591,11 +610,11 @@ def _draw_selection(self, selection_x: int, selection_y: int, selected: bool) ->
y = self.y + self.characters_top + selection_y * _CHAR_H
if selection_y < self.character_rows:
pos = selection_y * _CHARACTER_COLUMNS + selection_x
- text = _TEXT_INPUT_CHARACTERS[pos]
+ text = chr(_TEXT_INPUT_CHARACTERS[pos])
x = self.x + self.characters_left + selection_x * _CHAR_W
w = _CHAR_W
else:
- text = _TEXT_INPUT_SPACE_BAR
+ text = _TEXT_INPUT_SPACE_BAR.decode()
x = self.x + self.characters_left
w = self.input_w
_buffer = framebuf.FrameBuffer(_bit_buffer, w, _CHAR_H, framebuf.MONO_HMSB)
@@ -611,14 +630,13 @@ def __init__(self, id: int) -> None:
self.inside_w = _SELECT_TRIGGER_W
self.inside_h = _LABEL_H + _VALUE_H
- def open(self, frame, caller_id: int, label: str, options: list|tuple, selection: int, instant: bool = False, callback_func=None) -> None:
- '''open and draw pop-up and deselect selected page block; called by ui.process_user_input, Page*.process_user_input and
- PageProgram._callback_menu'''
+ def open(self, frame, caller_id: int, label: str, options: list|tuple, selection: int, callback_func=None) -> None:
+ '''open and draw pop-up and deselect selected page block; called by ui.process_user_input, ui._callback_confirm,
+ ui._callback_select, Page*.process_user_input and PageProgram._callback_menu'''
self.caller_id = caller_id
self.label = label
self.options = options
self.selection = selection
- self.instant = instant
self._callback_func = callback_func
super().open(frame)
@@ -661,24 +679,9 @@ def encoder(self, encoder_nr: int, value: int) -> None:
return
self.selection = value
self.draw(True)
- if not self.instant:
- return
- try:
- self._callback_func(self.caller_id, self.selection) # type: ignore
- except:
- pass
-
- def button_cancel(self) -> bool:
- '''process cancel button at pop-up level; called by ui.process_user_input'''
- if self.instant:
- return False
- self.close()
- return True
def button_select(self) -> None:
'''process select button press at pop-up level; called by SelectPopUp.button_confirm and ui.process_user_input'''
- if self.instant:
- return
self.close()
try:
self._callback_func(self.caller_id, self.selection) # type: ignore
@@ -823,7 +826,7 @@ def draw(self) -> None:
y = _MARGIN_LARGE
self.font.text_box(_MARGIN_LARGE, y, self.inside_w, _CONTROL_H, 'Cybo-Drummer', 1, _ALIGN_CENTRE, _buffer)
y += _CONTROL_H
- self.font.text_box(_MARGIN_LARGE, y, self.inside_w, _CONTROL_H, f'version {_VERSION}', 1, _ALIGN_CENTRE, _buffer)
+ self.font.text_box(_MARGIN_LARGE, y, self.inside_w, _CONTROL_H, f'version {_VERSION.decode()}', 1, _ALIGN_CENTRE, _buffer)
y += _CONTROL_H
self.font.text_box(_MARGIN_LARGE, y, self.inside_w, _CONTROL_H, f'(c) {_YEAR}', 1, _ALIGN_CENTRE, _buffer)
y += _CONTROL_H
diff --git a/src/ui_page_input.py b/src/ui_page_input.py
index 89f0fcc..1f628e9 100644
--- a/src/ui_page_input.py
+++ b/src/ui_page_input.py
@@ -179,60 +179,46 @@ def midi_learn(self, port: int, channel: int, note: int, program: int, cc: int,
if sub_page == _SUB_PAGE_PORTS:
if channel == _NONE and route_number == _NONE:
return
- port = (block - _PORT_FIRST_DEVICE) // 2
- col = (block - _PORT_FIRST_DEVICE) % 2
- if col == 1: # device
+ if (block - _PORT_FIRST_DEVICE) % 2 == 1: # device
return
- self.port_settings[port][col] = channel + 1
- if self._save_port_settings():
- self._set_port_options()
+ value = channel + 1
elif sub_page == _SUB_PAGE_DEVICES:
if block == _DEVICE_DEVICE:
if route_number == _NONE:
return
_router = ui.router
- from_device = _router.input_devices_tuple_assigned.index(_router.routing[route_number]['input_device'])
- if from_device == _router.input_device_value:
- return
- self.device_device = from_device + 1
- self.device_new_device = False
- self.device_trigger = 0
- self.device_new_trigger = False
- self._set_device_options()
+ value = _router.input_devices_tuple_assigned.index(_router.routing[route_number]['input_device'])
elif block == _DEVICE_TRIGGER_NOTE:
if note == _NONE or self.device_new_device or self.device_new_trigger:
return
- if self._save_device_settings(block, note):
- self._set_device_options()
+ value = note
+ self.blocks[_DEVICE_TRIGGER_NOTE].set_selection(note)
elif block == _DEVICE_TRIGGER_PEDAL_CC:
if note == _NONE or self.device_new_device or self.device_new_trigger:
return
- if self._save_device_settings(_DEVICE_TRIGGER_PEDAL_CC, cc):
- self._set_device_options()
+ value = cc
+ else:
+ return
elif sub_page == _SUB_PAGE_PRESETS:
if block == _PRESET_DEVICE:
if route_number == _NONE:
return
_router = ui.router
- from_device = _router.input_devices_tuple_assigned.index(_router.routing[route_number]['input_device'])
- if from_device == self.preset_device:
- return
- self.preset_device = from_device
- ui.ui.set_trigger(from_device)
+ value = _router.input_devices_tuple_assigned.index(_router.routing[route_number]['input_device'])
elif block == _PRESET_PRESET:
if route_number == _NONE:
return
_router = ui.router
- from_preset = _router.input_presets_tuple.index(_router.routing[route_number]['input_preset'])
- if from_preset == self.preset_preset:
- return
- self.preset_preset = from_preset
- ui.ui.set_trigger(preset=from_preset)
+ value = _router.input_presets_tuple.index(_router.routing[route_number]['input_preset'])
elif block == _PRESET_CC_MIN or block == _PRESET_CC_MAX:
if cc_value == _NONE or self.preset_new_preset:
return
- if self._save_preset_settings(block, cc_value, ''):
- self._set_preset_options()
+ value = cc_value
+ else:
+ return
+ else:
+ return
+ self.blocks[block].set_selection(value)
def _build_page(self) -> None:
'''build page (without drawing it); called by self.__init__ and self._build_sub_page'''
@@ -321,7 +307,7 @@ def _load_port_options(self) -> None:
port = map['port']
if port != _NONE:
settings[port] = [map['channel'], device]
- self._set_port_options(False)
+ self._set_port_options()
def _load_preset_options(self, redraw: bool = True) -> None:
'''load and set values to options and values to input blocks on preset sub-page; called by self.set_trigger and self._load'''
@@ -353,9 +339,8 @@ def _load_preset_options(self, redraw: bool = True) -> None:
preset_maps[i] = maps[i] if i < n else ''
self._set_preset_options(redraw)
- def _set_port_options(self, redraw: bool = True) -> None:
- '''set options and values to input blocks on ports sub-page; called by self.process_user_input, self.midi_learn and
- self._load_port_options'''
+ def _set_port_options(self) -> None:
+ '''set options and values to input blocks on ports sub-page; called by self._load_port_options'''
if self.sub_page != _SUB_PAGE_PORTS:
return
devices_tuple = ChainMapTuple(('____',), ui.router.input_devices_tuple_assigned)
@@ -368,13 +353,13 @@ def _set_port_options(self, redraw: bool = True) -> None:
if port == selected_port:
device = self.selected_port_device
device_option = 0 if device == '' else devices_tuple.index(device)
- blocks[_PORT_FIRST_DEVICE + 2 * port].set_options(devices_tuple, device_option, redraw)
+ blocks[_PORT_FIRST_DEVICE + 2 * port].set_options(devices_tuple, device_option, False)
channel_option = channel + 1 # _NONE becomes 0
- blocks[_PORT_FIRST_CHANNEL + 2 * port].set_options(selection=channel_option, redraw=redraw)
+ blocks[_PORT_FIRST_CHANNEL + 2 * port].set_options(selection=channel_option, redraw=False)
def _set_device_options(self, redraw: bool = True) -> None:
- '''load and set options and values to input blocks on device sub-page; called by self.process_user_input, self.midi_learn, self._load
- and self._callback_confirm'''
+ '''load and set options and values to input blocks on device sub-page; called by self.process_user_input, self._load and
+ self._callback_confirm'''
if self.sub_page != _SUB_PAGE_DEVICES:
return
_router = ui.router
@@ -409,8 +394,8 @@ def _set_device_options(self, redraw: bool = True) -> None:
blocks[_DEVICE_TRIGGER_PEDAL_CC].set_options(selection=pedal_cc, redraw=redraw)
def _set_preset_options(self, redraw: bool = True) -> None:
- '''set options and values to input blocks on preset sub-page; called by self.process_user_input, self.midi_learn,
- self._load_preset_option and self._callback_confirm'''
+ '''set options and values to input blocks on preset sub-page; called by self.process_user_input, self._load_preset_option and
+ self._callback_confirm'''
if self.sub_page != _SUB_PAGE_PRESETS:
return
_router = ui.router
@@ -441,7 +426,7 @@ def _set_preset_options(self, redraw: bool = True) -> None:
blocks[_PRESET_FIRST_MAP + i].set_options(triggers_tuple, value, redraw)
def _save_port_settings(self) -> None:
- '''save values from input blocks on ports sub-page; called by self.process_user_input and self.midi_learn'''
+ '''save values from input blocks on ports sub-page; called by self.process_user_input'''
_data = ui.data
mapping = _data.input_device_mapping
changed = False
@@ -509,7 +494,7 @@ def _save_device_settings(self, id: int, value: int) -> bool:
return changed
def _save_preset_settings(self, id: int, value: int, text: str) -> bool:
- '''save values from input blocks on preset sub-page; called by self.process_user_input and self.midi_learn'''
+ '''save values from input blocks on preset sub-page; called by self.process_user_input'''
_data = ui.data
changed = False
device_key = self.preset_device_name
diff --git a/src/ui_page_output.py b/src/ui_page_output.py
index 022345e..1f6984d 100644
--- a/src/ui_page_output.py
+++ b/src/ui_page_output.py
@@ -21,8 +21,6 @@
_NR_OUT_PORTS = const(6)
_MAX_PRESETS = const(5)
-_TRIGGER_OUT = const(2)
-
_ON_OFF_OPTIONS = ('off', 'on')
_CHANNEL_OPTIONS = GenOptions(16, 1, ('__',), str)
_NOTE_OPTIONS = GenOptions(128, first_options=('___',), func=mt.number_to_note)
@@ -204,49 +202,40 @@ def midi_learn(self, port: int, channel: int, note: int, program: int, cc: int,
if block == _DEVICE_CHANNEL:
if channel == _NONE or self.device_new_device:
return
- if self._save_device_settings(block, channel + 1):
- self._set_device_options()
+ value = channel + 1
+ else:
+ return
elif sub_page == _SUB_PAGE_TRIGGERS:
if block == _TRIGGER_CHANNEL:
if channel == _NONE or self.trigger_new_trigger:
return
- if self._save_trigger_settings(block, channel + 1):
- self._set_trigger_options()
+ value = channel + 1
elif block == _TRIGGER_NOTE:
if note == _NONE or self.trigger_new_trigger:
return
- if self._save_trigger_settings(block, note):
- self._set_trigger_options()
+ value = note
+ else:
+ return
elif sub_page == _SUB_PAGE_PRESETS:
if block == _PRESET_DEVICE:
if route_number == _NONE:
return
_router = ui.router
- to_device = _router.output_devices_tuple_assigned.index(_router.routing[route_number]['output_device'])
- if to_device == self.preset_device:
- return
- self.preset_device = to_device
- self.preset_new_preset = False
- ui.data.save_data_json_file()
- _router.update(False, False, False, False, False, False)
- self._load_preset_options()
+ value = _router.output_devices_tuple_assigned.index(_router.routing[route_number]['output_device'])
elif block == _PRESET_PRESET:
if route_number == _NONE:
return
_router = ui.router
- to_preset = _router.output_presets_tuples[self.device_device_name].index(_router.routing[route_number]['output_preset'])
- if to_preset == self.preset_preset:
- return
- self.preset_preset = to_preset
- self.preset_new_preset = False
- ui.data.save_data_json_file()
- _router.update(False, False, False, False, False, False)
- self._load_preset_options()
+ value = _router.output_presets_tuples[self.device_device_name].index(_router.routing[route_number]['output_preset'])
elif _PRESET_FIRST_NOTE <= block <= _PRESET_FIRST_NOTE + 2 * _MAX_PRESETS:
if note == _NONE or (block - _PRESET_FIRST_MAP) % 2 == 0:
return
- if self._save_preset_settings(block, note, ''):
- self._set_trigger_options()
+ value = note
+ else:
+ return
+ else:
+ return
+ self.blocks[block].set_selection(value)
def _build_page(self) -> None:
'''build page (without drawing it); called by self.__init__ and self._build_sub_page'''
@@ -351,8 +340,7 @@ def _load_port_options(self) -> None:
self._set_port_options(False)
def _load_preset_options(self, redraw: bool = True) -> None:
- '''load and set values to options and values to input blocks on preset sub-page; called by self.process_user_input, self.midi_learn
- and self._load'''
+ '''load and set values to options and values to input blocks on preset sub-page; called by self.process_user_input and self._load'''
if self.preset_new_preset:
self.preset_preset_name = _ADD_NEW_LABEL
n = 0
@@ -399,8 +387,8 @@ def _set_port_options(self, redraw: bool = True) -> None:
blocks[_PORT_FIRST_DEVICE + port].set_options(devices_tuple, device_option, redraw)
def _set_device_options(self, redraw: bool = True) -> None:
- '''load and set options and values to input blocks on device sub-page; called by self.process_user_input, self.midi_learn, self._load
- and self._callback_confirm'''
+ '''load and set options and values to input blocks on device sub-page; called by self.process_user_input, self._load and
+ self._callback_confirm'''
if self.sub_page != _SUB_PAGE_DEVICES:
return
devices_tuple = ChainMapTuple(ui.router.output_devices_tuple_all, (_ADD_NEW_LABEL,))
@@ -425,8 +413,7 @@ def _set_device_options(self, redraw: bool = True) -> None:
blocks[_DEVICE_RUNNING_STATUS].set_options(selection=running_status, redraw=redraw)
def _set_trigger_options(self, redraw: bool = True) -> None:
- '''load and set options and values to input blocks on triggers sub-page; called by self.process_user_input, self.midi_learn and
- self._load'''
+ '''load and set options and values to input blocks on triggers sub-page; called by self.process_user_input and self._load'''
if self.sub_page != _SUB_PAGE_TRIGGERS:
return
_router = ui.router
@@ -534,7 +521,7 @@ def _save_port_settings(self) -> None:
ui.router.update(False, False, False, False, False, False)
def _save_device_settings(self, id: int, value: int) -> bool:
- '''save values from input blocks on device sub-page; called by self.process_user_input and self.midi_learn'''
+ '''save values from input blocks on device sub-page; called by self.process_user_input'''
_data = ui.data
changed = False
device_key = self.device_device_name
@@ -561,7 +548,7 @@ def _save_device_settings(self, id: int, value: int) -> bool:
return changed
def _save_trigger_settings(self, id: int, value: int) -> bool:
- '''save values from input blocks on triggers sub-page; called by self.process_user_input and self.midi_learn'''
+ '''save values from input blocks on triggers sub-page; called by self.process_user_input'''
_data = ui.data
changed = False
device = _data.output_devices[self.trigger_device_name]
diff --git a/src/ui_page_program.py b/src/ui_page_program.py
index 2574446..6b2d979 100644
--- a/src/ui_page_program.py
+++ b/src/ui_page_program.py
@@ -235,51 +235,31 @@ def midi_learn(self, port: int, channel: int, note: int, program: int, cc: int,
if block == _NAME:
if program == _NONE or _router.active_program_number == program:
return
- _router.update(False, False, False, False, False, False, program)
+ value = program
elif block == _INPUT_DEVICE:
if route_number == _NONE:
return
- from_device = _router.input_devices_tuple_assigned.index(_router.routing[route_number]['input_device'])
- if from_device == _router.input_device_value:
- return
- ui.ui.set_trigger(from_device)
+ value = _router.input_devices_tuple_assigned.index(_router.routing[route_number]['input_device'])
elif block == _INPUT_PRESET:
if route_number == _NONE:
return
- from_preset = _router.input_presets_tuple.index(_router.routing[route_number]['input_preset'])
- if from_preset == _router.input_preset_value:
- return
- ui.ui.set_trigger(preset=from_preset)
+ value = _router.input_presets_tuple.index(_router.routing[route_number]['input_preset'])
elif sub_page == _SUB_PAGE_NOTE:
if note == _NONE:
return
- row = block - _FIRST_NOTE
- settings = self.mapping_settings
- if row >= len(settings) or settings[row][_MAPPING_PRESET_COL] == '':
- return
- settings[row][_MAPPING_NOTE_COL] = note
- if self._save_mapping_settings():
- self._set_note_options()
+ value = note
elif sub_page == _SUB_PAGE_PROGRAM:
- for i in range(_NR_OUT_PORTS):
- if block == _FIRST_PROGRAM + i:
- self.program_settings[port][1] = program
- if self._save_program_settings():
- self._set_program_options()
- return
+ if program == _NONE:
+ return
+ value = program
else: # sub_page == _SUB_PAGE_BANKS
- if cc == _CC_BANK_MSB:
- col = 0
- elif cc == _CC_BANK_LSB:
- col = 1
- else:
+ if cc_value == _NONE:
return
- for i in range(_NR_OUT_PORTS):
- if col == 0 and block == _FIRST_BANK_MSB + 2 * i or col == 1 and block == _FIRST_BANK_LSB + 2 * i:
- self.bank_settings[port][1][col] = cc_value
- if self._save_bank_settings():
- self._set_bank_options()
- return
+ col = (block - _FIRST_BANK_MSB) % 2
+ if not (col == 0 and cc == _CC_BANK_MSB or col == 1 and cc == _CC_BANK_LSB):
+ return
+ value = cc_value
+ self.blocks[block].set_selection(value)
def _build_page(self) -> None:
'''build page (without drawing it); called by self.__init__ and self._build_sub_page'''
@@ -354,13 +334,13 @@ def _load(self, redraw: bool = True) -> None:
and PageProgram.set_trigger'''
if ui.ui.active_pop_up is not None:
redraw = False
- self._set_mapping_options(False)
- self._set_program_options(False)
- self._set_bank_options(False)
+ self._set_mapping_options()
+ self._set_program_options()
+ self._set_bank_options()
if redraw:
self._draw()
- def _set_mapping_options(self, redraw: bool = True) -> None:
+ def _set_mapping_options(self) -> None:
'''load and set options and values to input blocks on mapping sub-page; called by self._load'''
sub_page = self.sub_page
if not (sub_page == _SUB_PAGE_MAPPING or sub_page == _SUB_PAGE_NOTE or sub_page == _SUB_PAGE_NOTE_OFF):
@@ -372,9 +352,9 @@ def _set_mapping_options(self, redraw: bool = True) -> None:
input_presets = _router.input_presets_tuple
input_device_value = _router.input_device_value
input_preset_value = _router.input_preset_value
- blocks[_NAME].set_options(ChainMapTuple(ui.data.programs_tuple, (_ADD_NEW_LABEL,)), _router.active_program_number, redraw)
- blocks[_INPUT_DEVICE].set_options(input_devices, input_device_value, redraw)
- blocks[_INPUT_PRESET].set_options(input_presets, input_preset_value, redraw)
+ blocks[_NAME].set_options(ChainMapTuple(ui.data.programs_tuple, (_ADD_NEW_LABEL,)), _router.active_program_number, False)
+ blocks[_INPUT_DEVICE].set_options(input_devices, input_device_value, False)
+ blocks[_INPUT_PRESET].set_options(input_presets, input_preset_value, False)
routing = _router.routing
if len(routing) > 0:
input_device = input_devices[input_device_value]
@@ -384,11 +364,11 @@ def _set_mapping_options(self, redraw: bool = True) -> None:
if route['input_device'] == input_device and route['input_preset'] == input_preset:
settings.append([route['output_device'], route['output_preset'], route['note'], route['note_off']])
if sub_page == _SUB_PAGE_MAPPING:
- self._set_trigger_options(redraw)
+ self._set_trigger_options(False)
elif sub_page == _SUB_PAGE_NOTE:
- self._set_note_options(redraw)
+ self._set_note_options(False)
else: # sub_page == _SUB_PAGE_NOTE_OFF
- self._set_note_off_options(redraw)
+ self._set_note_off_options(False)
def _set_trigger_options(self, redraw: bool = True) -> None:
'''load and set options and values to trigger selection blocks on mapping sub-page; called by self.process_user_input and
@@ -415,8 +395,7 @@ def _set_trigger_options(self, redraw: bool = True) -> None:
blocks[_FIRST_OUTPUT_PRESET + 2 * i].set_options(_EMPTY_OPTIONS_LONG, 0, redraw)
def _set_note_options(self, redraw: bool = True) -> None:
- '''load and set options and values to note blocks on mapping note sub-page; called by self.process_user_input, self.midi_learn
- and self._load'''
+ '''load and set options and values to note blocks on mapping note sub-page; called by self.process_user_input and self._load'''
settings = self.mapping_settings
blocks = self.blocks
for i, (output_device, output_preset, note, _) in enumerate(settings):
@@ -451,8 +430,8 @@ def _set_note_off_options(self, redraw: bool = True) -> None:
block.set_label('n/a: n/a - note off', False)
block.set_options(_EMPTY_OPTIONS_LONG, 0, redraw)
- def _set_program_options(self, redraw: bool = True) -> None:
- '''load and set options and values to input blocks on program change sub-page; called by self.midi_learn and self._load'''
+ def _set_program_options(self) -> None:
+ '''load and set options and values to input blocks on program change sub-page; called by self._load'''
if self.sub_page != _SUB_PAGE_PROGRAM:
return
_router = ui.router
@@ -479,10 +458,10 @@ def _set_program_options(self, redraw: bool = True) -> None:
options = _PROGRAM_CHANGE_OPTIONS
block = blocks[_FIRST_PROGRAM + port]
block.set_label(label, False)
- block.set_options(options, program + 1, redraw) # _NONE becomes 0
+ block.set_options(options, program + 1, False) # _NONE becomes 0
- def _set_bank_options(self, redraw: bool = True) -> None:
- '''load and set options and values to input blocks on bank select sub-page; called by self.midi_learn and self._load'''
+ def _set_bank_options(self) -> None:
+ '''load and set options and values to input blocks on bank select sub-page; called by self._load'''
if self.sub_page != _SUB_PAGE_BANK:
return
_router = ui.router
@@ -510,8 +489,8 @@ def _set_bank_options(self, redraw: bool = True) -> None:
options = _BANK_SELECT_OPTIONS
block = blocks[_FIRST_BANK_MSB + 2 * port]
block.set_label(label, False)
- block.set_options(options, bank[0] + 1, redraw) # _NONE becomes 0
- blocks[_FIRST_BANK_LSB + 2 * port].set_options(selection=bank[1] + 1, redraw=redraw) # _NONE becomes 0
+ block.set_options(options, bank[0] + 1, False) # _NONE becomes 0
+ blocks[_FIRST_BANK_LSB + 2 * port].set_options(selection=bank[1] + 1, redraw=False) # _NONE becomes 0
def _save_mapping_settings(self) -> bool:
'''save values from input blocks on mapping sub-pages; called by self.process_user_input'''
@@ -576,7 +555,7 @@ def _save_mapping_settings(self) -> bool:
return changed
def _save_program_settings(self) -> bool:
- '''save values from input blocks on program change sub-page; called by self.process_user_input and self.midi_learn'''
+ '''save values from input blocks on program change sub-page; called by self.process_user_input'''
_router = ui.router
changed = False
program_change = _router.program['program_change']
@@ -594,7 +573,7 @@ def _save_program_settings(self) -> bool:
return changed
def _save_bank_settings(self) -> bool:
- '''save values from input blocks on bank select sub-page; called by self.process_user_input and self.midi_learn'''
+ '''save values from input blocks on bank select sub-page; called by self.process_user_input'''
_router = ui.router
changed = False
bank_select = _router.program['bank_select']
@@ -678,7 +657,7 @@ def _callback_menu(self, caller_id: int, selection: int) -> None:
options = tuple(str(i) for i in range(len(ui.data.routing_programs)))
self._draw()
ui.ui.pop_ups[_POP_UP_SELECT].open(self, _SELECT_POSITION, 'move to:', options,
- _router.active_program_number, callback_func=self._callback_select)
+ _router.active_program_number, self._callback_select)
def _callback_select(self, caller_id: int, selection: int) -> None:
'''callback for select pop-up; called (passed on) by self._callback_menu'''
diff --git a/src/ui_pages.py b/src/ui_pages.py
index d442507..ac69cb4 100644
--- a/src/ui_pages.py
+++ b/src/ui_pages.py
@@ -45,8 +45,8 @@ def set_visibility(self, visible: bool) -> None:
if visible:
self.visible = visible
blocks = self.blocks
- if ui.ui.page_pressed:
- self._sub_page_selector()
+ if ui.ui.page_select_mode:
+ self._sub_page_selector(False)
elif len(blocks) > 0:
blocks[0].update(True, False)
self._load()
@@ -65,11 +65,11 @@ def restore(self):
blocks[self.selected_block].update(True, False)
self._draw()
- def set_page_encoders(self, page_pressed: bool) -> None:
+ def set_page_encoders(self, page_select_mode: bool) -> None:
'''switches value encoder input to or from page selection and changes colour of title bar (when page button is pressed); called by
ui.process_user_input'''
blocks = self.blocks
- if page_pressed:
+ if page_select_mode:
if len(blocks) > 0:
blocks[self.selected_block].update(False)
self._sub_page_selector()
@@ -106,11 +106,6 @@ def button_select(self) -> None:
'''process select button press at page level; called by ui.process_user_input'''
self.blocks[self.selected_block].button_select()
-######
- # def button_trigger(self) -> bool:
- # '''process trigger button press at page level; called by ui.process_user_input'''
- # return False
-
def process_user_input(self, id: int, value: int = _NONE, text: str = '',
button_encoder_0: bool = False, button_encoder_1: bool = False) -> None:
'''process user input at page level (ui.ui.set_user_input_dict > global user_input_dict > ui.process_user_input >
@@ -137,22 +132,20 @@ def _draw(self) -> None:
_ui = ui.ui
if not self.visible or _ui.active_pop_up is not None:
return
- page_pressed = _ui.page_pressed
+ page_select_mode = _ui.page_select_mode
blocks = self.blocks
selected_block = self.selected_block
if len(blocks) > 0:
- blocks[selected_block].update(not page_pressed, False)
+ blocks[selected_block].update(not page_select_mode, False)
for block in self.empty_blocks:
block.draw()
- title_bar = self.title_bar
- title_bar.draw()
for block in blocks:
- block.draw(_ui.page_pressed)
- title_bar.update(page_pressed)
+ block.draw(page_select_mode)
+ self.title_bar.update(page_select_mode)
_text_edit = _ui.pop_ups[_POP_UP_TEXT_EDIT]
if _text_edit.visible:
_text_edit.draw()
- elif not page_pressed:
+ elif not page_select_mode:
self._initiate_navigate_encoder(selected_block)
def _load(self, redraw: bool = True) -> None:
@@ -160,12 +153,12 @@ def _load(self, redraw: bool = True) -> None:
PageProgram.set_trigger and PageMonitor.add_to_monitor'''
pass
- def _sub_page_selector(self) -> None:
+ def _sub_page_selector(self, redraw: bool = True) -> None:
'''change title bar colour and set encoder range to select sub-page (when page button is pressed); called by self.set_visibility and
self.set_page_encoders'''
if len(self.sub_pages_title_bars) <= 1:
return
- self.title_bar.update(True)
+ self.title_bar.update(True, redraw)
ui.encoder_0.set(self.sub_page, 0, len(self.sub_pages_blocks) - 1)
def _set_sub_page(self, sub_page: int) -> None:
From cad12472c413e65311da494a5c473f62e3e844cd Mon Sep 17 00:00:00 2001
From: HLammers <62934625+HLammers@users.noreply.github.com>
Date: Thu, 14 Nov 2024 17:43:59 +0100
Subject: [PATCH 2/4] Update README.md
---
README.md | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index e7dbb5d..0d9e74f 100644
--- a/README.md
+++ b/README.md
@@ -776,8 +776,8 @@ To keep latency to a minimum the second core is dedicated to MIDI handling, whil
- [ ] There is currently no easy way to download and upload user settings (including user-defined programs) when updating the firmware for users who are not accustomed to using Python
- [ ] Program change and bank select assume the output device interprets value 0 as 1, but that’s not always the case (resulting in the value on Cybo-Drummer’s screen to be 1 off compared to what the output device shows – to solve this a device-level setting needs to be added to set the device’s behaviour
## Ideas for Features and Improvements to be Added
-- [ ] Change behaviour of the PAGE button from keeping pressed to toggle on/off, so it is easier to use with one hand – the keep pressed and turn knop event could be reused for program change, to make that available from everywhere
-- [ ] Change the value editing and storing behaviour from processing every change immediately to having to confirm a change with a click – that would make the user interface much more responsive, especially if many rotary encoder steps are involved (the way I imagine it: the moment you turn the VALUE knob the text colour changes to indicate the value has not changed yet; once you press the VALUE knob or you navigate away to another input field, the value gets saved and takes effect and the text colour changes back)
+- [x] Change behaviour of the PAGE button from keeping pressed to toggle on/off, so it is easier to use with one hand – the keep pressed and turn knop event could be reused for program change, to make that available from everywhere
+- [x] Change the value editing and storing behaviour from processing every change immediately to having to confirm a change with a click – that would make the user interface much more responsive, especially if many rotary encoder steps are involved (the way I imagine it: the moment you turn the VALUE knob the text colour changes to indicate the value has not changed yet; once you press the VALUE knob or you navigate away to another input field, the value gets saved and takes effect and the text colour changes back)
- [ ] Improved hardware, including proper front panel and 3d printable case
- [ ] Add USB MIDI input/output (USB MIDI support only became available in MicroPython 1.23, which was released at end of May 2024, which is after I developed the MIDI handling side of Cybo-Drummer)
- [ ] Migrate to Raspberry Pi Pico 2, which has about twice as much memory, allowing larger display buffer and thus snappier GUI performance (also worth trying: using one of the additional PIO processors for SPI, which some people suggest to be faster due to hardware SPI adding a short delay between data bytes, which for most devices isn’t necessary)
@@ -788,6 +788,7 @@ To keep latency to a minimum the second core is dedicated to MIDI handling, whil
- [ ] Add velocity mapping (triggering something different when hitting softly or hitting hard)
- [ ] Add option to send MIDI start, stop or continue messages on trigger
- [ ] Rethinking how to deal with choke events, which for some drum modules lead to MIDI note events (2Box, Alesis?) and for others to poly aftertouch/pressure MIDI CC messages (Roland, Yamaha)
+- [ ] Add choke groups which could combine different devices
- [ ] Thinking through how the specific possibilities and needs when using Cybo-Drummer with a multi-pad controller (I recently bought and old Roland SPD-11 for that purpose)
- [ ] Building an editor for program and device/trigger/preset definitions and settings files on a pc
## Licencing
From d365a55e6f3228f09840098223673a626201d190 Mon Sep 17 00:00:00 2001
From: HLammers <62934625+HLammers@users.noreply.github.com>
Date: Sat, 16 Nov 2024 15:22:11 +0100
Subject: [PATCH 3/4] Update ui.py
Disable trigger button when in page select mode
---
src/ui.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/ui.py b/src/ui.py
index a72e73d..ab87518 100644
--- a/src/ui.py
+++ b/src/ui.py
@@ -282,9 +282,10 @@ def process_user_input(self) -> None:
continue
# if button_number == _BUTTON_TRIGGER_CONFIRM
if self.active_pop_up is None:
- if value == _BUTTON_EVENT_PRESS: # short-press of trigger button
+ page_select_mode = self.page_select_mode
+ if value == _BUTTON_EVENT_PRESS and not page_select_mode: # short-press of trigger button and not in page select mode
_router.trigger() # type: ignore
- elif value == _BUTTON_EVENT_LONG_PRESS: # long-press of trigger button
+ elif value == _BUTTON_EVENT_LONG_PRESS and not page_select_mode: # long-press of trigger button and not in page select mode
options = []
input_devices_tuple_assigned = _router.input_devices_tuple_assigned # type: ignore
input_presets_tuples = _router.input_presets_tuples # type: ignore
From 6b833c97548fd6ba61ac099856b5833417667074 Mon Sep 17 00:00:00 2001
From: HLammers <62934625+HLammers@users.noreply.github.com>
Date: Sat, 16 Nov 2024 16:04:37 +0100
Subject: [PATCH 4/4] Update README.md
---
README.md | 47 +++++++++++++++++++++++++++--------------------
1 file changed, 27 insertions(+), 20 deletions(-)
diff --git a/README.md b/README.md
index 0d9e74f..bb5b956 100644
--- a/README.md
+++ b/README.md
@@ -14,13 +14,13 @@
>
> Of course I’m open for collaboration. Just let me know how you think you can contribute!
>
-> Please use the [issues tab](issues/) to report bug and other issues, or the [discussion tab](discussions/) to discuss anything else.
+> Please use the [issues tab]([hlammers/cybo-drummer/issues/](https://github.com/HLammers/cybo-drummer/issues) to report bug and other issues, or the [discussion tab](https://github.com/HLammers/cybo-drummer/discussions) to discuss anything else.
## Introduction
I own an electronic drum kit and a bunch of drum computers and my dream was to use the former to play the latter, so I went searching for a way to do just that – allowing me to easily switch between different configurations combining the sounds of one or more drum computers. I looked for hardware solutions, but couldn’t find any. I looked for software solutions, but I could only find MIDI mappers or other complex solutions that would never give me the easy to use experience I had it mind. It turns out that (as usual) I go against the current fashion of trying to make an electronic drum kit sound (and look) as acoustic as possible. So I decided to develop my own solution – and publish it as open source DIY project, hoping it finds like-minded drummers!
## Overview
Cybo-Drummer is a MIDI router/mapper with 6 input ports and 6 output ports, specially designed for mapping drum triggers (electronic drum kits’ brains) to drum computers. Since there is no standard for the MIDI messages sent by drum kits, nor the messages received by drum computers, Cybo-Drummer offers a flexible way of mapping the one to the other.
-The idea for the hardware was inspired by the work of [diyelectromusic (Kevin)](https://diyelectromusic.com/), in particular his [Raspberry Pi Pico Multi MIDI Router](https://diyelectromusic.com/2022/09/19/raspberry-pi-pico-multi-midi-router-part-5/). The first prototype is an additional PCB on top of the Multi Midi Router.
+The idea for the hardware was inspired by the work of [diyelectromusic (Kevin)](https://diyelectromusic.com), in particular his [Raspberry Pi Pico Multi MIDI Router](https://diyelectromusic.com/2022/09/19/raspberry-pi-pico-multi-midi-router-part-5). The first prototype is an additional PCB on top of the Multi Midi Router.
### Features
#### Hardware
* 6 times 5-pin DIN MIDI input port: connect up to 6 drum kits, drum pads, keyboards, etc.
@@ -107,7 +107,7 @@ graph LR
* MIDI monitor with three views: Show mapping flow (input preset > output preset), MIDI data coming in and MIDI data sent out
[^2]: Except SysEx
## Building Instructions
-Cybo-Drummer is a DIY project which is currently in a prototype stage. The hardware design is a pragmatic solution, building upon an existing design by [diyelectromusic (Kevin)](https://diyelectromusic.com/), who designed the bottom PCB with the MIDI ports. As a next step I want to design new hardware (including 3d printable enclosure) and now I have a working prototype I’m considering what to improve in the next iteration. These are a few of my considerations:
+Cybo-Drummer is a DIY project which is currently in a prototype stage. The hardware design is a pragmatic solution, building upon an existing design by [diyelectromusic (Kevin)](https://diyelectromusic.com), who designed the bottom PCB with the MIDI ports. As a next step I want to design new hardware (including 3d printable enclosure) and now I have a working prototype I’m considering what to improve in the next iteration. These are a few of my considerations:
* Where to place the MIDI ports? The current prototype has input ports at the front and output ports at the back. The disadvantage is that the MIDI cables take a lot of space at two sides of the device. Perhaps MIDI ports at the top of the device is a better solution? In two rows above the display, buttons and knobs? Alternatively I could leave the ports at the top and the bottom (angled PCB-mounted 5-pin DIN ports are much easier available than straight ones) and design a drum rack mounted casing (or a casing with 70mm × 10mm spaced holes to connect a standard size mounting plate for drum modules and multi-pads).
* Should I add more buttons? All GPIO pins of the Raspberry Pi Pico are in use, so that would require adding IO ports, for example using one ore more PCF8574 I2C IO expander ICs. Having more IO ports would allow improvements like:
* Splitting the TRIGGER button into a TRIGGER and a TRIGGER SELECT button
@@ -117,11 +117,11 @@ Cybo-Drummer is a DIY project which is currently in a prototype stage. The hardw
***I have no intention to sell Cybo-Drummer as a fully assembled product, nor as DIY package*** – I might change my mind in the future, but for now that doesn’t seem like a realistic idea, next to a full-time job and a family (especially because I expect it to be a very niche product – unless the a change in trend occurs and many more drummers become interested again in electronic drums as an instrument of its own – like in the 1980s – instead of a means to simulate acoustic drums). Nevertheless: if you think you can convince me to change my mind, feel free to try!
### Hardware
-Building the Cybo-Drummer hardware only requires basic soldering skills (only though-hole components). The PCBs can be ordered cheaply from Chinese PCB services like [JLCPCB](https://jlcpcb.com/) (no affiliate).
+Building the Cybo-Drummer hardware only requires basic soldering skills (only though-hole components). The PCBs can be ordered cheaply from Chinese PCB services like [JLCPCB](https://jlcpcb.com) (no affiliate).
> [!NOTE]
> The initial hardware is a crude, though functional prototype, without casing. I’m planning to design improved hardware including front panel and 3d printable case.
#### BOM (Bill of Materials)
-##### First Board: [Raspberry Pi Pico Multi MIDI Router](https://diyelectromusic.com/2022/09/19/raspberry-pi-pico-multi-midi-router-part-5/)
+##### First Board: [Raspberry Pi Pico Multi MIDI Router](https://diyelectromusic.com/2022/09/19/raspberry-pi-pico-multi-midi-router-part-5)
* The [Raspberry Pi Pico Multi MIDI Router PCB](https://github.com/diyelectromusic/sdemp_pcbs/tree/main/PicoMIDIRouter)
* 6× H11L1 optocouplers
* 6× 470Ω resistors
@@ -146,10 +146,10 @@ Building the Cybo-Drummer hardware only requires basic soldering skills (only th
* 7× 11mm spacers plus bolts/nuts
[^3]: Use the Raspberry Pi Pico and solder headers onto it yourself or the Pico H, which comes with pre-soldered headers; the pre-compiled Cybo-Drummer firmware does not support the Pico W, Pico WH nor Pico 2 (I will add support for the Pico 2 at a later stage)
> [!NOTE]
-> The schematics for the second board can be found in the [schematics folder](scematics/).
+> The schematics for the second board can be found in the [schematics folder](schematics/).
#### Build Instructions
##### First Board: Raspberry Pi Pico Multi MIDI Router
-* For the 1st board, follow the excellent instructions by diyelectromusic (Kevin): [Raspberry Pi Pico Multi MIDI Router](https://diyelectromusic.com/2022/09/19/raspberry-pi-pico-multi-midi-router-part-5/)
+* For the 1st board, follow the excellent instructions by diyelectromusic (Kevin): [Raspberry Pi Pico Multi MIDI Router](https://diyelectromusic.com/2022/09/19/raspberry-pi-pico-multi-midi-router-part-5)
@@ -164,7 +164,7 @@ Building the Cybo-Drummer hardware only requires basic soldering skills (only th
* Plug the second board to Pico header sockets of the first board and fix them together using four spacers
* Plug the Raspberry Pi Pico into the 2x 20-pin header sockets on the second board
### Software
-The easiest way to install the software is by downloading the latest firmware as a single file and uploading it the Cybo-Drummer, but it can be run directly from source as well.
+The easiest way to install the software is by downloading the [latest firmware]([releases/](https://github.com/HLammers/cybo-drummer/releases) as a single file and uploading it the Cybo-Drummer, but it can be run directly from source as well.
> [!NOTE]
> Cybo-Drummer offers a couple of key combinations specifically for debugging and firmware upload purposes:\
> **RESET**: resets Cybo-Drummer\
@@ -172,7 +172,7 @@ The easiest way to install the software is by downloading the latest firmware as
> **RESET + TRIGGER + PAGE**: (alternative to BOOTSEL + RESET) start bootloader (show Cybo-Drummer as drive called RPI-RP2 on your PC for uploading firmware) – press the RESET while keeping the TRIGGER and PAGE buttons pressed until the RPI-RP2 drive appears on your PC (after the Raspberry Pi Pico’s LED flashed)
#### Uploading Firmware
* Back up your user settings (see warning box below)
-* Download the [latest firmware release](releases/) (.uf2 file)
+* Download the [latest firmware release]([releases/](https://github.com/HLammers/cybo-drummer/releases) (.uf2 file)
* Connect Cybo-Drummer with a USB cable to your PC of choice (Windows/Linux/MacOS)
* Do one of the following to make the Cybo-Drummer appear as a drive called RPI-RP2 on your PC:
* Press the RESET button and wait for the LED on the Raspberry Pi Pico to turn on; in the 1 second the LED is on, press and keep the TRIGGER and PAGE buttons pressed until the RPI-RP2 drive appears on your PC
@@ -182,7 +182,7 @@ The easiest way to install the software is by downloading the latest firmware as
> [!WARNING]
> ***Uploading new firmware might delete your user settings (including user-defined programs) and reinstate default values!***
> User settings are stored internally in a file folder called data_files. Currently the easiest way To back-up (download) or restore (upload) the file is by following these steps (assuming you’re using a Windows PC):
-> * If you haven’t before: Install [Python](https://www.python.org/downloads/) – follow the instructions provided [here](https://docs.python.org/3/using/windows.html#windows-full) and **make sure to select ‘Add Python 3.x to PATH’**
+> * If you haven’t before: Install [Python](https://www.python.org/downloads) – follow the instructions provided [here](https://docs.python.org/3/using/windows.html#windows-full) and **make sure to select ‘Add Python 3.x to PATH’**
> * If you haven’t before: Download the source code of [MicroPython release v1.24.0](https://github.com/micropython/micropython/releases). (typically the zip version) and unzip it somewhere on your PC
> * In File Explorer go to the micropython-1.24.0\tools\mpremote folder (in the location where you unzipped MicroPython)
> * Right click somewhere in the folder (not on a file) and from the context menu select ‘Open in Terminal’
@@ -200,7 +200,7 @@ The easiest way to install the software is by downloading the latest firmware as
> * Press RESET button on Cybo-Drummer
> * Press ENTER on your PC to start downloading (backing up) or uploading (restoring)
#### Running From Source
-It is possible to run Cybo-Drummer from source by uploading the content from the [src folder](src/) to the Raspberry Pi Pico running [stock MicroPython](https://micropython.org/download/RPI_PICO/).
+It is possible to run Cybo-Drummer from source by uploading the content from the [src folder](src/) to the Raspberry Pi Pico running [stock MicroPython](https://micropython.org/download/RPI_PICO).
Keep in mind that when running from source (instead of frozen into firmware), Cybo-Drummer takes more time to start up and screen refreshing. To resolve this while playing around with the code it is also possible to freeze only part of the source code, following the instructions under [building firmware from source](#building-firmware-from-source).
#### Building Firmware From Source
@@ -253,17 +253,20 @@ The user interface displayed on the 2.2 inch TFT screen is organized as follows:
* **Blocks:** all sub-pages except those on the monitor page are structured in locks which can be selected to enter input; the active block is highlighted using a dark and light sea green colour
To control Cybo-Drummer’s user interface it has two buttons and two rotary encoders, which usually behave as follows:
-* **PAGE/YES:** Keep pressed to select page or sub-page
+* **PAGE/YES:**
+ * *Short press:* Toggle page select mode on or off
+ * *Long press:* Show pop-up to select program
* **TRIGGER/NO:**
* *Short press:* Trigger last selected output trigger preset (for testing purposes)
- * *Keep pressed:* Show pop-up to select trigger preset
+ * *Long press:* Show pop-up to select trigger preset
* **NAV/↕ | DEL:**
* *Turn:* Navigate / select active block
* *Press (when a program, device, preset or trigger name block is selected):* Delete program, device, preset or trigger (a confirmation pop-up will show)
* **VAL/↔ | SEL/OPT:**
* *Turn:* Change value of active block or pop-up
* *Press:*
- * *When a program, device, preset or trigger name block is selected:* Rename or show options menu
+ * *When a value was changed but not confirmed yet:* Confirm value and process the change (not yet confirmed value is indicated by a purple square around the text)
+ * *When a program, device, preset or trigger name block is selected:* Rename or show options menu (press twice if the value was changed, but not yet confirmed: the first time will confirm it, the second time triggers rename or options menu)
* *When a button block is selected:* Press/execute button
### Pages and sub-pages
Cybo-Drummer’s user interface is organized in five pages:
@@ -273,9 +276,9 @@ Cybo-Drummer’s user interface is organized in five pages:
* **MON (Monitor):** Show router and MIDI monitors
* **SET (Settings):** Adjust global settings
-To change the pages and sub-pages, keep the PAGE button pressed and turn the VAL/↔ knob (right knob) to change the page and the NAV/↕ know (left knob) to change the sub-page. While the PAGE button is pressed the page tabs and title bar are highlighted in dark and light sea green.
+To change the pages and sub-pages, short-press the PAGE button to enter page select mode. Now turn the VAL/↔ knob (right knob) to change the page and the NAV/↕ know (left knob) to change the sub-page. Short-press the PAGE button again to leave page select mode. While in page select mode the page tabs and title bar are highlighted in dark and light sea green.
> [!CAUTION]
-> The PRG (program) page does not save automatically, all other pages do. If there are unsaved changes to a program an asterisk will show behind the active program number. To save changes, go to the first program page (program: mapping), select the [program block](#program), press the SEL/OPT button and choose ‘save’. This will show a pop-up to ask for confirmation and another pop-up to ask if you want to replace the active program – select ‘yes’ to save changes to the active program or select ‘no’ to save the changes to a new program directly after the active program.
+> The PRG (program) page does not save automatically, all other pages do (after confirming a changed value). If there are unsaved changes to a program an asterisk will show behind the active program number. To save changes, go to the first program page (program: mapping), select the [program block](#program), short-press the SEL/OPT button and choose ‘save’. This will show a pop-up to ask for confirmation and another pop-up to ask if you want to replace the active program – select ‘yes’ to save changes to the active program or select ‘no’ to save the changes to a new program directly after the active program.
> [!TIP]
> To duplicate the active program, make a change and save without replacing, this creates a new program directly after the active program.
@@ -283,7 +286,9 @@ To change the pages and sub-pages, keep the PAGE button pressed and turn the VAL
### Trigger presets
-Short pressing the TRIGGER button triggers the last selected output trigger preset (for testing purposes). Long press the TRIGGER button to show a trigger preset selection pop-up. Keep the TRIGGER button pressed and turn the VAL/↔ knob to change the selected output trigger preset.
+Short-press the TRIGGER button to trigger the last selected output trigger preset (for testing purposes). Long-press the TRIGGER button to show a trigger preset selection pop-up.
+
+While the trigger selection pop-up is shown: Turn the VAL/↔ knob to selected an output trigger preset and press the YES button (left button) to confirm and close the pop-up, or the NO button (right button) to close the pop-up without changing the selected trigger.
@@ -293,6 +298,8 @@ Cybo-Drummer doesn’t have an undo feature, so to avoid accidentally losing dat
### Description of All Pages and Sub-Pages
#### PRG (Program)
The program page is the first page that shows when powering up Cybo-Drummer. Use this page to select or edit the active program.
+> [!TIP]
+> Programs can also be changed from any page/sub-page by long pressing the PAGE button. This will show the program selection pop-up. While the program selection pop-up is shown: Turn the VAL/↔ knob to selected a program and press the YES button (left button) to confirm and close the pop-up, or the NO button (right button) to close the pop-up without changing the active program.
@@ -698,7 +705,7 @@ Since Cybo-Drummer doesn’t make any sound on its own, but merely routes signal
> [!TIP]
> To upload the example presets to Cybo-Drummer, follow the same instructions as given for restoring a back-up when [uploading firmware](#uploading-firmware):
-> * If you haven’t before: Install [Python](https://www.python.org/downloads/) – follow the instructions provided [here](https://docs.python.org/3/using/windows.html#windows-full) and **make sure to select ‘Add Python 3.x to PATH’**
+> * If you haven’t before: Install [Python](https://www.python.org/downloads) – follow the instructions provided [here](https://docs.python.org/3/using/windows.html#windows-full) and **make sure to select ‘Add Python 3.x to PATH’**
> * If you haven’t before: Download the source code of [MicroPython release v1.24.0](https://github.com/micropython/micropython/releases). (typically the zip version) and unzip it somewhere on your PC
> * In File Explorer go to the micropython-1.24.0\tools\mpremote folder (in the location where you unzipped MicroPython)
> * Right click somewhere in the folder (not on a file) and from the context menu select ‘Open in Terminal’
@@ -776,8 +783,8 @@ To keep latency to a minimum the second core is dedicated to MIDI handling, whil
- [ ] There is currently no easy way to download and upload user settings (including user-defined programs) when updating the firmware for users who are not accustomed to using Python
- [ ] Program change and bank select assume the output device interprets value 0 as 1, but that’s not always the case (resulting in the value on Cybo-Drummer’s screen to be 1 off compared to what the output device shows – to solve this a device-level setting needs to be added to set the device’s behaviour
## Ideas for Features and Improvements to be Added
-- [x] Change behaviour of the PAGE button from keeping pressed to toggle on/off, so it is easier to use with one hand – the keep pressed and turn knop event could be reused for program change, to make that available from everywhere
-- [x] Change the value editing and storing behaviour from processing every change immediately to having to confirm a change with a click – that would make the user interface much more responsive, especially if many rotary encoder steps are involved (the way I imagine it: the moment you turn the VALUE knob the text colour changes to indicate the value has not changed yet; once you press the VALUE knob or you navigate away to another input field, the value gets saved and takes effect and the text colour changes back)
+- [x] ~~Change behaviour of the PAGE button from keeping pressed to toggle on/off, so it is easier to use with one hand – the keep pressed and turn knop event could be reused for program change, to make that available from everywhere~~
+- [x] ~~Change the value editing and storing behaviour from processing every change immediately to having to confirm a change with a click – that would make the user interface much more responsive, especially if many rotary encoder steps are involved (the way I imagine it: the moment you turn the VALUE knob the text colour changes to indicate the value has not changed yet; once you press the VALUE knob or you navigate away to another input field, the value gets saved and takes effect and the text colour changes back)~~
- [ ] Improved hardware, including proper front panel and 3d printable case
- [ ] Add USB MIDI input/output (USB MIDI support only became available in MicroPython 1.23, which was released at end of May 2024, which is after I developed the MIDI handling side of Cybo-Drummer)
- [ ] Migrate to Raspberry Pi Pico 2, which has about twice as much memory, allowing larger display buffer and thus snappier GUI performance (also worth trying: using one of the additional PIO processors for SPI, which some people suggest to be faster due to hardware SPI adding a short delay between data bytes, which for most devices isn’t necessary)