Github firmware updates #171

This commit is contained in:
Robert Strouse 2023-10-19 14:20:33 -07:00
parent b91c54d377
commit a9325eeca5
23 changed files with 922 additions and 78 deletions

View file

@ -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;
}

View file

@ -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);

View file

@ -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");

View file

@ -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
View 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
View 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

View file

@ -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();

View file

@ -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...");

View file

@ -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);
}
}

View file

@ -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:

View file

@ -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

View file

@ -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.

View file

@ -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();

View file

@ -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.

View file

@ -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);
}

View file

@ -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
View file

@ -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);

View file

@ -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);
}

View file

@ -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>

View file

@ -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]');

View file

@ -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;