Implementing E220 LoRa Remote Switch to Conserve Wyse Cam3 Battery


Guest post created with ♥ by William 🙂|

The main goal here is to save battery power for a Wyse Cam3 video camera, which uses 180 mA at 5 volts. Since the camera is web-based and only gets a few views a day, we want to stop it from draining power all the time. Right now, a 10,000-mA battery bank only lasts a day, so we need a better way to manage its power. By using a remote switch controlled by an Async Web Server request, we can ensure the camera isn’t wasting battery when it’s not being used.

ESP32 E220 and remote switch
ESP32 E220 and remote switch

Overview of the Project:

E220 Remote Switch project will reduce battery consumption by utilizing a countdown timer; it started upon web request and turned off on countdown timer expiration.  When a web request is severed, the E220-900T30D Sender module will send a wake-on-radio (WOR) message. Receiver module wakens from the Sleep Mode.  Sleep Mode is an ultra-low current state in the microamp range.  ESP32 will wake from Deep Sleep and turn on battery power; simultaneously, the E220 module will receive the WOR message.  When the countdown timer has expired, the E220 module will send a message, switching the battery power off and putting the ESP32 into deep sleep and the receiving E220 module into sleep mode.  The cycle repeats for new web requests.

Brief description of the E220-Remote-Switch project:

The E220-Remote-Switch project utilizes two Ebyte E220-900T30D RF modules and two ESP32 microcontrollers. The current project status is that the INA226 Battery Monitor and KY002S MOSFET switch have not been implemented in this update.

Demonstration mode:

  1. ESP32 Receiver: push the receiver reset button; this puts the ESP32 into a deep sleep.
  2. Open browser to http://10.0.0.27/relay; this will create a web request to turn on battery power. and start a countdown timer to turn off battery power, then put the ESP32 receiver into deep sleep.
  3. The first message is the WOR message to wake the E220 Receiver module and the Deep Sleeping ESP32. The second message from the E220 Sender is required to turn on battery power and start the countdown timer. Both messages are sent when a server request arrives from a click on a web link to view the video camera.  
E220-Remote-Switch Demo with Sleep Current Monitoring

Three advantages of using the Ebyte E220-900T30D are an increased distance of 10 km estimated, a power of 30 dbm, and a Sleep current of 5 uA. The third feature is the ability to send a WOR message to wake up the receiving transceiver, allowing a second message to turn on battery power.

A transmit current of 620 mA is almost instantaneous at 30 dBm and sends up to 200 bytes before dropping the current. Receiving current; for a message, 17.2 mA. Current values are from Ebytes E220-900T30D User Manual.  Measured Standby current 11.8 mA.  The E220 module always draws current; utilizing the E220 Sleep Mode, the sleep current ranges from .54 uA to 97.33 uA, and the complete cycle of messages is measured with a digital multimeter set for microamps.  Occasional very brief, on the order of a few milliseconds; meter overloads occur due to the message being transmitted or received.  

Components Used

E220 LoRa module; two E220-900T30D radios, transceivers, are used as Sender and Receiver. Suggest labeling the Sender and Receiver.  Ebyte E220-900T30D Module

EByte E220-900T30D Module
EByte E220-900T30D Module

You can find the modules on E220-400T22D 433MHz 5Km - E220-400T30D 433MHz 10Km - E220-900T22D 868MHz 915MHz 5Km - E220-900T30D 868MHz 915MHz 10Km

ESP32 microcontroller: two ESP32 Devkit v1 microcontrollers: one for the web server, one for receiving messages, and one for switching the battery power on. 

Here a selection of esp32 ESP32 Dev Kit v1 - TTGO T-Display 1.14 ESP32 - NodeMCU V3 V2 ESP8266 Lolin32 - NodeMCU ESP-32S - WeMos Lolin32 - WeMos Lolin32 mini - ESP32-CAM programmer - ESP32-CAM bundle - ESP32-WROOM-32 - ESP32-S

MOSFET bi-stable, switch KY002 for battery switch; the switch can handle 3-27 Volts at 1.5 Amps. 

3-27V 1.5A Single Button Bistable Switch Module Flip Flop Latch Falling Edge Trigger Switch KY002

INA226 Battery Monitor  36 Volt, 20 Amp, I²C 

INA226 Battery Monitor  

No low-power optimization was attempted because ESP32 Devkit v1 is a development board. However, the E220 module’s current consumption was lowered by using sleep mode.

Sender and Receiver E220 module connections

ESP32 DOIT DEV KIT v1 pinout
ESP32 DOIT DEV KIT v1 pinout

Setup connections for the Sender and Receiver require no wiring changes after the initial connections. The sender and Receiver mode of operation will use E220 library commands coded into ESP32 Sender and Receiver sketches.

esp32Ebyte E220
GPIO21M0
GPIO19M1
TXD2RX
RXD2TX
GPIO15 (RTC GPIO pin  impotant)AUX
5 VoltsVCC
GNDGND

KY002S Bi-stable MOSFET switch will be included in future updates; KY002S is emulated in this sketch with serial print statements.

INA226 Battery Monitor will be a future update; it will monitor voltage, current, and alarm at a set voltage and log to a file.

Detailed schematic showing connections between ESP32, LoRa E220 modules

Note: The E220 Module AUX pin is connected to GPIO15, which is an RTC GPIO pin required to wake up ESP32 

In the schema above, D18 is used as AUX; you just use D15 for the project.

Sender: required libraries for ESP32 and LoRa E220 module

#include <Arduino.h> // *
#include "WiFi.h" // *
#include <WiFiUdp.h> // *
#include <HTTPClient.h> // *
#include <time.h> // *
#include "LoRa_E220.h" // Ebyte LoRa E220 Library
#include <AsyncTCP.h> // AsyncTCP Library
#include "ESPAsyncWebServer.h" // ESPAsyncWebServer Library
#include <Ticker.h> // *
#import "index7.h" // HTML web page stored in memory –Do not remove; except to replace with new “index7.h”.

Libraries with “*” are included in Arduino IDE, Board Manager 2.0.17.  Board Manager 2.0.17 was used to compile sketches for this project.

Receiver: required libraries for ESP32 and LoRa 220 module:

#include "Arduino.h" // *
#include "LoRa_E220.h"
#include <WiFi.h> // *
#include <time.h> // *
#include <FS.h> // *
#include <LittleFS.h> // *
#include "esp_sleep.h" // *
#include "driver/gpio.h" // *
#include "esp_system.h" // *
#include <INA226_WE.h> // INA226_WE Library
#include <Wire.h> // *

Libraries with “*” included in Arduino IDE, Board Manager 2.0.17.  Board Manager 2.0.17 was used to compile sketches in this project.

Sending structure with E220

The project uses the E220 Library’s containers to send structured messages. There are two structures: one for time, using NTP time from the sender since the receiver will have no Internet access; the other, Message, uses integer variable data to switch the battery and the actual formatted timestamp; timestamping will be used in the Battery Monitor log file. The value of variable data determines whether the battery switch position is on or off.  

Container code for receiving structure Message message

ResponseStructContainer rsc = e220ttl.receiveMessage(sizeof(Message));

if (rsc.status.code == 1) { // Check if the status is SUCCESS
 Message message = *(Message*)rsc.data;
 //Serial.println(message.switchState); // This prints to monitor
 //Serial.println(message.dateTime); // This prints to monitor
 rsc.close();

The WOR message is sent when the server request arrives.  WOR was moved to a function to prevent it from sending the WOR message at sketch boot up and to be able to send two messages on one web user click to view the camera.

WOR function:

void sendWOR(){
      e220ttl.setMode(MODE_1_WOR_TRANSMITTER);
      delay(delayTime);
      // Send message
      ResponseStatus rs = e220ttl.sendFixedMessage(0, DESTINATION_ADDL, CHANNEL, "Hello, world? WOR!");
      e220ttl.setMode(MODE_0_NORMAL);
      delay(delayTime);
}

Problems and their solutions

The biggest issue was the intermittent connection between the breadboard and the E220 module. This was resolved by using female-to-male Dupont wires with module-to-breadboard connections.  

Another issue was sending a WOR message and then needing to send a second message using the same web request. The solution included sending the second message to the Async Web Server request.

Server request solution code:

server.on("/relay", HTTP_GET, [](AsyncWebServerRequest *request) {
     request->send_P(200, PSTR("text/html"), HTML7, processor7);
     sendWOR();  //Sends WOR message; moved the location for sending WOR message to sendWOR function.
     data = 1;  //Sets data to 1 used to turn on battery switch
     needAnotherCountdown = 1;  //Limits ticker once timer to be “reset” for multiple uses; instead of a single use.
     countdownTrigger();  //Starts countdown timer.
});

SendWOR function:

void sendWOR(){
    e220ttl.setMode(MODE_1_WOR_TRANSMITTER);
    delay(delayTime);
    // Send message
    ResponseStatus rs = e220ttl.sendFixedMessage(0, DESTINATION_ADDL, CHANNEL, "Hello, world? WOR!");
    e220ttl.setMode(MODE_0_NORMAL);
    delay(delayTime);
}

Function countdownTrigger():

void countdownTrigger() {
    // Perform countdown actions here
    Serial.println("\nCountdown timer triggered!\n");
    //getDateTime();
    // Schedule the next countdown if needed
    if (needAnotherCountdown == 1) {
        onceTick.once(60, ISRcamera);
        data = 1;
        switchOne(data);
        needAnotherCountdown = 0;
    }
}

Function switchOne() sends a structure Message message, includes the value of data and the formatted timestamp.

void switchOne(int data) {
	if (data == 1) {
		Serial.println("\nWaked up from external GPIO!");
		Serial.println("Wake and start listening!\n");
		delay(500);
		Serial.println("\nESP32 waking from Deep Sleep");
		Serial.println("Battery Switch is ON\n");
	}
	if (data == 2) {
		Serial.println("\nBattery power switched OFF");
		Serial.println("ESP32 going to Deep Sleep\n");
	}

	Serial.println("Hi, I'm going to send message!");
	e220ttl.setMode(MODE_1_WOR_TRANSMITTER);
	delay (delayTime);
	get_time();
	Message message;
	
	//initialize struct members
	message.switchState = data;

	// Initialize the dateTime
	String dateTimeStr = get_time();

	if (!dateTimeStr.isEmpty()) {
		strncpy(message.dateTime, dateTimeStr.c_str(), MAX_dateTime_LENGTH - 1);
		message.dateTime[MAX_dateTime_LENGTH - 1] = '\0'; // Ensure null-termination
	}

	Serial.print("switchState: ");
	Serial.println(message.switchState);

	Serial.print("dateTime: ");
	Serial.println(message.dateTime);

	// Send message
	ResponseStatus rs = e220ttl.sendFixedMessage(0, DESTINATION_ADDL, CHANNEL, &message, sizeof(Message));
	// Check If there is some problem of succesfully send
	Serial.println(rs.getResponseDescription());
}

int sendMessage(int data) {
	Serial.println("Hi, I'm going to send message!");
	e220ttl.setMode(MODE_1_WOR_TRANSMITTER);
	delay (delayTime);
	get_time();

	Message message;

	//Initialize struct members
	message.switchState = data;

	// Initialize the dateTime
	String dateTimeStr = get_time();
	if (!dateTimeStr.isEmpty()) {
		strncpy(message.dateTime, dateTimeStr.c_str(), MAX_dateTime_LENGTH - 1);
		message.dateTime[MAX_dateTime_LENGTH - 1] = '\0'; // Ensure null-termination
	}

	//Serial.print("switchState: "); Serial.println(message.switchState);
	//Serial.print("dateTime: "); Serial.println(message.dateTime);
	// Send message
	ResponseStatus rs = e220ttl.sendFixedMessage(0, DESTINATION_ADDL, CHANNEL, &message, sizeof(Message));
	// Check If there is some problem of successfully send
	Serial.println(rs.getResponseDescription());
}

Applications for DIY Makers with E220-Remote-Switch

  1. Home Automation Projects
    • Smart Lighting: Control home lighting remotely via smartphone or web interface.
    • Automated Curtains/Blinds: Open and close curtains or blinds based on time or light levels.
    • Garage Door Opener: Build a remote-controlled garage door opener.
  2. Garden and Outdoor Projects
    • Automated Irrigation System: Remote-controlled irrigation based on schedule or soil moisture.
    • Garden Lighting: Control outdoor lighting for aesthetics and security.
  3. Security Systems
    • Remote Door Lock: Lock/unlock doors remotely for security or access control.
    • Surveillance Cameras: Control the activation and angle of cameras.
  4. Environmental Monitoring
    • Weather Station: Monitor temperature, humidity, and pressure remotely.
    • Air Quality Monitor: Measure and report air quality with remote alerts.
  5. Pet Care
    • Automatic Pet Feeder: Dispense food remotely at specific times.
    • Pet Door: Control pet access based on schedule or command.
  6. DIY Robotics
    • Robot Control: Remotely navigate and operate robots.
    • Drone Projects: Remote control for DIY drones’ flight patterns and actions.
  7. Energy Management
    • Smart Plugs: Manage power consumption of devices remotely.
    • Thermostat Control: Regulate home heating and cooling systems remotely.
  8. Automotive Projects
    • Car Alarm System: Remote-controlled car alarm and tracking.
    • Remote Start: Start and stop car engine remotely.
  9. Entertainment and Media
    • Remote-Controlled Audio System: Control home audio setups remotely.
    • Projector Screen Control: Raise and lower projector screens remotely.
  10. Custom IoT Projects
    • Multi-Sensor Networks: Remote monitoring and control of sensor networks.
    • Data Logging: Collect and analyze sensor data remotely.
    • Mailbox Alert: Receive alerts for arriving postal mail.

These applications demonstrate the versatility and potential of the E220-Remote-Switch for DIY makers, enabling innovative and functional projects that enhance everyday life.

Complete sketches

Here are the complete sketches; you can also download Them from my GitHub repository.

Index HTML page

//index7.h
const char HTML7[] PROGMEM = R"====(
<!DOCTYPE html>
<html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
<style>
  html, body {
  margin: 0; /* Set margin to 0 to remove any unwanted spacing */
  padding: 0;
  height: 100%;
}

.container {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-between;
  height: 100vh; /* Use viewport height for container */
}

  @media (max-width: 768px) {
  .container {
    flex: 1 1 auto;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
  }
}
    
    header {
      margin: 5vh auto 0;
      width: 100%;
      text-align: center;
    }
    
    main {
      display: flex;
      align-items: center;
      justify-content: center;
    }
    
    iframe {
      aspect-ratio: 16 / 9;
      width: 1300px; /* Set the width to 100% to occupy the entire container */
      flex: 1; /* Use flex: 1 to make the iframe expand and fill the available space */
    }

    footer {
      width: 100%;
      text-align: center;
      padding: 10px; /* Add some padding to the footer for spacing */
    }
</style>
</head>
<body>
  <div class="container">
    <header>
      <br><br>This is original H264 video encoded by IP camera; server doesn't do any transcoding.  Wyze Cam v3 video feeds
      <br>Wyze-Bridge Docker, container; which provides webRTC video URL.  Camera maybe offline; depending on battery discharge state.
      <br><br>
    </header>
    <main>
      <iframe class="iframe" width="1300" height="731"src="http://c600.duckdns.org:8889/wyze-cam-v3/" frameborder="0"></iframe> 
    </main>
    <footer>
      <h2><a href="http://%LINK%/Weather" >ESP32 Server</a></h2>  
    </footer>
  </div>
</body>
</html>
)====";

Sender sketch

//E220_Remote_Switch_Sender.ino
//William Lucid 07/19/2024 @ 19:41 EST

//E220 Module is set to ADDL 3

//Fully connectd schema  AUX connected to ESP32, GPIO15
//Ardino IDE:  ESP32 Board Manager, Version 2.0.17

//  See library downloads for each library license.

// With FIXED SENDER configuration

#include <Arduino.h>
#include "WiFi.h"
#include <WiFiUdp.h> 
#include <HTTPClient.h>
#include <time.h>
#include "LoRa_E220.h"
#include <AsyncTCP.h>
#include "ESPAsyncWebServer.h"
#include <Ticker.h>

#import "index7.h"  //Video feed HTML; do not remove

#define DESTINATION_ADDL 2
#define FREQENCY_915
#define CHANNEL 66

#define RXD2 16
#define TXD2 17

#define AUX_PIN GPIO_NUM_15

int delayTime = 100;  //setmode delay duration

WiFiClient client;

///Are we currently connected?
boolean connected = false;

WiFiUDP udp;
// local port to listen for UDP packets
const int udpPort = 1337;
char incomingPacket[255];
char replyPacket[] = "Hi there! Got the message :-)";
//NTP Time Servers
const char * udpAddress1 = "pool.ntp.org";
const char * udpAddress2 = "time.nist.gov";

#define TZ "EST+5EDT,M3.2.0/2,M11.1.0/2"

int DOW, MONTH, DATE, YEAR, HOUR, MINUTE, SECOND;

char daysOfTheWeek[7][12] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};

char strftime_buf[64];

// ---------- esp32 pins --------------
 LoRa_E220 e220ttl(&Serial2, 15, 21, 19); //  RX AUX M0 M1

//LoRa_E220 e220ttl(&Serial2, 22, 4, 33, 21, 19, UART_BPS_RATE_9600); //  esp32 RX <-- e220 TX, esp32 TX --> e220 RX AUX_PIN M0 M1
// -------------------------------------

// Replace with your network details
const char *ssid = "R2D2";
const char *password = "sissy4357";

AsyncWebServer server(80);

int data = 0;

// Struct to hold date and time components
struct DateTime {
    int year;
    int month;
    int day;
    int hour;
    int minute;
    int second;
};

// Define the maximum length for the dateTime
const int MAX_dateTime_LENGTH = 30;

int switchState;

struct Message {
  int switchState;
  char dateTime[MAX_dateTime_LENGTH];  // Array to hold date/time string
};

Message message;

Ticker oneTick;
Ticker onceTick;

String linkAddress = "xxx.xxx.xxx.xxx:80";

portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;

volatile int watchdogCounter;
int totalwatchdogCounter;
int cameraPowerOff = 0;
int watchDog;

void ISRwatchdog() {

  portENTER_CRITICAL_ISR(&mux);

  watchdogCounter++;

  if (watchdogCounter >= 75) {

    watchDog = 1;
  }

  portEXIT_CRITICAL_ISR(&mux);
}

int cameraFlag;
int needAnotherCountdown = 0;

void ISRcamera() {
  batteryOff();
}

bool got_interrupt = false;
 
void interruptHandler() {
  got_interrupt = true;
}  

void sendWOR(){
  e220ttl.setMode(MODE_1_WOR_TRANSMITTER);
  delay(delayTime);
  // Send message
  ResponseStatus rs = e220ttl.sendFixedMessage(0, DESTINATION_ADDL, CHANNEL, "Hello, world? WOR!");
  e220ttl.setMode(MODE_0_NORMAL);
  delay(delayTime);
}

void setup() {
  Serial.begin(9600);
  delay(500);

  Serial2.begin(9600, SERIAL_8N1, RXD2, TXD2);
  
  Message message;
  message.switchState = data;
  String dateTimeStr = get_time();
  if (!dateTimeStr.isEmpty()) {
    strncpy(message.dateTime, dateTimeStr.c_str(), MAX_dateTime_LENGTH - 1);
    message.dateTime[MAX_dateTime_LENGTH - 1] = '\0'; // Ensure null-termination
  }

  wifi_Start(); 
  
  pinMode(AUX_PIN, INPUT);

  attachInterrupt(GPIO_NUM_15, interruptHandler, FALLING);
  
  // Startup all pins and UART
  e220ttl.begin();
  delay(delayTime);
  e220ttl.setMode(MODE_1_WOR_TRANSMITTER);
  delay(delayTime);

  //sendWOR();
 
  e220ttl.setMode(MODE_0_NORMAL);
  delay(delayTime);

  Serial.println("\n\n\nWebserver and");
  Serial.println("E220-900T30D Remote Switch\n");

  configTime(0, 0, "pool.ntp.org", "time.nist.gov");
  // See https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv for Timezone codes for your region
  setenv("TZ", "EST+5EDT,M3.2.0/2,M11.1.0/2", 3);   // this sets TZ to Indianapolis, Indiana


  server.on("/relay", HTTP_GET, [](AsyncWebServerRequest *request) {
    request->send_P(200, PSTR("text/html"), HTML7, processor7);
    sendWOR();
    data = 1;
    needAnotherCountdown = 1;
    countdownTrigger();
  });

  server.begin();

  oneTick.attach(1.0, ISRwatchdog);  //watchdog  ISR triggers every 1 second
} 

void loop() {

  DateTime currentDateTime = getCurrentDateTime();	
  
  if((currentDateTime.minute % 15 == 0) && (currentDateTime.second == 0)){
    //webInterface();  //Sends URL Get request to wake up Radio and ESP32 at 1 minute interval
                      // URL = http://10.0.0.27/relay	 
    //delay(1000);
  }
  
  //udp only send data when connected
  if (connected)
  {

    //Send a packet
    udp.beginPacket(udpAddress1, udpPort);
    udp.printf("Seconds since boot: %u", millis() / 1000);
    udp.endPacket();
  }

  // If something available
  if (e220ttl.available() > 1) {
    // read the String message
    ResponseContainer rc = e220ttl.receiveMessage();
    // Is something goes wrong print error
    if (rc.status.code != 1) {
      Serial.println(rc.status.getResponseDescription());
    } else {
      // Print the data received
      Serial.println(rc.status.getResponseDescription());
      Serial.println(rc.data);
    }
  }
}
      
String processor7(const String &var) {

  //index7.h

  if (var == F("LINK"))
    return linkAddress;

  return String();
}

void batteryOff() {
  int data = 2;
  switchOne(data);
  oneTick.detach();
}

void configTime()
{

  configTime(0, 0, udpAddress1, udpAddress2);
  setenv("TZ", "EST+5EDT,M3.2.0/2,M11.1.0/2", 3);   // this sets TZ to Indianapolis, Indiana
  tzset();

  //udp only send data when connected
  if (connected)
  {

    //Send a packet
    udp.beginPacket(udpAddress1, udpPort);
    udp.printf("Seconds since boot: %u", millis() / 1000);
    udp.endPacket();
  }

  Serial.print("wait for first valid dateTime");

  while (time(nullptr) < 100000ul)
  {
    Serial.print(".");
    delay(5000);
  }

  Serial.println("\nSystem Time set\n");

  get_time();

  Serial.println(message.dateTime);

}

void countdownTrigger() {
  // Perform countdown actions here
  Serial.println("\nCountdown timer triggered!\n");
  //getDateTime();
  // Schedule the next countdown if needed
  if (needAnotherCountdown == 1) {
    onceTick.once(60, ISRcamera);
    data = 1;
    switchOne(data);
    needAnotherCountdown = 0;
  }
}

// Function to get current date and time
DateTime getCurrentDateTime() {
    DateTime currentDateTime;
    time_t now = time(nullptr);
    struct tm *ti = localtime(&now);

    // Extract individual components
    currentDateTime.year = ti->tm_year + 1900;
    currentDateTime.month = ti->tm_mon + 1;
    currentDateTime.day = ti->tm_mday;
    currentDateTime.hour = ti->tm_hour;
    currentDateTime.minute = ti->tm_min;
    currentDateTime.second = ti->tm_sec;

    return currentDateTime;
}

// Function to get the dateTime
String get_time() {

    time_t now;
    time(&now);
    char time_output[MAX_dateTime_LENGTH];
    strftime(time_output, MAX_dateTime_LENGTH, "%a  %m/%d/%y   %T", localtime(&now)); 
    return String(time_output); // returns dateTime in the specified format
}

void switchOne(int data) {
  if (data == 1) {
    Serial.println("\nWaked up from external GPIO!");
    Serial.println("Wake and start listening!\n");
    delay(500);
    Serial.println("\nESP32 waking from Deep Sleep");  
    Serial.println("Battery Switch is ON\n"); 
  }

  if (data == 2) {
    Serial.println("\nBattery power switched OFF");
    Serial.println("ESP32 going to Deep Sleep\n");
  }

  Serial.println("Hi, I'm going to send message!");

  e220ttl.setMode(MODE_1_WOR_TRANSMITTER);
  
  delay(delayTime);
  
  get_time();

  Message message; 
  
  //initialize struct members
  message.switchState = data;
  
  // Initialize the dateTime 
  String dateTimeStr = get_time();
  if (!dateTimeStr.isEmpty()) {
    strncpy(message.dateTime, dateTimeStr.c_str(), MAX_dateTime_LENGTH - 1);
    message.dateTime[MAX_dateTime_LENGTH - 1] = '\0'; // Ensure null-termination
  }

  Serial.print("switchState:  "); Serial.println(message.switchState);
  Serial.print("dateTime:  "); Serial.println(message.dateTime);

  // Send message
  ResponseStatus rs = e220ttl.sendFixedMessage(0, DESTINATION_ADDL, CHANNEL, &message, sizeof(Message));
  // Check If there is some problem of succesfully send
    Serial.println(rs.getResponseDescription());  
}  


int sendMessage(int data){
  Serial.println("Hi, I'm going to send message!");

  e220ttl.setMode(MODE_1_WOR_TRANSMITTER);
  
  delay(delayTime);
  
  get_time();

  Message message; 

  //Initialize struct members
  message.switchState = data;
 
  // Initialize the dateTime 
  String dateTimeStr = get_time();
  if (!dateTimeStr.isEmpty()) {
    strncpy(message.dateTime, dateTimeStr.c_str(), MAX_dateTime_LENGTH - 1);
    message.dateTime[MAX_dateTime_LENGTH - 1] = '\0'; // Ensure null-termination
  }

  //Serial.print("switchState:  "); Serial.println(message.switchState);
  //Serial.print("dateTime:  "); Serial.println(message.dateTime);

  // Send message
  ResponseStatus rs = e220ttl.sendFixedMessage(0, DESTINATION_ADDL, CHANNEL, &message, sizeof(Message));
  // Check If there is some problem of succesfully send
  Serial.println(rs.getResponseDescription());  
}

void webInterface() {

  //getTimeDate();

  String data = "http://10.0.0.27/relay";

  if (WiFi.status() == WL_CONNECTED) {
    HTTPClient http;    // Declare object of class HTTPClient

    http.begin(data);  // Specify request destination

    // No need to add content-type header for a simple GET request

    int httpCode = http.GET();   // Send the GET request

    if (httpCode == HTTP_CODE_OK) {
      String payload = http.getString();  // Get the response payload

      Serial.print("HttpCode: ");
      Serial.print(httpCode);   // Print HTTP return code
      Serial.println("\n");
      //Serial.print("  Data echoed back from Hosted website: ");
      //Serial.println(payload);  // Print payload response      

      http.end();  // Close HTTPClient
    } else {
      Serial.print("HttpCode: ");
      Serial.print(httpCode);   // Print HTTP return code
      Serial.println("  URL Request failed.");

      http.end();   // Close HTTPClient
    }
  } else {
    Serial.println("Error in WiFi connection");
  }
}

void wifi_Start() {

//Server settings
#define ip { 10, 0, 0, 27}
#define subnet \
  { 255, 255, 255, 0 }
#define gateway \
  { 10, 0, 0, 1 }
#define dns \
  { 10, 0, 0, 1 }

  WiFi.mode(WIFI_AP_STA);

  Serial.println();
  Serial.print("MAC: ");
  Serial.println(WiFi.macAddress());

  // We start by connecting to WiFi Station
  Serial.print("Connecting to ");
  Serial.println(ssid);

  WiFi.begin(ssid, password);
  delay(1000);

  //setting the static addresses in function "wifi_Start
  IPAddress ip;
  IPAddress gateway;
  IPAddress subnet;
  IPAddress dns;

  WiFi.config(ip, gateway, subnet, dns);

  Serial.println("Web server running. Waiting for the ESP32 IP...");

  // Printing the ESP IP address
  Serial.print("Server IP:  ");
  Serial.println(WiFi.localIP());
  Serial.print("Port:  ");
  Serial.println("80");
  Serial.print("MAC: ");
  Serial.println(WiFi.macAddress());
  Serial.print("Wi-Fi Channel: ");
  Serial.println(WiFi.channel());
  Serial.println("\n");

  delay(300);

  WiFi.waitForConnectResult();

  Serial.printf("Connection result: %d\n", WiFi.waitForConnectResult());

  server.begin();


  if (WiFi.waitForConnectResult() != 3) {
    delay(3000);
    wifi_Start();
  }
}

Receiver sketch

//E220_Remote_Switch_Receiver.ino   Added KY002S Bi-Stable MOSFET Switch and INA226 Battery Monitor
//William Lucid 7/28/2024 @ 01:21 EDT

//E220 Module is set to ADDL 2

//Fully connectd schema  AUX connected to ESP32, GPIO15 --Important RTC_GPIO Pin 
//Ardino IDE:  ESP32 Board Manager, Version 2.0.17

//  See library downloads for each library license.

// With FIXED SENDER configuration

#include "Arduino.h"
#include "LoRa_E220.h"
#include <WiFi.h>
#include <time.h>
#include <FS.h>
#include <LittleFS.h>
#include "esp_sleep.h"
#include "driver/gpio.h"
#include "esp_system.h"
#include <INA226_WE.h>
#include <Wire.h>

#define AUX_PIN_BITMASK 0x8000

// Persisted RTC variable
RTC_DATA_ATTR int bootCount = 0;
RTC_DATA_ATTR int switch_State = 0;

const int pulseDuration = 300;  // 100 milliseconds (adjust as needed)

#define DESTINATION_ADDL 3
#define FREQUENCY_915
#define CHANNEL 66

#define RXD2 16
#define TXD2 17


#define M0_PIN GPIO_NUM_21
#define M1_PIN GPIO_NUM_19

#define AUX_PIN GPIO_NUM_15
#define TRIGGER 32  //KY002S MOSFET Bi-Stable Switch
#define ALERT 4     //INA226 Battery Monitor

#define SDA_PIN 25
#define SCL_PIN 26

int delayTime = 100;  //setmode delay duration

#define I2C_ADDRESS 0x40

INA226_WE ina226 = INA226_WE(I2C_ADDRESS);

volatile bool event = false;

void alert() {
  event = true;
  detachInterrupt(ALERT);
}

volatile bool alertFlag = false;

// Struct to hold date and time components
struct DateTime {
  int year;
  int month;
  int day;
  int hour;
  int minute;
  int second;
};

// Define the maximum length for the timestamp
const int MAX_dateTime_LENGTH = 30;

int data = 0;

int switchState;

struct Message {
  int switchState;
  char dateTime[MAX_dateTime_LENGTH];  // Array to hold date/time string
};

Message message;

#define FPM_SLEEP_MAX_TIME 0xFFFFFFF
void callback() {
  Serial.println("Callback");
  Serial.flush();
}

bool interruptExecuted = false;  // Ensure interruptExecuted is volatile

void IRAM_ATTR wakeUp() {
  // Do not use Serial on interrupt callback
  interruptExecuted = true;
  detachInterrupt(AUX_PIN);
}

void printParameters(struct Configuration configuration);

// ---------- esp32 pins ----------------
LoRa_E220 e220ttl(&Serial2, 15, 21, 19);  //  RX AUX M0 M1

//LoRa_E220 e220ttl(&Serial2, 22, 4, 33, 21, 19, UART_BPS_RATE_9600); //  esp32 RX <-- e220 TX, esp32 TX --> e220 RX AUX_PIN M0 M1
// -------------------------------------

void handleWakeupReason() {
  esp_sleep_wakeup_cause_t wakeup_reason = esp_sleep_get_wakeup_cause();
  switch (wakeup_reason) {
    case ESP_SLEEP_WAKEUP_EXT0:
      Serial.println("Wakeup caused by external signal using RTC_IO");
      break;
    case ESP_SLEEP_WAKEUP_EXT1:
      Serial.println("Wakeup caused by external signal using RTC_CNTL");
      break;
    case ESP_SLEEP_WAKEUP_TIMER:
      Serial.println("Wakeup caused by timer");
      break;
    case ESP_SLEEP_WAKEUP_TOUCHPAD:
      Serial.println("Wakeup caused by touchpad");
      break;
    case ESP_SLEEP_WAKEUP_ULP:
      Serial.println("Wakeup caused by ULP program");
      break;
    default:
      Serial.printf("Wakeup was not caused by deep sleep: %d\n", wakeup_reason);
      break;
  }
}

void enterSleepMode() {
  e220ttl.setMode(MODE_3_SLEEP);
  delay(delayTime);
}

void enterDeepSleep() {
  e220ttl.setMode(MODE_2_POWER_SAVING);
  esp_sleep_enable_ext0_wakeup(GPIO_NUM_15, LOW);
  Serial.flush();  // Ensure all Serial data is sent before sleep
  gpio_hold_en(GPIO_NUM_21);
  gpio_hold_en(GPIO_NUM_19);
  gpio_deep_sleep_hold_en();
  Serial.println("Going to deep sleep");
  esp_deep_sleep_start();
  Serial.println("This will never be printed");
}

void setup() {
  Serial.begin(9600);
  delay(1000);

  Serial2.begin(9600, SERIAL_8N1, RXD2, TXD2); // TX = 17, RX = 16
  delay(500);

  pinMode(M0_PIN, OUTPUT);
  pinMode(M1_PIN, OUTPUT);

  enterSleepMode();

  Serial.println("\n\nE220 Remote Switch Receiver\n");

  //Set default Trigger normal boot 
  digitalWrite(TRIGGER, LOW);

  // Increment boot number and print it every reboot
  ++bootCount;
  Serial.println("Boot number: " + String(bootCount));

  // Configure wake-up interrupt on GPIO15 (AUX pin)
  attachInterrupt(GPIO_NUM_15, wakeUp, FALLING);

  esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_FAST_MEM, ESP_PD_OPTION_ON);

  pinMode(AUX_PIN, INPUT_PULLUP);  // GPIO15 WakeUp
  pinMode(TRIGGER, OUTPUT);        // ESP32, GPIO23
  pinMode(ALERT, INPUT);          // ESP32, GPIO4

  bool fsok = LittleFS.begin(true);
  Serial.printf_P(PSTR("\nFS init: %s\n"), fsok ? PSTR("ok") : PSTR("fail!"));

  Wire.begin(SDA_PIN, SCL_PIN);

  if (!ina226.init()) {
    Serial.println("\nFailed to init INA226. Check your wiring.");
    //while(1){}
  }

  // INA226 configuration
  ina226.enableAlertLatch();
  ina226.setAlertType(BUS_UNDER, 5);
  attachInterrupt(digitalPinToInterrupt(ALERT), alert, FALLING);

  e220ttl.begin();
  delay(delayTime);
  e220ttl.setMode(MODE_2_WOR_RECEIVER);
  delay(delayTime);

  // Check if the wakeup was due to external GPIO
  if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_EXT0) {
    //Serial.println("Waked up from external GPIO!");

    gpio_hold_dis(GPIO_NUM_21);
    gpio_hold_dis(GPIO_NUM_19);
    gpio_deep_sleep_hold_dis();

    e220ttl.setMode(MODE_0_NORMAL);
    delay(delayTime);
    e220ttl.sendFixedMessage(0, DESTINATION_ADDL, CHANNEL, "We have waked up from message, but we can't read It!");
  } else {
    e220ttl.setMode(MODE_2_POWER_SAVING);
    delay(delayTime);
    Serial.println("Going to deep sleep!");
    delay(100);

    if (ESP_OK == gpio_hold_en(GPIO_NUM_21)) {
      Serial.println("HOLD 21");
    } else {
      Serial.println("NO HOLD 21");
    }
    if (ESP_OK == gpio_hold_en(GPIO_NUM_19)) {
      Serial.println("HOLD 19");
    } else {
      Serial.println("NO HOLD 19");
    }
    
    gpio_deep_sleep_hold_en();
    delay(1);

    //Set default Trigger going deep sleep
    digitalWrite(TRIGGER, LOW);

    esp_sleep_enable_ext0_wakeup(GPIO_NUM_15, LOW);

    esp_deep_sleep_start();
  }
}

void loop() {

  //Serial.println("Test deep sleep");

  e220ttl.setMode(MODE_2_WOR_RECEIVER);
  
  if (e220ttl.available() > 0) {
    Serial.println("\nMessage arrived!");

    ResponseStructContainer rsc = e220ttl.receiveMessage(sizeof(Message));
    if (rsc.status.code == 1) {  // Check if the status is SUCCESS
      Message message = *(Message*)rsc.data;
      //Serial.println(message.switchState);  // This prints to monitor
      //Serial.println(message.dateTime);     // This prints to monitor
      rsc.close();

      e220ttl.setMode(MODE_0_NORMAL);
      delay(delayTime);

      ResponseStatus rsSend = e220ttl.sendFixedMessage(0, DESTINATION_ADDL, CHANNEL, "We have received the message!");
      Serial.println(rsSend.getResponseDescription());
      delay(10);

      e220ttl.setMode(MODE_0_NORMAL);
      delay(delayTime);

      enterSleepMode();
      
      if (message.switchState == 1 ) {
        Serial.println("\nWaked up from external0 RTC GPIO!");
        Serial.println("Wake and start listening!\n");
        digitalWrite(TRIGGER, HIGH);
        Serial.println("\nBattery power switched ON");
        Serial.println("ESP32 wake from Deep Sleep\n");
        getINA226(message.dateTime);
      }     

      if (message.switchState == 2) {
        digitalWrite(TRIGGER, HIGH);
        Serial.println("\nBattery power switched OFF");
        Serial.println("ESP32 going to Deep Sleep\n");
        enterDeepSleep();
      }   

      if (event) {
        Serial.println("Under voltage alert");
        alertFlag = true;
        ina226.readAndClearFlags();
        //attachInterrupt(digitalPinToInterrupt(ALERT), alert, FALLING);
        event = false;
        ina226.readAndClearFlags();
        //enterDeepSleep();
      }   
    }
  }
}

int main() {
  // Create an instance of the Message struct
  Message message;

  // Get the timestamp using the get_time function and assign it to the struct member
  String timestamp = get_time();
  timestamp.toCharArray(message.dateTime, MAX_dateTime_LENGTH);

  // Now you can use message.timestamp as needed...

  return 0;
}

// Function to get the timestamp
String get_time() {
  time_t now;
  time(&now);
  char time_output[MAX_dateTime_LENGTH];
  strftime(time_output, MAX_dateTime_LENGTH, "%a  %d-%m-%y %T", localtime(&now));
  return String(time_output);  // returns timestamp in the specified format
}

void getINA226(const char* dtStamp) {
  float shuntVoltage_mV = 0.0;
  float loadVoltage_V = 0.0;
  float busVoltage_V = 0.0;
  float current_mA = 0.0;
  float power_mW = 0.0;
  ina226.startSingleMeasurement();
  ina226.readAndClearFlags();
  shuntVoltage_mV = ina226.getShuntVoltage_mV();
  busVoltage_V = ina226.getBusVoltage_V();
  current_mA = ina226.getCurrent_mA();
  power_mW = ina226.getBusPower();
  loadVoltage_V = busVoltage_V + (shuntVoltage_mV / 1000);
  checkForI2cErrors();

  Serial.println(dtStamp);
  Serial.print("Shunt Voltage [mV]: ");
  Serial.println(shuntVoltage_mV);
  Serial.print("Bus Voltage [V]: ");
  Serial.println(busVoltage_V);
  Serial.print("Load Voltage [V]: ");
  Serial.println(loadVoltage_V);
  Serial.print("Current[mA]: ");
  Serial.println(current_mA);
  Serial.print("Bus Power [mW]: ");
  Serial.println(power_mW);

  if (!ina226.overflow) {
    Serial.println("Values OK - no overflow");
  } else {
    Serial.println("Overflow! Choose higher current range");
  }
  Serial.println();

  // Open a "log.txt" for appended writing
  File log = LittleFS.open("/log.txt", "a");

  if (!log) {
    Serial.println("file 'log.txt' open failed");
  }

  log.print(dtStamp);
  log.print(" , ");
  log.print(shuntVoltage_mV, 3);
  log.print(" , ");
  log.print(busVoltage_V, 3);
  log.print(" , ");
  log.print(loadVoltage_V, 3);
  log.print(" , ");
  log.print(current_mA, 3);
  log.print(" , ");
  log.print(power_mW, 3);
  log.print(" , ");
  if(alertFlag){
    log.print("Under Voltage alert");
    alertFlag = false;
  }
  log.println("");
  log.close();
}

void checkForI2cErrors() {
  byte errorCode = ina226.getI2cErrorCode();
  if (errorCode) {
    Serial.print("I2C error: ");
    Serial.println(errorCode);
    switch (errorCode) {
      case 1:
        Serial.println("Data too long to fit in transmit buffer");
        break;
      case 2:
        Serial.println("Received NACK on transmit of address");
        break;
      case 3:
        Serial.println("Received NACK on transmit of data");
        break;
      case 4:
        Serial.println("Other error");
        break;
      case 5:
        Serial.println("Timeout");
        break;
      default:
        Serial.println("Can't identify the error");
    }
    if (errorCode) {
      while (1) {}
    }
  }
}

Utility sketch to upload the static content

#include <WiFi.h>
#include <FS.h>
#include <SPIFFS.h>
#include <LittleFS.h>
#include <FTPServer.h>

// Replace "XXXXXXXX" with your network details
const char * ssid = "XXXX";
const char * password = "XXXXXXXX";

WiFiClient client;

FTPServer ftpSrv(LittleFS);

void setup(){

  Serial.begin(9600);
  delay(1000);
  
  wifi_Start();

  bool fsok = LittleFS.begin(true);
  Serial.printf_P(PSTR("FS init: %s\n"), fsok ? PSTR("ok") : PSTR("fail!"));

  Serial.printf_P(PSTR("\nConnected to %s, IP address is %s\n"), ssid, WiFi.localIP().toString().c_str());

  // setup the ftp server with username and password
  // ports are defined in FTPCommon.h, default is
  //   21 for the control connection
  //   50009 for the data connection (passive mode by default)
  ftpSrv.begin(F("admin"), F("password")); //username, password for ftp.  set ports in ESP8266FtpServer.h  (default 21, 50009 for PASV)
}

void loop(){ 
  for(int x = 1; x < 5000; x++){
    ftpSrv.handleFTP();    
  }  
}

void wifi_Start() {

//Server settings
#define ip { 10, 0, 0, 28 }
#define subnet \
  { 255, 255, 255, 0 }
#define gateway \
  { 10, 0, 0, 1 }
#define dns \
  { 10, 0, 0, 1 }

  WiFi.mode(WIFI_AP_STA);

  Serial.println();
  Serial.print("MAC: ");
  Serial.println(WiFi.macAddress());

  // We start by connecting to WiFi Station
  Serial.print("Connecting to ");
  Serial.println(ssid);

  WiFi.begin(ssid, password);
  delay(1000);

  //setting the static addresses in function "wifi_Start
  IPAddress ip;
  IPAddress gateway;
  IPAddress subnet;
  IPAddress dns;

  WiFi.config(ip, gateway, subnet, dns);

  Serial.println("Web server running. Waiting for the ESP32 IP...");

  // Printing the ESP IP address
  Serial.print("Server IP:  ");
  Serial.println(WiFi.localIP());
  Serial.print("Port:  ");
  Serial.println("80");
  Serial.print("MAC: ");
  Serial.println(WiFi.macAddress());
  Serial.print("Wi-Fi Channel: ");
  Serial.println(WiFi.channel());
  Serial.println("\n");

  delay(500);

  WiFi.waitForConnectResult();

  Serial.printf("Connection result: %d\n", WiFi.waitForConnectResult());

  if (WiFi.waitForConnectResult() != 3) {
    delay(3000);
    wifi_Start();
  }
}


William

74 year old, retired Computer Specialist. Spend allot of time writing Arduino C++ code. I have a data logger (started building in 2013) that servers weather observations; viewable logs, graphs, supports esp32 server and domain hosted web page, has video feed, and logs data coninously to a Google Sheet month by month. I enjoy photography; have since the early age of twelve, when my grandfather, taught me darkroom skills. Now shoot images with a Pentax K-70; post processing and AI photo editing of images have me spoiled! Received my first Arduino from a friend Fall of 2013; have been programing it ever since. Major project has been “CameraRainGauge”; Projects published on Github.com

2 Responses

  1. Cornelius says:

    Good work! The Anchorage Amateur Radio Club will enjoy this article

Leave a Reply

Your email address will not be published. Required fields are marked *