Add the ability to set the number of repeats per motor when sending commands #113

This commit is contained in:
Robert Strouse 2023-07-23 15:22:20 -07:00
parent cd24ca3510
commit ff16bb6107
10 changed files with 134 additions and 71 deletions

View file

@ -6,10 +6,10 @@
extern Preferences pref;
#define SHADE_HDR_VER 11
#define SHADE_HDR_VER 12
#define SHADE_HDR_SIZE 24
#define SHADE_REC_SIZE 248
#define GROUP_REC_SIZE 176
#define SHADE_REC_SIZE 252
#define GROUP_REC_SIZE 180
bool ConfigFile::begin(const char* filename, bool readOnly) {
this->file = LittleFS.open(filename, readOnly ? "r" : "w");
@ -387,6 +387,7 @@ bool ShadeConfigFile::loadFile(SomfyShadeController *s, const char *filename) {
if(this->header.version >= 10) {
shade->flipPosition = this->readBool(false);
}
if(this->header.version >= 12) shade->repeats = this->readUInt8(1);
if(shade->getShadeId() == 255) shade->clear();
}
for(uint8_t i = 0; i < this->header.groupRecords; i++) {
@ -408,7 +409,9 @@ bool ShadeConfigFile::loadFile(SomfyShadeController *s, const char *filename) {
// Do this to eliminate gaps.
if(shadeId > 0) group->linkedShades[lsd++] = shadeId;
}
group->compressLinkedShadeIds();
if(this->header.version >= 12) group->repeats = this->readUInt8(1);
if(group->getGroupId() == 255) group->clear();
else group->compressLinkedShadeIds();
}
pref.end();
if(opened) {
@ -425,9 +428,9 @@ bool ShadeConfigFile::writeGroupRecord(SomfyGroup *group) {
this->writeUInt8(static_cast<uint8_t>(group->proto));
this->writeUInt8(group->bitLength);
for(uint8_t j = 0; j < SOMFY_MAX_GROUPED_SHADES; j++) {
if(j == SOMFY_MAX_GROUPED_SHADES - 1) this->writeUInt8(group->linkedShades[j], CFG_REC_END);
else this->writeUInt8(group->linkedShades[j]);
this->writeUInt8(group->linkedShades[j]);
}
this->writeUInt8(group->repeats, CFG_REC_END);
return true;
}
bool ShadeConfigFile::writeShadeRecord(SomfyShade *shade) {
@ -469,7 +472,8 @@ bool ShadeConfigFile::writeShadeRecord(SomfyShade *shade) {
this->writeFloat(0.0f, 5); // currentTiltPos
}
this->writeBool(shade->flipCommands);
this->writeBool(shade->flipPosition, CFG_REC_END);
this->writeBool(shade->flipPosition);
this->writeUInt8(shade->repeats, CFG_REC_END);
return true;
}
bool ShadeConfigFile::exists() { return LittleFS.exists("/shades.cfg"); }

View file

@ -3,7 +3,7 @@
#ifndef configsettings_h
#define configsettings_h
#define FW_VERSION "v2.1.1"
#define FW_VERSION "v2.1.2"
enum DeviceStatus {
DS_OK = 0,
DS_ERROR = 1,

View file

@ -622,10 +622,13 @@ void SomfyShade::clear() {
this->downTime = 10000;
this->tiltTime = 7000;
this->stepSize = 100;
this->repeats = 1;
}
void SomfyGroup::clear() {
this->setGroupId(255);
this->setRemoteAddress(0);
this->repeats = 1;
memset(&this->linkedShades[0], 0x00, sizeof(this->linkedShades));
}
bool SomfyShade::linkRemote(uint32_t address, uint16_t rollingCode) {
// Check to see if the remote is already linked. If it is
@ -914,13 +917,13 @@ void SomfyShade::checkMovement() {
// not moving otherwise the my function will kick in.
if(this->settingPos) {
if(!isAtTarget()) {
if(this->target != 100.0) SomfyRemote::sendCommand(somfy_commands::My);
if(this->target != 100.0) SomfyRemote::sendCommand(somfy_commands::My, this->repeats);
delay(100);
// We now need to move the tilt to the position we requested.
this->moveToTiltTarget(this->tiltTarget);
}
else
if(this->target != 100.0) SomfyRemote::sendCommand(somfy_commands::My);
if(this->target != 100.0) SomfyRemote::sendCommand(somfy_commands::My, this->repeats);
}
this->direction = 0;
this->tiltStart = curTime;
@ -959,13 +962,13 @@ void SomfyShade::checkMovement() {
// not moving otherwise the my function will kick in.
if(this->settingPos) {
if(!isAtTarget()) {
if(this->target != 0.0) SomfyRemote::sendCommand(somfy_commands::My);
if(this->target != 0.0) SomfyRemote::sendCommand(somfy_commands::My, this->repeats);
delay(100);
// We now need to move the tilt to the position we requested.
this->moveToTiltTarget(this->tiltTarget);
}
else
if(this->target != 0.0) SomfyRemote::sendCommand(somfy_commands::My);
if(this->target != 0.0) SomfyRemote::sendCommand(somfy_commands::My, this->repeats);
}
this->direction = 0;
this->tiltStart = curTime;
@ -1004,11 +1007,11 @@ void SomfyShade::checkMovement() {
if(this->settingTiltPos) {
if(this->tiltType == tilt_types::integrated) {
// If this is an integrated tilt mechanism the we will simply let it finish. If it is not then we will stop it.
if(this->tiltTarget != 100.0 || this->currentPos != 100.0) SomfyRemote::sendCommand(somfy_commands::My);
if(this->tiltTarget != 100.0 || this->currentPos != 100.0) SomfyRemote::sendCommand(somfy_commands::My, this->repeats);
}
else {
// This is a tilt motor so let it complete if it is going to 0.
if(this->tiltTarget != 100.0) SomfyRemote::sendCommand(somfy_commands::My);
if(this->tiltTarget != 100.0) SomfyRemote::sendCommand(somfy_commands::My, this->repeats);
}
}
this->tiltDirection = 0;
@ -1052,11 +1055,11 @@ void SomfyShade::checkMovement() {
if(this->settingTiltPos) {
if(this->tiltType == tilt_types::integrated) {
// If this is an integrated tilt mechanism the we will simply let it finish. If it is not then we will stop it.
if(this->tiltTarget != 0.0 || this->currentPos != 0.0) SomfyRemote::sendCommand(somfy_commands::My);
if(this->tiltTarget != 0.0 || this->currentPos != 0.0) SomfyRemote::sendCommand(somfy_commands::My, this->repeats);
}
else {
// This is a tilt motor so let it complete if it is going to 0.
if(this->tiltTarget != 0.0) SomfyRemote::sendCommand(somfy_commands::My);
if(this->tiltTarget != 0.0) SomfyRemote::sendCommand(somfy_commands::My, this->repeats);
}
}
this->tiltDirection = 0;
@ -1824,7 +1827,7 @@ void SomfyShade::setMyPosition(int8_t pos, int8_t tilt) {
this->moveToMyPosition();
}
else {
SomfyRemote::sendCommand(somfy_commands::My, 1);
SomfyRemote::sendCommand(somfy_commands::My, this->repeats);
this->settingPos = false;
this->settingMyPos = true;
}
@ -1854,7 +1857,7 @@ void SomfyShade::setMyPosition(int8_t pos, int8_t tilt) {
this->moveToMyPosition();
}
else {
SomfyRemote::sendCommand(somfy_commands::My, 1);
SomfyRemote::sendCommand(somfy_commands::My, this->repeats);
this->settingPos = false;
this->settingMyPos = true;
}
@ -1892,8 +1895,9 @@ void SomfyShade::moveToMyPosition() {
if(this->myPos >= 0.0f && this->myPos <= 100.0f) this->target = this->myPos;
if(this->myTiltPos >= 0.0f && this->myTiltPos <= 100.0f) this->tiltTarget = this->myTiltPos;
this->settingPos = false;
SomfyRemote::sendCommand(somfy_commands::My);
SomfyRemote::sendCommand(somfy_commands::My, this->repeats);
}
void SomfyShade::sendCommand(somfy_commands cmd) { this->sendCommand(cmd, this->repeats); }
void SomfyShade::sendCommand(somfy_commands cmd, uint8_t repeat) {
// This sendCommand function will always be called externally. sendCommand at the remote level
// is expected to be called internally when the motor needs commanded.
@ -1923,6 +1927,7 @@ void SomfyShade::sendCommand(somfy_commands cmd, uint8_t repeat) {
SomfyRemote::sendCommand(cmd, repeat);
}
}
void SomfyGroup::sendCommand(somfy_commands cmd) { this->sendCommand(cmd, this->repeats); }
void SomfyGroup::sendCommand(somfy_commands cmd, uint8_t repeat) {
// This sendCommand function will always be called externally. sendCommand at the remote level
// is expected to be called internally when the motor needs commanded.
@ -1954,15 +1959,15 @@ void SomfyGroup::sendCommand(somfy_commands cmd, uint8_t repeat) {
}
void SomfyShade::sendTiltCommand(somfy_commands cmd) {
if(cmd == somfy_commands::Up) {
SomfyRemote::sendCommand(cmd, this->tiltType == tilt_types::tiltmotor ? TILT_REPEATS : 1);
SomfyRemote::sendCommand(cmd, this->tiltType == tilt_types::tiltmotor ? TILT_REPEATS : this->repeats);
this->tiltTarget = 0.0f;
}
else if(cmd == somfy_commands::Down) {
SomfyRemote::sendCommand(cmd, this->tiltType == tilt_types::tiltmotor ? TILT_REPEATS : 1);
SomfyRemote::sendCommand(cmd, this->tiltType == tilt_types::tiltmotor ? TILT_REPEATS : this->repeats);
this->tiltTarget = 100.0f;
}
else if(cmd == somfy_commands::My) {
SomfyRemote::sendCommand(cmd, this->tiltType == tilt_types::tiltmotor ? TILT_REPEATS : 1);
SomfyRemote::sendCommand(cmd, this->tiltType == tilt_types::tiltmotor ? TILT_REPEATS : this->repeats);
this->tiltTarget = this->currentTiltPos;
}
}
@ -1981,10 +1986,10 @@ void SomfyShade::moveToTiltTarget(float target) {
Serial.print(this->currentTiltPos);
Serial.print("% using ");
Serial.println(translateSomfyCommand(cmd));
SomfyRemote::sendCommand(cmd, this->tiltType == tilt_types::tiltmotor ? TILT_REPEATS : 1);
SomfyRemote::sendCommand(cmd, this->tiltType == tilt_types::tiltmotor ? TILT_REPEATS : this->repeats);
}
else
SomfyRemote::sendCommand(cmd);
SomfyRemote::sendCommand(cmd, this->repeats);
}
this->tiltTarget = target;
}
@ -2013,7 +2018,7 @@ void SomfyShade::moveToTarget(float pos, float tilt) {
}
Serial.print("% using ");
Serial.println(translateSomfyCommand(cmd));
SomfyRemote::sendCommand(cmd);
SomfyRemote::sendCommand(cmd, this->repeats);
this->settingPos = true;
this->target = pos;
if(tilt >= 0) {
@ -2084,6 +2089,7 @@ bool SomfyShade::fromJSON(JsonObject &obj) {
}
if(obj.containsKey("flipCommands")) this->flipCommands = obj["flipCommands"].as<bool>();
if(obj.containsKey("flipPosition")) this->flipPosition = obj["flipPosition"].as<bool>();
if(obj.containsKey("repeats")) this->repeats = obj["repeats"];
if(obj.containsKey("tiltType")) {
if(obj["tiltType"].is<const char *>()) {
if(strncmp(obj["tiltType"].as<const char *>(), "none", 4) == 0)
@ -2124,6 +2130,7 @@ bool SomfyShade::toJSONRef(JsonObject &obj) {
obj["proto"] = static_cast<uint8_t>(this->proto);
obj["flags"] = this->flags;
obj["sunSensor"] = this->hasSunSensor();
obj["repeats"] = this->repeats;
SomfyRemote::toJSON(obj);
return true;
}
@ -2160,6 +2167,7 @@ bool SomfyShade::toJSON(JsonObject &obj) {
obj["flipPosition"] = this->flipPosition;
obj["inGroup"] = this->isInGroup();
obj["sunSensor"] = this->hasSunSensor();
obj["repeats"] = this->repeats;
SomfyRemote::toJSON(obj);
JsonArray arr = obj.createNestedArray("linkedRemotes");
@ -2177,7 +2185,8 @@ bool SomfyGroup::fromJSON(JsonObject &obj) {
if(obj.containsKey("remoteAddress")) this->setRemoteAddress(obj["remoteAddress"]);
if(obj.containsKey("bitLength")) this->bitLength = obj["bitLength"];
if(obj.containsKey("proto")) this->proto = static_cast<radio_proto>(obj["proto"].as<uint8_t>());
if(obj.containsKey("sunSensor")) obj["sunSensor"] = this->hasSunSensor();
//if(obj.containsKey("sunSensor")) this->hasSunSensor() = obj["sunSensor"]; This is calculated
if(obj.containsKey("repeats")) this->repeats = obj["repeats"];
if(obj.containsKey("linkedShades")) {
uint8_t linkedShades[SOMFY_MAX_GROUPED_SHADES];
memset(linkedShades, 0x00, sizeof(linkedShades));
@ -2199,6 +2208,7 @@ bool SomfyGroup::toJSON(JsonObject &obj) {
obj["proto"] = static_cast<uint8_t>(this->proto);
obj["sunSensor"] = this->hasSunSensor();
obj["flags"] = this->flags;
obj["repeats"] = this->repeats;
SomfyRemote::toJSON(obj);
JsonArray arr = obj.createNestedArray("linkedShades");
for(uint8_t i = 0; i < SOMFY_MAX_GROUPED_SHADES; i++) {
@ -2440,6 +2450,7 @@ somfy_commands SomfyRemote::transformCommand(somfy_commands cmd) {
}
return cmd;
}
void SomfyRemote::sendCommand(somfy_commands cmd) { this->sendCommand(cmd, this->repeats); }
void SomfyRemote::sendCommand(somfy_commands cmd, uint8_t repeat) {
somfy_frame_t frame;
frame.rollingCode = this->getNextRollingCode();

10
Somfy.h
View file

@ -178,6 +178,7 @@ class SomfyRemote {
bool flipCommands = false;
uint8_t flags = 0;
uint8_t bitLength = 0;
uint8_t repeats = 1;
char *getRemotePrefId() {return m_remotePrefId;}
virtual bool toJSON(JsonObject &obj);
virtual void setRemoteAddress(uint32_t address);
@ -187,7 +188,8 @@ class SomfyRemote {
uint16_t lastRollingCode = 0;
bool hasSunSensor();
void setSunSensor(bool bHasSensor);
virtual void sendCommand(somfy_commands cmd, uint8_t repeat = 1);
virtual void sendCommand(somfy_commands cmd);
virtual void sendCommand(somfy_commands cmd, uint8_t repeat);
somfy_commands transformCommand(somfy_commands cmd);
};
class SomfyLinkedRemote : public SomfyRemote {
@ -255,7 +257,8 @@ class SomfyShade : public SomfyRemote {
void moveToTarget(float pos, float tilt = -1.0f);
void moveToTiltTarget(float target);
void sendTiltCommand(somfy_commands cmd);
void sendCommand(somfy_commands cmd, uint8_t repeat = 1);
void sendCommand(somfy_commands cmd);
void sendCommand(somfy_commands cmd, uint8_t repeat);
bool linkRemote(uint32_t remoteAddress, uint16_t rollingCode = 0);
bool unlinkRemote(uint32_t remoteAddress);
void emitState(const char *evt = "shadeState");
@ -295,7 +298,8 @@ class SomfyGroup : public SomfyRemote {
void updateFlags();
void emitState(const char *evt = "groupState");
void emitState(uint8_t num, const char *evt = "groupState");
void sendCommand(somfy_commands cmd, uint8_t repeat = 1);
void sendCommand(somfy_commands cmd);
void sendCommand(somfy_commands cmd, uint8_t repeat);
};
struct transceiver_config_t {
bool printBuffer = false;

Binary file not shown.

Binary file not shown.

22
Web.cpp
View file

@ -323,7 +323,7 @@ void Web::begin() {
HTTPMethod method = apiServer.method();
uint8_t shadeId = 255;
uint8_t target = 255;
uint8_t repeat = 1;
int8_t repeat = -1;
somfy_commands command = somfy_commands::My;
if (method == HTTP_GET || method == HTTP_PUT || method == HTTP_POST) {
if (apiServer.hasArg("shadeId")) {
@ -373,8 +373,10 @@ void Web::begin() {
// Send the command to the shade.
if(target <= 100)
shade->moveToTarget(shade->transformPosition(target));
else
else if(repeat > 0)
shade->sendCommand(command, repeat);
else
shade->sendCommand(command);
DynamicJsonDocument sdoc(512);
JsonObject sobj = sdoc.to<JsonObject>();
shade->toJSON(sobj);
@ -390,7 +392,7 @@ void Web::begin() {
if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; }
HTTPMethod method = apiServer.method();
uint8_t groupId = 255;
uint8_t repeat = 1;
int8_t repeat = -1;
somfy_commands command = somfy_commands::My;
if (method == HTTP_GET || method == HTTP_PUT || method == HTTP_POST) {
if (apiServer.hasArg("groupId")) {
@ -434,7 +436,8 @@ void Web::begin() {
Serial.print("Received:");
Serial.println(apiServer.arg("plain"));
// Send the command to the group.
group->sendCommand(command, repeat);
if(repeat != -1) group->sendCommand(command, repeat);
else group->sendCommand(command);
DynamicJsonDocument sdoc(512);
JsonObject sobj = sdoc.to<JsonObject>();
group->toJSON(sobj);
@ -1068,7 +1071,7 @@ void Web::begin() {
HTTPMethod method = server.method();
uint8_t shadeId = 255;
uint8_t target = 255;
uint8_t repeat = 1;
int8_t repeat = -1;
somfy_commands command = somfy_commands::My;
if (method == HTTP_GET || method == HTTP_PUT || method == HTTP_POST) {
if (server.hasArg("shadeId")) {
@ -1118,8 +1121,10 @@ void Web::begin() {
// Send the command to the shade.
if(target <= 100)
shade->moveToTarget(shade->transformPosition(target));
else
else if(repeat > 0)
shade->sendCommand(command, repeat);
else
shade->sendCommand(command);
DynamicJsonDocument sdoc(512);
JsonObject sobj = sdoc.to<JsonObject>();
shade->toJSON(sobj);
@ -1135,7 +1140,7 @@ void Web::begin() {
if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; }
HTTPMethod method = server.method();
uint8_t groupId = 255;
uint8_t repeat = 1;
int8_t repeat = -1;
somfy_commands command = somfy_commands::My;
if (method == HTTP_GET || method == HTTP_PUT || method == HTTP_POST) {
if (server.hasArg("groupId")) {
@ -1179,7 +1184,8 @@ void Web::begin() {
Serial.print("Received:");
Serial.println(server.arg("plain"));
// Send the command to the group.
group->sendCommand(command, repeat);
if(repeat > 0) group->sendCommand(command, repeat);
else group->sendCommand(command);
DynamicJsonDocument sdoc(512);
JsonObject sobj = sdoc.to<JsonObject>();
group->toJSON(sobj);

View file

@ -1 +1 @@
2.1.1
2.1.2

View file

@ -3,11 +3,11 @@
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8">
<link rel="stylesheet" href="main.css?v=2.1.1" type="text/css" />
<link rel="stylesheet" href="widgets.css?v=2.1.1" type="text/css" />
<link rel="stylesheet" href="icons.css?v=2.1.1" type="text/css" />
<link rel="stylesheet" href="main.css?v=2.1.2" type="text/css" />
<link rel="stylesheet" href="widgets.css?v=2.1.2" type="text/css" />
<link rel="stylesheet" href="icons.css?v=2.1.2" type="text/css" />
<link rel="icon" type="image/png" href="favicon.png" />
<script type="text/javascript" src="index.js?v=2.1.1"></script>
<script type="text/javascript" src="index.js?v=2.1.2"></script>
</head>
<body>
<div id="divContainer" class="container main" data-auth="false">
@ -375,7 +375,7 @@
</div>
</div>
<div style="margin-top:-10px;">
<div class="field-group">
<div class="field-group" style="display:inline-block">
<input id="cbFlipCommands" name="flipCommands" data-bind="flipCommands" type="checkbox" style="" />
<label for="cbFlipCommands" style="display:block;font-size:1em;margin-top:0px;margin-left:7px;display:inline-block;">Invert Commands</label>
</div>
@ -386,6 +386,24 @@
<label for="cbFlipPosition" style="display:block;font-size:1em;margin-top:0px;margin-left:7px;display:inline-block;">Invert Position (expressed in % of open)</label>
</div>
</div>
<div classs="field-group" style="display:inline-block;">
<label for="selRepeatCommnds" style="cursor:pointer;color:#00bcd4;margin-right:4px;">Repeat Commands</label>
<select id="selRepeatCommands" data-bind="repeats" data-datatype="int" style="width:127px;">
<option value="0">No Repeats</option>
<option value="1">1 Time</option>
<option value="2">2 Times</option>
<option value="3">3 Times</option>
<option value="4">4 Times</option>
<option value="5">5 Times</option>
<option value="6">6 Times</option>
<option value="7">7 Times</option>
</select>
</div>
<div style="margin-top:-10px;">
<div class="field-group">
</div>
</div>
<div class="button-container" style="text-align:center;">
<button id="btnPairShade" type="button" onclick="somfy.pairShade(parseInt(document.getElementById('spanShadeId').innerText, 10));" style="display:inline-block;width:47%;">
Pair Shade
@ -459,6 +477,20 @@
<input id="fldGroupName" name="groupName" data-bind="name" type="text" length=20 placeholder="Name">
<label for="fldGroupName">Name</label>
</div>
<div classs="field-group" style="display:inline-block;">
<label for="selRepeatCommnds" style="cursor:pointer;color:#00bcd4;margin-right:4px;">Repeat Commands</label>
<select id="selRepeatCommands" data-bind="repeats" data-datatype="int" style="width:127px;">
<option value="0">No Repeats</option>
<option value="1">1 Time</option>
<option value="2">2 Times</option>
<option value="3">3 Times</option>
<option value="4">4 Times</option>
<option value="5">5 Times</option>
<option value="6">6 Times</option>
<option value="7">7 Times</option>
</select>
</div>
<div class="button-container" style="margin-top:-10px;padding-left:7px;padding-right:7px;">
<button id="btnSaveGroup" type="button" onclick="somfy.saveGroup();">
Save Group

View file

@ -1197,7 +1197,7 @@ var security = new Security();
class General {
initialized = false;
appVersion = 'v2.1.1';
appVersion = 'v2.1.2';
reloadApp = false;
init() {
if (this.initialized) return;
@ -2073,6 +2073,7 @@ class Somfy {
setGroupsList(groups) {
let divCfg = '';
let divCtl = '';
if (typeof groups !== 'undefined') {
for (let i = 0; i < groups.length; i++) {
let group = groups[i];
divCfg += `<div class="somfyGroup" data-groupid="${group.groupId}" data-remoteaddress="${group.remoteAddress}">`;
@ -2087,12 +2088,14 @@ class Somfy {
divCtl += `<div class="group-name">`;
divCtl += `<span class="groupctl-name">${group.name}</span>`;
divCtl += `<div class="groupctl-shades">`;
if (typeof group.linkedShades !== 'undefined') {
for (let j = 0; j < group.linkedShades.length; j++) {
divCtl += '<span>';
if (j !== 0) divCtl += ', ';
divCtl += group.linkedShades[j].name;
divCtl += '</span>';
}
}
divCtl += '</div></div>';
divCtl += `<div class="groupctl-buttons">`;
divCtl += `<div class="button-sunflag cmd-button" data-cmd="sunflag" data-groupid="${group.groupId}" data-on="${group.flags & 0x01 ? 'true' : 'false'}" style="${!group.sunSensor ? 'display:none' : ''}"><i class="icss-sun-c"></i><i class="icss-sun-o"></i></div>`;
@ -2101,6 +2104,7 @@ class Somfy {
divCtl += `<div class="button-outline cmd-button" data-cmd="down" data-groupid="${group.groupId}"><i class="icss-somfy-down" style="margin-top:-4px;"></i></div>`;
divCtl += '</div></div>';
}
}
document.getElementById('divGroupList').innerHTML = divCfg;
let groupControls = document.getElementById('divGroupControls');
groupControls.innerHTML = divCtl;
@ -3305,7 +3309,7 @@ class Firmware {
}
backup() {
var link = document.createElement('a');
link.href = '/backup';
link.href = baseUrl.length > 0 ? `${baseUrl}/backup` : '/backup';
link.setAttribute('download', 'backup');
document.body.appendChild(link);
link.click();
@ -3431,8 +3435,10 @@ class Firmware {
prog.style.display = '';
btnSelectFile.style.visibility = 'hidden';
formData.append('file', field.files[0]);
let xhr = new XMLHttpRequest();
xhr.open('POST', service, true);
//xhr.open('POST', service, true);
xhr.open('POST', baseUrl.length > 0 ? `${baseUrl}${service}` : service, true);
xhr.upload.onprogress = function (evt) {
let pct = evt.total ? Math.round((evt.loaded / evt.total) * 100) : 0;
prog.style.setProperty('--progress', `${pct}%`);