So marvellous code, illustrations and explanation I hope I can do just 50% of that.
That’s why I have to drop everything I’m currently doing to add some little mods to your code.
Hope you don’t mind.
#define BLYNK_PRINT Serial
#include <ESP8266WiFi.h>
#include <BlynkSimpleEsp8266.h>
#include <Wire.h>
#include "Adafruit_MCP23017.h" // https://github.com/adafruit/Adafruit-MCP23017-Arduino-Library
char auth[] = "REDACTED";
char ssid[] = "REDACTED";
char pass[] = "REDACTED";
// For more info, See https://community.blynk.cc/t/using-the-mcp23017-io-expansion-board/44525
// Connect pin SCL of the first expander (mcp_A) to SCL (D1)
// Connect pin SDA of the first expander (mcp_A) to SDA (D2)
// Connect pin INTA or INTB of the first expander (mcp_A) to ESP8266 pin D5 (GPIO14)
// don't solder A0,A1,A2 (default ID of 7) on the first expander (mcp_A)
// Connect 8 physical momentary push buttons to mcp_A pins 0 to 7 inclusive (labelled PA0 to PA7), with the other side of the button connected to GND
// Connect an 8-way Active LOW relay board to mcp_A pins 8 to 15 inclusive (labelled PB0 to PB7 inclusive)
// Attach Blynk button widgets (in Switch mode) to virtual pins 0 to 7 inclusive. These mirror the physical momentary buttons of mcp_A pins PA0 to PA7
// Solder 0.1" pitch pins to the connections at the far end of mcp_A and
// connect the corresponding wires from mcp_B on to these (INTB to INTB, INTA to INTA etc)
// solder A0 (to give an ID of 6) on the second expander (mcp_B)
// Connect 8 physical momentary push buttons to mcp_B pins 0 to 7 inclusive (labelled PA0 to PA7), with the other side of the button connected to GND
// Connect a second 8-way Active LOW relay board to mcp_B pins 8 to 15 inclusive (labelled PB0 to PB7 inclusive)
// Attach Blynk button widgets (in Switch mode) to virtual pins 8 to 15 inclusive. These mirror the physical momentary buttons of mcp_B pins PA0 to PA7
// If additional mcp boards are added (C, D, E etc) then connectors need to be soldered to the previous board in the daisy chain to allow them to connect together
// and each board needs to have a unique I2C address/ID, see below for details..
/*
The begin() param can be 0 to 7, the default param is 7 (no solder bridges on A0, A1 & A2) giving an address of 0x27.
Addr(BIN) Addr(hex) ID A2 A1 A0
010 0111 0x27 7 0 0 0 <--- Default for WaveShare Board
010 0110 0x26 6 0 0 1
010 0101 0x25 5 0 1 0
010 0100 0x24 4 0 1 1
010 0011 0x23 3 1 0 0
010 0010 0x22 2 1 0 1
010 0001 0x21 1 1 1 0
010 0000 0x20 0 1 1 1
*/
#define MCP23017_I2C_ADDRESS_0 0x20
#define MCP23017_I2C_ADDRESS_1 0x21
#define MCP23017_I2C_ADDRESS_2 0x22
#define MCP23017_I2C_ADDRESS_3 0x23
#define MCP23017_I2C_ADDRESS_4 0x24
#define MCP23017_I2C_ADDRESS_5 0x25
#define MCP23017_I2C_ADDRESS_6 0x26 // solder pad A0 bridged
#define MCP23017_I2C_ADDRESS_7 0x27 // default address, no solder pads bridged
#define MCP23017_ID_0 ( MCP23017_I2C_ADDRESS_0 & 0x07 )
#define MCP23017_ID_1 ( MCP23017_I2C_ADDRESS_1 & 0x07 )
#define MCP23017_ID_2 ( MCP23017_I2C_ADDRESS_2 & 0x07 )
#define MCP23017_ID_3 ( MCP23017_I2C_ADDRESS_3 & 0x07 )
#define MCP23017_ID_4 ( MCP23017_I2C_ADDRESS_4 & 0x07 )
#define MCP23017_ID_5 ( MCP23017_I2C_ADDRESS_5 & 0x07 )
#define MCP23017_ID_6 ( MCP23017_I2C_ADDRESS_6 & 0x07 ) // solder pad A0 bridged
#define MCP23017_ID_7 ( MCP23017_I2C_ADDRESS_7 & 0x07 ) // default address, no solder pads bridged
typedef struct
{
Adafruit_MCP23017 mcp;
int device_ID;
} Adafruit_MCP23017_Array;
// To add more devices, just add more instances of Adafruit_MCP23017 and entries in Adafruit_MCP23017_Array
// For example, see the commented lines
// Create the instances of the mcp objects
Adafruit_MCP23017 mcp_0;
Adafruit_MCP23017 mcp_1;
// Uncomment this and the below line to add one more device and so on. Remember to change device_ID
//Adafruit_MCP23017 mcp_2;
Adafruit_MCP23017_Array mcpArray[ ] =
{
{ mcp_0, MCP23017_ID_7 },
{ mcp_1, MCP23017_ID_6 },
// this line
//{ mcp_2, MCP23017_ID_5 },
};
#define NUM_MCP23017 ( sizeof(mcpArray) / sizeof(Adafruit_MCP23017_Array) )
// duplicate the above code if additional mcp boards are added to the daisy chain
// Define the interrupt pin on the MCU
int esp_interrupt_pin = 14; // GPIO14 (D5)
volatile unsigned long last_interrupt; // Used to store the time in millis of the last interrupt, for debouncing
int debounce_delay = 100; // The minimum delay in milliseconds between button presses, for debouncing
volatile int mcp_board; // when an interrupt is detected and decoded the device which generated the interrupt is stored here
volatile int mcp_device_ID; // when an interrupt is detected and decoded the device which generated the interrupt is stored here
volatile uint8_t pin_num; // when an interrupt is detected and decoded the pin which generated the interrupt is stored here
// Define friendly names for the pins...
// These mcp_A pins will have physical buttons connected to them, with the other side connected to ground.
// the pins will be pulled-up when we declare them later...
enum
{
mcp_pin_P0 = 0,
mcp_pin_P1,
mcp_pin_P2,
mcp_pin_P3,
mcp_pin_P4,
mcp_pin_P5,
mcp_pin_P6,
mcp_pin_P7,
mcp_pin_P8,
mcp_pin_P9,
mcp_pin_P10,
mcp_pin_P11,
mcp_pin_P12,
mcp_pin_P13,
mcp_pin_P14,
mcp_pin_P15,
mcp_pin_max
} MCP23017_PIN;
#define NUM_RELAY_PER_MCP23017 ( mcp_pin_max / 2 )
// duplicate the above definitions if additional mcp boards are added to the daisy chain
BLYNK_CONNECTED()
{
// This code runs when the MCU connects or re-connects to the Blynk server
// We want to get the latest widget switch values and update the relays so that they match this
// Loop through pins 0-15 and do a Blynk.syncVirtual
// If more pins are used that relate to additional mcps, or different patterns of virtual pins are used on the existing mcps
// the this for loop range will need to be adjusted...
// Loop through pins 0-NUM_RELAY_PER_MCP23017 of NUM_MCP23017 boards, and do a Blynk.syncVirtual
for (int board = 0; board < NUM_MCP23017; board++)
{
for (int v_pin = 0; v_pin < NUM_RELAY_PER_MCP23017; v_pin++)
{
Blynk.syncVirtual(v_pin); // This forces the BLYNK_WRITE_DEFAULT callback function to trigger for each virtual pin in turn
}
}
}
BLYNK_WRITE_DEFAULT()
{
int widget_pin = request.pin; // Which virtual pin triggered this BLYNK_WRITE_DEFAULT callback?
int widget_value = param.asInt(); // Get the value from the virtual pin (O = off, 1 = on)
// if it was a virtual pin in the range 0-7 then it's a command meant for mcp_A
// if it was a virtual pin in the range 8-15 then it's a command meant for mcp_B
// Write the opposite state to the pin on mcp_A to toggle the relay
mcpArray[ (int) (widget_pin / NUM_RELAY_PER_MCP23017) ].mcp.digitalWrite(widget_pin + NUM_RELAY_PER_MCP23017, !widget_value);
}
void clear_mcp_interrupts()
{
for (int board = 0; board < NUM_MCP23017; board++)
{
for (int mcp_pin = mcp_pin_P0; mcp_pin <= mcp_pin_P7; mcp_pin++) // Loop through the 8 pins on mcp_A that have interrupts attached
{
mcpArray[ board ].mcp.digitalRead(mcp_pin); // Read the pin to clear the interrupt register for that pin
}
}
}
// Interrupt handler for pins mcb_A pins 0-7 and mcb_B pins 0-7
void ICACHE_RAM_ATTR intCallBack()
{
noInterrupts();
// If the interrupt occurred on mcp_A then reading the mcp_A interrupt register will return the pin number
// (0-15 or 0-7 in this case as we aren't using interrupts on pins 8-15),
// otherwise it will return 255.
// Therefore a value >15 means that the interrupt didn't occur on mcp_A but on one of the other mcp boards in the daisy chain so we
// repeat the process for each board until we get a value in the range of 0 to 15
// To later reference which board the interrupt came from we use mcp_device_ID
for (int board = 0; board < NUM_MCP23017; board++)
{
if (mcpArray[ board ].mcp.getLastInterruptPin() < mcp_pin_max)
{
pin_num = mcpArray[ board ].mcp.getLastInterruptPin();
mcp_device_ID = mcpArray[ board ].device_ID;
mcp_board = board;
break;
}
}
if (millis() - last_interrupt >= debounce_delay) // Debounce routine
{
// for debugging only...
Serial.println("_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-");
Serial.println("A button has been pressed!!!");
Serial.print("Device = ");
Serial.println(mcp_device_ID);
Serial.print("Pin = ");
Serial.println(pin_num);
Serial.println("_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-");
Serial.println();
process_button_press(); // Call the function that will activate the correct relay etc.
}
last_interrupt = millis();
interrupts();
clear_mcp_interrupts(); // Call the function that clears all of the interrupt registers
}
void process_button_press()
{
// This code assumes that the switch on mcp_A pin 0 controls the relay on mcp_A pin 8 (= NUM_RELAY_PER_MCP23017)
// so we just add 8 to the number of the pin that triggered the interrupt
int current_pin_state = mcpArray[ mcp_board ].mcp.digitalRead(pin_num + NUM_RELAY_PER_MCP23017); // Get the current state of the pin
mcpArray[ mcp_board ].mcp.digitalWrite(pin_num + NUM_RELAY_PER_MCP23017, !current_pin_state); // Write the opposite state to the pin to toggle it
// This code assumes that we have Blynk button widgets (in Switch mode) on virtual pins that are numbered the same
// as the ones used for the physical buttons (V0 = MCP Pin 0, V1 = MCP Pin 1 etc)
// As the relays are Active LOW then we need to write the opposite state to the Blynk switch widget than we do to the MCP pin...
// Relay = Off (HIGH or 1) then Blynk button widget = 0
// Relay = On (LOW or 0) then Blynk button widget = 1
Blynk.virtualWrite(pin_num, current_pin_state);
// additional case statements would be added if more expander boards are added to the daisy chain,
// assuming that interrupts are used on those additional boards
}
void setup()
{
Serial.begin(115200);
pinMode(esp_interrupt_pin, INPUT); // Initialise the MCU pin used for the Interrupt line (INTA or INTB on the Waveshare board)
for (int board = 0; board < NUM_MCP23017; board++)
{
mcpArray[ board ].mcp.begin(mcpArray[ board ].device_ID);
// We mirror INTA and INTB, so that only one line is required between MCP and Arduino for int reporting
// The INTA/B will not be Floating
// INTs will be signalled with a LOW
mcpArray[ board ].mcp.setupInterrupts(true, false, LOW);
}
// duplicate the above code if more expanders are added to the daisy chain (assuming that more pins with interrupts are needed on these boards)
// In this next section I could have used the friendly names (0-15) defined in the declarations, but chose to stick with the
// names screen-printed on the mcp board to aid wiring and debugging.
// In the following blocks of code we declare each of our pins that will be used for interrupts (Pins 0-7) on mcb_A
// If other pins have interrupts assigned to them then they must be added to the clear_mcp_interrupts() function
for (int board = 0; board < NUM_MCP23017; board++)
{
for ( int mcp_pin = mcp_pin_P0; mcp_pin < mcp_pin_max; mcp_pin++)
{
// configuration for a button PA0 on mcb_A
// interrupt will trigger when the pin is taken to ground by a pushbutton
mcpArray[ board ].mcp.pinMode(mcp_pin, INPUT);
mcpArray[ board ].mcp.pullUp(mcp_pin, HIGH); // turn on a 100K pullup internally
mcpArray[ board ].mcp.setupInterruptPin(mcp_pin, FALLING);
}
}
// duplicate the above code if more expander boards are added to the daisy chain and use the correct
// mode definition etc for the type of use for that pin (input, output, input with interrupt etc)
// If other board/pin combinations have interrupts assigned to them then they must be added to the clear_mcp_interrupts() function
clear_mcp_interrupts(); // Call the function that clears all of the interrupt registers
Blynk.begin(auth, ssid, pass);
// Here we attach an interrupt to the MCU pin that will be used to listen for interrupts from the mcp_A...
attachInterrupt(digitalPinToInterrupt(esp_interrupt_pin), intCallBack, FALLING);
// It's not necessary to do this for the other mcp's, as it's only mcp_A that's directly connected to the NodeMCU's interrupt pin
}
void loop()
{
Blynk.run();
}