Register / Login  |  Desktop view  |  Jump to bottom of page

IoAbstraction & TaskManagerIO » Suitable for my project

Author: TheTomRom
26/03/2020 17:03:16
Hello there,
I am quite new to Arduino, but I have started an ambitious (at least for me) project. And now I found your fantastic library that could potentially solve most of the problems I would certainly have.

Short description of what I am planning: Button box for PC gaming, using a “pro micro” with a library that enables the pro micro to be recognized as HID to emulate key presses from the keyboard. This button box has 14 push buttons, 18 toggle buttons and 7 rotary encoders with push. I got 4 x the 23017 IO expander.

My question is: Do you think the IoAbstraction library is capable of doing what I am describing? Are there limitations and do I have to change basic values (I think I already understood that I would have to change the limitation of maximum usable buttons)? My goal is to have complete software based debounce – included in the library if I am not wrong. Also, I want button presses to be handled by interrupts. It seems to me that this is the better way of coding and in addition, I want to update OLEDs from time to time (I got an TCA9548A as I am using multiple displays), which makes polling a bad idea.

I would be very happy if you could answer my questions and tell me potential caveats of my idea. Thanks in advance!
Tom


Author: davetcc
27/03/2020 09:25:11
 
I am quite new to Arduino, but I have started an ambitious (at least for me) project. And now I found your fantastic library that could potentially solve most of the problems I would certainly have.


That's great, IoAbstraction is designed for complex projects, to help with some of the tedious tasks that are traditionally done in the run loop.

Short description of what I am planning: Button box for PC gaming, using a “pro micro” with a library that enables the pro micro to be recognized as HID to emulate key presses from the keyboard. This button box has 14 push buttons, 18 toggle buttons and 7 rotary encoders with push. I got 4 x the 23017 IO expander.


This should be well within the capabilities of the library. If the push buttons are connected one to each input, then switches should be able to cope with that quite easily. You'll just need to increase the number of switches in SwitchInput.h. Similarly for rotary encoders, you should be able to incease the number of rotary encoders in the same file.

To change the number of keys:

#ifndef MAX_KEYS
#define MAX_KEYS 5
#endif // MAX_KEYS defined


To change the number of encoders:

#ifndef MAX_ROTARY_ENCODERS
#define MAX_ROTARY_ENCODERS 4
#endif // MAX_ROTARY_ENCODERS


It should be easy for you to do a quick test sketch that tested 7 encoders and 20 - 30 switches. I don't forsee any problems with that volume, but you may want to use the interrupt support in switches to avoid polling.

My question is: Do you think the IoAbstraction library is capable of doing what I am describing? Are there limitations and do I have to change basic values (I think I already understood that I would have to change the limitation of maximum usable buttons)?


See above.

My goal is to have complete software based debounce – included in the library if I am not wrong. Also, I want button presses to be handled by interrupts. It seems to me that this is the better way of coding and in addition, I want to update OLEDs from time to time (I got an TCA9548A as I am using multiple displays), which makes polling a bad idea.


Agreed, as per above, use the interrupt mode in switches and just make sure you call the taskManager runLoop very frequently.

I would be very happy if you could answer my questions and tell me potential caveats of my idea. Thanks in advance!


Hopefully, this covers most of it, I recommend a writing a test sketch that just records the value of all the encoders and switches to globals. Then maybe prints the current values once a second using a taskManager task.

Author: TheTomRom
29/03/2020 23:48:48
Thanks for the kind and fast response! I have read through the examples and tutorials and made the firsts tests. I came across a couple of questions:

1) Sometimes, you define arduino pin 2 for an interrupt, in other examples it seems to be unnecessary - I do not understand why. Would you also have to connect the button physically?
2) As I said, I am going to simulate key presses in my project (like sending a "q" character to the pc, when button 3 ist pressed or so) I wanted to function part of the rotatry encoders the same way: turning left sends a "p", turning right sends a "g" (just an example). It's just quicker than hitting "g" 10 times on the keyboard. I know that this is different from using the encorder in a "classical" way. To do this, can you switch off the encoder maximum value completely (unlimited) or will it always have a maximum value? Also, is there a way to find out the direction (L/R) of the encoder change? Of course, I could do this by some simple math (value de- or increased) but I need for some of the encoders would be: one step left -> send "p", one step right "send g".
3) Lastly, a wiring questions, if you don't feel like answering them, it's totally fine though! SDA/SCL seem to need pull up resistors. Do I need one for each I2C device separately or can I just use one for all of them, basically making a "rail" and connect many devices? Same for setting addresses on the 23017: I will need pull up or pull down resistors to A0, A1 or A2, right? For many 23017s, can I group all things to be pulled high (let's say A0 and A1 from expander 1 and A2 from expander 2) or does every input needs a separate resistor?

Thanks again,
Tom

Author: davetcc
30/03/2020 12:59:54
1. In terms of interrupts, rotary encoders must always have PIN_A attached to an interrupt pin. With switches, you have the choice of interrupt operation or regular polling. If you use a 23017, then the interrupt pin from that device should be in OR mode, where you use just one interrupt port for all pins and connect this to an interrupt capable pin on the Arduino. This is the pin you configure when you're creating the ioFrom23017 object. It relays the interrupts from the IO expander back to the Arduino. IoAbstraction takes care of this for you as long as you define that pin.

2. The rotary encoder classes are not really designed for the way you want to use them. They are designed to work with integer values, IE some value between 0 and the maximum value. You could determine the direction by holding the last value and comparing it as you said in the sketch. Not too much overhead, but you will eventually hit the limits either way. You could reduce that by starting it at 32768 and setting the range to 65355. But eventually, you could hit the limits.

3. Wiring is a difficult one, without seeing the design and lengths of each run, it's difficult to know how to answer. A couple of things, there are many articles on the size of pull-up resistors for a given length of wire. In terms of pull-ups, IO Abstraction turns on INPUT_PULLUP by default if the pin supports it. So, if your switches and encoders are very close you may get away without resistors. If you do need resistors, one per switch is needed.


Author: davetcc
15/04/2020 13:39:26
Please take a look at https://github.com/davetcc/IoAbstraction/issues/85 and https://github.com/davetcc/IoAbstraction/issues/83

The change that has been suggested in issue 83 would probably be exactly what you wanted for the rotary encoders that have no actual value, just representing a direction.

Dave.

Author: TheTomRom
15/04/2020 14:10:17
Thanks Dave for letting me know! I have avoided the problem by storing the old encoder value and comparing it with the new one to get the direction. In the unlikey event of the value getting close to the boundary of 65.536, it will just set the encoder back to starting value (32.76 smilie. That works fine for me, but I am happy that you plan to integrate this functionality.

While working with the encoders I also noticed that you would have to create a callback for each encoder separately. Maybe integrate that the encoders return not only the new value, but also the pin number, like the buttons do. That way you could have one callback function for all encorders.

Author: TheTomRom
04/05/2020 13:35:16
Hey Dave,

in case this question should be better asked in a new topic, please feel free to move it, but it is still concerning the same project of mine.

The situation: For the looks and feels of my button box, I have some physical toggle buttons that are connected to simply be on or off (their behavior is like permanent push buttons).
The wanted behavior: Send a virtual keystroke (let's say a "k" character) to the PC whenever the toggle status is CHANGED.
The question: How would you do this using IoAbstraction? Using normal callback and a "onRelease" Callback just initiating the same keypress?
The problem: Using a toggle button would always call the button callback twice, one time for button "pressed", one time for "held". How can I avoid this?

Thanks in advance,
Tom

Author: davetcc
05/05/2020 09:02:24
Could you not just wrap the callback and check if the held status is true, ignoring it.

eg

void onButtonDown(uint8_t pin, bool heldDown) {
  if(heldDown) return;
  do other stuff
}

Author: TheTomRom
22/05/2020 17:14:26
Hi Dave,

thanks for your help, I have done it as suggested. I am almost done, many of the things do work - all the buttons and displays work as intended. But I am completely stuck with the rotary encoders. Although I have tested everything before, I cannot get it to work. Here is the relevant part of my code (I have omitted all the display and menu stuff). I know that this is not the greatest way of coding probably, but maybe I have done something wrong that I cannot find.

What I have tested:
1) Whether the interrupt is working and is connected to PIN 7 of the Pro Micro board (I used switches.initialiseInterrupt -> everything works, setting it programmically to PIN 8 while physically to PIN 7 -> nothing works which is the desired behavior)
2) Testing the 23017 expander (I added the encoders as buttons and it worked, the corresponding PIN numbers showed up in the serial monitor)
3) Made sure that all rotary encoders are on the same 23017 and the Max switches and Max encoders numbers are changed in the *.h source file.
4) The encoder callback works, as it gives an output to the serial monitor when the encoder object is created

I really don't know what else could be the reason, but I had two ideas:
A) On all my tests the rotary encoders were located on the first 23017 expander (0x20). Now they are on 0x22. Could that be the reason?
B) Do I have to change something with the "slots" for the encoder objects? I don't really know what they are for.

One more thing about the code down there: I temporary disabled all but the first encoder for testing, but even this is not working.

Any help is greatly appreciated!
Tom


#include <IoAbstraction.h>
#include <IoAbstractionWire.h>
#include <Wire.h>
#include <Keyboard.h>
#include <Arduino.h>
#include <U8g2lib.h>




/////////////////
// DEFINITIONS //
/////////////////

// start value and maximum encoder value before reset
uint16_t EncValList[] = {32000, 32000, 32000, 32000, 32000, 32000, 32000, 32000};
const uint16_t MaxEncValList[] = {65000, 65000, 65000, 65000, 65000, 65000, 65000, 65000};

// Hardcoded hardware addresses
const byte TcaAddress = 0x70; // I2C address of the TCA I2C MUX
const byte ResPinsForArduino = 100; // input value for the IoAbstraction library, reserved pins for Arduino
const byte ArduinoInterruptPin = 7; //physical pin on the arduino to record interrupts

// profile, menu and displays
const byte NoOfButtons = 64;

// instantiate IoAbstraction
MultiIoAbstractionRef MultiIo = multiIoExpander(ResPinsForArduino);





///////////////////////////////
// STANDARD BUTTON CALLBACKS //
///////////////////////////////



void OnPushBtnPress(byte PinNum, bool HeldDown) {
  // ignore when buttons are held
  if (HeldDown == false) {
    ButtonHandler(PinNum, HeldDown);
  }
}

void OnToggleBtnPress(byte PinNum, bool HeldDown) {
  // ignore when buttons are held
  if (HeldDown == false) {
    ButtonHandler(PinNum, HeldDown);
  }
}

void OnToggleBtnRelease(byte PinNum) {
  // Could be the same as OnToggleBtnPress, but separated in case of future changes
  ButtonHandler(PinNum, 0);
}

void onEncChange0(uint16_t EncVal) {
  Serial.println(F("IN: onEncChange0"));
  EncoderToButton(0, EncVal, 36, 44);
}

void onEncChange1(uint16_t EncVal) {
  EncoderToButton(1, EncVal, 37, 45);
}

void onEncChange2(uint16_t EncVal) {
  EncoderToButton(2, EncVal, 39, 47);
}

void onEncChange3(uint16_t EncVal) {
  EncoderToButton(3, EncVal, 38, 46);
}

void onEncChange4(uint16_t EncVal) {
  EncoderToButton(4, EncVal, 35, 43);
}

void onEncChange5(uint16_t EncVal) {
  EncoderToButton(5, EncVal, 34, 42);
}

void onEncChange6(uint16_t EncVal) {
  EncoderToButton(6, EncVal, 33, 41);
}

void onEncChange7(uint16_t EncVal) {
  EncoderToButton(7, EncVal, 32, 40);
}




/////////////////////////////
// ROTARY ENCODER WRAPPER  //
/////////////////////////////

void EncoderToButton(byte EncAddr, uint16_t EncVal, byte BtnBack, byte BtnFwd) {
  Serial.print(F("Encoder Address: "));
  Serial.println(EncAddr);
  Serial.print(F("Encoder Value: "));
  Serial.println(EncVal);


  int EncDir = GetEncDirection(EncAddr, EncVal);
  if (EncDir == 1) {
    ButtonHandler(ResPinsForArduino + BtnBack, false);
  }
  else if (EncDir == -1) {
    ButtonHandler(ResPinsForArduino + BtnFwd, false);
  }
}


int GetEncDirection(byte EncAddr, uint16_t NewEncVal) {
  //Serial.println(" ");
  //Serial.println("-------------------------");
  //Serial.println("INSIDE getEncDirection");
  int EncDir;
  uint16_t OldEndVal = EncValList[EncAddr];

  if (NewEncVal > OldEndVal) {
    EncDir = 1;
  }
  else if (NewEncVal < OldEndVal) {
    EncDir = -1;
  }
  else if (NewEncVal == OldEndVal) {
    EncDir = 0;
    Serial.println(F("ERROR: New EncoderVal = Old EncoderVal!"));
  }

  if (NewEncVal > 60000 || NewEncVal < 3000) {
    NewEncVal = 32000;
  }

  EncValList[EncAddr] = NewEncVal;
  //Serial.print("getEncDirection, Dir: ");
  //Serial.println(EncDir);
  return EncDir;
}





void ButtonHandler(byte PinNum, bool HeldDown) {
  //Serial.println(" ");
  //Serial.println("-------------------------");
  //Serial.println("INSIDE Task Handler");

  // Check whether system is on or off
  if (OnOffState == 0) {
    return;
  }

  // wake up the displays
  WakeUp();

  // get the real ButtonID, identical to array indices
  byte BtnId = PinNum - ResPinsForArduino;

  // get the number of display to be sent (via DispSelect)
  byte DispAddr = RetrieveNumsFromFlash (4, BtnId); // first input is ArrayNumber = 4 for DisplayNumber

  //Get the information, whether this button is used as toggle
  bool IsToggle = RetrieveNumsFromFlash (6, BtnId); // ArrayNum = 6 is EnableToggleList


  Serial.print(F("PinNum: "));
  Serial.println(PinNum);
  Serial.print(F("BtnId: "));
  Serial.println(BtnId);
  Serial.print(F("IsToggle: "));
  Serial.println(IsToggle);


  // set FontInverse
  bool IsFontInverse = false;

  // if this is a toggle button, change the state in BtnStateList
  // and redraw the display
  if (IsToggle == true) {
    bool OldBtnState = BtnStateList[BtnId];
    BtnStateList[BtnId] = !OldBtnState;

    Serial.print(F("OldBtnState: "));
    Serial.println(OldBtnState);
    Serial.print(F("NewBtnState: "));
    Serial.println(!OldBtnState);

    if (!OldBtnState == true) {
      IsFontInverse = true;
    }

    // draw the text to display
    DispDrawFull(DispAddr);
  }

  //send the keystroke(s)
  KeypressHandler(BtnId);

  // Start the Sleep Counter
  PrepareSleep();
}





///////////
// SETUP //
///////////


void setup() {
  Wire.begin();
  Serial.begin(9600);

  //InitializeDisplays;
  for (byte k = 0; k < 8; k++) {
    DispSelect(k);
    u8g2.begin();
    u8g2.firstPage();
  }

  // adding all 23017 expander
  multiIoAddExpander(MultiIo, ioFrom23017(0x20, ACTIVE_LOW_OPEN, ArduinoInterruptPin), 16);
  multiIoAddExpander(MultiIo, ioFrom23017(0x21, ACTIVE_LOW_OPEN, ArduinoInterruptPin), 16);
  multiIoAddExpander(MultiIo, ioFrom23017(0x22, ACTIVE_LOW_OPEN, ArduinoInterruptPin), 16);
  multiIoAddExpander(MultiIo, ioFrom23017(0x23, ACTIVE_LOW_OPEN, ArduinoInterruptPin), 16);

  switches.initialiseInterrupt(MultiIo, true);   //switches.initialiseInterrupt(MultiIo, true);

  // Basic controls
  switches.addSwitch(ResPinsForArduino + 7, OnMainSwitchPress);   // ON
  switches.onRelease(ResPinsForArduino + 7, OnMainSwitchRelease); // OFF
  switches.addSwitch(ResPinsForArduino + 6, OnEnableBtnPress);    // Enable ON
  switches.onRelease(ResPinsForArduino + 6, OnEnableBtnRelease);  // Enable OFF
  switches.addSwitch(ResPinsForArduino + 3, OnProfileChBtnPress); // Change Profile
  switches.addSwitch(ResPinsForArduino + 2, OnSleepTimeBtnPress); // Change SleepTimer
  switches.addSwitch(ResPinsForArduino + 5, OnProfileSelBtnPress);// Select Profile
  switches.addSwitch(ResPinsForArduino + 4, OnDispOnOffBtnPress); // Displays On/Off

  // Toggle switches, initialise in a loop
  for (byte k = 16; k < 32; k++) {
    switches.addSwitch(ResPinsForArduino + k, OnToggleBtnPress);
    switches.onRelease(ResPinsForArduino + k, OnToggleBtnRelease);
  }

  // Large Toggle switches
  switches.addSwitch(ResPinsForArduino + 56, OnToggleBtnPress);
  switches.addSwitch(ResPinsForArduino + 57, OnToggleBtnPress);

  // Push Buttons
  switches.addSwitch(ResPinsForArduino + 52, OnPushBtnPress);
  switches.addSwitch(ResPinsForArduino + 53, OnPushBtnPress);
  switches.addSwitch(ResPinsForArduino + 54, OnPushBtnPress);
  switches.addSwitch(ResPinsForArduino + 55, OnPushBtnPress);
  switches.addSwitch(ResPinsForArduino + 60, OnPushBtnPress);
  switches.addSwitch(ResPinsForArduino + 61, OnPushBtnPress);
  switches.addSwitch(ResPinsForArduino + 62, OnPushBtnPress);
  switches.addSwitch(ResPinsForArduino + 63, OnPushBtnPress);

  // Color Push Buttons
  switches.addSwitch(ResPinsForArduino + 51, OnPushBtnPress); // Green 0
  switches.addSwitch(ResPinsForArduino + 50, OnPushBtnPress); // Green 1
  switches.addSwitch(ResPinsForArduino + 58, OnPushBtnPress); // Red 0
  switches.addSwitch(ResPinsForArduino + 59, OnPushBtnPress); // Red 1

  // Encoder Click buttons, initialise in a loop
  for (byte m = 8; m < 16; m++) {
    switches.addSwitch(ResPinsForArduino + m, OnPushBtnPress);
  }

  // Only for testing, Encoder buttons as normal switches
  //for (byte m = 32; m < 48; m++) {
  //  switches.addSwitch(ResPinsForArduino + m, OnPushBtnPress);
  //}

  // setting up first encoder
  setupRotaryEncoderWithInterrupt(ResPinsForArduino + 36, ResPinsForArduino + 35, onEncChange0);
  switches.changeEncoderPrecision(50000, 25000);

  /* setting up extra encoders. Make sure they are all on the same expander
    HardwareRotaryEncoder* EncObj1 = new HardwareRotaryEncoder(ResPinsForArduino + 37, ResPinsForArduino + 45, onEncChange1);
    switches.setEncoder(1, EncObj1);
    switches.changeEncoderPrecision(1, MaxEncValList[1], EncValList[1]);

    HardwareRotaryEncoder* EncObj2 = new HardwareRotaryEncoder(ResPinsForArduino + 39, ResPinsForArduino + 47, onEncChange2);
    switches.setEncoder(2, EncObj2);
    switches.changeEncoderPrecision(2, MaxEncValList[2], EncValList[2]);

    HardwareRotaryEncoder* EncObj3 = new HardwareRotaryEncoder(ResPinsForArduino + 38, ResPinsForArduino + 46, onEncChange3);
    switches.setEncoder(3, EncObj3);
    switches.changeEncoderPrecision(3, MaxEncValList[3], EncValList[3]);

    HardwareRotaryEncoder* EncObj4 = new HardwareRotaryEncoder(ResPinsForArduino + 35, ResPinsForArduino + 43, onEncChange4);
    switches.setEncoder(4, EncObj4);
    switches.changeEncoderPrecision(4, MaxEncValList[4], EncValList[4]);

    HardwareRotaryEncoder* EncObj5 = new HardwareRotaryEncoder(ResPinsForArduino + 34, ResPinsForArduino + 42, onEncChange5);
    switches.setEncoder(5, EncObj5);
    switches.changeEncoderPrecision(5, MaxEncValList[5], EncValList[5]);

    HardwareRotaryEncoder* EncObj6 = new HardwareRotaryEncoder(ResPinsForArduino + 33, ResPinsForArduino + 41, onEncChange6);
    switches.setEncoder(6, EncObj6);
    switches.changeEncoderPrecision(6, MaxEncValList[6], EncValList[6]);

    HardwareRotaryEncoder* EncObj7 = new HardwareRotaryEncoder(ResPinsForArduino + 32, ResPinsForArduino + 40, onEncChange7);
    switches.setEncoder(7, EncObj7);
    switches.changeEncoderPrecision(7, MaxEncValList[7], EncValList[7]);
  */

  // Setting the LEDs of the color push buttons (connected to Arduino directly) to Output
  ioDevicePinMode(MultiIo, 4, OUTPUT);
  ioDevicePinMode(MultiIo, 5, OUTPUT);

  // Getting button states at start
  if (switches.isSwitchPressed(106)) {
    EnableState = 1;
  }
  if (switches.isSwitchPressed(107)) {
    OnOffState = 1;
  }

  uint8_t taskId = taskManager.scheduleFixedRate(2000, ReportIn);
  Serial.print( "task ID: " );
  Serial.println(taskId);


}

void loop() {
  taskManager.runLoop();
}

Author: davetcc
24/05/2020 15:47:55
That's a lot of code to try and debug. It's very hard to debug that much code at once. Couple of things I noticed, firstly, there seems to be a lot of expanders sharing the same interrupt pin. I've never tried 64 pins / 8 expanders sharing one interrupt pin.

I'm assuming at some point it was working and then stopped working.

What I would do, is keep commenting out functionality in the code until you get back to a working solution. Leave all the hardware in place and try to get functions working in isolation, by commenting / uncommenting sections of code. Maybe just one switch and one rotary encoder, get that working and then introduce another encoder and so on. I would work through all the pin numbering that you are using carefully. You may be hitting some limitations somewhere but with such a complex sketch it's really hard to determine what's going wrong.




Register / Login  |  Desktop view  |  Jump to top of page