// // uibuttons.cpp // // MiniDexed - Dexed FM synthesizer for bare metal Raspberry Pi // Copyright (C) 2022 The MiniDexed Team // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . // #include "uibuttons.h" #include #include #include #include LOGMODULE ("uibuttons"); CUIButton::CUIButton (void) : m_pinNumber (0), m_pin (0), m_midipin (0), m_lastValue (1), m_timer (0), m_debounceTimer (0), m_numClicks (0), m_clickEvent(BtnEventNone), m_doubleClickEvent(BtnEventNone), m_longPressEvent(BtnEventNone), m_doubleClickTimeout(0), m_longPressTimeout(0) { } CUIButton::~CUIButton (void) { if (m_pin) { delete m_pin; } if (m_midipin) { delete m_midipin; } } void CUIButton::reset (void) { m_timer = m_longPressTimeout; m_numClicks = 0; } boolean CUIButton::Initialize (unsigned pinNumber, unsigned doubleClickTimeout, unsigned longPressTimeout) { assert (!m_pin); assert(longPressTimeout >= doubleClickTimeout); m_pinNumber = pinNumber; m_doubleClickTimeout = doubleClickTimeout; m_longPressTimeout = longPressTimeout; // Initialise timing values m_timer = m_longPressTimeout; m_debounceTimer = DEBOUNCE_TIME; if (m_pinNumber != 0) { if (isMidiPin(m_pinNumber)) { LOGDBG("MIDI Button on msg: %d (%x)", MidiPinToCC(m_pinNumber), MidiPinToCC(m_pinNumber)); m_midipin = new CMIDIPin (m_pinNumber); } else { LOGDBG("GPIO Button on pin: %d (%x)", m_pinNumber, m_pinNumber); m_pin = new CGPIOPin (m_pinNumber, GPIOModeInputPullUp); } } return TRUE; } void CUIButton::setClickEvent(BtnEvent clickEvent) { m_clickEvent = clickEvent; } void CUIButton::setDoubleClickEvent(BtnEvent doubleClickEvent) { m_doubleClickEvent = doubleClickEvent; } void CUIButton::setLongPressEvent(BtnEvent longPressEvent) { m_longPressEvent = longPressEvent; } unsigned CUIButton::getPinNumber(void) { return m_pinNumber; } CUIButton::BtnTrigger CUIButton::ReadTrigger (void) { unsigned value; if (isMidiPin(m_pinNumber)) { if (!m_midipin) { // Always return "not pressed" if not configured return BtnTriggerNone; } value = m_midipin->Read(); } else { if (!m_pin) { // Always return "not pressed" if not configured return BtnTriggerNone; } value = m_pin->Read(); } if (m_timer < m_longPressTimeout) { m_timer++; if (m_timer == m_doubleClickTimeout && m_lastValue == 1 && m_numClicks == 1) { // The user has clicked and released the button once within the // timeout - this must be a single click reset(); return BtnTriggerClick; } if (m_timer == m_longPressTimeout) { if (m_lastValue == 0 && m_numClicks == 1) { // Single long press reset(); return BtnTriggerLongPress; } else { // Just reset it - we've run out of possible interactions reset(); } } } // Debounce here - we don't need to do anything if the debounce timer is active if (m_debounceTimer < DEBOUNCE_TIME) { m_debounceTimer++; return BtnTriggerNone; } // Buttons in PULL UP mode are "active low" if (value == 0) { if (m_lastValue == 0) { // 0 -> 0 : Button is pressed, was already pressed } else { // 1 -> 0 : Button was not pressed but is now pressed m_lastValue = 0; m_debounceTimer = 0; if (m_numClicks == 0) { // No clicks recorded - start a new timer m_timer = 0; } if (m_numClicks < 2) { m_numClicks++; } } } else { if (m_lastValue == 1) { // 1 -> 1 : Button is not pressed, was already not pressed } else { // 0 -> 1 : Button was pressed but is now not pressed (it was released) m_lastValue = 1; m_debounceTimer = 0; if (m_numClicks == 1 && (m_doubleClickEvent == BtnEventNone || m_timer >= m_doubleClickTimeout && m_timer < m_longPressTimeout) ) { // Either the user released the button when there is no double // click mapped // OR: // The user released the button after the double click // timeout, but before the long press timeout reset(); return BtnTriggerClick; } else if (m_numClicks == 2) { // This is the second release in a short period of time reset(); return BtnTriggerDoubleClick; } } } return BtnTriggerNone; } void CUIButton::Write (unsigned nValue) { // This only makes sense for MIDI buttons. if (m_midipin && isMidiPin(m_pinNumber)) { // Update the "MIDI Pin" m_midipin->Write(nValue); } } CUIButton::BtnEvent CUIButton::Read (void) { BtnTrigger trigger = ReadTrigger(); if (trigger == BtnTriggerClick) { return m_clickEvent; } else if (trigger == BtnTriggerDoubleClick) { return m_doubleClickEvent; } else if (trigger == BtnTriggerLongPress) { return m_longPressEvent; } assert (trigger == BtnTriggerNone); return BtnEventNone; } CUIButton::BtnTrigger CUIButton::triggerTypeFromString(const char* triggerString) { if (strcmp(triggerString, "") == 0 || strcmp(triggerString, "none") == 0) { return BtnTriggerNone; } else if (strcmp(triggerString, "click") == 0) { return BtnTriggerClick; } else if (strcmp(triggerString, "doubleclick") == 0) { return BtnTriggerDoubleClick; } else if (strcmp(triggerString, "longpress") == 0) { return BtnTriggerLongPress; } LOGERR("Invalid action: %s", triggerString); return BtnTriggerNone; } CUIButtons::CUIButtons ( unsigned prevPin, const char *prevAction, unsigned nextPin, const char *nextAction, unsigned backPin, const char *backAction, unsigned selectPin, const char *selectAction, unsigned homePin, const char *homeAction, unsigned pgmUpPin, const char *pgmUpAction, unsigned pgmDownPin, const char *pgmDownAction, unsigned TGUpPin, const char *TGUpAction, unsigned TGDownPin, const char *TGDownAction, unsigned doubleClickTimeout, unsigned longPressTimeout, unsigned notesMidi, unsigned prevMidi, unsigned nextMidi, unsigned backMidi, unsigned selectMidi, unsigned homeMidi, unsigned pgmUpMidi, unsigned pgmDownMidi, unsigned TGUpMidi, unsigned TGDownMidi ) : m_doubleClickTimeout(doubleClickTimeout), m_longPressTimeout(longPressTimeout), m_prevPin(prevPin), m_prevAction(CUIButton::triggerTypeFromString(prevAction)), m_nextPin(nextPin), m_nextAction(CUIButton::triggerTypeFromString(nextAction)), m_backPin(backPin), m_backAction(CUIButton::triggerTypeFromString(backAction)), m_selectPin(selectPin), m_selectAction(CUIButton::triggerTypeFromString(selectAction)), m_homePin(homePin), m_homeAction(CUIButton::triggerTypeFromString(homeAction)), m_pgmUpPin(pgmUpPin), m_pgmUpAction(CUIButton::triggerTypeFromString(pgmUpAction)), m_pgmDownPin(pgmDownPin), m_pgmDownAction(CUIButton::triggerTypeFromString(pgmDownAction)), m_TGUpPin(TGUpPin), m_TGUpAction(CUIButton::triggerTypeFromString(TGUpAction)), m_TGDownPin(TGDownPin), m_TGDownAction(CUIButton::triggerTypeFromString(TGDownAction)), m_notesMidi(notesMidi), m_prevMidi(ccToMidiPin(prevMidi)), m_nextMidi(ccToMidiPin(nextMidi)), m_backMidi(ccToMidiPin(backMidi)), m_selectMidi(ccToMidiPin(selectMidi)), m_homeMidi(ccToMidiPin(homeMidi)), m_pgmUpMidi(ccToMidiPin(pgmUpMidi)), m_pgmDownMidi(ccToMidiPin(pgmDownMidi)), m_TGUpMidi(ccToMidiPin(TGUpMidi)), m_TGDownMidi(ccToMidiPin(TGDownMidi)), m_eventHandler (0), m_lastTick (0) { } CUIButtons::~CUIButtons (void) { } boolean CUIButtons::Initialize (void) { // First sanity check and convert the timeouts: // Internally values are in tenths of a millisecond, but config values // are in milliseconds unsigned doubleClickTimeout = m_doubleClickTimeout * 10; unsigned longPressTimeout = m_longPressTimeout * 10; if (longPressTimeout < doubleClickTimeout) { // This is invalid - long press must be longest timeout LOGERR("LongPressTimeout (%u) should not be shorter than DoubleClickTimeout (%u)", m_longPressTimeout, m_doubleClickTimeout); // Just make long press as long as double click longPressTimeout = doubleClickTimeout; } // Each normal button can be assigned up to 3 actions: click, doubleclick and // longpress. We may not initialise all of the buttons. // MIDI buttons only support a single click. unsigned pins[MAX_BUTTONS] = { m_prevPin, m_nextPin, m_backPin, m_selectPin, m_homePin, m_pgmUpPin, m_pgmDownPin, m_TGUpPin, m_TGDownPin, m_prevMidi, m_nextMidi, m_backMidi, m_selectMidi, m_homeMidi, m_pgmUpMidi, m_pgmDownMidi, m_TGUpMidi, m_TGDownMidi }; CUIButton::BtnTrigger triggers[MAX_BUTTONS] = { // Normal buttons m_prevAction, m_nextAction, m_backAction, m_selectAction, m_homeAction, m_pgmUpAction, m_pgmDownAction, m_TGUpAction, m_TGDownAction, // MIDI Buttons only support a single click (at present) CUIButton::BtnTriggerClick, CUIButton::BtnTriggerClick, CUIButton::BtnTriggerClick, CUIButton::BtnTriggerClick, CUIButton::BtnTriggerClick, CUIButton::BtnTriggerClick, CUIButton::BtnTriggerClick, CUIButton::BtnTriggerClick, CUIButton::BtnTriggerClick }; CUIButton::BtnEvent events[MAX_BUTTONS] = { // Normal buttons CUIButton::BtnEventPrev, CUIButton::BtnEventNext, CUIButton::BtnEventBack, CUIButton::BtnEventSelect, CUIButton::BtnEventHome, CUIButton::BtnEventPgmUp, CUIButton::BtnEventPgmDown, CUIButton::BtnEventTGUp, CUIButton::BtnEventTGDown, // MIDI buttons CUIButton::BtnEventPrev, CUIButton::BtnEventNext, CUIButton::BtnEventBack, CUIButton::BtnEventSelect, CUIButton::BtnEventHome, CUIButton::BtnEventPgmUp, CUIButton::BtnEventPgmDown, CUIButton::BtnEventTGUp, CUIButton::BtnEventTGDown }; // Setup normal GPIO buttons first for (unsigned i=0; i 0) { // LOGDBG("BtnMIDICmdHandler (notes): %x %x %x)", nMidiCmd, nMidiData1, nMidiData2); // Using MIDI Note messages for MIDI buttons unsigned midiPin = ccToMidiPin(nMidiData1); for (unsigned i=0; i