Close

Clicking the buttons

A project log for RemuterMCS

Remote control for muting and unmuting the microphone, camera, and speakers

wjcarpenterWJCarpenter 03/06/2021 at 20:220 Comments

Since the RemuterMCS device user interface is a series of dolphin-like clicks and pops, I wanted to outsource the button handling as much as reasonable. I didn't feel like writing fiddly timing code myself. There are many button-handling libraries for the Arduino IDE ecosystem. I spent a little time looking at a few of those, and I have settled on using AceButton by Brian Parks.

The main reasons that I like that particular library:

  1. It provides the button features I want (single click, double click, multiple buttons) and some others that I might find a use for later.
  2. The quality of the documentation and sample code is great. Also, the author's explanation of why and how they wrote the library is consistent with how I think about things.
  3. It uses an event callback scheme for button activity. Novices may find that a bit confounding, but it's second nature to me and will greatly simplify the code I write using the library.
  4. The library API looks clean and sensible.
  5. The most recent versions of it are included in the Arduino IDE library manager, which is convenient.

I did some quick experiments with this library, tweaking the sample code, to make sure it works as advertised. I didn't doubt that it would, once I had read through the extensive documentation. Here is a sketch, KeysAndClicks, that reacts to single and double clicks by printing the targeted reaction on the serial console (which it expects to be configured for 115,200 speed).

/**
 * Reacts to single and double clicks, but the reaction just prints
 * what should happen.
 */

#include <AceButton.h>
#include <M5StickC.h>

using namespace ace_button;

const int LED_PIN = M5_LED;
const int LED_OFF = HIGH;
const int LED_ON  = LOW;

// The pin number attached to the button.
#define ACTION_BUTTON_PIN M5_BUTTON_HOME
#define MODE_BUTTON_PIN   M5_BUTTON_RST

AceButton actionButton(ACTION_BUTTON_PIN, HIGH, 1);
AceButton modeButton(MODE_BUTTON_PIN, HIGH, 2);
AceButton *buttons[] = {&actionButton, &modeButton};
const uint8_t NUMBER_OF_BUTTONS = sizeof(buttons) / sizeof(buttons[0]);

void setup() {
  Serial.begin(115200);
  Serial.println("Starting");

  pinMode(LED_PIN, OUTPUT);
  turnLedOff();

  pinMode(ACTION_BUTTON_PIN, INPUT_PULLUP);
  pinMode(MODE_BUTTON_PIN,   INPUT_PULLUP);

  // using method 3, ClickVersusDoubleClickUsingBoth, to distinguish clicks
  ButtonConfig* actionButtonConfig = actionButton.getButtonConfig();
  actionButtonConfig->setEventHandler(handleButtonEvent);
  actionButtonConfig->setFeature(ButtonConfig::kFeatureDoubleClick);
  actionButtonConfig->setFeature(ButtonConfig::kFeatureSuppressClickBeforeDoubleClick);
  actionButtonConfig->setFeature(ButtonConfig::kFeatureSuppressAfterClick);
  actionButtonConfig->setFeature(ButtonConfig::kFeatureSuppressAfterDoubleClick);

  ButtonConfig* modeButtonConfig = modeButton.getButtonConfig();
  modeButtonConfig->setEventHandler(handleButtonEvent);
  modeButtonConfig->setFeature(ButtonConfig::kFeatureDoubleClick);
  modeButtonConfig->setFeature(ButtonConfig::kFeatureSuppressClickBeforeDoubleClick);
  modeButtonConfig->setFeature(ButtonConfig::kFeatureSuppressAfterClick);
  modeButtonConfig->setFeature(ButtonConfig::kFeatureSuppressAfterDoubleClick);
}

typedef struct targetDevice {
  int id;
  const char *name;
} targetDevice_t;

targetDevice_t microphone = {0, "microphone"};
targetDevice_t camera     = {1, "camera"};
targetDevice_t speaker    = {2, "speaker"};
targetDevice_t *targetDevices[] = {µphone, &camera, &speaker};
const uint8_t NUMBER_OF_TARGET_DEVICES = sizeof(targetDevices) / sizeof(targetDevices[0]);
int activeTargetDeviceNumber = 0;

#define ACTION_CLICK_NONE   0
#define ACTION_CLICK_SINGLE 1
#define ACTION_CLICK_DOUBLE 2
int actionClickType = ACTION_CLICK_NONE;

void loop() {
  manageLedState();

  actionButton.check();
  if (actionClickType != ACTION_CLICK_NONE) {
    Serial.print("action click "); Serial.println(actionClickType == 1 ? "SINGLE" : "DOUBLE");
    actionClickType = ACTION_CLICK_NONE;
  }

  int previousActiveTargetDeviceNumber = activeTargetDeviceNumber;
  modeButton.check();
  if (activeTargetDeviceNumber != previousActiveTargetDeviceNumber) {
    Serial.print("change focus to "); Serial.println(targetDevices[activeTargetDeviceNumber]->name);
  }

}

void manageLedState() {
  // We forego the debouncing of the button library in order to be able
  // to accurately control the LED for press and release. If we get the
  // button states via AceButton, the LED reaction is slowed down by the
  // AceButton built-in delays.
  bool anyButtonIsPressed = false;
  for (int ii=0; ii<NUMBER_OF_BUTTONS; ++ii) {
    int pin = buttons[ii]->getPin();
    if (digitalRead(pin) == LOW) {
      anyButtonIsPressed = true;
      break;
    }
  }
  if (anyButtonIsPressed) {
    turnLedOn();
  } else {
    turnLedOff();
  }
}

bool ledIsOn = true; // for boot-up purposes
void turnLedOn() {
  if (!ledIsOn) {
    digitalWrite(LED_PIN, LED_ON);
  }
  ledIsOn = true;
}

void turnLedOff() {
  if (ledIsOn) {
    digitalWrite(LED_PIN, LED_OFF);
  }
  ledIsOn = false;
}

void handleButtonEvent(AceButton* button, uint8_t eventType, uint8_t buttonState) {
  if (button->getPin() == ACTION_BUTTON_PIN) {
    handleActionButtonEvent(button, eventType, buttonState);
  } else if (button->getPin() == MODE_BUTTON_PIN) {
    handleModeButtonEvent(button, eventType, buttonState);
  } else {
    // shouldn't happen
    Serial.print("Unknown button pin "); Serial.println(button->getPin(), DEC);
  }
}

void handleActionButtonEvent(AceButton* button, uint8_t eventType, uint8_t buttonState) {
  actionClickType = ACTION_CLICK_NONE;
  switch (eventType) {
    case AceButton::kEventClicked:
    case AceButton::kEventReleased:
      actionClickType = ACTION_CLICK_SINGLE;
      break;
    case AceButton::kEventDoubleClicked:
      actionClickType = ACTION_CLICK_DOUBLE;
      break;
  }
}

void handleModeButtonEvent(AceButton* button, uint8_t eventType, uint8_t buttonState) {
  switch (eventType) {
    case AceButton::kEventClicked:
    case AceButton::kEventReleased:
      activeTargetDeviceNumber++;
      break;
    case AceButton::kEventDoubleClicked:
      // just for demonstrating double click of the mode button
      activeTargetDeviceNumber += 2;
      break;
  }
  activeTargetDeviceNumber %= NUMBER_OF_TARGET_DEVICES;
}

This is one step in the evolution of the RemuterMCS source code, but pay no attention to the specifics of the data structures, constants, and whatnot. Those will surely change as the software evolves.

Discussions