Projects

Let There Be Light - Part 2

Automating Plantation Shutters Part 2

Peter Stewart

Issue 78, January 2024

This article includes additional downloadable resources.
Please log in to access.

Log in

An ESP32-based remote controlled servo motor to open and close plantation shutters.

This month, following on from part 1 of "Let There Be Light", I discuss the software for the Plantation Shutter Controller (PSC) project. I look at the software functions and how to configure the PSC via the included Webserver. I also, briefly, look at setting up the Arduino IDE for the ESP32 used in this project.

THE SOFTWARE

The software consists of three files:

  1. PSC_ESP32_HC12_V2.ino (the main module);
  2. PSC_ESP32_Config_V2.h (Header file configuration data and Webserver function prototypes);
  3. PSC_ESP32_V2_WS_Functions.ino (Webserver functions);

Setting up the Arduino IDE

All three files have to be contained in the same folder “PSC_ESP32_HC12_V2” for the Arduino IDE to compile. Before you can compile a program for an ESP32, you need to configure the Arduino IDE. Note, the following is for the Microsoft Windows environment.

Firstly open the Arduino IDE. Select File/Preferences and add https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json to the “Additional Boards Manager URLs”.

Second, open the Boards Manager at ‘Tools/Board: “….”/Board Manager’. Then install “esp32” by Espressif Systems.

Once installed, select the settings shown in the following under “Tools”. Note the Flash Size defaults to 4MB. I have ESP32 Modules with 16 MB so I have to make a few modifications.

Program Description

As usual for Arduino programs, the Libraries (Header Files) used are declared at the beginning of the program. In the main module “PSC_ESP32_HC12_V2.ino”, I have only one Header File, "PSC_ESP32_Config_V2.h" This Header File contains all the Header Files for the Libraries used, plus all the configuration data, data declarations and Function Prototypes. It is worth looking at the contents of this Header File before looking at the Main Module. Below shows a list of all the Libraries used by this program.

// Libraries
#include <WiFi.h>          // ESP32 WiFi functions
#include <OneWire.h>       // Low level functions for DS18B20
#include <DallasTemperature.h>// For DS18B20
#include <ESP32Servo.h>    // Servo Motor functions
#include <Wire.h>          // I2C low level functions
#include <BH1750.h>        // BH1750 functions
#include <WebServer.h>     // Web Server functions
#include <PubSubClient.h>  // MQTT functions
#include <EEPROM.h>        // EEPROM functions
#include "esp32/rom/rtc.h" // RTC&Reset definitions
#include <ArduinoOTA.h>    // OTA functions

In the Header File, this is preceded by the Debug Print definitions I used when debugging the program.

This is followed by all the #defines, constructors and variables used plus the Function Prototypes for the Web Server Functions (See the code in the resources).

Now back to the Main Module, there are a few simple functions. The first function is “OpenCloseShutter()”. Two parameters are passed into this function.

The first indicates whether the Shutter is to be Opened or Closed. The second parameter is the Maximum Angle to which a Shutter is opened. The first thing the function performs is enabling power to the Servo. Note, this is a function from my early versions, as the comment refers to a Relay, whereas this has been replaced by a MOSFET.

There is then a short delay to allow the 5 Volt Voltage Regulator to stabilise. Next the Servo connection is set up. I have also used the option to set the minimum and maximum pulse widths (in microseconds) sent to the Servo.

I derived these numbers by testing the MG996R Servo Motor for different angles. If you use a different Servo Motor, you may want to vary these values. Once the Shutter is Opened or Closed, the state is saved and the Servo is disabled and powered down.

void OpenCloseShutter( int OpenClose, int MaxAngle )
{
  int pos;
  digitalWrite( SERVO_PWR, HIGH);  
  // Turn on relay to enable power to Servo
  delay(500);                
  // Allow time for everything to settle
  myservo.attach(SERVO_PIN,500,2200);  // attaches 
  // the servo on GPIO14 to the servo object 
  delay(50);
  if(OpenClose == SHUTTERCLOSED)
  {
    for (pos = MaxAngle; pos >0; pos--)
    {
      myservo.write(pos);   // Closing Shutter
      delay(25); // delay to allow servo to move
    }
    StoredShutterState = SHUTTERCLOSED;
    PRINTLN("Shutter now CLOSED!");
  }
  else
  {
    for (pos = 0; pos < MaxAngle; pos++)
    {
      myservo.write(pos);   // Opening Shutter
   delay(25);   // delay to allow servo to move
    }
    StoredShutterState = SHUTTEROPEN;
    PRINTLN("Shutter now OPEN!");
  }
  delay(500);     // delay 1/2 seconds to allow 
              // servo to get to position
  myservo.detach(); // Disable servo
  delay(100);
  digitalWrite( SERVO_PWR, LOW);  
// Turn off relay to disable power to Servo
}

Next is the “Buzzer_Beeps()” function. From the input parameter, the buzzer will make the required number of beeps. Initially, this function used the “Tone” Library. However, I found during debugging that a warning was issued whenever a “beep” was required. The Buzzer still provided a beep, but I decided to make my own equivalent function.

Basically, I worked out the half period of a 4kHz signal – “BUZZ_PERIOD” (4kHz is the resonant frequency of the selected Buzzer). This is used to generate a square wave

on the pin (Buzz_Pin) that drives the Buzzer. I then tested the buzzer by driving it with a number of squarewaves - “BUZZ_REPS”, to get a satisfactory “beep” sound.

void Buzzer_Beeps(int num)
{
  pinMode(Buzz_Pin, OUTPUT);
  for (int i = 0; i < num; i++)
  {
    for(int j =0; j<BUZZ_REPS; j++)
    {
      digitalWrite(Buzz_Pin,HIGH);
      delayMicroseconds(BUZZ_PERIOD);
      digitalWrite(Buzz_Pin,LOW);
      delayMicroseconds(BUZZ_PERIOD);
    }
//    tone(Buzz_Pin, Buzz_Freq);
//    delay(125);
//    noTone(Buzz_Pin);
    delay(250);
  }
}

The next function is only used when debugging is enabled. “print_wakeup_reason()” prints the reason for the processor waking up from “Deep Sleep”.

Next is the function to set up the HC-12 433 MHz transceiver. “setup_HC12()” uses the second UART on the ESP32 to first set the channel to be used for communications between the PSC HC-12 and the Remote Control Unit HC-12. Once the command string is sent to the HC-12, the ESP32 waits until the “OKrn“ is received from the HC-12 Module acknowledging the command. Next the command to set the Low Current mode is issued, “AT+FU2” and also acknowledged.

void setup_HC12( void)
{
  String R_Str;
  // First set channel to CHAN_SET
  Serial2.println("AT+C0"+String(CHAN_SET));
  R_Str = Serial2.readStringUntil('n');
  PRINTL("R_Str 1 is ", R_Str);
  // Then set up for low current mode "FU2"
  Serial2.println("AT+FU2");
  R_Str = Serial2.readStringUntil('n');
  PRINTL("R_Str 2 is ", R_Str);
}

The next function “Set_HC12_to_4800Baud()” is important as it ensures the HC-12 Baud Rate is set to 4800 Baud. The default of a new HC-12 is 9600 Baud. To make any changes to the functioning of the HC-12 Transceiver Module, the Module had to be first put into “AT Command Mode”. This is performed by the ESP32 by placing a low on the “HC12_SET” pin, which is connected to the “SET” pin on the HC-12 Module.

Then by sending the standard “AT” command to the HC-12 Module (noting that the ESP32 Serial Port 2 has already been set to 4800 baud), the ESP32 waits to receive the “OKrn” reply. If all is good, the number of bytes received, “HC12_cnt” will be greater than or equal to zero and the HC-12 Module can now be configured by “setup_HC12()”. If “HC12_cnt” is less than zero, a timeout has occurred and the HC-12 Module is most probably set to the default 9600 Baud. If this is the case, the ESP32 reconfigures Serial Port 2 to 9600 Baud.

If this then works, “OK” response received from sending the “AT” command, then the Channel Number and Low Current mode can be set up, “AT+FU2”. Note, by setting the Low Current Mode, the HC-12 will automatically change its Baud rate to 4800 Baud. Once this is complete, the ESP32 changes Serial Port 2 to 4800 Baud and sets Data Mode on the HC-12 Module.

void Set_HC12_to_4800Baud( void)
{
  String R_Str;
  int HC12_cnt =0;
  digitalWrite(HC12_SET, LOW);   
  // Enable AT Cmd Mode
  delay(HC12_CMD_START_DELAY);
  Serial2.println("AT");  
  // First check if set to 9600 baud
  Read_Str = Serial2.readStringUntil('n');
  HC12_cnt = Read_Str.indexOf("OK");
  PRINTL("Read_Str 3 is ", Read_Str);
  if(HC12_cnt >= 0)
  {
    // means baud is set to 4800
    // so set HC-12 to FU2 Mode ie. 
    // 4800baud and Channel 55
    setup_HC12();
  }
  else
  {
      // baud rate is 9600?
    Serial2.flush();
    Serial2.begin(9600);
    delay(100);
    Serial2.println("AT");
    Read_Str = Serial2.readStringUntil('n');
    //    Read_Str.trim();
    PRINTL("Read_Str 4 is ", R_Str);
    HC12_cnt = Read_Str.indexOf("OK");
    if(HC12_cnt >= 0)
    {
      setup_HC12();
    }
    else
    {
      // If not 4800 or 9600 baud, help
      PRINTLN("Something is wrong!!");
    }
    Serial2.flush();
    Serial2.begin(4800);
    delay(100);
  }
  digitalWrite(HC12_SET, HIGH); // Enable Data Mode
  delay(HC12_CMD_END_DELAY);    
  // wait time for HC-12 to be ready
}

When the ESP32 wakes up from “Deep Sleep” and the ESP32 determines the wakeup reason to be due to a “LOW” signal being applied to an I/O Pin (GPIO4 in the design, see Figure 6), which has been caused by the HC-12 receiving a command from the Remote Control Unit (RCU), Function “Get_HC12_Cmd()” is called. Initially, this function assumes no valid command has been received. It then reads the data from the HC-12 Module in a “while” loop, which is exited on a timeout, i.e. nothing received, or an acknowledged command has occurred (“ack_sent” is set to “true”).

In the “while” loop, once something has been received, “Read_str” not empty, it is checked to see if it is a “Broadcast Open" or “Broadcast Close” command. The RCU can issue a “Broadcast Open” or “Broadcast Close” command so that all the shutters will be opened or closed at the same time. If it was not a Broadcast command that was received, the command is then checked against the Shutter Number that was configured during the Web Server setup (more on this later), to determine if the command was for this particular shutter, “cmd_num”. Then, if the command was a Broadcast command or a command for this particular shutter, an acknowledgement is sent to the RCU. The RCU indicates this to the user by illuminating its LED for one second.

If the command received was not for this shutter, “cmd_num” is set to “REM_NO_CMD” and “ack_sent” is set to “true” to break the “while” loop. Whatever “cmd_num” is set to is then returned by this function and the ESP32 processes it appropriately.

int Get_HC12_Cmd( void )
{
  int cmd_num = REM_NO_CMD;
  ack_sent = false;
  hc12_tmr = millis();
  // Allow up to 2 seconds to determine 
  // incoming command from the Remote
  while((millis() < (hc12_tmr + HC12_CMD_WAIT_TIME)) 
&& (!ack_sent))
  {
    Read_Str = Serial2.readStringUntil('n');
    PRINTL("Read_Str is ", Read_Str);
    Read_Str.trim();    // Remove "r"
    PRINTL("Read_Str after trim() ", Read_Str);
    if(Read_Str!="")
    {
      cmd_num = Read_Str.toInt();
  PRINTL("cmd_num after Int conversion ", cmd_num);
      // Check HC12 Command is not a broadcast 
      // command, otherwise process it
      if((cmd_num != REM_CMD_BC_OPEN) && 
(cmd_num != REM_CMD_BC_CLOSE))
      {
        cmd_num = cmd_num - 
((S_EData.ShutterNumber - 1)*NUM_REM_CMDS);
      }
      
      switch(cmd_num)
      {
        case REM_CMD_OPEN:
        case REM_CMD_CLOSE:
        case REM_CMD_RESET:
        case REM_CMD_BC_OPEN:
        case REM_CMD_BC_CLOSE:
          // Acknowledge Remote Broadcast 
          // Commands for this PSC
          Serial2.println("ACK");
          delay(100);
          Serial2.println("ACK");  // send again to make sure
          delay(100);
          PRINTLN("ACK sent");
          ack_sent = true;
          break;
        default:
          PRINTLN("Not a command for this PSC");
          ack_sent = true;    // break while loop
          cmd_num = REM_NO_CMD; 
// Not a command for processing
          break;
      }
    }
    delay(10);
  }
  return cmd_num;
}

The last function before the “Setup()” function is the “Read_Sensors()” function. This initially reads the Battery Voltage via the Resistor Divider (R1/R2 Figure 6) using the ESP32 12-bit ADC. It is then mapped to give a real voltage. Then the Light Sensor (BH1750FVI) is activated as is the Temperature Sensor (DS18B20). The Temperature is first read, then the Light Level. The Light Sensor takes a small amount of time to wakeup, which is why it is read second. These sensor values can then be used by the ESP32 as required.

void Read_Sensors(void)
{
  // Read battery Voltage
  // Max count of 4096 = 9.00 Volts 
  // (scaled to allow 
  // for resistor tolerance)
  //  BatteryVoltage =   
  //  map(analogRead(BatteryPin),443,3990,100,900); 
  BatteryVoltage = map(analogRead(BatteryPin),2717, 3888, 650, 850);
  PRINTS("Battery Voltage is : ");
  PRINTN((float)(BatteryVoltage)/100.0);
  PRINTLN(" V");
  PDELAY(100);
  // Setup for reading BH1750 Light Sensor
  Wire.begin();
  myBH1750.begin(myBH1750.ONE_TIME_HIGH_RES_MODE);
  //Setup for reading DS18B20 Temperature Sensor
  DS18B20.begin();
    DS18B20.setResolution(TEMP_RES);   
// Set Resolution to +/- 0.5C
  DS18B20.requestTemperatures();     
// Request DS18B20 to take a temperature measurement
  delay(DS_SUPT); // Wait SetUP Time before attempting to read temperature
  // Read temperature as Celsius (the default)
  ShutterTemp = DS18B20.getTempCByIndex(0);
  PRINTL("Temperature is ", ShutterTemp);
  // Read Light Level  
  LightLevel = myBH1750.readLightLevel();
  PRINT("Light Level is : ", LightLevel);
  PRINTLN(" lx");
  PDELAY(100);
}

We are now ready to start the ESP32 by looking at the “Setup()” function, which of course is executed on “power up” and “wakeup from Deep Sleep”. Initially, if Debugging is enabled, the Serial Port is initialised, then Serial Port 2, for the HC-12 Module is initialised. EEPROM is initialised for reading and writing. This is followed by determining the wakeup reason. If Debugging is enabled, the wakeup reason is printed via the Serial Port. The reset reason is then determined for both cores of the ESP32. This is used later with the wakeup reason to determine what action the ESP32 is to take. The HC-12 Module is then put in “Tx/Rx” or “Data” Mode. The Buzzer pin (Buzz_Pin) is then setup. Lastly, the Power Control pin (SERVO_PIN) for the Servo Motor is set for power off. Timers used by the Servo Motor functions are initialised and the Servo Motor frequency set to the standard 50Hz.

void setup() {
  // Setup Serial output for debugging
  BPRINT(115200);
  Serial2.begin(4800);   // For HC-12 Module
  delay(100);   // Allow time for Serial to setup
  
  // Initialise EEPROM for reading/writing
  EEPROM.begin(sizeof(S_EData));
  EEPROM.get(0,S_EData);
  PRINTLN("nESP Starting");
  //Increment boot number and print it every reboot
  ++bootCount;
  PRINTL("Boot number: ", bootCount);
    // Determine Wakeup reason
  wakeup_reason = esp_sleep_get_wakeup_cause();
  #ifdef DEBUG
  //Print the wakeup reason for ESP32
  print_wakeup_reason(wakeup_reason);
  #endif
  R_R0 = rtc_get_reset_reason(0);  
  // Core 0 Reset Reason
  R_R1 = rtc_get_reset_reason(1);  
  // Core 1 Reset Reason
  PRINTL("Reset Reason CPU-0 ", R_R[R_R0]);
  PRINTL("Reset Reason CPU-1 ", R_R[R_R1]);
  // Setup for HC-12
  pinMode(HC12_SET, OUTPUT);
  digitalWrite(HC12_SET, HIGH);  
  // Initially set to Tx/Rx Mode
  delay(100);
  
  // Setup pin for Buzzer driver
  pinMode(Buzz_Pin, OUTPUT);
  // Setup Servo Power to off to ensure no 
  // current is being drawn
  pinMode(SERVO_PWR, OUTPUT); 
  // Set Relay GPIO to Output mode
  digitalWrite( SERVO_PWR, LOW);
  
  // Allow allocation of all timers
  ESP32PWM::allocateTimer(0);
  ESP32PWM::allocateTimer(1);
  ESP32PWM::allocateTimer(2);
  ESP32PWM::allocateTimer(3);
  myservo.setPeriodHertz(50);    
// standard 50 hz servo

Now the ESP32 looks at why it has started. Now if the Web Server Setup Configuration has been previously executed (“S_EData.InitCode” = “true”) and the wakeup reason is one of the defined reasons or again “S_EData.InitCode” = “true” and one of the CPU cores has executed a Software Reset, then the Sensors are read. If the wakeup reason is due to a command being received from the HC-12 Module it is processed.

If MQTT parameters have been setup, a MQTT message is sent to the Home Automation System (“SendUpdMQTT()”). Also, all wakeup reasons are disabled. “ShutterMode” is set to “false” to indicate “Manual Mode”. If however, the HC-12 command received is to perform a Shutter close when it is already closed or a Shutter open when it is already open, then “ShutterMode” is set to “true” to indicate “Automatic Mode”. No MQTT message is sent to the Home Automation System in this case as the Shutter state has not changed.

If the HC-12 command received was a “Reset” command, the initialised state “S_EData.InitCode’ is set to zero, ie. not initialised. The ESP32 is then put into a “Deep Sleep” for a second so it can restart and enter into Web Server mode.

If the wakeup reason was not due to the HC-12 receiving a command, then it was most probably due to the “Deep Sleep” timer expiring. In which case, the Shutter state “StoredShutterState” is examined and if it is “Closed” state and the Light Level is above the maximum and the temperature is less than the maximum, then the Shutter is opened. Also, if the MQTT has been set up, a MQTT message is sent to the Home Automation System.

If the Shutter state was not closed, then it must be open. In which case, if the Light Level is below minimum or the temperature was above the maximum, then the Shutter is closed. Also, again, if the MQTT has been set up, a MQTT message is sent to the Home Automation System.

Now, to let me know me know the PSC is still alive, approximately once an hour (“MQTT_Counter > MAX_MQTT_COUNT”), if MQTT has been setup, a MQTT Message is sent to the Home Automation System. The function “SendUpdMQTT()” will also reset “MQTT_Counter”. You may have noticed a number in the brackets of this function, ie “SendUpdMQTT(6)”. By examining MQTT Messages in a MQTT Viewer, eg MQTT.fx, I can determine exactly where in the code the MQTT message has been sent from. This is useful when Debugging has been disabled. Of course, MQTT has to have been setup.

But wait there's more. As the Battery Voltage has been read and can be sent to the Home Automation System as part of the MQTT Message, what if the MQTT is not set up. In this case, the buzzer will beep if the Battery Voltage has fallen to below the “BATMIN” setting, which is currently set to 6.5 Volts.

Now that all this processing has been completed, it is time to put the ESP back into “Deep Sleep”. Firstly, wakeup reason for a command being received from the HC-12 is set, “esp_sleep_enable_ext0_wakeup(GPIO_NUM_4,0);”. Also, it the PSC is in Automatic Mode (“ShutterMode” is set to “true”) the wakeup from deep sleep timer is also set “esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR);”. The ESP32 is then placed into “Deep Sleep”.

if(((S_EData.InitCode == INITCODE) && (wakeup_reason != ESP_SLEEP_WAKEUP_UNDEFINED)) || ((S_EData.InitCode == INITCODE) && ((R_R0 == SW_CPU_RESET) || (R_R1 == SW_CPU_RESET))))
  {
    // Get temperature, light and battery 
    // Voltage readings
    Read_Sensors();
    // if "wakeup_reason" is due to an interrupt 
    // on an External IO
    // The HC12 transceiver has received a 
    // command from the Remote
    if(wakeup_reason == ESP_SLEEP_WAKEUP_EXT0)
    {
      HC12_Cmd = Get_HC12_Cmd();
      PRINTL("HC-12 Command is : ", HC12_Cmd);
      switch (HC12_Cmd)
      {
        case REM_CMD_OPEN:
        case REM_CMD_BC_OPEN:
          // Remote command to open Shutter
          if(StoredShutterState == SHUTTEROPEN)
          {
       // Return to auto control ie. set up 
    // Deep sleep timer if not Broadcast command
      // ESP32 set to wake up by default every 
    // "TIME_TO_SLEEP" seconds
            Buzzer_Beeps(1);  // Acknowledge PSC is in auto mode
            if(HC12_Cmd != REM_CMD_BC_OPEN) ShutterMode = true;
          }
          else
          {
            OpenCloseShutter(SHUTTEROPEN, S_EData.MaxShutterAngle);
            if(S_EData.MQTTEnabled == INITCODE) SendUpdMQTT(1);;
            esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL); // do not want RTC Timer wakeup
            ShutterMode = false;
          }
          break;
        case REM_CMD_CLOSE:
        case REM_CMD_BC_CLOSE:
          // Remote command to close Shutter
          if(StoredShutterState == SHUTTERCLOSED)
          {
               // Return to auto control ie. set up 
    // Deep sleep timer if not Broadcast command
      // ESP32 set to wake up by default 
    // every "TIME_TO_SLEEP" seconds
            Buzzer_Beeps(1);  // Acknowledge PSC is in auto mode
            if(HC12_Cmd != REM_CMD_BC_CLOSE) ShutterMode = true;;
          }
          else
          {
            OpenCloseShutter(SHUTTERCLOSED, S_EData.MaxShutterAngle);
            if(S_EData.MQTTEnabled == INITCODE) SendUpdMQTT(2);
            esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL); // do not want RTC Timer wakeup
            ShutterMode = false;
          }
          break;
        case REM_CMD_RESET:
          // Remote command to Reset ESP32 
        // and start webserver
          S_EData.InitCode = 0;
          EEPROM.put(0, S_EData);
          EEPROM.commit(); 
          Buzzer_Beeps(2);  
        // Acknowledge PSC is to be reset 
          // ESP32 set to wake up in one second
     esp_sleep_enable_timer_wakeup(uS_TO_S_FACTOR);
          PRINTLN("Deep Sleep Starting");
          PDELAY(1000);
          esp_deep_sleep_start();
          break;
        case REM_NO_CMD:
        default:
          PRINTLN("Not a command for this PSC");
          break;
      }
    }
    else
    {
      // Deep Sleep Timer expired (most probably -
    //  External reset?) If Shutter is closed 
    // determine if it can be opened
      if(StoredShutterState == SHUTTERCLOSED)
      {
        if((LightLevel > S_EData.MaxLightLevel) && (ShutterTemp < S_EData.MaxShutterTemp))
        {
          OpenCloseShutter(SHUTTEROPEN, S_EData.MaxShutterAngle);
          if(S_EData.MQTTEnabled == INITCODE)
          {
            SendUpdMQTT(3);
          }
        }
      }
      else
      {
        if((LightLevel < S_EData.MinLightLevel) || (ShutterTemp > S_EData.MaxShutterTemp))
        {
          OpenCloseShutter(SHUTTERCLOSED, S_EData.MaxShutterAngle);
          if(S_EData.MQTTEnabled == INITCODE) 
          {
            SendUpdMQTT(4);
          }
        }
      }
    }
    // If MQTT Counter has exceeded MAX_MQTT_COUNT,
    // send a MQTT Update message, if MQTT Enabled
    if(S_EData.MQTTEnabled == INITCODE)
    {
      if(MQTT_Counter > MAX_MQTT_COUNT)
      {
        SendUpdMQTT(6);
      }
      MQTT_Counter++;
    }
    // If battery Voltage less than allowed battery 
    // minimum voltage, sound beeper
    if(BatteryVoltage < BATMIN) Buzzer_Beeps(1); 
    // ESP32 also setup to wakeup if a low is 
    // detected on GPIO04 which is tied to
    // GPIO16 (RXD2) which is connected to the 
    // HC-12 Transceiver which receives
    // commands from a remote controller
    esp_sleep_enable_ext0_wakeup(GPIO_NUM_4,0); 
    //1 = High, 0 = Low
    if(ShutterMode)
    {
      esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR);
      //Go to sleep now
      PRINTLN("Going to sleep now for "+ String(TIME_TO_SLEEP) + " Seconds");
    }
    PRINTLN("Deep Sleep Starting");
    PDELAY(1000);
    esp_deep_sleep_start();
  }

If the ESP32 has started the “Setup()” function and it was not for the wakeup reasons or reset causes described above, then the EEPROM data is most probably not initialised or at least it has been “reset”, then the Web Server is fired up. This involves setting the ESP32, firstly, as an Access Point (AP), with an SSID of “PSController” and password of “12345678”.

You can change this in the “PSC_ESP32_Config.h” file if you want, if you want stronger security. Then all the Web Server handlers are set up “”server.on(…)” to process the various parameters to be set up for the PSC. Then the Web Server is started, “server.begin()”. Also, if parameters have not been previously set up in EEPROM, they are set to defaults. You may have reset the PSC via the RCU to change some of the parameters, in which case the previous settings will be shown on the various Web pages for setting up the PSC parameters.

Now the Sensors are read in preparation of processing the Web pages in the “Loop()” function.

    else
  {
    // Setup Webserver
    PRINTLN("Setting up Webserver");
    //reset networking
    WiFi.softAPdisconnect(true);
    WiFi.disconnect();          
    delay(1000);
    WiFi.mode(WIFI_AP);
    WiFi.softAPConfig(local_ip, gateway, subnet);
    WiFi.softAP(ssid, password);
    delay(100);
    IPAddress myIP = WiFi.softAPIP();
    PRINTL("AP IP address: ", myIP);
        PDELAY(1000);
    server.on("/", handle_OnConnect);
    server.on("/ButWiFi", handle_WiFi);                    // Handle Wifi Setup button being pressed from Main Menu
    server.on("/WiFiSetup", HTTP_POST, handle_WiFi_Setup); // Handle WiFi Settings being saved
    server.on("/ButMQTT", handle_MQTT);                    // Handle MQTT Setup button being pressed from Main Menu
    server.on("/MQTTSetup", HTTP_POST, handle_MQTT_Setup); // Handle WiFi Settings being saved
    server.on("/ButShutterNo", handle_Shutter_Number);     // Handle Shutter Number Setup button being pressed from Main Menu
    server.on("/ShutterNoSetup", HTTP_POST, handle_Shutter_Number_Setup);  // Handle WiFi Settings being saved
    server.on("/ButLightLevels", handle_Light_Levels);     // Handle Light Levels Setup button being pressed from Main Menu
    server.on("/LightLevelsSetup", HTTP_POST, handle_Light_Levels_Setup);  // Handle Light Level Settings being saved
    server.on("/ButTemperature", handle_Temperature);      // Handle Temperature Setup button being pressed from Main Menu
    server.on("/TemperatureSetup", HTTP_POST, handle_Temperature_Setup);// Handle Temperature Settings being saved
    server.on("/ButAngle", handle_Angle);                  // Handle Angle Setup button being pressed from Main Menu
    server.on("/AngleSetup", HTTP_POST, handle_Angle_Setup); // Handle Angle Settings being saved
    server.on("/checkShutter", handle_checkShutter);
    server.on("/FinishCal", handle_FinishCal);             // Calibration Finish Button has been pressed
    server.on("/ReadLightLevel", handle_ReadLightLevel);   // Go to Read Light Level sensor
    server.on("/HomePage", handle_HomePage);               // Go to Home Page
    server.on("/ButOTA", handle_OTA);                      // Go to Enable OTA
    server.onNotFound(handle_NotFound);
    server.begin();
    // If PSC was previously initialised, keep data, but allow to be changed otherwise initialise all data
    if(S_EData.InitCode != INITCODE)
    {
      S_EData.ShutterNumber = 0;     // Initialise to no shutter number
      S_EData.MaxShutterTemp = 25;   // Initialise Max Temperature setting for closing shutters
      S_EData.MaxShutterAngle = 90;  // Initialise Max Servo Angle setting
      S_EData.MinLightLevel = 200.0; // Initialise Min Light Level Setting
      S_EData.MaxLightLevel = 500.0; // Initialise Max Light Level Setting
      S_EData.RouterSSID[0] = 0;     // Initialise Router SSID to NULL
      S_EData.MQTTIPAddress[0] = 0;  // Initialise MQTT IP Address to NULL
    }
    S_EData.WiFiEnabled = 0; // WiFi may not be needed
    S_EData.MQTTEnabled = 0; // MQTT may not be needed
    OTA_Flag = false;        // OTA not yet enabled
    // Read all sensors
    Read_Sensors();
  }
}
    

The “Loop()” function, which runs continuously after “Setup()”, initially looks at the Over the Air update flag, “OTA_Flag”. This can be set from one of the Web pages, if a software update is required. In which case, the OTA update process will be initiated.

A note of warning here. If the partitions have not been set up correctly in the Arduino IDE, the OTA process can crash. This did happen to me and I had to disassemble my PSC and perform a manual update using the special FTDI interface on the PCB. Once all the parameters have been setup via the Web Server and the “FINISH” button has been pressed on the Home page, various parameters initialised (the “MQTT_Counter” is saved in RTC memory so will survive all except a power down cycle) , the Shutter is Closed, if the MQTT parameter were setup, a MQTT message is sent to the Home Automation System, the HC-12 Transceiver is initialised, all parameters are then saved to EEPROM, the Sensors are read again to initiate some power down functions (the Light Sensor, will go into power down mode when read). “Deep Sleep” is now initiated and the fun begins!

void loop() {
  // If OTA is enabled, handle OTA process
  if(OTA_Flag) ArduinoOTA.handle();
  // Do Shutter calibration
  server.handleClient();
  if(CalFinished)
  {
    S_EData.InitCode = INITCODE;
    StoredShutterState = SHUTTERCLOSED;
    ShutterMode = true;
    MQTT_Counter = 0;
    // Close the Shutter, just in case the 
    // shutter is not already closed
    OpenCloseShutter(SHUTTERCLOSED,S_EData.MaxShutterAngle);
    if(S_EData.MQTTEnabled == INITCODE) SendUpdMQTT(5);
    // Set up HC-12 Transceiver
    Set_HC12_to_4800Baud();
 
    delay(1000);
// Allow time for final web page to be output
    // Write the data to EEPROM for later
    EEPROM.put(0, S_EData);
    EEPROM.commit(); 
    PRINTL("Max Temperature Setting ", S_EData.MaxShutterTemp);
    PRINTL("Max Shutter Angle Setting ", S_EData.MaxShutterAngle);
    PRINTL("Max Light Level Setting ", S_EData.MaxLightLevel);
    PRINTL("Min Light Level Setting ", S_EData.MinLightLevel);
    PRINTL("Shutter Number is ", S_EData.ShutterNumber);
    PRINTL("Router SSID is ", S_EData.RouterSSID);
    PRINTL("Router Password ", S_EData.RouterPasscode);
    PRINTL("WiFi Init Code is ", S_EData.WiFiEnabled);
    PRINTL("MQTT Server IP Address ", S_EData.MQTTIPAddress);
    PRINTL("MQTT Server Name ", S_EData.MQTTServerName);
    PRINTL("MQTT Server Password ", S_EData.MQTTPassword);
    PRINTL("MQTT Port Number ", S_EData.MQTTPortNumber);
    PRINTL("MQTT Topic ", S_EData.topicShutterStatus);
    PRINTL("MQTT Init Code is ", S_EData.MQTTEnabled);
    Read_Sensors();  // Power reduction??
    delay(500);
    /*
    First we configure the wake up source
    We set our ESP32 to wake up for an external   
    trigger.
    */
    // ESP32 set to wake up by default every 
    // "TIME_TO_SLEEP" seconds
    esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR);
    PRINTLN("Setup ESP32 to sleep for every " + String(TIME_TO_SLEEP) + " Seconds");
    // ESP32 also setup to wakeup if a low is 
    // detected on GPIO04 which is tied to
    // GPIO16 (RXD2) which is connected to the 
    // HC-12 Transceiver which receives
    // commands from a remote controller
    esp_sleep_enable_ext0_wakeup(GPIO_NUM_4,0); 
    //1 = High, 0 = Low
    //Go to sleep now
    PRINTLN("Going to sleep now ");
    PDELAY(1000);
    esp_deep_sleep_start();
    delay(10);
  }
  delay(50);
}

Setup via the Web Server

When powering up the PSC for the first time, the Web Server is initiated. The Web Server can also be initiated by sending the “Reset” command from the RCU.

Once the Web Server is up and running, to access the Web pages for setting up the various parameters for the PSC operation, you first need to connect your Notebook, Smart Phone, Tablet, or some device which has WiFi and Web Browser capability to the PSC. So to connect to the PSC Access Point (AP), find on your WiFi device, the PSC AP SSID, which is “PSController” and connect.

The password is “12345678”. Once you have connected, open your Browser and in the Address bar, type in “192.168.1.1”. This will open the Home Page of the PSC. You will be presented with several options (see previous Screenshot).

Note the Home Page is generated by the Web Server calling function “handle_OnConnect()” in this instance.

The first thing you need to do is set the Shutter Number. So click on the “Shutter” button. The Web Server will call function “handle_Shutter_Number()”. This brings up the screen for setting up the Shutter Number. The Shutter Number is important as this Number is used by the RCU to control a particular Shutter. The Shutter Number must be in the range 1 to 5.

If a number outside this range is entered and the “Save” button is clicked, the same Web Page is displayed but with an error message, “Invalid Shutter Number”. Clicking the “Save” button causes the Web Server to call function “handle_Shutter_Number_Setup()”. The Shutter Number Range can be changed, but I would not advise increasing the range too much as the RCU is only a four button remote with the fourth button having to be clicked to change the Shutter Number, to which commands are sent.

Once you have set and saved the Shutter Number, click the “HOME” button. The Web Server now calls Function “handle_HomePage()”. You are then returned to the Home Page as before.

If you want to send information to your Home Automation System or perform a Software Update, you will have to set up the WiFi credentials. So click the “Setup WiFi” button. The Web Server will call Function “handle_WiFi()” and the previous screenshot will appear in your Browser.

Initially, there are no WiFi credentials set, so the message “Invalid SSID” is displayed on the Web page. Once you input your SSID and password and click the “Save” button, the Web Server will call Function “handle_WiFi_Setup()”. Here the PSC will check that the SSID is valid by scanning for all WiFi networks. If it finds the Network with the SSID you have input, the Web page is refreshed indicating the SSID is valid. Clicking the “HOME” button will now display a new Home page.

You will now see that two more buttons have been added “Setup MQTT” and “S/W Upgrade”.

Now, if you want to send MQTT messages to your Home Automation System. You need to click the “Setup MQTT” button. This will cause the Web Server to call Function “handle_MQTT()”. The above screenshot shows the Web page that will be displayed.

There are several parameters that need to be set up as can be seen. If your MQTT Broker does not have a password, this can be left blank. Note the default MQTT Server Port Number is “1883”. If this is correct for your MQTT Broker, you do not need to enter anything in this field.

Once you have input all the required parameters, click the “Save” button. Once the PSC has saved all the parameters, the Web Page is updated as shown in the above screenshot. Click the “HOME” button to return to the Home page.

Click the “Light” button to set up the Minimum and Maximum Light Levels. The Web Server will call Function “handle_Light_Levels()”. The Minimum Light Level is the level, below which, the Shutter will be closed. The Maximum Light Level is the level, above which, the Shutter will be opened.

You may have to play around with the values, which is why there is a “READ” button. By clicking the “READ” button, the Web Server will call Function “ReadLightLevel()”. This reads the BH1750 Light Sensor and updates the Web Page. Once you are happy with the input Light Levels, click the “Save” button. The Web page is again updated and the input values are displayed. Click the “HOME” button to return to the Home page.

Now click the “Temperature” button. This allows the Maximum Temperature to be set up to which the Shutter can remain open. The Web Server calls Function “handle_Temperature()” and the Web page shown above is displayed. A value between 25 and 35 °C must be input, otherwise the Web Page will be updated indicating an error when clicking the “Save” button. The Web Server calls Function “handle_Temperature_Setup()” on clicking the “Save” button.

Once finished, click the “HOME” button to return to the Home page.

To set the angle that the Servo Motor rotates and thus the amount the PSC opens the Shutter, click the “Angle” button. The Web Server calls Function “handle_Angle()” and displays the Web page shown above.

An angle between 0 and 180 degrees must be entered, if not an error will be indicated when clicking the “Save” button, which causes the Web Server to call Function “handle_Angle_Setup()” as the Web page is refreshed.

Now to test that you have input the correct angle, you can click on the “TEST” button, which causes the Web Server to call Function “handle_checkShutter()”. This function will initially put the Servo Motor in the Shutter Closed position. Then with a delay, the Shutter is placed in the Open position. Another delay and the Shutter is returned to the Closed position.

The Angle can be changed as many times as required, clicking the “Save” button each time and then the “TEST” button to check the new angle setting. Click the “HOME” button when you are happy that the Servo Angle is set correctly, to return to the Home page

Note, setting Light Levels, Temperature and Servo Angle can be performed in any order and as many times as you like.

There are two more buttons on the Home Page, “S/W Upgrade” and “FINISH”. Clicking the FINISH button will cause the Web Server to call the “handle_FinishCal()” function. This will save all the parameters into EEPROM, Close the Shutter and put the PSC into “Deep Sleep”. As a parting gesture, the following is displayed in the Web Browser.

If you want to Upgrade the Software, firstly have the Arduino IDE opened with the new program. Click the “S/W Upgrade” Button and the PSC Web Server will call the Function “handle_OTA()”, The message “OTA Preparing” will be displayed on your Web Browser.

Now go to the Arduino IDE and under “Tools/Port” select the “Network Port” with “PSCESP32” in its name. Now at the Arduino IDE, click the “Upload” button and the new software will be installed in the PSC. The PSC will be reset and the new software will be executed. Note, if you are only going to perform a Software Upgrade, you only have to set the WiFi Credentials before clicking on the “S/W Upgrade” button.

CONCLUSION

I think I have covered most aspects of the Plantation Shutter Controller (PSC) design, build, installation and operation. There are still two things I have not covered and that is the Remote Control Unit (RCU) and the Solar Battery Charger. Both of these will be presented in a future article.

I believe, what I have presented in this article, is fairly comprehensive. This has been a long term project for me. I have spent several years developing seven versions of my design.

One of the most important occurrences during this time was the acquisition of my Creality Ender 3 V2 3D Printer. This has allowed me to create custom enclosures, mounting plates and linkage arms to help install the PSC in a way that is both complete and satisfying.

I must admit, I have had the 3D printer for well over a year. I have upgraded many things and only recently achieved a reliable 3D printer. It has taken many viewings of YouTube videos on 3D printers and the reading of many articles in 3D Printer forums. I am sure, many more of my projects are going to include 3D printed components.

There may still be some improvements in my 3D printed linkages between the PSC Servo Motor and the Shutter Blades. I am not a Mechanical Engineer, but am still learning about the mechanics of movement. So there may be an update to my Servo/Shutter linkages in the future. Or, if anyone has any ideas for improvement, I would appreciate hearing about them.

Next month, we'll look at adding Remote Control.

NEXT MONTH: PART 3 - ADDING REMOTE CONTROL

ABOUT THE AUTHOR:

Peter Stewart is a retired engineer and regular contributor to DIYODE magazine.

Other projects include an ATtiny85-based general purpose timer with buzzer in Issue 53, WiFi Temperature Sensor with Scrolling Display in Issue 60, an ATtiny84-based Auto-Ranging USB Current Meter in Issue 64, and Arduino-based Car Presence Sensor & Parking Assistant.

Peter Stewart

Peter Stewart