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)