This commit is contained in:
cjkas 2026-03-10 20:23:48 +01:00
parent eb75868adb
commit a64b648f7a
56 changed files with 1003 additions and 0 deletions

1051
src/ConfigFile.cpp Normal file

File diff suppressed because it is too large Load diff

99
src/ConfigFile.h Normal file
View file

@ -0,0 +1,99 @@
#include <ArduinoJson.h>
#include <LittleFS.h>
#include "Somfy.h"
#include "ConfigSettings.h"
#ifndef configfile_h
#define configfile_h
#define CFG_VALUE_SEP ','
#define CFG_REC_END '\n'
#define CFG_TOK_NONE 0x00
#define CFG_TOK_QUOTE '"'
struct config_header_t {
uint8_t version = 1;
uint8_t repeaterRecords = 0;
uint8_t repeaterRecordSize = 0;
uint8_t roomRecords = 0;
uint16_t roomRecordSize = 0;
uint16_t shadeRecordSize = 0;
uint8_t shadeRecords = 0;
uint16_t groupRecordSize = 0;
uint8_t groupRecords = 0;
uint16_t settingsRecordSize = 0;
uint16_t netRecordSize = 0;
uint16_t transRecordSize = 0;
char serverId[10] = ""; // This must match the server id size in the ConfigSettings.
int8_t length = 0;
};
class ConfigFile {
protected:
File file;
bool readOnly = false;
bool begin(const char *filename, bool readOnly = false);
uint32_t startRecPos = 0;
bool _opened = false;
public:
config_header_t header;
void end();
bool isOpen();
bool seekRecordByIndex(uint16_t ndx);
bool readHeader();
bool seekChar(const char val);
bool writeHeader(const config_header_t &header);
bool writeHeader();
bool writeSeparator();
bool writeRecordEnd();
bool writeChar(const char val);
bool writeInt8(const int8_t val, const char tok = CFG_VALUE_SEP);
bool writeUInt8(const uint8_t val, const char tok = CFG_VALUE_SEP);
bool writeUInt16(const uint16_t val, const char tok = CFG_VALUE_SEP);
bool writeUInt32(const uint32_t val, const char tok = CFG_VALUE_SEP);
bool writeBool(const bool val, const char tok = CFG_VALUE_SEP);
bool writeFloat(const float val, const uint8_t prec, const char tok = CFG_VALUE_SEP);
bool readString(char *buff, size_t len);
bool readVarString(char *buff, size_t len);
bool skipValue(size_t len);
bool writeString(const char *val, size_t len, const char tok = CFG_VALUE_SEP);
bool writeVarString(const char *val, const char tok = CFG_VALUE_SEP);
char readChar(const char defVal = '\0');
int8_t readInt8(const int8_t defVal = 0);
uint8_t readUInt8(const uint8_t defVal = 0);
uint16_t readUInt16(const uint16_t defVal = 0);
uint32_t readUInt32(const uint32_t defVal = 0);
bool readBool(const bool defVal = false);
float readFloat(const float defVal = 0.00);
};
class ShadeConfigFile : public ConfigFile {
protected:
bool writeRepeaterRecord(SomfyShadeController *s);
bool writeRoomRecord(SomfyRoom *room);
bool writeShadeRecord(SomfyShade *shade);
bool writeGroupRecord(SomfyGroup *group);
bool writeSettingsRecord();
bool writeNetRecord();
bool writeTransRecord(transceiver_config_t &cfg);
bool readRepeaterRecord(SomfyShadeController *s);
bool readRoomRecord(SomfyRoom *room);
bool readShadeRecord(SomfyShade *shade);
bool readGroupRecord(SomfyGroup *group);
bool readSettingsRecord();
bool readNetRecord(restore_options_t &opts);
bool readTransRecord(transceiver_config_t &cfg);
public:
static bool exists();
static bool load(SomfyShadeController *somfy, const char *filename = "/shades.cfg");
static bool restore(SomfyShadeController *somfy, const char *filename, restore_options_t &opts);
bool begin(const char *filename, bool readOnly = false);
bool begin(bool readOnly = false);
bool save(SomfyShadeController *somfy);
bool backup(SomfyShadeController *somfy);
bool loadFile(SomfyShadeController *somfy, const char *filename = "/shades.cfg");
bool restoreFile(SomfyShadeController *somfy, const char *filename, restore_options_t &opts);
void end();
//bool seekRecordById(uint8_t id);
bool validate();
};
#endif

755
src/ConfigSettings.cpp Normal file
View file

@ -0,0 +1,755 @@
#include <Arduino.h>
#include <LittleFS.h> // https://github.com/espressif/arduino-esp32/tree/master/libraries/LittleFS
#include <time.h>
#include <WiFi.h>
#include <Preferences.h>
#include "ConfigSettings.h"
#include "Utils.h"
#include "esp_chip_info.h"
Preferences pref;
void restore_options_t::fromJSON(JsonObject &obj) {
if(obj.containsKey("shades")) this->shades = obj["shades"];
if(obj.containsKey("settings")) this->settings = obj["settings"];
if(obj.containsKey("network")) this->network = obj["network"];
if(obj.containsKey("transceiver")) this->transceiver = obj["transceiver"];
if(obj.containsKey("repeaters")) this->repeaters = obj["repeaters"];
if(obj.containsKey("mqtt")) this->mqtt = obj["mqtt"];
}
int8_t appver_t::compare(appver_t &ver) {
if(this->major == ver.major && this->minor == ver.minor && this->build == ver.build) return 0;
if(this->major > ver.major) return 1;
else if(this->major < ver.major) return -1;
else {
if(this->minor > ver.minor) return 1;
else if(this->minor < ver.minor) return -1;
else {
if(this->build > ver.build) return 1;
else if(this->build < ver.build) return -1;
}
}
return 0;
}
void appver_t::copy(appver_t &ver) {
strcpy(this->name, ver.name);
this->major = ver.major;
this->minor = ver.minor;
this->build = ver.build;
strcpy(this->suffix, ver.suffix);
}
void appver_t::parse(const char *ver) {
// Now lets parse this pig.
memset(this, 0x00, sizeof(appver_t));
strlcpy(this->name, ver, sizeof(this->name));
char num[3];
uint8_t i = 0;
memset(num, 0x00, sizeof(num));
for(uint8_t j = 0; j < 3 && i < strlen(ver);) {
char ch = ver[i++];
// Trim off all the prefix.
if(ch == '.') break;
if(!isdigit(ch)) continue;
if(ch != '.')
num[j++] = ch;
else
break;
}
this->major = static_cast<uint8_t>(atoi(num) & 0xFF);
memset(num, 0x00, sizeof(num));
for(uint8_t j = 0; j < 3 && i < strlen(ver);) {
char ch = ver[i++];
if(ch != '.')
num[j++] = ch;
else
break;
}
this->minor = static_cast<uint8_t>(atoi(num) & 0xFF);
memset(num, 0x00, sizeof(num));
for(uint8_t j = 0; j < 3 && i < strlen(ver);) {
char ch = ver[i++];
if(!isdigit(ch)) break;
if(ch != '.')
num[j++] = ch;
else
break;
}
this->build = static_cast<uint8_t>(atoi(num) & 0xFF);
if(strlen(ver) < i) strlcpy(this->suffix, &ver[i], sizeof(this->suffix));
}
bool appver_t::toJSON(JsonObject &obj) {
obj["name"] = this->name;
obj["major"] = this->major;
obj["minor"] = this->minor;
obj["build"] = this->build;
obj["suffix"] = this->suffix;
return true;
}
void appver_t::toJSON(JsonResponse &json) {
json.addElem("name", this->name);
json.addElem("major", this->major);
json.addElem("minor", this->minor);
json.addElem("build", this->build);
json.addElem("suffix", this->suffix);
}
void appver_t::toJSON(JsonSockEvent *json) {
json->addElem("name", this->name);
json->addElem("major", this->major);
json->addElem("minor", this->minor);
json->addElem("build", this->build);
json->addElem("suffix", this->suffix);
}
bool BaseSettings::load() { return true; }
bool BaseSettings::loadFile(const char *filename) {
size_t filesize = 10;
String data = "";
if(LittleFS.exists(filename)) {
File file = LittleFS.open(filename, "r");
filesize += file.size();
while(file.available()) {
char c = file.read();
data += c;
}
DynamicJsonDocument doc(filesize);
deserializeJson(doc, data);
JsonObject obj = doc.as<JsonObject>();
this->fromJSON(obj);
file.close();
}
return false;
}
bool BaseSettings::saveFile(const char *filename) {
File file = LittleFS.open(filename, "w");
DynamicJsonDocument doc(2048);
JsonObject obj = doc.as<JsonObject>();
this->toJSON(obj);
serializeJson(doc, file);
file.close();
return true;
}
bool BaseSettings::parseValueString(JsonObject &obj, const char *prop, char *pdest, size_t size) {
if(obj.containsKey(prop)) strlcpy(pdest, obj[prop], size);
return true;
}
bool BaseSettings::parseIPAddress(JsonObject &obj, const char *prop, IPAddress *pdest) {
if(obj.containsKey(prop)) {
char buff[16];
strlcpy(buff, obj[prop], sizeof(buff));
pdest->fromString(buff);
}
return true;
}
int BaseSettings::parseValueInt(JsonObject &obj, const char *prop, int defVal) {
if(obj.containsKey(prop)) return obj[prop];
return defVal;
}
double BaseSettings::parseValueDouble(JsonObject &obj, const char *prop, double defVal) {
if(obj.containsKey(prop)) return obj[prop];
return defVal;
}
bool ConfigSettings::begin() {
uint32_t chipId = 0;
esp_chip_info_t ci;
esp_chip_info(&ci);
switch(ci.model) {
case esp_chip_model_t::CHIP_ESP32:
strcpy(this->chipModel, "");
break;
case esp_chip_model_t::CHIP_ESP32S3:
strcpy(this->chipModel, "s3");
break;
case esp_chip_model_t::CHIP_ESP32S2:
strcpy(this->chipModel, "s2");
break;
case esp_chip_model_t::CHIP_ESP32C3:
strcpy(this->chipModel, "c3");
break;
// case esp_chip_model_t::CHIP_ESP32C2:
// strcpy(this->chipModel, "c2");
// break;
// case esp_chip_model_t::CHIP_ESP32C6:
// strcpy(this->chipModel, "c6");
// break;
case esp_chip_model_t::CHIP_ESP32H2:
strcpy(this->chipModel, "h2");
break;
default:
sprintf(this->chipModel, "UNK%d", static_cast<int>(ci.model));
break;
}
Serial.printf("Chip Model ESP32-%s\n", this->chipModel);
this->fwVersion.parse(FW_VERSION);
uint64_t mac = ESP.getEfuseMac();
for(int i=0; i<17; i=i+8) {
chipId |= ((mac >> (40 - i)) & 0xff) << i;
}
snprintf_P(this->serverId, sizeof(this->serverId), "%02X%02X%02X",
(uint16_t)((chipId >> 16) & 0xff),
(uint16_t)((chipId >> 8) & 0xff),
(uint16_t)chipId & 0xff);
this->load();
this->Security.begin();
this->IP.begin();
this->WIFI.begin();
this->Ethernet.begin();
this->NTP.begin();
this->MQTT.begin();
this->print();
return true;
}
bool ConfigSettings::load() {
this->fwVersion.parse(FW_VERSION);
this->getAppVersion();
pref.begin("CFG");
pref.getString("hostname", this->hostname, sizeof(this->hostname));
this->ssdpBroadcast = pref.getBool("ssdpBroadcast", true);
this->checkForUpdate = pref.getBool("checkForUpdate", true);
this->connType = static_cast<conn_types_t>(pref.getChar("connType", 0x00));
//Serial.printf("Preference GFG Free Entries: %d\n", pref.freeEntries());
pref.end();
if(this->connType == conn_types_t::unset) {
// We are doing this to convert the data from previous versions.
this->connType = conn_types_t::wifi;
pref.begin("WIFI");
pref.getString("hostname", this->hostname, sizeof(this->hostname));
this->ssdpBroadcast = pref.getBool("ssdpBroadcast", true);
pref.remove("hostname");
pref.remove("ssdpBroadcast");
pref.end();
this->save();
}
return true;
}
bool ConfigSettings::getAppVersion() {
char app[15];
if(!LittleFS.exists("/appversion")) return false;
File f = LittleFS.open("/appversion", "r");
memset(app, 0x00, sizeof(app));
f.read((uint8_t *)app, sizeof(app) - 1);
f.close();
_trim(app);
this->appVersion.parse(app);
return true;
}
bool ConfigSettings::save() {
pref.begin("CFG");
pref.putString("hostname", this->hostname);
pref.putBool("ssdpBroadcast", this->ssdpBroadcast);
pref.putChar("connType", static_cast<uint8_t>(this->connType));
pref.putBool("checkForUpdate", this->checkForUpdate);
pref.end();
return true;
}
bool ConfigSettings::toJSON(JsonObject &obj) {
obj["ssdpBroadcast"] = this->ssdpBroadcast;
obj["hostname"] = this->hostname;
obj["connType"] = static_cast<uint8_t>(this->connType);
obj["chipModel"] = this->chipModel;
obj["checkForUpdate"] = this->checkForUpdate;
return true;
}
void ConfigSettings::toJSON(JsonResponse &json) {
json.addElem("ssdpBroadcast", this->ssdpBroadcast);
json.addElem("hostname", this->hostname);
json.addElem("connType", static_cast<uint8_t>(this->connType));
json.addElem("chipModel", this->chipModel);
json.addElem("checkForUpdate", this->checkForUpdate);
}
bool ConfigSettings::requiresAuth() { return this->Security.type != security_types::None; }
bool ConfigSettings::fromJSON(JsonObject &obj) {
if(obj.containsKey("ssdpBroadcast")) this->ssdpBroadcast = obj["ssdpBroadcast"];
if(obj.containsKey("hostname")) this->parseValueString(obj, "hostname", this->hostname, sizeof(this->hostname));
if(obj.containsKey("connType")) this->connType = static_cast<conn_types_t>(obj["connType"].as<uint8_t>());
if(obj.containsKey("checkForUpdate")) this->checkForUpdate = obj["checkForUpdate"];
return true;
}
void ConfigSettings::print() {
this->Security.print();
Serial.printf("Connection Type: %u\n", (unsigned int) this->connType);
this->NTP.print();
if(this->connType == conn_types_t::wifi || this->connType == conn_types_t::unset) this->WIFI.print();
if(this->connType == conn_types_t::ethernet || this->connType == conn_types_t::ethernetpref) this->Ethernet.print();
}
void ConfigSettings::emitSockets() {}
void ConfigSettings::emitSockets(uint8_t num) {}
uint16_t ConfigSettings::calcSettingsRecSize() {
return strlen(this->fwVersion.name) + 3
+ strlen(this->hostname) + 3
+ strlen(this->NTP.ntpServer) + 3
+ strlen(this->NTP.posixZone) + 3
+ 6 // ssdpbroadcast
+ 6; // updateCheck
}
uint16_t ConfigSettings::calcNetRecSize() {
return 4 // connType
+ 6 // dhcp
+ this->IP.ip.toString().length() + 3
+ this->IP.gateway.toString().length() + 3
+ this->IP.subnet.toString().length() + 3
+ this->IP.dns1.toString().length() + 3
+ this->IP.dns2.toString().length() + 3
+ strlen(this->MQTT.protocol) + 3
+ strlen(this->MQTT.hostname) + 3
+ 6 // MQTT Port
+ 6 // PubDisco
+ strlen(this->MQTT.rootTopic) + 3
+ strlen(this->MQTT.discoTopic) + 3
+ 4 // ETH.boardType
+ 4 // ETH.phyType
+ 4 // ETH.clkMode
+ 5 // ETH.phyAddress
+ 5 // ETH.PWRPin
+ 5 // ETH.MDCPin
+ 5; // ETH.MDIOPin
}
bool MQTTSettings::begin() {
this->load();
return true;
}
void MQTTSettings::toJSON(JsonResponse &json) {
json.addElem("enabled", this->enabled);
json.addElem("pubDisco", this->pubDisco);
json.addElem("protocol", this->protocol);
json.addElem("hostname", this->hostname);
json.addElem("port", (uint32_t)this->port);
json.addElem("username", this->username);
json.addElem("password", this->password);
json.addElem("rootTopic", this->rootTopic);
json.addElem("discoTopic", this->discoTopic);
}
bool MQTTSettings::toJSON(JsonObject &obj) {
obj["enabled"] = this->enabled;
obj["pubDisco"] = this->pubDisco;
obj["protocol"] = this->protocol;
obj["hostname"] = this->hostname;
obj["port"] = this->port;
obj["username"] = this->username;
obj["password"] = this->password;
obj["rootTopic"] = this->rootTopic;
obj["discoTopic"] = this->discoTopic;
return true;
}
bool MQTTSettings::fromJSON(JsonObject &obj) {
if(obj.containsKey("enabled")) this->enabled = obj["enabled"];
if(obj.containsKey("pubDisco")) this->pubDisco = obj["pubDisco"];
this->parseValueString(obj, "protocol", this->protocol, sizeof(this->protocol));
this->parseValueString(obj, "hostname", this->hostname, sizeof(this->hostname));
this->parseValueString(obj, "username", this->username, sizeof(this->username));
this->parseValueString(obj, "password", this->password, sizeof(this->password));
this->parseValueString(obj, "rootTopic", this->rootTopic, sizeof(this->rootTopic));
this->parseValueString(obj, "discoTopic", this->discoTopic, sizeof(this->discoTopic));
if(obj.containsKey("port")) this->port = obj["port"];
return true;
}
bool MQTTSettings::save() {
pref.begin("MQTT");
pref.clear();
pref.putString("protocol", this->protocol);
pref.putString("hostname", this->hostname);
pref.putShort("port", this->port);
pref.putString("username", this->username);
pref.putString("password", this->password);
pref.putString("rootTopic", this->rootTopic);
pref.putBool("enabled", this->enabled);
pref.putBool("pubDisco", this->pubDisco);
pref.putString("discoTopic", this->discoTopic);
pref.end();
return true;
}
bool MQTTSettings::load() {
pref.begin("MQTT");
pref.getString("protocol", this->protocol, sizeof(this->protocol));
pref.getString("hostname", this->hostname, sizeof(this->hostname));
this->port = pref.getShort("port", 1883);
pref.getString("username", this->username, sizeof(this->username));
pref.getString("password", this->password, sizeof(this->password));
pref.getString("rootTopic", this->rootTopic, sizeof(this->rootTopic));
this->enabled = pref.getBool("enabled", false);
this->pubDisco = pref.getBool("pubDisco", false);
pref.getString("discoTopic", this->discoTopic, sizeof(this->discoTopic));
pref.end();
return true;
}
bool ConfigSettings::toJSON(DynamicJsonDocument &doc) {
doc["fwVersion"] = this->fwVersion.name;
JsonObject objWIFI = doc.createNestedObject("WIFI");
this->WIFI.toJSON(objWIFI);
JsonObject objNTP = doc.createNestedObject("NTP");
this->NTP.toJSON(objNTP);
JsonObject objMQTT = doc.createNestedObject("MQTT");
this->MQTT.toJSON(objMQTT);
return true;
}
bool NTPSettings::begin() {
this->load();
this->apply();
return true;
}
bool NTPSettings::save() {
pref.begin("NTP");
pref.clear();
pref.putString("ntpServer", this->ntpServer);
pref.putString("posixZone", this->posixZone);
pref.end();
struct tm dt;
configTime(0, 0, this->ntpServer);
if(!getLocalTime(&dt)) return false;
setenv("TZ", this->posixZone, 1);
return true;
}
bool NTPSettings::load() {
pref.begin("NTP");
pref.getString("ntpServer", this->ntpServer, sizeof(this->ntpServer));
pref.getString("posixZone", this->posixZone, sizeof(this->posixZone));
pref.end();
return true;
}
void NTPSettings::print() {
Serial.println("NTP Settings ");
Serial.print(this->ntpServer);
Serial.print(" TZ:");
Serial.println(this->posixZone);
}
bool NTPSettings::fromJSON(JsonObject &obj) {
this->parseValueString(obj, "ntpServer", this->ntpServer, sizeof(this->ntpServer));
this->parseValueString(obj, "posixZone", this->posixZone, sizeof(this->posixZone));
return true;
}
void NTPSettings::toJSON(JsonResponse &json) {
json.addElem("ntpServer", this->ntpServer);
json.addElem("posixZone", this->posixZone);
}
bool NTPSettings::toJSON(JsonObject &obj) {
obj["ntpServer"] = this->ntpServer;
obj["posixZone"] = this->posixZone;
return true;
}
bool NTPSettings::apply() {
struct tm dt;
configTime(0, 0, this->ntpServer);
if(!getLocalTime(&dt)) return false;
setenv("TZ", this->posixZone, 1);
return true;
}
IPSettings::IPSettings() {}
bool IPSettings::begin() {
this->load();
return true;
}
bool IPSettings::fromJSON(JsonObject &obj) {
if(obj.containsKey("dhcp")) this->dhcp = obj["dhcp"];
this->parseIPAddress(obj, "ip", &this->ip);
this->parseIPAddress(obj, "gateway", &this->gateway);
this->parseIPAddress(obj, "subnet", &this->subnet);
this->parseIPAddress(obj, "dns1", &this->dns1);
this->parseIPAddress(obj, "dns2", &this->dns2);
return true;
}
bool IPSettings::toJSON(JsonObject &obj) {
IPAddress ipEmpty(0,0,0,0);
obj["dhcp"] = this->dhcp;
obj["ip"] = this->ip == ipEmpty ? "" : this->ip.toString();
obj["gateway"] = this->gateway == ipEmpty ? "" : this->gateway.toString();
obj["subnet"] = this->subnet == ipEmpty ? "" : this->subnet.toString();
obj["dns1"] = this->dns1 == ipEmpty ? "" : this->dns1.toString();
obj["dns2"] = this->dns2 == ipEmpty ? "" : this->dns2.toString();
return true;
}
void IPSettings::toJSON(JsonResponse &json) {
IPAddress ipEmpty(0,0,0,0);
json.addElem("dhcp", this->dhcp);
json.addElem("ip", this->ip.toString().c_str());
json.addElem("gateway", this->gateway.toString().c_str());
json.addElem("subnet", this->subnet.toString().c_str());
json.addElem("dns1", this->dns1.toString().c_str());
json.addElem("dns2", this->dns2.toString().c_str());
}
bool IPSettings::save() {
pref.begin("IP");
pref.clear();
pref.putBool("dhcp", this->dhcp);
pref.putString("ip", this->ip.toString());
pref.putString("gateway", this->gateway.toString());
pref.putString("subnet", this->subnet.toString());
pref.putString("dns1", this->dns1.toString());
pref.putString("dns2", this->dns2.toString());
pref.end();
return true;
}
bool IPSettings::load() {
pref.begin("IP");
this->dhcp = pref.getBool("dhcp", true);
char buff[16];
if(pref.isKey("ip")) {
pref.getString("ip", buff, sizeof(buff));
this->ip.fromString(buff);
}
if(pref.isKey("gateway")) {
pref.getString("gateway", buff, sizeof(buff));
this->gateway.fromString(buff);
}
if(pref.isKey("subnet")) {
pref.getString("subnet", buff, sizeof(buff));
this->subnet.fromString(buff);
}
if(pref.isKey("dns1")) {
pref.getString("dns1", buff, sizeof(buff));
this->dns1.fromString(buff);
}
if(pref.isKey("dns2")) {
pref.getString("dns2", buff, sizeof(buff));
this->dns2.fromString(buff);
}
Serial.printf("Preference IP Free Entries: %d\n", pref.freeEntries());
pref.end();
return true;
}
bool SecuritySettings::begin() {
this->load();
return true;
}
bool SecuritySettings::fromJSON(JsonObject &obj) {
if(obj.containsKey("type")) this->type = static_cast<security_types>(obj["type"].as<uint8_t>());
this->parseValueString(obj, "username", this->username, sizeof(this->username));
this->parseValueString(obj, "password", this->password, sizeof(this->password));
this->parseValueString(obj, "pin", this->pin, sizeof(this->pin));
if(obj.containsKey("permissions")) this->permissions = obj["permissions"];
return true;
}
bool SecuritySettings::toJSON(JsonObject &obj) {
obj["type"] = static_cast<uint8_t>(this->type);
obj["username"] = this->username;
obj["password"] = this->password;
obj["pin"] = this->pin;
obj["permissions"] = this->permissions;
return true;
}
void SecuritySettings::toJSON(JsonResponse &json) {
json.addElem("type", static_cast<uint8_t>(this->type));
json.addElem("username", this->username);
json.addElem("password", this->password);
json.addElem("pin", this->pin);
json.addElem("permissions", this->permissions);
}
bool SecuritySettings::save() {
pref.begin("SEC");
pref.clear();
pref.putChar("type", static_cast<uint8_t>(this->type));
pref.putString("username", this->username);
pref.putString("password", this->password);
pref.putString("pin", this->pin);
pref.putChar("permissions", this->permissions);
pref.end();
return true;
}
bool SecuritySettings::load() {
pref.begin("SEC");
this->type = static_cast<security_types>(pref.getChar("type", 0));
if(pref.isKey("username")) pref.getString("username", this->username, sizeof(this->username));
if(pref.isKey("password")) pref.getString("password", this->password, sizeof(this->password));
if(pref.isKey("pin")) pref.getString("pin", this->pin, sizeof(this->pin));
if(pref.isKey("permissions")) this->permissions = pref.getChar("permissions", this->permissions);
pref.end();
return true;
}
void SecuritySettings::print() {
Serial.print("SECURITY Type:");
Serial.print(static_cast<uint8_t>(this->type));
Serial.print(" Username:[");
Serial.print(this->username);
Serial.print("] Password:[");
Serial.print(this->password);
Serial.print("] Pin:[");
Serial.print(this->pin);
Serial.print("] Permissions:");
Serial.println(this->permissions);
}
WifiSettings::WifiSettings() {}
bool WifiSettings::begin() {
this->load();
return true;
}
bool WifiSettings::fromJSON(JsonObject &obj) {
this->parseValueString(obj, "ssid", this->ssid, sizeof(this->ssid));
this->parseValueString(obj, "passphrase", this->passphrase, sizeof(this->passphrase));
if(obj.containsKey("roaming")) this->roaming = obj["roaming"];
if(obj.containsKey("hidden")) this->hidden = obj["hidden"];
return true;
}
bool WifiSettings::toJSON(JsonObject &obj) {
obj["ssid"] = this->ssid;
obj["passphrase"] = this->passphrase;
obj["roaming"] = this->roaming;
obj["hidden"] = this->hidden;
return true;
}
void WifiSettings::toJSON(JsonResponse &json) {
json.addElem("ssid", this->ssid);
json.addElem("passphrase", this->passphrase);
json.addElem("roaming", this->roaming);
json.addElem("hidden", this->hidden);
}
bool WifiSettings::save() {
pref.begin("WIFI");
pref.clear();
pref.putString("ssid", this->ssid);
pref.putString("passphrase", this->passphrase);
pref.putBool("roaming", this->roaming);
pref.putBool("hidden", this->hidden);
pref.end();
return true;
}
bool WifiSettings::load() {
pref.begin("WIFI");
pref.getString("ssid", this->ssid, sizeof(this->ssid));
pref.getString("passphrase", this->passphrase, sizeof(this->passphrase));
this->ssid[sizeof(this->ssid) - 1] = '\0';
this->passphrase[sizeof(this->passphrase) - 1] = '\0';
this->roaming = pref.getBool("roaming", true);
this->hidden = pref.getBool("hidden", false);
pref.end();
return true;
}
String WifiSettings::mapEncryptionType(int type) {
switch(type) {
case WIFI_AUTH_OPEN:
return "Open";
case WIFI_AUTH_WEP:
return "WEP";
case WIFI_AUTH_WPA_PSK:
return "WPA/PSK";
case WIFI_AUTH_WPA2_PSK:
return "WPA2/PSK";
case WIFI_AUTH_WPA_WPA2_PSK:
return "WPA/WPA2/PSK";
case WIFI_AUTH_WPA2_ENTERPRISE:
return "WPA/Enterprise";
}
return "Unknown";
}
void WifiSettings::print() {
Serial.println("WIFI Settings");
Serial.print(" SSID: [");
Serial.print(this->ssid);
Serial.print("] PassPhrase: [");
Serial.print(this->passphrase);
Serial.println("]");
}
void WifiSettings::printNetworks() {
int n = WiFi.scanNetworks(false, false);
Serial.print("Scanned ");
Serial.print(n);
Serial.println(" Networks...");
String network;
for(int i = 0; i < n; i++) {
if(WiFi.SSID(i).compareTo(this->ssid) == 0) Serial.print("*");
else Serial.print(" ");
Serial.print(i);
Serial.print(": ");
Serial.print(WiFi.SSID(i));
Serial.print(" (");
Serial.print(WiFi.RSSI(i));
Serial.print("dBm) CH:");
Serial.print(WiFi.channel(i));
Serial.print(" MAC:");
Serial.print(WiFi.BSSIDstr(i));
Serial.println();
}
}
bool WifiSettings::ssidExists(const char *ssid) {
int n = WiFi.scanNetworks(false, true);
for(int i = 0; i < n; i++) {
if(WiFi.SSID(i).compareTo(ssid) == 0) return true;
}
return false;
}
EthernetSettings::EthernetSettings() {}
bool EthernetSettings::begin() {
this->load();
return true;
}
bool EthernetSettings::fromJSON(JsonObject &obj) {
if(obj.containsKey("boardType")) this->boardType = obj["boardType"];
if(obj.containsKey("phyAddress")) this->phyAddress = obj["phyAddress"];
if(obj.containsKey("CLKMode")) this->CLKMode = static_cast<eth_clock_mode_t>(obj["CLKMode"]);
if(obj.containsKey("phyType")) this->phyType = static_cast<eth_phy_type_t>(obj["phyType"]);
if(obj.containsKey("PWRPin")) this->PWRPin = obj["PWRPin"];
if(obj.containsKey("MDCPin")) this->MDCPin = obj["MDCPin"];
if(obj.containsKey("MDIOPin")) this->MDIOPin = obj["MDIOPin"];
return true;
}
bool EthernetSettings::toJSON(JsonObject &obj) {
obj["boardType"] = this->boardType;
obj["phyAddress"] = this->phyAddress;
obj["CLKMode"] = static_cast<uint8_t>(this->CLKMode);
obj["phyType"] = static_cast<uint8_t>(this->phyType);
obj["PWRPin"] = this->PWRPin;
obj["MDCPin"] = this->MDCPin;
obj["MDIOPin"] = this->MDIOPin;
return true;
}
void EthernetSettings::toJSON(JsonResponse &json) {
json.addElem("boardType", this->boardType);
json.addElem("phyAddress", this->phyAddress);
json.addElem("CLKMode", static_cast<uint8_t>(this->CLKMode));
json.addElem("phyType", static_cast<uint8_t>(this->phyType));
json.addElem("PWRPin", this->PWRPin);
json.addElem("MDCPin", this->MDCPin);
json.addElem("MDIOPin", this->MDIOPin);
}
bool EthernetSettings::usesPin(uint8_t pin) {
if((this->CLKMode == 0 || this->CLKMode == 1) && pin == 0) return true;
else if(this->CLKMode == 2 && pin == 16) return true;
else if(this->CLKMode == 3 && pin == 17) return true;
else if(this->PWRPin == pin) return true;
else if(this->MDCPin == pin) return true;
else if(this->MDIOPin == pin) return true;
return false;
}
bool EthernetSettings::save() {
pref.begin("ETH");
pref.clear();
pref.putChar("boardType", this->boardType);
pref.putChar("phyAddress", this->phyAddress);
pref.putChar("phyType", static_cast<uint8_t>(this->phyType));
pref.putChar("CLKMode", static_cast<uint8_t>(this->CLKMode));
pref.putChar("PWRPin", this->PWRPin);
pref.putChar("MDCPin", this->MDCPin);
pref.putChar("MDIOPin", this->MDIOPin);
pref.end();
return true;
}
bool EthernetSettings::load() {
pref.begin("ETH");
this->boardType = pref.getChar("boardType", this->boardType);
this->phyType = static_cast<eth_phy_type_t>(pref.getChar("phyType", ETH_PHY_LAN8720));
this->CLKMode = static_cast<eth_clock_mode_t>(pref.getChar("CLKMode", ETH_CLOCK_GPIO0_IN));
this->phyAddress = pref.getChar("phyAddress", this->phyAddress);
this->PWRPin = pref.getChar("PWRPin", this->PWRPin);
this->MDCPin = pref.getChar("MDCPin", this->MDCPin);
this->MDIOPin = pref.getChar("MDIOPin", this->MDIOPin);
pref.end();
return true;
}
void EthernetSettings::print() {
Serial.println("Ethernet Settings");
Serial.printf("Board:%d PHYType:%d CLK:%d ADDR:%d PWR:%d MDC:%d MDIO:%d\n", this->boardType, this->phyType, this->CLKMode, this->phyAddress, this->PWRPin, this->MDCPin, this->MDIOPin);
}
void ConfigSettings::printAvailHeap() {
Serial.print("Max Heap: ");
Serial.println(ESP.getMaxAllocHeap());
Serial.print("Free Heap: ");
Serial.println(ESP.getFreeHeap());
Serial.print("Min Heap: ");
Serial.println(ESP.getMinFreeHeap());
}

202
src/ConfigSettings.h Normal file
View file

@ -0,0 +1,202 @@
#include <ArduinoJson.h>
#include <ETH.h>
#ifndef configsettings_h
#define configsettings_h
#include "WResp.h"
#define FW_VERSION "v2.4.7"
enum class conn_types_t : byte {
unset = 0x00,
wifi = 0x01,
ethernet = 0x02,
ethernetpref = 0x03,
ap = 0x04
};
enum DeviceStatus {
DS_OK = 0,
DS_ERROR = 1,
DS_FWUPDATE = 2
};
struct restore_options_t {
bool settings = false;
bool shades = false;
bool network = false;
bool transceiver = false;
bool repeaters = false;
bool mqtt = false;
void fromJSON(JsonObject &obj);
};
struct appver_t {
char name[15] = "";
uint8_t major = 0;
uint8_t minor = 0;
uint8_t build = 0;
char suffix[4] = "";
void parse(const char *ver);
bool toJSON(JsonObject &obj);
void toJSON(JsonResponse &json);
void toJSON(JsonSockEvent *json);
int8_t compare(appver_t &ver);
void copy(appver_t &ver);
};
class BaseSettings {
public:
bool loadFile(const char* filename);
bool fromJSON(JsonObject &obj);
bool toJSON(JsonObject &obj);
void toJSON(JsonResponse &json);
bool parseIPAddress(JsonObject &obj, const char *prop, IPAddress *);
bool parseValueString(JsonObject &obj, const char *prop, char *dest, size_t size);
int parseValueInt(JsonObject &obj, const char *prop, int defVal);
double parseValueDouble(JsonObject &obj, const char *prop, double defVal);
bool saveFile(const char* filename);
bool save();
bool load();
};
class NTPSettings: BaseSettings {
public:
char ntpServer[65] = "pool.ntp.org";
char posixZone[64] = "";
bool fromJSON(JsonObject &obj);
bool toJSON(JsonObject &obj);
void toJSON(JsonResponse &json);
bool apply();
bool begin();
bool save();
bool load();
void print();
};
class WifiSettings: BaseSettings {
public:
WifiSettings();
bool roaming = true;
bool hidden = false;
char ssid[65] = "";
char passphrase[65] = "";
//bool ssdpBroadcast = true;
bool begin();
bool fromJSON(JsonObject &obj);
bool toJSON(JsonObject &obj);
void toJSON(JsonResponse &json);
String mapEncryptionType(int type);
bool ssidExists(const char *ssid);
void printNetworks();
bool save();
bool load();
void print();
};
class EthernetSettings: BaseSettings {
public:
EthernetSettings();
uint8_t boardType = 0; // These board types are enumerated in the ui and used to set the chip settings.
eth_phy_type_t phyType = ETH_PHY_LAN8720;
eth_clock_mode_t CLKMode = ETH_CLOCK_GPIO0_IN;
int8_t phyAddress = ETH_PHY_ADDR;
int8_t PWRPin = ETH_PHY_POWER;
int8_t MDCPin = ETH_PHY_MDC;
int8_t MDIOPin = ETH_PHY_MDIO;
bool begin();
bool fromJSON(JsonObject &obj);
bool toJSON(JsonObject &obj);
void toJSON(JsonResponse &json);
bool load();
bool save();
void print();
bool usesPin(uint8_t pin);
};
class IPSettings: BaseSettings {
public:
IPSettings();
bool dhcp = true;
IPAddress ip;
IPAddress subnet = IPAddress(255,255,255,0);
IPAddress gateway = IPAddress(0,0,0,0);
IPAddress dns1 = IPAddress(0,0,0,0);
IPAddress dns2 = IPAddress(0,0,0,0);
bool begin();
bool fromJSON(JsonObject &obj);
bool toJSON(JsonObject &obj);
void toJSON(JsonResponse &json);
bool load();
bool save();
void print();
};
enum class security_types : byte {
None = 0x00,
PinEntry = 0x01,
Password = 0x02
};
enum class security_permissions : byte {
ConfigOnly = 0x01
};
class SecuritySettings: BaseSettings {
public:
security_types type = security_types::None;
char username[33] = "";
char password[33] = "";
char pin[5] = "";
uint8_t permissions = 0;
bool begin();
bool save();
bool load();
void print();
bool toJSON(JsonObject &obj);
void toJSON(JsonResponse &json);
bool fromJSON(JsonObject &obj);
};
class MQTTSettings: BaseSettings {
public:
bool enabled = false;
bool pubDisco = false;
char hostname[65] = "ESPSomfyRTS";
char protocol[10] = "mqtt://";
uint16_t port = 1883;
char username[33] = "";
char password[33] = "";
char rootTopic[65] = "";
char discoTopic[65] = "homeassistant";
bool begin();
bool save();
bool load();
bool toJSON(JsonObject &obj);
void toJSON(JsonResponse &json);
bool fromJSON(JsonObject &obj);
};
class ConfigSettings: BaseSettings {
public:
static void printAvailHeap();
char serverId[10] = "";
char hostname[32] = "ESPSomfyRTS";
char chipModel[10] = "ESP32";
conn_types_t connType = conn_types_t::unset;
appver_t fwVersion;
appver_t appVersion;
bool ssdpBroadcast = true;
bool checkForUpdate = true;
uint8_t status;
IPSettings IP;
WifiSettings WIFI;
EthernetSettings Ethernet;
NTPSettings NTP;
MQTTSettings MQTT;
SecuritySettings Security;
bool requiresAuth();
bool fromJSON(JsonObject &obj);
bool toJSON(JsonObject &obj);
void toJSON(JsonResponse &json);
bool begin();
bool save();
bool load();
void print();
void emitSockets();
void emitSockets(uint8_t num);
bool toJSON(DynamicJsonDocument &doc);
uint16_t calcSettingsRecSize();
uint16_t calcNetRecSize();
bool getAppVersion();
};
#endif

588
src/GitOTA.cpp Normal file
View file

@ -0,0 +1,588 @@
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <Update.h>
#include <HTTPClient.h>
#include <esp_task_wdt.h>
#include "ConfigSettings.h"
#include "GitOTA.h"
#include "Utils.h"
#include "Sockets.h"
#include "Somfy.h"
#include "Web.h"
#include "WResp.h"
#include "Network.h"
extern ConfigSettings settings;
extern SocketEmitter sockEmit;
extern SomfyShadeController somfy;
extern rebootDelay_t rebootDelay;
extern Web webServer;
extern Network net;
#define MAX_BUFF_SIZE 4096
void GitRelease::setReleaseProperty(const char *key, const char *val) {
if(strcmp(key, "id") == 0) this->id = atol(val);
else if(strcmp(key, "draft") == 0) this->draft = toBoolean(val, false);
else if(strcmp(key, "prerelease") == 0) this->preRelease = toBoolean(val, false);
else if(strcmp(key, "name") == 0) strlcpy(this->name, val, sizeof(this->name));
else if(strcmp(key, "tag_name") == 0) {
this->version.parse(val);
}
else if(strcmp(key, "published_at") == 0) {
//Serial.printf("Key:[%s] Value:[%s]\n", key, val);
this->releaseDate = Timestamp::parseUTCTime(val);
}
}
void GitRelease::setAssetProperty(const char *key, const char *val) {
if(strcmp(key, "name") == 0) {
//Serial.println(val);
if(strstr(val, "littlefs.bin")) this->hasFS = true;
else if(strstr(val, "ino.esp32.bin")) {
if(strlen(this->hwVersions)) strcat(this->hwVersions, ",");
strcat(this->hwVersions, "32");
}
else if(strstr(val, "ino.esp32s3.bin")) {
if(strlen(this->hwVersions)) strcat(this->hwVersions, ",");
strcat(this->hwVersions, "s3");
}
else if(strstr(val, "ino.esp32s2.bin")) {
if(strlen(this->hwVersions)) strcat(this->hwVersions, ",");
strcat(this->hwVersions, "s2");
}
else if(strstr(val, "ino.esp32c3.bin")) {
if(strlen(this->hwVersions)) strcat(this->hwVersions, ",");
strcat(this->hwVersions, "c3");
}
else if(strstr(val, "ino.esp32c2.bin")) {
if(strlen(this->hwVersions)) strcat(this->hwVersions, ",");
strcat(this->hwVersions, "c2");
}
else if(strstr(val, "ino.esp32c6.bin")) {
if(strlen(this->hwVersions)) strcat(this->hwVersions, ",");
strcat(this->hwVersions, "c6");
}
else if(strstr(val, "ino.esp32h2.bin")) {
if(strlen(this->hwVersions)) strcat(this->hwVersions, ",");
strcat(this->hwVersions, "h2");
}
}
}
void GitRelease::toJSON(JsonResponse &json) {
Timestamp ts;
char buff[20];
sprintf(buff, "%llu", this->id);
json.addElem("id", buff);
json.addElem("name", this->name);
json.addElem("date", ts.getISOTime(this->releaseDate));
json.addElem("draft", this->draft);
json.addElem("preRelease", this->preRelease);
json.addElem("main", this->main);
json.addElem("hasFS", this->hasFS);
json.addElem("hwVersions", this->hwVersions);
json.beginObject("version");
this->version.toJSON(json);
json.endObject();
}
#define ERR_CLIENT_OFFSET -50
int16_t GitRepo::getReleases(uint8_t num) {
WiFiClientSecure sclient;
sclient.setInsecure();
sclient.setHandshakeTimeout(3);
uint8_t ndx = 0;
uint8_t count = min((uint8_t)GIT_MAX_RELEASES, num);
char url[128];
memset(this->releases, 0x00, sizeof(GitRelease) * GIT_MAX_RELEASES);
sprintf(url, "https://api.github.com/repos/rstrouse/espsomfy-rts/releases?per_page=%d&page=1", count);
GitRelease *main = &this->releases[GIT_MAX_RELEASES];
main->releaseDate = Timestamp::now();
main->id = 1;
main->main = true;
strcpy(main->version.name, "main");
strcpy(main->name, "Main");
strcpy(main->hwVersions, "32,s3");
HTTPClient https;
https.setReuse(false);
if(https.begin(sclient, url)) {
esp_task_wdt_reset();
int httpCode = https.GET();
Serial.printf("[HTTPS] GET... code: %d\n", httpCode);
if(httpCode > 0) {
int len = https.getSize();
Serial.printf("[HTTPS] GET... code: %d - %d\n", httpCode, len);
if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) {
WiFiClient *stream = https.getStreamPtr();
uint8_t buff[128] = {0};
char jsonElem[32] = "";
char jsonValue[128] = "";
int arrTok = 0;
int objTok = 0;
bool inQuote = false;
bool inElem = false;
bool inValue = false;
bool awaitValue = false;
bool inAss = false;
while(https.connected() && (len > 0 || len == -1) && ndx < count) {
size_t size = stream->available();
if(size) {
esp_task_wdt_reset();
int c = stream->readBytes(buff, ((size > sizeof(buff)) ? sizeof(buff) : size));
//Serial.write(buff, c);
if(len > 0) len -= c;
// Now we should have some data.
for(uint8_t i = 0; i < c; i++) {
// Read the buffer a byte at a time until we have a key value pair.
char ch = static_cast<char>(buff[i]);
if(ch == '[') {
arrTok++;
if(arrTok == 2 && strcmp(jsonElem, "assets") == 0) {
inElem = inValue = awaitValue = false;
inAss = true;
//Serial.printf("%s: %d\n", jsonElem, arrTok);
}
else if(arrTok < 2) inAss = false;
}
else if(ch == ']') {
arrTok--;
if(arrTok < 2) inAss = false;
}
else if(ch == '{') {
objTok++;
if(objTok != 1 && !inAss) inElem = inValue = awaitValue = false;
}
else if(ch == '}') {
objTok--;
if(objTok == 0) ndx++;
}
else if(objTok == 1 || inAss) {
// We only want data from the root object.
//if(inAss) Serial.print(ch);
if(ch == '\"') {
inQuote = !inQuote;
if(inElem) {
inElem = false;
awaitValue = true;
}
else if(inValue) {
inValue = false;
inElem = false;
awaitValue = false;
if(inAss)
this->releases[ndx].setAssetProperty(jsonElem, jsonValue);
else
this->releases[ndx].setReleaseProperty(jsonElem, jsonValue);
memset(jsonElem, 0x00, sizeof(jsonElem));
memset(jsonValue, 0x00, sizeof(jsonValue));
}
else if(awaitValue) inValue = true;
else {
inElem = true;
awaitValue = false;
}
}
else if(awaitValue) {
if(ch != ' ' && ch != ':') {
strncat(jsonValue, &ch, 1);
awaitValue = false;
inValue = true;
}
}
else if((!inQuote && ch == ',') || ch == '\r' || ch == '\n') {
inElem = inValue = awaitValue = false;
if(strlen(jsonElem) > 0) {
if(inAss)
this->releases[ndx].setAssetProperty(jsonElem, jsonValue);
else
this->releases[ndx].setReleaseProperty(jsonElem, jsonValue);
}
memset(jsonElem, 0x00, sizeof(jsonElem));
memset(jsonValue, 0x00, sizeof(jsonValue));
}
else {
if(inElem) {
if(strlen(jsonElem) < sizeof(jsonElem) - 1) strncat(jsonElem, &ch, 1);
}
else if(inValue) {
if(strlen(jsonValue) < sizeof(jsonValue) - 1) strncat(jsonValue, &ch, 1);
}
}
}
}
delay(1);
}
//else break;
}
}
else {
https.end();
sclient.stop();
return httpCode;
}
}
https.end();
sclient.stop();
}
settings.printAvailHeap();
return 0;
}
void GitRepo::toJSON(JsonResponse &json) {
json.beginObject("fwVersion");
settings.fwVersion.toJSON(json);
json.endObject();
json.beginObject("appVersion");
settings.appVersion.toJSON(json);
json.endObject();
json.beginArray("releases");
for(uint8_t i = 0; i < GIT_MAX_RELEASES + 1; i++) {
if(this->releases[i].id == 0) continue;
json.beginObject();
this->releases[i].toJSON(json);
json.endObject();
}
json.endArray();
}
#define UPDATE_ERR_OFFSET 20
#define ERR_DOWNLOAD_HTTP -40
#define ERR_DOWNLOAD_BUFFER -41
#define ERR_DOWNLOAD_CONNECTION -42
void GitUpdater::loop() {
if(!net.connected()) return;
if(this->status == GIT_STATUS_READY) {
if(settings.checkForUpdate &&
(millis() > net.connectTime + 60000) && // Wait a minute before checking after connection.
(this->lastCheck + 86400000 < millis() || this->lastCheck == 0) && !rebootDelay.reboot) { // 1 day
this->checkForUpdate();
}
}
else if(this->status == GIT_AWAITING_UPDATE) {
Serial.println("Starting update process.........");
this->status = GIT_UPDATING;
this->beginUpdate(this->targetRelease);
this->status = GIT_STATUS_READY;
this->emitUpdateCheck();
}
else if(this->status == GIT_UPDATE_CANCELLING) {
Serial.println("Cancelling update process..........");
if(!this->lockFS) {
this->status = GIT_UPDATE_CANCELLED;
this->cancelled = true;
this->emitUpdateCheck();
}
}
}
void GitUpdater::checkForUpdate() {
if(this->status != 0) return; // If we are already checking.
Serial.println("Check github for updates...");
this->status = GIT_STATUS_CHECK;
settings.printAvailHeap();
this->lastCheck = millis();
if(this->checkInternet() == 0) {
GitRepo repo;
this->updateAvailable = false;
this->error = repo.getReleases(2);
if(this->error == 0) { // Get 2 releases so we can filter our pre-releases
this->setCurrentRelease(repo);
}
else {
this->emitUpdateCheck();
}
}
this->status = GIT_STATUS_READY;
}
void GitUpdater::setCurrentRelease(GitRepo &repo) {
this->updateAvailable = false;
for(uint8_t i = 0; i < 2; i++) {
if(repo.releases[i].draft || repo.releases[i].preRelease || repo.releases[i].id == 0) continue;
// Compare the versions.
this->latest.copy(repo.releases[i].version);
if(repo.releases[i].version.compare(settings.fwVersion) > 0) {
// We have a new release.
this->updateAvailable = true;
}
break;
}
this->emitUpdateCheck();
}
void GitUpdater::toJSON(JsonResponse &json) {
json.addElem("available", this->updateAvailable);
json.addElem("status", this->status);
json.addElem("error", (int32_t)this->error);
json.addElem("cancelled", this->cancelled);
json.addElem("checkForUpdate", settings.checkForUpdate);
json.addElem("inetAvailable", this->inetAvailable);
json.beginObject("fwVersion");
settings.fwVersion.toJSON(json);
json.endObject();
json.beginObject("appVersion");
settings.appVersion.toJSON(json);
json.endObject();
json.beginObject("latest");
this->latest.toJSON(json);
json.endObject();
}
void GitUpdater::emitUpdateCheck(uint8_t num) {
JsonSockEvent *json = sockEmit.beginEmit("fwStatus");
json->beginObject();
json->addElem("available", this->updateAvailable);
json->addElem("status", this->status);
json->addElem("error", (int32_t)this->error);
json->addElem("cancelled", this->cancelled);
json->addElem("checkForUpdate", settings.checkForUpdate);
json->addElem("inetAvailable", this->inetAvailable);
json->beginObject("fwVersion");
settings.fwVersion.toJSON(json);
json->endObject();
json->beginObject("appVersion");
settings.appVersion.toJSON(json);
json->endObject();
json->beginObject("latest");
this->latest.toJSON(json);
json->endObject();
json->endObject();
sockEmit.endEmit(num);
}
int GitUpdater::checkInternet() {
int err = 500;
uint32_t t = millis();
WiFiClientSecure sclient;
sclient.setInsecure();
sclient.setHandshakeTimeout(3);
esp_task_wdt_reset();
HTTPClient https;
https.setReuse(false);
if(https.begin(sclient, "https://github.com/rstrouse/ESPSomfy-RTS")) {
https.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS);
https.setTimeout(3000);
esp_task_wdt_reset();
int httpCode = https.sendRequest("HEAD");
esp_task_wdt_reset();
if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY || httpCode == HTTP_CODE_FOUND) {
err = 0;
Serial.printf("Internet is Available: %ldms\n", millis() - t);
this->inetAvailable = true;
}
else {
err = httpCode;
Serial.printf("Internet is Unavailable: %d: %ldms\n", err, millis() - t);
this->inetAvailable = false;
}
https.end();
sclient.stop();
}
esp_task_wdt_reset();
return err;
}
void GitUpdater::emitDownloadProgress(size_t total, size_t loaded, const char *evt) { this->emitDownloadProgress(255, total, loaded, evt); }
void GitUpdater::emitDownloadProgress(uint8_t num, size_t total, size_t loaded, const char *evt) {
JsonSockEvent *json = sockEmit.beginEmit(evt);
json->beginObject();
json->addElem("ver", this->targetRelease);
json->addElem("part", (int32_t)this->partition);
json->addElem("file", this->currentFile);
json->addElem("total", (uint32_t)total);
json->addElem("loaded", (uint32_t)loaded);
json->addElem("error", (uint32_t)this->error);
json->endObject();
sockEmit.endEmit(num);
/*
char buf[420];
snprintf(buf, sizeof(buf), "{\"ver\":\"%s\",\"part\":%d,\"file\":\"%s\",\"total\":%d,\"loaded\":%d, \"error\":%d}", this->targetRelease, this->partition, this->currentFile, total, loaded, this->error);
if(num >= 255) sockEmit.sendToClients(evt, buf);
else sockEmit.sendToClient(num, evt, buf);
*/
sockEmit.loop();
webServer.loop();
}
void GitUpdater::setFirmwareFile() {
esp_chip_info_t ci;
esp_chip_info(&ci);
switch(ci.model) {
case esp_chip_model_t::CHIP_ESP32S3:
strcpy(this->currentFile, "SomfyController.ino.esp32s3.bin");
break;
case esp_chip_model_t::CHIP_ESP32S2:
strcpy(this->currentFile, "SomfyController.ino.esp32s2.bin");
break;
case esp_chip_model_t::CHIP_ESP32C3:
strcpy(this->currentFile, "SomfyController.ino.esp32c3.bin");
break;
default:
strcpy(this->currentFile, "SomfyController.ino.esp32.bin");
break;
}
}
bool GitUpdater::beginUpdate(const char *version) {
Serial.println("Begin update called...");
if(strcmp(version, "Main") == 0) strcpy(this->baseUrl, "https://raw.githubusercontent.com/rstrouse/ESPSomfy-RTS/master/");
else sprintf(this->baseUrl, "https://github.com/rstrouse/ESPSomfy-RTS/releases/download/%s/", version);
strcpy(this->targetRelease, version);
this->emitUpdateCheck();
this->setFirmwareFile();
this->partition = U_FLASH;
this->lockFS = this->cancelled = false;
this->error = 0;
this->error = this->downloadFile();
if(this->error == 0 && !this->cancelled) {
somfy.commit();
strcpy(this->currentFile, "SomfyController.littlefs.bin");
this->partition = U_SPIFFS;
this->lockFS = true;
this->error = this->downloadFile();
this->lockFS = false;
if(this->error == 0) {
settings.fwVersion.parse(version);
delay(100);
Serial.println("Committing Configuration...");
somfy.commit();
}
rebootDelay.reboot = true;
rebootDelay.rebootTime = millis() + 500;
}
this->status = GIT_UPDATE_COMPLETE;
this->emitUpdateCheck();
return true;
}
bool GitUpdater::recoverFilesystem() {
sprintf(this->baseUrl, "https://github.com/rstrouse/ESPSomfy-RTS/releases/download/%s/", settings.fwVersion.name);
strcpy(this->currentFile, "SomfyController.littlefs.bin");
this->status = GIT_UPDATING;
this->partition = U_SPIFFS;
this->lockFS = true;
this->error = this->downloadFile();
this->lockFS = false;
if(this->error == 0) {
delay(100);
Serial.println("Committing Configuration...");
somfy.commit();
}
this->status = GIT_UPDATE_COMPLETE;
rebootDelay.reboot = true;
rebootDelay.rebootTime = millis() + 500;
return true;
}
bool GitUpdater::endUpdate() { return true; }
int8_t GitUpdater::downloadFile() {
Serial.printf("Begin update %s\n", this->currentFile);
WiFiClientSecure sclient;
sclient.setInsecure();
HTTPClient https;
char url[196];
sprintf(url, "%s%s", this->baseUrl, this->currentFile);
Serial.println(url);
esp_task_wdt_reset();
if(https.begin(sclient, url)) {
https.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS);
Serial.print("[HTTPS] GET...\n");
int httpCode = https.GET();
if(httpCode > 0) {
size_t len = https.getSize();
size_t total = 0;
uint8_t pct = 0;
Serial.printf("[HTTPS] GET... code: %d - %d\n", httpCode, len);
if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY || httpCode == HTTP_CODE_FOUND) {
WiFiClient *stream = https.getStreamPtr();
if(!Update.begin(len, this->partition)) {
Serial.println("Update Error detected!!!!!");
Update.printError(Serial);
https.end();
return -(Update.getError() + UPDATE_ERR_OFFSET);
}
uint8_t *buff = (uint8_t *)malloc(MAX_BUFF_SIZE);
if(buff) {
this->emitDownloadProgress(len, total);
int timeouts = 0;
while(https.connected() && (len > 0 || len == -1) && total < len) {
size_t size = stream->available();
esp_task_wdt_reset();
if(size) {
timeouts = 0;
if(this->cancelled && !this->lockFS) {
Update.abort();
free(buff);
https.end();
return -(Update.getError() + UPDATE_ERR_OFFSET);
}
int c = stream->readBytes(buff, ((size > MAX_BUFF_SIZE) ? MAX_BUFF_SIZE : size));
total += c;
//Serial.println(total);
if (Update.write(buff, c) != c) {
Update.printError(Serial);
Serial.printf("Upload of %s aborted invalid size %d\n", url, c);
free(buff);
https.end();
sclient.stop();
return -(Update.getError() + UPDATE_ERR_OFFSET);
}
// Calculate the percentage.
uint8_t p = (uint8_t)floor(((float)total / (float)len) * 100.0f);
if(p != pct) {
pct = p;
Serial.printf("LEN:%d TOTAL:%d %d%%\n", len, total, pct);
this->emitDownloadProgress(len, total);
}
delay(1);
if(total >= len) {
if(!Update.end(true)) {
Serial.println("Error downloading update...");
Update.printError(Serial);
}
else {
Serial.println("Update.end Called...");
}
https.end();
sclient.stop();
}
}
else {
timeouts++;
if(timeouts >= 500) {
Update.abort();
https.end();
free(buff);
Serial.println("Stream timeout!!!");
return -43;
}
sockEmit.loop();
webServer.loop();
delay(100);
}
}
free(buff);
if(len > total) {
Update.abort();
somfy.commit();
Serial.println("Error downloading file!!!");
return -42;
}
else
Serial.printf("Update %s complete\n", this->currentFile);
}
else {
// TODO: memory allocation error.
Serial.println("Unable to allocate memory for update!!!");
}
}
else {
Serial.printf("Invalid HTTP Code... %d", httpCode);
return httpCode;
}
}
else {
Serial.printf("Invalid HTTP Code: %d\n", httpCode);
}
https.end();
sclient.stop();
Serial.printf("End update %s\n", this->currentFile);
}
esp_task_wdt_reset();
return 0;
}

68
src/GitOTA.h Normal file
View file

@ -0,0 +1,68 @@
#ifndef GITOTA_H
#define GITOTA_H
#include <Arduino.h>
#include <ArduinoJson.h>
#include <time.h>
#include "ConfigSettings.h"
#include "WResp.h"
#define GIT_MAX_RELEASES 5
#define GIT_STATUS_READY 0
#define GIT_STATUS_CHECK 1
#define GIT_AWAITING_UPDATE 2
#define GIT_UPDATING 3
#define GIT_UPDATE_COMPLETE 4
#define GIT_UPDATE_CANCELLING 5
#define GIT_UPDATE_CANCELLED 6
class GitRelease {
public:
uint64_t id = 0;
bool draft = false;
bool preRelease = false;
bool main = false;
bool hasFS = false;
char hwVersions[128] = "";
time_t releaseDate;
char name[32] = "";
appver_t version;
void setReleaseProperty(const char *key, const char *val);
void setAssetProperty(const char *key, const char *val);
void toJSON(JsonResponse &json);
};
class GitRepo {
public:
int16_t getReleases(uint8_t num = GIT_MAX_RELEASES);
GitRelease releases[GIT_MAX_RELEASES + 1];
void toJSON(JsonResponse &json);
};
class GitUpdater {
public:
bool lockFS = false;
bool canCancel = true;
uint8_t status = 0;
uint32_t lastCheck = 0;
bool updateAvailable = false;
bool inetAvailable = false;
appver_t latest;
bool cancelled = false;
int16_t error = 0;
char targetRelease[32];
char currentFile[64] = "";
char baseUrl[128] = "";
int partition = 0;
void checkForUpdate();
bool beginUpdate(const char *release);
bool endUpdate();
int8_t downloadFile();
void setFirmwareFile();
void setCurrentRelease(GitRepo &repo);
void loop();
void toJSON(JsonResponse &json);
bool recoverFilesystem();
int checkInternet();
void emitUpdateCheck(uint8_t num=255);
void emitDownloadProgress(size_t total, size_t loaded, const char *evt = "updateProgress");
void emitDownloadProgress(uint8_t num, size_t total, size_t loaded, const char *evt = "updateProgress");
};
#endif

371
src/MQTT.cpp Normal file
View file

@ -0,0 +1,371 @@
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <esp_task_wdt.h>
#include "ConfigSettings.h"
#include "MQTT.h"
#include "Somfy.h"
#include "Network.h"
#include "Utils.h"
WiFiClient tcpClient;
PubSubClient mqttClient(tcpClient);
#define MQTT_MAX_RESPONSE 2048
static char g_content[MQTT_MAX_RESPONSE];
extern ConfigSettings settings;
extern SomfyShadeController somfy;
extern Network net;
extern rebootDelay_t rebootDelay;
bool MQTTClass::begin() {
this->suspended = false;
return true;
}
bool MQTTClass::end() {
this->suspended = true;
this->disconnect();
return true;
}
void MQTTClass::reset() {
this->disconnect();
this->lastConnect = 0;
this->connect();
}
bool MQTTClass::loop() {
if(settings.MQTT.enabled && !rebootDelay.reboot && !this->suspended && !mqttClient.connected()) {
esp_task_wdt_reset();
if(!this->connected() && net.connected()) this->connect();
}
esp_task_wdt_reset();
if(settings.MQTT.enabled) mqttClient.loop();
return true;
}
void MQTTClass::receive(const char *topic, byte*payload, uint32_t length) {
esp_task_wdt_reset(); // Make sure we do not reboot here.
Serial.print("MQTT Topic:");
Serial.print(topic);
Serial.print(" payload:");
for(uint32_t i=0; i<length; i++)
Serial.print((char)payload[i]);
Serial.println();
// We need to start at the last slash in the data
uint8_t len = strlen(topic);
uint8_t slashes = 0;
uint16_t ndx = strlen(topic) - 1;
while(ndx > 0) {
if(topic[ndx] == '/') slashes++;
if(slashes == 4) break;
ndx--;
}
char entityId[4];
char command[32];
char entityType[7];
char value[10];
memset(command, 0x00, sizeof(command));
memset(entityId, 0x00, sizeof(entityId));
memset(entityType, 0x00, sizeof(entityType));
memset(value, 0x00, sizeof(value));
uint8_t i = 0;
while(topic[ndx] == '/' && ndx < len) ndx++;
while(ndx < len) {
if(topic[ndx] != '/' && i < sizeof(entityType))
entityType[i++] = topic[ndx];
ndx++;
if(topic[ndx] == '/') break;
}
i = 0;
while(topic[ndx] == '/' && ndx < len) ndx++;
while(ndx < len) {
if(topic[ndx] != '/' && i < sizeof(entityId))
entityId[i++] = topic[ndx];
ndx++;
if(topic[ndx] == '/') break;
}
i = 0;
while(topic[ndx] == '/' && ndx < len) ndx++;
while(ndx < len) {
if(topic[ndx] != '/' && i < sizeof(command))
command[i++] = topic[ndx];
ndx++;
if(topic[ndx] == '/') break;
}
for(uint8_t j = 0; j < length && j < sizeof(value); j++)
value[j] = payload[j];
Serial.print("MQTT type:[");
Serial.print(entityType);
Serial.print("] command:[");
Serial.print(command);
Serial.print("] entityId:");
Serial.print(entityId);
Serial.print(" value:");
Serial.println(value);
if(strncmp(entityType, "shades", sizeof(entityType)) == 0) {
SomfyShade* shade = somfy.getShadeById(atoi(entityId));
if (shade) {
int val = atoi(value);
if(strncmp(command, "target", sizeof(command)) == 0) {
if(val >= 0 && val <= 100)
shade->moveToTarget(shade->transformPosition(atoi(value)));
}
if(strncmp(command, "tiltTarget", sizeof(command)) == 0) {
if(val >= 0 && val <= 100)
shade->moveToTiltTarget(atoi(value));
}
else if(strncmp(command, "direction", sizeof(command)) == 0) {
if(val < 0)
shade->sendCommand(somfy_commands::Up);
else if(val > 0)
shade->sendCommand(somfy_commands::Down);
else
shade->sendCommand(somfy_commands::My);
}
else if(strncmp(command, "mypos", sizeof(command)) == 0) {
if(val >= 0 && val <= 100)
shade->setMyPosition(val);
}
else if(strncmp(command, "myTiltPos", sizeof(command)) == 0) {
if(val >= 0 && val <= 100)
shade->setMyPosition(shade->myPos, val);
}
else if(strncmp(command, "sunFlag", sizeof(command)) == 0) {
if(val > 0) shade->sendCommand(somfy_commands::SunFlag);
else shade->sendCommand(somfy_commands::Flag);
}
else if(strncmp(command, "position", sizeof(command)) == 0) {
if(val >= 0 && val <= 100) {
shade->target = shade->currentPos = shade->transformPosition((float)val);
shade->emitState();
}
}
else if(strncmp(command, "tiltPosition", sizeof(command)) == 0) {
if(val >= 0 && val <= 100) {
shade->tiltTarget = shade->currentTiltPos = (float)val;
shade->emitState();
}
}
else if(strncmp(command, "sunny", sizeof(command)) == 0) {
if(val >= 0) shade->sendSensorCommand(-1, val, shade->repeats);
}
else if(strncmp(command, "windy", sizeof(command)) == 0) {
if(val >= 0) shade->sendSensorCommand(val, -1, shade->repeats);
}
}
}
else if(strncmp(entityType, "groups", sizeof(entityType)) == 0) {
SomfyGroup* group = somfy.getGroupById(atoi(entityId));
if (group) {
int val = atoi(value);
if(strncmp(command, "direction", sizeof(command)) == 0) {
if(val < 0)
group->sendCommand(somfy_commands::Up);
else if(val > 0)
group->sendCommand(somfy_commands::Down);
else
group->sendCommand(somfy_commands::My);
}
else if(strncmp(command, "sunFlag", sizeof(command)) == 0) {
if(val > 0)
group->sendCommand(somfy_commands::Flag);
else
group->sendCommand(somfy_commands::SunFlag);
}
else if(strncmp(command, "sunny", sizeof(command)) == 0) {
if(val >= 0) group->sendSensorCommand(-1, val, group->repeats);
}
else if(strncmp(command, "windy", sizeof(command)) == 0) {
if(val >= 0) group->sendSensorCommand(val, -1, group->repeats);
}
}
}
esp_task_wdt_reset(); // Make sure we do not reboot here.
}
bool MQTTClass::connect() {
esp_task_wdt_reset(); // Make sure we do not reboot here.
if(mqttClient.connected()) {
if(!settings.MQTT.enabled || this->suspended)
return this->disconnect();
else
return true;
}
if(settings.MQTT.enabled && !this->suspended) {
if(this->lastConnect + 10000 > millis()) return false;
uint64_t mac = ESP.getEfuseMac();
snprintf(this->clientId, sizeof(this->clientId), "client-%08x%08x", (uint32_t)((mac >> 32) & 0xFFFFFFFF), (uint32_t)(mac & 0xFFFFFFFF));
if(strlen(settings.MQTT.protocol) > 0 && strlen(settings.MQTT.hostname) > 0) {
mqttClient.setServer(settings.MQTT.hostname, settings.MQTT.port);
char lwtTopic[128] = "status";
if(strlen(settings.MQTT.rootTopic) > 0)
snprintf(lwtTopic, sizeof(lwtTopic), "%s/status", settings.MQTT.rootTopic);
esp_task_wdt_reset();
if(mqttClient.connect(this->clientId, settings.MQTT.username, settings.MQTT.password, lwtTopic, 0, true, "offline")) {
Serial.print("Successfully connected MQTT client ");
Serial.println(this->clientId);
this->publish("status", "online", true);
this->publish("ipAddress", settings.IP.ip.toString().c_str(), true);
this->publish("host", settings.hostname, true);
this->publish("firmware", settings.fwVersion.name, true);
this->publish("serverId", settings.serverId, true);
this->publish("mac", net.mac.c_str());
somfy.publish();
this->subscribe("shades/+/target/set");
this->subscribe("shades/+/tiltTarget/set");
this->subscribe("shades/+/direction/set");
this->subscribe("shades/+/mypos/set");
this->subscribe("shades/+/myTiltPos/set");
this->subscribe("shades/+/sunFlag/set");
this->subscribe("shades/+/sunny/set");
this->subscribe("shades/+/windy/set");
this->subscribe("shades/+/position/set");
this->subscribe("shades/+/tiltPosition/set");
this->subscribe("groups/+/direction/set");
this->subscribe("groups/+/sunFlag/set");
this->subscribe("groups/+/sunny/set");
this->subscribe("groups/+/windy/set");
mqttClient.setCallback(MQTTClass::receive);
Serial.println("MQTT Startup Completed");
esp_task_wdt_reset();
this->lastConnect = millis();
return true;
}
else {
Serial.print("MQTT Connection failed for: ");
Serial.println(mqttClient.state());
this->lastConnect = millis();
return false;
}
}
else
return true;
}
return true;
}
bool MQTTClass::disconnect() {
if(mqttClient.connected()) {
this->unsubscribe("shades/+/target/set");
this->unsubscribe("shades/+/direction/set");
this->unsubscribe("shades/+/tiltTarget/set");
this->unsubscribe("shades/+/mypos/set");
this->unsubscribe("shades/+/myTiltPos/set");
this->unsubscribe("shades/+/sunFlag/set");
this->unsubscribe("groups/+/direction/set");
this->unsubscribe("shades/+/sunny/set");
this->unsubscribe("shades/+/windy/set");
this->unsubscribe("shades/+/position/set");
this->unsubscribe("shades/+/tiltPosition/set");
this->unsubscribe("groups/+/direction/set");
this->unsubscribe("groups/+/sunFlag/set");
this->unsubscribe("groups/+/sunny/set");
this->unsubscribe("groups/+/windy/set");
mqttClient.disconnect();
}
return true;
}
bool MQTTClass::unsubscribe(const char *topic) {
if(mqttClient.connected()) {
char top[128];
if(strlen(settings.MQTT.rootTopic) > 0)
snprintf(top, sizeof(top), "%s/%s", settings.MQTT.rootTopic, topic);
else
strlcpy(top, topic, sizeof(top));
Serial.print("MQTT Unsubscribed from:");
Serial.println(top);
return mqttClient.unsubscribe(top);
}
return true;
}
bool MQTTClass::subscribe(const char *topic) {
if(mqttClient.connected()) {
esp_task_wdt_reset(); // Make sure we do not reboot here.
char top[128];
if(strlen(settings.MQTT.rootTopic) > 0)
snprintf(top, sizeof(top), "%s/%s", settings.MQTT.rootTopic, topic);
else
strlcpy(top, topic, sizeof(top));
Serial.print("MQTT Subscribed to:");
Serial.println(top);
return mqttClient.subscribe(top);
}
return true;
}
bool MQTTClass::publish(const char *topic, const char *payload, bool retain) {
if(mqttClient.connected()) {
char top[128];
if(strlen(settings.MQTT.rootTopic) > 0)
snprintf(top, sizeof(top), "%s/%s", settings.MQTT.rootTopic, topic);
else
strlcpy(top, topic, sizeof(top));
esp_task_wdt_reset(); // Make sure we do not reboot here.
mqttClient.publish(top, payload, retain);
return true;
}
return false;
}
bool MQTTClass::publish(const char *topic, uint32_t val, bool retain) {
snprintf(g_content, sizeof(g_content), "%u", val);
return this->publish(topic, g_content, retain);
}
bool MQTTClass::unpublish(const char *topic) {
if(mqttClient.connected()) {
char top[128];
if(strlen(settings.MQTT.rootTopic) > 0)
snprintf(top, sizeof(top), "%s/%s", settings.MQTT.rootTopic, topic);
else
strlcpy(top, topic, sizeof(top));
esp_task_wdt_reset(); // Make sure we do not reboot here.
mqttClient.publish(top, (const uint8_t *)"", 0, true);
return true;
}
return false;
// mqttClient.beginPublish(topic, 0, true);
// mqttClient.endPublish();
}
bool MQTTClass::publishBuffer(const char *topic, uint8_t *data, uint16_t len, bool retain) {
size_t res;
uint16_t offset = 0;
uint16_t to_write = len;
uint16_t buff_len;
esp_task_wdt_reset(); // Make sure we do not reboot here.
mqttClient.beginPublish(topic, len, retain);
do {
buff_len = to_write;
if(buff_len > 128) buff_len = 128;
res = mqttClient.write(data+offset, buff_len);
offset += buff_len;
to_write -= buff_len;
} while(res == buff_len && to_write > 0);
mqttClient.endPublish();
return true;
}
bool MQTTClass::publishDisco(const char *topic, JsonObject &obj, bool retain) {
serializeJson(obj, g_content, sizeof(g_content));
this->publishBuffer(topic, (uint8_t *)g_content, strlen(g_content), retain);
return true;
}
bool MQTTClass::publish(const char *topic, int8_t val, bool retain) {
snprintf(g_content, sizeof(g_content), "%d", val);
return this->publish(topic, g_content, retain);
}
bool MQTTClass::publish(const char *topic, uint8_t val, bool retain) {
snprintf(g_content, sizeof(g_content), "%u", val);
return this->publish(topic, g_content, retain);
}
bool MQTTClass::publish(const char *topic, uint16_t val, bool retain) {
snprintf(g_content, sizeof(g_content), "%u", val);
return this->publish(topic, g_content, retain);
}
bool MQTTClass::publish(const char *topic, bool val, bool retain) {
snprintf(g_content, sizeof(g_content), "%s", val ? "true" : "false");
return this->publish(topic, g_content, retain);
}
bool MQTTClass::connected() {
if(settings.MQTT.enabled) return mqttClient.connected();
return false;
}

31
src/MQTT.h Normal file
View file

@ -0,0 +1,31 @@
#ifndef MQTT_H
#define MQTT_H
#include <Arduino.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
class MQTTClass {
public:
uint64_t lastConnect = 0;
bool suspended = false;
char clientId[32] = {'\0'};
bool begin();
bool loop();
bool end();
bool connect();
bool disconnect();
bool connected();
void reset();
bool unpublish(const char *topic);
bool publish(const char *topic, const char *payload, bool retain = false);
bool publish(const char *topic, uint8_t val, bool retain = false);
bool publish(const char *topic, int8_t val, bool retain = false);
bool publish(const char *topic, uint32_t val, bool retain = false);
bool publish(const char *topic, uint16_t val, bool retain = false);
bool publish(const char *topic, bool val, bool retain = false);
bool publishBuffer(const char *topic, uint8_t *data, uint16_t len, bool retain = false);
bool publishDisco(const char *topic, JsonObject &obj, bool retain = false);
bool subscribe(const char *topic);
bool unsubscribe(const char *topic);
static void receive(const char *topic, byte *payload, uint32_t length);
};
#endif

711
src/Network.cpp Normal file
View file

@ -0,0 +1,711 @@
#include <ETH.h>
#include <WiFi.h>
#include <ESPmDNS.h>
#include <esp_task_wdt.h>
#include "ConfigSettings.h"
#include "Network.h"
#include "Web.h"
#include "Sockets.h"
#include "Utils.h"
#include "SSDP.h"
#include "MQTT.h"
extern ConfigSettings settings;
extern Web webServer;
extern SocketEmitter sockEmit;
extern MQTTClass mqtt;
extern rebootDelay_t rebootDelay;
extern Network net;
extern SomfyShadeController somfy;
static unsigned long _lastHeapEmit = 0;
static bool _apScanning = false;
static uint32_t _lastMaxHeap = 0;
static uint32_t _lastHeap = 0;
int connectRetries = 0;
void Network::end() {
SSDP.end();
mqtt.end();
sockEmit.end();
delay(100);
}
bool Network::setup() {
WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN);
WiFi.setSortMethod(WIFI_CONNECT_AP_BY_SIGNAL);
WiFi.persistent(false);
WiFi.setAutoReconnect(false);
WiFi.onEvent(this->networkEvent);
this->disconnectTime = millis();
if(WiFi.status() == WL_CONNECTED) WiFi.disconnect(true, true);
if(settings.connType == conn_types_t::wifi || settings.connType == conn_types_t::unset) {
WiFi.persistent(false);
if(settings.hostname[0] != '\0') WiFi.setHostname(settings.hostname);
Serial.print("WiFi Mode: ");
Serial.println(WiFi.getMode());
WiFi.mode(WIFI_STA);
}
sockEmit.begin();
return true;
}
conn_types_t Network::preferredConnType() {
switch(settings.connType) {
case conn_types_t::wifi:
return settings.WIFI.ssid[0] != '\0' ? conn_types_t::wifi : conn_types_t::ap;
case conn_types_t::unset:
case conn_types_t::ap:
return conn_types_t::ap;
case conn_types_t::ethernetpref:
return settings.WIFI.ssid[0] != '\0' && (!ETH.linkUp() && this->ethStarted) ? conn_types_t::wifi : conn_types_t::ethernet;
case conn_types_t::ethernet:
return ETH.linkUp() || !this->ethStarted ? conn_types_t::ethernet : conn_types_t::ap;
default:
return settings.connType;
}
}
void Network::loop() {
// ORDER OF OPERATIONS:
// ----------------------------------------------
// 1. If we are in the middle of a connection process we need to simply bail after the connect method. The
// connect method will take care of our target connection for us.
// 2. Check to see what type of target connection we need.
// a. If this is an ethernet target then the connection needs to perform a fallback if applicable.
// b. If this is a wifi target then we need to first check to see if the SSID is available.
// c. If an SSID has not been set then we need to turn on the Soft AP.
// 3. If the Soft AP is open and the target is either wifi, ethernet, or ethernetpref then
// we need to shut it down if there are no connections and the preferred connection is available.
// a. Ethernet: Check for an active ethernet connection. We cannot rely on linkup because the PHY will
// report that the link is up when no IP address is being served.
// b. WiFi: Perform synchronous scan for APs related to the SSID. If the SSID can be found then perform
// the connection process for the WiFi connection.
// c. SoftAP: This condition retains the Soft AP because no other connection method is available.
conn_types_t ctype = this->preferredConnType();
this->connect(ctype); // Connection timeout handled in connect function as well as the opening of the Soft AP if needed.
if(this->connecting()) return; // If we are currently attempting to connect to something then we need to bail here.
if(_apScanning) {
if(settings.WIFI.hidden || // This user has elected to use a hidden AP.
(this->connected() && !settings.WIFI.roaming) || // We are already connected and should not be roaming.
(this->softAPOpened && WiFi.softAPgetStationNum() != 0) || // The Soft AP is open and a user is connected.
(ctype != conn_types_t::wifi)) { // The Ethernet link is up so we should ignore this scan.
Serial.println("Cancelling WiFi STA Scan...");
_apScanning = false;
WiFi.scanDelete();
}
else {
int16_t n = WiFi.scanComplete();
if( n >= 0) { // If the scan is complete but the WiFi isn't ready this can return 0.
uint8_t bssid[6];
int32_t channel = 0;
if(this->getStrongestAP(settings.WIFI.ssid, bssid, &channel)) {
if(!WiFi.BSSID() || memcmp(bssid, WiFi.BSSID(), sizeof(bssid)) != 0) {
if(!this->connected()) {
Serial.printf("Connecting to AP %02X:%02X:%02X:%02X:%02X:%02X CH: %d\n", bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], channel);
this->connectWiFi(bssid, channel);
}
else {
Serial.printf("Found stronger AP %02X:%02X:%02X:%02X:%02X:%02X CH: %d\n", bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], channel);
this->changeAP(bssid, channel);
}
}
}
_apScanning = false;
}
}
}
if(!this->connecting() && !settings.WIFI.hidden) {
if((this->softAPOpened && WiFi.softAPgetStationNum() == 0) ||
(!this->connected() && ctype == conn_types_t::wifi)) {
// If the Soft AP is opened and there are no clients connected then we need to scan for an AP. If
// our target exists we will exit out of the Soft AP and start that connection. We are also
// going to continuously scan when there is no connection and our preferred connection is wifi.
if(ctype == conn_types_t::wifi) {
// Scan for an AP but only if we are not already scanning.
if(!_apScanning && WiFi.scanNetworks(true, false, true, 300, 0, settings.WIFI.ssid) == -1) {
_apScanning = true;
}
}
}
else if(this->connected() && ctype == conn_types_t::wifi && settings.WIFI.roaming) {
// Periodically look for a roaming AP.
if(millis() > SSID_SCAN_INTERVAL + this->lastWifiScan) {
//Serial.println("Started scan for access points");
if(!_apScanning && WiFi.scanNetworks(true, false, true, 300, 0, settings.WIFI.ssid) == -1) {
_apScanning = true;
this->lastWifiScan = millis();
}
}
}
}
if(millis() - this->lastEmit > 1500) {
// Post our connection status if needed.
this->lastEmit = millis();
if(this->connected()) {
this->emitSockets();
this->lastEmit = millis();
}
esp_task_wdt_reset(); // Make sure we do not reboot here.
}
sockEmit.loop();
mqtt.loop();
if(settings.ssdpBroadcast && this->connected()) {
if(!SSDP.isStarted) SSDP.begin();
if(SSDP.isStarted) SSDP.loop();
}
else if(!settings.ssdpBroadcast && SSDP.isStarted) SSDP.end();
}
bool Network::changeAP(const uint8_t *bssid, const int32_t channel) {
esp_task_wdt_reset(); // Make sure we do not reboot here.
if(SSDP.isStarted) SSDP.end();
mqtt.disconnect();
//sockEmit.end();
WiFi.disconnect(false, true);
this->connType = conn_types_t::unset;
this->_connecting = true;
this->connectStart = millis();
WiFi.begin(settings.WIFI.ssid, settings.WIFI.passphrase, channel, bssid);
this->connectStart = millis();
return false;
}
void Network::emitSockets() {
this->emitHeap();
if(this->needsBroadcast ||
(this->connType == conn_types_t::wifi && (abs(abs(WiFi.RSSI()) - abs(this->lastRSSI)) > 1 || WiFi.channel() != this->lastChannel))) {
this->emitSockets(255);
sockEmit.loop();
this->needsBroadcast = false;
}
}
void Network::emitSockets(uint8_t num) {
if(this->connType == conn_types_t::ethernet) {
JsonSockEvent *json = sockEmit.beginEmit("ethernet");
json->beginObject();
json->addElem("connected", this->connected());
json->addElem("speed", ETH.linkSpeed());
json->addElem("fullduplex", ETH.fullDuplex());
json->endObject();
sockEmit.endEmit(num);
}
else {
if(WiFi.status() == WL_CONNECTED) {
JsonSockEvent *json = sockEmit.beginEmit("wifiStrength");
json->beginObject();
json->addElem("ssid", WiFi.SSID().c_str());
json->addElem("strength", (int32_t)WiFi.RSSI());
json->addElem("channel", (int32_t)this->channel);
json->endObject();
sockEmit.endEmit(num);
this->lastRSSI = WiFi.RSSI();
this->lastChannel = WiFi.channel();
}
else {
JsonSockEvent *json = sockEmit.beginEmit("wifiStrength");
json->beginObject();
json->addElem("ssid", "");
json->addElem("strength", (int8_t)-100);
json->addElem("channel", (int8_t)-1);
json->endObject();
sockEmit.endEmit(num);
json = sockEmit.beginEmit("ethernet");
json->beginObject();
json->addElem("connected", false);
json->addElem("speed", (uint8_t)0);
json->addElem("fullduplex", false);
json->endObject();
sockEmit.endEmit(num);
this->lastRSSI = -100;
this->lastChannel = -1;
}
}
this->emitHeap(num);
}
void Network::setConnected(conn_types_t connType) {
esp_task_wdt_reset();
this->connType = connType;
this->connectTime = millis();
connectRetries = 0;
Serial.println("Setting connected...");
if(this->connType == conn_types_t::wifi) {
if(this->softAPOpened && WiFi.softAPgetStationNum() == 0) {
WiFi.softAPdisconnect(true);
WiFi.mode(WIFI_STA);
}
this->_connecting = false;
this->ssid = WiFi.SSID();
this->mac = WiFi.BSSIDstr();
this->strength = WiFi.RSSI();
this->channel = WiFi.channel();
this->connectAttempts++;
}
else if(this->connType == conn_types_t::ethernet) {
if(this->softAPOpened) {
Serial.println("Disonnecting from SoftAP");
WiFi.softAPdisconnect(true);
WiFi.mode(WIFI_OFF);
}
this->connectAttempts++;
this->_connecting = false;
this->wifiFallback = false;
}
// NET: Begin this in the startup.
//sockEmit.begin();
esp_task_wdt_reset();
if(this->connectAttempts == 1) {
Serial.println();
if(this->connType == conn_types_t::wifi) {
Serial.print("Successfully Connected to WiFi!!!!");
Serial.print(WiFi.localIP());
Serial.print(" (");
Serial.print(this->strength);
Serial.println("dbm)");
if(settings.IP.dhcp) {
settings.IP.ip = WiFi.localIP();
settings.IP.subnet = WiFi.subnetMask();
settings.IP.gateway = WiFi.gatewayIP();
settings.IP.dns1 = WiFi.dnsIP(0);
settings.IP.dns2 = WiFi.dnsIP(1);
}
}
else {
Serial.print("Successfully Connected to Ethernet!!! ");
Serial.print(ETH.localIP());
if(ETH.fullDuplex()) {
Serial.print(" FULL DUPLEX");
}
Serial.print(" ");
Serial.print(ETH.linkSpeed());
Serial.println("Mbps");
if(settings.IP.dhcp) {
settings.IP.ip = ETH.localIP();
settings.IP.subnet = ETH.subnetMask();
settings.IP.gateway = ETH.gatewayIP();
settings.IP.dns1 = ETH.dnsIP(0);
settings.IP.dns2 = ETH.dnsIP(1);
}
esp_task_wdt_reset();
JsonSockEvent *json = sockEmit.beginEmit("ethernet");
json->beginObject();
json->addElem("connected", this->connected());
json->addElem("speed", ETH.linkSpeed());
json->addElem("fullduplex", ETH.fullDuplex());
json->endObject();
sockEmit.endEmit();
esp_task_wdt_reset();
}
}
else {
Serial.println();
Serial.print("Reconnected after ");
Serial.print(1.0 * (millis() - this->connectStart)/1000);
Serial.print("sec IP: ");
if(this->connType == conn_types_t::wifi) {
Serial.print(WiFi.localIP());
Serial.print(" ");
Serial.print(this->mac);
Serial.print(" CH:");
Serial.print(this->channel);
Serial.print(" (");
Serial.print(this->strength);
Serial.print(" dBm)");
}
else {
Serial.print(ETH.localIP());
if(ETH.fullDuplex()) {
Serial.print(" FULL DUPLEX");
}
Serial.print(" ");
Serial.print(ETH.linkSpeed());
Serial.print("Mbps");
}
Serial.print(" Disconnected ");
Serial.print(this->connectAttempts - 1);
Serial.println(" times");
}
SSDP.setHTTPPort(80);
SSDP.setSchemaURL(0, "upnp.xml");
SSDP.setChipId(0, this->getChipId());
SSDP.setDeviceType(0, "urn:schemas-rstrouse-org:device:ESPSomfyRTS:1");
SSDP.setName(0, settings.hostname);
//SSDP.setSerialNumber(0, "C2496952-5610-47E6-A968-2FC19737A0DB");
//SSDP.setUUID(0, settings.uuid);
SSDP.setModelName(0, "ESPSomfy RTS");
if(strlen(settings.chipModel) == 0) SSDP.setModelNumber(0, "ESP32");
else {
char sModel[20] = "";
snprintf(sModel, sizeof(sModel), "ESP32-%s", settings.chipModel);
SSDP.setModelNumber(0, sModel);
}
SSDP.setModelURL(0, "https://github.com/rstrouse/ESPSomfy-RTS");
SSDP.setManufacturer(0, "rstrouse");
SSDP.setManufacturerURL(0, "https://github.com/rstrouse");
SSDP.setURL(0, "/");
SSDP.setActive(0, true);
esp_task_wdt_reset();
if(MDNS.begin(settings.hostname)) {
Serial.printf("MDNS Responder Started: serverId=%s\n", settings.serverId);
MDNS.addService("http", "tcp", 80);
//MDNS.addServiceTxt("http", "tcp", "board", "ESP32");
//MDNS.addServiceTxt("http", "tcp", "model", "ESPSomfyRTS");
MDNS.addService("espsomfy_rts", "tcp", 8080);
MDNS.addServiceTxt("espsomfy_rts", "tcp", "serverId", String(settings.serverId));
MDNS.addServiceTxt("espsomfy_rts", "tcp", "model", "ESPSomfyRTS");
MDNS.addServiceTxt("espsomfy_rts", "tcp", "version", String(settings.fwVersion.name));
}
if(settings.ssdpBroadcast) {
esp_task_wdt_reset();
SSDP.begin();
}
else if(SSDP.isStarted) SSDP.end();
esp_task_wdt_reset();
this->emitSockets();
settings.printAvailHeap();
this->needsBroadcast = true;
}
bool Network::connectWired() {
if(ETH.linkUp()) {
// If the ethernet link is re-established then we need to shut down wifi.
if(WiFi.status() == WL_CONNECTED) {
//sockEmit.end();
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
}
if(this->connType != conn_types_t::ethernet) this->setConnected(conn_types_t::ethernet);
return true;
}
else if(this->ethStarted) {
// There is no wired connection so we need to fallback if appropriate.
if(settings.connType == conn_types_t::ethernetpref && settings.WIFI.ssid[0] != '\0')
return this->connectWiFi();
}
if(this->connectAttempts > 0) {
Serial.printf("Ethernet Connection Lost... %d Reconnecting ", this->connectAttempts);
Serial.println(this->mac);
}
else
Serial.println("Connecting to Wired Ethernet");
this->_connecting = true;
this->connTarget = conn_types_t::ethernet;
this->connType = conn_types_t::unset;
if(!this->ethStarted) {
// Currently the ethernet module will leak memory if you call begin more than once.
this->ethStarted = true;
WiFi.mode(WIFI_OFF);
if(settings.hostname[0] != '\0')
ETH.setHostname(settings.hostname);
else
ETH.setHostname("ESPSomfy-RTS");
Serial.print("Set hostname to:");
Serial.println(ETH.getHostname());
if(!ETH.begin(settings.Ethernet.phyAddress, settings.Ethernet.PWRPin, settings.Ethernet.MDCPin, settings.Ethernet.MDIOPin, settings.Ethernet.phyType, settings.Ethernet.CLKMode)) {
Serial.println("Ethernet Begin failed");
this->ethStarted = false;
if(settings.connType == conn_types_t::ethernetpref) {
this->wifiFallback = true;
return connectWiFi();
}
return false;
}
else {
if(!settings.IP.dhcp) {
if(!ETH.config(settings.IP.ip, settings.IP.gateway, settings.IP.subnet, settings.IP.dns1, settings.IP.dns2)) {
Serial.println("Unable to configure static IP address....");
ETH.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE);
}
}
else
ETH.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE);
}
}
this->connectStart = millis();
return true;
}
void Network::updateHostname() {
if(settings.hostname[0] != '\0' && this->connected()) {
if(this->connType == conn_types_t::ethernet &&
strcmp(settings.hostname, ETH.getHostname()) != 0) {
Serial.printf("Updating host name to %s...\n", settings.hostname);
ETH.setHostname(settings.hostname);
MDNS.setInstanceName(settings.hostname);
SSDP.setName(0, settings.hostname);
}
else if(strcmp(settings.hostname, WiFi.getHostname()) != 0) {
Serial.printf("Updating host name to %s...\n", settings.hostname);
WiFi.setHostname(settings.hostname);
MDNS.setInstanceName(settings.hostname);
SSDP.setName(0, settings.hostname);
}
}
}
bool Network::connectWiFi(const uint8_t *bssid, const int32_t channel) {
if(this->softAPOpened && WiFi.softAPgetStationNum() > 0) {
// There is a client connected to the soft AP. We do not want to close out the connection. While both the
// Soft AP and a wifi connection can coexist on ESP32 the performance is abysmal.
WiFi.disconnect(false);
this->_connecting = false;
this->connType = conn_types_t::unset;
return true;
}
WiFi.setSleep(false);
if(!settings.IP.dhcp) {
if(!WiFi.config(settings.IP.ip, settings.IP.gateway, settings.IP.subnet, settings.IP.dns1, settings.IP.dns2))
WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE);
}
else
WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE);
if(settings.hostname[0] != '\0') WiFi.setHostname(settings.hostname);
delay(100);
if(bssid && channel > 0) {
if(WiFi.status() == WL_CONNECTED && WiFi.SSID().compareTo(settings.WIFI.ssid) == 0
&& WiFi.channel() == channel) {
this->disconnected = 0;
return true;
}
this->connTarget = conn_types_t::wifi;
this->connType = conn_types_t::unset;
Serial.println("WiFi begin...");
this->_connecting = true;
WiFi.begin(settings.WIFI.ssid, settings.WIFI.passphrase, channel, bssid);
this->connectStart = millis();
}
else if(settings.WIFI.ssid[0] != '\0') {
if(WiFi.status() == WL_CONNECTED && WiFi.SSID().compareTo(settings.WIFI.ssid) == 0) {
// If we are connected to the target SSID then just return.
this->disconnected = 0;
this->_connecting = true;
return true;
}
if(this->_connecting) return true;
this->_connecting = true;
this->connTarget = conn_types_t::wifi;
this->connType = conn_types_t::unset;
if(this->connectAttempts > 0) {
Serial.print("Connection Lost...");
Serial.print(this->mac);
Serial.print(" CH:");
Serial.print(this->channel);
Serial.print(" (");
Serial.print(this->strength);
Serial.println("dbm) ");
}
else Serial.println("Connecting to AP");
delay(100);
// There is also another method simply called hostname() but this is legacy for esp8266.
if(settings.hostname[0] != '\0') WiFi.setHostname(settings.hostname);
Serial.print("Set hostname to:");
Serial.println(WiFi.getHostname());
WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN);
WiFi.setSortMethod(WIFI_CONNECT_AP_BY_SIGNAL);
uint8_t _bssid[6];
int32_t _channel = 0;
if(!settings.WIFI.hidden && this->getStrongestAP(settings.WIFI.ssid, _bssid, &_channel)) {
Serial.printf("Found strongest AP %02X:%02X:%02X:%02X:%02X:%02X CH:%d\n", _bssid[0], _bssid[1], _bssid[2], _bssid[3], _bssid[4], _bssid[5], _channel);
WiFi.begin(settings.WIFI.ssid, settings.WIFI.passphrase, _channel, _bssid);
}
else
// If the user has the hidden flag set just connect to whatever the AP gives us.
WiFi.begin(settings.WIFI.ssid, settings.WIFI.passphrase);
}
this->connectStart = millis();
return true;
}
bool Network::connect(conn_types_t ctype) {
esp_task_wdt_reset();
if(this->connecting()) return true;
if(this->disconnectTime == 0) this->disconnectTime = millis();
if(ctype == conn_types_t::ethernet && this->connType != conn_types_t::ethernet) {
// Here we need to call the connect to ethernet.
this->connectWired();
}
else if(ctype == conn_types_t::ap || (!this->connected() && millis() > this->disconnectTime + CONNECT_TIMEOUT)) {
if(!this->softAPOpened && !this->openingSoftAP) {
this->disconnectTime = millis();
this->openSoftAP();
}
else if(this->softAPOpened && !this->openingSoftAP &&
(ctype == conn_types_t::wifi && this->connType != conn_types_t::wifi && settings.WIFI.hidden)) {
// When thge softAP is open then we need to try to connect to wifi repeatedly if the user connects to a hidden SSID.
this->connectWiFi();
}
}
else if((ctype == conn_types_t::wifi && this->connType != conn_types_t::wifi && settings.WIFI.hidden)) {
this->connectWiFi();
}
return true;
}
uint32_t Network::getChipId() {
uint32_t chipId = 0;
uint64_t mac = ESP.getEfuseMac();
for(int i=0; i<17; i=i+8) {
chipId |= ((mac >> (40 - i)) & 0xff) << i;
}
return chipId;
}
bool Network::getStrongestAP(const char *ssid, uint8_t *bssid, int32_t *channel) {
// The new AP must be at least 10dbm greater.
int32_t strength = this->connected() ? WiFi.RSSI() + 10 : -127;
int32_t chan = -1;
memset(bssid, 0x00, 6);
esp_task_wdt_delete(NULL);
int16_t n = WiFi.scanComplete();
//int16_t n = this->connected() ? WiFi.scanComplete() : WiFi.scanNetworks(false, false, false, 300, 0, ssid);
esp_task_wdt_add(NULL);
for(int16_t i = 0; i < n; i++) {
if(WiFi.SSID(i).compareTo(ssid) == 0) {
if(WiFi.RSSI(i) > strength) {
strength = WiFi.RSSI(i);
memcpy(bssid, WiFi.BSSID(i), 6);
*channel = chan = WiFi.channel(i);
}
}
}
WiFi.scanDelete();
return chan > 0;
}
bool Network::openSoftAP() {
if(this->softAPOpened || this->openingSoftAP) return true;
if(this->connected()) WiFi.disconnect(false);
this->openingSoftAP = true;
Serial.println();
Serial.println("Turning the HotSpot On");
esp_task_wdt_reset(); // Make sure we do not reboot here.
WiFi.softAP(strlen(settings.hostname) > 0 ? settings.hostname : "ESPSomfy RTS", "");
delay(200);
return true;
}
bool Network::connected() {
if(this->connecting()) return false;
else if(this->connType == conn_types_t::unset) return false;
else if(this->connType == conn_types_t::wifi) return WiFi.status() == WL_CONNECTED;
else if(this->connType == conn_types_t::ethernet) return ETH.linkUp();
else return this->connType != conn_types_t::unset;
return false;
}
bool Network::connecting() {
if(this->_connecting && millis() > this->connectStart + CONNECT_TIMEOUT) this->_connecting = false;
return this->_connecting;
}
void Network::clearConnecting() { this->_connecting = false; }
void Network::networkEvent(WiFiEvent_t event) {
switch(event) {
case ARDUINO_EVENT_WIFI_READY: Serial.println("(evt) WiFi interface ready"); break;
case ARDUINO_EVENT_WIFI_SCAN_DONE:
Serial.printf("(evt) Completed scan for access points (%d)\n", WiFi.scanComplete());
//Serial.println("(evt) Completed scan for access points");
net.lastWifiScan = millis();
break;
case ARDUINO_EVENT_WIFI_STA_START:
Serial.println("WiFi station mode started");
if(settings.hostname[0] != '\0') WiFi.setHostname(settings.hostname);
break;
case ARDUINO_EVENT_WIFI_STA_STOP: Serial.println("(evt) WiFi clients stopped"); break;
case ARDUINO_EVENT_WIFI_STA_CONNECTED: Serial.println("(evt) Connected to WiFi STA access point"); break;
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
Serial.printf("(evt) Disconnected from WiFi STA access point. Connecting: %d\n", net.connecting());
net.connType = conn_types_t::unset;
net.disconnectTime = millis();
net.clearConnecting();
break;
case ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE: Serial.println("(evt) Authentication mode of STA access point has changed"); break;
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
Serial.print("(evt) Got WiFi STA IP: ");
Serial.println(WiFi.localIP());
net.connType = conn_types_t::wifi;
net.connectTime = millis();
net.setConnected(conn_types_t::wifi);
break;
case ARDUINO_EVENT_WIFI_STA_LOST_IP: Serial.println("Lost IP address and IP address is reset to 0"); break;
case ARDUINO_EVENT_ETH_GOT_IP:
// If the Wifi is connected then drop that connection
if(WiFi.status() == WL_CONNECTED) WiFi.disconnect(true);
Serial.print("Got Ethernet IP ");
Serial.println(ETH.localIP());
net.connectTime = millis();
net.connType = conn_types_t::ethernet;
if(settings.IP.dhcp) {
settings.IP.ip = ETH.localIP();
settings.IP.subnet = ETH.subnetMask();
settings.IP.gateway = ETH.gatewayIP();
settings.IP.dns1 = ETH.dnsIP(0);
settings.IP.dns2 = ETH.dnsIP(1);
}
net.setConnected(conn_types_t::ethernet);
break;
case ARDUINO_EVENT_ETH_CONNECTED:
Serial.print("(evt) Ethernet Connected ");
break;
case ARDUINO_EVENT_ETH_DISCONNECTED:
Serial.println("(evt) Ethernet Disconnected");
net.connType = conn_types_t::unset;
net.disconnectTime = millis();
net.clearConnecting();
break;
case ARDUINO_EVENT_ETH_START:
Serial.println("(evt) Ethernet Started");
net.ethStarted = true;
break;
case ARDUINO_EVENT_ETH_STOP:
Serial.println("(evt) Ethernet Stopped");
net.connType = conn_types_t::unset;
net.ethStarted = false;
break;
case ARDUINO_EVENT_WIFI_AP_START:
Serial.print("(evt) WiFi SoftAP Started IP:");
Serial.println(WiFi.softAPIP());
net.openingSoftAP = false;
net.softAPOpened = true;
break;
case ARDUINO_EVENT_WIFI_AP_STOP:
if(!net.openingSoftAP) Serial.println("(evt) WiFi SoftAP Stopped");
net.softAPOpened = false;
break;
default:
if(event > ARDUINO_EVENT_ETH_START)
Serial.printf("(evt) Unknown Ethernet Event %d\n", event);
break;
}
}
void Network::emitHeap(uint8_t num) {
bool bEmit = false;
bool bTimeEmit = millis() - _lastHeapEmit > 15000;
bool bRoomEmit = false;
bool bValEmit = false;
if(num != 255 || this->needsBroadcast) bEmit = true;
if(millis() - _lastHeapEmit > 15000) bTimeEmit = true;
uint32_t freeHeap = ESP.getFreeHeap();
uint32_t maxHeap = ESP.getMaxAllocHeap();
uint32_t minHeap = ESP.getMinFreeHeap();
if(abs((int)(freeHeap - _lastHeap)) > 1500) bValEmit = true;
if(abs((int)(maxHeap - _lastMaxHeap)) > 1500) bValEmit = true;
bRoomEmit = sockEmit.activeClients(0) > 0;
if(bValEmit) bTimeEmit = millis() - _lastHeapEmit > 7000;
if(bEmit || bTimeEmit || bRoomEmit || bValEmit) {
JsonSockEvent *json = sockEmit.beginEmit("memStatus");
json->beginObject();
json->addElem("max", maxHeap);
json->addElem("free", freeHeap);
json->addElem("min", minHeap);
json->addElem("total", ESP.getHeapSize());
json->endObject();
if(num == 255 && bTimeEmit && bValEmit) {
sockEmit.endEmit(num);
_lastHeapEmit = millis();
_lastHeap = freeHeap;
_lastMaxHeap = maxHeap;
//Serial.printf("BROAD HEAP: Emit:%d TimeEmit:%d ValEmit:%d\n", bEmit, bTimeEmit, bValEmit);
}
else if(num != 255) {
sockEmit.endEmit(num);
//Serial.printf("TARGET HEAP %d: Emit:%d TimeEmit:%d ValEmit:%d\n", num, bEmit, bTimeEmit, bValEmit);
}
else if(bRoomEmit) {
sockEmit.endEmitRoom(0);
//Serial.printf("ROOM HEAP: Emit:%d TimeEmit:%d ValEmit:%d\n", bEmit, bTimeEmit, bValEmit);
}
}
}

59
src/Network.h Normal file
View file

@ -0,0 +1,59 @@
#include <Arduino.h>
#ifndef Network_h
#define Network_h
//enum class conn_types_t : byte;
#define CONNECT_TIMEOUT 20000
#define SSID_SCAN_INTERVAL 60000
class Network {
protected:
unsigned long lastEmit = 0;
unsigned long lastMDNS = 0;
int lastRSSI = 0;
int lastChannel = 0;
int linkSpeed = 0;
bool _connecting = false;
public:
unsigned long lastWifiScan = 0;
bool ethStarted = false;
bool wifiFallback = false;
bool softAPOpened = false;
bool openingSoftAP = false;
bool needsBroadcast = true;
conn_types_t connType = conn_types_t::unset;
conn_types_t connTarget = conn_types_t::unset;
bool connected();
bool connecting();
void clearConnecting();
conn_types_t preferredConnType();
String ssid;
String mac;
int channel;
int strength;
int disconnected = 0;
int connectAttempts = 0;
uint32_t disconnectTime = 0;
uint32_t connectStart = 0;
uint32_t connectTime = 0;
bool openSoftAP();
bool connect(conn_types_t ctype);
bool connectWiFi(const uint8_t *bssid = nullptr, const int32_t channel = -1);
bool connectWired();
void setConnected(conn_types_t connType);
bool getStrongestAP(const char *ssid, uint8_t *bssid, int32_t *channel);
bool changeAP(const uint8_t *bssid, const int32_t channel);
//int getStrengthByMac(const char *mac);
//int getStrengthBySSID(const char *ssid);
void updateHostname();
bool setup();
void loop();
void end();
void emitSockets();
void emitSockets(uint8_t num);
void emitHeap(uint8_t num = 255);
uint32_t getChipId();
static void networkEvent(WiFiEvent_t event);
};
#endif

835
src/SSDP.cpp Normal file
View file

@ -0,0 +1,835 @@
#include <functional>
#include <AsyncUDP.h>
#include "Utils.h"
#include "ConfigSettings.h"
#include "SSDP.h"
#define SSDP_PORT 1900
#define SSDP_METHOD_SIZE 10
#define SSDP_URI_SIZE 2
#define SSDP_BUFFER_SIZE 64
#define SSDP_MULTICAST_ADDR 239, 255, 255, 250
//#define DEBUG_SSDP Serial
//#define DEBUG_SSDP_PACKET Serial
extern ConfigSettings settings;
static const char _ssdp_uuid_template[] PROGMEM = "C2496952-5610-47E6-A968-2FC1%02X%02X%02X%02X";
static const char _ssdp_serial_number_template[] PROGMEM = "ESP32-%02x%02x%02x";
static const char _ssdp_usn_root_template[] PROGMEM = "uuid:%s::upnp:rootdevice";
static const char _ssdp_usn_uuid_template[] PROGMEM = "uuid:%s";
static const char _ssdp_usn_urn_template[] PROGMEM = "uuid:%s::%s";
static const char _ssdp_response_template[] PROGMEM =
"HTTP/1.1 200 OK\r\n"
"EXT:\r\n";
static const char _ssdp_notify_template[] PROGMEM =
"NOTIFY * HTTP/1.1\r\n"
"HOST: 239.255.255.250:1900\r\n"
"NTS: ssdp:alive\r\n";
static const char _ssdp_bye_template[] PROGMEM =
"NOTIFY * HTTP/1.1\r\n"
"HOST: 239.255.255.250:1900\r\n"
"NTS: ssdp:byebye\r\n"
"NT: %s\r\n"
"USN: %s\r\n"
"BOOTID.UPNP.ORG: %lu\r\n"
"CONFIGID.UPNP.ORG: %d\r\n"
"\r\n";
static const char _ssdp_packet_template[] PROGMEM =
"%s" // _ssdp_response_template / _ssdp_notify_template
"CACHE-CONTROL: max-age=%u\r\n" // _interval
"SERVER: Arduino/1.0 UPnP/1.1 ESPSomfyRTS/2.0\r\n"
"USN: %s\r\n" // _uuid
"%s: %s\r\n" // "NT" or "ST", _deviceType
"LOCATION: http://%u.%u.%u.%u:%u/%s\r\n" // WiFi.localIP(), _port, _schemaURL
"BOOTID.UPNP.ORG: %lu\r\n"
"CONFIGID.UPNP.ORG: %d\r\n"
"\r\n";
static const char _ssdp_device_schema_template[] PROGMEM =
"<device>"
"<deviceType>%s</deviceType>"
"<friendlyName>%s</friendlyName>"
"<presentationURL>%s</presentationURL>"
"<serialNumber>%s</serialNumber>"
"<modelName>%s</modelName>"
"<modelNumber>%s</modelNumber>"
"<modelURL>%s</modelURL>"
"<manufacturer>%s</manufacturer>"
"<manufacturerURL>%s</manufacturerURL>"
"<modelDescription>ESPSomfy RTS Controller</modelDescription>"
"<firmwareVersion>%s</firmwareVersion>"
"<UDN>%s</UDN>"
"<serviceList></serviceList>"
"<deviceList></deviceList>"
"</device>";
static const char _ssdp_schema_template[] PROGMEM =
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/xml\r\n"
"Connection: close\r\n"
"Access-Control-Allow-Origin: *\r\n"
"\r\n"
"<?xml version=\"1.0\"?>"
"<root xmlns=\"urn:schemas-upnp-org:device-1-0\">"
"<specVersion>"
"<major>1</major>"
"<minor>0</minor>"
"</specVersion>"
"<URLBase>http://%s:%u/</URLBase>"; // WiFi.localIP(), _port
UPNPDeviceType::UPNPDeviceType() {
lastNotified = 0;
schemaURL[0] = '\0';
uuid[0] = '\0';
deviceType[0] = '\0';
friendlyName[0] = '\0';
serialNumber[0] = '\0';
presentationURL[0] = '\0';
manufacturer[0] = '\0';
manufacturerURL[0] = '\0';
modelName[0] = '\0';
modelURL[0] = '\0';
modelNumber[0] = '\0';
m_usn[0] = '\0';
isActive = true;
}
UPNPDeviceType::UPNPDeviceType(const char *deviceType) { this->setDeviceType(deviceType); }
UPNPDeviceType::UPNPDeviceType(const char *deviceType, const char *uuid){ this->setDeviceType(deviceType); this->setUUID(uuid); }
UPNPDeviceType::UPNPDeviceType(const char *deviceType, const char *uuid, const char*friendlyName) { this->setDeviceType(deviceType); this->setUUID(uuid); this->setName(friendlyName);}
void UPNPDeviceType::setSchemaURL(const char *url) { strlcpy(schemaURL, url, sizeof(schemaURL)); }
void UPNPDeviceType::setDeviceType(const char *dtype) { strlcpy(this->deviceType, dtype, sizeof(deviceType)); }
void UPNPDeviceType::setUUID(const char *id) { snprintf_P(uuid, sizeof(uuid), PSTR("uuid:%s"), id); }
void UPNPDeviceType::setName(const char *name) { strlcpy(friendlyName, name, sizeof(friendlyName)); }
void UPNPDeviceType::setURL(const char *url) { strlcpy(presentationURL, url, sizeof(presentationURL)); }
void UPNPDeviceType::setSerialNumber(const char *sn) { strlcpy(serialNumber, sn, sizeof(serialNumber)); }
void UPNPDeviceType::setSerialNumber(const uint32_t sn) { snprintf(serialNumber, sizeof(uint32_t) * 2 + 1, "%08X", sn); }
void UPNPDeviceType::setModelName(const char *name) { strlcpy(modelName, name, sizeof(modelName)); }
void UPNPDeviceType::setModelNumber(const char *num) { strlcpy(modelNumber, num, sizeof(modelNumber));}
void UPNPDeviceType::setModelURL(const char *url) { strlcpy(modelURL, url, sizeof(modelURL)); }
void UPNPDeviceType::setManufacturer(const char *name) { strlcpy(manufacturer, name, sizeof(manufacturer)); }
void UPNPDeviceType::setManufacturerURL(const char *url) { strlcpy(manufacturerURL, url, sizeof(manufacturerURL)); }
//char * UPNPDeviceType::getUSN() { return this->getUSN(this->deviceType); }
char * UPNPDeviceType::getUSN(response_types_t responseType) {
switch(responseType) {
case response_types_t::root:
snprintf_P(this->m_usn, sizeof(this->m_usn) - 1, _ssdp_usn_root_template, this->uuid);
break;
case response_types_t::deviceType:
snprintf_P(this->m_usn, sizeof(this->m_usn) -1, _ssdp_usn_urn_template, this->uuid, this->deviceType);
break;
default:
snprintf_P(this->m_usn, sizeof(this->m_usn) - 1, _ssdp_usn_uuid_template, this->uuid);
break;
}
return this->m_usn;
}
char * UPNPDeviceType::getUSN(const char *st) {
//#ifdef DEBUG_SSDP
//DEBUG_SSDP.print("GETUSN ST: ");
//DEBUG_SSDP.println(st);
//DEBUG_SSDP.print("GETUSN UUID: ");
//DEBUG_SSDP.println(this->uuid);
//DEBUG_SSDP.print("sizeof(this->m_usn)");
//DEBUG_SSDP.println(sizeof(this->m_usn));
//#endif
if(strncmp("upnp:rootdevice", st, strlen(st)) == 0) {
snprintf_P(this->m_usn, sizeof(this->m_usn) - 1, _ssdp_usn_root_template, this->uuid);
}
else if(strncmp("uuid:", st, 5) == 0)
snprintf_P(this->m_usn, sizeof(this->m_usn) - 1, _ssdp_usn_uuid_template, this->uuid);
else if(strncmp("urn:", st, 4) == 0)
snprintf_P(this->m_usn, sizeof(this->m_usn) -1, _ssdp_usn_urn_template, this->uuid, this->deviceType);
else {
snprintf_P(this->m_usn, sizeof(this->m_usn) - 1, _ssdp_usn_uuid_template, this->uuid);
}
//#ifdef DEBUG_SSDP
//DEBUG_SSDP.print("RESUSN UUID: ");
//DEBUG_SSDP.println(this->m_usn);
//#endif
return this->m_usn;
}
void UPNPDeviceType::setChipId(uint32_t chipId) {
snprintf_P(this->uuid, sizeof(this->uuid), _ssdp_uuid_template,
0x40,
(uint16_t)((chipId >> 16) & 0xff),
(uint16_t)((chipId >> 8) & 0xff),
(uint16_t)chipId & 0xff);
snprintf_P(this->serialNumber, sizeof(this->serialNumber),
_ssdp_serial_number_template,
(uint16_t)((chipId >> 16) & 0xff),
(uint16_t)((chipId >> 8) & 0xff),
(uint16_t)chipId & 0xff);
}
SSDPClass::SSDPClass():sendQueue{false, INADDR_NONE, 0, nullptr, false, 0, "", response_types_t::root} {}
SSDPClass::~SSDPClass() { end(); this->isStarted = false; }
bool SSDPClass::begin() {
for(int i = 0; i < SSDP_QUEUE_SIZE; i++) {
this->sendQueue[i].waiting = false;
}
if(this->_server.connected()) this->end();
//assert(NULL == _server);
if(_server.connected()) {
#ifdef DEBUG_SSDP
DEBUG_SSDP.println(PSTR("Already connected to SSDP."));
this->isStarted = true;
#endif
return false;
}
this->bootId = Timestamp::epoch();
if(this->bootId < 1000) {
this->isStarted = false;
return false;
}
this->configId = (settings.fwVersion.major * 100) + (settings.fwVersion.minor * 10) + settings.fwVersion.build;
_server.onPacket([](void * arg, AsyncUDPPacket& packet) { ((SSDPClass*)(arg))->_processRequest(packet); }, this);
if(!_server.listenMulticast(IPAddress(SSDP_MULTICAST_ADDR), SSDP_PORT)) {
#ifdef DEBUG_SSDP
IPAddress mcast(SSDP_MULTICAST_ADDR);
DEBUG_SSDP.println(PSTR("Error starting SSDP listener on: "));
DEBUG_SSDP.print(mcast);
DEBUG_SSDP.print(":");
DEBUG_SSDP.println(SSDP_PORT);
#endif
return false;
}
for(uint8_t i = 0; i < this->m_cdeviceTypes; i++) {
Serial.printf("SSDP: %s - %s\n", this->deviceTypes[i].deviceType, this->deviceTypes[i].isActive ? "true" : "false");
}
this->isStarted = true;
this->_sendByeBye();
this->_sendNotify();
Serial.println("Connected to SSDP...");
return true;
}
void SSDPClass::end() {
if(!this->_server || !this->_server.connected()) return; // server isn't connected nothing to do
#ifdef DEBUG_SSDP
DEBUG_SSDP.printf(PSTR("SSDP end ...\n "));
#endif
if(this->_server.connected()) {
this->_sendByeBye();
this->_server.close();
Serial.println("Disconnected from SSDP...");
}
this->isStarted = false;
// Clear out the last notified so if the user starts us up again it will notify
// that we exist again.
for(uint8_t i = 0; i < this->m_cdeviceTypes; i++) {
this->deviceTypes[i].lastNotified = 0;
}
}
UPNPDeviceType* SSDPClass::getDeviceType(uint8_t ndx) { if(ndx < this->m_cdeviceTypes) return &this->deviceTypes[ndx]; return nullptr; }
UPNPDeviceType* SSDPClass::findDeviceByType(char *devType) {
for(uint8_t i = 0; i < this->m_cdeviceTypes; i++) {
if (strcasecmp(devType, this->deviceTypes[i].deviceType) == 0) return &this->deviceTypes[i];
}
return nullptr;
}
UPNPDeviceType* SSDPClass::findDeviceByUUID(char *uuid) {
for(uint8_t i = 0; i < this->m_cdeviceTypes; i++) {
if (strcasecmp(uuid, this->deviceTypes[i].uuid) == 0) return &this->deviceTypes[i];
}
return nullptr;
}
bool SSDPClass::_startsWith(const char *sw, const char *str) { return strncmp(sw, str, strlen(sw)) == 0; }
void SSDPClass::_parsePacket(ssdp_packet_t *pkt, AsyncUDPPacket &p) {
//MSEARCH = "M-SEARCH * HTTP/1.1\nHost: 239.255.255.250:1900\nMan: \"ssdp:discover\"\nST: roku:ecp\n";
memset(pkt, 0x00, sizeof(ssdp_packet_t));
pkt->method = NONE;
pkt->type = UNKNOWN;
pkt->recvd = millis();
pkt->valid = true;
char buffer[SSDP_BUFFER_SIZE] = {0};
uint8_t pos = 0;
while(p.available() > 0) {
char c = p.read();
if(pkt->method == NONE) {
if(c == ' ') {
_trim(buffer);
pos = 0;
if(strcmp(buffer, "M-SEARCH") == 0) pkt->method = SEARCH;
else if(strcmp(buffer, "NOTIFY") == 0) pkt->method = NOTIFY;
}
else {
if(pos < SSDP_BUFFER_SIZE - 1) {
buffer[pos++] = c;
buffer[pos] = '\0';
}
}
}
else {
if(c == '\r' || c == '\n') {
_trim(buffer);
if(strcasecmp(buffer, "* HTTP/1.1") != 0) pkt->valid = false;
break;
}
else {
if(pos < SSDP_BUFFER_SIZE) {
buffer[pos++] = c;
buffer[pos] = '\0';
}
}
}
if(c == ' ' && pkt->method == NONE) {
pkt->valid = false;
break; // We didn't get a method we want.
}
}
enum {KEY, HOST, MAN, ST, MX, AGENT, HEAD, SERVER, NTS, LOCATION, USN, NT} keys;
keys = KEY;
if(pkt->method == SEARCH) {
// Get all the key:value pairs
pos = 0;
buffer[0] = '\0';
while(p.available() > 0) {
char c = p.read();
if(keys == KEY) {
if(c == ':') {
_trim(buffer);
if(strcasecmp(buffer, "MAN") == 0) keys = MAN;
else if(strcasecmp(buffer, "ST") == 0) keys = ST;
else if(strcasecmp(buffer, "MX") == 0) keys = MX;
else if(strcasecmp(buffer, "HOST") == 0) keys = HOST;
else if(strcasecmp(buffer, "USER-AGENT") == 0) keys = AGENT;
else if(strcasecmp(buffer, "SERVER") == 0) keys = SERVER;
else if(strcasecmp(buffer, "NTS") == 0) keys = NTS;
else if(strcasecmp(buffer, "NT") == 0) keys = NT;
else if(strcasecmp(buffer, "LOCATION") == 0) keys = LOCATION;
else if(strcasecmp(buffer, "USN") == 0) keys = USN;
else keys = HEAD;
pos = 0;
buffer[0] = '\0';
}
else if(c == '\r' || c == '\n') {
// If we find a key that ends before a : then we need to bugger out.
keys = KEY;
pos = 0;
buffer[0] = '\0';
//break;
}
else {
if(pos < SSDP_BUFFER_SIZE - 1) {
buffer[pos++] = c;
buffer[pos] = '\0';
}
}
}
else {
// We are reading a value
if(c == '\r' || c == '\n') {
_trim(buffer);
switch(keys) {
case HOST:
if(strcasecmp(buffer, "239.255.255.250:1900") == 0) pkt->type = MULTICAST;
else pkt->type = UNICAST;
strcpy(pkt->host, buffer);
break;
case MAN:
strcpy(pkt->man, buffer);
break;
case ST:
strcpy(pkt->st, buffer);
break;
case MX:
pkt->mx = atoi(buffer);
break;
case AGENT:
strcpy(pkt->agent, buffer);
break;
default:
break;
}
keys = KEY;
pos = 0;
buffer[0] = '\0';
}
else {
if(pos < SSDP_BUFFER_SIZE - 1) {
buffer[pos++] = c;
buffer[pos] = '\0';
}
}
}
}
}
if(pkt->valid) {
if(pkt->method == NONE) pkt->valid = false;
if(pkt->method == SEARCH) {
if(strcmp(pkt->man, "ssdp:discover") != 0) pkt->valid = false;
if(strlen(pkt->st) == 0) pkt->valid = false;
else if(strcmp(pkt->st, "ssdp:all") != 0 &&
strcmp(pkt->st, "upnp:rootdevice") != 0 &&
!this->_startsWith("uuid:", pkt->st) &&
!this->_startsWith("urn:", pkt->st))
pkt->valid = false;
if(pkt->type == UNICAST) {
if(strlen(pkt->host) == 0) pkt->valid = false;
}
else if(pkt->type == MULTICAST) {
if(pkt->mx == 0 || isnan(pkt->mx)) pkt->valid = false;
}
}
else if(pkt->method == NOTIFY) {
// This is somebody else's notify so we should blow it off.
}
else {
pkt->valid = false;
}
}
}
IPAddress SSDPClass::localIP()
{
// Make sure we don't get a null IPAddress.
tcpip_adapter_ip_info_t ip;
if (WiFi.getMode() == WIFI_STA) {
if (tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_STA, &ip)) {
return IPAddress();
}
} else if (WiFi.getMode() == WIFI_OFF) {
if (tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_ETH, &ip)) {
return IPAddress();
}
}
return IPAddress(ip.ip.addr);
}
void SSDPClass::_sendResponse(IPAddress addr, uint16_t port, UPNPDeviceType *d, const char *st, response_types_t responseType) {
char buffer[1460];
IPAddress ip = this->localIP();
char *pbuff = (char *)malloc(strlen_P(_ssdp_response_template)+1);
if(!pbuff) {
#ifdef DEBUG_SSDP
DEBUG_SSDP.println("Out of memory for SSDP response");
#endif
return;
}
strcpy_P(pbuff, _ssdp_response_template);
// Don't use ip.toString as this fragments the heap like no tomorrow.
snprintf_P(buffer, sizeof(buffer)-1,
_ssdp_packet_template,
pbuff,
this->_interval,
d->getUSN(responseType),
"ST", st,
ip[0], ip[1], ip[2], ip[3], this->_port, d->schemaURL,
this->bootId, this->configId);
buffer[sizeof(buffer) - 1] = '\0';
this->_sendResponse(addr, port, buffer);
free(pbuff);
}
void SSDPClass::_sendResponse(IPAddress addr, uint16_t port, const char *buff) {
#ifdef DEBUG_SSDP
DEBUG_SSDP.print("Sending Response to ");
DEBUG_SSDP.print(IPAddress(addr));
DEBUG_SSDP.print(":");
DEBUG_SSDP.println(port);
DEBUG_SSDP.println(buff);
#endif
_server.writeTo((const uint8_t *)buff, strlen(buff), addr, port);
}
void SSDPClass::_sendNotify() {
for(uint8_t i = 0; i < this->m_cdeviceTypes; i++) {
UPNPDeviceType *dev = &this->deviceTypes[i];
if(i == 0 && (strlen(dev->deviceType) == 0 || !dev->isActive)) Serial.printf("The device type is empty: %s\n", dev->isActive ? "true" : "false");
if(strlen(dev->deviceType) > 0 && dev->isActive) {
unsigned long elapsed = (millis() - dev->lastNotified);
if(!dev->lastNotified || (elapsed * 5) > (this->_interval * 1000)) {
#ifdef DEBUG_SSDP
DEBUG_SSDP.print(dev->deviceType);
DEBUG_SSDP.print(" Time since last notified: ");
DEBUG_SSDP.print(elapsed/1000);
DEBUG_SSDP.print("sec ");
DEBUG_SSDP.print(this->_interval);
DEBUG_SSDP.println("sec");
#endif
this->_sendNotify(dev, i == 0);
}
else {
/*
#ifdef DEBUG_SSDP
DEBUG_SSDP.print(dev->deviceType);
DEBUG_SSDP.print(" Time since last notified: ");
DEBUG_SSDP.print(elapsed/1000);
DEBUG_SSDP.print("sec ");
DEBUG_SSDP.print(this->_interval);
DEBUG_SSDP.println("sec -- SKIPPING");
#endif
*/
}
}
}
}
void SSDPClass::_sendNotify(const char *msg) {
//_server->append(msg, strlen(msg));
//_server->send(IPAddress(SSDP_MULTICAST_ADDR), SSDP_PORT);
#ifdef DEBUG_SSDP
DEBUG_SSDP.println("--------------- SEND NOTIFY PACKET ----------------");
DEBUG_SSDP.println(msg);
#endif
_server.writeTo((uint8_t *)msg, strlen(msg), IPAddress(SSDP_MULTICAST_ADDR), SSDP_PORT);
}
void SSDPClass::_sendNotify(UPNPDeviceType *d) { this->_sendNotify(d, strcmp(d->deviceType, this->deviceTypes[0].deviceType) == 0); }
void SSDPClass::_sendNotify(UPNPDeviceType *d, bool root) {
char buffer[1460];
IPAddress ip = this->localIP();
/*
"NOTIFY * HTTP/1.1\r\n"
"HOST: 239.255.255.250:1900\r\n"
"NTS: ssdp:alive\r\n";
"CACHE-CONTROL: max-age=%u\r\n" // _interval
"SERVER: Arduino/1.0 UPNP/1.1 %s/%s\r\n" // _modelName, _modelNumber
"USN: %s\r\n" // _uuid
"%s: %s\r\n" // "NT" or "ST", _deviceType
"LOCATION: http://%s:%u/%s\r\n" // WiFi.localIP(), _port, _schemaURL
*/
char *pbuff = (char *)malloc(strlen_P(_ssdp_notify_template)+1);
if(!pbuff) {
#ifdef DEBUG_SSDP
DEBUG_SSDP.println(PSTR("Out of memory for SSDP response"));
#endif
return;
}
strcpy_P(pbuff, _ssdp_notify_template);
if(root) {
// Send 1 for root
snprintf_P(buffer, sizeof(buffer),
_ssdp_packet_template,
pbuff,
this->_interval,
d->getUSN(response_types_t::root), // USN
"NT", "upnp:rootdevice",
ip[0], ip[1], ip[2], ip[3], this->_port, d->schemaURL, this->bootId, this->configId);
this->_sendNotify(buffer);
}
// Send 1 for uuid
snprintf_P(buffer, sizeof(buffer),
_ssdp_packet_template,
pbuff,
this->_interval,
d->getUSN(response_types_t::uuid),
"NT", d->getUSN(response_types_t::uuid),
ip[0], ip[1], ip[2], ip[3], _port, d->schemaURL, this->bootId, this->configId);
this->_sendNotify(buffer);
// Send 1 for deviceType
snprintf_P(buffer, sizeof(buffer),
_ssdp_packet_template,
pbuff,
this->_interval,
d->getUSN(response_types_t::deviceType),
"NT", d->deviceType,
ip[0], ip[1], ip[2], ip[3], _port, d->schemaURL, this->bootId, this->configId);
this->_sendNotify(buffer);
d->lastNotified = millis();
free(pbuff);
}
void SSDPClass::setActive(uint8_t ndx, bool isActive) {
UPNPDeviceType *d = &this->deviceTypes[ndx];
if(!isActive) {
if(this->isStarted) this->_sendByeBye(d, ndx == 0);
d->isActive = false;
}
else {
d->isActive = true;
d->lastNotified = 0;
if(this->isStarted) this->_sendNotify(d, ndx == 0);
}
}
void SSDPClass::_sendByeBye() {
for(uint8_t i = 0; i < this->m_cdeviceTypes; i++) {
UPNPDeviceType *dev = &this->deviceTypes[i];
if(strlen(dev->deviceType) > 0) {
#ifdef DEBUG_SSDP
DEBUG_SSDP.print(dev->deviceType);
DEBUG_SSDP.print(" ");
DEBUG_SSDP.print(this->_interval);
DEBUG_SSDP.println("sec");
#endif
this->_sendByeBye(dev, i == 0);
}
}
}
void SSDPClass::_sendByeBye(const char *msg) {
//_server->append(msg, strlen(msg));
//_server->send(IPAddress(SSDP_MULTICAST_ADDR), SSDP_PORT);
_server.writeTo((uint8_t *)msg, strlen(msg), IPAddress(SSDP_MULTICAST_ADDR), SSDP_PORT);
}
void SSDPClass::_sendByeBye(UPNPDeviceType *d) { this->_sendByeBye(d, strcmp(d->deviceType, this->deviceTypes[0].deviceType) == 0); }
void SSDPClass::_sendByeBye(UPNPDeviceType *d, bool root) {
char buffer[1460];
IPAddress ip = this->localIP();
/*
"NOTIFY * HTTP/1.1\r\n"
"HOST: 239.255.255.250:1900\r\n"
"NTS: ssdp:byebye\r\n";
"CACHE-CONTROL: max-age=%u\r\n" // _interval
"SERVER: Arduino/1.0 UPNP/1.1 %s/%s\r\n" // _modelName, _modelNumber
"USN: %s\r\n" // _uuid
"%s: %s\r\n" // "NT" or "ST", _deviceType
"LOCATION: http://%s:%u/%s\r\n" // WiFi.localIP(), _port, _schemaURL
*/
if(root) {
// Send 1 for root
snprintf_P(buffer, sizeof(buffer)-1,
_ssdp_bye_template,
"upnp:rootdevice", d->getUSN(response_types_t::root), this->bootId, this->configId);
this->_sendNotify(buffer);
}
// Send 1 for uuid
snprintf_P(buffer, sizeof(buffer)-1,
_ssdp_bye_template,
d->getUSN(response_types_t::uuid),
d->getUSN(response_types_t::uuid), this->bootId, this->configId);
this->_sendNotify(buffer);
// Send 1 for deviceType
snprintf_P(buffer, sizeof(buffer)-1,
_ssdp_bye_template,
d->deviceType,
d->getUSN(response_types_t::deviceType), this->bootId, this->configId);
this->_sendNotify(buffer);
}
void SSDPClass::_addToSendQueue(IPAddress addr, uint16_t port, UPNPDeviceType *d, const char *st, response_types_t responseType, uint8_t sec) {
/*
typedef struct ssdp_response_t {
IPAddress address;
uint16_t port;
UPNPDeviceType *dev;
bool sendUUID;
unsigned long sendTime;
char st[SSDP_DEVICE_TYPE_SIZE];
};
*/
for(uint8_t i = 0; i < SSDP_QUEUE_SIZE; i++) {
if(this->sendQueue[i].waiting) {
// Check to see if this is a reply to the same place.
ssdp_response_t *q = &this->sendQueue[i];
if(q->address == addr && q->port == port && q->responseType == responseType) {
#ifdef DEBUG_SSDP
DEBUG_SSDP.printf("There is already a response to this query in slot %u\n", i);
#endif
return;
}
}
}
// Find an empty queue slot and fill it.
for(uint8_t i = 0; i < SSDP_QUEUE_SIZE; i++) {
if(!this->sendQueue[i].waiting) {
#ifdef DEBUG_SSDP
DEBUG_SSDP.printf("Queueing SSDP response in slot %u\n", i);
DEBUG_SSDP.println("*********************************");
#endif
ssdp_response_t *q = &this->sendQueue[i];
q->dev = d;
q->sendTime = millis() + (random(0, sec - 1) * 1000L);
q->address = addr;
q->port = port;
q->responseType = responseType;
strlcpy(q->st, st, sizeof(ssdp_response_t::st)-1);
q->waiting = true;
return;
}
}
// If we made it here then there were was not space available on the queue.
#ifdef DEBUG_SSDP
DEBUG_SSDP.println("The SSDP response queue was full. Dropping request");
#endif
}
void SSDPClass::_sendQueuedResponses() {
for(uint8_t i = 0; i < SSDP_QUEUE_SIZE; i++) {
if(this->sendQueue[i].waiting) {
ssdp_response_t *q = &this->sendQueue[i];
if(q->sendTime < millis()) {
// Send the response and delete the pointer.
#ifdef DEBUG_SSDP
DEBUG_SSDP.print("Sending SSDP queued response ");
DEBUG_SSDP.println(i);
#endif
this->_sendResponse(q->address, q->port, q->dev, q->st, q->responseType);
q->waiting = false;
return;
}
}
}
}
void SSDPClass::_printPacket(ssdp_packet_t *pkt) {
Serial.printf("Rec: %lu\n", pkt->recvd);
switch(pkt->method) {
case NONE:
Serial.println("Method: NONE");
break;
case SEARCH:
Serial.println("Method: SEARCH");
break;
case NOTIFY:
Serial.println("Method: NOTIFY");
break;
default:
Serial.println("Method: UNKOWN");
break;
}
Serial.printf("ST: %s\n", pkt->st);
Serial.printf("MAN: %s\n", pkt->man);
Serial.printf("AGENT: %s\n", pkt->agent);
Serial.printf("HOST: %s\n", pkt->host);
Serial.printf("MX: %d\n", pkt->mx);
Serial.printf("type: %d\n", pkt->type);
Serial.printf("valid: %d\n", pkt->valid);
}
void SSDPClass::_processRequest(AsyncUDPPacket &p) {
// This pending BS should probably be for unicast request only but we will play along for now.
if(!this->_server.connected()) return;
if(p.length() == 0) {
//this->_sendQueuedResponses();
return;
}
ssdp_packet_t pkt;
this->_parsePacket(&pkt, p);
if(pkt.valid && pkt.method == SEARCH) {
// Check to see if we have anything to respond to from this packet.
if(strcmp("ssdp:all", pkt.st) == 0) {
#ifdef DEBUG_SSDP
DEBUG_SSDP.println("--------------- ALL ---------------------");
this->_printPacket(&pkt);
#endif
for(uint8_t i = 0; i < this->m_cdeviceTypes; i++) {
UPNPDeviceType *dev = &this->deviceTypes[i];
if(strlen(this->deviceTypes[i].deviceType) > 0) {
this->_addToSendQueue(p.remoteIP(), p.remotePort(), dev, pkt.st, response_types_t::root, pkt.mx);
this->_addToSendQueue(p.remoteIP(), p.remotePort(), dev, pkt.st, response_types_t::uuid, pkt.mx);
this->_addToSendQueue(p.remoteIP(), p.remotePort(), dev, pkt.st, response_types_t::deviceType, pkt.mx);
}
}
}
else if(strcmp("upnp:rootdevice", pkt.st) == 0) {
UPNPDeviceType *dev = &this->deviceTypes[0];
#ifdef DEBUG_SSDP
DEBUG_SSDP.println("--------------- ROOT ---------------------");
this->_printPacket(&pkt);
#endif
if(pkt.type == MULTICAST)
this->_addToSendQueue(IPAddress(SSDP_MULTICAST_ADDR), SSDP_PORT, dev, pkt.st, response_types_t::root, pkt.mx);
else
this->_sendResponse(p.remoteIP(), p.remotePort(), dev, pkt.st, response_types_t::root);
}
else {
UPNPDeviceType *dev = nullptr;
bool useUUID = false;
if(this->_startsWith("uuid:", pkt.st)) {
dev = this->findDeviceByUUID(pkt.st);
useUUID = true;
}
else if(this->_startsWith("urn:", pkt.st)) { dev = this->findDeviceByType(pkt.st); }
if(dev) {
#ifdef DEBUG_SSDP
this->_printPacket(&pkt);
DEBUG_SSDP.println("-------------- ACCEPT --------------------");
#endif
if(pkt.type == MULTICAST)
this->_addToSendQueue(IPAddress(SSDP_MULTICAST_ADDR), SSDP_PORT, dev, pkt.st, useUUID ? response_types_t::uuid : response_types_t::root, pkt.mx);
else {
this->_sendResponse(p.remoteIP(), p.remotePort(), dev, pkt.st, useUUID ? response_types_t::uuid : response_types_t::root);
}
}
}
}
}
void SSDPClass::loop() {
this->_sendQueuedResponses();
// We need to send the notify if required.
this->_sendNotify();
}
void SSDPClass::schema(Print &client) {
IPAddress ip = this->localIP();
uint8_t devCount = 0;
for(uint8_t i = 0; i < this->m_cdeviceTypes; i++) {
if(this->deviceTypes[i].deviceType && strlen(this->deviceTypes[i].deviceType) > 0) devCount++;
}
char schema_template[strlen_P(_ssdp_schema_template)+1];
char device_template[strlen_P(_ssdp_device_schema_template)+1];
strcpy_P(schema_template, _ssdp_schema_template);
strcpy_P(device_template, _ssdp_device_schema_template);
char buff[sizeof(device_template) + sizeof(UPNPDeviceType)];
buff[0] = '\0';
client.printf(schema_template,
ip.toString().c_str(), _port);
UPNPDeviceType *r = &this->deviceTypes[0];
sprintf(buff, device_template,
r->deviceType,
r->friendlyName,
r->presentationURL,
r->serialNumber,
r->modelName,
r->modelNumber,
r->modelURL,
r->manufacturer,
r->manufacturerURL,
settings.fwVersion.name,
r->uuid );
char *devList = strstr(buff, "</device>") - strlen("</deviceList>");
devList[0] = '\0';
client.print(buff);
for(uint8_t i = 1; i < this->m_cdeviceTypes; i++) {
UPNPDeviceType *dev = &this->deviceTypes[i];
if(strlen(dev->deviceType) > 0) {
//Serial.print(devList);
client.printf(device_template,
dev->deviceType,
dev->friendlyName,
dev->presentationURL,
dev->serialNumber,
dev->modelName,
dev->modelNumber,
dev->modelURL,
dev->manufacturer,
dev->manufacturerURL,
settings.fwVersion.name,
dev->uuid);
}
}
client.print(F("</deviceList></device></root>\r\n"));
#ifdef DEBUG_SSDP
DEBUG_SSDP.println("Sending upnp.xml");
#endif
}
/*
void SSDPClass::setDeviceType(uint8_t ndx, UPNPDeviceType *dt) {
this->setDeviceType(ndx, dt->deviceType);
this->setName(ndx, dt->friendlyName);
this->setURL(ndx, dt->presentationURL);
this->setSerialNumber(ndx, dt->serialNumber);
this->setUUID(ndx, dt->uuid);
this->setModelName(ndx, dt->modelName);
this->setModelNumber(ndx, dt->modelNumber);
this->setManufacturer(ndx, dt->manufacturer);
this->setManufacturerURL(ndx, dt->manufacturerURL);
}
*/
void SSDPClass::setTTL(const uint8_t ttl) { _ttl = ttl; }
void SSDPClass::setHTTPPort(uint16_t port) { _port = port; }
void SSDPClass::setInterval(uint32_t interval) { _interval = interval; }
void SSDPClass::setDeviceType(uint8_t ndx, const char *type) { this->deviceTypes[ndx].setDeviceType(type); }
void SSDPClass::setSchemaURL(uint8_t ndx, const char *url) { this->deviceTypes[ndx].setSchemaURL(url); }
void SSDPClass::setUUID(uint8_t ndx, const char *uuid) { this->deviceTypes[ndx].setUUID(uuid); }
void SSDPClass::setUUID(uint8_t ndx, const char *prefix, uint32_t id) {
char svcuuid[50];
sprintf(svcuuid, "%s%02X%02X%02X",
prefix,
(uint16_t)((id >> 16) & 0xff),
(uint16_t)((id >> 8) & 0xff),
(uint16_t)id & 0xff);
this->setUUID(ndx, svcuuid);
}
void SSDPClass::setName(uint8_t ndx, const char *name) { this->deviceTypes[ndx].setName(name); }
void SSDPClass::setURL(uint8_t ndx, const char *url) { this->deviceTypes[ndx].setURL(url); }
void SSDPClass::setSerialNumber(uint8_t ndx, const char *serialNumber) { this->deviceTypes[ndx].setSerialNumber(serialNumber); }
//void SSDPClass::setSerialNumber(uint8_t ndx, const uint32_t serialNumber) { this->deviceTypes[ndx].setSerialNumber(serialNumber); }
void SSDPClass::setModelName(uint8_t ndx, const char *name) { this->deviceTypes[ndx].setModelName(name); }
void SSDPClass::setModelNumber(uint8_t ndx, const char *num) { this->deviceTypes[ndx].setModelNumber(num); }
void SSDPClass::setModelURL(uint8_t ndx, const char *url) { this->deviceTypes[ndx].setModelURL(url); }
void SSDPClass::setManufacturer(uint8_t ndx, const char *name) { this->deviceTypes[ndx].setManufacturer(name); }
void SSDPClass::setManufacturerURL(uint8_t ndx, const char *url) { this->deviceTypes[ndx].setManufacturerURL(url); }
void SSDPClass::setChipId(uint8_t ndx, uint32_t chipId) { this->deviceTypes[ndx].setChipId(chipId); }
#if !defined(NO_GLOBAL_INSTANCES) && !defined(NO_GLOBAL_SSDP)
SSDPClass SSDP;
#endif

183
src/SSDP.h Normal file
View file

@ -0,0 +1,183 @@
#ifndef SSDP_h
#define SSDP_h
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncUDP.h>
#define SSDP_UUID_SIZE 42
#define SSDP_SCHEMA_URL_SIZE 64
#define SSDP_DEVICE_TYPE_SIZE 64
#define SSDP_SERIAL_NUMBER_SIZE 37
#define SSDP_PRESENTATION_URL_SIZE 64
#define SSDP_MODEL_NAME_SIZE 64
#define SSDP_MODEL_URL_SIZE 64
#define SSDP_MODEL_VERSION_SIZE 32
#define SSDP_MANUFACTURER_SIZE 64
#define SSDP_USN_SIZE 128
#define SSDP_MANUFACTURER_URL_SIZE 64
#define SSDP_FRIENDLY_NAME_SIZE 64
#define SSDP_INTERVAL_SECONDS 1800
#define SSDP_MULTICAST_TTL 2
#define SSDP_HTTP_PORT 80
#define SSDP_CHILD_DEVICES 0
#define SSDP_QUEUE_SIZE 5
//#define DEBUG_SSDP Serial
//#define DEBUG_SSDP_PACKET Serial
typedef enum {
NONE,
SEARCH,
NOTIFY
} ssdp_method_t;
typedef enum {
root,
uuid,
deviceType
} response_types_t;
#define SSDP_FIELD_LEN 128
#define SSDP_LOCATION_LEN 256
typedef enum {
UNKNOWN,
MULTICAST,
UNICAST
} ssdp_req_types_t;
struct ssdp_packet_t {
bool pending;
unsigned long recvd;
ssdp_method_t method;
char req [SSDP_FIELD_LEN];
char st [SSDP_FIELD_LEN];
char man [SSDP_FIELD_LEN];
char agent [SSDP_FIELD_LEN];
char host [SSDP_LOCATION_LEN];
uint8_t mx;
ssdp_req_types_t type;
bool valid;
};
class UPNPDeviceType {
char m_usn[SSDP_USN_SIZE];
public:
UPNPDeviceType();
UPNPDeviceType(const char *deviceType);
UPNPDeviceType(const String& deviceType) { UPNPDeviceType(deviceType.c_str()); }
UPNPDeviceType(const String& deviceType, const String& uuid);
UPNPDeviceType(const char *deviceType, const char *uuid, const char *friendlyName);
UPNPDeviceType(const char *deviceType, const char *uuid);
UPNPDeviceType(const String& deviceType, const String& uuid, const String& friendlyName) { UPNPDeviceType(deviceType.c_str(), uuid.c_str(), friendlyName.c_str()); }
unsigned long lastNotified;
char schemaURL[SSDP_SCHEMA_URL_SIZE];
char uuid[SSDP_UUID_SIZE];
char deviceType[SSDP_DEVICE_TYPE_SIZE];
char friendlyName[SSDP_FRIENDLY_NAME_SIZE];
char serialNumber[SSDP_SERIAL_NUMBER_SIZE];
char presentationURL[SSDP_PRESENTATION_URL_SIZE];
char manufacturer[SSDP_MANUFACTURER_SIZE];
char manufacturerURL[SSDP_MANUFACTURER_URL_SIZE];
char modelName[SSDP_MODEL_NAME_SIZE];
char modelURL[SSDP_MODEL_URL_SIZE];
char modelNumber[SSDP_MODEL_VERSION_SIZE];
bool isActive;
void setSchemaURL(const char *url);
void setDeviceType(const char *dtype);
void setUUID(const char *id);
void setName(const char *name);
void setURL(const char *url);
void setSerialNumber(const char *sn);
void setSerialNumber(const uint32_t sn);
void setModelName(const char *name);
void setModelNumber(const char *num);
void setModelURL(const char *url);
void setManufacturer(const char *name);
void setManufacturerURL(const char *url);
//char *getUSN();
char *getUSN(const char *st);
char *getUSN(response_types_t responseType);
void setChipId(uint32_t chipId);
};
struct ssdp_response_t {
bool waiting;
IPAddress address;
uint16_t port;
UPNPDeviceType *dev;
bool sendUUID;
unsigned long sendTime;
char st[SSDP_DEVICE_TYPE_SIZE];
response_types_t responseType;
};
class SSDPClass {
uint8_t m_cdeviceTypes = SSDP_CHILD_DEVICES + 1;
protected:
ssdp_response_t sendQueue[SSDP_QUEUE_SIZE];
//ssdp_response_t sendQueue[SSDP_QUEUE_SIZE];
//void _send(ssdp_method_t method, UPNPDeviceType *dev, bool useUUID);
//void _sendAll(ssdp_method_t method, bool useUUID);
void _startTimer();
void _stopTimer();
void _sendNotify();
void _sendNotify(UPNPDeviceType *d);
void _sendNotify(UPNPDeviceType *d, bool root);
void _sendNotify(const char *);
void _sendByeBye();
void _sendByeBye(UPNPDeviceType *d);
void _sendByeBye(UPNPDeviceType *d, bool root);
void _sendByeBye(const char *);
void _sendQueuedResponses();
void _sendResponse(IPAddress addr, uint16_t port, UPNPDeviceType *d, const char *st, response_types_t responseType);
void _sendResponse(IPAddress addr, uint16_t port, const char *msg);
AsyncUDP _server;
hw_timer_t* _timer = nullptr;
uint16_t _port = SSDP_HTTP_PORT;
uint8_t _ttl = SSDP_MULTICAST_TTL;
uint32_t _interval = SSDP_INTERVAL_SECONDS;
void _processRequest(AsyncUDPPacket &p);
void _parsePacket(ssdp_packet_t *pkt, AsyncUDPPacket &p);
void _printPacket(ssdp_packet_t *pkt);
bool _startsWith(const char* pre, const char* str);
void _addToSendQueue(IPAddress addr, uint16_t port, UPNPDeviceType *d, const char *st, response_types_t responseType, uint8_t sec);
public:
SSDPClass();
~SSDPClass();
UPNPDeviceType deviceTypes[SSDP_CHILD_DEVICES + 1];
bool begin();
void loop();
void end();
bool isStarted = false;
unsigned long bootId = 0;
int configId = 0;
IPAddress localIP();
UPNPDeviceType* getDeviceTypes(uint8_t ndx);
UPNPDeviceType* getDeviceType(uint8_t ndx);
UPNPDeviceType* findDeviceByType(char *devType);
UPNPDeviceType* findDeviceByUUID(char *uuid);
void setHTTPPort(uint16_t port);
void setTTL(uint8_t ttl);
void setInterval(uint32_t interval);
void schema(WiFiClient client) { schema((Print&)std::ref(client)); }
void schema(Print &print);
void setDeviceType(uint8_t ndx, UPNPDeviceType *dt);
void setDeviceType(uint8_t ndx, const String& type) { setDeviceType(ndx, type.c_str()); }
void setDeviceType(uint8_t ndx, const char *type);
void setUUID(uint8_t ndx, const char *uuid);
void setUUID(uint8_t ndx, const char *prefix, uint32_t id);
void setName(uint8_t ndx, const char *name);
void setURL(uint8_t ndx, const char *url);
void setSchemaURL(uint8_t ndx, const char *url);
void setSerialNumber(uint8_t ndx, const char *serialNumber);
void setModelName(uint8_t ndx, const char *name);
void setModelNumber(uint8_t ndx, const char *num);
void setModelURL(uint8_t ndx, const char *url);
void setManufacturer(uint8_t ndx, const char *name);
void setManufacturerURL(uint8_t ndx, const char *url);
void setActive(uint8_t ndx, bool bActive);
void setChipId(uint8_t ndx, uint32_t chipId);
};
#if !defined(NO_GLOBAL_INSTANCES) && !defined(NO_GLOBAL_SSSDP)
extern SSDPClass SSDP;
#endif
#endif

203
src/Sockets.cpp Normal file
View file

@ -0,0 +1,203 @@
#include <Arduino.h>
#include <ArduinoJson.h>
#include <WebSocketsServer.h>
#include <esp_task_wdt.h>
#include "Sockets.h"
#include "ConfigSettings.h"
#include "Somfy.h"
#include "Network.h"
#include "GitOTA.h"
extern ConfigSettings settings;
extern Network net;
extern SomfyShadeController somfy;
extern SocketEmitter sockEmit;
extern GitUpdater git;
WebSocketsServer sockServer = WebSocketsServer(8080);
#define MAX_SOCK_RESPONSE 2048
static char g_response[MAX_SOCK_RESPONSE];
bool room_t::isJoined(uint8_t num) {
for(uint8_t i = 0; i < sizeof(this->clients); i++) {
if(this->clients[i] == num) return true;
}
return false;
}
bool room_t::join(uint8_t num) {
if(this->isJoined(num)) return true;
for(uint8_t i = 0; i < sizeof(this->clients); i++) {
if(this->clients[i] == 255) {
this->clients[i] = num;
return true;
}
}
return false;
}
bool room_t::leave(uint8_t num) {
if(!this->isJoined(num)) return false;
for(uint8_t i = 0; i < sizeof(this->clients); i++) {
if(this->clients[i] == num) this->clients[i] = 255;
}
return true;
}
void room_t::clear() {
memset(this->clients, 255, sizeof(this->clients));
}
uint8_t room_t::activeClients() {
uint8_t n = 0;
for(uint8_t i = 0; i < sizeof(this->clients); i++) {
if(this->clients[i] != 255) n++;
}
return n;
}
/*********************************************************************
* ClientSocketEvent class members
********************************************************************/
/*
void ClientSocketEvent::prepareMessage(const char *evt, const char *payload) {
if(strlen(payload) + 5 >= sizeof(this->msg)) Serial.printf("Socket buffer overflow %d > 2048\n", strlen(payload) + 5 + strlen(evt));
snprintf(this->msg, sizeof(this->msg), "42[%s,%s]", evt, payload);
}
void ClientSocketEvent::prepareMessage(const char *evt, JsonDocument &doc) {
memset(this->msg, 0x00, sizeof(this->msg));
snprintf(this->msg, sizeof(this->msg), "42[%s,", evt);
serializeJson(doc, &this->msg[strlen(this->msg)], sizeof(this->msg) - strlen(this->msg) - 2);
strcat(this->msg, "]");
}
*/
/*********************************************************************
* SocketEmitter class members
********************************************************************/
void SocketEmitter::startup() {
}
void SocketEmitter::begin() {
sockServer.begin();
sockServer.enableHeartbeat(20000, 10000, 3);
sockServer.onEvent(this->wsEvent);
Serial.println("Socket Server Started...");
//settings.printAvailHeap();
}
void SocketEmitter::loop() {
this->initClients();
sockServer.loop();
}
JsonSockEvent *SocketEmitter::beginEmit(const char *evt) {
this->json.beginEvent(&sockServer, evt, g_response, sizeof(g_response));
return &this->json;
}
void SocketEmitter::endEmit(uint8_t num) { this->json.endEvent(num); sockServer.loop(); }
void SocketEmitter::endEmitRoom(uint8_t room) {
if(room < SOCK_MAX_ROOMS) {
room_t *r = &this->rooms[room];
for(uint8_t i = 0; i < sizeof(r->clients); i++) {
if(r->clients[i] != 255) this->json.endEvent(r->clients[i]);
}
}
}
uint8_t SocketEmitter::activeClients(uint8_t room) {
if(room < SOCK_MAX_ROOMS) return this->rooms[room].activeClients();
return 0;
}
void SocketEmitter::initClients() {
for(uint8_t i = 0; i < sizeof(this->newClients); i++) {
uint8_t num = this->newClients[i];
if(num != 255) {
if(sockServer.clientIsConnected(num)) {
Serial.printf("Initializing Socket Client %u\n", num);
esp_task_wdt_reset();
settings.emitSockets(num);
somfy.emitState(num);
git.emitUpdateCheck(num);
net.emitSockets(num);
esp_task_wdt_reset();
}
this->newClients[i] = 255;
}
}
}
void SocketEmitter::delayInit(uint8_t num) {
for(uint8_t i=0; i < sizeof(this->newClients); i++) {
if(this->newClients[i] == num) break;
else if(this->newClients[i] == 255) {
this->newClients[i] = num;
break;
}
}
}
void SocketEmitter::end() {
sockServer.close();
for(uint8_t i = 0; i < SOCK_MAX_ROOMS; i++)
this->rooms[i].clear();
}
void SocketEmitter::disconnect() { sockServer.disconnect(); }
void SocketEmitter::wsEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) {
switch(type) {
case WStype_ERROR:
if(length > 0)
Serial.printf("Socket Error: %s\n", payload);
else
Serial.println("Socket Error: \n");
break;
case WStype_DISCONNECTED:
if(length > 0)
Serial.printf("Socket [%u] Disconnected!\n [%s]", num, payload);
else
Serial.printf("Socket [%u] Disconnected!\n", num);
for(uint8_t i = 0; i < SOCK_MAX_ROOMS; i++) {
sockEmit.rooms[i].leave(num);
}
break;
case WStype_CONNECTED:
{
IPAddress ip = sockServer.remoteIP(num);
Serial.printf("Socket [%u] Connected from %d.%d.%d.%d url: %s\n", num, ip[0], ip[1], ip[2], ip[3], payload);
// Send all the current shade settings to the client.
sockServer.sendTXT(num, "Connected");
//sockServer.loop();
sockEmit.delayInit(num);
}
break;
case WStype_TEXT:
if(strncmp((char *)payload, "join:", 5) == 0) {
// In this instance the client wants to join a room. Let's do some
// work to get the ordinal of the room that the client wants to join.
uint8_t roomNum = atoi((char *)&payload[5]);
Serial.printf("Client %u joining room %u\n", num, roomNum);
if(roomNum < SOCK_MAX_ROOMS) sockEmit.rooms[roomNum].join(num);
}
else if(strncmp((char *)payload, "leave:", 6) == 0) {
uint8_t roomNum = atoi((char *)&payload[6]);
Serial.printf("Client %u leaving room %u\n", num, roomNum);
if(roomNum < SOCK_MAX_ROOMS) sockEmit.rooms[roomNum].leave(num);
}
else {
Serial.printf("Socket [%u] text: %s\n", num, payload);
}
// send message to client
// webSocket.sendTXT(num, "message here");
// send data to all connected clients
// sockServer.broadcastTXT("message here");
break;
case WStype_BIN:
Serial.printf("[%u] get binary length: %u\n", num, length);
//hexdump(payload, length);
// send message to client
// sockServer.sendBIN(num, payload, length);
break;
case WStype_PONG:
//Serial.printf("Pong from %u\n", num);
break;
case WStype_PING:
//Serial.printf("Ping from %u\n", num);
break;
default:
break;
}
}

38
src/Sockets.h Normal file
View file

@ -0,0 +1,38 @@
#include <WebSocketsServer.h>
#include "WResp.h"
#ifndef sockets_h
#define sockets_h
#define SOCK_MAX_ROOMS 1
#define ROOM_EMIT_FRAME 0
struct room_t {
uint8_t clients[5] = {255, 255, 255, 255, 255};
uint8_t activeClients();
bool isJoined(uint8_t num);
bool join(uint8_t num);
bool leave(uint8_t num);
void clear();
};
class SocketEmitter {
protected:
uint8_t newclients = 0;
uint8_t newClients[5] = {255,255,255,255,255};
void delayInit(uint8_t num);
public:
JsonSockEvent json;
//ClientSocketEvent evt;
room_t rooms[SOCK_MAX_ROOMS];
uint8_t activeClients(uint8_t room);
void initClients();
void startup();
void begin();
void loop();
void end();
void disconnect();
JsonSockEvent * beginEmit(const char *evt);
void endEmit(uint8_t num = 255);
void endEmitRoom(uint8_t num);
static void wsEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length);
};
#endif

5132
src/Somfy.cpp Normal file

File diff suppressed because it is too large Load diff

587
src/Somfy.h Normal file
View file

@ -0,0 +1,587 @@
#ifndef SOMFY_H
#define SOMFY_H
#include "ConfigSettings.h"
#include "WResp.h"
#define SOMFY_MAX_SHADES 32
#define SOMFY_MAX_GROUPS 16
#define SOMFY_MAX_LINKED_REMOTES 7
#define SOMFY_MAX_GROUPED_SHADES 32
#define SOMFY_MAX_ROOMS 16
#define SOMFY_MAX_REPEATERS 7
#define SECS_TO_MILLIS(x) ((x) * 1000)
#define MINS_TO_MILLIS(x) SECS_TO_MILLIS((x) * 60)
#define SOMFY_SUN_TIMEOUT MINS_TO_MILLIS(2)
#define SOMFY_NO_SUN_TIMEOUT MINS_TO_MILLIS(20)
#define SOMFY_WIND_TIMEOUT SECS_TO_MILLIS(2)
#define SOMFY_NO_WIND_TIMEOUT MINS_TO_MILLIS(12)
#define SOMFY_NO_WIND_REMOTE_TIMEOUT SECS_TO_MILLIS(30)
enum class radio_proto : byte { // Ordinal byte 0-255
RTS = 0x00,
RTW = 0x01,
RTV = 0x02,
GP_Relay = 0x08,
GP_Remote = 0x09
};
enum class somfy_commands : byte {
Unknown0 = 0x0,
My = 0x1,
Up = 0x2,
MyUp = 0x3,
Down = 0x4,
MyDown = 0x5,
UpDown = 0x6,
MyUpDown = 0x7,
Prog = 0x8,
SunFlag = 0x9,
Flag = 0xA,
StepDown = 0xB,
Toggle = 0xC,
UnknownD = 0xD,
Sensor = 0xE,
RTWProto = 0xF, // RTW Protocol
// Command extensions for 80 bit frames
StepUp = 0x8B,
Favorite = 0xC1,
Stop = 0xF1
};
enum class group_types : byte {
channel = 0x00
};
enum class shade_types : byte {
roller = 0x00,
blind = 0x01,
ldrapery = 0x02,
awning = 0x03,
shutter = 0x04,
garage1 = 0x05,
garage3 = 0x06,
rdrapery = 0x07,
cdrapery = 0x08,
drycontact = 0x09,
drycontact2 = 0x0A,
lgate = 0x0B,
cgate = 0x0C,
rgate = 0x0D,
lgate1 = 0x0E,
cgate1 = 0x0F,
rgate1 = 0x10
};
enum class tilt_types : byte {
none = 0x00,
tiltmotor = 0x01,
integrated = 0x02,
tiltonly = 0x03,
euromode = 0x04
};
String translateSomfyCommand(const somfy_commands cmd);
somfy_commands translateSomfyCommand(const String& string);
#define MAX_TIMINGS 300
#define MAX_RX_BUFFER 3
#define MAX_TX_BUFFER 5
typedef enum {
waiting_synchro = 0,
receiving_data = 1,
complete = 2
} t_status;
struct somfy_rx_t {
void clear() {
this->status = t_status::waiting_synchro;
this->bit_length = 56;
this->cpt_synchro_hw = 0;
this->cpt_bits = 0;
this->previous_bit = 0;
this->waiting_half_symbol = false;
memset(this->payload, 0, sizeof(this->payload));
memset(this->pulses, 0, sizeof(this->pulses));
this->pulseCount = 0;
}
t_status status;
uint8_t bit_length = 56;
uint8_t cpt_synchro_hw = 0;
uint8_t cpt_bits = 0;
uint8_t previous_bit = 0;
bool waiting_half_symbol;
uint8_t payload[10];
unsigned int pulses[MAX_TIMINGS];
uint16_t pulseCount = 0;
};
// A simple FIFO queue to hold rx buffers. We are using
// a byte index to make it so we don't have to reorganize
// the storage each time we push or pop.
struct somfy_rx_queue_t {
void init();
uint8_t length = 0;
uint8_t index[MAX_RX_BUFFER];
somfy_rx_t items[MAX_RX_BUFFER];
void push(somfy_rx_t *rx);
bool pop(somfy_rx_t *rx);
};
struct somfy_tx_t {
void clear() {
this->hwsync = 0;
this->bit_length = 0;
memset(this->payload, 0x00, sizeof(this->payload));
}
uint8_t hwsync = 0;
uint8_t bit_length = 0;
uint8_t payload[10] = {};
};
struct somfy_tx_queue_t {
somfy_tx_queue_t() { this->clear(); }
void clear() {
for (uint8_t i = 0; i < MAX_TX_BUFFER; i++) {
this->index[i] = 255;
this->items[i].clear();
}
this->length = 0;
}
unsigned long delay_time = 0;
uint8_t length = 0;
uint8_t index[MAX_TX_BUFFER] = {255};
somfy_tx_t items[MAX_TX_BUFFER];
bool pop(somfy_tx_t *tx);
void push(somfy_rx_t *rx); // Used for repeats
void push(uint8_t hwsync, byte *payload, uint8_t bit_length);
};
enum class somfy_flags_t : byte {
SunFlag = 0x01,
SunSensor = 0x02,
DemoMode = 0x04,
Light = 0x08,
Windy = 0x10,
Sunny = 0x20,
Lighted = 0x40,
SimMy = 0x80
};
enum class gpio_flags_t : byte {
LowLevelTrigger = 0x01
};
struct somfy_relay_t {
uint32_t remoteAddress = 0;
uint8_t sync = 0;
byte frame[10] = {0};
};
struct somfy_frame_t {
bool valid = false;
bool processed = false;
bool synonym = false;
radio_proto proto = radio_proto::RTS;
int rssi = 0;
byte lqi = 0x0;
somfy_commands cmd;
uint32_t remoteAddress = 0;
uint16_t rollingCode = 0;
uint8_t encKey = 0xA7;
uint8_t checksum = 0;
uint8_t hwsync = 0;
uint8_t repeats = 0;
uint32_t await = 0;
uint8_t bitLength = 56;
uint16_t pulseCount = 0;
uint8_t stepSize = 0;
void print();
void encode80BitFrame(byte *frame, uint8_t repeat);
byte calc80Checksum(byte b0, byte b1, byte b2);
byte encode80Byte7(byte start, uint8_t repeat);
void encodeFrame(byte *frame);
void decodeFrame(byte* frame);
void decodeFrame(somfy_rx_t *rx);
bool isRepeat(somfy_frame_t &f);
bool isSynonym(somfy_frame_t &f);
void copy(somfy_frame_t &f);
};
class SomfyRoom {
public:
uint8_t roomId = 0;
char name[21] = "";
int8_t sortOrder = 0;
void clear();
bool save();
bool fromJSON(JsonObject &obj);
void toJSON(JsonResponse &json);
void emitState(const char *evt = "roomState");
void emitState(uint8_t num, const char *evt = "roomState");
void publish();
void unpublish();
};
class SomfyRemote {
// These sizes for the data have been
// confirmed. The address is actually 24bits
// and the rolling code is 16 bits.
protected:
char m_remotePrefId[11] = "";
uint32_t m_remoteAddress = 0;
public:
radio_proto proto = radio_proto::RTS;
uint8_t gpioFlags = 0;
int8_t gpioDir = 0;
uint8_t gpioUp = 0;
uint8_t gpioDown = 0;
uint8_t gpioMy = 0;
uint32_t gpioRelease = 0;
somfy_frame_t lastFrame;
bool flipCommands = false;
uint16_t lastRollingCode = 0;
uint8_t flags = 0;
uint8_t bitLength = 0;
uint8_t repeats = 1;
virtual bool isLastCommand(somfy_commands cmd);
char *getRemotePrefId() {return m_remotePrefId;}
virtual void toJSON(JsonResponse &json);
virtual void setRemoteAddress(uint32_t address);
virtual uint32_t getRemoteAddress();
virtual uint16_t getNextRollingCode();
virtual uint16_t setRollingCode(uint16_t code);
bool hasSunSensor();
bool hasLight();
bool simMy();
void setSunSensor(bool bHasSensor);
void setLight(bool bHasLight);
void setSimMy(bool bSimMy);
virtual void sendCommand(somfy_commands cmd);
virtual void sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize = 0);
void sendSensorCommand(int8_t isWindy, int8_t isSunny, uint8_t repeat);
void repeatFrame(uint8_t repeat);
virtual uint16_t p_lastRollingCode(uint16_t code);
somfy_commands transformCommand(somfy_commands cmd);
virtual void triggerGPIOs(somfy_frame_t &frame);
};
class SomfyLinkedRemote : public SomfyRemote {
public:
SomfyLinkedRemote();
};
class SomfyShade : public SomfyRemote {
protected:
uint8_t shadeId = 255;
uint64_t moveStart = 0;
uint64_t tiltStart = 0;
uint64_t noSunStart = 0;
uint64_t sunStart = 0;
uint64_t windStart = 0;
uint64_t windLast = 0;
uint64_t noWindStart = 0;
bool noSunDone = true;
bool sunDone = true;
bool windDone = true;
bool noWindDone = true;
float startPos = 0.0f;
float startTiltPos = 0.0f;
bool settingMyPos = false;
bool settingPos = false;
bool settingTiltPos = false;
uint32_t awaitMy = 0;
public:
uint8_t roomId = 0;
int8_t sortOrder = 0;
bool flipPosition = false;
shade_types shadeType = shade_types::roller;
tilt_types tiltType = tilt_types::none;
#ifdef USE_NVS
void load();
#endif
float currentPos = 0.0f;
float currentTiltPos = 0.0f;
int8_t lastMovement = 0;
int8_t direction = 0; // 0 = stopped, 1=down, -1=up.
int8_t tiltDirection = 0; // 0=stopped, 1=clockwise, -1=counter clockwise
float target = 0.0f;
float tiltTarget = 0.0f;
float myPos = -1.0f;
float myTiltPos = -1.0f;
SomfyLinkedRemote linkedRemotes[SOMFY_MAX_LINKED_REMOTES];
bool paired = false;
int8_t validateJSON(JsonObject &obj);
void toJSONRef(JsonResponse &json);
int8_t fromJSON(JsonObject &obj);
void toJSON(JsonResponse &json) override;
char name[21] = "";
void setShadeId(uint8_t id) { shadeId = id; }
uint8_t getShadeId() { return shadeId; }
uint32_t upTime = 10000;
uint32_t downTime = 10000;
uint32_t tiltTime = 7000;
uint16_t stepSize = 100;
bool save();
bool isIdle();
bool isInGroup();
void checkMovement();
void processFrame(somfy_frame_t &frame, bool internal = false);
void processInternalCommand(somfy_commands cmd, uint8_t repeat = 1);
void setTiltMovement(int8_t dir);
void setMovement(int8_t dir);
void setTarget(float target);
bool isAtTarget();
bool isToggle();
void moveToTarget(float pos, float tilt = -1.0f);
void moveToTiltTarget(float target);
void sendTiltCommand(somfy_commands cmd);
void sendCommand(somfy_commands cmd);
void sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize = 0);
bool linkRemote(uint32_t remoteAddress, uint16_t rollingCode = 0);
bool unlinkRemote(uint32_t remoteAddress);
void emitState(const char *evt = "shadeState");
void emitState(uint8_t num, const char *evt = "shadeState");
void emitCommand(somfy_commands cmd, const char *source, uint32_t sourceAddress, const char *evt = "shadeCommand");
void emitCommand(uint8_t num, somfy_commands cmd, const char *source, uint32_t sourceAddress, const char *evt = "shadeCommand");
void setMyPosition(int8_t pos, int8_t tilt = -1);
void moveToMyPosition();
void processWaitingFrame();
void publish();
void unpublish();
static void unpublish(uint8_t id);
static void unpublish(uint8_t id, const char *topic);
void publishState();
void commit();
void commitShadePosition();
void commitTiltPosition();
void commitMyPosition();
void clear();
int8_t transformPosition(float fpos);
void setGPIOs();
void triggerGPIOs(somfy_frame_t &frame);
bool usesPin(uint8_t pin);
// State Setters
int8_t p_direction(int8_t dir);
int8_t p_tiltDirection(int8_t dir);
float p_target(float target);
float p_tiltTarget(float target);
float p_myPos(float pos);
float p_myTiltPos(float pos);
bool p_flag(somfy_flags_t flag, bool val);
bool p_sunFlag(bool val);
bool p_sunny(bool val);
bool p_windy(bool val);
float p_currentPos(float pos);
float p_currentTiltPos(float pos);
uint16_t p_lastRollingCode(uint16_t code);
bool publish(const char *topic, const char *val, bool retain = false);
bool publish(const char *topic, uint8_t val, bool retain = false);
bool publish(const char *topic, int8_t val, bool retain = false);
bool publish(const char *topic, uint32_t val, bool retain = false);
bool publish(const char *topic, uint16_t val, bool retain = false);
bool publish(const char *topic, bool val, bool retain = false);
void publishDisco();
void unpublishDisco();
};
class SomfyGroup : public SomfyRemote {
protected:
uint8_t groupId = 255;
public:
uint8_t roomId = 0;
int8_t sortOrder = 0;
group_types groupType = group_types::channel;
int8_t direction = 0; // 0 = stopped, 1=down, -1=up.
char name[21] = "";
uint8_t linkedShades[SOMFY_MAX_GROUPED_SHADES];
void setGroupId(uint8_t id) { groupId = id; }
uint8_t getGroupId() { return groupId; }
bool save();
void clear();
bool fromJSON(JsonObject &obj);
//bool toJSON(JsonObject &obj);
void toJSON(JsonResponse &json);
void toJSONRef(JsonResponse &json);
bool linkShade(uint8_t shadeId);
bool unlinkShade(uint8_t shadeId);
bool hasShadeId(uint8_t shadeId);
void compressLinkedShadeIds();
void publish();
void unpublish();
static void unpublish(uint8_t id);
static void unpublish(uint8_t id, const char *topic);
void publishState();
void updateFlags();
void emitState(const char *evt = "groupState");
void emitState(uint8_t num, const char *evt = "groupState");
void sendCommand(somfy_commands cmd);
void sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize = 0);
int8_t p_direction(int8_t dir);
bool publish(const char *topic, uint8_t val, bool retain = false);
bool publish(const char *topic, int8_t val, bool retain = false);
bool publish(const char *topic, uint32_t val, bool retain = false);
bool publish(const char *topic, uint16_t val, bool retain = false);
bool publish(const char *topic, bool val, bool retain = false);
};
struct transceiver_config_t {
bool printBuffer = false;
bool enabled = false;
uint8_t type = 56; // 56 or 80 bit protocol.
radio_proto proto = radio_proto::RTS;
uint8_t SCKPin = 18;
uint8_t TXPin = 13;
uint8_t RXPin = 12;
uint8_t MOSIPin = 23;
uint8_t MISOPin = 19;
uint8_t CSNPin = 5;
bool radioInit = false;
float frequency = 433.42; // Basic frequency
float deviation = 47.60; // Set the Frequency deviation in kHz. Value from 1.58 to 380.85. Default is 47.60 kHz.
float rxBandwidth = 99.97; // Receive bandwidth in kHz. Value from 58.03 to 812.50. Default is 99.97kHz.
int8_t txPower = 10; // Transmission power {-30, -20, -15, -10, -6, 0, 5, 7, 10, 11, 12}. Default is 12.
/*
bool internalCCMode = false; // Use internal transmission mode FIFO buffers.
byte modulationMode = 2; // Modulation mode. 0 = 2-FSK, 1 = GFSK, 2 = ASK/OOK, 3 = 4-FSK, 4 = MSK.
uint8_t channel = 0; // The channel number from 0 to 255
float channelSpacing = 199.95; // Channel spacing in multiplied by the channel number and added to the base frequency in kHz. 25.39 to 405.45. Default 199.95
float dataRate = 99.97; // The data rate in kBaud. 0.02 to 1621.83 Default is 99.97.
uint8_t syncMode = 0; // 0=No preamble/sync,
// 1=16 sync word bits detected,
// 2=16/16 sync words bits detected.
// 3=30/32 sync word bits detected,
// 4=No preamble/sync carrier above threshold
// 5=15/16 + carrier above threshold.
// 6=16/16 + carrier-sense above threshold
// 7=0/32 + carrier-sense above threshold
uint16_t syncWordHigh = 211; // The sync word used to the sync mode.
uint16_t syncWordLow = 145; // The sync word used to the sync mode.
uint8_t addrCheckMode = 0; // 0=No address filtration
// 1=Check address without broadcast.
// 2=Address check with 0 as broadcast.
// 3=Address check with 0 or 255 as broadcast.
uint8_t checkAddr = 0; // Packet filter address depending on addrCheck settings.
bool dataWhitening = false; // Indicates whether data whitening should be applied.
uint8_t pktFormat = 0; // 0=Use FIFO buffers form RX and TX
// 1=Synchronous serial mode. RX on GDO0 and TX on either GDOx pins.
// 2=Random TX mode. Send data using PN9 generator.
// 3=Asynchronous serial mode. RX on GDO0 and TX on either GDOx pins.
uint8_t pktLengthMode = 0; // 0=Fixed packet length
// 1=Variable packet length
// 2=Infinite packet length
// 3=Reserved
uint8_t pktLength = 0; // Packet length
bool useCRC = false; // Indicates whether CRC is to be used.
bool autoFlushCRC = false; // Automatically flush RX FIFO when CRC fails. If more than one packet is in the buffer it too will be flushed.
bool disableDCFilter = false; // Digital blocking filter for demodulator. Only for data rates <= 250k.
bool enableManchester = true; // Enable/disable Manchester encoding.
bool enableFEC = false; // Enable/disable forward error correction.
uint8_t minPreambleBytes = 0; // The minimum number of preamble bytes to be transmitten.
// 0=2bytes
// 1=3bytes
// 2=4bytes
// 3=6bytes
// 4=8bytes
// 5=12bytes
// 6=16bytes
// 7=24bytes
uint8_t pqtThreshold = 0; // Preamble quality estimator threshold. The preable quality estimator increase an internal counter by one each time a bit is received that is different than the prevoius bit and
// decreases the bounter by 8 each time a bit is received that is the same as the lats bit. A threshold of 4 PQT for this counter is used to gate sync word detection.
// When PQT = 0 a sync word is always accepted.
bool appendStatus = false; // Appends the RSSI and LQI values to the TX packed as well as the CRC.
*/
void fromJSON(JsonObject& obj);
//void toJSON(JsonObject& obj);
void toJSON(JsonResponse& json);
void save();
void load();
void apply();
void removeNVSKey(const char *key);
};
class Transceiver {
private:
static void handleReceive();
bool _received = false;
somfy_frame_t frame;
public:
transceiver_config_t config;
bool printBuffer = false;
//bool toJSON(JsonObject& obj);
void toJSON(JsonResponse& json);
bool fromJSON(JsonObject& obj);
bool save();
bool begin();
void loop();
bool end();
bool receive(somfy_rx_t *rx);
void clearReceived();
void enableReceive();
void disableReceive();
somfy_frame_t& lastFrame();
void sendFrame(byte *frame, uint8_t sync, uint8_t bitLength = 56);
void beginTransmit();
void endTransmit();
void emitFrame(somfy_frame_t *frame, somfy_rx_t *rx = nullptr);
void beginFrequencyScan();
void endFrequencyScan();
void processFrequencyScan(bool received = false);
void emitFrequencyScan(uint8_t num = 255);
bool usesPin(uint8_t pin);
};
class SomfyShadeController {
protected:
uint8_t m_shadeIds[SOMFY_MAX_SHADES];
uint32_t lastCommit = 0;
public:
bool useNVS();
bool isDirty = false;
uint32_t startingAddress;
uint8_t getNextRoomId();
uint8_t getNextShadeId();
uint8_t getNextGroupId();
int8_t getMaxRoomOrder();
int8_t getMaxShadeOrder();
int8_t getMaxGroupOrder();
uint32_t getNextRemoteAddress(uint8_t shadeId);
SomfyShadeController();
Transceiver transceiver;
SomfyRoom *addRoom();
SomfyRoom *addRoom(JsonObject &obj);
SomfyShade *addShade();
SomfyShade *addShade(JsonObject &obj);
SomfyGroup *addGroup();
SomfyGroup *addGroup(JsonObject &obj);
bool deleteRoom(uint8_t roomId);
bool deleteShade(uint8_t shadeId);
bool deleteGroup(uint8_t groupId);
bool begin();
void loop();
void end();
void compressRepeaters();
uint32_t repeaters[SOMFY_MAX_REPEATERS] = {0};
SomfyRoom rooms[SOMFY_MAX_ROOMS];
SomfyShade shades[SOMFY_MAX_SHADES];
SomfyGroup groups[SOMFY_MAX_GROUPS];
bool linkRepeater(uint32_t address);
bool unlinkRepeater(uint32_t address);
void toJSONShades(JsonResponse &json);
void toJSONRooms(JsonResponse &json);
void toJSONGroups(JsonResponse &json);
void toJSONRepeaters(JsonResponse &json);
uint8_t repeaterCount();
uint8_t roomCount();
uint8_t shadeCount();
uint8_t groupCount();
void updateGroupFlags();
SomfyShade * getShadeById(uint8_t shadeId);
SomfyRoom * getRoomById(uint8_t roomId);
SomfyGroup * getGroupById(uint8_t groupId);
SomfyShade * findShadeByRemoteAddress(uint32_t address);
SomfyGroup * findGroupByRemoteAddress(uint32_t address);
void sendFrame(somfy_frame_t &frame, uint8_t repeats = 0);
void processFrame(somfy_frame_t &frame, bool internal = false);
void emitState(uint8_t num = 255);
void publish();
void processWaitingFrame();
void commit();
void writeBackup();
bool loadShadesFile(const char *filename);
#ifdef USE_NVS
bool loadLegacy();
#endif
};
#endif

86
src/SomfyController.ino Normal file
View file

@ -0,0 +1,86 @@
#include <WiFi.h>
#include <LittleFS.h>
#include <esp_task_wdt.h>
#include "ConfigSettings.h"
#include "Network.h"
#include "Web.h"
#include "Sockets.h"
#include "Utils.h"
#include "Somfy.h"
#include "MQTT.h"
#include "GitOTA.h"
ConfigSettings settings;
Web webServer;
SocketEmitter sockEmit;
Network net;
rebootDelay_t rebootDelay;
SomfyShadeController somfy;
MQTTClass mqtt;
GitUpdater git;
uint32_t oldheap = 0;
void setup() {
Serial.begin(115200);
Serial.println();
Serial.println("Startup/Boot....");
Serial.println("Mounting File System...");
if(LittleFS.begin()) Serial.println("File system mounted successfully");
else Serial.println("Error mounting file system");
settings.begin();
if(WiFi.status() == WL_CONNECTED) WiFi.disconnect(true);
delay(10);
Serial.println();
webServer.startup();
webServer.begin();
delay(1000);
net.setup();
somfy.begin();
//git.checkForUpdate();
esp_task_wdt_init(7, true); //enable panic so ESP32 restarts
esp_task_wdt_add(NULL); //add current thread to WDT watch
}
void loop() {
// put your main code here, to run repeatedly:
//uint32_t heap = ESP.getFreeHeap();
if(rebootDelay.reboot && millis() > rebootDelay.rebootTime) {
Serial.print("Rebooting after ");
Serial.print(rebootDelay.rebootTime);
Serial.println("ms");
net.end();
ESP.restart();
return;
}
uint32_t timing = millis();
net.loop();
if(millis() - timing > 100) Serial.printf("Timing Net: %ldms\n", millis() - timing);
timing = millis();
esp_task_wdt_reset();
somfy.loop();
if(millis() - timing > 100) Serial.printf("Timing Somfy: %ldms\n", millis() - timing);
timing = millis();
esp_task_wdt_reset();
if(net.connected() || net.softAPOpened) {
if(!rebootDelay.reboot && net.connected() && !net.softAPOpened) {
git.loop();
esp_task_wdt_reset();
}
webServer.loop();
esp_task_wdt_reset();
if(millis() - timing > 100) Serial.printf("Timing WebServer: %ldms\n", millis() - timing);
esp_task_wdt_reset();
timing = millis();
sockEmit.loop();
if(millis() - timing > 100) Serial.printf("Timing Socket: %ldms\n", millis() - timing);
esp_task_wdt_reset();
timing = millis();
}
if(rebootDelay.reboot && millis() > rebootDelay.rebootTime) {
net.end();
ESP.restart();
}
esp_task_wdt_reset();
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

131
src/Utils.cpp Normal file
View file

@ -0,0 +1,131 @@
#include <Arduino.h>
#include <time.h>
#include "Utils.h"
/*********************************************************************
* Timestamp class members
********************************************************************/
unsigned long Timestamp::epoch() {
struct tm tmNow;
time_t now;
if(!getLocalTime(&tmNow,50)) return 0;
time(&now);
return now;
}
time_t Timestamp::now() {
struct tm tmNow;
getLocalTime(&tmNow,50);
return mktime(&tmNow);
}
time_t Timestamp::getUTC() {
time_t t;
time(&t);
return t;
}
time_t Timestamp::mkUTCTime(struct tm *dt) {
time_t tsBadLocal = mktime(dt);
struct tm tmUTC;
struct tm tmLocal;
gmtime_r(&tsBadLocal, &tmUTC);
localtime_r(&tsBadLocal, &tmLocal);
time_t tsBadUTC = mktime(&tmUTC);
time_t tsLocal = mktime(&tmLocal);
time_t tsLocalOffset = tsLocal - tsBadUTC;
return tsBadLocal + tsLocalOffset;
}
time_t Timestamp::parseUTCTime(const char *buff) {
struct tm dt;
dt.tm_hour = 0;
dt.tm_mday = 0;
dt.tm_mon = 0;
dt.tm_year = 0;
dt.tm_wday = 0;
dt.tm_yday = 0;
dt.tm_isdst = false;
char num[5];
uint8_t i = 0;
memset(num, 0x00, sizeof(num));
for(uint8_t j = 0; j < 5 && i < strlen(buff);) {
char ch = buff[i++];
if(ch == '-') break;
if(!isdigit(ch)) continue;
else num[j++] = ch;
}
dt.tm_year = atoi(num)-1900;
memset(num, 0x00, sizeof(num));
for(uint8_t j = 0; j < 5 && i < strlen(buff);) {
char ch = buff[i++];
if(ch == '-') break;
if(!isdigit(ch)) continue;
else num[j++] = ch;
}
dt.tm_mon = atoi(num)-1;
memset(num, 0x00, sizeof(num));
for(uint8_t j = 0; j < 5 && i < strlen(buff);) {
char ch = buff[i++];
if(ch == '-' || ch == 'T' || ch == 't') break;
if(!isdigit(ch)) continue;
else num[j++] = ch;
}
dt.tm_mday = atoi(num);
memset(num, 0x00, sizeof(num));
for(uint8_t j = 0; j < 5 && i < strlen(buff);) {
char ch = buff[i++];
if(ch == '-' || ch == ':') break;
if(!isdigit(ch)) continue;
else num[j++] = ch;
}
dt.tm_hour = atoi(num);
memset(num, 0x00, sizeof(num));
for(uint8_t j = 0; j < 5 && i < strlen(buff);) {
char ch = buff[i++];
if(ch == '-' || ch == ':') break;
if(!isdigit(ch)) continue;
else num[j++] = ch;
}
dt.tm_min = atoi(num);
for(uint8_t j = 0; j < 5 && i < strlen(buff);) {
char ch = buff[i++];
if(ch == '-' || ch == ':' || ch == 'Z') break;
if(!isdigit(ch)) continue;
else num[j++] = ch;
}
dt.tm_sec = atoi(num);
//Serial.printf("Y:%d M:%d D:%d H:%d M:%d S:%d\n", dt.tm_year, dt.tm_mon, dt.tm_mday, dt.tm_hour, dt.tm_min, dt.tm_sec);
return Timestamp::mkUTCTime(&dt);
}
time_t Timestamp::getUTC(time_t t) {
tm tmUTC;
gmtime_r(&t, &tmUTC);
return mktime(&tmUTC);
}
char * Timestamp::getISOTime() { return this->getISOTime(this->getUTC()); }
char * Timestamp::getISOTime(time_t epoch) {
struct tm *dt = localtime((time_t *)&epoch);
return this->formatISO(dt, this->tzOffset());
}
char * Timestamp::formatISO(struct tm *dt, int tz) {
int tzHrs = floor(tz/100);
int tzMin = tz - (tzHrs * 100);
int ms = millis() % 1000;
snprintf(this->_timeBuffer, sizeof(this->_timeBuffer), "%04d-%02d-%02dT%02d:%02d:%02d.%03d%s%02d%02d",
dt->tm_year + 1900, dt->tm_mon + 1, dt->tm_mday, dt->tm_hour, dt->tm_min, dt->tm_sec, ms, tzHrs < 0 ? "-" : "+", abs(tzHrs), abs(tzMin));
return this->_timeBuffer;
}
int Timestamp::calcTZOffset(time_t *dt) {
tm tmLocal, tmUTC;
gmtime_r(dt, &tmUTC);
localtime_r(dt, &tmLocal);
long diff = mktime(&tmLocal) - mktime(&tmUTC);
if(tmLocal.tm_isdst) diff += 3600;
int hrs = (int)((diff/3600) * 100);
int mins = diff - (hrs * 36);
return hrs + mins;
}
int Timestamp::tzOffset() {
time_t now;
time(&now);
return Timestamp::calcTZOffset(&now);
}

99
src/Utils.h Normal file
View file

@ -0,0 +1,99 @@
#ifndef utils_h
#define utils_h
#include <Arduino.h>
#define DEBUG_SOMFY Serial
[[maybe_unused]] static void SETCHARPROP(char *prop, const char *value, size_t size) {strncpy(prop, value, size); prop[size - 1] = '\0';}
/*
namespace util {
// Createa a custom to_string function. C++ can be annoying
// with all the trailing 0s on number formats.
template <typename T> std::string to_string(const T& t) {
std::string str{std::to_string (t)};
int offset{1};
if (str.find_last_not_of('0') == str.find('.')) {
offset = 0;
}
str.erase ( str.find_last_not_of('0') + 1, std::string::npos );
str.erase ( str.find_last_not_of('.') + 1, std::string::npos );
return str;
}
}
*/
static void _ltrim(char *str) {
int s = 0, j, k = 0;
int e = strlen(str);
while(s < e && (str[s] == ' ' || str[s] == '\n' || str[s] == '\r' || str[s] == '\t' || str[s] == '"')) s++;
if(s > 0) {
for(j = s; j < e; j++) {
str[k] = str[j];
k++;
}
str[k] = '\0';
}
//if(s > 0) strcpy(str, &str[s]);
}
static void _rtrim(char *str) {
int e = strlen(str) - 1;
while(e >= 0 && (str[e] == ' ' || str[e] == '\n' || str[e] == '\r' || str[e] == '\t' || str[e] == '"')) {str[e] = '\0'; e--;}
}
[[maybe_unused]] static void _trim(char *str) { _ltrim(str); _rtrim(str); }
struct rebootDelay_t {
bool reboot = false;
int rebootTime = 0;
bool closed = false;
};
[[maybe_unused]] static bool toBoolean(const char *str, bool def) {
if(!str) return def;
if(strlen(str) == 0) return def;
else if(str[0] == 't' || str[0] == 'T' || str[0] == '1') return true;
else if(str[0] == 'f' || str[0] == 'F' || str[0] == '0') return false;
return def;
}
class Timestamp {
char _timeBuffer[128];
public:
time_t getUTC();
time_t getUTC(time_t epoch);
char * getISOTime();
char * getISOTime(time_t epoch);
char * formatISO(struct tm *dt, int tz);
int tzOffset();
static time_t parseUTCTime(const char *buff);
static time_t mkUTCTime(struct tm *dt);
static int calcTZOffset(time_t *dt);
static time_t now();
static unsigned long epoch();
};
// Sort an array
template<typename AnyType> void sortArray(AnyType array[], size_t sizeOfArray);
// Sort in reverse
template<typename AnyType> void sortArrayReverse(AnyType array[], size_t sizeOfArray);
// Sort an array with custom comparison function
template<typename AnyType> void sortArray(AnyType array[], size_t sizeOfArray, bool (*largerThan)(AnyType, AnyType));
// Sort in reverse with custom comparison function
template<typename AnyType> void sortArrayReverse(AnyType array[], size_t sizeOfArray, bool (*largerThan)(AnyType, AnyType));
namespace ArduinoSort {
template<typename AnyType> bool builtinLargerThan(AnyType first, AnyType second) { return first > second; }
//template<> bool builtinLargerThan(char* first, char* second) { return strcmp(first, second) > 0; }
template<typename AnyType> void insertionSort(AnyType array[], size_t sizeOfArray, bool reverse, bool (*largerThan)(AnyType, AnyType)) { for (size_t i = 1; i < sizeOfArray; i++) {
for (size_t j = i; j > 0 && (largerThan(array[j-1], array[j]) != reverse); j--) {
AnyType tmp = array[j-1];
array[j-1] = array[j];
array[j] = tmp;
}
}
}
}
template<typename AnyType> void sortArray(AnyType array[], size_t sizeOfArray) { ArduinoSort::insertionSort(array, sizeOfArray, false, ArduinoSort::builtinLargerThan); }
template<typename AnyType> void sortArrayReverse(AnyType array[], size_t sizeOfArray) { ArduinoSort::insertionSort(array, sizeOfArray, true, ArduinoSort::builtinLargerThan); }
template<typename AnyType> void sortArray(AnyType array[], size_t sizeOfArray, bool (*largerThan)(AnyType, AnyType)) { ArduinoSort::insertionSort(array, sizeOfArray, false, largerThan); }
template<typename AnyType> void sortArrayReverse(AnyType array[], size_t sizeOfArray, bool (*largerThan)(AnyType, AnyType)) { ArduinoSort::insertionSort(array, sizeOfArray, true, largerThan); }
#endif

204
src/WResp.cpp Normal file
View file

@ -0,0 +1,204 @@
#include "WResp.h"
void JsonSockEvent::beginEvent(WebSocketsServer *server, const char *evt, char *buff, size_t buffSize) {
this->server = server;
this->buff = buff;
this->buffSize = buffSize;
this->_nocomma = true;
this->_closed = false;
snprintf(this->buff, buffSize, "42[%s,", evt);
}
void JsonSockEvent::closeEvent() {
if(!this->_closed) {
if(strlen(this->buff) < buffSize) strcat(this->buff, "]");
else this->buff[buffSize - 1] = ']';
}
this->_nocomma = true;
this->_closed = true;
}
void JsonSockEvent::endEvent(uint8_t num) {
this->closeEvent();
if(num == 255) this->server->broadcastTXT(this->buff);
else this->server->sendTXT(num, this->buff);
}
void JsonSockEvent::_safecat(const char *val, bool escape) {
size_t len = (escape ? this->calcEscapedLength(val) : strlen(val)) + strlen(this->buff);
if(escape) len += 2;
if(len >= this->buffSize) {
Serial.printf("Socket exceeded buffer size %d - %d\n", this->buffSize, len);
Serial.println(this->buff);
return;
}
if(escape) strcat(this->buff, "\"");
if(escape) this->escapeString(val, &this->buff[strlen(this->buff)]);
else strcat(this->buff, val);
if(escape) strcat(this->buff, "\"");
}
void JsonResponse::beginResponse(WebServer *server, char *buff, size_t buffSize) {
this->server = server;
this->buff = buff;
this->buffSize = buffSize;
this->buff[0] = 0x00;
this->_nocomma = true;
server->setContentLength(CONTENT_LENGTH_UNKNOWN);
}
void JsonResponse::endResponse() {
if(strlen(buff)) this->send();
server->sendContent("", 0);
}
void JsonResponse::send() {
if(!this->_headersSent) server->send_P(200, "application/json", this->buff);
else server->sendContent(this->buff);
//Serial.printf("Sent %d bytes %d\n", strlen(this->buff), this->buffSize);
this->buff[0] = 0x00;
this->_headersSent = true;
}
void JsonResponse::_safecat(const char *val, bool escape) {
size_t len = (escape ? this->calcEscapedLength(val) : strlen(val)) + strlen(this->buff);
if(escape) len += 2;
if(len >= this->buffSize) {
this->send();
}
if(escape) strcat(this->buff, "\"");
if(escape) this->escapeString(val, &this->buff[strlen(this->buff)]);
else strcat(this->buff, val);
if(escape) strcat(this->buff, "\"");
}
void JsonFormatter::beginObject(const char *name) {
if(name && strlen(name) > 0) this->appendElem(name);
else if(!this->_nocomma) this->_safecat(",");
this->_safecat("{");
this->_objects++;
this->_nocomma = true;
}
void JsonFormatter::endObject() {
//if(strlen(this->buff) + 1 > this->buffSize - 1) this->send();
this->_safecat("}");
this->_objects--;
this->_nocomma = false;
}
void JsonFormatter::beginArray(const char *name) {
if(name && strlen(name) > 0) this->appendElem(name);
else if(!this->_nocomma) this->_safecat(",");
this->_safecat("[");
this->_arrays++;
this->_nocomma = true;
}
void JsonFormatter::endArray() {
//if(strlen(this->buff) + 1 > this->buffSize - 1) this->send();
this->_safecat("]");
this->_arrays--;
this->_nocomma = false;
}
void JsonFormatter::appendElem(const char *name) {
if(!this->_nocomma) this->_safecat(",");
if(name && strlen(name) > 0) {
this->_safecat(name, true);
this->_safecat(":");
}
this->_nocomma = false;
}
void JsonFormatter::addElem(const char *name, const char *val) {
if(!val) return;
this->appendElem(name);
this->_safecat(val, true);
}
void JsonFormatter::addElem(const char *val) { this->addElem(nullptr, val); }
void JsonFormatter::addElem(float fval) { sprintf(this->_numbuff, "%.4f", fval); this->_appendNumber(nullptr); }
void JsonFormatter::addElem(int8_t nval) { sprintf(this->_numbuff, "%d", nval); this->_appendNumber(nullptr); }
void JsonFormatter::addElem(uint8_t nval) { sprintf(this->_numbuff, "%u", nval); this->_appendNumber(nullptr); }
void JsonFormatter::addElem(int32_t nval) { sprintf(this->_numbuff, "%ld", (long)nval); this->_appendNumber(nullptr); }
void JsonFormatter::addElem(uint32_t nval) { sprintf(this->_numbuff, "%lu", (unsigned long)nval); this->_appendNumber(nullptr); }
/*
void JsonFormatter::addElem(int16_t nval) { sprintf(this->_numbuff, "%d", nval); this->_appendNumber(nullptr); }
void JsonFormatter::addElem(uint16_t nval) { sprintf(this->_numbuff, "%u", nval); this->_appendNumber(nullptr); }
void JsonFormatter::addElem(int64_t lval) { sprintf(this->_numbuff, "%lld", (long long)lval); this->_appendNumber(nullptr); }
void JsonFormatter::addElem(uint64_t lval) { sprintf(this->_numbuff, "%llu", (unsigned long long)lval); this->_appendNumber(nullptr); }
*/
void JsonFormatter::addElem(bool bval) { strcpy(this->_numbuff, bval ? "true" : "false"); this->_appendNumber(nullptr); }
void JsonFormatter::addElem(const char *name, float fval) { sprintf(this->_numbuff, "%.4f", fval); this->_appendNumber(name); }
void JsonFormatter::addElem(const char *name, int8_t nval) { sprintf(this->_numbuff, "%d", nval); this->_appendNumber(name); }
void JsonFormatter::addElem(const char *name, uint8_t nval) { sprintf(this->_numbuff, "%u", nval); this->_appendNumber(name); }
void JsonFormatter::addElem(const char *name, int32_t nval) { sprintf(this->_numbuff, "%ld", (long)nval); this->_appendNumber(name); }
void JsonFormatter::addElem(const char *name, uint32_t nval) { sprintf(this->_numbuff, "%lu", (unsigned long)nval); this->_appendNumber(name); }
/*
void JsonFormatter::addElem(const char *name, int16_t nval) { sprintf(this->_numbuff, "%d", nval); this->_appendNumber(name); }
void JsonFormatter::addElem(const char *name, uint16_t nval) { sprintf(this->_numbuff, "%u", nval); this->_appendNumber(name); }
void JsonFormatter::addElem(const char *name, int64_t lval) { sprintf(this->_numbuff, "%lld", (long long)lval); this->_appendNumber(name); }
void JsonFormatter::addElem(const char *name, uint64_t lval) { sprintf(this->_numbuff, "%llu", (unsigned long long)lval); this->_appendNumber(name); }
*/
void JsonFormatter::addElem(const char *name, bool bval) { strcpy(this->_numbuff, bval ? "true" : "false"); this->_appendNumber(name); }
void JsonFormatter::_safecat(const char *val, bool escape) {
size_t len = (escape ? this->calcEscapedLength(val) : strlen(val)) + strlen(this->buff);
if(escape) len += 2;
if(len >= this->buffSize) {
return;
}
if(escape) strcat(this->buff, "\"");
if(escape) this->escapeString(val, &this->buff[strlen(this->buff)]);
else strcat(this->buff, val);
if(escape) strcat(this->buff, "\"");
}
void JsonFormatter::_appendNumber(const char *name) { this->appendElem(name); this->_safecat(this->_numbuff); }
uint32_t JsonFormatter::calcEscapedLength(const char *raw) {
uint32_t len = 0;
for(size_t i = strlen(raw); i > 0; i--) {
switch(raw[i]) {
case '"':
case '/':
case '\b':
case '\f':
case '\n':
case '\r':
case '\t':
case '\\':
len += 2;
break;
default:
len++;
break;
}
}
return len;
}
void JsonFormatter::escapeString(const char *raw, char *escaped) {
for(uint32_t i = 0; i < strlen(raw); i++) {
switch(raw[i]) {
case '"':
strcat(escaped, "\\\"");
break;
case '/':
strcat(escaped, "\\/");
break;
case '\b':
strcat(escaped, "\\b");
break;
case '\f':
strcat(escaped, "\\f");
break;
case '\n':
strcat(escaped, "\\n");
break;
case '\r':
strcat(escaped, "\\r");
break;
case '\t':
strcat(escaped, "\\t");
break;
case '\\':
strcat(escaped, "\\\\");
break;
default:
size_t len = strlen(escaped);
escaped[len] = raw[i];
escaped[len+1] = 0x00;
break;
}
}
}

74
src/WResp.h Normal file
View file

@ -0,0 +1,74 @@
#include <WebServer.h>
#include <WebSocketsServer.h>
#include "Somfy.h"
#ifndef wresp_h
#define wresp_h
class JsonFormatter {
protected:
char *buff;
size_t buffSize;
bool _headersSent = false;
uint8_t _objects = 0;
uint8_t _arrays = 0;
bool _nocomma = true;
char _numbuff[25] = {0};
virtual void _safecat(const char *val, bool escape = false);
void _appendNumber(const char *name);
public:
void escapeString(const char *raw, char *escaped);
uint32_t calcEscapedLength(const char *raw);
void beginObject(const char *name = nullptr);
void endObject();
void beginArray(const char *name = nullptr);
void endArray();
void appendElem(const char *name = nullptr);
void addElem(const char* val);
void addElem(float fval);
void addElem(int8_t nval);
void addElem(uint8_t nval);
/*
void addElem(int32_t nval);
void addElem(int16_t nval);
void addElem(uint16_t nval);
void addElem(unsigned int nval);
*/
void addElem(int32_t lval);
void addElem(uint32_t lval);
void addElem(bool bval);
void addElem(const char* name, float fval);
void addElem(const char* name, int8_t nval);
void addElem(const char* name, uint8_t nval);
/*
void addElem(const char* name, int nval);
void addElem(const char* name, int16_t nval);
void addElem(const char* name, uint16_t nval);
void addElem(const char* name, unsigned int nval);
*/
void addElem(const char* name, int32_t lval);
void addElem(const char* name, uint32_t lval);
void addElem(const char* name, bool bval);
void addElem(const char *name, const char *val);
};
class JsonResponse : public JsonFormatter {
protected:
void _safecat(const char *val, bool escape = false) override;
public:
WebServer *server;
void beginResponse(WebServer *server, char *buff, size_t buffSize);
void endResponse();
void send();
};
class JsonSockEvent : public JsonFormatter {
protected:
bool _closed = false;
void _safecat(const char *val, bool escape = false) override;
public:
WebSocketsServer *server = nullptr;
void beginEvent(WebSocketsServer *server, const char *evt, char *buff, size_t buffSize);
void endEvent(uint8_t clientNum = 255);
void closeEvent();
};
#endif

2733
src/Web.cpp Normal file

File diff suppressed because it is too large Load diff

50
src/Web.h Normal file
View file

@ -0,0 +1,50 @@
#include <WebServer.h>
#include "Somfy.h"
#ifndef webserver_h
#define webserver_h
class Web {
public:
bool uploadSuccess = false;
void sendCORSHeaders(WebServer &server);
void sendCacheHeaders(uint32_t seconds=604800);
void startup();
void handleLogin(WebServer &server);
void handleLogout(WebServer &server);
void handleStreamFile(WebServer &server, const char *filename, const char *encoding);
void handleController(WebServer &server);
void handleLoginContext(WebServer &server);
void handleGetRepeaters(WebServer &server);
void handleGetRooms(WebServer &server);
void handleGetShades(WebServer &server);
void handleGetGroups(WebServer &server);
void handleShadeCommand(WebServer &server);
void handleRepeatCommand(WebServer &server);
void handleGroupCommand(WebServer &server);
void handleTiltCommand(WebServer &server);
void handleDiscovery(WebServer &server);
void handleNotFound(WebServer &server);
void handleRoom(WebServer &server);
void handleShade(WebServer &server);
void handleGroup(WebServer &server);
void handleSetPositions(WebServer &server);
void handleSetSensor(WebServer &server);
void handleDownloadFirmware(WebServer &server);
void handleBackup(WebServer &server, bool attach = false);
void handleReboot(WebServer &server);
void handleDeserializationError(WebServer &server, DeserializationError &err);
void begin();
void loop();
void end();
// Web Handlers
bool createAPIToken(const IPAddress ipAddress, char *token);
bool createAPIToken(const char *payload, char *token);
bool createAPIPinToken(const IPAddress ipAddress, const char *pin, char *token);
bool createAPIPasswordToken(const IPAddress ipAddress, const char *username, const char *password, char *token);
bool isAuthenticated(WebServer &server, bool cfg = false);
//void chunkRoomsResponse(WebServer &server, const char *elem = nullptr);
//void chunkShadesResponse(WebServer &server, const char *elem = nullptr);
//void chunkGroupsResponse(WebServer &server, const char *elem = nullptr);
//void chunkGroupResponse(WebServer &server, SomfyGroup *, const char *prefix = nullptr);
};
#endif

46087
src/esp32.svd Normal file

File diff suppressed because it is too large Load diff