From 8c59cf490390f193f476c3b38e438da04dabbd69 Mon Sep 17 00:00:00 2001 From: cjkas Date: Thu, 29 Oct 2026 16:59:52 +0100 Subject: [PATCH] test: read and write shades.cfg and backups on the PC The records move to ConfigRecords, free of the file system and the shade model, with 33 unit tests: each record's size against the header, a round trip of each, the layouts of versions 2 to 24, and a real backup when CONFIG_BACKUP names one. The tests found, and this fixes: - A quoted value as long as its field (a hostname of 30 or more characters, a full NTP server, MQTT host or time zone) was read short, and every value after it shifted. - A comma in a shade, group or room name shifted the rest of its record. Names are read at their fixed width. - 4.0 boards without Ethernet declared the network record 28 bytes longer than they wrote it, so a restore of the radio without the network read the radio from the middle of its record. Records are read and skipped to their newline, and backup() measures what it writes. - A restore cleared rooms beyond the backup's only on load. A restore now takes the backup's rolling codes even when they are lower than NVS's; a load still keeps the higher. Co-Authored-By: Claude Opus 5.5 --- platformio.ini | 2 +- src/ConfigFile.cpp | 1186 +++++------------ src/ConfigFile.h | 60 +- src/ConfigRecords.cpp | 495 +++++++ src/ConfigRecords.h | 222 +++ src/ConfigSettings.cpp | 30 - src/ConfigSettings.h | 2 - .../test_configrecords/test_configrecords.cpp | 799 +++++++++++ 8 files changed, 1881 insertions(+), 915 deletions(-) create mode 100644 src/ConfigRecords.cpp create mode 100644 src/ConfigRecords.h create mode 100644 test/test_configrecords/test_configrecords.cpp diff --git a/platformio.ini b/platformio.ini index e7e5b60..b575be3 100644 --- a/platformio.ini +++ b/platformio.ini @@ -112,7 +112,7 @@ build_flags = platform = native test_framework = unity test_build_src = yes -build_src_filter = -<*> + + + + + + + + + + + + + + + + + + + + + + + +build_src_filter = -<*> + + + + + + + + + + + + + + + + + + + + + + + + + lib_deps = bblanchon/ArduinoJson@^7.2.2 build_flags = diff --git a/src/ConfigFile.cpp b/src/ConfigFile.cpp index e7854ca..69f6cde 100644 --- a/src/ConfigFile.cpp +++ b/src/ConfigFile.cpp @@ -8,20 +8,20 @@ static const char *TAG = "ConfigFile"; - -#define SHADE_HDR_VER 24 -#define SHADE_HDR_SIZE 76 -#define SHADE_REC_SIZE 276 -#define GROUP_REC_SIZE 200 -// writeTransRecord: enabled, 8 one-byte fields, 3 floats, txPower and noiseDetection. -// It was left at 74 when noiseDetection was added, so files written before -// the fix declare this record 6 bytes short. -#define TRANS_REC_SIZE 80 -#define ROOM_REC_SIZE 29 -#define REPEATER_REC_SIZE 77 - extern ConfigSettings settings; +// The records' strings are read straight into these fields. +static_assert(CFG_NAME_LEN == sizeof(SomfyShade::name), "shade name size"); +static_assert(CFG_NAME_LEN == sizeof(SomfyGroup::name), "group name size"); +static_assert(CFG_NAME_LEN == sizeof(SomfyRoom::name), "room name size"); +static_assert(CFG_HOSTNAME_LEN == sizeof(ConfigSettings::hostname), "hostname size"); +static_assert(CFG_NTP_SERVER_LEN == sizeof(NTPSettings::ntpServer), "NTP server size"); +static_assert(CFG_POSIX_ZONE_LEN == sizeof(NTPSettings::posixZone), "time zone size"); +static_assert(CFG_MQTT_PROTOCOL_LEN == sizeof(MQTTSettings::protocol), "MQTT protocol size"); +static_assert(CFG_MQTT_HOSTNAME_LEN == sizeof(MQTTSettings::hostname), "MQTT hostname size"); +static_assert(CFG_MQTT_TOPIC_LEN == sizeof(MQTTSettings::rootTopic), "MQTT root topic size"); +static_assert(CFG_MQTT_TOPIC_LEN == sizeof(MQTTSettings::discoTopic), "MQTT discovery topic size"); + bool ConfigFile::begin(const char* filename, bool readOnly) { this->file = LittleFS.open(filename, readOnly ? "r" : "w"); this->_opened = true; @@ -40,369 +40,292 @@ void ConfigFile::end() { 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::readByte(uint8_t &val) { + if(!this->file) return false; + return this->file.read(&val, 1) == 1; } -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) { +uint32_t ConfigFile::position() { return this->file.position(); } +bool ConfigFile::writeBytes(const uint8_t *buf, size_t len) { if(!this->isOpen()) return false; - this->writeUInt8(hdr.version); - this->writeUInt8(hdr.length); - this->writeUInt16(hdr.roomRecordSize); - this->writeUInt8(hdr.roomRecords); - this->writeUInt16(hdr.shadeRecordSize); - this->writeUInt8(hdr.shadeRecords); - this->writeUInt16(hdr.groupRecordSize); - this->writeUInt8(hdr.groupRecords); - this->writeUInt16(hdr.repeaterRecordSize); - this->writeUInt8(hdr.repeaterRecords); - this->writeUInt16(hdr.settingsRecordSize); - this->writeUInt16(hdr.netRecordSize); - this->writeUInt16(hdr.transRecordSize); - this->writeString(settings.serverId, sizeof(hdr.serverId), CFG_REC_END); - return true; + if(_ramBuf) { + for(size_t i = 0; i < len; i++) _ramBuf->concat(static_cast(buf[i])); + return true; + } + return this->file.write(buf, len) == len; +} +bool ConfigFile::writeHeader() { + if(!this->isOpen()) return false; + return writeConfigHeader(*this, this->header); } bool ConfigFile::readHeader() { if(!this->isOpen()) return false; - //if(this->file.position() != 0) this->file.seek(0, SeekSet); - ESP_LOGD(TAG, "Reading header at %u", this->file.position()); - this->header.version = this->readUInt8(this->header.version); - this->header.length = this->readUInt8(0); - if(this->header.version >= 19) { - this->header.roomRecordSize = this->readUInt16(this->header.roomRecordSize); - this->header.roomRecords = this->readUInt8(this->header.roomRecords); - } - if(this->header.version >= 13) this->header.shadeRecordSize = this->readUInt16(this->header.shadeRecordSize); - else this->header.shadeRecordSize = this->readUInt8((uint8_t)this->header.shadeRecordSize); - this->header.shadeRecords = this->readUInt8(this->header.shadeRecords); - if(this->header.version > 10) { - if(this->header.version >= 13) this->header.groupRecordSize = this->readUInt16(this->header.groupRecordSize); - else this->header.groupRecordSize = this->readUInt8(this->header.groupRecordSize); - this->header.groupRecords = this->readUInt8(this->header.groupRecords); - } - if(this->header.version >= 21) { - this->header.repeaterRecordSize = this->readUInt16(this->header.repeaterRecordSize); - this->header.repeaterRecords = this->readUInt8(this->header.repeaterRecords); - } - if(this->header.version > 13) { - this->header.settingsRecordSize = this->readUInt16(this->header.settingsRecordSize); - this->header.netRecordSize = this->readUInt16(this->header.netRecordSize); - this->header.transRecordSize = this->readUInt16(this->header.transRecordSize); - this->readString(this->header.serverId, sizeof(this->header.serverId)); - } + readConfigHeader(*this, this->header); ESP_LOGD(TAG, "version:%u len:%u roomSize:%u roomRecs:%u shadeSize:%u shadeRecs:%u groupSize:%u groupRecs: %u pos:%d", this->header.version, this->header.length, this->header.roomRecordSize, this->header.roomRecords, 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; + +// Between the shade model and its records. +static room_record_t roomRecord(const SomfyRoom *room) { + room_record_t rec; + rec.roomId = room->roomId; + strlcpy(rec.name, room->name, sizeof(rec.name)); + rec.sortOrder = room->sortOrder; + return rec; } -*/ -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; +static shade_record_t shadeRecord(SomfyShade *shade) { + if(shade->tiltType == tilt_types::none || shade->shadeType != shade_types::blind) { + shade->clearTilt(); + shade->tiltType = tilt_types::none; } - _rtrim(buff); - return true; + shade_record_t rec; + rec.shadeId = shade->getShadeId(); + rec.paired = shade->paired; + rec.shadeType = shade->shadeType; + rec.remoteAddress = shade->getRemoteAddress(); + strlcpy(rec.name, shade->name, sizeof(rec.name)); + rec.tiltType = shade->tiltType; + rec.proto = shade->proto; + rec.bitLength = shade->bitLength; + rec.upTime = shade->upTime; + rec.downTime = shade->downTime; + rec.tiltTime = shade->tiltTime; + rec.stepSize = shade->stepSize; + for(uint8_t j = 0; j < SOMFY_MAX_LINKED_REMOTES; j++) rec.linkedRemotes[j] = shade->linkedRemotes[j].getRemoteAddress(); + rec.lastRollingCode = shade->lastRollingCode; + rec.flags = shade->getFlags(); + rec.myPos = shade->state().myPos; + rec.myTiltPos = shade->state().myTiltPos; + rec.currentPos = shade->state().currentPos; + rec.currentTiltPos = shade->state().currentTiltPos; + rec.flipCommands = shade->flipCommands; + rec.flipPosition = shade->flipPosition; + rec.repeats = shade->repeats; + rec.sortOrder = shade->sortOrder; + rec.gpioUp = shade->gpioUp; + rec.gpioDown = shade->gpioDown; + rec.gpioMy = shade->gpioMy; + rec.gpioFlags = shade->gpioFlags; + rec.roomId = shade->roomId; + return rec; } -bool ConfigFile::skipValue(size_t len) { - if(!this->file) return false; - uint8_t quotes = 0; - uint8_t j = 0; - while(j < len) { - uint8_t val; - j++; - if(this->file.read(&val, 1) == 1) { - switch(val) { - case CFG_VALUE_SEP: - if(quotes >= 2 || quotes == 0) return true; - break; - case CFG_REC_END: - return true; - case CFG_TOK_QUOTE: - quotes++; - break; - } - } - else return false; - } - return true; +static group_record_t groupRecord(SomfyGroup *group) { + group_record_t rec; + rec.groupId = group->getGroupId(); + rec.groupType = group->groupType; + rec.remoteAddress = group->getRemoteAddress(); + strlcpy(rec.name, group->name, sizeof(rec.name)); + rec.proto = group->proto; + rec.bitLength = group->bitLength; + memcpy(rec.linkedShades, group->linkedShades, sizeof(rec.linkedShades)); + rec.repeats = group->repeats; + rec.sortOrder = group->sortOrder; + rec.flipCommands = group->flipCommands; + rec.roomId = group->roomId; + rec.lastRollingCode = group->lastRollingCode; + return rec; } -bool ConfigFile::readVarString(char *buff, size_t len) { - if(!this->file) return false; - memset(buff, 0x00, len); - uint8_t quotes = 0; - uint16_t i = 0; - uint16_t j = 0; - while(j < len) { - uint8_t val; - j++; - if(this->file.read(&val, 1) == 1) { - switch(val) { - case CFG_VALUE_SEP: - if(quotes >= 2) { - _rtrim(buff); - return true; - } - break; - case CFG_REC_END: - return true; - case CFG_TOK_QUOTE: - quotes++; - continue; - } - buff[i++] = val; - if(i == len) { - _rtrim(buff); - return true; - } - } - else - return false; - } - _rtrim(buff); - return true; +static settings_record_t settingsRecord() { + settings_record_t rec; + strlcpy(rec.fwVersion, settings.fwVersion.name, sizeof(rec.fwVersion)); + strlcpy(rec.hostname, settings.hostname, sizeof(rec.hostname)); + strlcpy(rec.ntpServer, settings.NTP.ntpServer, sizeof(rec.ntpServer)); + strlcpy(rec.posixZone, settings.NTP.posixZone, sizeof(rec.posixZone)); + rec.ssdpBroadcast = settings.ssdpBroadcast; + rec.checkForUpdate = settings.checkForUpdate; + return rec; +} +static net_record_t netRecord() { + net_record_t rec; + rec.connType = static_cast(settings.connType); + rec.dhcp = settings.IP.dhcp; + strlcpy(rec.ip, settings.IP.ip.toString().c_str(), sizeof(rec.ip)); + strlcpy(rec.gateway, settings.IP.gateway.toString().c_str(), sizeof(rec.gateway)); + strlcpy(rec.subnet, settings.IP.subnet.toString().c_str(), sizeof(rec.subnet)); + strlcpy(rec.dns1, settings.IP.dns1.toString().c_str(), sizeof(rec.dns1)); + strlcpy(rec.dns2, settings.IP.dns2.toString().c_str(), sizeof(rec.dns2)); + strlcpy(rec.mqttProtocol, settings.MQTT.protocol, sizeof(rec.mqttProtocol)); + strlcpy(rec.mqttHostname, settings.MQTT.hostname, sizeof(rec.mqttHostname)); + rec.mqttPort = settings.MQTT.port; + rec.mqttPubDisco = settings.MQTT.pubDisco; + strlcpy(rec.mqttRootTopic, settings.MQTT.rootTopic, sizeof(rec.mqttRootTopic)); + strlcpy(rec.mqttDiscoTopic, settings.MQTT.discoTopic, sizeof(rec.mqttDiscoTopic)); +#if HAS_ETHERNET + rec.ethBoardType = settings.Ethernet.boardType; + rec.ethPhyType = settings.Ethernet.phyType; + rec.ethClkMode = static_cast(settings.Ethernet.CLKMode); + rec.ethPhyAddress = settings.Ethernet.phyAddress; + rec.ethPWRPin = settings.Ethernet.PWRPin; + rec.ethMDCPin = settings.Ethernet.MDCPin; + rec.ethMDIOPin = settings.Ethernet.MDIOPin; +#endif + return rec; +} +static trans_record_t transRecord(const transceiver_config_t &cfg) { + trans_record_t rec; + rec.enabled = cfg.enabled; + rec.proto = cfg.proto; + rec.type = cfg.type; + rec.SCKPin = cfg.SCKPin; + rec.CSNPin = cfg.CSNPin; + rec.MOSIPin = cfg.MOSIPin; + rec.MISOPin = cfg.MISOPin; + rec.TXPin = cfg.TXPin; + rec.RXPin = cfg.RXPin; + rec.frequency = cfg.frequency; + rec.rxBandwidth = cfg.rxBandwidth; + rec.deviation = cfg.deviation; + rec.txPower = cfg.txPower; + rec.noiseDetection = cfg.noiseDetection; + return rec; +} +// NVS keeps the last rolling code each remote sent. +static uint16_t keepRollingCode(Preferences &pref, SomfyRemote *remote, uint16_t inFile, bool restoring) { + const uint16_t stored = pref.getUShort(remote->getRemotePrefId(), 0); + const uint16_t code = rollingCodeAfterLoad(stored, inFile, restoring); + if(code != stored) pref.putUShort(remote->getRemotePrefId(), code); + return code; } -bool ConfigFile::writeString(const char *val, size_t len, const char tok) { - if(!this->isOpen()) return false; - int slen = strlen(val); - if(_ramBuf) { - if(slen > 0) _ramBuf->concat(val); - while(slen < (int)len - 1) { _ramBuf->concat(' '); slen++; } - if(tok != CFG_TOK_NONE) _ramBuf->concat(tok); - return true; +void ShadeConfigFile::readRoom(SomfyRoom *room) { + room_record_t rec; + readRoomRecord(*this, this->header, rec); + room->roomId = rec.roomId; + strlcpy(room->name, rec.name, sizeof(room->name)); + room->sortOrder = rec.sortOrder; +} +void ShadeConfigFile::readShade(SomfyShade *shade) { + shade_record_t rec; + readShadeRecord(*this, this->header, rec); + if(rec.shadeId == 255) { + shade->clear(); + return; } - 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 < (int)len - 1) { - this->file.write(' '); - slen++; + Preferences pref; + pref.begin("ShadeCodes"); + shade->setShadeId(rec.shadeId); + shade->paired = rec.paired; + shade->shadeType = rec.shadeType; + shade->setRemoteAddress(rec.remoteAddress); + strlcpy(shade->name, rec.name, sizeof(shade->name)); + shade->tiltType = rec.tiltType; + shade->proto = rec.proto; + shade->bitLength = rec.bitLength; + shade->upTime = rec.upTime; + shade->downTime = rec.downTime; + shade->tiltTime = rec.tiltTime; + shade->stepSize = rec.stepSize; + for(uint8_t j = 0; j < SOMFY_MAX_LINKED_REMOTES; j++) { + SomfyLinkedRemote *rem = &shade->linkedRemotes[j]; + rem->setRemoteAddress(rec.linkedRemotes[j]); + if(rem->getRemoteAddress() != 0) rem->lastRollingCode = pref.getUShort(rem->getRemotePrefId(), 0); } - // 255 = len = 4 slen = 3 - if(tok != CFG_TOK_NONE) - return this->writeChar(tok); - return true; -} -bool ConfigFile::writeVarString(const char *val, const char tok) { - if(!this->isOpen()) return false; - int slen = strlen(val); - if(_ramBuf) { - _ramBuf->concat((char)CFG_TOK_QUOTE); - if(slen > 0) _ramBuf->concat(val); - _ramBuf->concat((char)CFG_TOK_QUOTE); - if(tok != CFG_TOK_NONE) _ramBuf->concat(tok); - return true; + shade->lastRollingCode = rec.lastRollingCode; + if(shade->getRemoteAddress() != 0) shade->lastRollingCode = keepRollingCode(pref, shade, rec.lastRollingCode, this->restoring); + pref.end(); + shade->setFlags(rec.flags); + // The targets are where the shade is. + shade->restore(rec.currentPos, rec.currentTiltPos, rec.myPos, rec.myTiltPos); + shade->flipCommands = rec.flipCommands; + shade->flipPosition = rec.flipPosition; + shade->repeats = rec.repeats; + shade->sortOrder = rec.sortOrder; + shade->gpioUp = rec.gpioUp; + shade->gpioDown = rec.gpioDown; + shade->gpioMy = rec.gpioMy; + shade->gpioFlags = rec.gpioFlags; + shade->roomId = rec.roomId; + if(shade->proto == radio_proto::GP_Relay || shade->proto == radio_proto::GP_Remote) { + pinMode(shade->gpioUp, OUTPUT); + pinMode(shade->gpioDown, OUTPUT); } - this->writeChar(CFG_TOK_QUOTE); - if(slen > 0) if(this->file.write((uint8_t *)val, slen) != slen) return false; - this->writeChar(CFG_TOK_QUOTE); - if(tok != CFG_TOK_NONE) return this->writeChar(tok); - return true; + if(shade->proto == radio_proto::GP_Remote) + pinMode(shade->gpioMy, OUTPUT); } -bool ConfigFile::writeChar(const char val) { - if(!this->isOpen()) return false; - if(_ramBuf) { _ramBuf->concat(val); return true; } - if(this->file.write(static_cast(val)) == 1) return true; - return false; -} -bool ConfigFile::writeInt8(const int8_t val, const char tok) { - char buff[5]; - snprintf(buff, sizeof(buff), "%4d", val); - return this->writeString(buff, sizeof(buff), tok); -} -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; -} -int8_t ConfigFile::readInt8(const int8_t defVal) { - char buff[5]; - if(this->readString(buff, sizeof(buff))) - return static_cast(atoi(buff)); - return defVal; -} -uint8_t ConfigFile::readUInt8(const uint8_t defVal) { - char buff[4]; - if(this->readString(buff, sizeof(buff))) - return static_cast(atoi(buff)); - return defVal; -} -uint16_t ConfigFile::readUInt16(const uint16_t defVal) { - char buff[6]; - if(this->readString(buff, sizeof(buff))) - return static_cast(atoi(buff)); - return defVal; -} -uint32_t ConfigFile::readUInt32(const uint32_t defVal) { - char buff[11]; - if(this->readString(buff, sizeof(buff))) - return static_cast(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; - } +void ShadeConfigFile::readGroup(SomfyGroup *group) { + group_record_t rec; + readGroupRecord(*this, this->header, rec); + if(rec.groupId == 255) { + group->clear(); + return; } - 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); + group->setGroupId(rec.groupId); + group->groupType = rec.groupType; + group->setRemoteAddress(rec.remoteAddress); + strlcpy(group->name, rec.name, sizeof(group->name)); + group->proto = rec.proto; + group->bitLength = rec.bitLength; + memcpy(group->linkedShades, rec.linkedShades, sizeof(group->linkedShades)); + group->compressLinkedShadeIds(); + group->repeats = rec.repeats; + group->sortOrder = rec.sortOrder; + group->flipCommands = rec.flipCommands; + group->roomId = rec.roomId; + group->lastRollingCode = rec.lastRollingCode; + if(group->getRemoteAddress() != 0) { + Preferences pref; + pref.begin("ShadeCodes"); + group->lastRollingCode = keepRollingCode(pref, group, rec.lastRollingCode, this->restoring); + pref.end(); } - return false; } -*/ +void ShadeConfigFile::readRepeaters(SomfyShadeController *s) { + uint32_t repeaters[SOMFY_MAX_REPEATERS]; + readRepeaterRecord(*this, repeaters); + for(uint8_t i = 0; i < SOMFY_MAX_REPEATERS; i++) s->linkRepeater(repeaters[i]); +} + 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) { +bool ShadeConfigFile::writeModel(SomfyShadeController *s) { this->header.version = SHADE_HDR_VER; + this->header.length = SHADE_HDR_SIZE; this->header.roomRecordSize = ROOM_REC_SIZE; this->header.roomRecords = s->roomCount(); this->header.shadeRecordSize = SHADE_REC_SIZE; - this->header.length = SHADE_HDR_SIZE; this->header.shadeRecords = s->shadeCount(); this->header.groupRecordSize = GROUP_REC_SIZE; this->header.groupRecords = s->groupCount(); this->header.repeaterRecords = 1; this->header.repeaterRecordSize = REPEATER_REC_SIZE; + strlcpy(this->header.serverId, settings.serverId, sizeof(this->header.serverId)); + this->writeHeader(); + for(uint8_t i = 0; i < SOMFY_MAX_ROOMS; i++) { + SomfyRoom *room = &s->rooms[i]; + if(room->roomId != 0) writeRoomRecord(*this, roomRecord(room)); + } + for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) { + SomfyShade *shade = &s->shades[i]; + if(shade->getShadeId() != 255) writeShadeRecord(*this, shadeRecord(shade)); + } + for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++) { + SomfyGroup *group = &s->groups[i]; + if(group->getGroupId() != 255) writeGroupRecord(*this, groupRecord(group)); + } + writeRepeaterRecord(*this, s->repeaters); + return true; +} +bool ShadeConfigFile::save(SomfyShadeController *s) { this->header.settingsRecordSize = 0; this->header.netRecordSize = 0; this->header.transRecordSize = 0; - this->writeHeader(); - for(uint8_t i = 0; i < SOMFY_MAX_ROOMS; i++) { - SomfyRoom *room = &s->rooms[i]; - if(room->roomId != 0) - this->writeRoomRecord(room); - } - for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) { - SomfyShade *shade = &s->shades[i]; - if(shade->getShadeId() != 255) - this->writeShadeRecord(shade); - } - for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++) { - SomfyGroup *group = &s->groups[i]; - if(group->getGroupId() != 255) - this->writeGroupRecord(group); - } - this->writeRepeaterRecord(s); - return true; + return this->writeModel(s); } bool ShadeConfigFile::backup(SomfyShadeController *s) { - this->header.version = SHADE_HDR_VER; - this->header.roomRecordSize = ROOM_REC_SIZE; - this->header.roomRecords = s->roomCount(); - this->header.shadeRecordSize = SHADE_REC_SIZE; - this->header.length = SHADE_HDR_SIZE; - this->header.shadeRecords = s->shadeCount(); - this->header.groupRecordSize = GROUP_REC_SIZE; - this->header.groupRecords = s->groupCount(); - this->header.repeaterRecords = 1; - this->header.repeaterRecordSize = REPEATER_REC_SIZE; - this->header.settingsRecordSize = settings.calcSettingsRecSize(); - this->header.netRecordSize = settings.calcNetRecSize(); + const settings_record_t settingsRec = settingsRecord(); + const net_record_t netRec = netRecord(); + const trans_record_t transRec = transRecord(s->transceiver.config); + // The header gives each record's size, which a counter measures. + ConfigCounter settingsBytes, netBytes; + writeSettingsRecord(settingsBytes, settingsRec); + writeNetRecord(netBytes, netRec, HAS_ETHERNET); + this->header.settingsRecordSize = settingsBytes.bytes; + this->header.netRecordSize = netBytes.bytes; this->header.transRecordSize = TRANS_REC_SIZE; - this->writeHeader(); - for(uint8_t i = 0; i < SOMFY_MAX_ROOMS; i++) { - SomfyRoom *room = &s->rooms[i]; - if(room->roomId != 0) - this->writeRoomRecord(room); - } - for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) { - SomfyShade *shade = &s->shades[i]; - if(shade->getShadeId() != 255) - this->writeShadeRecord(shade); - } - for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++) { - SomfyGroup *group = &s->groups[i]; - if(group->getGroupId() != 255) - this->writeGroupRecord(group); - } - this->writeRepeaterRecord(s); - this->writeSettingsRecord(); - this->writeNetRecord(); - this->writeTransRecord(s->transceiver.config); + this->writeModel(s); + writeSettingsRecord(*this, settingsRec); + writeNetRecord(*this, netRec, HAS_ETHERNET); + writeTransRecord(*this, transRec); return true; } bool ShadeConfigFile::validate() { @@ -415,32 +338,20 @@ bool ShadeConfigFile::validate() { ESP_LOGE(TAG, "Invalid Shade Record Size:%u", this->header.shadeRecordSize); return false; } - /* - if(this->header.shadeRecords != SOMFY_MAX_SHADES) { - ESP_LOGE(TAG, "Invalid Shade Record Count:%u", this->header.shadeRecords); - return false; - } - */ if(this->header.version > 10) { if(this->header.groupRecordSize < 100) { ESP_LOGE(TAG, "Invalid Group Record Size:%u", this->header.groupRecordSize); return false; } - /* - if(this->header.groupRecords != SOMFY_MAX_GROUPS) { - ESP_LOGE(TAG, "Invalid Group Record Count:%u", this->header.groupRecords); - return false; - - } - */ } if(this->file.position() != this->header.length) { ESP_LOGE(TAG, "File not positioned at %u end of header: %d", this->header.length, this->file.position()); return false; } - - // Only a warning: older backups declare the transceiver record 6 bytes - // short (see TRANS_REC_SIZE), and the record checks below catch real damage. + + // Only a warning: backups written before 4.0 declare the transceiver record + // 6 bytes short, and 4.0 backups from boards without Ethernet declare the + // network record 28 bytes long. Records are read to their newline. const uint32_t fsize = expectedFileBytes(this->header); if(this->file.size() != fsize) { ESP_LOGW(TAG, "File is %u bytes but its header adds up to %u", (unsigned)this->file.size(), (unsigned)fsize); @@ -506,7 +417,7 @@ bool ShadeConfigFile::validate() { } } this->file.seek(startPos, SeekSet); - return true; + return true; } bool ShadeConfigFile::load(SomfyShadeController *s, const char *filename) { ShadeConfigFile file; @@ -546,76 +457,100 @@ bool ShadeConfigFile::restoreFile(SomfyShadeController *s, const char *filename, if(opened) this->end(); return false; } + this->restoring = true; if(opts.shades) { ESP_LOGI(TAG, "Restoring Rooms..."); - for(uint8_t i = 0; i < this->header.roomRecords; i++) { - this->readRoomRecord(&s->rooms[i]); - ESP_LOGD(TAG, "Room %u", s->rooms[i].roomId); - } + for(uint8_t i = 0; i < this->header.roomRecords; i++) this->readRoom(&s->rooms[i]); + for(uint8_t i = this->header.roomRecords; i < SOMFY_MAX_ROOMS; i++) s->rooms[i].clear(); ESP_LOGI(TAG, "Restoring Shades..."); - // We should be valid so start reading. - for(uint8_t i = 0; i < this->header.shadeRecords; i++) { - this->readShadeRecord(&s->shades[i]); - ESP_LOGD(TAG, "Shade %u", s->shades[i].getShadeId()); - } - if(this->header.shadeRecords < SOMFY_MAX_SHADES) { - uint8_t ndx = this->header.shadeRecords; - // Clear out any positions that are not in the shade file. - while(ndx < SOMFY_MAX_SHADES) { - ((SomfyShade *)&s->shades[ndx++])->clear(); - } - } + for(uint8_t i = 0; i < this->header.shadeRecords; i++) this->readShade(&s->shades[i]); + for(uint8_t i = this->header.shadeRecords; i < SOMFY_MAX_SHADES; i++) s->shades[i].clear(); ESP_LOGI(TAG, "Restoring Groups..."); - for(uint8_t i = 0; i < this->header.groupRecords; i++) { - ESP_LOGD(TAG, "Group %u", s->groups[i].getGroupId()); - this->readGroupRecord(&s->groups[i]); - } - if(this->header.groupRecords < SOMFY_MAX_GROUPS) { - uint8_t ndx = this->header.groupRecords; - // Clear out any positions that are not in the shade file. - while(ndx < SOMFY_MAX_GROUPS) { - ((SomfyGroup *)&s->groups[ndx++])->clear(); - } - } + for(uint8_t i = 0; i < this->header.groupRecords; i++) this->readGroup(&s->groups[i]); + for(uint8_t i = this->header.groupRecords; i < SOMFY_MAX_GROUPS; i++) s->groups[i].clear(); } else { ESP_LOGI(TAG, "Shade data ignored"); - // FF past the rooms, shades and groups. - this->file.seek(this->file.position() + shadeModelBytes(this->header), SeekSet); + skipRecords(*this, this->header.roomRecords + this->header.shadeRecords + this->header.groupRecords); } - if(opts.repeaters) { + if(opts.repeaters && this->header.repeaterRecords > 0) { ESP_LOGI(TAG, "Restoring Repeaters..."); - if(this->header.repeaterRecords > 0) { - memset(s->repeaters, 0x00, sizeof(uint32_t) * SOMFY_MAX_REPEATERS); - for(uint8_t i = 0; i < this->header.repeaterRecords; i++) { - this->readRepeaterRecord(s); + memset(s->repeaters, 0x00, sizeof(s->repeaters)); + for(uint8_t i = 0; i < this->header.repeaterRecords; i++) this->readRepeaters(s); + } + else skipRecords(*this, this->header.repeaterRecords); + if(this->header.settingsRecordSize > 0) { + if(opts.settings) { + settings_record_t rec; + readSettingsRecord(*this, this->header, rec); + strlcpy(settings.hostname, rec.hostname, sizeof(settings.hostname)); + strlcpy(settings.NTP.ntpServer, rec.ntpServer, sizeof(settings.NTP.ntpServer)); + strlcpy(settings.NTP.posixZone, rec.posixZone, sizeof(settings.NTP.posixZone)); + settings.ssdpBroadcast = rec.ssdpBroadcast; + settings.checkForUpdate = rec.checkForUpdate; + } + else skipRecords(*this, 1); + } + if(this->header.netRecordSize > 0) { + if(opts.network || opts.mqtt) { + // Only the board that wrote the Ethernet settings takes them back. + const bool sameBoard = HAS_ETHERNET && strncmp(settings.serverId, this->header.serverId, sizeof(settings.serverId)) == 0; + net_record_t rec; + readNetRecord(*this, this->header, rec, sameBoard); + if(opts.network) { + settings.connType = static_cast(rec.connType); + settings.IP.dhcp = rec.dhcp; + settings.IP.ip.fromString(rec.ip); + settings.IP.gateway.fromString(rec.gateway); + settings.IP.subnet.fromString(rec.subnet); + settings.IP.dns1.fromString(rec.dns1); + settings.IP.dns2.fromString(rec.dns2); +#if HAS_ETHERNET + if(sameBoard) { + ESP_LOGI(TAG, "Restoring Ethernet adapter settings"); + settings.Ethernet.boardType = rec.ethBoardType; + settings.Ethernet.phyType = rec.ethPhyType; + settings.Ethernet.CLKMode = static_cast(rec.ethClkMode); + settings.Ethernet.phyAddress = rec.ethPhyAddress; + settings.Ethernet.PWRPin = rec.ethPWRPin; + settings.Ethernet.MDCPin = rec.ethMDCPin; + settings.Ethernet.MDIOPin = rec.ethMDIOPin; + } +#endif + } + if(opts.mqtt && this->header.version >= 22) { + strlcpy(settings.MQTT.protocol, rec.mqttProtocol, sizeof(settings.MQTT.protocol)); + strlcpy(settings.MQTT.hostname, rec.mqttHostname, sizeof(settings.MQTT.hostname)); + settings.MQTT.port = rec.mqttPort; + settings.MQTT.pubDisco = rec.mqttPubDisco; + strlcpy(settings.MQTT.rootTopic, rec.mqttRootTopic, sizeof(settings.MQTT.rootTopic)); + strlcpy(settings.MQTT.discoTopic, rec.mqttDiscoTopic, sizeof(settings.MQTT.discoTopic)); } } - } - else { - this->file.seek(this->file.position() + this->header.repeaterRecordSize, SeekSet); - } - if(opts.settings) { - // First read out the data. - this->readSettingsRecord(); - } - else { - this->file.seek(this->file.position() + this->header.settingsRecordSize, SeekSet); - } - if(opts.network || opts.mqtt) { - this->readNetRecord(opts); - } - else { - this->file.seek(this->file.position() + this->header.netRecordSize, SeekSet); + else skipRecords(*this, 1); } // The repeaters are saved in shades.cfg too. Left to somfy.loop(), they // race the reboot. if(opts.shades || opts.repeaters) s->commit(); - if(opts.transceiver) - { + if(opts.transceiver && this->header.transRecordSize > 0) { // Read into a copy so the radio task never transmits on a half restored config. + trans_record_t rec = transRecord(s->transceiver.config); + readTransRecord(*this, rec); transceiver_config_t cfg = s->transceiver.config; - this->readTransRecord(cfg); + cfg.enabled = rec.enabled; + cfg.proto = rec.proto; + cfg.type = rec.type; + cfg.SCKPin = rec.SCKPin; + cfg.CSNPin = rec.CSNPin; + cfg.MOSIPin = rec.MOSIPin; + cfg.MISOPin = rec.MISOPin; + cfg.TXPin = rec.TXPin; + cfg.RXPin = rec.RXPin; + cfg.frequency = rec.frequency; + cfg.rxBandwidth = rec.rxBandwidth; + cfg.deviation = rec.deviation; + cfg.txPower = rec.txPower; + cfg.noiseDetection = rec.noiseDetection; s->transceiver.setConfig(cfg); } if(opts.settings || opts.network) settings.save(); @@ -630,271 +565,6 @@ bool ShadeConfigFile::restoreFile(SomfyShadeController *s, const char *filename, if(opts.mqtt) settings.MQTT.save(); return true; } -bool ShadeConfigFile::readNetRecord(restore_options_t &opts) { - if(this->header.netRecordSize > 0) { - uint32_t startPos = this->file.position(); - if(opts.network) { - ESP_LOGD(TAG, "Reading network settings from file..."); - settings.connType = static_cast(this->readUInt8(static_cast(conn_types_t::unset))); - settings.IP.dhcp = this->readBool(true); - char ip[24]; - this->readVarString(ip, sizeof(ip)); - settings.IP.ip.fromString(ip); - this->readVarString(ip, sizeof(ip)); - settings.IP.gateway.fromString(ip); - this->readVarString(ip, sizeof(ip)); - settings.IP.subnet.fromString(ip); - this->readVarString(ip, sizeof(ip)); - settings.IP.dns1.fromString(ip); - this->readVarString(ip, sizeof(ip)); - settings.IP.dns2.fromString(ip); - } - else { - this->skipValue(4); // connType - this->skipValue(6); // dhcp flag - this->skipValue(24); // ip - this->skipValue(24); // gateway - this->skipValue(24); // subnet - this->skipValue(24); // dns1 - this->skipValue(24); // dns2 - } - if(this->header.version >= 22) { - if(opts.mqtt) { - this->readVarString(settings.MQTT.protocol, sizeof(settings.MQTT.protocol)); - this->readVarString(settings.MQTT.hostname, sizeof(settings.MQTT.hostname)); - settings.MQTT.port = this->readUInt16(1883); - settings.MQTT.pubDisco = this->readBool(false); - this->readVarString(settings.MQTT.rootTopic, sizeof(settings.MQTT.rootTopic)); - this->readVarString(settings.MQTT.discoTopic, sizeof(settings.MQTT.discoTopic)); - } - else { - this->skipValue(sizeof(settings.MQTT.protocol)); - this->skipValue(sizeof(settings.MQTT.hostname)); - this->skipValue(6); // Port - this->skipValue(6); // pubDisco - this->skipValue(sizeof(settings.MQTT.rootTopic)); - this->skipValue(sizeof(settings.MQTT.discoTopic)); - } - } - // Now lets check to see if we are the same board. If we are then we will restore - // the ethernet phy settings. - if(opts.network) { - if(strncmp(settings.serverId, this->header.serverId, sizeof(settings.serverId)) == 0) { - ESP_LOGI(TAG, "Restoring Ethernet adapter settings"); -#if HAS_ETHERNET - settings.Ethernet.boardType = this->readUInt8(1); - settings.Ethernet.phyType = this->readUInt8(0); - settings.Ethernet.CLKMode = static_cast(this->readUInt8(0)); - settings.Ethernet.phyAddress = this->readInt8(1); - settings.Ethernet.PWRPin = this->readInt8(1); - settings.Ethernet.MDCPin = this->readInt8(16); - settings.Ethernet.MDIOPin = this->readInt8(23); -#endif - } - } - if(this->file.position() != startPos + this->header.netRecordSize) { - ESP_LOGD(TAG, "Reading to end of network record"); - this->seekChar(CFG_REC_END); - } - } - return true; -} -bool ShadeConfigFile::readTransRecord(transceiver_config_t &cfg) { - if(this->header.transRecordSize > 0) { - uint32_t startPos = this->file.position(); - ESP_LOGD(TAG, "Reading Transceiver settings from file..."); - cfg.enabled = this->readBool(false); - cfg.proto = static_cast(this->readUInt8(0)); - cfg.type = this->readUInt8(56); - cfg.SCKPin = this->readUInt8(cfg.SCKPin); - cfg.CSNPin = this->readUInt8(cfg.CSNPin); - cfg.MOSIPin = this->readUInt8(cfg.MOSIPin); - cfg.MISOPin = this->readUInt8(cfg.MISOPin); - cfg.TXPin = this->readUInt8(cfg.TXPin); - cfg.RXPin = this->readUInt8(cfg.RXPin); - cfg.frequency = this->readFloat(cfg.frequency); - cfg.rxBandwidth = this->readFloat(cfg.rxBandwidth); - cfg.deviation = this->readFloat(cfg.deviation); - cfg.txPower = this->readInt8(cfg.txPower); - cfg.noiseDetection = this->readBool(false); - if(this->file.position() != startPos + this->header.transRecordSize) { - ESP_LOGD(TAG, "Reading to end of transceiver record"); - this->seekChar(CFG_REC_END); - } - - } - return true; -} -bool ShadeConfigFile::readSettingsRecord() { - if(this->header.settingsRecordSize > 0) { - uint32_t startPos = this->file.position(); - ESP_LOGD(TAG, "Reading settings from file..."); - char ver[24]; - this->readVarString(ver, sizeof(ver)); - this->readVarString(settings.hostname, sizeof(settings.hostname)); - this->readVarString(settings.NTP.ntpServer, sizeof(settings.NTP.ntpServer)); - this->readVarString(settings.NTP.posixZone, sizeof(settings.NTP.posixZone)); - settings.ssdpBroadcast = this->readBool(false); - if(this->header.version >= 20) settings.checkForUpdate = this->readBool(true); - if(this->file.position() != startPos + this->header.settingsRecordSize) { - ESP_LOGD(TAG, "Reading to end of settings record"); - this->seekChar(CFG_REC_END); - } - } - return true; -} -bool ShadeConfigFile::readGroupRecord(SomfyGroup *group) { - Preferences pref; - pref.begin("ShadeCodes"); - uint32_t startPos = this->file.position(); - group->setGroupId(this->readUInt8(255)); - group->groupType = static_cast(this->readUInt8(0)); - group->setRemoteAddress(this->readUInt32(0)); - this->readString(group->name, sizeof(group->name)); - group->proto = static_cast(this->readUInt8(0)); - group->bitLength = this->readUInt8(56); - if(this->header.version == 23) group->lastRollingCode = this->readUInt16(0); - uint8_t lsd = 0; - memset(group->linkedShades, 0x00, sizeof(group->linkedShades)); - 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(this->header.version >= 13) group->sortOrder = this->readUInt8(group->getGroupId() - 1); - else group->sortOrder = group->getGroupId() - 1; - - if(group->getGroupId() == 255) group->clear(); - else group->compressLinkedShadeIds(); - if(this->header.version >= 18) group->flipCommands = this->readBool(false); - if(this->header.version >= 19) group->roomId = this->readUInt8(0); - if(this->header.version >= 24) group->lastRollingCode = this->readUInt16(0); - 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); - } - - pref.end(); - if(this->file.position() != startPos + this->header.groupRecordSize) { - ESP_LOGD(TAG, "Reading to end of group record"); - this->seekChar(CFG_REC_END); - } - return true; -} -bool ShadeConfigFile::readRepeaterRecord(SomfyShadeController *s) { - uint32_t startPos = this->file.position(); - - for(uint8_t i = 0; i < SOMFY_MAX_REPEATERS; i++) { - s->linkRepeater(this->readUInt32(0)); - } - if(this->file.position() != startPos + this->header.repeaterRecordSize) { - ESP_LOGD(TAG, "Reading to end of repeater record"); - this->seekChar(CFG_REC_END); - } - return true; -} -bool ShadeConfigFile::readRoomRecord(SomfyRoom *room) { - uint32_t startPos = this->file.position(); - room->roomId = this->readUInt8(0); - this->readString(room->name, sizeof(room->name)); - room->sortOrder = this->readUInt8(room->roomId - 1); - if(this->file.position() != startPos + this->header.roomRecordSize) { - ESP_LOGD(TAG, "Reading to end of room record"); - this->seekChar(CFG_REC_END); - } - return true; -} - -bool ShadeConfigFile::readShadeRecord(SomfyShade *shade) { - Preferences pref; - pref.begin("ShadeCodes"); - uint32_t startPos = this->file.position(); - shade->setShadeId(this->readUInt8(255)); - shade->paired = this->readBool(false); - shade->shadeType = static_cast(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(this->readUInt8(0)); - if(this->header.version > 6) shade->proto = static_cast(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->setFlags(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); - } - float myPos = shade->state().myPos; - float myTiltPos = shade->state().myTiltPos; - if(this->header.version < 4) - myPos = static_cast(this->readUInt8(255)); - else { - myPos = this->readFloat(-1); - myTiltPos = this->readFloat(-1); - } - if(myPos > 100 || myPos < 0) myPos = -1; - if(myTiltPos > 100 || myTiltPos < 0) myTiltPos = -1; - float currentPos = this->readFloat(0); - float currentTiltPos = this->readFloat(0); - if(shade->tiltType == tilt_types::none || shade->shadeType != shade_types::blind) { - myTiltPos = -1; - currentTiltPos = 0; - shade->tiltType = tilt_types::none; - } - if(this->header.version < 3) { - currentPos = currentPos * 100; - currentTiltPos = currentTiltPos * 100; - } - // The targets are where the shade is. - shade->restore(currentPos, currentTiltPos, myPos, myTiltPos); - 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(this->header.version >= 13) shade->sortOrder = this->readUInt8(shade->getShadeId() - 1); - else shade->sortOrder = shade->getShadeId() - 1; - if(this->header.version > 14) { - shade->gpioUp = this->readUInt8(shade->gpioUp); - shade->gpioDown = this->readUInt8(shade->gpioDown); - } - if(this->header.version > 15) - shade->gpioMy = this->readUInt8(shade->gpioMy); - if(this->header.version > 16) - shade->gpioFlags = this->readUInt8(shade->gpioFlags); - if(shade->getShadeId() == 255) shade->clear(); - else if(shade->tiltType == tilt_types::tiltonly) { - shade->restore(100.0f, shade->state().currentTiltPos, 100.0f, shade->state().myTiltPos); - } - pref.end(); - if(shade->proto == radio_proto::GP_Relay || shade->proto == radio_proto::GP_Remote) { - pinMode(shade->gpioUp, OUTPUT); - pinMode(shade->gpioDown, OUTPUT); - } - if(shade->proto == radio_proto::GP_Remote) - pinMode(shade->gpioMy, OUTPUT); - if(this->header.version >= 19) shade->roomId = this->readUInt8(0); - if(this->file.position() != startPos + this->header.shadeRecordSize) { - ESP_LOGD(TAG, "Reading to end of shade record"); - this->seekChar(CFG_REC_END); - } - return true; -} bool ShadeConfigFile::loadFile(SomfyShadeController *s, const char *filename) { bool opened = false; if(!this->isOpen()) { @@ -907,42 +577,16 @@ bool ShadeConfigFile::loadFile(SomfyShadeController *s, const char *filename) { if(opened) this->end(); return false; } - for(uint8_t i = 0; i < this->header.roomRecords;i++) { - this->readRoomRecord(&s->rooms[i]); - } - if(this->header.roomRecords < SOMFY_MAX_ROOMS) { - uint8_t ndx = this->header.roomRecords; - // Clear out any positions that are not in the shade file. - while(ndx < SOMFY_MAX_ROOMS) { - ((SomfyRoom *)&s->rooms[ndx++])->clear(); - } - } - - // We should be valid so start reading. - for(uint8_t i = 0; i < this->header.shadeRecords; i++) { - this->readShadeRecord(&s->shades[i]); - } - if(this->header.shadeRecords < SOMFY_MAX_SHADES) { - uint8_t ndx = this->header.shadeRecords; - // Clear out any positions that are not in the shade file. - while(ndx < SOMFY_MAX_SHADES) { - ((SomfyShade *)&s->shades[ndx++])->clear(); - } - } - for(uint8_t i = 0; i < this->header.groupRecords; i++) { - this->readGroupRecord(&s->groups[i]); - } - if(this->header.groupRecords < SOMFY_MAX_GROUPS) { - uint8_t ndx = this->header.groupRecords; - // Clear out any positions that are not in the shade file. - while(ndx < SOMFY_MAX_GROUPS) { - ((SomfyGroup *)&s->groups[ndx++])->clear(); - } - } + this->restoring = false; + for(uint8_t i = 0; i < this->header.roomRecords; i++) this->readRoom(&s->rooms[i]); + for(uint8_t i = this->header.roomRecords; i < SOMFY_MAX_ROOMS; i++) s->rooms[i].clear(); + for(uint8_t i = 0; i < this->header.shadeRecords; i++) this->readShade(&s->shades[i]); + for(uint8_t i = this->header.shadeRecords; i < SOMFY_MAX_SHADES; i++) s->shades[i].clear(); + for(uint8_t i = 0; i < this->header.groupRecords; i++) this->readGroup(&s->groups[i]); + for(uint8_t i = this->header.groupRecords; i < SOMFY_MAX_GROUPS; i++) s->groups[i].clear(); if(this->header.repeaterRecords > 0) { - memset(s->repeaters, 0x00, sizeof(uint32_t) * SOMFY_MAX_REPEATERS); - for(uint8_t i = 0; i < this->header.repeaterRecords; i++) - this->readRepeaterRecord(s); + memset(s->repeaters, 0x00, sizeof(s->repeaters)); + for(uint8_t i = 0; i < this->header.repeaterRecords; i++) this->readRepeaters(s); } if(opened) { ESP_LOGI(TAG, "Closing shade config file"); @@ -950,134 +594,4 @@ bool ShadeConfigFile::loadFile(SomfyShadeController *s, const char *filename) { } return true; } -bool ShadeConfigFile::writeGroupRecord(SomfyGroup *group) { - this->writeUInt8(group->getGroupId()); - this->writeUInt8(static_cast(group->groupType)); - this->writeUInt32(group->getRemoteAddress()); - this->writeString(group->name, sizeof(group->name)); - this->writeUInt8(static_cast(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); - this->writeUInt8(group->sortOrder); - this->writeBool(group->flipCommands); - this->writeUInt8(group->roomId); - this->writeUInt16(group->lastRollingCode, CFG_REC_END); - return true; -} -bool ShadeConfigFile::writeRepeaterRecord(SomfyShadeController *s) { - for(uint8_t i = 0; i < SOMFY_MAX_REPEATERS; i++) { - this->writeUInt32(s->repeaters[i], i == SOMFY_MAX_REPEATERS - 1 ? CFG_REC_END : CFG_VALUE_SEP); - } - return true; -} -bool ShadeConfigFile::writeRoomRecord(SomfyRoom *room) { - this->writeUInt8(room->roomId); - this->writeString(room->name, sizeof(room->name)); - this->writeUInt8(room->sortOrder, CFG_REC_END); - return true; -} -bool ShadeConfigFile::writeShadeRecord(SomfyShade *shade) { - if(shade->tiltType == tilt_types::none || shade->shadeType != shade_types::blind) { - shade->clearTilt(); - shade->tiltType = tilt_types::none; - } - this->writeUInt8(shade->getShadeId()); - this->writeBool(shade->paired); - this->writeUInt8(static_cast(shade->shadeType)); - this->writeUInt32(shade->getRemoteAddress()); - this->writeString(shade->name, sizeof(shade->name)); - this->writeUInt8(static_cast(shade->tiltType)); - this->writeUInt8(static_cast(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->getFlags()); - this->writeFloat(shade->state().myPos, 5); - this->writeFloat(shade->state().myTiltPos, 5); - this->writeFloat(shade->state().currentPos, 5); - this->writeFloat(shade->state().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); - this->writeUInt8(shade->sortOrder); - this->writeUInt8(shade->gpioUp); - this->writeUInt8(shade->gpioDown); - this->writeUInt8(shade->gpioMy); - this->writeUInt8(shade->gpioFlags); - this->writeUInt8(shade->roomId, CFG_REC_END); - return true; -} -bool ShadeConfigFile::writeSettingsRecord() { - this->writeVarString(settings.fwVersion.name); - this->writeVarString(settings.hostname); - this->writeVarString(settings.NTP.ntpServer); - this->writeVarString(settings.NTP.posixZone); - this->writeBool(settings.ssdpBroadcast); - this->writeBool(settings.checkForUpdate, CFG_REC_END); - return true; -} -bool ShadeConfigFile::writeNetRecord() { - this->writeUInt8(static_cast(settings.connType)); - this->writeBool(settings.IP.dhcp); - this->writeVarString(settings.IP.ip.toString().c_str()); - this->writeVarString(settings.IP.gateway.toString().c_str()); - this->writeVarString(settings.IP.subnet.toString().c_str()); - this->writeVarString(settings.IP.dns1.toString().c_str()); - this->writeVarString(settings.IP.dns2.toString().c_str()); - this->writeVarString(settings.MQTT.protocol); - this->writeVarString(settings.MQTT.hostname); - this->writeUInt16(settings.MQTT.port); - this->writeBool(settings.MQTT.pubDisco); - this->writeVarString(settings.MQTT.rootTopic); - this->writeVarString(settings.MQTT.discoTopic); -#if HAS_ETHERNET - this->writeUInt8(settings.Ethernet.boardType); - this->writeUInt8(settings.Ethernet.phyType); - this->writeUInt8(static_cast(settings.Ethernet.CLKMode)); - this->writeInt8(settings.Ethernet.phyAddress); - this->writeInt8(settings.Ethernet.PWRPin); - this->writeInt8(settings.Ethernet.MDCPin); - this->writeInt8(settings.Ethernet.MDIOPin, CFG_REC_END); -#else - this->writeUInt8(0, CFG_REC_END); -#endif - return true; -} -bool ShadeConfigFile::writeTransRecord(transceiver_config_t &cfg) { - this->writeBool(cfg.enabled); - this->writeUInt8(static_cast(cfg.proto)); - this->writeUInt8(cfg.type); - this->writeUInt8(cfg.SCKPin); - this->writeUInt8(cfg.CSNPin); - this->writeUInt8(cfg.MOSIPin); - this->writeUInt8(cfg.MISOPin); - this->writeUInt8(cfg.TXPin); - this->writeUInt8(cfg.RXPin); - this->writeFloat(cfg.frequency, 3); - this->writeFloat(cfg.rxBandwidth, 2); - this->writeFloat(cfg.deviation, 2); - this->writeInt8(cfg.txPower); - this->writeBool(cfg.noiseDetection, CFG_REC_END); - return true; -} bool ShadeConfigFile::exists() { return LittleFS.exists("/shades.cfg"); } diff --git a/src/ConfigFile.h b/src/ConfigFile.h index d7932e9..3e8777c 100644 --- a/src/ConfigFile.h +++ b/src/ConfigFile.h @@ -3,72 +3,40 @@ #include "Somfy.h" #include "ConfigSettings.h" #include "ConfigHeader.h" +#include "ConfigRecords.h" #ifndef configfile_h #define configfile_h -#define CFG_VALUE_SEP ',' -#define CFG_REC_END '\n' -#define CFG_TOK_NONE 0x00 -#define CFG_TOK_QUOTE '"' - - -class ConfigFile { +// Opens shades.cfg or a backup and writes or reads its values +// (ConfigRecords.h), from the file or into a String in RAM. +class ConfigFile : public ConfigReader, public ConfigWriter { protected: File file; String *_ramBuf = nullptr; bool readOnly = false; bool begin(const char *filename, bool readOnly = false); - uint32_t startRecPos = 0; bool _opened = false; public: config_header_t header; bool beginRAM(String *buf); void end(); bool isOpen(); - bool seekRecordByIndex(uint16_t ndx); bool readHeader(); - bool seekChar(const char val); - bool writeHeader(const config_header_t &header); bool writeHeader(); - bool writeSeparator(); - bool writeRecordEnd(); - bool writeChar(const char val); - bool writeInt8(const int8_t val, const char tok = CFG_VALUE_SEP); - bool writeUInt8(const uint8_t val, const char tok = CFG_VALUE_SEP); - bool writeUInt16(const uint16_t val, const char tok = CFG_VALUE_SEP); - bool writeUInt32(const uint32_t val, const char tok = CFG_VALUE_SEP); - bool writeBool(const bool val, const char tok = CFG_VALUE_SEP); - bool writeFloat(const float val, const uint8_t prec, const char tok = CFG_VALUE_SEP); - bool readString(char *buff, size_t len); - bool readVarString(char *buff, size_t len); - bool skipValue(size_t len); - bool writeString(const char *val, size_t len, const char tok = CFG_VALUE_SEP); - bool writeVarString(const char *val, const char tok = CFG_VALUE_SEP); - char readChar(const char defVal = '\0'); - int8_t readInt8(const int8_t defVal = 0); - uint8_t readUInt8(const uint8_t defVal = 0); - uint16_t readUInt16(const uint16_t defVal = 0); - uint32_t readUInt32(const uint32_t defVal = 0); - bool readBool(const bool defVal = false); - float readFloat(const float defVal = 0.00); + bool readByte(uint8_t &val) override; + uint32_t position() override; + bool writeBytes(const uint8_t *buf, size_t len) override; }; class ShadeConfigFile : public ConfigFile { protected: - bool writeRepeaterRecord(SomfyShadeController *s); - bool writeRoomRecord(SomfyRoom *room); - bool writeShadeRecord(SomfyShade *shade); - bool writeGroupRecord(SomfyGroup *group); - bool writeSettingsRecord(); - bool writeNetRecord(); - bool writeTransRecord(transceiver_config_t &cfg); - bool readRepeaterRecord(SomfyShadeController *s); - bool readRoomRecord(SomfyRoom *room); - bool readShadeRecord(SomfyShade *shade); - bool readGroupRecord(SomfyGroup *group); - bool readSettingsRecord(); - bool readNetRecord(restore_options_t &opts); - bool readTransRecord(transceiver_config_t &cfg); + // A restore takes the rolling codes in the file (rollingCodeAfterLoad). + bool restoring = false; + bool writeModel(SomfyShadeController *s); + void readRoom(SomfyRoom *room); + void readShade(SomfyShade *shade); + void readGroup(SomfyGroup *group); + void readRepeaters(SomfyShadeController *s); public: static bool exists(); static bool load(SomfyShadeController *somfy, const char *filename = "/shades.cfg"); diff --git a/src/ConfigRecords.cpp b/src/ConfigRecords.cpp new file mode 100644 index 0000000..6ba37b8 --- /dev/null +++ b/src/ConfigRecords.cpp @@ -0,0 +1,495 @@ +#include "ConfigRecords.h" +#include "Utils.h" + +bool ConfigReader::next(uint8_t &val) { + if(!this->readByte(val)) return false; + this->_last = val; + return true; +} +void ConfigReader::endRecord() { + if(!this->atRecordEnd()) this->seekChar(CFG_REC_END); +} +bool ConfigReader::seekChar(const char val) { + uint8_t ch; + do { + if(!this->next(ch)) return false; + } while(ch != static_cast(val)); + return true; +} +bool ConfigReader::readString(char *buff, size_t len) { + memset(buff, 0x00, len); + if(this->atRecordEnd()) return false; + size_t i = 0; + uint8_t val; + while(this->next(val)) { + if(val == CFG_REC_END || val == CFG_VALUE_SEP) { + _rtrim(buff); + return true; + } + if(i < len - 1) buff[i++] = val; + } + return false; +} +bool ConfigReader::readFixedString(char *buff, size_t len) { + memset(buff, 0x00, len); + if(this->atRecordEnd()) return false; + uint8_t val; + for(size_t i = 0; i < len - 1; i++) { + if(!this->next(val)) return false; + if(val == CFG_REC_END) { + _rtrim(buff); + return true; + } + buff[i] = val; + } + // The separator after the padding. + if(!this->next(val)) return false; + _rtrim(buff); + return true; +} +bool ConfigReader::readVarString(char *buff, size_t len) { + memset(buff, 0x00, len); + if(this->atRecordEnd()) return false; + uint8_t quotes = 0; + size_t i = 0; + uint8_t val; + while(this->next(val)) { + if(val == CFG_REC_END || (val == CFG_VALUE_SEP && quotes >= 2)) { + _rtrim(buff); + return true; + } + if(val == CFG_TOK_QUOTE) { + quotes++; + continue; + } + if(i < len - 1) buff[i++] = val; + } + return false; +} +char ConfigReader::readChar(const char defVal) { + uint8_t ch; + if(this->next(ch)) return (char)ch; + return defVal; +} +int8_t ConfigReader::readInt8(const int8_t defVal) { + char buff[5]; + if(this->readString(buff, sizeof(buff))) return static_cast(atoi(buff)); + return defVal; +} +uint8_t ConfigReader::readUInt8(const uint8_t defVal) { + char buff[4]; + if(this->readString(buff, sizeof(buff))) return static_cast(atoi(buff)); + return defVal; +} +uint16_t ConfigReader::readUInt16(const uint16_t defVal) { + char buff[6]; + if(this->readString(buff, sizeof(buff))) return static_cast(atoi(buff)); + return defVal; +} +uint32_t ConfigReader::readUInt32(const uint32_t defVal) { + char buff[11]; + if(this->readString(buff, sizeof(buff))) return static_cast(strtoul(buff, nullptr, 10)); + return defVal; +} +float ConfigReader::readFloat(const float defVal) { + char buff[25]; + if(this->readString(buff, sizeof(buff))) return atof(buff); + return defVal; +} +bool ConfigReader::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 ConfigWriter::writeChar(const char val) { + const uint8_t b = static_cast(val); + return this->writeBytes(&b, 1); +} +bool ConfigWriter::writeString(const char *val, size_t len, const char tok) { + size_t slen = strlen(val); + if(slen > 0 && !this->writeBytes(reinterpret_cast(val), slen)) return false; + // Pad the value to its fixed width. + while(slen + 1 < len) { + if(!this->writeChar(' ')) return false; + slen++; + } + if(tok != CFG_TOK_NONE) return this->writeChar(tok); + return true; +} +bool ConfigWriter::writeVarString(const char *val, const char tok) { + this->writeChar(CFG_TOK_QUOTE); + const size_t slen = strlen(val); + if(slen > 0 && !this->writeBytes(reinterpret_cast(val), slen)) return false; + this->writeChar(CFG_TOK_QUOTE); + if(tok != CFG_TOK_NONE) return this->writeChar(tok); + return true; +} +bool ConfigWriter::writeInt8(const int8_t val, const char tok) { + char buff[5]; + snprintf(buff, sizeof(buff), "%4d", val); + return this->writeString(buff, sizeof(buff), tok); +} +bool ConfigWriter::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 ConfigWriter::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 ConfigWriter::writeUInt32(const uint32_t val, const char tok) { + char buff[11]; + snprintf(buff, sizeof(buff), "%10u", static_cast(val)); + return this->writeString(buff, sizeof(buff), tok); +} +bool ConfigWriter::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 ConfigWriter::writeBool(const bool val, const char tok) { + return this->writeString(val ? "true" : "false", 6, tok); +} + +bool writeConfigHeader(ConfigWriter &w, const config_header_t &header) { + w.writeUInt8(header.version); + w.writeUInt8(header.length); + w.writeUInt16(header.roomRecordSize); + w.writeUInt8(header.roomRecords); + w.writeUInt16(header.shadeRecordSize); + w.writeUInt8(header.shadeRecords); + w.writeUInt16(header.groupRecordSize); + w.writeUInt8(header.groupRecords); + w.writeUInt16(header.repeaterRecordSize); + w.writeUInt8(header.repeaterRecords); + w.writeUInt16(header.settingsRecordSize); + w.writeUInt16(header.netRecordSize); + w.writeUInt16(header.transRecordSize); + return w.writeString(header.serverId, sizeof(header.serverId), CFG_REC_END); +} +bool readConfigHeader(ConfigReader &r, config_header_t &header) { + r.beginRecord(); + header.version = r.readUInt8(header.version); + header.length = r.readUInt8(0); + if(header.version >= 19) { + header.roomRecordSize = r.readUInt16(header.roomRecordSize); + header.roomRecords = r.readUInt8(header.roomRecords); + } + if(header.version >= 13) header.shadeRecordSize = r.readUInt16(header.shadeRecordSize); + else header.shadeRecordSize = r.readUInt8(static_cast(header.shadeRecordSize)); + header.shadeRecords = r.readUInt8(header.shadeRecords); + if(header.version > 10) { + if(header.version >= 13) header.groupRecordSize = r.readUInt16(header.groupRecordSize); + else header.groupRecordSize = r.readUInt8(header.groupRecordSize); + header.groupRecords = r.readUInt8(header.groupRecords); + } + if(header.version >= 21) { + header.repeaterRecordSize = r.readUInt16(header.repeaterRecordSize); + header.repeaterRecords = r.readUInt8(header.repeaterRecords); + } + if(header.version > 13) { + header.settingsRecordSize = r.readUInt16(header.settingsRecordSize); + header.netRecordSize = r.readUInt16(header.netRecordSize); + header.transRecordSize = r.readUInt16(header.transRecordSize); + r.readString(header.serverId, sizeof(header.serverId)); + } + return true; +} + +bool writeRoomRecord(ConfigWriter &w, const room_record_t &rec) { + w.writeUInt8(rec.roomId); + w.writeString(rec.name, sizeof(rec.name)); + return w.writeUInt8(static_cast(rec.sortOrder), CFG_REC_END); +} +bool readRoomRecord(ConfigReader &r, const config_header_t &header, room_record_t &rec) { + (void)header; + r.beginRecord(); + rec.roomId = r.readUInt8(0); + r.readFixedString(rec.name, sizeof(rec.name)); + rec.sortOrder = static_cast(r.readUInt8(rec.roomId - 1)); + r.endRecord(); + return true; +} + +bool writeShadeRecord(ConfigWriter &w, const shade_record_t &rec) { + w.writeUInt8(rec.shadeId); + w.writeBool(rec.paired); + w.writeUInt8(static_cast(rec.shadeType)); + w.writeUInt32(rec.remoteAddress); + w.writeString(rec.name, sizeof(rec.name)); + w.writeUInt8(static_cast(rec.tiltType)); + w.writeUInt8(static_cast(rec.proto)); + w.writeUInt8(rec.bitLength); + w.writeUInt32(rec.upTime); + w.writeUInt32(rec.downTime); + w.writeUInt32(rec.tiltTime); + w.writeUInt16(rec.stepSize); + for(uint8_t j = 0; j < SOMFY_MAX_LINKED_REMOTES; j++) w.writeUInt32(rec.linkedRemotes[j]); + w.writeUInt16(rec.lastRollingCode); + if(rec.shadeId != 255) { + w.writeUInt8(rec.flags); + w.writeFloat(rec.myPos, 5); + w.writeFloat(rec.myTiltPos, 5); + w.writeFloat(rec.currentPos, 5); + w.writeFloat(rec.currentTiltPos, 5); + } + else { + // A deleted shade is written cleared. + w.writeUInt8(0); + w.writeFloat(-1.0f, 5); + w.writeFloat(-1.0f, 5); + w.writeFloat(0.0f, 5); + w.writeFloat(0.0f, 5); + } + w.writeBool(rec.flipCommands); + w.writeBool(rec.flipPosition); + w.writeUInt8(rec.repeats); + w.writeUInt8(static_cast(rec.sortOrder)); + w.writeUInt8(rec.gpioUp); + w.writeUInt8(rec.gpioDown); + w.writeUInt8(rec.gpioMy); + w.writeUInt8(rec.gpioFlags); + return w.writeUInt8(rec.roomId, CFG_REC_END); +} +bool readShadeRecord(ConfigReader &r, const config_header_t &header, shade_record_t &rec) { + const uint8_t ver = header.version; + r.beginRecord(); + rec.shadeId = r.readUInt8(255); + rec.paired = r.readBool(false); + rec.shadeType = static_cast(r.readUInt8(0)); + rec.remoteAddress = r.readUInt32(0); + r.readFixedString(rec.name, sizeof(rec.name)); + if(ver < 3) rec.tiltType = r.readBool(false) ? tilt_types::none : tilt_types::tiltmotor; + else rec.tiltType = static_cast(r.readUInt8(0)); + if(ver > 6) rec.proto = static_cast(r.readUInt8(0)); + if(ver > 1) rec.bitLength = r.readUInt8(56); + rec.upTime = r.readUInt32(rec.upTime); + rec.downTime = r.readUInt32(rec.downTime); + rec.tiltTime = r.readUInt32(rec.tiltTime); + if(ver > 5) rec.stepSize = r.readUInt16(100); + // Before version 5 a shade had 5 linked remotes. + const uint8_t remotes = ver < 5 ? 5 : SOMFY_MAX_LINKED_REMOTES; + for(uint8_t j = 0; j < remotes; j++) rec.linkedRemotes[j] = r.readUInt32(0); + rec.lastRollingCode = r.readUInt16(0); + if(ver > 7) rec.flags = r.readUInt8(0); + if(ver < 4) rec.myPos = static_cast(r.readUInt8(255)); + else { + rec.myPos = r.readFloat(-1); + rec.myTiltPos = r.readFloat(-1); + } + if(rec.myPos > 100 || rec.myPos < 0) rec.myPos = -1; + if(rec.myTiltPos > 100 || rec.myTiltPos < 0) rec.myTiltPos = -1; + rec.currentPos = r.readFloat(0); + rec.currentTiltPos = r.readFloat(0); + if(rec.tiltType == tilt_types::none || rec.shadeType != shade_types::blind) { + rec.myTiltPos = -1; + rec.currentTiltPos = 0; + rec.tiltType = tilt_types::none; + } + if(ver < 3) { + // Positions were fractions. + rec.currentPos = rec.currentPos * 100; + rec.currentTiltPos = rec.currentTiltPos * 100; + } + if(ver >= 9) rec.flipCommands = r.readBool(false); + if(ver >= 10) rec.flipPosition = r.readBool(false); + if(ver >= 12) rec.repeats = r.readUInt8(1); + if(ver >= 13) rec.sortOrder = static_cast(r.readUInt8(rec.shadeId - 1)); + else rec.sortOrder = static_cast(rec.shadeId - 1); + if(ver > 14) { + rec.gpioUp = r.readUInt8(rec.gpioUp); + rec.gpioDown = r.readUInt8(rec.gpioDown); + } + if(ver > 15) rec.gpioMy = r.readUInt8(rec.gpioMy); + if(ver > 16) rec.gpioFlags = r.readUInt8(rec.gpioFlags); + if(rec.shadeId != 255 && rec.tiltType == tilt_types::tiltonly) { + // A tilt only shade is always all the way down. + rec.currentPos = 100.0f; + rec.myPos = 100.0f; + } + if(ver >= 19) rec.roomId = r.readUInt8(0); + r.endRecord(); + return true; +} + +bool writeGroupRecord(ConfigWriter &w, const group_record_t &rec) { + w.writeUInt8(rec.groupId); + w.writeUInt8(static_cast(rec.groupType)); + w.writeUInt32(rec.remoteAddress); + w.writeString(rec.name, sizeof(rec.name)); + w.writeUInt8(static_cast(rec.proto)); + w.writeUInt8(rec.bitLength); + for(uint8_t j = 0; j < SOMFY_MAX_GROUPED_SHADES; j++) w.writeUInt8(rec.linkedShades[j]); + w.writeUInt8(rec.repeats); + w.writeUInt8(static_cast(rec.sortOrder)); + w.writeBool(rec.flipCommands); + w.writeUInt8(rec.roomId); + return w.writeUInt16(rec.lastRollingCode, CFG_REC_END); +} +bool readGroupRecord(ConfigReader &r, const config_header_t &header, group_record_t &rec) { + const uint8_t ver = header.version; + r.beginRecord(); + rec.groupId = r.readUInt8(255); + rec.groupType = static_cast(r.readUInt8(0)); + rec.remoteAddress = r.readUInt32(0); + r.readFixedString(rec.name, sizeof(rec.name)); + rec.proto = static_cast(r.readUInt8(0)); + rec.bitLength = r.readUInt8(56); + // Version 23 put the rolling code here, version 24 at the end. + if(ver == 23) rec.lastRollingCode = r.readUInt16(0); + memset(rec.linkedShades, 0x00, sizeof(rec.linkedShades)); + uint8_t linked = 0; + for(uint8_t j = 0; j < SOMFY_MAX_GROUPED_SHADES; j++) { + const uint8_t shadeId = r.readUInt8(0); + if(shadeId > 0) rec.linkedShades[linked++] = shadeId; + } + if(ver >= 12) rec.repeats = r.readUInt8(1); + if(ver >= 13) rec.sortOrder = static_cast(r.readUInt8(rec.groupId - 1)); + else rec.sortOrder = static_cast(rec.groupId - 1); + if(ver >= 18) rec.flipCommands = r.readBool(false); + if(ver >= 19) rec.roomId = r.readUInt8(0); + if(ver >= 24) rec.lastRollingCode = r.readUInt16(0); + r.endRecord(); + return true; +} + +bool writeRepeaterRecord(ConfigWriter &w, const uint32_t (&repeaters)[SOMFY_MAX_REPEATERS]) { + for(uint8_t i = 0; i < SOMFY_MAX_REPEATERS; i++) + w.writeUInt32(repeaters[i], i == SOMFY_MAX_REPEATERS - 1 ? CFG_REC_END : CFG_VALUE_SEP); + return true; +} +bool readRepeaterRecord(ConfigReader &r, uint32_t (&repeaters)[SOMFY_MAX_REPEATERS]) { + r.beginRecord(); + for(uint8_t i = 0; i < SOMFY_MAX_REPEATERS; i++) repeaters[i] = r.readUInt32(0); + r.endRecord(); + return true; +} + +bool writeSettingsRecord(ConfigWriter &w, const settings_record_t &rec) { + w.writeVarString(rec.fwVersion); + w.writeVarString(rec.hostname); + w.writeVarString(rec.ntpServer); + w.writeVarString(rec.posixZone); + w.writeBool(rec.ssdpBroadcast); + return w.writeBool(rec.checkForUpdate, CFG_REC_END); +} +bool readSettingsRecord(ConfigReader &r, const config_header_t &header, settings_record_t &rec) { + r.beginRecord(); + r.readVarString(rec.fwVersion, sizeof(rec.fwVersion)); + r.readVarString(rec.hostname, sizeof(rec.hostname)); + r.readVarString(rec.ntpServer, sizeof(rec.ntpServer)); + r.readVarString(rec.posixZone, sizeof(rec.posixZone)); + rec.ssdpBroadcast = r.readBool(false); + if(header.version >= 20) rec.checkForUpdate = r.readBool(true); + r.endRecord(); + return true; +} + +bool writeNetRecord(ConfigWriter &w, const net_record_t &rec, bool withEthernet) { + w.writeUInt8(rec.connType); + w.writeBool(rec.dhcp); + w.writeVarString(rec.ip); + w.writeVarString(rec.gateway); + w.writeVarString(rec.subnet); + w.writeVarString(rec.dns1); + w.writeVarString(rec.dns2); + w.writeVarString(rec.mqttProtocol); + w.writeVarString(rec.mqttHostname); + w.writeUInt16(rec.mqttPort); + w.writeBool(rec.mqttPubDisco); + w.writeVarString(rec.mqttRootTopic); + w.writeVarString(rec.mqttDiscoTopic); + if(!withEthernet) return w.writeUInt8(0, CFG_REC_END); + w.writeUInt8(rec.ethBoardType); + w.writeUInt8(rec.ethPhyType); + w.writeUInt8(rec.ethClkMode); + w.writeInt8(rec.ethPhyAddress); + w.writeInt8(rec.ethPWRPin); + w.writeInt8(rec.ethMDCPin); + return w.writeInt8(rec.ethMDIOPin, CFG_REC_END); +} +bool readNetRecord(ConfigReader &r, const config_header_t &header, net_record_t &rec, bool withEthernet) { + r.beginRecord(); + rec.connType = r.readUInt8(0); + rec.dhcp = r.readBool(true); + r.readVarString(rec.ip, sizeof(rec.ip)); + r.readVarString(rec.gateway, sizeof(rec.gateway)); + r.readVarString(rec.subnet, sizeof(rec.subnet)); + r.readVarString(rec.dns1, sizeof(rec.dns1)); + r.readVarString(rec.dns2, sizeof(rec.dns2)); + if(header.version >= 22) { + r.readVarString(rec.mqttProtocol, sizeof(rec.mqttProtocol)); + r.readVarString(rec.mqttHostname, sizeof(rec.mqttHostname)); + rec.mqttPort = r.readUInt16(1883); + rec.mqttPubDisco = r.readBool(false); + r.readVarString(rec.mqttRootTopic, sizeof(rec.mqttRootTopic)); + r.readVarString(rec.mqttDiscoTopic, sizeof(rec.mqttDiscoTopic)); + } + if(withEthernet) { + rec.ethBoardType = r.readUInt8(1); + rec.ethPhyType = r.readUInt8(0); + rec.ethClkMode = r.readUInt8(0); + rec.ethPhyAddress = r.readInt8(1); + rec.ethPWRPin = r.readInt8(1); + rec.ethMDCPin = r.readInt8(16); + rec.ethMDIOPin = r.readInt8(23); + } + r.endRecord(); + return true; +} + +bool writeTransRecord(ConfigWriter &w, const trans_record_t &rec) { + w.writeBool(rec.enabled); + w.writeUInt8(static_cast(rec.proto)); + w.writeUInt8(rec.type); + w.writeUInt8(rec.SCKPin); + w.writeUInt8(rec.CSNPin); + w.writeUInt8(rec.MOSIPin); + w.writeUInt8(rec.MISOPin); + w.writeUInt8(rec.TXPin); + w.writeUInt8(rec.RXPin); + w.writeFloat(rec.frequency, 3); + w.writeFloat(rec.rxBandwidth, 2); + w.writeFloat(rec.deviation, 2); + w.writeInt8(rec.txPower); + return w.writeBool(rec.noiseDetection, CFG_REC_END); +} +bool readTransRecord(ConfigReader &r, trans_record_t &rec) { + r.beginRecord(); + rec.enabled = r.readBool(false); + rec.proto = static_cast(r.readUInt8(0)); + rec.type = r.readUInt8(56); + rec.SCKPin = r.readUInt8(rec.SCKPin); + rec.CSNPin = r.readUInt8(rec.CSNPin); + rec.MOSIPin = r.readUInt8(rec.MOSIPin); + rec.MISOPin = r.readUInt8(rec.MISOPin); + rec.TXPin = r.readUInt8(rec.TXPin); + rec.RXPin = r.readUInt8(rec.RXPin); + rec.frequency = r.readFloat(rec.frequency); + rec.rxBandwidth = r.readFloat(rec.rxBandwidth); + rec.deviation = r.readFloat(rec.deviation); + rec.txPower = r.readInt8(rec.txPower); + rec.noiseDetection = r.readBool(false); + r.endRecord(); + return true; +} + +bool skipRecords(ConfigReader &r, uint32_t count) { + for(uint32_t i = 0; i < count; i++) { + r.beginRecord(); + if(!r.seekChar(CFG_REC_END)) return false; + } + return true; +} diff --git a/src/ConfigRecords.h b/src/ConfigRecords.h new file mode 100644 index 0000000..462c367 --- /dev/null +++ b/src/ConfigRecords.h @@ -0,0 +1,222 @@ +#ifndef CONFIG_RECORDS_H +#define CONFIG_RECORDS_H +#include +#include "ConfigHeader.h" +#include "ShadeTypes.h" +#include "SomfyFrame.h" + +// The records of shades.cfg and of a backup, and how they are written and +// read. The file is text: each value is padded to a fixed width and followed +// by a comma, strings that vary in length are quoted, and each record ends +// with a newline. Older versions of the file lack the values added since, +// and read as their defaults. Kept free of the file system and the shade +// model so every version can be unit tested on the PC (pio test -e native). + +#define CFG_VALUE_SEP ',' +#define CFG_REC_END '\n' +#define CFG_TOK_NONE 0x00 +#define CFG_TOK_QUOTE '"' + +#define SHADE_HDR_VER 24 +#define SHADE_HDR_SIZE 76 +#define SHADE_REC_SIZE 276 +#define GROUP_REC_SIZE 200 +#define ROOM_REC_SIZE 29 +#define REPEATER_REC_SIZE 77 +// Files written before 4.0 declare the transceiver record 74 bytes: it was +// left there when noiseDetection was added. +#define TRANS_REC_SIZE 80 + +// The sizes of the strings, which are those of the fields they are read into. +#define CFG_NAME_LEN 21 +#define CFG_VERSION_LEN 24 +#define CFG_HOSTNAME_LEN 32 +#define CFG_NTP_SERVER_LEN 65 +#define CFG_POSIX_ZONE_LEN 64 +#define CFG_IP_LEN 24 +#define CFG_MQTT_PROTOCOL_LEN 10 +#define CFG_MQTT_HOSTNAME_LEN 65 +#define CFG_MQTT_TOPIC_LEN 65 + +// Reads the values of a file. A record's values past its end read as their +// defaults, so a record shorter than expected never reads into the next one. +class ConfigReader { + private: + uint8_t _last = 0; + bool next(uint8_t &val); + public: + virtual ~ConfigReader() {} + // The next byte of the file; false at its end. + virtual bool readByte(uint8_t &val) = 0; + virtual uint32_t position() = 0; + void beginRecord() { _last = 0; } + // Moves past the rest of the record: the values a newer version added. + void endRecord(); + bool atRecordEnd() const { return _last == CFG_REC_END; } + bool seekChar(const char val); + // A value up to the next separator, cut to fit buff. + bool readString(char *buff, size_t len); + // A value padded to len - 1 bytes, read whole so it may hold commas: a + // name. + bool readFixedString(char *buff, size_t len); + // A quoted value of any length, cut to fit buff. + bool readVarString(char *buff, size_t len); + char readChar(const char defVal = '\0'); + int8_t readInt8(const int8_t defVal = 0); + uint8_t readUInt8(const uint8_t defVal = 0); + uint16_t readUInt16(const uint16_t defVal = 0); + uint32_t readUInt32(const uint32_t defVal = 0); + bool readBool(const bool defVal = false); + float readFloat(const float defVal = 0.00); +}; +class ConfigWriter { + public: + virtual ~ConfigWriter() {} + virtual bool writeBytes(const uint8_t *buf, size_t len) = 0; + bool writeChar(const char val); + bool writeString(const char *val, size_t len, const char tok = CFG_VALUE_SEP); + bool writeVarString(const char *val, const char tok = CFG_VALUE_SEP); + bool writeInt8(const int8_t val, const char tok = CFG_VALUE_SEP); + bool writeUInt8(const uint8_t val, const char tok = CFG_VALUE_SEP); + bool writeUInt16(const uint16_t val, const char tok = CFG_VALUE_SEP); + bool writeUInt32(const uint32_t val, const char tok = CFG_VALUE_SEP); + bool writeBool(const bool val, const char tok = CFG_VALUE_SEP); + bool writeFloat(const float val, const uint8_t prec, const char tok = CFG_VALUE_SEP); +}; +// Counts what would be written: the size of a record that varies in length. +class ConfigCounter : public ConfigWriter { + public: + uint32_t bytes = 0; + bool writeBytes(const uint8_t *buf, size_t len) override { (void)buf; bytes += len; return true; } +}; + +struct room_record_t { + uint8_t roomId = 0; + char name[CFG_NAME_LEN] = ""; + int8_t sortOrder = 0; +}; +struct shade_record_t { + uint8_t shadeId = 255; + bool paired = false; + shade_types shadeType = shade_types::roller; + uint32_t remoteAddress = 0; + char name[CFG_NAME_LEN] = ""; + tilt_types tiltType = tilt_types::none; + radio_proto proto = radio_proto::RTS; + uint8_t bitLength = 56; + uint32_t upTime = 10000; + uint32_t downTime = 10000; + uint32_t tiltTime = 7000; + uint16_t stepSize = 100; + uint32_t linkedRemotes[SOMFY_MAX_LINKED_REMOTES] = {}; + uint16_t lastRollingCode = 0; + uint8_t flags = 0; + float myPos = -1; + float myTiltPos = -1; + float currentPos = 0; + float currentTiltPos = 0; + bool flipCommands = false; + bool flipPosition = false; + uint8_t repeats = 1; + int8_t sortOrder = 0; + uint8_t gpioUp = 0; + uint8_t gpioDown = 0; + uint8_t gpioMy = 0; + uint8_t gpioFlags = 0; + uint8_t roomId = 0; +}; +struct group_record_t { + uint8_t groupId = 255; + group_types groupType = group_types::channel; + uint32_t remoteAddress = 0; + char name[CFG_NAME_LEN] = ""; + radio_proto proto = radio_proto::RTS; + uint8_t bitLength = 56; + // Read without the gaps the file may have. + uint8_t linkedShades[SOMFY_MAX_GROUPED_SHADES] = {}; + uint8_t repeats = 1; + int8_t sortOrder = 0; + bool flipCommands = false; + uint8_t roomId = 0; + uint16_t lastRollingCode = 0; +}; +struct settings_record_t { + // The firmware that wrote the backup. Not restored. + char fwVersion[CFG_VERSION_LEN] = ""; + char hostname[CFG_HOSTNAME_LEN] = ""; + char ntpServer[CFG_NTP_SERVER_LEN] = ""; + char posixZone[CFG_POSIX_ZONE_LEN] = ""; + bool ssdpBroadcast = false; + bool checkForUpdate = true; +}; +struct net_record_t { + uint8_t connType = 0; + bool dhcp = true; + char ip[CFG_IP_LEN] = ""; + char gateway[CFG_IP_LEN] = ""; + char subnet[CFG_IP_LEN] = ""; + char dns1[CFG_IP_LEN] = ""; + char dns2[CFG_IP_LEN] = ""; + char mqttProtocol[CFG_MQTT_PROTOCOL_LEN] = ""; + char mqttHostname[CFG_MQTT_HOSTNAME_LEN] = ""; + uint16_t mqttPort = 1883; + bool mqttPubDisco = false; + char mqttRootTopic[CFG_MQTT_TOPIC_LEN] = ""; + char mqttDiscoTopic[CFG_MQTT_TOPIC_LEN] = ""; + // Only boards with Ethernet write these, and only the board that wrote + // them reads them back. + uint8_t ethBoardType = 1; + uint8_t ethPhyType = 0; + uint8_t ethClkMode = 0; + int8_t ethPhyAddress = 1; + int8_t ethPWRPin = 1; + int8_t ethMDCPin = 16; + int8_t ethMDIOPin = 23; +}; +struct trans_record_t { + bool enabled = false; + radio_proto proto = radio_proto::RTS; + uint8_t type = 56; + uint8_t SCKPin = 18; + uint8_t CSNPin = 5; + uint8_t MOSIPin = 23; + uint8_t MISOPin = 19; + uint8_t TXPin = 13; + uint8_t RXPin = 12; + float frequency = 433.42; + float rxBandwidth = 99.97; + float deviation = 47.60; + int8_t txPower = 10; + bool noiseDetection = false; +}; + +bool writeConfigHeader(ConfigWriter &w, const config_header_t &header); +bool readConfigHeader(ConfigReader &r, config_header_t &header); +bool writeRoomRecord(ConfigWriter &w, const room_record_t &rec); +bool readRoomRecord(ConfigReader &r, const config_header_t &header, room_record_t &rec); +bool writeShadeRecord(ConfigWriter &w, const shade_record_t &rec); +bool readShadeRecord(ConfigReader &r, const config_header_t &header, shade_record_t &rec); +bool writeGroupRecord(ConfigWriter &w, const group_record_t &rec); +bool readGroupRecord(ConfigReader &r, const config_header_t &header, group_record_t &rec); +bool writeRepeaterRecord(ConfigWriter &w, const uint32_t (&repeaters)[SOMFY_MAX_REPEATERS]); +bool readRepeaterRecord(ConfigReader &r, uint32_t (&repeaters)[SOMFY_MAX_REPEATERS]); +bool writeSettingsRecord(ConfigWriter &w, const settings_record_t &rec); +bool readSettingsRecord(ConfigReader &r, const config_header_t &header, settings_record_t &rec); +// withEthernet: the Ethernet settings, which a board without Ethernet leaves out. +bool writeNetRecord(ConfigWriter &w, const net_record_t &rec, bool withEthernet); +bool readNetRecord(ConfigReader &r, const config_header_t &header, net_record_t &rec, bool withEthernet); +bool writeTransRecord(ConfigWriter &w, const trans_record_t &rec); +bool readTransRecord(ConfigReader &r, trans_record_t &rec); +// Moves past count records without reading them. +bool skipRecords(ConfigReader &r, uint32_t count); + +// The rolling code a remote goes on with. NVS holds the last code sent and +// shades.cfg the code when it was saved, so a load keeps the higher. A +// restore takes the backup's code even when it is lower. A motor ignores +// codes below the last one it heard, so after restoring an older backup to +// the same device it ignores the shade until the codes pass that one. +inline uint16_t rollingCodeAfterLoad(uint16_t stored, uint16_t inFile, bool restoring) { + if(restoring) return inFile; + return stored > inFile ? stored : inFile; +} +#endif diff --git a/src/ConfigSettings.cpp b/src/ConfigSettings.cpp index 42def2d..88776d3 100644 --- a/src/ConfigSettings.cpp +++ b/src/ConfigSettings.cpp @@ -226,36 +226,6 @@ void ConfigSettings::print() { if(this->connType == conn_types_t::ethernet || this->connType == conn_types_t::ethernetpref) this->Ethernet.print(); #endif } -uint16_t ConfigSettings::calcSettingsRecSize() { - return strlen(this->fwVersion.name) + 3 - + strlen(this->hostname) + 3 - + strlen(this->NTP.ntpServer) + 3 - + strlen(this->NTP.posixZone) + 3 - + 6 // ssdpbroadcast - + 6; // updateCheck -} -uint16_t ConfigSettings::calcNetRecSize() { - return 4 // connType - + 6 // dhcp - + this->IP.ip.toString().length() + 3 - + this->IP.gateway.toString().length() + 3 - + this->IP.subnet.toString().length() + 3 - + this->IP.dns1.toString().length() + 3 - + this->IP.dns2.toString().length() + 3 - + strlen(this->MQTT.protocol) + 3 - + strlen(this->MQTT.hostname) + 3 - + 6 // MQTT Port - + 6 // PubDisco - + strlen(this->MQTT.rootTopic) + 3 - + strlen(this->MQTT.discoTopic) + 3 - + 4 // ETH.boardType - + 4 // ETH.phyType - + 4 // ETH.clkMode - + 5 // ETH.phyAddress - + 5 // ETH.PWRPin - + 5 // ETH.MDCPin - + 5; // ETH.MDIOPin -} bool MQTTSettings::begin() { this->load(); return true; diff --git a/src/ConfigSettings.h b/src/ConfigSettings.h index 043ebe4..6434c3b 100644 --- a/src/ConfigSettings.h +++ b/src/ConfigSettings.h @@ -186,8 +186,6 @@ class ConfigSettings: BaseSettings { bool save(); bool load(); void print(); - uint16_t calcSettingsRecSize(); - uint16_t calcNetRecSize(); bool getAppVersion(); }; #endif diff --git a/test/test_configrecords/test_configrecords.cpp b/test/test_configrecords/test_configrecords.cpp new file mode 100644 index 0000000..c8e741f --- /dev/null +++ b/test/test_configrecords/test_configrecords.cpp @@ -0,0 +1,799 @@ +#include +#include +#include +#include +#include "ConfigRecords.h" + +void setUp() {} +void tearDown() {} + +// A file in memory. +class TextWriter : public ConfigWriter { + public: + std::string text; + bool writeBytes(const uint8_t *buf, size_t len) override { text.append(reinterpret_cast(buf), len); return true; } +}; +class TextReader : public ConfigReader { + public: + std::string text; + size_t pos = 0; + explicit TextReader(const std::string &t) : text(t) {} + bool readByte(uint8_t &val) override { + if(pos >= text.size()) return false; + val = static_cast(text[pos++]); + return true; + } + uint32_t position() override { return static_cast(pos); } +}; + +static config_header_t header(uint8_t version = SHADE_HDR_VER) { + config_header_t h; + h.version = version; + return h; +} +static shade_record_t aShade() { + shade_record_t rec; + rec.shadeId = 7; + rec.paired = true; + rec.shadeType = shade_types::blind; + rec.remoteAddress = 0x123456; + strcpy(rec.name, "Kitchen"); + rec.tiltType = tilt_types::tiltmotor; + rec.proto = radio_proto::GP_Remote; + rec.bitLength = 80; + rec.upTime = 21000; + rec.downTime = 19500; + rec.tiltTime = 3200; + rec.stepSize = 250; + for(uint8_t j = 0; j < SOMFY_MAX_LINKED_REMOTES; j++) rec.linkedRemotes[j] = 0x200000 + j; + rec.lastRollingCode = 65000; + rec.flags = 0x21; + rec.myPos = 42.5f; + rec.myTiltPos = 12.25f; + rec.currentPos = 66.0f; + rec.currentTiltPos = 33.5f; + rec.flipCommands = true; + rec.flipPosition = true; + rec.repeats = 4; + rec.sortOrder = 3; + rec.gpioUp = 25; + rec.gpioDown = 26; + rec.gpioMy = 27; + rec.gpioFlags = 2; + rec.roomId = 5; + return rec; +} +static group_record_t aGroup() { + group_record_t rec; + rec.groupId = 3; + rec.remoteAddress = 0x654321; + strcpy(rec.name, "Upstairs"); + rec.proto = radio_proto::RTS; + rec.bitLength = 80; + rec.linkedShades[0] = 1; + rec.linkedShades[1] = 7; + rec.linkedShades[2] = 32; + rec.repeats = 2; + rec.sortOrder = 9; + rec.flipCommands = true; + rec.roomId = 4; + rec.lastRollingCode = 1234; + return rec; +} +static net_record_t aNet() { + net_record_t rec; + rec.connType = 2; + rec.dhcp = false; + strcpy(rec.ip, "192.168.100.200"); + strcpy(rec.gateway, "192.168.100.1"); + strcpy(rec.subnet, "255.255.255.0"); + strcpy(rec.dns1, "1.1.1.1"); + strcpy(rec.dns2, "8.8.8.8"); + strcpy(rec.mqttProtocol, "mqtts://"); + strcpy(rec.mqttHostname, "broker.local"); + rec.mqttPort = 8883; + rec.mqttPubDisco = true; + strcpy(rec.mqttRootTopic, "somfy"); + strcpy(rec.mqttDiscoTopic, "ha"); + rec.ethBoardType = 3; + rec.ethPhyType = 1; + rec.ethClkMode = 3; + rec.ethPhyAddress = -1; + rec.ethPWRPin = 12; + rec.ethMDCPin = 23; + rec.ethMDIOPin = 18; + return rec; +} +static trans_record_t aTrans() { + trans_record_t rec; + rec.enabled = true; + rec.type = 80; + rec.SCKPin = 4; + rec.CSNPin = 7; + rec.MOSIPin = 6; + rec.MISOPin = 5; + rec.TXPin = 10; + rec.RXPin = 3; + rec.frequency = 433.25f; + rec.rxBandwidth = 812.5f; + rec.deviation = 1.58f; + rec.txPower = -30; + rec.noiseDetection = true; + return rec; +} +static void assertShadesEqual(const shade_record_t &want, const shade_record_t &got) { + TEST_ASSERT_EQUAL_UINT8(want.shadeId, got.shadeId); + TEST_ASSERT_EQUAL(want.paired, got.paired); + TEST_ASSERT_EQUAL_UINT8(static_cast(want.shadeType), static_cast(got.shadeType)); + TEST_ASSERT_EQUAL_HEX32(want.remoteAddress, got.remoteAddress); + TEST_ASSERT_EQUAL_STRING(want.name, got.name); + TEST_ASSERT_EQUAL_UINT8(static_cast(want.tiltType), static_cast(got.tiltType)); + TEST_ASSERT_EQUAL_UINT8(static_cast(want.proto), static_cast(got.proto)); + TEST_ASSERT_EQUAL_UINT8(want.bitLength, got.bitLength); + TEST_ASSERT_EQUAL_UINT32(want.upTime, got.upTime); + TEST_ASSERT_EQUAL_UINT32(want.downTime, got.downTime); + TEST_ASSERT_EQUAL_UINT32(want.tiltTime, got.tiltTime); + TEST_ASSERT_EQUAL_UINT16(want.stepSize, got.stepSize); + TEST_ASSERT_EQUAL_HEX32_ARRAY(want.linkedRemotes, got.linkedRemotes, SOMFY_MAX_LINKED_REMOTES); + TEST_ASSERT_EQUAL_UINT16(want.lastRollingCode, got.lastRollingCode); + TEST_ASSERT_EQUAL_HEX8(want.flags, got.flags); + TEST_ASSERT_EQUAL_FLOAT(want.myPos, got.myPos); + TEST_ASSERT_EQUAL_FLOAT(want.myTiltPos, got.myTiltPos); + TEST_ASSERT_EQUAL_FLOAT(want.currentPos, got.currentPos); + TEST_ASSERT_EQUAL_FLOAT(want.currentTiltPos, got.currentTiltPos); + TEST_ASSERT_EQUAL(want.flipCommands, got.flipCommands); + TEST_ASSERT_EQUAL(want.flipPosition, got.flipPosition); + TEST_ASSERT_EQUAL_UINT8(want.repeats, got.repeats); + TEST_ASSERT_EQUAL_INT8(want.sortOrder, got.sortOrder); + TEST_ASSERT_EQUAL_UINT8(want.gpioUp, got.gpioUp); + TEST_ASSERT_EQUAL_UINT8(want.gpioDown, got.gpioDown); + TEST_ASSERT_EQUAL_UINT8(want.gpioMy, got.gpioMy); + TEST_ASSERT_EQUAL_UINT8(want.gpioFlags, got.gpioFlags); + TEST_ASSERT_EQUAL_UINT8(want.roomId, got.roomId); +} + +// Every record is the size the header says, or validate() refuses the file. +static void test_each_record_is_the_size_the_header_declares() { + config_header_t h = header(); + h.length = SHADE_HDR_SIZE; + strcpy(h.serverId, "a1b2c3d4"); + TextWriter w; + writeConfigHeader(w, h); + TEST_ASSERT_EQUAL(SHADE_HDR_SIZE, w.text.size()); + + room_record_t room; + room.roomId = 16; + strcpy(room.name, "12345678901234567890"); + w.text.clear(); + writeRoomRecord(w, room); + TEST_ASSERT_EQUAL(ROOM_REC_SIZE, w.text.size()); + + w.text.clear(); + writeShadeRecord(w, aShade()); + TEST_ASSERT_EQUAL(SHADE_REC_SIZE, w.text.size()); + + w.text.clear(); + writeGroupRecord(w, aGroup()); + TEST_ASSERT_EQUAL(GROUP_REC_SIZE, w.text.size()); + + uint32_t repeaters[SOMFY_MAX_REPEATERS] = {0xFFFFFF, 1}; + w.text.clear(); + writeRepeaterRecord(w, repeaters); + TEST_ASSERT_EQUAL(REPEATER_REC_SIZE, w.text.size()); + + w.text.clear(); + writeTransRecord(w, aTrans()); + TEST_ASSERT_EQUAL(TRANS_REC_SIZE, w.text.size()); +} +// backup() measures the records that vary in length with a ConfigCounter. +static void test_a_counter_measures_what_a_writer_writes() { + TextWriter w; + ConfigCounter c; + writeNetRecord(w, aNet(), false); + writeNetRecord(c, aNet(), false); + TEST_ASSERT_EQUAL(w.text.size(), c.bytes); +} +static void test_a_deleted_shade_is_written_cleared() { + shade_record_t rec = aShade(); + rec.shadeId = 255; + TextWriter w; + writeShadeRecord(w, rec); + TEST_ASSERT_EQUAL(SHADE_REC_SIZE, w.text.size()); + TextReader r(w.text); + shade_record_t got; + readShadeRecord(r, header(), got); + TEST_ASSERT_EQUAL_FLOAT(-1, got.myPos); + TEST_ASSERT_EQUAL_FLOAT(0, got.currentPos); + TEST_ASSERT_EQUAL_HEX8(0, got.flags); +} + +static void test_the_header_reads_back() { + config_header_t h = header(); + h.length = SHADE_HDR_SIZE; + h.roomRecordSize = ROOM_REC_SIZE; + h.roomRecords = 3; + h.shadeRecordSize = SHADE_REC_SIZE; + h.shadeRecords = 32; + h.groupRecordSize = GROUP_REC_SIZE; + h.groupRecords = 16; + h.repeaterRecordSize = REPEATER_REC_SIZE; + h.repeaterRecords = 1; + h.settingsRecordSize = 94; + h.netRecordSize = 166; + h.transRecordSize = TRANS_REC_SIZE; + strcpy(h.serverId, "a1b2c3d4"); + TextWriter w; + writeConfigHeader(w, h); + TextReader r(w.text); + config_header_t got; + readConfigHeader(r, got); + TEST_ASSERT_EQUAL_UINT8(24, got.version); + TEST_ASSERT_EQUAL_INT8(SHADE_HDR_SIZE, got.length); + TEST_ASSERT_EQUAL_UINT8(3, got.roomRecords); + TEST_ASSERT_EQUAL_UINT8(32, got.shadeRecords); + TEST_ASSERT_EQUAL_UINT8(16, got.groupRecords); + TEST_ASSERT_EQUAL_UINT8(REPEATER_REC_SIZE, got.repeaterRecordSize); + TEST_ASSERT_EQUAL_UINT16(94, got.settingsRecordSize); + TEST_ASSERT_EQUAL_UINT16(166, got.netRecordSize); + TEST_ASSERT_EQUAL_UINT16(TRANS_REC_SIZE, got.transRecordSize); + TEST_ASSERT_EQUAL_STRING("a1b2c3d4", got.serverId); + TEST_ASSERT_EQUAL(SHADE_HDR_SIZE, r.position()); +} +static void test_a_version_12_header_has_no_rooms_repeaters_or_settings() { + // version, length, shade size and count (one byte each then), group size and count. + TextReader r("12, 20,200, 2,150, 1\n"); + config_header_t got; + readConfigHeader(r, got); + TEST_ASSERT_EQUAL_UINT8(12, got.version); + TEST_ASSERT_EQUAL_UINT16(200, got.shadeRecordSize); + TEST_ASSERT_EQUAL_UINT8(2, got.shadeRecords); + TEST_ASSERT_EQUAL_UINT16(150, got.groupRecordSize); + TEST_ASSERT_EQUAL_UINT8(1, got.groupRecords); + TEST_ASSERT_EQUAL_UINT8(0, got.roomRecords); + TEST_ASSERT_EQUAL_UINT8(0, got.repeaterRecords); + TEST_ASSERT_EQUAL_UINT16(0, got.settingsRecordSize); +} + +static void test_a_shade_reads_back() { + TextWriter w; + writeShadeRecord(w, aShade()); + TextReader r(w.text); + shade_record_t got; + readShadeRecord(r, header(), got); + assertShadesEqual(aShade(), got); + TEST_ASSERT_EQUAL(w.text.size(), r.position()); +} +static void test_a_room_reads_back() { + room_record_t room; + room.roomId = 2; + strcpy(room.name, "Living room"); + room.sortOrder = 5; + TextWriter w; + writeRoomRecord(w, room); + TextReader r(w.text); + room_record_t got; + readRoomRecord(r, header(), got); + TEST_ASSERT_EQUAL_UINT8(2, got.roomId); + TEST_ASSERT_EQUAL_STRING("Living room", got.name); + TEST_ASSERT_EQUAL_INT8(5, got.sortOrder); +} +static void test_a_group_reads_back() { + TextWriter w; + writeGroupRecord(w, aGroup()); + TextReader r(w.text); + group_record_t got; + readGroupRecord(r, header(), got); + const group_record_t want = aGroup(); + TEST_ASSERT_EQUAL_UINT8(want.groupId, got.groupId); + TEST_ASSERT_EQUAL_HEX32(want.remoteAddress, got.remoteAddress); + TEST_ASSERT_EQUAL_STRING(want.name, got.name); + TEST_ASSERT_EQUAL_UINT8(want.bitLength, got.bitLength); + TEST_ASSERT_EQUAL_UINT8_ARRAY(want.linkedShades, got.linkedShades, SOMFY_MAX_GROUPED_SHADES); + TEST_ASSERT_EQUAL_UINT8(want.repeats, got.repeats); + TEST_ASSERT_EQUAL_INT8(want.sortOrder, got.sortOrder); + TEST_ASSERT_TRUE(got.flipCommands); + TEST_ASSERT_EQUAL_UINT8(want.roomId, got.roomId); + TEST_ASSERT_EQUAL_UINT16(want.lastRollingCode, got.lastRollingCode); +} +static void test_a_group_reads_its_shades_without_gaps() { + group_record_t rec = aGroup(); + memset(rec.linkedShades, 0, sizeof(rec.linkedShades)); + rec.linkedShades[2] = 4; + rec.linkedShades[10] = 9; + TextWriter w; + writeGroupRecord(w, rec); + TextReader r(w.text); + group_record_t got; + readGroupRecord(r, header(), got); + TEST_ASSERT_EQUAL_UINT8(4, got.linkedShades[0]); + TEST_ASSERT_EQUAL_UINT8(9, got.linkedShades[1]); + TEST_ASSERT_EQUAL_UINT8(0, got.linkedShades[2]); +} +static void test_the_settings_read_back() { + settings_record_t rec; + strcpy(rec.fwVersion, "v4.0.0"); + strcpy(rec.hostname, "somfy"); + strcpy(rec.ntpServer, "time.google.com"); + strcpy(rec.posixZone, "CET-1CEST,M3.5.0,M10.5.0/3"); + rec.ssdpBroadcast = true; + rec.checkForUpdate = false; + TextWriter w; + writeSettingsRecord(w, rec); + TextReader r(w.text); + settings_record_t got; + readSettingsRecord(r, header(), got); + TEST_ASSERT_EQUAL_STRING("v4.0.0", got.fwVersion); + TEST_ASSERT_EQUAL_STRING("somfy", got.hostname); + TEST_ASSERT_EQUAL_STRING("time.google.com", got.ntpServer); + // The commas are inside the quotes. + TEST_ASSERT_EQUAL_STRING("CET-1CEST,M3.5.0,M10.5.0/3", got.posixZone); + TEST_ASSERT_TRUE(got.ssdpBroadcast); + TEST_ASSERT_FALSE(got.checkForUpdate); +} +static void test_the_network_reads_back_with_ethernet() { + TextWriter w; + writeNetRecord(w, aNet(), true); + TextReader r(w.text); + net_record_t got; + readNetRecord(r, header(), got, true); + TEST_ASSERT_EQUAL_UINT8(2, got.connType); + TEST_ASSERT_FALSE(got.dhcp); + TEST_ASSERT_EQUAL_STRING("192.168.100.200", got.ip); + TEST_ASSERT_EQUAL_STRING("192.168.100.1", got.gateway); + TEST_ASSERT_EQUAL_STRING("255.255.255.0", got.subnet); + TEST_ASSERT_EQUAL_STRING("1.1.1.1", got.dns1); + TEST_ASSERT_EQUAL_STRING("8.8.8.8", got.dns2); + TEST_ASSERT_EQUAL_STRING("mqtts://", got.mqttProtocol); + TEST_ASSERT_EQUAL_STRING("broker.local", got.mqttHostname); + TEST_ASSERT_EQUAL_UINT16(8883, got.mqttPort); + TEST_ASSERT_TRUE(got.mqttPubDisco); + TEST_ASSERT_EQUAL_STRING("somfy", got.mqttRootTopic); + TEST_ASSERT_EQUAL_STRING("ha", got.mqttDiscoTopic); + TEST_ASSERT_EQUAL_UINT8(3, got.ethBoardType); + TEST_ASSERT_EQUAL_UINT8(1, got.ethPhyType); + TEST_ASSERT_EQUAL_UINT8(3, got.ethClkMode); + TEST_ASSERT_EQUAL_INT8(-1, got.ethPhyAddress); + TEST_ASSERT_EQUAL_INT8(12, got.ethPWRPin); + TEST_ASSERT_EQUAL_INT8(23, got.ethMDCPin); + TEST_ASSERT_EQUAL_INT8(18, got.ethMDIOPin); + TEST_ASSERT_EQUAL(w.text.size(), r.position()); +} +static void test_the_radio_reads_back() { + TextWriter w; + writeTransRecord(w, aTrans()); + TextReader r(w.text); + trans_record_t got; + readTransRecord(r, got); + TEST_ASSERT_TRUE(got.enabled); + TEST_ASSERT_EQUAL_UINT8(80, got.type); + TEST_ASSERT_EQUAL_UINT8(4, got.SCKPin); + TEST_ASSERT_EQUAL_UINT8(7, got.CSNPin); + TEST_ASSERT_EQUAL_UINT8(6, got.MOSIPin); + TEST_ASSERT_EQUAL_UINT8(5, got.MISOPin); + TEST_ASSERT_EQUAL_UINT8(10, got.TXPin); + TEST_ASSERT_EQUAL_UINT8(3, got.RXPin); + TEST_ASSERT_EQUAL_FLOAT(433.25f, got.frequency); + TEST_ASSERT_EQUAL_FLOAT(812.5f, got.rxBandwidth); + TEST_ASSERT_EQUAL_FLOAT(1.58f, got.deviation); + TEST_ASSERT_EQUAL_INT8(-30, got.txPower); + TEST_ASSERT_TRUE(got.noiseDetection); +} + +// The web UI takes any name, and the record would shift by a value at the +// comma. +static void test_a_name_with_a_comma_keeps_the_record_in_line() { + shade_record_t rec = aShade(); + strcpy(rec.name, "Left, by the door"); + TextWriter w; + writeShadeRecord(w, rec); + TEST_ASSERT_EQUAL(SHADE_REC_SIZE, w.text.size()); + TextReader r(w.text); + shade_record_t got; + readShadeRecord(r, header(), got); + assertShadesEqual(rec, got); +} +static void test_the_longest_name_reads_back() { + room_record_t room; + room.roomId = 1; + strcpy(room.name, "12345678901234567890"); + room.sortOrder = 7; + TextWriter w; + writeRoomRecord(w, room); + TextReader r(w.text); + room_record_t got; + readRoomRecord(r, header(), got); + TEST_ASSERT_EQUAL_STRING("12345678901234567890", got.name); + TEST_ASSERT_EQUAL_INT8(7, got.sortOrder); +} +// A quoted value as long as its field, with its quotes and comma, is longer +// than the field. Reading stopped at the field's size and left the rest to +// the next value. +static void test_the_longest_settings_read_back() { + settings_record_t rec; + strcpy(rec.fwVersion, "v4.0.0"); + memset(rec.hostname, 'h', sizeof(rec.hostname) - 1); + memset(rec.ntpServer, 'n', sizeof(rec.ntpServer) - 1); + memset(rec.posixZone, 'z', sizeof(rec.posixZone) - 1); + rec.ssdpBroadcast = true; + rec.checkForUpdate = false; + TextWriter w; + writeSettingsRecord(w, rec); + TextReader r(w.text); + settings_record_t got; + readSettingsRecord(r, header(), got); + TEST_ASSERT_EQUAL_STRING(rec.hostname, got.hostname); + TEST_ASSERT_EQUAL_STRING(rec.ntpServer, got.ntpServer); + TEST_ASSERT_EQUAL_STRING(rec.posixZone, got.posixZone); + TEST_ASSERT_TRUE(got.ssdpBroadcast); + TEST_ASSERT_FALSE(got.checkForUpdate); +} +static void test_the_longest_mqtt_settings_read_back() { + net_record_t rec = aNet(); + memset(rec.mqttHostname, 'm', sizeof(rec.mqttHostname) - 1); + memset(rec.mqttRootTopic, 'r', sizeof(rec.mqttRootTopic) - 1); + TextWriter w; + writeNetRecord(w, rec, false); + TextReader r(w.text); + net_record_t got; + readNetRecord(r, header(), got, false); + TEST_ASSERT_EQUAL_STRING(rec.mqttHostname, got.mqttHostname); + TEST_ASSERT_EQUAL_UINT16(8883, got.mqttPort); + TEST_ASSERT_EQUAL_STRING(rec.mqttRootTopic, got.mqttRootTopic); + TEST_ASSERT_EQUAL_STRING("ha", got.mqttDiscoTopic); +} +// A value too long for its field is cut, and the next value still reads. +static void test_a_value_too_long_is_cut() { + TextReader r("\"0123456789012345678901234567890123456789\",\"next\"\n"); + char ip[CFG_IP_LEN]; + char next[CFG_IP_LEN]; + r.beginRecord(); + r.readVarString(ip, sizeof(ip)); + r.readVarString(next, sizeof(next)); + TEST_ASSERT_EQUAL_STRING("01234567890123456789012", ip); + TEST_ASSERT_EQUAL_STRING("next", next); +} + +// The layout of a restore past the shades: the network record of a board +// without Ethernet, then the radio record. +static std::string backupTail(const net_record_t &net, const trans_record_t &trans) { + TextWriter w; + writeNetRecord(w, net, false); + writeTransRecord(w, trans); + return w.text; +} +// 4.0 boards without Ethernet declared the network record 28 bytes longer +// than they wrote, and a restore that skipped it by its size read the radio +// from the middle of its record. +static void test_skipping_the_network_of_a_board_without_ethernet_finds_the_radio() { + TextReader r(backupTail(aNet(), aTrans())); + skipRecords(r, 1); + trans_record_t got; + readTransRecord(r, got); + TEST_ASSERT_EQUAL_UINT8(4, got.SCKPin); + TEST_ASSERT_EQUAL_UINT8(3, got.RXPin); + TEST_ASSERT_EQUAL_FLOAT(433.25f, got.frequency); + TEST_ASSERT_TRUE(got.noiseDetection); +} +// The board that wrote a backup reads its Ethernet settings back; a board +// with Ethernet restoring a backup from one without finds none. +static void test_a_network_record_without_ethernet_keeps_the_ethernet_defaults() { + TextReader r(backupTail(aNet(), aTrans())); + net_record_t got; + readNetRecord(r, header(), got, true); + TEST_ASSERT_EQUAL_UINT8(0, got.ethBoardType); + TEST_ASSERT_EQUAL_INT8(1, got.ethPhyAddress); + TEST_ASSERT_EQUAL_INT8(23, got.ethMDIOPin); + trans_record_t trans; + readTransRecord(r, trans); + TEST_ASSERT_EQUAL_UINT8(4, trans.SCKPin); +} +// Before 4.0 the radio record was declared 74 bytes and is 80. +static void test_the_radio_reads_whole_whatever_its_declared_size() { + TextWriter w; + writeTransRecord(w, aTrans()); + TEST_ASSERT_NOT_EQUAL(74, w.text.size()); + TextReader r(w.text); + trans_record_t got; + readTransRecord(r, got); + TEST_ASSERT_TRUE(got.noiseDetection); + TEST_ASSERT_EQUAL(w.text.size(), r.position()); +} +// A record from a newer version has values this one doesn't know. +static void test_values_a_newer_version_added_are_skipped() { + TextWriter w; + writeRoomRecord(w, room_record_t{1, "Hall", 0}); + w.text.insert(w.text.size() - 1, ", 9, 9"); + writeRoomRecord(w, room_record_t{2, "Attic", 1}); + TextReader r(w.text); + room_record_t got; + readRoomRecord(r, header(), got); + readRoomRecord(r, header(), got); + TEST_ASSERT_EQUAL_UINT8(2, got.roomId); + TEST_ASSERT_EQUAL_STRING("Attic", got.name); +} +// A record shorter than its version says reads defaults for the rest and +// leaves the next record alone. +static void test_a_short_record_never_reads_the_next() { + TextWriter w; + writeRoomRecord(w, room_record_t{1, "Hall", 0}); + // Ends after the name. + std::string text = w.text.substr(0, 4 + CFG_NAME_LEN - 1) + CFG_REC_END; + w.text.clear(); + writeRoomRecord(w, room_record_t{2, "Attic", 1}); + TextReader r(text + w.text); + room_record_t got; + readRoomRecord(r, header(), got); + TEST_ASSERT_EQUAL_UINT8(1, got.roomId); + TEST_ASSERT_EQUAL_STRING("Hall", got.name); + TEST_ASSERT_EQUAL_INT8(0, got.sortOrder); + readRoomRecord(r, header(), got); + TEST_ASSERT_EQUAL_UINT8(2, got.roomId); + TEST_ASSERT_EQUAL_INT8(1, got.sortOrder); +} +static void test_skipping_records_passes_their_newlines() { + TextWriter w; + writeShadeRecord(w, aShade()); + writeGroupRecord(w, aGroup()); + writeRoomRecord(w, room_record_t{4, "Last", 2}); + TextReader r(w.text); + TEST_ASSERT_TRUE(skipRecords(r, 2)); + room_record_t got; + readRoomRecord(r, header(), got); + TEST_ASSERT_EQUAL_STRING("Last", got.name); + TEST_ASSERT_FALSE(skipRecords(r, 1)); +} + +// Older versions, written field by field in the layout they had. +static void writeShadeStart(TextWriter &w, uint8_t id) { + w.writeUInt8(id); + w.writeBool(true); + w.writeUInt8(static_cast(shade_types::roller)); + w.writeUInt32(0x111111); + w.writeString("Old", CFG_NAME_LEN); +} +static void test_a_version_4_shade() { + TextWriter w; + writeShadeStart(w, 2); + w.writeUInt8(0); // tiltType + w.writeUInt8(56); // bitLength + w.writeUInt32(15000); // upTime + w.writeUInt32(14000); // downTime + w.writeUInt32(0); // tiltTime + for(uint8_t j = 0; j < 5; j++) w.writeUInt32(j == 0 ? 0x222222 : 0); + w.writeUInt16(300); // lastRollingCode + w.writeFloat(50, 5); // myPos + w.writeFloat(-1, 5); // myTiltPos + w.writeFloat(25, 5); // currentPos + w.writeFloat(0, 5, CFG_REC_END); + TextReader r(w.text); + shade_record_t got; + readShadeRecord(r, header(4), got); + TEST_ASSERT_EQUAL_UINT8(2, got.shadeId); + TEST_ASSERT_EQUAL_STRING("Old", got.name); + TEST_ASSERT_EQUAL_UINT8(static_cast(radio_proto::RTS), static_cast(got.proto)); + TEST_ASSERT_EQUAL_UINT32(15000, got.upTime); + TEST_ASSERT_EQUAL_UINT16(100, got.stepSize); + TEST_ASSERT_EQUAL_HEX32(0x222222, got.linkedRemotes[0]); + TEST_ASSERT_EQUAL_HEX32(0, got.linkedRemotes[5]); + TEST_ASSERT_EQUAL_UINT16(300, got.lastRollingCode); + TEST_ASSERT_EQUAL_FLOAT(50, got.myPos); + TEST_ASSERT_EQUAL_FLOAT(25, got.currentPos); + TEST_ASSERT_EQUAL_UINT8(1, got.repeats); + TEST_ASSERT_EQUAL_INT8(1, got.sortOrder); + TEST_ASSERT_EQUAL_UINT8(0, got.roomId); + TEST_ASSERT_EQUAL(w.text.size(), r.position()); +} +// Version 2 saved positions as fractions and the favorite as a byte. +static void test_a_version_2_shade_has_fractions() { + TextWriter w; + writeShadeStart(w, 1); + w.writeBool(false); // tilt + w.writeUInt8(56); + w.writeUInt32(10000); + w.writeUInt32(10000); + w.writeUInt32(0); + for(uint8_t j = 0; j < 5; j++) w.writeUInt32(0); + w.writeUInt16(12); + w.writeUInt8(40); // myPos + w.writeFloat(0.5f, 5); // currentPos + w.writeFloat(0, 5, CFG_REC_END); + TextReader r(w.text); + shade_record_t got; + readShadeRecord(r, header(2), got); + TEST_ASSERT_EQUAL_FLOAT(40, got.myPos); + TEST_ASSERT_EQUAL_FLOAT(50, got.currentPos); + TEST_ASSERT_EQUAL_UINT8(static_cast(tilt_types::none), static_cast(got.tiltType)); + TEST_ASSERT_EQUAL(w.text.size(), r.position()); +} +// Version 13 had the sort order and no GPIOs or room. +static void test_a_version_13_shade() { + TextWriter w; + writeShadeStart(w, 5); + w.writeUInt8(0); + w.writeUInt8(static_cast(radio_proto::RTS)); + w.writeUInt8(80); + w.writeUInt32(10000); + w.writeUInt32(10000); + w.writeUInt32(0); + w.writeUInt16(200); + for(uint8_t j = 0; j < SOMFY_MAX_LINKED_REMOTES; j++) w.writeUInt32(0); + w.writeUInt16(77); + w.writeUInt8(0x01); // flags + w.writeFloat(-1, 5); + w.writeFloat(-1, 5); + w.writeFloat(100, 5); + w.writeFloat(0, 5); + w.writeBool(true); // flipCommands + w.writeBool(false); // flipPosition + w.writeUInt8(3); // repeats + w.writeUInt8(8, CFG_REC_END); + TextReader r(w.text); + shade_record_t got; + readShadeRecord(r, header(13), got); + TEST_ASSERT_EQUAL_UINT8(80, got.bitLength); + TEST_ASSERT_EQUAL_UINT16(200, got.stepSize); + TEST_ASSERT_EQUAL_UINT16(77, got.lastRollingCode); + TEST_ASSERT_EQUAL_HEX8(0x01, got.flags); + TEST_ASSERT_EQUAL_FLOAT(100, got.currentPos); + TEST_ASSERT_TRUE(got.flipCommands); + TEST_ASSERT_EQUAL_UINT8(3, got.repeats); + TEST_ASSERT_EQUAL_INT8(8, got.sortOrder); + TEST_ASSERT_EQUAL_UINT8(0, got.gpioUp); + TEST_ASSERT_EQUAL_UINT8(0, got.roomId); + TEST_ASSERT_EQUAL(w.text.size(), r.position()); +} +static void writeGroupStart(TextWriter &w) { + w.writeUInt8(2); + w.writeUInt8(0); + w.writeUInt32(0x333333); + w.writeString("Group", CFG_NAME_LEN); + w.writeUInt8(0); + w.writeUInt8(56); +} +// Version 23 put the group's rolling code before its shades. +static void test_a_version_23_group() { + TextWriter w; + writeGroupStart(w); + w.writeUInt16(4321); + for(uint8_t j = 0; j < SOMFY_MAX_GROUPED_SHADES; j++) w.writeUInt8(j < 2 ? j + 1 : 0); + w.writeUInt8(1); + w.writeUInt8(1); + w.writeBool(false); + w.writeUInt8(6, CFG_REC_END); + TextReader r(w.text); + group_record_t got; + readGroupRecord(r, header(23), got); + TEST_ASSERT_EQUAL_UINT16(4321, got.lastRollingCode); + TEST_ASSERT_EQUAL_UINT8(2, got.linkedShades[1]); + TEST_ASSERT_EQUAL_UINT8(6, got.roomId); + TEST_ASSERT_EQUAL(w.text.size(), r.position()); +} +static void test_a_version_11_group() { + TextWriter w; + writeGroupStart(w); + for(uint8_t j = 0; j < SOMFY_MAX_GROUPED_SHADES; j++) w.writeUInt8(j == 0 ? 4 : 0, j == SOMFY_MAX_GROUPED_SHADES - 1 ? CFG_REC_END : CFG_VALUE_SEP); + TextReader r(w.text); + group_record_t got; + readGroupRecord(r, header(11), got); + TEST_ASSERT_EQUAL_UINT8(4, got.linkedShades[0]); + TEST_ASSERT_EQUAL_UINT8(1, got.repeats); + TEST_ASSERT_EQUAL_INT8(1, got.sortOrder); + TEST_ASSERT_FALSE(got.flipCommands); + TEST_ASSERT_EQUAL_UINT16(0, got.lastRollingCode); +} +// Before version 20 the settings had no update check. +static void test_version_19_settings_check_for_updates() { + TextReader r("\"v2.0.0\",\"host\",\"pool.ntp.org\",\"\",true \n"); + settings_record_t got; + readSettingsRecord(r, header(19), got); + TEST_ASSERT_EQUAL_STRING("host", got.hostname); + TEST_ASSERT_TRUE(got.ssdpBroadcast); + TEST_ASSERT_TRUE(got.checkForUpdate); +} +// Before version 22 the network record had no MQTT settings. +static void test_a_version_21_network_has_no_mqtt() { + TextReader r(" 1,true ,\"\",\"\",\"\",\"\",\"\", 1, 0, 0, 0, -1, 23, 18\n 9\n"); + net_record_t got; + readNetRecord(r, header(21), got, true); + TEST_ASSERT_EQUAL_UINT8(1, got.connType); + TEST_ASSERT_EQUAL_STRING("", got.mqttHostname); + TEST_ASSERT_EQUAL_UINT16(1883, got.mqttPort); + TEST_ASSERT_EQUAL_INT8(-1, got.ethPWRPin); + r.beginRecord(); + TEST_ASSERT_EQUAL_UINT8(9, r.readUInt8(0)); +} + +static void test_a_load_keeps_the_higher_rolling_code() { + TEST_ASSERT_EQUAL_UINT16(500, rollingCodeAfterLoad(500, 300, false)); + TEST_ASSERT_EQUAL_UINT16(700, rollingCodeAfterLoad(500, 700, false)); +} +static void test_a_restore_takes_the_backups_rolling_code() { + TEST_ASSERT_EQUAL_UINT16(300, rollingCodeAfterLoad(500, 300, true)); + TEST_ASSERT_EQUAL_UINT16(700, rollingCodeAfterLoad(500, 700, true)); +} + +// A real backup, which isn't in the repo: it holds the remotes' addresses. +// CONFIG_BACKUP= pio test -e native -f test_configrecords +static void test_a_real_backup_reads_to_its_end() { + const char *path = getenv("CONFIG_BACKUP"); + if(!path) TEST_IGNORE_MESSAGE("set CONFIG_BACKUP to a backup file"); + std::ifstream in(path, std::ios::binary); + TEST_ASSERT_TRUE_MESSAGE(in.good(), "can't open CONFIG_BACKUP"); + std::stringstream ss; + ss << in.rdbuf(); + TextReader r(ss.str()); + config_header_t h; + readConfigHeader(r, h); + TEST_ASSERT_EQUAL(static_cast(h.length), r.position()); + for(uint8_t i = 0; i < h.roomRecords; i++) { + room_record_t rec; + readRoomRecord(r, h, rec); + TEST_ASSERT_NOT_EQUAL(0, rec.roomId); + } + for(uint8_t i = 0; i < h.shadeRecords; i++) { + shade_record_t rec; + readShadeRecord(r, h, rec); + TEST_ASSERT_TRUE(rec.shadeId >= 1 && rec.shadeId <= SOMFY_MAX_SHADES); + TEST_ASSERT_TRUE(rec.currentPos >= 0 && rec.currentPos <= 100); + // Written back, it is the record it was read from. + TextWriter w; + writeShadeRecord(w, rec); + TEST_ASSERT_EQUAL(h.shadeRecordSize, w.text.size()); + } + for(uint8_t i = 0; i < h.groupRecords; i++) { + group_record_t rec; + readGroupRecord(r, h, rec); + TEST_ASSERT_TRUE(rec.groupId >= 1 && rec.groupId <= SOMFY_MAX_GROUPS); + } + for(uint8_t i = 0; i < h.repeaterRecords; i++) { + uint32_t repeaters[SOMFY_MAX_REPEATERS]; + readRepeaterRecord(r, repeaters); + } + settings_record_t settings; + readSettingsRecord(r, h, settings); + TEST_ASSERT_EQUAL_UINT8('v', settings.fwVersion[0]); + net_record_t net; + readNetRecord(r, h, net, true); + trans_record_t trans; + readTransRecord(r, trans); + TEST_ASSERT_TRUE(trans.frequency > 300 && trans.frequency < 950); + TEST_ASSERT_EQUAL(ss.str().size(), r.position()); +} + +int main(int argc, char **argv) { + UNITY_BEGIN(); + RUN_TEST(test_each_record_is_the_size_the_header_declares); + RUN_TEST(test_a_counter_measures_what_a_writer_writes); + RUN_TEST(test_a_deleted_shade_is_written_cleared); + RUN_TEST(test_the_header_reads_back); + RUN_TEST(test_a_version_12_header_has_no_rooms_repeaters_or_settings); + RUN_TEST(test_a_shade_reads_back); + RUN_TEST(test_a_room_reads_back); + RUN_TEST(test_a_group_reads_back); + RUN_TEST(test_a_group_reads_its_shades_without_gaps); + RUN_TEST(test_the_settings_read_back); + RUN_TEST(test_the_network_reads_back_with_ethernet); + RUN_TEST(test_the_radio_reads_back); + RUN_TEST(test_a_name_with_a_comma_keeps_the_record_in_line); + RUN_TEST(test_the_longest_name_reads_back); + RUN_TEST(test_the_longest_settings_read_back); + RUN_TEST(test_the_longest_mqtt_settings_read_back); + RUN_TEST(test_a_value_too_long_is_cut); + RUN_TEST(test_skipping_the_network_of_a_board_without_ethernet_finds_the_radio); + RUN_TEST(test_a_network_record_without_ethernet_keeps_the_ethernet_defaults); + RUN_TEST(test_the_radio_reads_whole_whatever_its_declared_size); + RUN_TEST(test_values_a_newer_version_added_are_skipped); + RUN_TEST(test_a_short_record_never_reads_the_next); + RUN_TEST(test_skipping_records_passes_their_newlines); + RUN_TEST(test_a_version_4_shade); + RUN_TEST(test_a_version_2_shade_has_fractions); + RUN_TEST(test_a_version_13_shade); + RUN_TEST(test_a_version_23_group); + RUN_TEST(test_a_version_11_group); + RUN_TEST(test_version_19_settings_check_for_updates); + RUN_TEST(test_a_version_21_network_has_no_mqtt); + RUN_TEST(test_a_load_keeps_the_higher_rolling_code); + RUN_TEST(test_a_restore_takes_the_backups_rolling_code); + RUN_TEST(test_a_real_backup_reads_to_its_end); + return UNITY_END(); +}