mirror of
https://github.com/rstrouse/ESPSomfy-RTS.git
synced 2025-12-13 02:52:11 +01:00
518 lines
18 KiB
C++
518 lines
18 KiB
C++
#include <Arduino.h>
|
|
#include <LittleFS.h>
|
|
#include <Preferences.h>
|
|
#include "ConfigFile.h"
|
|
#include "Utils.h"
|
|
|
|
extern Preferences pref;
|
|
|
|
#define SHADE_HDR_VER 12
|
|
#define SHADE_HDR_SIZE 24
|
|
#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");
|
|
this->_opened = true;
|
|
return true;
|
|
}
|
|
void ConfigFile::end() {
|
|
if(this->isOpen()) {
|
|
if(!this->readOnly) this->file.flush();
|
|
this->file.close();
|
|
}
|
|
this->_opened = false;
|
|
}
|
|
bool ConfigFile::isOpen() { return this->_opened; }
|
|
bool ConfigFile::seekChar(const char val) {
|
|
if(!this->isOpen()) return false;
|
|
char ch;
|
|
do {
|
|
ch = this->readChar('\0');
|
|
if(ch == '\0') return false;
|
|
} while(ch != val);
|
|
return true;
|
|
}
|
|
bool ConfigFile::writeSeparator() {return this->writeChar(CFG_VALUE_SEP); }
|
|
bool ConfigFile::writeRecordEnd() { return this->writeChar(CFG_REC_END); }
|
|
bool ConfigFile::writeHeader() { return this->writeHeader(this->header); }
|
|
bool ConfigFile::writeHeader(const config_header_t &hdr) {
|
|
if(!this->isOpen()) return false;
|
|
this->writeUInt8(hdr.version);
|
|
this->writeUInt8(hdr.length);
|
|
this->writeUInt8(hdr.shadeRecordSize);
|
|
this->writeUInt8(hdr.shadeRecords);
|
|
this->writeUInt8(hdr.groupRecordSize);
|
|
this->writeUInt8(hdr.groupRecords, CFG_REC_END);
|
|
return true;
|
|
}
|
|
bool ConfigFile::readHeader() {
|
|
if(!this->isOpen()) return false;
|
|
//if(this->file.position() != 0) this->file.seek(0, SeekSet);
|
|
Serial.printf("Reading header at %u\n", this->file.position());
|
|
this->header.version = this->readUInt8(this->header.version);
|
|
this->header.length = this->readUInt8(0);
|
|
this->header.shadeRecordSize = this->readUInt8(this->header.shadeRecordSize);
|
|
this->header.shadeRecords = this->readUInt8(this->header.shadeRecords);
|
|
if(this->header.version > 10) {
|
|
this->header.groupRecordSize = this->readUInt8(this->header.groupRecordSize);
|
|
this->header.groupRecords = this->readUInt8(this->header.groupRecords);
|
|
}
|
|
Serial.printf("version:%u len:%u shadeSize:%u shadeRecs:%u groupSize:%u groupRecs: %u pos:%d\n", this->header.version, this->header.length, this->header.shadeRecordSize, this->header.shadeRecords, this->header.groupRecordSize, this->header.groupRecords, this->file.position());
|
|
return true;
|
|
}
|
|
/*
|
|
bool ConfigFile::seekRecordByIndex(uint16_t ndx) {
|
|
if(!this->file) {
|
|
return false;
|
|
}
|
|
if(((this->header.recordSize * ndx) + this->header.length) > this->file.size()) return false;
|
|
return true;
|
|
}
|
|
*/
|
|
bool ConfigFile::readString(char *buff, size_t len) {
|
|
if(!this->file) return false;
|
|
memset(buff, 0x00, len);
|
|
uint16_t i = 0;
|
|
while(i < len) {
|
|
uint8_t val;
|
|
if(this->file.read(&val, 1) == 1) {
|
|
switch(val) {
|
|
case CFG_REC_END:
|
|
case CFG_VALUE_SEP:
|
|
_rtrim(buff);
|
|
return true;
|
|
}
|
|
buff[i++] = val;
|
|
if(i == len) {
|
|
_rtrim(buff);
|
|
return true;
|
|
}
|
|
}
|
|
else
|
|
return false;
|
|
}
|
|
_rtrim(buff);
|
|
return true;
|
|
}
|
|
bool ConfigFile::writeString(const char *val, size_t len, const char tok) {
|
|
if(!this->isOpen()) return false;
|
|
int slen = strlen(val);
|
|
if(slen > 0)
|
|
if(this->file.write((uint8_t *)val, slen) != slen) return false;
|
|
// Now we need to pad the end of the string so that it is of a fixed length.
|
|
while(slen < len - 1) {
|
|
this->file.write(' ');
|
|
slen++;
|
|
}
|
|
// 255 = len = 4 slen = 3
|
|
if(tok != CFG_TOK_NONE)
|
|
return this->writeChar(tok);
|
|
return true;
|
|
}
|
|
bool ConfigFile::writeChar(const char val) {
|
|
if(!this->isOpen()) return false;
|
|
if(this->file.write(static_cast<uint8_t>(val)) == 1) return true;
|
|
return false;
|
|
}
|
|
bool ConfigFile::writeUInt8(const uint8_t val, const char tok) {
|
|
char buff[4];
|
|
snprintf(buff, sizeof(buff), "%3u", val);
|
|
return this->writeString(buff, sizeof(buff), tok);
|
|
}
|
|
bool ConfigFile::writeUInt16(const uint16_t val, const char tok) {
|
|
char buff[6];
|
|
snprintf(buff, sizeof(buff), "%5u", val);
|
|
return this->writeString(buff, sizeof(buff), tok);
|
|
}
|
|
bool ConfigFile::writeUInt32(const uint32_t val, const char tok) {
|
|
char buff[11];
|
|
snprintf(buff, sizeof(buff), "%10u", val);
|
|
return this->writeString(buff, sizeof(buff), tok);
|
|
}
|
|
bool ConfigFile::writeFloat(const float val, const uint8_t prec, const char tok) {
|
|
char buff[20];
|
|
snprintf(buff, sizeof(buff), "%*.*f", 7 + prec, prec, val);
|
|
return this->writeString(buff, 8 + prec, tok);
|
|
}
|
|
bool ConfigFile::writeBool(const bool val, const char tok) {
|
|
return this->writeString(val ? "true" : "false", 6, tok);
|
|
}
|
|
char ConfigFile::readChar(const char defVal) {
|
|
uint8_t ch;
|
|
if(this->file.read(&ch, 1) == 1) return (char)ch;
|
|
return defVal;
|
|
}
|
|
uint8_t ConfigFile::readUInt8(const uint8_t defVal) {
|
|
char buff[4];
|
|
if(this->readString(buff, sizeof(buff)))
|
|
return static_cast<uint8_t>(atoi(buff));
|
|
return defVal;
|
|
}
|
|
uint16_t ConfigFile::readUInt16(const uint16_t defVal) {
|
|
char buff[6];
|
|
if(this->readString(buff, sizeof(buff)))
|
|
return static_cast<uint16_t>(atoi(buff));
|
|
return defVal;
|
|
}
|
|
uint32_t ConfigFile::readUInt32(const uint32_t defVal) {
|
|
char buff[11];
|
|
if(this->readString(buff, sizeof(buff)))
|
|
return static_cast<uint32_t>(atoi(buff));
|
|
return defVal;
|
|
}
|
|
float ConfigFile::readFloat(const float defVal) {
|
|
char buff[25];
|
|
if(this->readString(buff, sizeof(buff)))
|
|
return atof(buff);
|
|
return defVal;
|
|
}
|
|
bool ConfigFile::readBool(const bool defVal) {
|
|
char buff[6];
|
|
if(this->readString(buff, sizeof(buff))) {
|
|
switch(buff[0]) {
|
|
case 't':
|
|
case 'T':
|
|
case '1':
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
return defVal;
|
|
}
|
|
/*
|
|
bool ShadeConfigFile::seekRecordById(uint8_t id) {
|
|
if(this->isOpen()) return false;
|
|
this->file.seek(this->header.length, SeekSet); // Start at the beginning of the file after the header.
|
|
uint8_t i = 0;
|
|
while(i < SOMFY_MAX_SHADES) {
|
|
uint32_t pos = this->file.position();
|
|
uint8_t len = this->readUInt8(this->header.recordSize);
|
|
uint8_t cid = this->readUInt8(255);
|
|
if(cid == id) {
|
|
this->file.seek(pos, SeekSet);
|
|
return true;
|
|
}
|
|
pos += len;
|
|
this->file.seek(pos, SeekSet);
|
|
}
|
|
return false;
|
|
}
|
|
*/
|
|
bool ShadeConfigFile::begin(bool readOnly) { return this->begin("/shades.cfg", readOnly); }
|
|
bool ShadeConfigFile::begin(const char *filename, bool readOnly) { return ConfigFile::begin(filename, readOnly); }
|
|
void ShadeConfigFile::end() { ConfigFile::end(); }
|
|
bool ShadeConfigFile::save(SomfyShadeController *s) {
|
|
this->header.version = SHADE_HDR_VER;
|
|
this->header.shadeRecordSize = SHADE_REC_SIZE;
|
|
this->header.length = SHADE_HDR_SIZE;
|
|
this->header.shadeRecords = SOMFY_MAX_SHADES;
|
|
this->header.groupRecordSize = GROUP_REC_SIZE;
|
|
this->header.groupRecords = SOMFY_MAX_GROUPS;
|
|
this->writeHeader();
|
|
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
|
|
SomfyShade *shade = &s->shades[i];
|
|
this->writeShadeRecord(shade);
|
|
}
|
|
for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++) {
|
|
SomfyGroup *group = &s->groups[i];
|
|
this->writeGroupRecord(group);
|
|
}
|
|
return true;
|
|
}
|
|
bool ShadeConfigFile::validate() {
|
|
this->readHeader();
|
|
if(this->header.version < 1) {
|
|
Serial.print("Invalid Header Version:");
|
|
Serial.println(this->header.version);
|
|
return false;
|
|
}
|
|
if(this->header.shadeRecordSize < 100) {
|
|
Serial.print("Invalid Shade Record Size:");
|
|
Serial.println(this->header.shadeRecordSize);
|
|
return false;
|
|
}
|
|
if(this->header.shadeRecords != SOMFY_MAX_SHADES) {
|
|
Serial.print("Invalid Shade Record Count:");
|
|
Serial.println(this->header.shadeRecords);
|
|
return false;
|
|
}
|
|
if(this->header.version > 10) {
|
|
if(this->header.groupRecordSize < 100) {
|
|
Serial.print("Invalid Group Record Size:");
|
|
Serial.println(this->header.groupRecordSize);
|
|
return false;
|
|
}
|
|
if(this->header.groupRecords != SOMFY_MAX_GROUPS) {
|
|
Serial.print("Invalid Group Record Count:");
|
|
Serial.println(this->header.groupRecords);
|
|
return false;
|
|
|
|
}
|
|
}
|
|
if(this->file.position() != this->header.length) {
|
|
Serial.printf("File not positioned at %u end of header: %d\n", this->header.length, this->file.position());
|
|
return false;
|
|
}
|
|
|
|
// We should know the file size based upon the record information in the header
|
|
uint32_t fsize = this->header.length + (this->header.shadeRecordSize * this->header.shadeRecords);
|
|
if(this->header.version > 10) fsize += (this->header.groupRecordSize * this->header.groupRecords);
|
|
if(this->file.size() != fsize) {
|
|
Serial.printf("File size is not correct should be %d and got %d\n", fsize, this->file.size());
|
|
}
|
|
// Next check to see if the records match the header length.
|
|
uint8_t recs = 0;
|
|
uint32_t startPos = this->file.position();
|
|
while(recs < this->header.shadeRecords) {
|
|
uint32_t pos = this->file.position();
|
|
if(!this->seekChar(CFG_REC_END)) {
|
|
Serial.printf("Failed to find the shade record end %d\n", recs);
|
|
return false;
|
|
}
|
|
if(this->file.position() - pos != this->header.shadeRecordSize) {
|
|
Serial.printf("Shade record length is %d and should be %d\n", this->file.position() - pos, this->header.shadeRecordSize);
|
|
return false;
|
|
}
|
|
recs++;
|
|
}
|
|
if(this->header.version > 10) {
|
|
recs = 0;
|
|
while(recs < this->header.groupRecords) {
|
|
uint32_t pos = this->file.position();
|
|
if(!this->seekChar(CFG_REC_END)) {
|
|
Serial.printf("Failed to find the group record end %d\n", recs);
|
|
return false;
|
|
}
|
|
recs++;
|
|
if(this->file.position() - pos != this->header.groupRecordSize) {
|
|
Serial.printf("Group record length is %d and should be %d\n", this->file.position() - pos, this->header.groupRecordSize);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
this->file.seek(startPos, SeekSet);
|
|
return true;
|
|
}
|
|
bool ShadeConfigFile::load(SomfyShadeController *s, const char *filename) {
|
|
ShadeConfigFile file;
|
|
if(file.begin(filename, true)) {
|
|
bool success = file.loadFile(s, filename);
|
|
file.end();
|
|
return success;
|
|
}
|
|
return false;
|
|
}
|
|
bool ShadeConfigFile::loadFile(SomfyShadeController *s, const char *filename) {
|
|
bool opened = false;
|
|
if(!this->isOpen()) {
|
|
Serial.println("Opening shade config file");
|
|
this->begin(filename, true);
|
|
opened = true;
|
|
}
|
|
else {
|
|
//this->file.seek(0, SeekSet);
|
|
}
|
|
if(!this->validate()) {
|
|
Serial.println("Shade config file invalid!");
|
|
if(opened) this->end();
|
|
return false;
|
|
}
|
|
// We should be valid so start reading.
|
|
pref.begin("ShadeCodes");
|
|
for(uint8_t i = 0; i < this->header.shadeRecords; i++) {
|
|
SomfyShade *shade = &s->shades[i];
|
|
shade->setShadeId(this->readUInt8(255));
|
|
shade->paired = this->readBool(false);
|
|
shade->shadeType = static_cast<shade_types>(this->readUInt8(0));
|
|
shade->setRemoteAddress(this->readUInt32(0));
|
|
this->readString(shade->name, sizeof(shade->name));
|
|
if(this->header.version < 3)
|
|
shade->tiltType = this->readBool(false) ? tilt_types::none : tilt_types::tiltmotor;
|
|
else
|
|
shade->tiltType = static_cast<tilt_types>(this->readUInt8(0));
|
|
if(this->header.version > 6) {
|
|
shade->proto = static_cast<radio_proto>(this->readUInt8(0));
|
|
}
|
|
if(this->header.version > 1) {
|
|
shade->bitLength = this->readUInt8(56);
|
|
}
|
|
shade->upTime = this->readUInt32(shade->upTime);
|
|
shade->downTime = this->readUInt32(shade->downTime);
|
|
shade->tiltTime = this->readUInt32(shade->tiltTime);
|
|
if(this->header.version > 5) {
|
|
shade->stepSize = this->readUInt16(100);
|
|
}
|
|
for(uint8_t j = 0; j < SOMFY_MAX_LINKED_REMOTES; j++) {
|
|
SomfyLinkedRemote *rem = &shade->linkedRemotes[j];
|
|
rem->setRemoteAddress(this->readUInt32(0));
|
|
if(rem->getRemoteAddress() != 0) rem->lastRollingCode = pref.getUShort(rem->getRemotePrefId(), 0);
|
|
if(this->header.version < 5 && j == 4) break; // Prior to version 5 we only supported 5 linked remotes.
|
|
}
|
|
shade->lastRollingCode = this->readUInt16(0);
|
|
if(this->header.version > 7) shade->flags = this->readUInt8(0);
|
|
if(shade->getRemoteAddress() != 0) {
|
|
// If the last rolling code stored on the nvs is less than the rc we currently have
|
|
// then we need to set it.
|
|
uint16_t rc = pref.getUShort(shade->getRemotePrefId(), 0);
|
|
shade->lastRollingCode = max(rc, shade->lastRollingCode);
|
|
if(rc < shade->lastRollingCode) pref.putUShort(shade->getRemotePrefId(), shade->lastRollingCode);
|
|
}
|
|
if(this->header.version < 4)
|
|
shade->myPos = static_cast<float>(this->readUInt8(255));
|
|
else {
|
|
shade->myPos = this->readFloat(-1);
|
|
shade->myTiltPos = this->readFloat(-1);
|
|
}
|
|
if(shade->myPos > 100 || shade->myPos < 0) shade->myPos = -1;
|
|
if(shade->myTiltPos > 100 || shade->myTiltPos < 0) shade->myTiltPos = -1;
|
|
shade->currentPos = this->readFloat(0);
|
|
shade->currentTiltPos = this->readFloat(0);
|
|
if(shade->tiltType == tilt_types::none || shade->shadeType != shade_types::blind) {
|
|
shade->myTiltPos = -1;
|
|
shade->currentTiltPos = 0;
|
|
shade->tiltType = tilt_types::none;
|
|
}
|
|
|
|
if(this->header.version < 3) {
|
|
shade->currentPos = shade->currentPos * 100;
|
|
shade->currentTiltPos = shade->currentTiltPos * 100;
|
|
}
|
|
shade->target = floor(shade->currentPos);
|
|
shade->tiltTarget = floor(shade->currentTiltPos);
|
|
if(this->header.version >= 9) {
|
|
shade->flipCommands = this->readBool(false);
|
|
}
|
|
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++) {
|
|
SomfyGroup *group = &s->groups[i];
|
|
group->setGroupId(this->readUInt8(255));
|
|
group->groupType = static_cast<group_types>(this->readUInt8(0));
|
|
group->setRemoteAddress(this->readUInt32(0));
|
|
this->readString(group->name, sizeof(group->name));
|
|
group->proto = static_cast<radio_proto>(this->readUInt8(0));
|
|
group->bitLength = this->readUInt8(56);
|
|
if(group->getRemoteAddress() != 0) {
|
|
uint16_t rc = pref.getUShort(group->getRemotePrefId(), 0);
|
|
group->lastRollingCode = max(rc, group->lastRollingCode);
|
|
if(rc < group->lastRollingCode) pref.putUShort(group->getRemotePrefId(), group->lastRollingCode);
|
|
}
|
|
uint8_t lsd = 0;
|
|
for(uint8_t j = 0; j < SOMFY_MAX_GROUPED_SHADES; j++) {
|
|
uint8_t shadeId = this->readUInt8(0);
|
|
// Do this to eliminate gaps.
|
|
if(shadeId > 0) group->linkedShades[lsd++] = shadeId;
|
|
}
|
|
if(this->header.version >= 12) group->repeats = this->readUInt8(1);
|
|
if(group->getGroupId() == 255) group->clear();
|
|
else group->compressLinkedShadeIds();
|
|
}
|
|
pref.end();
|
|
if(opened) {
|
|
Serial.println("Closing shade config file");
|
|
this->end();
|
|
}
|
|
return true;
|
|
}
|
|
bool ShadeConfigFile::writeGroupRecord(SomfyGroup *group) {
|
|
this->writeUInt8(group->getGroupId());
|
|
this->writeUInt8(static_cast<uint8_t>(group->groupType));
|
|
this->writeUInt32(group->getRemoteAddress());
|
|
this->writeString(group->name, sizeof(group->name));
|
|
this->writeUInt8(static_cast<uint8_t>(group->proto));
|
|
this->writeUInt8(group->bitLength);
|
|
for(uint8_t j = 0; j < SOMFY_MAX_GROUPED_SHADES; j++) {
|
|
this->writeUInt8(group->linkedShades[j]);
|
|
}
|
|
this->writeUInt8(group->repeats, CFG_REC_END);
|
|
return true;
|
|
}
|
|
bool ShadeConfigFile::writeShadeRecord(SomfyShade *shade) {
|
|
if(shade->tiltType == tilt_types::none || shade->shadeType != shade_types::blind) {
|
|
shade->myTiltPos = -1;
|
|
shade->currentTiltPos = 0;
|
|
shade->tiltType = tilt_types::none;
|
|
}
|
|
this->writeUInt8(shade->getShadeId());
|
|
this->writeBool(shade->paired);
|
|
this->writeUInt8(static_cast<uint8_t>(shade->shadeType));
|
|
this->writeUInt32(shade->getRemoteAddress());
|
|
this->writeString(shade->name, sizeof(shade->name));
|
|
this->writeUInt8(static_cast<uint8_t>(shade->tiltType));
|
|
this->writeUInt8(static_cast<uint8_t>(shade->proto));
|
|
this->writeUInt8(shade->bitLength);
|
|
this->writeUInt32(shade->upTime);
|
|
this->writeUInt32(shade->downTime);
|
|
this->writeUInt32(shade->tiltTime);
|
|
this->writeUInt16(shade->stepSize);
|
|
for(uint8_t j = 0; j < SOMFY_MAX_LINKED_REMOTES; j++) {
|
|
SomfyLinkedRemote *rem = &shade->linkedRemotes[j];
|
|
this->writeUInt32(rem->getRemoteAddress());
|
|
}
|
|
this->writeUInt16(shade->lastRollingCode);
|
|
if(shade->getShadeId() != 255) {
|
|
this->writeUInt8(shade->flags & 0x0F);
|
|
this->writeFloat(shade->myPos, 5);
|
|
this->writeFloat(shade->myTiltPos, 5);
|
|
this->writeFloat(shade->currentPos, 5);
|
|
this->writeFloat(shade->currentTiltPos, 5);
|
|
}
|
|
else {
|
|
// Make sure that we write cleared values when the shade is deleted.
|
|
this->writeUInt8(0);
|
|
this->writeFloat(-1.0f, 5); // MyPos
|
|
this->writeFloat(-1.0f, 5); // MyTiltPos
|
|
this->writeFloat(0.0f, 5); // currentPos
|
|
this->writeFloat(0.0f, 5); // currentTiltPos
|
|
}
|
|
this->writeBool(shade->flipCommands);
|
|
this->writeBool(shade->flipPosition);
|
|
this->writeUInt8(shade->repeats, CFG_REC_END);
|
|
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;
|
|
}
|