Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use ESP32's AsyncUDP when ESPALEXA_ASYNC requested #212

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
232 changes: 158 additions & 74 deletions src/Espalexa.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* @version 2.7.0
* @author Christian Schwinne
* @license MIT
* @contributors d-999
* @contributors d-999, DeLoachAero
*/

#include "Arduino.h"
Expand All @@ -27,12 +27,18 @@
#ifndef ESPALEXA_MAXDEVICES
#define ESPALEXA_MAXDEVICES 10 //this limit only has memory reasons, set it higher should you need to, max 128
#endif
#ifndef SSDP_INTERVAL
#define SSDP_INTERVAL 100 // cache control interval for SSDP Search response
#endif

//#define ESPALEXA_DEBUG
//#define ESPALEXA_DEBUG // for general debugging of ESPAlexa
//#define ESPALEXA_DEBUG_ALL_SSDP // if you want to see all the SSDP UDP discovery packet contents received when ESPALEXA_DEBUG is on

#ifdef ESPALEXA_ASYNC
#ifdef ARDUINO_ARCH_ESP32
#define USE_ESP32_ASYNC_UDP
#include <AsyncTCP.h>
#include <AsyncUDP.h>
#else
#include <ESPAsyncTCP.h>
#endif
Expand All @@ -46,15 +52,19 @@
#include <ESP8266WiFi.h>
#endif
#endif
#include <WiFiUdp.h>
#ifndef USE_ESP32_ASYNC_UDP
#include <WiFiUdp.h>
#endif

#ifdef ESPALEXA_DEBUG
#pragma message "Espalexa 2.7.0 debug mode"
#define EA_DEBUG(x) Serial.print (x)
#define EA_DEBUGLN(x) Serial.println (x)
#define EA_DEBUGBUF(buf,len) Serial.write(buf, len)
#else
#define EA_DEBUG(x)
#define EA_DEBUGLN(x)
#define EA_DEBUGBUF(buf,len)
#endif

#include "EspalexaDevice.h"
Expand All @@ -76,11 +86,15 @@ class Espalexa {
uint8_t currentDeviceCount = 0;
bool discoverable = true;
bool udpConnected = false;
bool enableSubnetFilter = false; // default false for backward compatibility

EspalexaDevice* devices[ESPALEXA_MAXDEVICES] = {};
//Keep in mind that Device IDs go from 1 to DEVICES, cpp arrays from 0 to DEVICES-1!!

#ifdef USE_ESP32_ASYNC_UDP
AsyncUDP espalexaUdp;
#else
WiFiUDP espalexaUdp;
#endif
IPAddress ipMulti;
uint32_t mac24; //bottom 24 bits of mac
String escapedMac=""; //lowercase mac address
Expand Down Expand Up @@ -283,7 +297,7 @@ class Espalexa {
}

//respond to UDP SSDP M-SEARCH
void respondToSearch()
void respondToSearch(IPAddress remoteIP, uint16_t remotePort)
{
IPAddress localIP = WiFi.localIP();
char s[16];
Expand All @@ -293,21 +307,25 @@ class Espalexa {

sprintf_P(buf,PSTR("HTTP/1.1 200 OK\r\n"
"EXT:\r\n"
"CACHE-CONTROL: max-age=100\r\n" // SSDP_INTERVAL
"CACHE-CONTROL: max-age=%d\r\n" // SSDP_INTERVAL
"LOCATION: http://%s:80/description.xml\r\n"
"SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/1.17.0\r\n" // _modelName, _modelNumber
"hue-bridgeid: %s\r\n"
"ST: urn:schemas-upnp-org:device:basic:1\r\n" // _deviceType
"USN: uuid:2f402f80-da50-11e1-9b23-%s::upnp:rootdevice\r\n" // _uuid::_deviceType
"\r\n"),s,escapedMac.c_str(),escapedMac.c_str());
"\r\n"),SSDP_INTERVAL,s,escapedMac.c_str(),escapedMac.c_str());

espalexaUdp.beginPacket(espalexaUdp.remoteIP(), espalexaUdp.remotePort());
#ifdef ARDUINO_ARCH_ESP32
espalexaUdp.write((uint8_t*)buf, strlen(buf));
#ifdef USE_ESP32_ASYNC_UDP
espalexaUdp.writeTo((uint8_t*)buf, strlen(buf), remoteIP, remotePort);
#else
espalexaUdp.write(buf);
espalexaUdp.beginPacket(remoteIP, remotePort);
#ifdef ARDUINO_ARCH_ESP32
espalexaUdp.write((uint8_t*)buf, strlen(buf));
#else
espalexaUdp.write(buf);
#endif
espalexaUdp.endPacket();
#endif
espalexaUdp.endPacket();
}

public:
Expand Down Expand Up @@ -337,10 +355,20 @@ class Espalexa {
#else
server = externalServer;
#endif
#ifdef ARDUINO_ARCH_ESP32
udpConnected = espalexaUdp.beginMulticast(IPAddress(239, 255, 255, 250), 1900);
#ifdef USE_ESP32_ASYNC_UDP
udpConnected = espalexaUdp.listenMulticast(IPAddress(239, 255, 255, 250), 1900);
if (udpConnected)
{
espalexaUdp.onPacket([this](AsyncUDPPacket packet) {
HandleUdpPacket(packet.data(), packet.length(), packet.remoteIP(), packet.remotePort());
});
}
#else
udpConnected = espalexaUdp.beginMulticast(WiFi.localIP(), IPAddress(239, 255, 255, 250), 1900);
#ifdef ARDUINO_ARCH_ESP32
udpConnected = espalexaUdp.beginMulticast(IPAddress(239, 255, 255, 250), 1900);
#else
udpConnected = espalexaUdp.beginMulticast(WiFi.localIP(), IPAddress(239, 255, 255, 250), 1900);
#endif
#endif

if (udpConnected){
Expand All @@ -352,15 +380,54 @@ class Espalexa {
EA_DEBUGLN("Failed");
return false;
}

void HandleUdpPacket(uint8_t *packetBuffer, size_t packetSize, IPAddress remoteIp, uint16_t remotePort)
{
if (packetSize < 1) return; //no new udp packet
if (!discoverable) return; //do not reply to M-SEARCH if not discoverable

const char* request = (const char *) packetBuffer;
if (strnstr(request, "M-SEARCH", packetSize) == nullptr) return;

EA_DEBUG("Got UDP! Packet Length: ");
EA_DEBUGLN(packetSize);

#if defined(ESPALEXA_DEBUG) && defined(ESPALEXA_DEBUG_ALL_SSDP)
EA_DEBUGBUF(request, packetSize);
EA_DEBUGLN("");
#endif
// check remote IP if subnet filter enabled
if (!enableSubnetFilter || ((remoteIp & WiFi.subnetMask()) == (WiFi.localIP() & WiFi.subnetMask()) && remoteIp != WiFi.localIP()))
{
// remote caller is on same subnet, or subnet filter not enabled
if (strnstr(request, "ssdp:disc", packetSize) != nullptr && //short for "ssdp:discover"
(strnstr(request, "upnp:rootd", packetSize) != nullptr || //short for "upnp:rootdevice"
strnstr(request, "ssdp:all", packetSize) != nullptr ||
strnstr(request, "asic:1", packetSize) != nullptr )) //short for "device:basic:1"
{
EA_DEBUG("Responding to search req from ");
EA_DEBUGLN(remoteIp.toString());
respondToSearch(remoteIp, remotePort);
}
}
else
{
// remote caller not on same subnet, or sent my own local IP as the remote IP
EA_DEBUG("UDP remote IP not on same subnet or using device IP! ");
EA_DEBUGLN(remoteIp.toString());
}
}

//service loop
//service loop for non-async modes
void loop() {
#ifndef ESPALEXA_ASYNC
if (server == nullptr) return; //only if begin() was not called
server->handleClient();
#endif

#ifndef USE_ESP32_ASYNC_UDP
if (!udpConnected) return;

int packetSize = espalexaUdp.parsePacket();
if (packetSize < 1) return; //no new udp packet

Expand All @@ -371,20 +438,8 @@ class Espalexa {
packetBuffer[packetSize] = 0;

espalexaUdp.flush();
if (!discoverable) return; //do not reply to M-SEARCH if not discoverable

const char* request = (const char *) packetBuffer;
if (strstr(request, "M-SEARCH") == nullptr) return;

EA_DEBUGLN(request);
if (strstr(request, "ssdp:disc") != nullptr && //short for "ssdp:discover"
(strstr(request, "upnp:rootd") != nullptr || //short for "upnp:rootdevice"
strstr(request, "ssdp:all") != nullptr ||
strstr(request, "asic:1") != nullptr )) //short for "device:basic:1"
{
EA_DEBUGLN("Responding search req...");
respondToSearch();
}
HandleUdpPacket(packetBuffer, packetSize, espalexaUdp.remoteIP(), espalexaUdp.remotePort());
#endif
}

// returns device index or 0 on failure
Expand Down Expand Up @@ -441,9 +496,19 @@ class Espalexa {
bool handleAlexaApiCall(AsyncWebServerRequest* request)
{
server = request; //copy request reference
String req = request->url(); //body from global variable
// note we may get body from global "body" variable or the request, depending on type of call
const String& req = request->url();
#ifdef ESPALEXA_DEBUG
EA_DEBUG("Request from client IP ");
if (request->client() == nullptr)
EA_DEBUGLN("Null");
else
EA_DEBUGLN(request->client()->remoteIP());
#endif

EA_DEBUGLN(request->contentType());
if (request->hasParam("body", true)) // This is necessary, otherwise ESP crashes if there is no body
// body may have been set by separate handler, but might instead be part of this request instance
if (request->hasParam("body", true))
{
EA_DEBUG("BodyMethod2");
body = request->getParam("body", true)->value();
Expand All @@ -454,25 +519,25 @@ class Espalexa {
bool handleAlexaApiCall(String req, String body)
{
#endif
EA_DEBUGLN("AlexaApiCall");
EA_DEBUG("AlexaApiCall ");
EA_DEBUGLN(req);
if (req.indexOf("api") <0) return false; //return if not an API call
EA_DEBUGLN("ok");

if (body.indexOf("devicetype") > 0) //client wants a hue api username, we don't care and give static
{
EA_DEBUGLN("devType");
body = "";
body.clear();
server->send(200, "application/json", F("[{\"success\":{\"username\":\"2WLEDHardQrI3WHYTHoMcXHgEspsM8ZZRpSKtBQr\"}}]"));
return true;
}

if ((req.indexOf("state") > 0) && (body.length() > 0)) //client wants to control light
{
server->send(200, "application/json", F("[{\"success\":{\"/lights/1/state/\": true}}]"));

uint32_t devId = req.substring(req.indexOf("lights")+7).toInt();
EA_DEBUG("ls"); EA_DEBUGLN(devId);
EA_DEBUG("ls ");
EA_DEBUGLN(devId);

unsigned idx = decodeLightKey(devId);
if (idx >= currentDeviceCount) return true; //return if invalid ID
EspalexaDevice* dev = devices[idx];
Expand All @@ -483,52 +548,60 @@ class Espalexa {
{
dev->setValue(0);
dev->setPropertyChanged(EspalexaDeviceProperty::off);
dev->doCallback();
return true;
}

if (body.indexOf("true") >0) //ON command
{
dev->setValue(dev->getLastValue());
dev->setPropertyChanged(EspalexaDeviceProperty::on);
}

if (body.indexOf("bri") >0) //BRIGHTNESS command
else
{
uint8_t briL = body.substring(body.indexOf("bri") +5).toInt();
if (briL == 255)
if (body.indexOf("true") >0) //ON command
{
dev->setValue(255);
} else {
dev->setValue(briL+1);
dev->setValue(dev->getLastValue() ? dev->getLastValue() : 255);
dev->setPropertyChanged(EspalexaDeviceProperty::on);
}

if (body.indexOf("bri") >0) //BRIGHTNESS command
{
uint8_t briL = body.substring(body.indexOf("bri") +5).toInt();
if (briL == 255)
{
dev->setValue(255);
} else {
dev->setValue(briL+1);
}
dev->setPropertyChanged(EspalexaDeviceProperty::bri);
}

if (body.indexOf("xy") >0) //COLOR command (XY mode)
{
dev->setColorXY(body.substring(body.indexOf("[") +1).toFloat(), body.substring(body.indexOf(",0") +1).toFloat());
dev->setPropertyChanged(EspalexaDeviceProperty::xy);
}

if (body.indexOf("hue") >0) //COLOR command (HS mode)
{
dev->setColor(body.substring(body.indexOf("hue") +5).toInt(), body.substring(body.indexOf("sat") +5).toInt());
dev->setPropertyChanged(EspalexaDeviceProperty::hs);
}

if (body.indexOf("ct") >0) //COLOR TEMP command (white spectrum)
{
dev->setColor(body.substring(body.indexOf("ct") +4).toInt());
dev->setPropertyChanged(EspalexaDeviceProperty::ct);
}
dev->setPropertyChanged(EspalexaDeviceProperty::bri);
}

if (body.indexOf("xy") >0) //COLOR command (XY mode)
{
dev->setColorXY(body.substring(body.indexOf("[") +1).toFloat(), body.substring(body.indexOf(",0") +1).toFloat());
dev->setPropertyChanged(EspalexaDeviceProperty::xy);
}

if (body.indexOf("hue") >0) //COLOR command (HS mode)
{
dev->setColor(body.substring(body.indexOf("hue") +5).toInt(), body.substring(body.indexOf("sat") +5).toInt());
dev->setPropertyChanged(EspalexaDeviceProperty::hs);
}

if (body.indexOf("ct") >0) //COLOR TEMP command (white spectrum)
{
dev->setColor(body.substring(body.indexOf("ct") +4).toInt());
dev->setPropertyChanged(EspalexaDeviceProperty::ct);
}

dev->doCallback();

#ifdef ESPALEXA_DEBUG
if (dev->getLastChangedProperty() == EspalexaDeviceProperty::none)
EA_DEBUGLN("STATE REQ WITHOUT BODY (likely Content-Type issue #6)");
#endif

body.clear();
char rsp[128];
sprintf_P(rsp, PSTR("[{\"success\":{\"/lights/%d/state/on\": %s}}]"), devId, dev->getState() ? "true" : "false");
server->send(200, "application/json", rsp);
EA_DEBUG("State Response: ");
EA_DEBUGLN(rsp);

dev->doCallback();

return true;
}

Expand Down Expand Up @@ -565,16 +638,20 @@ class Espalexa {
char buf[512];
deviceJsonString(devices[idx], buf);
server->send(200, "application/json", buf);
EA_DEBUG("Response: ");
EA_DEBUGLN(buf);
} else {
server->send(200, "application/json", "{}");
EA_DEBUGLN("Response: {}");
}
}

body.clear();
return true;
}

//we don't care about other api commands at this time and send empty JSON
server->send(200, "application/json", "{}");
body.clear();
return true;
}

Expand All @@ -583,6 +660,13 @@ class Espalexa {
{
discoverable = d;
}

// set true to require remote UDP addresses be on the same subnet as the device, and not the same IP as the device,
// to avoid remote denial of service attacks (two common SSDP attacks)
void setEnableSubnetFilter(bool f)
{
enableSubnetFilter = f;
}

//get EspalexaDevice at specific index
EspalexaDevice* getDevice(uint8_t index)
Expand Down