MENWIZ: how do I relocate nav buttons to I2C port expander

I am planning to add menus to my project but am running out of I/O pins. As LCD is already on I2C bus, I am adding another 8574 port expander and moving navigation buttons to it.

I am new to C++ so please bear with me. For tests, I loaded MENWIZ quick_tour sketch as is and it runs fine with default button pins.
Next, I redefined pin numbers to reflect the expander I/O pin range (i.e. 0 … 7), set the expander’s I2C address (0x26) and, using included I2CIO library, created ‘remIO’ object of I2CIO class.
Using I2CIO class methods, I initialised the ‘remIO’ with expander’s address and, reading the status of an expander pin, turned on arduino LED (pin 13) while the pushbutton connected to the pin is depressed. This worked as designed and confirms the hardware is functional. Here are relevant parts of the modified code (with the actual quick_tour sketch code removed for clarity).

//The full code is in library example file Quick_tour.ino
#include <Wire.h>
#include <LCD.h>
#include <LiquidCrystal_I2C.h>
#include <buttons.h>
#include <MENWIZ.h>
#include <EEPROM.h>

// DEFINE PORT EXPANDER PINS FOR NAVIGATION BUTTONS
#define UP_BUTTON_PIN       2
#define DOWN_BUTTON_PIN     3
#define LEFT_BUTTON_PIN     4 
#define RIGHT_BUTTON_PIN    5
#define CONFIRM_BUTTON_PIN  6
#define ESCAPE_BUTTON_PIN   7

// create port expander obj using I2CIO lib
I2CIO remIO;
byte IOaddr = 0x26; // port expander's hardware I2C address

void setup(){
  remIO.begin(IOaddr); // initialize remIO port expander at IOaddr
  remIO.portMode(INPUT);  // set all pins as inputs
}

void loop(){
// turn ON on-board LED while 'right' button pressed 
// read using I2CIO method and write using arduino method
  (remIO.digitalRead(RIGHT_BUTTON_PIN))?digitalWrite(13,LOW):digitalWrite(13,HIGH); 
  }

Looking at the quick_tour sketch, it appears that the navigational buttons are only accessed by methods of the ‘Button’ class, notably by Button::check() which reads the pin status using arduino’s digitalRead(pin) method.

byte Button::check() {
	if (digitalRead(pin)) {
		switch (mode) {

Replacing ‘digitalRead(pin)’ with ‘remIO.digitalRead(pin)’ compiled with errors (i.e. ‘remIO not defined in this scope’). Including ‘I2CIO.h’ into ‘buttons.h’ or into ‘buttons.cpp’ made no difference (and anyways, ‘I2CIO.h’ is already included in ‘LiquidCrystal_I2C.h’ included earlier).

How do I ‘convince’ the program to use I2CIO class methods instead of Arduino methods?

No takers…

OK, I found a solution on my own. I’ll detail it here (such as it is) as it may be useful for those in similar predicament.

I downloaded the MENWIZ from GitHub - brunialti/MENWIZ: ARDUINO LCD menu library: short user code to manage complex menu structures. All libraries used are included. As a test bed, I used the quick_tour.ino from the \examples folder.

Here is what I did:

  1. redefine the pins for navigation buttons (in range 0 …7)
// DEFINE PORT EXPANDER PINS FOR NAVIGATION BUTTONS
#define UP_BUTTON_PIN       1  // was 9
#define DOWN_BUTTON_PIN     2  // was 10
#define LEFT_BUTTON_PIN     4  // was 7
#define RIGHT_BUTTON_PIN    5  // was 8
#define CONFIRM_BUTTON_PIN  6  // was 12
#define ESCAPE_BUTTON_PIN   7  // was 11
  1. Create and initialize port expander object (here ‘remIO’) using I2CIO library
// create port expander obj using I2CIO lib
I2CIO remIO;
byte IOaddr = 0x26; // port expander's hardware I2C address
byte portIn = 0; // port expander's inputs

void setup(){
  remIO.begin(IOaddr); // initialize remIO port expander at IOaddr
  remIO.portMode(INPUT);  // set all pins as inputs
  1. Read the port expander data continuously
void loop(){
  portIn = remIO.read(); // scan port expander continuously
  1. Modify the (MENWIZ class) ‘navButtons’ method by adding the pointer to port expander’s data as argument (here the ‘portIn’ byte)

In quick_tour.ino (original code commented out) :

//  tree.navButtons(UP_BUTTON_PIN,DOWN_BUTTON_PIN,LEFT_BUTTON_PIN,RIGHT_BUTTON_PIN,ESCAPE_BUTTON_PIN,CONFIRM_BUTTON_PIN);
  tree.navButtons(&portIn,UP_BUTTON_PIN,DOWN_BUTTON_PIN,LEFT_BUTTON_PIN,RIGHT_BUTTON_PIN,ESCAPE_BUTTON_PIN,CONFIRM_BUTTON_PIN);

In MENWIZ.h :

  void     navButtons(byte*,int,int,int,int,int,int); // was navButtons(int,int,int,int,int,int);
  void     navButtons(byte*,int,int,int,int); // was navButtons(int,int,int,int);

In MENWIZ.cpp :

void menwiz::navButtons(byte*IO,int btu,int btd,int bte,int btc){

and:

void menwiz::navButtons(byte*IO,int btu,int btd,int btl,int btr,int bte,int btc){
  1. While you are in MENWIZ.cpp, add (actually copy) pointer to buttons ‘assign’ method calls (calls are from within ‘navButtons’ methods) as well as delete calls to ‘turnOnPullUp’ method (not needed as 8574 has always active built-in pullups) :
/*
  if(btu!=0){btx->BTU.assign(btu);  btx->BTU.setMode(OneShot);  btx->BTU.turnOnPullUp();} 
  if(btd!=0){btx->BTD.assign(btd);  btx->BTD.setMode(OneShot);  btx->BTD.turnOnPullUp();}
  if(bte!=0){btx->BTE.assign(bte);  btx->BTE.setMode(OneShot);  btx->BTE.turnOnPullUp();} 
  if(btc!=0){btx->BTC.assign(btc);  btx->BTC.setMode(OneShot);  btx->BTC.turnOnPullUp();} 
*/
  if(btu!=0){btx->BTU.assign(IO, btu);  btx->BTU.setMode(OneShot);} 
  if(btd!=0){btx->BTD.assign(IO, btd);  btx->BTD.setMode(OneShot);} 
  if(bte!=0){btx->BTE.assign(IO, bte);  btx->BTE.setMode(OneShot);} 
  if(btc!=0){btx->BTC.assign(IO, btc);  btx->BTC.setMode(OneShot);}

as well as

/*
  if(btu!=0){btx->BTU.assign(btu);  btx->BTU.setMode(OneShot);  btx->BTU.turnOnPullUp();}
  if(btd!=0){btx->BTD.assign( btd);  btx->BTD.setMode(OneShot);  btx->BTD.turnOnPullUp();}
  if(btl!=0){btx->BTL.assign( btl);  btx->BTL.setMode(OneShot);  btx->BTL.turnOnPullUp();}
  if(btr!=0){btx->BTR.assign(btr);  btx->BTR.setMode(OneShot);  btx->BTR.turnOnPullUp();}
  if(bte!=0){btx->BTE.assign(bte);  btx->BTE.setMode(OneShot);  btx->BTE.turnOnPullUp();}
  if(btc!=0){btx->BTC.assign(btc);  btx->BTC.setMode(OneShot);  btx->BTC.turnOnPullUp();}
*/
  if(btu!=0){btx->BTU.assign(IO, btu);  btx->BTU.setMode(OneShot);}
  if(btd!=0){btx->BTD.assign(IO, btd);  btx->BTD.setMode(OneShot);}
  if(btl!=0){btx->BTL.assign(IO, btl);  btx->BTL.setMode(OneShot);}
  if(btr!=0){btx->BTR.assign(IO, btr);  btx->BTR.setMode(OneShot);}
  if(bte!=0){btx->BTE.assign(IO, bte);  btx->BTE.setMode(OneShot);}
  if(btc!=0){btx->BTC.assign(IO, btc);  btx->BTC.setMode(OneShot);}
  1. Add pointer argument to Button class ‘assign’ method:

In buttons.h :

//	void assign(byte pin);
	void assign(byte*, byte pin);

as well as declare pointer variable in ‘private’ section:

	byte *_io;  // add place to hold pointer

and while you are there comment out pull up methods (optional):

//	void turnOnPullUp();   - not needed for port expander
//	void turnOffPullUp();  - not needed for port expander

In buttons.cpp initialize pointer in constructors:

Button::Button() { 
	previous = false;
	mode = OneShot;
	hold_timer = millis();
	refresh_timer = millis();
	hold_level = 1000; // 1 second
	hold_refresh = 100; // 100 ms
	pin = 0;
	_io = NULL; // pointer to port expander data
}
Button::Button(byte mode_v) {
	previous = false;
	mode = mode_v;
	hold_timer = millis();
	refresh_timer = millis();
	hold_level = 1000;
	hold_refresh = 100;
	pin = 0;
	_io = NULL; // pointer to port expander data
}

add pointer argument to assign method and comment out ‘pinMode’ (and pull up methods)

void Button::assign(byte*IO, byte pin_v) { 
	pin = pin_v;
        _io = IO;  // save pointer to data (read from port expander in the main sketch)
//	pinMode(pin, INPUT); this is now handled by 'remIO.portMode(INPUT);' in the main sketch
}
// void Button::turnOnPullUp() { if (pin) digitalWrite(pin, HIGH); }
// void Button::turnOffPullUp() { if (pin) digitalWrite(pin, LOW); }
  1. and finally, use the buttons attached to port expander pins:

In buttons.cpp

byte Button::check() {
//	if (digitalRead(pin)) - replaced by 'if (*_io & (1 << pin))' below
	if (*_io & (1 << pin)) {
		switch (mode) {

If somebody has a better or cleaner way, please let me know