mirror of
https://github.com/rstrouse/ESPSomfy-RTS.git
synced 2025-12-13 02:52:11 +01:00
Github firmware updates #171
This commit is contained in:
parent
b91c54d377
commit
a9325eeca5
23 changed files with 922 additions and 78 deletions
|
|
@ -779,7 +779,7 @@ bool ShadeConfigFile::writeShadeRecord(SomfyShade *shade) {
|
|||
return true;
|
||||
}
|
||||
bool ShadeConfigFile::writeSettingsRecord() {
|
||||
this->writeVarString(settings.fwVersion);
|
||||
this->writeVarString(settings.fwVersion.name);
|
||||
this->writeVarString(settings.hostname);
|
||||
this->writeVarString(settings.NTP.ntpServer);
|
||||
this->writeVarString(settings.NTP.posixZone);
|
||||
|
|
@ -820,42 +820,3 @@ bool ShadeConfigFile::writeTransRecord(transceiver_config_t &cfg) {
|
|||
return true;
|
||||
}
|
||||
bool ShadeConfigFile::exists() { return LittleFS.exists("/shades.cfg"); }
|
||||
bool ShadeConfigFile::getAppVersion(appver_t &ver) {
|
||||
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();
|
||||
// Now lets parse this pig.
|
||||
memset(&ver, 0x00, sizeof(appver_t));
|
||||
char num[3];
|
||||
uint8_t i = 0;
|
||||
for(uint8_t j = 0; j < 3 && i < strlen(app); j++) {
|
||||
char ch = app[i++];
|
||||
if(ch != '.')
|
||||
num[j] = ch;
|
||||
else
|
||||
break;
|
||||
}
|
||||
ver.major = static_cast<uint8_t>(atoi(num) & 0xFF);
|
||||
memset(num, 0x00, sizeof(num));
|
||||
for(uint8_t j = 0; j < 3 && i < strlen(app); j++) {
|
||||
char ch = app[i++];
|
||||
if(ch != '.')
|
||||
num[j] = ch;
|
||||
else
|
||||
break;
|
||||
}
|
||||
ver.minor = static_cast<uint8_t>(atoi(num) & 0xFF);
|
||||
memset(num, 0x00, sizeof(num));
|
||||
for(uint8_t j = 0; j < 3 && i < strlen(app); j++) {
|
||||
char ch = app[i++];
|
||||
if(ch != '.')
|
||||
num[j] = ch;
|
||||
else
|
||||
break;
|
||||
}
|
||||
ver.build = static_cast<uint8_t>(atoi(num) & 0xFF);
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,7 +74,6 @@ class ShadeConfigFile : public ConfigFile {
|
|||
bool readNetRecord();
|
||||
bool readTransRecord(transceiver_config_t &cfg);
|
||||
public:
|
||||
static bool getAppVersion(appver_t &ver);
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include <WiFi.h>
|
||||
#include <Preferences.h>
|
||||
#include "ConfigSettings.h"
|
||||
#include "Utils.h"
|
||||
|
||||
|
||||
Preferences pref;
|
||||
|
|
@ -14,7 +15,74 @@ void restore_options_t::fromJSON(JsonObject &obj) {
|
|||
if(obj.containsKey("network")) this->network = obj["network"];
|
||||
if(obj.containsKey("transceiver")) this->transceiver = obj["transceiver"];
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
bool BaseSettings::load() { return true; }
|
||||
bool BaseSettings::loadFile(const char *filename) {
|
||||
|
|
@ -66,7 +134,7 @@ double BaseSettings::parseValueDouble(JsonObject &obj, const char *prop, double
|
|||
}
|
||||
bool ConfigSettings::begin() {
|
||||
uint32_t chipId = 0;
|
||||
|
||||
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;
|
||||
|
|
@ -86,11 +154,13 @@ bool ConfigSettings::begin() {
|
|||
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->connType = static_cast<conn_types>(pref.getChar("connType", 0x00));
|
||||
Serial.printf("Preference GFG Free Entries: %d\n", pref.freeEntries());
|
||||
//Serial.printf("Preference GFG Free Entries: %d\n", pref.freeEntries());
|
||||
pref.end();
|
||||
if(this->connType == conn_types::unset) {
|
||||
// We are doing this to convert the data from previous versions.
|
||||
|
|
@ -105,6 +175,17 @@ bool ConfigSettings::load() {
|
|||
}
|
||||
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);
|
||||
|
|
@ -136,7 +217,7 @@ void ConfigSettings::print() {
|
|||
void ConfigSettings::emitSockets() {}
|
||||
void ConfigSettings::emitSockets(uint8_t num) {}
|
||||
uint16_t ConfigSettings::calcSettingsRecSize() {
|
||||
return strlen(this->fwVersion) + 3
|
||||
return strlen(this->fwVersion.name) + 3
|
||||
+ strlen(this->hostname) + 3
|
||||
+ strlen(this->NTP.ntpServer) + 3
|
||||
+ strlen(this->NTP.posixZone) + 3
|
||||
|
|
@ -208,7 +289,7 @@ bool MQTTSettings::load() {
|
|||
return true;
|
||||
}
|
||||
bool ConfigSettings::toJSON(DynamicJsonDocument &doc) {
|
||||
doc["fwVersion"] = this->fwVersion;
|
||||
doc["fwVersion"] = this->fwVersion.name;
|
||||
JsonObject objWIFI = doc.createNestedObject("WIFI");
|
||||
this->WIFI.toJSON(objWIFI);
|
||||
JsonObject objNTP = doc.createNestedObject("NTP");
|
||||
|
|
|
|||
|
|
@ -16,6 +16,18 @@ struct restore_options_t {
|
|||
bool transceiver = 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);
|
||||
int8_t compare(appver_t &ver);
|
||||
void copy(appver_t &ver);
|
||||
};
|
||||
|
||||
|
||||
class BaseSettings {
|
||||
public:
|
||||
|
|
@ -142,7 +154,8 @@ class ConfigSettings: BaseSettings {
|
|||
char serverId[10] = "";
|
||||
char hostname[32] = "ESPSomfyRTS";
|
||||
conn_types connType = conn_types::unset;
|
||||
const char* fwVersion = FW_VERSION;
|
||||
appver_t fwVersion;
|
||||
appver_t appVersion;
|
||||
bool ssdpBroadcast = true;
|
||||
uint8_t status;
|
||||
IPSettings IP;
|
||||
|
|
@ -163,6 +176,7 @@ class ConfigSettings: BaseSettings {
|
|||
bool toJSON(DynamicJsonDocument &doc);
|
||||
uint16_t calcSettingsRecSize();
|
||||
uint16_t calcNetRecSize();
|
||||
bool getAppVersion();
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
|||
365
GitOTA.cpp
Normal file
365
GitOTA.cpp
Normal file
|
|
@ -0,0 +1,365 @@
|
|||
#include <WiFi.h>
|
||||
#include <WiFiClientSecure.h>
|
||||
#include <Update.h>
|
||||
#include <HTTPClient.h>
|
||||
#include "GitOTA.h"
|
||||
#include "Utils.h"
|
||||
#include "ConfigSettings.h"
|
||||
#include "Sockets.h"
|
||||
#include "Somfy.h"
|
||||
#include "Web.h"
|
||||
|
||||
|
||||
extern ConfigSettings settings;
|
||||
extern SocketEmitter sockEmit;
|
||||
extern SomfyShadeController somfy;
|
||||
extern rebootDelay_t rebootDelay;
|
||||
extern Web webServer;
|
||||
|
||||
|
||||
#define MAX_BUFF_SIZE 8192
|
||||
void GitRelease::setProperty(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);
|
||||
//Serial.println(this->releaseDate);
|
||||
}
|
||||
}
|
||||
bool GitRelease::toJSON(JsonObject &obj) {
|
||||
Timestamp ts;
|
||||
obj["id"] = this->id;
|
||||
obj["name"] = this->name;
|
||||
obj["date"] = ts.getISOTime(this->releaseDate);
|
||||
obj["draft"] = this->draft;
|
||||
obj["preRelease"] = this->preRelease;
|
||||
obj["main"] = this->main;
|
||||
JsonObject ver = obj.createNestedObject("version");
|
||||
this->version.toJSON(ver);
|
||||
return true;
|
||||
}
|
||||
#define ERR_CLIENT_OFFSET -50
|
||||
|
||||
int8_t GitRepo::getReleases(uint8_t num) {
|
||||
WiFiClientSecure *client = new WiFiClientSecure;
|
||||
if(client) {
|
||||
client->setInsecure();
|
||||
HTTPClient https;
|
||||
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");
|
||||
if(https.begin(*client, url)) {
|
||||
Serial.print("[HTTPS] GET...\n");
|
||||
int httpCode = https.GET();
|
||||
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;
|
||||
while(https.connected() && (len > 0 || len == -1) && ndx < count) {
|
||||
size_t size = stream->available();
|
||||
if(size) {
|
||||
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++) {
|
||||
char ch = static_cast<char>(buff[i]);
|
||||
if(ch == '[') arrTok++;
|
||||
else if(ch == ']') arrTok--;
|
||||
else if(ch == '{') {
|
||||
objTok++;
|
||||
if(objTok != 1) inElem = inValue = awaitValue = false;
|
||||
}
|
||||
else if(ch == '}') {
|
||||
objTok--;
|
||||
if(objTok == 0) ndx++;
|
||||
}
|
||||
else if(objTok == 1) {
|
||||
// We only want data from the root object.
|
||||
if(ch == '\"') {
|
||||
inQuote = !inQuote;
|
||||
if(inElem) {
|
||||
inElem = false;
|
||||
awaitValue = true;
|
||||
}
|
||||
else if(inValue) {
|
||||
inValue = false;
|
||||
inElem = false;
|
||||
awaitValue = false;
|
||||
this->releases[ndx].setProperty(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) {
|
||||
this->releases[ndx].setProperty(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 return httpCode;
|
||||
}
|
||||
https.end();
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
bool GitRepo::toJSON(JsonObject &obj) {
|
||||
JsonObject fw = obj.createNestedObject("fwVersion");
|
||||
settings.fwVersion.toJSON(fw);
|
||||
JsonObject app = obj.createNestedObject("appVersion");
|
||||
settings.appVersion.toJSON(app);
|
||||
JsonArray arr = obj.createNestedArray("releases");
|
||||
for(uint8_t i = 0; i < GIT_MAX_RELEASES + 1; i++) {
|
||||
if(this->releases[i].id == 0) continue;
|
||||
JsonObject o = arr.createNestedObject();
|
||||
this->releases[i].toJSON(o);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#define UPDATE_ERR_OFFSET 20
|
||||
#define ERR_DOWNLOAD_HTTP -40
|
||||
#define ERR_DOWNLOAD_BUFFER -41
|
||||
#define ERR_DOWNLOAD_CONNECTION -42
|
||||
|
||||
void GitUpdater::loop() {
|
||||
if(this->status == GIT_STATUS_READY) {
|
||||
if(this->lastCheck + 14400000 < millis()) { // 4 hours
|
||||
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..........");
|
||||
this->status = GIT_UPDATE_CANCELLED;
|
||||
this->emitUpdateCheck();
|
||||
this->cancelled = true;
|
||||
}
|
||||
}
|
||||
void GitUpdater::checkForUpdate() {
|
||||
if(this->status != 0) return; // If we are already checking.
|
||||
this->status = GIT_STATUS_CHECK;
|
||||
GitRepo repo;
|
||||
this->lastCheck = millis();
|
||||
this->updateAvailable = false;
|
||||
if(repo.getReleases(2) == 0) { // Get 2 releases so we can filter our pre-releases
|
||||
this->setCurrentRelease(repo);
|
||||
}
|
||||
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(JsonObject &obj) {
|
||||
obj["available"] = this->updateAvailable;
|
||||
obj["status"] = this->status;
|
||||
obj["error"] = this->error;
|
||||
obj["cancelled"] = this->cancelled;
|
||||
JsonObject fw = obj.createNestedObject("fwVersion");
|
||||
settings.fwVersion.toJSON(fw);
|
||||
JsonObject app = obj.createNestedObject("appVersion");
|
||||
settings.appVersion.toJSON(app);
|
||||
JsonObject latest = obj.createNestedObject("latest");
|
||||
this->latest.toJSON(latest);
|
||||
}
|
||||
void GitUpdater::emitUpdateCheck(uint8_t num) {
|
||||
ClientSocketEvent evt("fwStatus");
|
||||
DynamicJsonDocument doc(512);
|
||||
JsonObject obj = doc.to<JsonObject>();
|
||||
this->toJSON(obj);
|
||||
if(num == 255)
|
||||
sockEmit.sendToClients("fwStatus", doc);
|
||||
else
|
||||
sockEmit.sendToClient(num, "fwStatus", doc);
|
||||
}
|
||||
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) {
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
bool GitUpdater::beginUpdate(const char *version) {
|
||||
Serial.println("Begin update called...");
|
||||
if(strcmp(version, "main") == 0) strcpy(this->baseUrl, "https://github.com/rstrouse/ESPSomfy-RTS/blob/main/");
|
||||
else sprintf(this->baseUrl, "https://github.com/rstrouse/ESPSomfy-RTS/releases/download/%s/", version);
|
||||
strcpy(this->targetRelease, version);
|
||||
this->emitUpdateCheck();
|
||||
strcpy(this->currentFile, "SomfyController.ino.esp32.bin");
|
||||
this->partition = U_FLASH;
|
||||
this->cancelled = false;
|
||||
this->error = 0;
|
||||
this->error = this->downloadFile();
|
||||
if(this->error == 0) {
|
||||
strcpy(this->currentFile, "SomfyController.littlefs.bin");
|
||||
this->partition = U_SPIFFS;
|
||||
this->error = this->downloadFile();
|
||||
if(this->error == 0) {
|
||||
settings.fwVersion.parse(version);
|
||||
somfy.commit();
|
||||
rebootDelay.reboot = true;
|
||||
rebootDelay.rebootTime = millis() + 500;
|
||||
}
|
||||
}
|
||||
this->status = GIT_UPDATE_COMPLETE;
|
||||
this->emitUpdateCheck();
|
||||
return true;
|
||||
}
|
||||
bool GitUpdater::endUpdate() { return true; }
|
||||
int8_t GitUpdater::downloadFile() {
|
||||
WiFiClientSecure *client = new WiFiClientSecure;
|
||||
Serial.printf("Begin update %s\n", this->currentFile);
|
||||
if(client) {
|
||||
client->setInsecure();
|
||||
HTTPClient https;
|
||||
uint8_t ndx = 0;
|
||||
char url[128];
|
||||
sprintf(url, "%s%s", this->baseUrl, this->currentFile);
|
||||
if(https.begin(*client, 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)) {
|
||||
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);
|
||||
while(https.connected() && (len > 0 || len == -1) && total < len) {
|
||||
size_t size = stream->available();
|
||||
if(size) {
|
||||
if(this->cancelled) {
|
||||
Update.abort();
|
||||
https.end();
|
||||
free(buff);
|
||||
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();
|
||||
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()) {
|
||||
Update.printError(Serial);
|
||||
}
|
||||
else {
|
||||
|
||||
}
|
||||
https.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
Serial.printf("Update %s complete\n", this->currentFile);
|
||||
|
||||
free(buff);
|
||||
}
|
||||
else {
|
||||
// TODO: memory allocation error.
|
||||
}
|
||||
}
|
||||
}
|
||||
https.end();
|
||||
Serial.printf("End update %s\n", this->currentFile);
|
||||
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
58
GitOTA.h
Normal file
58
GitOTA.h
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
#ifndef GITOTA_H
|
||||
#define GITOTA_H
|
||||
#include <Arduino.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <time.h>
|
||||
#include "ConfigSettings.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;
|
||||
time_t releaseDate;
|
||||
char name[32] = "";
|
||||
appver_t version;
|
||||
void setProperty(const char *key, const char *val);
|
||||
bool toJSON(JsonObject &obj);
|
||||
};
|
||||
|
||||
class GitRepo {
|
||||
public:
|
||||
int8_t getReleases(uint8_t num = GIT_MAX_RELEASES);
|
||||
GitRelease releases[GIT_MAX_RELEASES + 1];
|
||||
bool toJSON(JsonObject &obj);
|
||||
};
|
||||
class GitUpdater {
|
||||
public:
|
||||
uint8_t status = 0;
|
||||
unsigned long lastCheck = 0;
|
||||
bool updateAvailable = false;
|
||||
appver_t latest;
|
||||
bool cancelled = false;
|
||||
int8_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 setCurrentRelease(GitRepo &repo);
|
||||
void loop();
|
||||
void toJSON(JsonObject &obj);
|
||||
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
|
||||
2
MQTT.cpp
2
MQTT.cpp
|
|
@ -199,7 +199,7 @@ bool MQTTClass::connect() {
|
|||
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, true);
|
||||
this->publish("firmware", settings.fwVersion.name, true);
|
||||
this->publish("serverId", settings.serverId, true);
|
||||
this->publish("mac", net.mac.c_str());
|
||||
somfy.publish();
|
||||
|
|
|
|||
|
|
@ -198,7 +198,7 @@ void Network::setConnected(conn_types connType) {
|
|||
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));
|
||||
MDNS.addServiceTxt("espsomfy_rts", "tcp", "version", String(settings.fwVersion.name));
|
||||
}
|
||||
if(settings.ssdpBroadcast) {
|
||||
if(SSDP.begin()) Serial.println("SSDP Client Started...");
|
||||
|
|
|
|||
4
SSDP.cpp
4
SSDP.cpp
|
|
@ -787,7 +787,7 @@ void SSDPClass::schema(Print &client) {
|
|||
r->modelURL,
|
||||
r->manufacturer,
|
||||
r->manufacturerURL,
|
||||
settings.fwVersion,
|
||||
settings.fwVersion.name,
|
||||
r->uuid );
|
||||
char *devList = strstr(buff, "</device>") - strlen("</deviceList>");
|
||||
devList[0] = '\0';
|
||||
|
|
@ -806,7 +806,7 @@ void SSDPClass::schema(Print &client) {
|
|||
dev->modelURL,
|
||||
dev->manufacturer,
|
||||
dev->manufacturerURL,
|
||||
settings.fwVersion,
|
||||
settings.fwVersion.name,
|
||||
dev->uuid);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
61
Sockets.cpp
61
Sockets.cpp
|
|
@ -5,12 +5,14 @@
|
|||
#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);
|
||||
|
||||
|
|
@ -50,7 +52,12 @@ uint8_t room_t::activeClients() {
|
|||
void ClientSocketEvent::prepareMessage(const char *evt, const char *payload) {
|
||||
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
|
||||
|
|
@ -85,6 +92,34 @@ void ClientSocketEvent::appendMessage(const char *text) {
|
|||
strcat(this->msg, text);
|
||||
strcat(this->msg, "]");
|
||||
}
|
||||
/*
|
||||
void ClientSocketEvent::appendJSONElem(const char *elem) {
|
||||
this->msg[strlen(this->msg) - 1] = '\0'; // Trim off the ending bracket.
|
||||
uint16_t len = strlen(this->msg);
|
||||
if(len > 0) {
|
||||
if(this->msg[strlen(this->msg) - 1] == '{') strcat(this->msg, ',');
|
||||
}
|
||||
strcat(this->msg, "\"");
|
||||
strcat(this->msg, elem);
|
||||
strcat(this->msg, "\":");
|
||||
strcat(this->msg, "]");
|
||||
}
|
||||
void ClientSocketEvent::appendJSON(const char *elem, const char *text, bool quoted) {
|
||||
this->appendJSONElem(elem);
|
||||
this->msg[strlen(this->msg) - 1] = '\0'; // Trim off the ending bracket.
|
||||
if(quoted) strcat(this->msg, "\"");
|
||||
strcat(this->msg, text);
|
||||
if(quoted) strcat(this->msg, "\"");
|
||||
strcat(this->msg, "]");
|
||||
}
|
||||
void ClientSocketEvent::appendJSON(const char *elem, const bool b) { this->appendJSON(elem, b ? "true" : "false", false); }
|
||||
void ClientSocketEvent::appendJSON(const char *elem, const uint8_t val) {
|
||||
char buff[5];
|
||||
sprintf(buff, "%d", val);
|
||||
this->appendJSON(elem, buff, false);
|
||||
}
|
||||
*/
|
||||
|
||||
uint8_t SocketEmitter::activeClients(uint8_t room) {
|
||||
if(room < SOCK_MAX_ROOMS) return this->rooms[room].activeClients();
|
||||
return 0;
|
||||
|
|
@ -99,8 +134,14 @@ bool SocketEmitter::sendToRoom(uint8_t room, ClientSocketEvent *evt) {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
bool SocketEmitter::sendToClients(ClientSocketEvent *evt) { return sockServer.broadcastTXT(evt->msg); }
|
||||
bool SocketEmitter::sendToClient(uint8_t num, ClientSocketEvent *evt) { return sockServer.sendTXT(num, evt->msg); }
|
||||
bool SocketEmitter::sendToClients(ClientSocketEvent *evt) {
|
||||
if(evt->msg[strlen(evt->msg) - 1] != ']') strcat(evt->msg, "]");
|
||||
return sockServer.broadcastTXT(evt->msg);
|
||||
}
|
||||
bool SocketEmitter::sendToClient(uint8_t num, ClientSocketEvent *evt) {
|
||||
if(evt->msg[strlen(evt->msg) - 1] != ']') strcat(evt->msg, "]");
|
||||
return sockServer.sendTXT(num, evt->msg);
|
||||
}
|
||||
bool SocketEmitter::sendToClients(const char *evt, const char *payload) {
|
||||
if(settings.status == DS_FWUPDATE) return true;
|
||||
this->evt.prepareMessage(evt, payload);
|
||||
|
|
@ -111,6 +152,17 @@ bool SocketEmitter::sendToClient(uint8_t num, const char *evt, const char *paylo
|
|||
this->evt.prepareMessage(evt, payload);
|
||||
return sockServer.sendTXT(num, this->evt.msg);
|
||||
}
|
||||
bool SocketEmitter::sendToClient(uint8_t num, const char *evt, JsonDocument &doc) {
|
||||
if(settings.status == DS_FWUPDATE) return true;
|
||||
this->evt.prepareMessage(evt, doc);
|
||||
return sockServer.sendTXT(num, this->evt.msg);
|
||||
}
|
||||
bool SocketEmitter::sendToClients(const char *evt, JsonDocument &doc) {
|
||||
if(settings.status == DS_FWUPDATE) return true;
|
||||
this->evt.prepareMessage(evt, doc);
|
||||
return sockServer.broadcastTXT(this->evt.msg);
|
||||
}
|
||||
|
||||
void SocketEmitter::end() { sockServer.close(); }
|
||||
void SocketEmitter::disconnect() { sockServer.disconnect(); }
|
||||
void SocketEmitter::wsEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) {
|
||||
|
|
@ -139,6 +191,7 @@ void SocketEmitter::wsEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t
|
|||
settings.emitSockets(num);
|
||||
somfy.emitState(num);
|
||||
net.emitSockets(num);
|
||||
git.emitUpdateCheck(num);
|
||||
}
|
||||
break;
|
||||
case WStype_TEXT:
|
||||
|
|
|
|||
|
|
@ -22,7 +22,10 @@ class ClientSocketEvent {
|
|||
ClientSocketEvent(const char *evt, const char *data);
|
||||
char msg[2048];
|
||||
void prepareMessage(const char *evt, const char *data);
|
||||
void prepareMessage(const char *evt, JsonDocument &doc);
|
||||
void appendMessage(const char *text);
|
||||
void appendElement(const char *elem, const char *val);
|
||||
|
||||
|
||||
};
|
||||
class SocketEmitter {
|
||||
|
|
@ -41,8 +44,8 @@ class SocketEmitter {
|
|||
bool sendToClient(uint8_t num, ClientSocketEvent *evt);
|
||||
bool sendToClients(const char *evt, const char *data);
|
||||
bool sendToClient(uint8_t num, const char *evt, const char *data);
|
||||
//bool sendToClients(const char *evt, JsonObject &obj);
|
||||
//bool sendToClient(uint8_t num, const char *evt, JsonObject &obj);
|
||||
bool sendToClients(const char *evt, JsonDocument &doc);
|
||||
bool sendToClient(uint8_t num, const char *evt, JsonDocument &doc);
|
||||
static void wsEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length);
|
||||
};
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -413,6 +413,7 @@ SomfyShadeController::SomfyShadeController() {
|
|||
uint64_t mac = ESP.getEfuseMac();
|
||||
this->startingAddress = mac & 0x0FFFFF;
|
||||
}
|
||||
bool SomfyShadeController::useNVS() { return !(settings.appVersion.major > 1 || settings.appVersion.minor >= 4); };
|
||||
SomfyShade *SomfyShadeController::findShadeByRemoteAddress(uint32_t address) {
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
|
||||
SomfyShade &shade = this->shades[i];
|
||||
|
|
@ -492,8 +493,8 @@ bool SomfyShadeController::loadLegacy() {
|
|||
}
|
||||
bool SomfyShadeController::begin() {
|
||||
// Load up all the configuration data.
|
||||
ShadeConfigFile::getAppVersion(this->appVersion);
|
||||
Serial.printf("App Version:%u.%u.%u\n", this->appVersion.major, this->appVersion.minor, this->appVersion.build);
|
||||
//ShadeConfigFile::getAppVersion(this->appVersion);
|
||||
Serial.printf("App Version:%u.%u.%u\n", settings.appVersion.major, settings.appVersion.minor, settings.appVersion.build);
|
||||
if(!this->useNVS()) { // At 1.4 we started using the configuration file. If the file doesn't exist then booh.
|
||||
// We need to remove all the extraeneous data from NVS for the shades. From here on out we
|
||||
// will rely on the shade configuration.
|
||||
|
|
|
|||
9
Somfy.h
9
Somfy.h
|
|
@ -1,5 +1,6 @@
|
|||
#ifndef SOMFY_H
|
||||
#define SOMFY_H
|
||||
#include "ConfigSettings.h"
|
||||
|
||||
#define SOMFY_MAX_SHADES 32
|
||||
#define SOMFY_MAX_GROUPS 16
|
||||
|
|
@ -17,11 +18,6 @@
|
|||
#define SOMFY_NO_WIND_REMOTE_TIMEOUT SECS_TO_MILLIS(30)
|
||||
|
||||
|
||||
struct appver_t {
|
||||
uint8_t major;
|
||||
uint8_t minor;
|
||||
uint8_t build;
|
||||
};
|
||||
enum class radio_proto : byte { // Ordinal byte 0-255
|
||||
RTS = 0x00,
|
||||
RTW = 0x01,
|
||||
|
|
@ -458,8 +454,7 @@ class SomfyShadeController {
|
|||
uint8_t m_shadeIds[SOMFY_MAX_SHADES];
|
||||
uint32_t lastCommit = 0;
|
||||
public:
|
||||
appver_t appVersion;
|
||||
bool useNVS() { return !(this->appVersion.major > 1 || this->appVersion.minor >= 4); }
|
||||
bool useNVS();
|
||||
bool isDirty = false;
|
||||
uint32_t startingAddress;
|
||||
uint8_t getNextShadeId();
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#include "Utils.h"
|
||||
#include "Somfy.h"
|
||||
#include "MQTT.h"
|
||||
#include "GitOTA.h"
|
||||
|
||||
ConfigSettings settings;
|
||||
Web webServer;
|
||||
|
|
@ -15,15 +16,16 @@ Network net;
|
|||
rebootDelay_t rebootDelay;
|
||||
SomfyShadeController somfy;
|
||||
MQTTClass mqtt;
|
||||
GitUpdater git;
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
Serial.println();
|
||||
Serial.println("Startup/Boot....");
|
||||
settings.begin();
|
||||
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();
|
||||
|
|
@ -32,9 +34,12 @@ void setup() {
|
|||
delay(1000);
|
||||
net.setup();
|
||||
somfy.begin();
|
||||
git.checkForUpdate();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
if(!rebootDelay.reboot) git.loop();
|
||||
|
||||
// put your main code here, to run repeatedly:
|
||||
if(rebootDelay.reboot && millis() > rebootDelay.rebootTime) {
|
||||
Serial.print("Rebooting after ");
|
||||
|
|
@ -56,7 +61,6 @@ void loop() {
|
|||
sockEmit.loop();
|
||||
if(millis() - timing > 100) Serial.printf("Timing Socket: %dms\n", millis() - timing);
|
||||
timing = millis();
|
||||
|
||||
}
|
||||
if(rebootDelay.reboot && millis() > rebootDelay.rebootTime) {
|
||||
net.end();
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
84
Utils.cpp
84
Utils.cpp
|
|
@ -6,11 +6,82 @@
|
|||
/*********************************************************************
|
||||
* Timestamp class members
|
||||
********************************************************************/
|
||||
time_t Timestamp::now() {
|
||||
struct tm tmNow;
|
||||
getLocalTime(&tmNow);
|
||||
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 = {0};
|
||||
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);
|
||||
|
|
@ -29,15 +100,18 @@ char * Timestamp::formatISO(struct tm *dt, int tz) {
|
|||
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::tzOffset() {
|
||||
time_t now;
|
||||
time(&now);
|
||||
int Timestamp::calcTZOffset(time_t *dt) {
|
||||
tm tmLocal, tmUTC;
|
||||
gmtime_r(&now, &tmUTC);
|
||||
localtime_r(&now, &tmLocal);
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
4
Utils.h
4
Utils.h
|
|
@ -59,6 +59,10 @@ class Timestamp {
|
|||
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();
|
||||
};
|
||||
// Sort an array
|
||||
template<typename AnyType> void sortArray(AnyType array[], size_t sizeOfArray);
|
||||
|
|
|
|||
73
Web.cpp
73
Web.cpp
|
|
@ -10,6 +10,7 @@
|
|||
#include "SSDP.h"
|
||||
#include "Somfy.h"
|
||||
#include "MQTT.h"
|
||||
#include "GitOTA.h"
|
||||
|
||||
extern ConfigSettings settings;
|
||||
extern SSDPClass SSDP;
|
||||
|
|
@ -17,6 +18,8 @@ extern rebootDelay_t rebootDelay;
|
|||
extern SomfyShadeController somfy;
|
||||
extern Web webServer;
|
||||
extern MQTTClass mqtt;
|
||||
extern GitUpdater git;
|
||||
|
||||
#define WEB_MAX_RESPONSE 16384
|
||||
static char g_content[WEB_MAX_RESPONSE];
|
||||
|
||||
|
|
@ -231,7 +234,7 @@ void Web::handleLoginContext(WebServer &server) {
|
|||
obj["type"] = static_cast<uint8_t>(settings.Security.type);
|
||||
obj["permissions"] = settings.Security.permissions;
|
||||
obj["serverId"] = settings.serverId;
|
||||
obj["version"] = settings.fwVersion;
|
||||
obj["version"] = settings.fwVersion.name;
|
||||
obj["model"] = "ESPSomfyRTS";
|
||||
obj["hostname"] = settings.hostname;
|
||||
serializeJson(doc, g_content);
|
||||
|
|
@ -687,7 +690,7 @@ void Web::handleDiscovery(WebServer &server) {
|
|||
DynamicJsonDocument doc(16384);
|
||||
JsonObject obj = doc.to<JsonObject>();
|
||||
obj["serverId"] = settings.serverId;
|
||||
obj["version"] = settings.fwVersion;
|
||||
obj["version"] = settings.fwVersion.name;
|
||||
obj["model"] = "ESPSomfyRTS";
|
||||
obj["hostname"] = settings.hostname;
|
||||
obj["authType"] = static_cast<uint8_t>(settings.Security.type);
|
||||
|
|
@ -892,6 +895,68 @@ void Web::begin() {
|
|||
server.on("/loginContext", []() { webServer.handleLoginContext(server); });
|
||||
server.on("/shades.cfg", []() { webServer.handleStreamFile(server, "/shades.cfg", _encoding_text); });
|
||||
server.on("/shades.tmp", []() { webServer.handleStreamFile(server, "/shades.tmp", _encoding_text); });
|
||||
server.on("/getReleases", []() {
|
||||
webServer.sendCORSHeaders(server);
|
||||
if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; }
|
||||
GitRepo repo;
|
||||
repo.getReleases();
|
||||
git.setCurrentRelease(repo);
|
||||
DynamicJsonDocument doc(2048);
|
||||
JsonObject obj = doc.to<JsonObject>();
|
||||
repo.toJSON(obj);
|
||||
serializeJson(doc, g_content);
|
||||
server.send(200, _encoding_json, g_content);
|
||||
});
|
||||
server.on("/downloadFirmware", []() {
|
||||
webServer.sendCORSHeaders(server);
|
||||
if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; }
|
||||
GitRepo repo;
|
||||
GitRelease *rel = nullptr;
|
||||
int8_t err = repo.getReleases();
|
||||
Serial.println("downloadFirmware called...");
|
||||
if(err == 0) {
|
||||
if(server.hasArg("ver")) {
|
||||
if(strcmp(server.arg("ver").c_str(), "latest") == 0) rel = &repo.releases[0];
|
||||
else if(strcmp(server.arg("ver").c_str(), "main") == 0) {
|
||||
rel = &repo.releases[GIT_MAX_RELEASES];
|
||||
}
|
||||
else {
|
||||
for(uint8_t i = 0; i < GIT_MAX_RELEASES; i++) {
|
||||
if(repo.releases[i].id == 0) continue;
|
||||
if(strcmp(repo.releases[i].name, server.arg("ver").c_str()) == 0) {
|
||||
rel = &repo.releases[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
if(rel) {
|
||||
DynamicJsonDocument sdoc(512);
|
||||
JsonObject sobj = sdoc.to<JsonObject>();
|
||||
rel->toJSON(sobj);
|
||||
serializeJson(sdoc, g_content);
|
||||
server.send(200, _encoding_json, g_content);
|
||||
strcpy(git.targetRelease, rel->name);
|
||||
git.status = GIT_AWAITING_UPDATE;
|
||||
}
|
||||
else
|
||||
server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Release not found in repo.\"}"));
|
||||
}
|
||||
else
|
||||
server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Release version not supplied.\"}"));
|
||||
}
|
||||
else {
|
||||
server.send(err, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Error communicating with Github.\"}"));
|
||||
}
|
||||
});
|
||||
server.on("/cancelFirmware", []() {
|
||||
webServer.sendCORSHeaders(server);
|
||||
if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; }
|
||||
DynamicJsonDocument sdoc(512);
|
||||
JsonObject sobj = sdoc.to<JsonObject>();
|
||||
git.status = GIT_UPDATE_CANCELLING;
|
||||
git.toJSON(sobj);
|
||||
serializeJson(sdoc, g_content);
|
||||
server.send(200, _encoding_json, g_content);
|
||||
});
|
||||
server.on("/backup", []() {
|
||||
webServer.sendCORSHeaders(server);
|
||||
char filename[120];
|
||||
|
|
@ -2212,7 +2277,7 @@ void Web::begin() {
|
|||
webServer.sendCORSHeaders(server);
|
||||
DynamicJsonDocument doc(512);
|
||||
JsonObject obj = doc.to<JsonObject>();
|
||||
doc["fwVersion"] = settings.fwVersion;
|
||||
doc["fwVersion"] = settings.fwVersion.name;
|
||||
settings.toJSON(obj);
|
||||
//settings.Ethernet.toJSON(obj);
|
||||
//settings.WIFI.toJSON(obj);
|
||||
|
|
@ -2224,7 +2289,7 @@ void Web::begin() {
|
|||
webServer.sendCORSHeaders(server);
|
||||
DynamicJsonDocument doc(2048);
|
||||
JsonObject obj = doc.to<JsonObject>();
|
||||
doc["fwVersion"] = settings.fwVersion;
|
||||
doc["fwVersion"] = settings.fwVersion.name;
|
||||
settings.toJSON(obj);
|
||||
JsonObject eth = obj.createNestedObject("ethernet");
|
||||
settings.Ethernet.toJSON(eth);
|
||||
|
|
|
|||
|
|
@ -1222,4 +1222,29 @@ div.indicator-sun {
|
|||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
i.icss-github-o {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
border-radius: 50%;
|
||||
border: 0.068em solid currentcolor;
|
||||
background-color: transparent;
|
||||
background-image: radial-gradient(ellipse 100% 73% at 50% 0.48em, currentColor 0%, currentColor 38%, transparent 38%), radial-gradient(circle at 35.5% 0.5em, currentColor 0%, currentColor 27%, transparent 27%), radial-gradient(circle at 64.5% 0.5em, currentColor 0%, currentColor 27%, transparent 27%), radial-gradient(ellipse 45% 100% at 50% 0.88em, currentColor 0%, currentColor 40%, transparent 40%);
|
||||
}
|
||||
|
||||
i.icss-github-o:before {
|
||||
border-width: .1em .1em;
|
||||
border-style: solid;
|
||||
border-radius: 0.02em 60% 100% 80%;
|
||||
left: .14em;
|
||||
top: .2em;
|
||||
transform: rotate(20deg);
|
||||
}
|
||||
|
||||
i.icss-github-o:after {
|
||||
border-width: .1em .1em;
|
||||
border-style: solid;
|
||||
border-radius: 0.02em 80% 100% 60%;
|
||||
left: .54em;
|
||||
top: .2em;
|
||||
transform: rotate(65deg);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
<body>
|
||||
<div id="divContainer" class="container main" data-auth="false">
|
||||
<div id="divAuthenticated" style="display:none;">
|
||||
<div id="divFirmwareUpdate" class="firmware-message" style="display:none;">Firmware Update Available</div>
|
||||
<div id="divRadioError" class="header-message">Radio Not Initialized</div>
|
||||
<h1 style="text-align: center;"><img src="icon.png" style="width:50px;float:left;margin-left:1px;margin-top:-10px;" /><span>ESPSomfy RTS</span><span class="button-outline" onclick="ui.toggleConfig();" style="float:right;font-size:1.25rem;display:inline-block;vertical-align:middle;width:38px;height:38px;position:relative;padding-top:4px;"><span style="vertical-align:middle;clear:both;text-align:center;display:inline-block;"><i id="icoConfig" class="icss-gear" style=""></i></span></span></h1>
|
||||
<div id="divConfigPnl" style="display:none;">
|
||||
|
|
@ -120,6 +121,9 @@
|
|||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button id="btnUpdateGithub" type="button" onclick="firmware.updateGithub();">
|
||||
<i class="icss-github-o" style="font-size:1.7em;margin-top:-14px;margin-bottom:-10px;margin-right:7px;"></i><span>Github Update</span>
|
||||
</button>
|
||||
<button id="btnUpdateFirmware" type="button" onclick="firmware.updateFirmware();">
|
||||
Update Firmware
|
||||
</button>
|
||||
|
|
|
|||
133
data/index.js
133
data/index.js
|
|
@ -1,7 +1,22 @@
|
|||
var errors = [
|
||||
{ code: -10, desc: "Pin setting in use for Transceiver. Output pins cannot be re-used." },
|
||||
{ code: -11, desc: "Pin setting in use for Ethernet Adapter. Output pins cannot be re-used." },
|
||||
{ code: -12, desc: "Pin setting in use on another motor. Output pins cannot be re-used." }
|
||||
{ code: -12, desc: "Pin setting in use on another motor. Output pins cannot be re-used." },
|
||||
{ code: -21, desc: "Git Update: Flash write failed." },
|
||||
{ code: -22, desc: "Git Update: Flash erase failed." },
|
||||
{ code: -23, desc: "Git Update: Flash read failed." },
|
||||
{ code: -24, desc: "Git Update: Not enough space." },
|
||||
{ code: -25, desc: "Git Update: Invalid file size given." },
|
||||
{ code: -26, desc: "Git Update: Stream read timeout." },
|
||||
{ code: -27, desc: "Git Update: MD5 check failed." },
|
||||
{ code: -28, desc: "Git Update: Wrong Magic Byte." },
|
||||
{ code: -29, desc: "Git Update: Could not activate firmware." },
|
||||
{ code: -30, desc: "Git Update: Partition could not be found." },
|
||||
{ code: -31, desc: "Git Update: Bad Argument." },
|
||||
{ code: -32, desc: "Git Update: Aborted." },
|
||||
{ code: -40, desc: "Git Download: Http Error." },
|
||||
{ code: -41, desc: "Git Download: Buffer Allocation Error." },
|
||||
{ code: -42, desc: "Git Download: Download Connection Error." }
|
||||
]
|
||||
document.oncontextmenu = (event) => {
|
||||
if (event.target && event.target.tagName.toLowerCase() === 'input' && (event.target.type.toLowerCase() === 'text' || event.target.type.toLowerCase() === 'password'))
|
||||
|
|
@ -469,6 +484,12 @@ async function initSockets() {
|
|||
return value;
|
||||
});
|
||||
switch (eventName) {
|
||||
case 'updateProgress':
|
||||
firmware.procUpdateProgress(msg);
|
||||
break;
|
||||
case 'fwStatus':
|
||||
firmware.procFwStatus(msg);
|
||||
break;
|
||||
case 'remoteFrame':
|
||||
somfy.procRemoteFrame(msg);
|
||||
break;
|
||||
|
|
@ -4053,6 +4074,116 @@ class Firmware {
|
|||
div.innerHTML = html;
|
||||
return div;
|
||||
}
|
||||
procFwStatus(rel) {
|
||||
console.log(rel);
|
||||
let div = document.getElementById('divFirmwareUpdate');
|
||||
if (rel.updateAvailable && rel.status === 0) {
|
||||
div.style.color = 'red';
|
||||
div.innerHTML = `Firmware ${rel.latest.name} Available`;
|
||||
}
|
||||
else {
|
||||
switch (rel.status) {
|
||||
case 2: // Awaiting update.
|
||||
div.style.color = 'red';
|
||||
div.innerHTML = `Preparing firmware update`;
|
||||
break;
|
||||
case 3: // Updating -- this will be set by the update progress.
|
||||
break;
|
||||
case 5:
|
||||
div.style.color = 'red';
|
||||
div.innerHTML = `Cancelling firmware update`;
|
||||
break;
|
||||
case 6:
|
||||
div.style.color = 'red';
|
||||
div.innerHTML = `Firmware update cancelled`;
|
||||
break;
|
||||
|
||||
default:
|
||||
div.style.color = 'black';
|
||||
div.innerHTML = `Firmware ${rel.fwVersion.name} Installed`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
div.style.display = '';
|
||||
}
|
||||
procUpdateProgress(prog) {
|
||||
let pct = Math.round((prog.loaded / prog.total) * 100);
|
||||
let file = prog.part === 100 ? 'Application' : 'Firmware';
|
||||
let div = document.getElementById('divFirmwareUpdate');
|
||||
if (div) {
|
||||
div.style.color = 'red';
|
||||
div.innerHTML = `Updating ${file} to ${prog.ver} ${pct}%`;
|
||||
}
|
||||
let git = document.getElementById('divGitInstall');
|
||||
if (git) {
|
||||
// Update the status on the client that started the install.
|
||||
if (pct >= 100 && prog.part === 100) git.remove();
|
||||
else {
|
||||
let p = prog.part === 100 ? document.getElementById('progApplicationDownload') : document.getElementById('progFirmwareDownload');
|
||||
if (p) {
|
||||
p.style.setProperty('--progress', `${pct}%`);
|
||||
p.setAttribute('data-progress', `${pct}%`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
installGitRelease(div) {
|
||||
let obj = ui.fromElement(div);
|
||||
console.log(obj);
|
||||
putJSONSync(`/downloadFirmware?ver=${obj.version}`, {}, (err, ver) => {
|
||||
if (err) ui.serviceError(err);
|
||||
else {
|
||||
// Change the display and allow the percentage to be shown when the socket emits the progress.
|
||||
let html = `<div>Installing ${ver.name}</div><div style="font-size:.7em;margin-top:4px;">Please wait as the files are downloaded and installed.</div>`;
|
||||
html += `<div class="progress-bar" id="progFirmwareDownload" style="--progress:0%;margin-top:10px;text-align:center;"></div>`;
|
||||
html += `<label for="progFirmwareDownload" style="font-size:10pt;">Firmware Install Progress</label>`;
|
||||
html += `<div class="progress-bar" id="progApplicationDownload" style="--progress:0%;margin-top:10px;text-align:center;"></div>`;
|
||||
html += `<label for="progFirmwareDownload" style="font-size:10pt;">Application Install Progress</label>`;
|
||||
html += `<hr></hr><div class="button-container" style="text-align:center;">`;
|
||||
html += `<button id="btnCancelUpdate" type="button" style="width:40%;padding-left:20px;padding-right:20px;display:inline-block;" onclick="firmware.cancelInstallGit(document.getElementById('divGitInstall'));">Cancel</button>`;
|
||||
html += `</div>`;
|
||||
div.innerHTML = html;
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
cancelInstallGit(div) {
|
||||
putJSONSync(`/cancelFirmware`, {}, (err, ver) => {
|
||||
if (err) ui.serviceError(err);
|
||||
else console.log(ver);
|
||||
div.remove();
|
||||
});
|
||||
}
|
||||
updateGithub() {
|
||||
getJSONSync('/getReleases', (err, rel) => {
|
||||
if (err) ui.serviceError(err);
|
||||
else {
|
||||
console.log(rel);
|
||||
let div = document.createElement('div');
|
||||
div.setAttribute('id', 'divGitInstall')
|
||||
div.setAttribute('class', 'inst-overlay');
|
||||
div.style.width = '100%';
|
||||
div.style.alignContent = 'center';
|
||||
let html = `<div>Select a version from the repository to install using the dropdown below. Then press the update button to install that version.</div><div style="font-size:.7em;margin-top:4px;">Select Main to install the most recent alpha version from the repository.</div>`;
|
||||
html += `<div class="field-group" style = "text-align:center;">`;
|
||||
html += `<select id="selVersion" data-bind="version" style="width:50%;font-size:2em;color:white;">`
|
||||
for (let i = 0; i < rel.releases.length; i++) {
|
||||
html += `<option style="text-align:left;font-size:.5em;color:black;" value="${rel.releases[i].version.name}">${rel.releases[i].name}</option>`
|
||||
}
|
||||
html += `<label for="selVersion">Select a version</label>`;
|
||||
html += '</select></div>';
|
||||
html += `<hr></hr><div class="button-container" style="text-align:center;">`;
|
||||
html += `<button id="btnUpdate" type="button" style="width:40%;padding-left:20px;padding-right:20px;display:inline-block;margin-right:7px;" onclick="firmware.installGitRelease(document.getElementById('divGitInstall'));">Update</button>`;
|
||||
html += `<button id="btnClose" type="button" style="width:40%;padding-left:20px;padding-right:20px;display:inline-block;" onclick="document.getElementById('divGitInstall').remove();">Cancel</button>`;
|
||||
html += `</div></div>`;
|
||||
|
||||
div.innerHTML = html;
|
||||
document.getElementById('divContainer').appendChild(div);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
updateFirmware() {
|
||||
let div = this.createFileUploader('/updateFirmware');
|
||||
let inst = div.querySelector('div[id=divInstText]');
|
||||
|
|
|
|||
|
|
@ -22,6 +22,13 @@
|
|||
padding: 4px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.firmware-message {
|
||||
text-align:center;
|
||||
font-weight:bold;
|
||||
position:relative;
|
||||
margin-top:-14px;
|
||||
font-size:8pt;
|
||||
}
|
||||
.inst-overlay {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue