ESP8266-01 and MCP23017 port expander

Pete:

Hopefully I modified embedded code as you kindly requested. (very new here, sorry)

LA

Pete:

I am challenged with modifying a 4-channel relay project in hopes of expanding to 12- channels using ESP8266-01 and MCP23017 port expander. Using your guidance, I have attempted to declare the MCP23017 and “pin” numbers/names to a well functioning scheduler/controller (using only the original 4-channels for now), setting up for a total of 12-channels later).

Using the ESP-01, it’s a bit difficult to do much serial monitor tracking, but I have tested other sketches and I am certain all the hardware/wiring is fully functional, using only LED’s to observe the output states of selected pin on the MCP23017.

My feeble attempts compile fine, with few warnings of library issues, so I hope the problem is simple (so I can handle it :slight_smile: )

Your kind guidance is appreciated:

CODE FOLLOWS:


/* MODIFIED C:\Users\REDACTED\Arduino\libraries\Blynk\src WidgetTimeInput.h
  replace “-1” of the code “mWeekdays = -1;” by “0”. You can find the code in line “33” of file “WidgetTimeInput.h” (Version: Aug 2016, respectively 14 Sep 2016).*/
#define BLYNK_PRINT Serial
#define WIFI_LED 10

// define configuration (number of switches and number of timers)
#define SWITCH_CNT 4
#define TIME_CNT 4

#include <ESP8266WiFi.h>
#include <BlynkSimpleEsp8266.h>
#include <TimeLib.h>
#include <WidgetRTC.h>
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include "Adafruit_MCP23017.h"

Adafruit_MCP23017 mcp;

byte mcp_pin_PA0=0;
byte mcp_pin_PA1=1;
byte mcp_pin_PA2=2;
byte mcp_pin_PA3=3;
byte mcp_pin_PA4=4;
byte mcp_pin_PA5=5;
byte mcp_pin_PA6=6;
byte mcp_pin_PA7=7;
byte mcp_pin_PB0=8;
byte mcp_pin_PB1=9;
byte mcp_pin_PB2=10;
byte mcp_pin_PB3=11;
byte mcp_pin_PB4=12;
byte mcp_pin_PB5=13;
byte mcp_pin_PB6=14;
byte mcp_pin_PB7=15;

//char auth[] = "REDACTED"; //LOLIN D1 Mini
char auth[] = "REDACTED";   //ESP8266-01 with MCP23017 port expander
char ssid[] = "REDACTED";
char pass[] = "REDACTED";

#define SCREEN_WIDTH 64 // OLED display width, in pixels
#define SCREEN_HEIGHT 48 // OLED display height, in pixels
// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
#define OLED_RESET 15  // GPIO15
Adafruit_SSD1306 display(OLED_RESET);

byte switch_pins[] = {mcp_pin_PA0 , mcp_pin_PA1, mcp_pin_PA2 , mcp_pin_PA3}; // number of MCP23017 "gpio" to be used as switch/relay control

bool switch_default[] = {HIGH, HIGH, HIGH, HIGH}; // switches that use reverse polarity should be set to HIGH here

////////////////////////////////////////////////////////////////////////////////////////
// This code can control up to 4 switches                                            //
// For each switch up to 4 schedule start and end times can be configured - (V0..V15) //
// A default duration can be defined per switch                           - (V16..V19)//
// If an end time is not defined at the app the default duration is used              //
// default duration is also used when manually activating the switch       - (V20..V23)//
// for each switch when start time is reached the switch turns on for the duration    //
//                                                                                    //
// maximum duration is used for safety (to limit activation time)          - (V24..V27)//
////////////////////////////////////////////////////////////////////////////////////////
int  start_time_sec[SWITCH_CNT][TIME_CNT];     // array of 4 start times (in seconds) for 12 switches [switch number][schedual timer number]
bool start_valid[SWITCH_CNT][TIME_CNT];        // is the start time valid ?
bool weekdays[SWITCH_CNT][TIME_CNT][8];        // array of 8 days (day 0 not used) for each schedual time
int  active_duration[SWITCH_CNT][TIME_CNT + 1]; // duration per switch per time(in sec)
bool end_valid[SWITCH_CNT][TIME_CNT];          // is the end time valid ?
int  max_duration[SWITCH_CNT];                 // max duration per switch
int auto_off = 1;           // 1 is auto or on

char zone  ;      // HOPEFUL GLOBAL VARIABLE TO DISPLAY ON OLED, I ATTEMPT TO CHANGE VALUE AND ASSIGN TO "ZONE" IN THE "handle switch state" loop

// when activating a switch a timer is set for the configured duration
// when the duration ends the switch is turned off by the timer
// the id of the timer is saved using end_timer_id
// if switch is manually turned off the end_timer_id is used to stop the timer.

// end_timer_id is initialized to 32 (per entry) at the setup section

int end_timer_id[SWITCH_CNT];

// timer object declaration
BlynkTimer timer;

// this code use Real Time Clock widget in the blynk app to keep the clock updated from net
WidgetRTC rtc;


BLYNK_CONNECTED() {
  // Synchronize time on connection
  rtc.begin();
  Blynk.syncAll();
}
WidgetLED led1(V55);

bool ledStatus = auto_off;


// V55 LED Widget is ON or OFF
void LedWidget()
{ ledStatus = auto_off;
  if (ledStatus) {
    led1.on();
  } else {
    led1.off();

  }
}

//////////////////////////////////////////////////////////
// get schedual parameters from App                     //
//////////////////////////////////////////////////////////
void set_time(BlynkParam param, byte switch_no, byte time_no) {

  TimeInputParam t(param);
  // Process start time

  if (t.hasStartTime())
  {

    Serial.println(String("Start: ") +
                   t.getStartHour() + ":" +
                   t.getStartMinute() + ":" +
                   t.getStartSecond());
    Serial.println(String("Start in sec: ") + param[0].asLong() + String(" for switch") + switch_no + String(" time_no: ") + time_no);
    start_time_sec[switch_no][time_no] = param[0].asLong();
    start_valid[switch_no][time_no] = true;

  }
  else
  {
    // Do nothing
    Serial.println(String("No Start Time Given for switch: ") + switch_no + String(" time_no: ") + time_no);
    start_valid[switch_no][time_no] = false;
  }

  //////////////////////////////////////////////////////////////////////////////
  // check if end time is received convert and save it as day time in seconds //
  //////////////////////////////////////////////////////////////////////////////
  if (t.hasStopTime())
  {

    Serial.println(String("Stop: ") +
                   t.getStopHour() + ":" +
                   t.getStopMinute() + ":" +
                   t.getStopSecond());
    Serial.println(String("Stop in sec: ") + param[1].asLong() + String(" for switch") + switch_no + String(" time_no: ") + time_no);

    active_duration[switch_no][time_no] = (param[1].asLong() - start_time_sec[switch_no][time_no]);

    // if end time is smaller than start time this means end time is at next day
    if (active_duration[switch_no][time_no] < 0) active_duration[switch_no][time_no] = 86400 + active_duration[switch_no][time_no];

    Serial.println(String("Stop duration: ") + active_duration[switch_no][time_no]);

    end_valid[switch_no][time_no] = true;


  }
  else // if end time is not defined //
  {
    // Do nothing
    Serial.println(String("No Stop Time Given for switch: ") + switch_no + String(" time_no: ") + time_no);
    end_valid[switch_no][time_no] = false;
  }


  // Process weekdays (1. Mon, 2. Tue, 3. Wed, ...)

  for (int i = 1; i <= 7; i++) {
    if (t.isWeekdaySelected(i)) {
      Serial.println(String("Day ") + i + " is selected");
      weekdays[switch_no][time_no][i] = true;
    }
    else {
      weekdays[switch_no][time_no][i] = false;
    }

  }

  Serial.println();

}
//this reads status of on/off mode and displays on screen
BLYNK_WRITE(V50) {
  int Vpin50 = param.asInt();
  auto_off = Vpin50;
}


// V0..V3 for switch 0, V4..V7 for switch 1 ...
BLYNK_WRITE(V0)  {
  set_time(param, 0, mcp_pin_PA0);
}
BLYNK_WRITE(V1)  {
  set_time(param, 0, mcp_pin_PA1);
}
BLYNK_WRITE(V2)  {
  set_time(param, 0, mcp_pin_PA2);
}
BLYNK_WRITE(V3)  {
  set_time(param, 0, mcp_pin_PA3);
}

BLYNK_WRITE(V4)  {
  set_time(param, 1, mcp_pin_PA0);
}
BLYNK_WRITE(V5)  {
  set_time(param, 1, mcp_pin_PA1);
}
BLYNK_WRITE(V6)  {
  set_time(param, 1, mcp_pin_PA2);
}
BLYNK_WRITE(V7)  {
  set_time(param, 1, mcp_pin_PA3);
}

BLYNK_WRITE(V8)  {
  set_time(param, 2, mcp_pin_PA0);
}
BLYNK_WRITE(V9)  {
  set_time(param, 2, mcp_pin_PA1);
}
BLYNK_WRITE(V10) {
  set_time(param, 2, mcp_pin_PA2);
}
BLYNK_WRITE(V11) {
  set_time(param, 2, mcp_pin_PA3);
}

BLYNK_WRITE(V12) {
  set_time(param, 3, mcp_pin_PA0);
}
BLYNK_WRITE(V13) {
  set_time(param, 3, mcp_pin_PA1);
}
BLYNK_WRITE(V14) {
  set_time(param, 3, mcp_pin_PA2);
}
BLYNK_WRITE(V15) {
  set_time(param, 3, mcp_pin_PA3);
}

// use a slider to define default activation duration (slider count in minute)
BLYNK_WRITE(V16) {
  active_duration[mcp_pin_PA0][TIME_CNT] = param.asInt() * 60;
}
BLYNK_WRITE(V17) {
  active_duration[mcp_pin_PA1][TIME_CNT] = param.asInt() * 60;
}
BLYNK_WRITE(V18) {
  active_duration[mcp_pin_PA2][TIME_CNT] = param.asInt() * 60;
}
BLYNK_WRITE(V19) {
  active_duration[mcp_pin_PA3][TIME_CNT] = param.asInt() * 60;
}


// use a slider to define default max duration (slider count in minute)   ***HARD CODE THIS*** NL
BLYNK_WRITE(V24) {
  max_duration[mcp_pin_PA0] = param.asInt() * 60;
}
BLYNK_WRITE(V25) {
  max_duration[mcp_pin_PA1] = param.asInt() * 60;
}
BLYNK_WRITE(V26) {
  max_duration[mcp_pin_PA2] = param.asInt() * 60;
}
BLYNK_WRITE(V27) {
  max_duration[mcp_pin_PA3] = param.asInt() * 60;
}

/////////////////////////////////////////////////////////////////
// Handle switch events (from app or from scheduler )          //
/////////////////////////////////////////////////////////////////

// turn off switch after active duration ends
// duration number is not important here
void turn_off_switch_no_0() {
  turn_on_off(0, mcp_pin_PA0, 0);
  Blynk.virtualWrite(V20, 0);
  Serial.println(String("timer turn off switch 0 ") );
}
void turn_off_switch_no_1() {
  turn_on_off(0, mcp_pin_PA1, 0);
  Blynk.virtualWrite(V21, 0);
  Serial.println(String("timer turn off switch 1 ") );
}
void turn_off_switch_no_2() {
  turn_on_off(0, mcp_pin_PA2, 0);
  Blynk.virtualWrite(V22, 0);
  Serial.println(String("timer turn off switch 2 ") );
}
void turn_off_switch_no_3() {
  turn_on_off(0, mcp_pin_PA3, 0);
  Blynk.virtualWrite(V23, 0);
  Serial.println(String("timer turn off switch 3 ") );
}

// handle switch state
void turn_on_off(int on_off, byte switch_no , byte time_no) {
  long active_duration_ms ;
  char Time_print[16];

  if ((on_off == 1) && (auto_off == 1)) //auto_off is a slider in app to shut off the program
  {
    // create time as string to print on activation button
    sprintf(Time_print, "%02d:%02d", hour(), minute());

    // turn on the switch (or off if default is on)
    mcp.digitalWrite(switch_pins[switch_no], !switch_default[switch_no]);

    // if end time is valid use the active duration assigned to this time
    // (change to msec will be done later)
    if (end_valid[switch_no][time_no])
      active_duration_ms = ((long)active_duration[switch_no][time_no]);
    else // otherwise use the general time duration
      active_duration_ms = ((long)active_duration[switch_no][4]);


    // max duration smaller than two min is not valid
    if ( (max_duration[switch_no] < 120) | (max_duration[switch_no] > active_duration_ms) )
      active_duration_ms = active_duration_ms * 1000;
    else
      active_duration_ms = ((long)max_duration[switch_no]) * 1000;


    // if new timer is set before another one ended then disable previous timer
    if (end_timer_id[switch_no] != 32) timer.deleteTimer(end_timer_id[switch_no]);

    // turn on switch and set timer
    switch (switch_no) {
      case 0:
        Blynk.setProperty(V20, "onLabel", String(Time_print));
        Blynk.virtualWrite(V20, 1);
        end_timer_id[mcp_pin_PA0] = timer.setTimeout(active_duration_ms, turn_off_switch_no_0);
        zone = 1;
        break;
      case 1:
        Blynk.setProperty(V21, "onLabel", String(Time_print));
        Blynk.virtualWrite(V21, 1);
        end_timer_id[mcp_pin_PA1] = timer.setTimeout(active_duration_ms, turn_off_switch_no_1);
        zone = 2;
        break;
      case 2:
        Blynk.setProperty(V22, "onLabel", String(Time_print));
        Blynk.virtualWrite(V22, 1);
        end_timer_id[mcp_pin_PA2] = timer.setTimeout(active_duration_ms, turn_off_switch_no_2);
        zone = 3;
        break;
      case 3:
        Blynk.setProperty(V23, "onLabel", String(Time_print));
        Blynk.virtualWrite(V23, 1);
        end_timer_id[mcp_pin_PA3] = timer.setTimeout(active_duration_ms, turn_off_switch_no_3);
        zone = 4;
        break;

    }
    Serial.println(String("turn ON switch: ") + switch_no + String(" for duration: ") + active_duration_ms / 60000 + String("min "));
  }
  else
  {
    mcp.digitalWrite(switch_pins[switch_no], switch_default[switch_no]);
    timer.deleteTimer(end_timer_id[switch_no]);
    end_timer_id[switch_no] = 32;
    Serial.println(String("turn OFF switch: ") + switch_no);

    zone = '\0';

  }
}

// set switch state from APP
BLYNK_WRITE(V20) {
  turn_on_off(param.asInt(), mcp_pin_PA0, TIME_CNT);
}
BLYNK_WRITE(V21) {
  turn_on_off(param.asInt(), mcp_pin_PA1, TIME_CNT);
}
BLYNK_WRITE(V22) {
  turn_on_off(param.asInt(), mcp_pin_PA2, TIME_CNT);
}
BLYNK_WRITE(V23) {
  turn_on_off(param.asInt(), mcp_pin_PA3, TIME_CNT);
}

/////////////////////////////////////////////////////////////////////////////////////////////
// the following function is called every 60 seconds by a timer                            //
//  the function checks if a start time is reached and if yes it will call                 //
//   the turn_on_off function (to turn on the switch and set timer for turning off switch) //
/////////////////////////////////////////////////////////////////////////////////////////////
void activetoday() {        // check if schedule #1 should run today

  if (Blynk.connected()) // set wifi led if no connection
  {
    mcp.digitalWrite(WIFI_LED, LOW);
  }
  else
  {
    mcp.digitalWrite(WIFI_LED, HIGH);
  }

  if (year() != 1970) {
    unsigned int nowseconds = ((hour() * 3600) + (minute() * 60) + second());
    int dayadjustment = -1;
    if (weekday() == 1) {
      dayadjustment = 6; // needed for Sunday Time library is day 1 and Blynk is day 7
    }

    for (int switch_cnt = 0;  switch_cnt < SWITCH_CNT; switch_cnt++) {
      for (int timer_cnt = 0;  timer_cnt < TIME_CNT; timer_cnt++) {
        if (start_valid[switch_cnt][timer_cnt] == true) {
          if (weekdays[switch_cnt][timer_cnt][weekday() + dayadjustment] == true) {

            if (nowseconds >= start_time_sec[switch_cnt][timer_cnt]) {

              if (nowseconds < start_time_sec[switch_cnt][timer_cnt] + 90) {
                turn_on_off(1, switch_cnt, timer_cnt);
                Serial.println(String("turn ON switch: ") + switch_cnt);

              }
            }
          }
        }
      }

    }

  }
}

/////////////////////////////////////

// Digital clock display of the time
void clockDisplay()
{
  // You can call hour(), minute(), ... at any time
  // Please see Time library examples for details

  String currentTime = String(hour()) + ":" + minute() + ":" + second();
  String currentDate = String(month()) + "." + day() + "." + year();
  Serial.print("Current time: ");
  Serial.print(currentTime);
  Serial.print(" ");
  Serial.print(currentDate);
  Serial.println();

  // Send DATE and DATE to Blynk APP
  Blynk.virtualWrite(V101, currentTime);
  // Send date to the App--Not used but is available--this will show processor date if it is important to you
  Blynk.virtualWrite(V102, currentDate);
}

void setup() {

  Serial.begin(115200);

  // Initialize the MCP23017
  mcp.begin(7);      // use hardware default address 7

  mcp.pinMode(mcp_pin_PA0, OUTPUT);
  mcp.pinMode(mcp_pin_PA1, OUTPUT);
  mcp.pinMode(mcp_pin_PA2, OUTPUT);
  mcp.pinMode(mcp_pin_PA3, OUTPUT);

  mcp.digitalWrite(mcp_pin_PA0, HIGH);
  mcp.digitalWrite(mcp_pin_PA1, HIGH);
  mcp.digitalWrite(mcp_pin_PA2, HIGH);
  mcp.digitalWrite(mcp_pin_PA3, HIGH);

  // reset output pins used and system variables
  for (int i = 0; i < SWITCH_CNT ; i++) {

    mcp.digitalWrite(switch_pins[i], switch_default[i]); // set switch to default
    end_timer_id[i] = 32;            // reset end timer id's

    for (int j = 0; j < TIME_CNT ; j++)
    {
      end_valid[i][j] = false;       // reset end valid array
    }

  }

  mcp.pinMode(WIFI_LED, OUTPUT);

  Blynk.begin(auth, ssid, pass);
  rtc.begin();  // Begin synchronizing time
  timer.setInterval(10000L, clockDisplay);
  timer.setInterval(60000L, activetoday);  // check every 60s if ON / OFF trigger time has been reached
  setSyncInterval(10 * 60); // Sync interval in seconds (10 minutes)

  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);  // initialize with the I2C addr 0x3C (for the 64x48)

  delay(2000);
  display.clearDisplay();
  display.setTextColor(WHITE);

}

void loop()
{
  Blynk.run();
  timer.run();
  LedWidget();

  // clear display
  display.clearDisplay();

  // display
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.print("TIME");
  display.setTextSize(1);
  display.setCursor(0, 10);
  display.print(String(hour()) + ":" + minute() + ":" + second());
  display.print(" ");
  display.setTextSize(1);
  display.setCursor(0, 30);
  display.print("ZONE");
  display.setTextSize(3);
  display.setCursor(40, 25);
  display.printf("%d", zone);
  display.display();
}

@bear1 please edit your post, using the pencil icon at the bottom, and add triple backticks at the beginning and end of your code so that it displays correctly.
Triple backticks look like this:
```

Pete.

Okay, I’ve moved this to a new topic as I don’t want people to view my “Projects made with…” topic and come across this code and try to use it.

The code isn’t “Blynk friendly” with its cluttered void loop, and I can’t see how the code could possibly function with an ESP-01, when you use GPIOs that aren’t broken-out to ESP-01 pins…

I also don’t understand how you’re managing to run an I2C bus for your physical LCD and the MCP23017 port extender, with the ESP-01 and your current software setup.

Why are you using an ESP-01?

If you’re 100% committed to an ESP-01 then I would try running my original code, and decide which pins you are going to use for the I2C bus and declare these in your setup.

Pete.

Pete:

Apologies to my induced confusion, I have left the OLED segments as I have them in the original D1 mini sketch as I attempt to modify it towards using 12 relays. (OLED maybe later as possible).

As to ESP-01, I am waiting for another batch of LOLIN D1 mini pro’s.

You may have found my mistake already, where it doesn’t look like I have properly declared the GPIO0 and GPIO2 from the lowly EXP-01. I’m chasing that discovery immediately.

Huge thanks,

Pete:

It seems my challenge is mostly in re-identifying former GPIO’s from a LOLIN D1 mini pro, to proper “pin” naming for the MCP23017.

My modified code attempts to follow (closely at least) your conventions where declaring the “mcp” and associated output(s) only to :

mcp_pin_PA0 , mcp_pin_PA1, mcp_pin_PA2 , mcp_pin_PA3

Wherein formerly with D1 mini:

byte switch_pins[] = {12 , 13 , 14 , 15}; // number of gpio to be used as switch/relay control

Then following, to replace all instances of the numbers called in the coding to; “12 > mcp_pin_PA0, … , etc”.

It just doesn’t change the state of any of the outputs of the MCP23017 with a BLYNK command previously associated in code with a GPIO (ex. 12, V20)

Example from lines 374-376:

// set switch state from APP
BLYNK_WRITE(V20) {
turn_on_off(param.asInt(), mcp_pin_PA0, TIME_CNT);

Pete:

Chasing your good code example, it seems the declaration of the D1 & D2 are not shown.

I suspect this is “implied” or embedded in the Adafruit_MCP23017.h library?

Please keep the discussion in this topic, not in the old one. I’ve moved your additional post from that topic over to this one.

I think your wasting your time working with the ESP-01, unless you start doing some micro soldering to the pins on the chip.

You need at least three available pins:

NodeMCU Expansion Board A

D2 (GPIO4) SDA (Blue wire)
D1 (GPIO5) SCL (Yellow wire)
D5 (GPIO14) INTA (Orange wire

and you just don’t have these available on the ESP-01. You also need to have the interrupt handler code in place if you want to use it in the same way as my example.

Pete.

Pete:

Albeit I have no current plans for interrupts (physical switches), I trust your guidance to the limits of the lowly ESP-01, but I have to wait for the “Junk” with my D1 mini’s to arrive from China…

Still curious how you managed to define the connection to D1 & D2 (SCL & SDA) for the NodeMCU, where I am hopeful to assign to D0 & D2 for the ESP-01.

We’re good for now, with sincere appreciation for your wizardry.

These are the default pins for the NodeMCU type boards, so don’t need to be defined.

If you want to use other pins then it may be as simple as adding a wire.begin(sda_pin,scl_pin) command, but it depends on how the the Adafruit library has been written.

Pete.

Pete:

As you suggested (with caveat) I added following in setup.:

Wire.begin(0, 2); //SDA, SCL

It was as “simple”, as that !

All the rest of the coding from your coaching worked flawlessly and BLYNK controls the expected mcp pins.

HUGE APPRECIATION FOR YOUR GUIDANCE AND SUPPORT !!

(the humble ESP-01 is capable of another small victory with more I/O than imagined)

@bear1
Was wondering if you would be willing to share your finalized code and schematic diagram to know if you have used pull-up resistors for ESP-01 ?
Thanks,
Watersoup