Background: I'm creating a LoRa transmitter and receiver to monitor the power of my washer and dryer so I can send alerts when they've finished running.
The setup (on the basement end)
- A Heltec WiFi LoRa ESP32(V3) functions as a WiFi Access Point (AP). The distance is too far to connect to an AP in my apartment, so I am using LoRa to cover the distance.
- A Tasmota NOUS A1T smart plug connects to the AP and sends messages via MQTT
The problem: The Tasmota plug will not connect to the ESP32 AP.
The ESP32 AP is working and I am able to connect to it with my iPhone. I confirmed the iPhone to ESP32 connection is working by creating an MQTT message on the iPhone with MQTT Analyzer app and sending. The ESP32 receives it and processes the message properly.
I also have it display the number of connections on the ESP32 (via serial and the built-in display) and it says 1 connection for the iPhone.
In the console for the Tasmota plug, it gives the following error after trying to connect:
Connect failed as AP cannot be reached
The Tasmota plug does successfully connect to my home WiFi AP (which I've set as WiFi 2 in the Tasmota config).
Troubleshooting:
- Triple checked AP name and password. Tried different AP names and passwords, including no password.
- Manually set the Wifi channel, max connections, disabled WPA3, and the local IP/gateway/subnet.
- Updated the Tasmota plug to the latest firmware (Tasmota 14.4.1)
I'm not sure how to debug the WiFi side. The SoftAccessPoint documentation doesn't provide much around debugging.
Sorry, I'm a bit new to all of this so and help is very much appreciated.
Here's my sketch (I removed the passwords):
/* ---------- LORA TRANSMITTER (LAUNDRY SIDE) ---------- */
// LoRa
#include "LoRaWan_APP.h"
#include "Arduino.h"
// MQTT
#include <PicoMQTT.h>
#include <ArduinoJson.h>
// Screen
#include <Wire.h>
#include "HT_SSD1306Wire.h"
// Display: Define
SSD1306Wire factory_display(0x3c, 500000, SDA_OLED, SCL_OLED, GEOMETRY_128_64, RST_OLED); // addr , freq , i2c group , resolution , rst
// Display: Button pin definition
#define BUTTON_PIN 0
#define DEBOUNCE_TIME 50
// Display: Button variables
int buttonLastSteadyState = LOW;
int buttonLastFlickerableState = LOW;
int buttonCurrentState;
unsigned long buttonLastDebounceTime = 0;
// Display: State
bool displayOn = true;
// LoRa: Definitions
#define RF_FREQUENCY 915000000
#define TX_OUTPUT_POWER 5
#define LORA_BANDWIDTH 0
#define LORA_SPREADING_FACTOR 7
#define LORA_CODINGRATE 1
#define LORA_PREAMBLE_LENGTH 8
#define LORA_SYMBOL_TIMEOUT 0
#define LORA_FIX_LENGTH_PAYLOAD_ON false
#define LORA_IQ_INVERSION_ON false
#define RX_TIMEOUT_VALUE 1000
#define BUFFER_SIZE 30
// LoRa: Variables
char txpacket[BUFFER_SIZE];
char rxpacket[BUFFER_SIZE];
double txNumber;
bool lora_idle = true;
static RadioEvents_t RadioEvents;
void OnTxDone(void);
void OnTxTimeout(void);
//WiFi
const char *ssid = "LaundrySensors";
const char *password = "XXXXXX";
WiFiServer server(80);
// Define SensorData structure
struct SensorData {
String device;
int power;
String time;
};
// Display functions
void VextON(void) {
pinMode(Vext, OUTPUT);
digitalWrite(Vext, LOW);
}
void VextOFF(void) //Vext default OFF
{
pinMode(Vext, OUTPUT);
digitalWrite(Vext, HIGH);
}
void OnTxDone(void) {
lora_idle = true;
// Display
if (displayOn) {
factory_display.setTextAlignment(TEXT_ALIGN_LEFT);
factory_display.drawString(0, 50, "LoRa sent:");
factory_display.setTextAlignment(TEXT_ALIGN_RIGHT);
factory_display.drawString(128, 50, "Yes");
factory_display.display();
}
}
void OnTxTimeout(void) {
Radio.Sleep();
lora_idle = true;
}
// MQTT
class MQTT : public PicoMQTT::Server {
protected:
PicoMQTT::ConnectReturnCode auth(const char *client_id, const char *username, const char *password) override {
// Only accept client IDs which are 3 chars or longer
if (String(client_id).length() < 3) { // Client_id is never NULL
return PicoMQTT::CRC_IDENTIFIER_REJECTED;
}
// Only accept connections if username and password are provided
if (!username || !password) { // Username and password can not be NULL
return PicoMQTT::CRC_NOT_AUTHORIZED; // No username or password supplied
}
// Accept two user/password combinations
if ((String(username) == "laundry") && (String(password) == "XXXXXX")) {
return PicoMQTT::CRC_ACCEPTED;
}
// Reject all other credentials
return PicoMQTT::CRC_BAD_USERNAME_OR_PASSWORD;
}
} mqtt;
SensorData parseMqttMessage(const char *topic, Stream &stream, String device) {
SensorData data; // Create an instance of SensorData
StaticJsonDocument<256> sensor;
DeserializationError error = deserializeJson(sensor, stream);
if (error) {
Serial.print(F("deserializeJson() failed (in parseMessage func): "));
Serial.println(error.f_str());
return data; // Returns an empty SensorData object
}
Serial.printf("Received message in topic '%s':\n", topic);
data.device = device;
data.power = sensor["ENERGY"]["Power"];
data.time = sensor["Time"].as<String>();
Serial.printf("Device: %s\n", data.device);
Serial.printf("Power: %d\n", data.power);
Serial.printf("Time: %s\n", data.time.c_str());
// Display sensor data
if (displayOn) {
setDisplayHeader();
factory_display.setTextAlignment(TEXT_ALIGN_LEFT);
factory_display.drawString(0, 20, "> Packet received. Sending...");
factory_display.setTextAlignment(TEXT_ALIGN_LEFT);
factory_display.drawString(0, 30, "Device: " + data.device);
factory_display.setTextAlignment(TEXT_ALIGN_RIGHT);
factory_display.drawString(128, 30, "Power: " + String(data.power));
factory_display.setTextAlignment(TEXT_ALIGN_LEFT);
factory_display.drawString(0, 40, "Time:");
factory_display.setTextAlignment(TEXT_ALIGN_RIGHT);
factory_display.drawString(128, 40, data.time);
factory_display.display();
}
return data;
}
void sendLoRaPacket(int power, String time, String device) {
StaticJsonDocument<128> jsonPacket;
jsonPacket["power"] = power;
jsonPacket["time"] = time;
jsonPacket["device"] = device;
char txpacket[128]; // Adjust size based on the expected JSON length
size_t len = serializeJson(jsonPacket, txpacket);
// LoRa send (three times to increase likelyhood of reception)
// for (int i = 0; i < 3; i++) { // Loop 3 times
Serial.printf("Sending packet: \"%s\", Length: %d\r\n\n", txpacket, strlen(txpacket));
Serial.println();
Radio.Send((uint8_t *)txpacket, strlen(txpacket));
Radio.IrqProcess();
delay(500);
// }
lora_idle = false; // Set LoRa to idle after sending
}
void setDisplayHeader() {
factory_display.clear();
factory_display.display();
factory_display.setTextAlignment(TEXT_ALIGN_CENTER);
factory_display.drawString(64, 0, "-------------------- LORA TRANSMITTER --------------------");
// WiFi server status
String wifiStatus;
if (WiFi.softAPIP()) {
wifiStatus = "WiFi: Yes";
} else {
wifiStatus = "WiFi: No";
}
// Number of clients connected to WiFi
size_t numClients = WiFi.softAPgetStationNum();
// Set display
factory_display.setTextAlignment(TEXT_ALIGN_LEFT);
factory_display.drawString(0, 10, wifiStatus);
factory_display.setTextAlignment(TEXT_ALIGN_RIGHT);
factory_display.drawString(128, 10, "Clients: " + String(numClients));
}
void setDefaultDisplay() {
setDisplayHeader();
factory_display.setTextAlignment(TEXT_ALIGN_LEFT);
factory_display.drawString(0, 20, ">Waiting on next packet...");
factory_display.display();
}
void setup() {
// Setup serialvfvc
Serial.begin(115200);
// LoRa: Setup
Mcu.begin(HELTEC_BOARD, SLOW_CLK_TPYE);
// Initialize the button pin (for display)
pinMode(BUTTON_PIN, INPUT_PULLUP);
txNumber = 0;
RadioEvents.TxDone = OnTxDone;
RadioEvents.TxTimeout = OnTxTimeout;
Radio.Init(&RadioEvents);
Radio.SetChannel(RF_FREQUENCY);
Radio.SetTxConfig(MODEM_LORA, TX_OUTPUT_POWER, 0, LORA_BANDWIDTH,
LORA_SPREADING_FACTOR, LORA_CODINGRATE,
LORA_PREAMBLE_LENGTH, LORA_FIX_LENGTH_PAYLOAD_ON,
true, 0, 0, LORA_IQ_INVERSION_ON, 3000);
// Display: Setup
VextON();
delay(100);
factory_display.init();
pinMode(LED, OUTPUT);
digitalWrite(LED, LOW);
// WiFI
Serial.print("Setting AP (Access Point)...");
WiFi.softAP(ssid, password, 1, false, 4, false);
IPAddress local_IP(192, 168, 4, 1); // ESP32's IP
IPAddress gateway(192, 168, 4, 1); // Gateway
IPAddress subnet(255, 255, 255, 0); // Subnet mask
WiFi.softAPConfig(local_IP, gateway, subnet);
IPAddress IP = WiFi.softAPIP();
Serial.print("AP IP address: ");
Serial.println(IP);
server.begin();
// Subscribe to MQTT topics
mqtt.subscribe("tele/laundry/washer/SENSOR", [](const char *topic, Stream &stream) {
SensorData data = parseMqttMessage(topic, stream, "washer"); // Parse Mqtt
delay(1000);
sendLoRaPacket(data.power, data.time, data.device); // Send LoRa Packet
});
mqtt.subscribe("tele/laundry/dryer/SENSOR", [](const char *topic, Stream &stream) {
SensorData data = parseMqttMessage(topic, stream, "dryer"); // Parse Mqtt
delay(1000);
sendLoRaPacket(data.power, data.time, data.device); // Send LoRa Packet
});
mqtt.begin();
delay(250);
// Set the display
setDefaultDisplay();
}
void loop() {
mqtt.loop();
// Button pressing and debounce
buttonCurrentState = digitalRead(BUTTON_PIN);
if (buttonCurrentState != buttonLastFlickerableState) {
buttonLastDebounceTime = millis();
buttonLastFlickerableState = buttonCurrentState;
}
if ((millis() - buttonLastDebounceTime) > DEBOUNCE_TIME) { // Button
if (buttonLastSteadyState == HIGH && buttonCurrentState == LOW) {
Serial.println("Button pressed");
Serial.println();
if (displayOn) {
displayOn = false;
factory_display.clear();
factory_display.display();
} else {
displayOn = true;
setDefaultDisplay();
}
} else if (buttonLastSteadyState == LOW && buttonCurrentState == HIGH) {
Serial.println("Button released");
Serial.println();
}
buttonLastSteadyState = buttonCurrentState;
}
}