MIDI Control Surface - Encoder shift function

Hello,
I'm working on a project where I would like to be able to select which V_POT to use with a single encoder.
For example I have an encoder wired for controlling V_POT_5, when I press a button I want it to control V_POT_1.

I tired with the Bank ChangeAddress function but I can only control the next V_POT.

Can anybody help me?

Can't help without you posting code and wiring diagram.

Sure thing, here's the code:

#include <Control_Surface.h>

// MIDI interface
USBMIDI_Interface midi;

// --- Default Mode Encoders (normal volume control) ---
CCRotaryEncoder encoder1_default({3, 4}, MCU::V_POT_2, 1); // V_POT_2
CCRotaryEncoder encoder2_default({5, 6}, MCU::V_POT_3, 1); // V_POT_3
CCRotaryEncoder encoder3_default({7, 8}, MCU::V_POT_4, 1); // V_POT_4

// --- Override Mode Encoders (button held down) ---
CCRotaryEncoder encoder1_override({3, 4}, MCU::V_POT_1, 1); // V_POT_1
CCRotaryEncoder encoder2_override({5, 6}, MCU::V_POT_1, 1); // V_POT_1
CCRotaryEncoder encoder3_override({7, 8}, MCU::V_POT_1, 1); // V_POT_1


// Button Pins
const uint8_t button1Pin = 14;
const uint8_t button2Pin = 15;
const uint8_t button3Pin = 16;

void setup() {
  Control_Surface.begin();

  pinMode(button1Pin, INPUT_PULLUP);
  pinMode(button2Pin, INPUT_PULLUP);
  pinMode(button3Pin, INPUT_PULLUP);
}

void loop() {
  Control_Surface.loop();


  // Read button states (active low)
  bool button1Pressed = !digitalRead(button1Pin);
  bool button2Pressed = !digitalRead(button2Pin);
  bool button3Pressed = !digitalRead(button3Pin);

  // Override logic: only one encoder active at a time
  if (button1Pressed) {
    encoder1_override.update();
  } else if (button2Pressed) {
    encoder2_override.update();
  } else if (button3Pressed) {
    encoder3_override.update();
  } else {
    // Normal mode: all default encoders active
    encoder1_default.update();
    encoder2_default.update();
    encoder3_default.update();
  }

}

Physically there are 3 buttons and 3 encoders.
In the sketch there here are two groups of encoders, normal encoders controlling volume, and override encoders; while the button is down only the corresponding encoder is active and it controls V_POT_1.

The sketch get's uploaded but nothing happens.
Let me know your thoughts.

Thank you!

Maybe you missed this part.

Hi, thanks for your reply.
Please see the wiring diagram attached.

thank you

Can you also tell us what sort of processor (Arduino) you are using along with the IDE version please.

I’m using the latest version of IDE.
I’ll run the sketch on a Leonardo/pro micro.
Thanks

There are two latest versions of the IDE 1.8.19 and 2.x.x. what are you using?

There are three versions of the pro micro.
Arduino / SparkFun/ Generic eBay. They all have different pinouts.

IDE version 2.1.1

I thought Arduino / SparkFun/ Generic eBay where interchangeable.
Could you please show the evidence of pinout discrepancy?

Hi again,
I’ve been trying hard to get the sketch above working, but no luck so far.
Could anyone give me a hint?

You cannot have multiple encoders on the same pins (this is because encoders use interrupts when possible). If you really need multiple MIDI output elements that read the same encoder, you'll have to create an Encoder first, and then pass it by reference to one or more BorrowedCCRotaryEncoders.

This doesn't do what you think it does: Control_Surface.loop() already updates all MIDI elements. If you want to prevent that, you'll need to disable() specific elements.

Thanks for helping @PieterP, much appreciated as always.

I've found this old post of yours where you describe how to use BorrowedCCRotaryEncoders:

So I made the following sketch but it still doesn't work; by the way, the sketch in the above mentioned link doesn't work either, same error.

#include <Encoder.h>

#include <Control_Surface.h>

USBMIDI_Interface midi;

Button button2 = 14;
Button button3 = 15;
Button button4 = 16;

Encoder enc2 = { 3, 4 };
Encoder enc3 = { 5, 6 };
Encoder enc4 = { 7, 8 };


BorrowedMIDIRotaryEncoder<ContinuousCCSender> enc2to1 = {
  enc2,
  MCU::V_POT_1,
  1,   // multiplier
  1,   // pulses per step
  {},  // MIDI sender
};

BorrowedMIDIRotaryEncoder<ContinuousCCSender> enc3to1 = {
  enc3,
  MCU::V_POT_1,
  1,   // multiplier
  1,   // pulses per step
  {},  // MIDI sender
};

BorrowedMIDIRotaryEncoder<ContinuousCCSender> enc4to1 = {
  enc4,
  MCU::V_POT_1,
  1,   // multiplier
  1,   // pulses per step
  {},  // MIDI sender
};

BorrowedMIDIRotaryEncoder<ContinuousCCSender> enc2to2 = {
  enc2,
  MCU::V_POT_2,
  1,   // multiplier
  1,   // pulses per step
  {},  // MIDI sender
};

BorrowedMIDIRotaryEncoder<ContinuousCCSender> enc3to3 = {
  enc3,
  MCU::V_POT_3,
  1,   // multiplier
  1,   // pulses per step
  {},  // MIDI sender
};

BorrowedMIDIRotaryEncoder<ContinuousCCSender> enc4to4 = {
  enc4,
  MCU::V_POT_4,
  1,   // multiplier
  1,   // pulses per step
  {},  // MIDI sender
};

void setup() {

  Control_Surface.begin();

  button2.begin();
  button3.begin();
  button4.begin();

  enc2to1.disable();
  enc3to1.disable();
  enc4to1.disable();
}

void loop() {
  Control_Surface.loop();

  auto button2State = button2.update();
  if (button2State == Button::Falling) {  //when button2 is presses it enables enc2 to control V_POT_1 and it disables all the other encoders
    enc2to1.enable();
    enc3to1.disable();
    enc4to1.disable();

    enc2to2.disable();
    enc3to3.disable();
    enc4to4.disable();


  } else if (button2State == Button::Rising) {  //restore normal mode
    enc2to1.disable();
    enc3to1.disable();
    enc4to1.disable();

    enc2to2.enable();
    enc3to3.enable();
    enc4to4.enable();
  }

  auto button3State = button3.update();
  if (button3State == Button::Falling) {  //when button3 is presses it enables enc3 to control V_POT_1 and it disables all the other encoders
    enc2to1.disable();
    enc3to1.enable();
    enc4to1.disable();

    enc2to2.disable();
    enc3to3.disable();
    enc4to4.disable();


  } else if (button3State == Button::Rising) {  //restore normal mode
    enc2to1.disable();
    enc3to1.disable();
    enc4to1.disable();

    enc2to2.enable();
    enc3to3.enable();
    enc4to4.enable();
  }

  auto button4State = button4.update();
  if (button4State == Button::Falling) {  //when button3 is presses it enables enc4 to control V_POT_1 and it disables all the other encoders
    enc2to1.disable();
    enc3to1.disable();
    enc4to1.enable();

    enc2to2.disable();
    enc3to3.disable();
    enc4to4.disable();


  } else if (button4State == Button::Rising) {  //restore normal mode
    enc2to1.disable();
    enc3to1.disable();
    enc4to1.disable();

    enc2to2.enable();
    enc3to3.enable();
    enc4to4.enable();
  }
}

Thank you!

Hi again, I forgot to attach the compilation error:

could not convert '{enc2, cs::MCU::V_POT_1, 1, 1, ()}' from '' to 'cs::BorrowedMIDIRotaryEncodercs::ContinuousCCSender {aka cs::GenericMIDIRotaryEncoder<cs::AHEncoder&, cs::ContinuousCCSender>}'

  • };*

Same error each BorrowedMIDIRotaryEncoder.
Thank you

Please see the Control Surface release notes. Remove #include <Encoder.h> and use AHEncoder instead of Encoder.

AHEncoder enc2{3, 4};
AHEncoder enc3{5, 6};
AHEncoder enc4{7, 8};

BorrowedCCRotaryEncoder enc2to1 {
  enc2,
  MCU::V_POT_1,
  1,   // multiplier
  1,   // pulses per step
};

BorrowedCCRotaryEncoder enc3to1 {
  enc3,
  MCU::V_POT_1,
  1,   // multiplier
  1,   // pulses per step
};

// etc.

I haven't checked your disable/enable logic in detail, but keep in mind that you cannot disable an element that is already disabled.

Thanks for replying @PieterP, noted about The Encoder class being replaced by AHEncoder.

Now the sketch works but there are a couple of things to fix:

  • the encoders send value of 127 & 1 rather than 65 & 1 and the reading is imprecise (skipping between values).

  • the disable/enable logic still needs attention. I've added a latch function when the buttons are long-pressed; the sketch below works but it crashes if I long-press a button while already in long-press mode—likely due to a conflict.

EDIT: I want to ensure a clean switch between modes by disabling all encoders briefly (100 ms) before calling the LongPress mode. I thought this could help prevent glitches or crashes due to overlapping encoder state transitions. I've added a 100ms delay between disabling all encoders and enabling the new one.
The sketch runs but the board freezes when I LongPress.

RE-EDIT: I managed to get the sketch working making sure I wasn't disabling elements already disabled. I still have the problem with the encoders though.

RE-RE-Edit: Problem with the encoders sorted: I changed the ContinuousCC Sender to RelativeCCSender.

#include <Control_Surface.h>

USBMIDI_Interface midi;

Button button2 = 14;
Button button3 = 15;
Button button4 = 16;

AHEncoder enc2 = { 3, 4 };
AHEncoder enc3 = { 5, 6 };
AHEncoder enc4 = { 7, 8 };

constexpr unsigned long LONG_PRESS_DURATION = 500;  // ms

struct LongPressLatch {
  Button &button;
  unsigned long pressStartTime = 0;
  bool longPressDetected = false;
  bool menuActive = false;

  LongPressLatch(Button &btn) : button(btn) {}

  void begin() {
    button.begin();
  }

  bool update() {
    button.update();
    if (button.getState() == Button::Pressed) {
      if (!pressStartTime) {
        pressStartTime = millis();
      } else if (!longPressDetected && (millis() - pressStartTime >= LONG_PRESS_DURATION)) {
        longPressDetected = true;
        menuActive = !menuActive;  // Toggle menu state
        return true;  // Indicates latch state changed
      }
    } else {
      pressStartTime = 0;
      longPressDetected = false;
    }
    return false;
  }

  bool isMenuActive() const {
    return menuActive;
  }
};

LongPressLatch lpButton2(button2);
LongPressLatch lpButton3(button3);
LongPressLatch lpButton4(button4);

// Encoders for Menu Mode
BorrowedMIDIRotaryEncoder<ContinuousCCSender> enc2to1 = {
  enc2, MCU::V_POT_1, 1, 1, {},
};
BorrowedMIDIRotaryEncoder<ContinuousCCSender> enc3to1 = {
  enc3, MCU::V_POT_1, 1, 1, {},
};
BorrowedMIDIRotaryEncoder<ContinuousCCSender> enc4to1 = {
  enc4, MCU::V_POT_1, 1, 1, {},
};

// Encoders for Normal Mode
BorrowedMIDIRotaryEncoder<ContinuousCCSender> enc2to2 = {
  enc2, MCU::V_POT_2, 1, 1, {},
};
BorrowedMIDIRotaryEncoder<ContinuousCCSender> enc3to3 = {
  enc3, MCU::V_POT_3, 1, 1, {},
};
BorrowedMIDIRotaryEncoder<ContinuousCCSender> enc4to4 = {
  enc4, MCU::V_POT_4, 1, 1, {},
};

void setup() {
  Control_Surface.begin();

  lpButton2.begin();
  lpButton3.begin();
  lpButton4.begin();

  enc2to1.disable();
  enc3to1.disable();
  enc4to1.disable();
}

void disableAllEncoders() {
  enc2to1.disable();
  enc3to1.disable();
  enc4to1.disable();
  enc2to2.disable();
  enc3to3.disable();
  enc4to4.disable();
}

void loop() {
  Control_Surface.loop();

  // Button 2
  if (lpButton2.update()) {
    disableAllEncoders();
    delay(100);
    if (lpButton2.isMenuActive()) {
      enc2to1.enable();
    } else {
      enc2to1.disable();
      enc2to2.enable();
      enc3to3.enable();
      enc4to4.enable();
    }
  }

  // Button 3
  if (lpButton3.update()) {
    disableAllEncoders();
    delay(100);
    if (lpButton3.isMenuActive()) {
      enc3to1.enable();
    } else {
      enc3to1.disable();
      enc2to2.enable();
      enc3to3.enable();
      enc4to4.enable();
    }
  }

  // Button 4
  if (lpButton4.update()) {
    disableAllEncoders();
    delay(100);
    if (lpButton4.isMenuActive()) {
      enc4to1.enable();
    } else {
      enc4to1.disable();
      enc2to2.enable();
      enc3to3.enable();
      enc4to4.enable();
    }
  }
}

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.