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 <noreply@anthropic.com>
This commit is contained in:
cjkas 2026-10-29 16:59:52 +01:00
parent 6db449e878
commit 8c59cf4903
8 changed files with 1881 additions and 915 deletions

View file

@ -112,7 +112,7 @@ build_flags =
platform = native platform = native
test_framework = unity test_framework = unity
test_build_src = yes test_build_src = yes
build_src_filter = -<*> +<Utils.cpp> +<SomfyFrame.cpp> +<JsonFormatter.cpp> +<ShadeMotion.cpp> +<ShadeSensors.cpp> +<ShadeBehaviour.cpp> +<PulseDecoder.cpp> +<TxBuffer.cpp> +<RadioScheduler.cpp> +<ShadeRequests.cpp> +<entryPoints/MqttCommands.cpp> +<ItemSnapshot.cpp> +<ModelEditor.cpp> +<ShadeChanges.cpp> +<ProtoSchema.cpp> +<ProtoFrame.cpp> +<ProtoSync.cpp> +<ProtoParse.cpp> +<ProtoSlots.cpp> +<ProtoOps.cpp> +<ProtoEdits.cpp> +<ProtoSettings.cpp> +<ProtoAssembly.cpp> build_src_filter = -<*> +<Utils.cpp> +<ConfigRecords.cpp> +<MqttDisco.cpp> +<SomfyFrame.cpp> +<JsonFormatter.cpp> +<ShadeMotion.cpp> +<ShadeSensors.cpp> +<ShadeBehaviour.cpp> +<PulseDecoder.cpp> +<TxBuffer.cpp> +<RadioScheduler.cpp> +<ShadeRequests.cpp> +<entryPoints/MqttCommands.cpp> +<ItemSnapshot.cpp> +<ModelEditor.cpp> +<ShadeChanges.cpp> +<ProtoSchema.cpp> +<ProtoFrame.cpp> +<ProtoSync.cpp> +<ProtoParse.cpp> +<ProtoSlots.cpp> +<ProtoOps.cpp> +<ProtoEdits.cpp> +<ProtoSettings.cpp> +<ProtoAssembly.cpp>
lib_deps = lib_deps =
bblanchon/ArduinoJson@^7.2.2 bblanchon/ArduinoJson@^7.2.2
build_flags = build_flags =

File diff suppressed because it is too large Load diff

View file

@ -3,72 +3,40 @@
#include "Somfy.h" #include "Somfy.h"
#include "ConfigSettings.h" #include "ConfigSettings.h"
#include "ConfigHeader.h" #include "ConfigHeader.h"
#include "ConfigRecords.h"
#ifndef configfile_h #ifndef configfile_h
#define configfile_h #define configfile_h
#define CFG_VALUE_SEP ',' // Opens shades.cfg or a backup and writes or reads its values
#define CFG_REC_END '\n' // (ConfigRecords.h), from the file or into a String in RAM.
#define CFG_TOK_NONE 0x00 class ConfigFile : public ConfigReader, public ConfigWriter {
#define CFG_TOK_QUOTE '"'
class ConfigFile {
protected: protected:
File file; File file;
String *_ramBuf = nullptr; String *_ramBuf = nullptr;
bool readOnly = false; bool readOnly = false;
bool begin(const char *filename, bool readOnly = false); bool begin(const char *filename, bool readOnly = false);
uint32_t startRecPos = 0;
bool _opened = false; bool _opened = false;
public: public:
config_header_t header; config_header_t header;
bool beginRAM(String *buf); bool beginRAM(String *buf);
void end(); void end();
bool isOpen(); bool isOpen();
bool seekRecordByIndex(uint16_t ndx);
bool readHeader(); bool readHeader();
bool seekChar(const char val);
bool writeHeader(const config_header_t &header);
bool writeHeader(); bool writeHeader();
bool writeSeparator(); bool readByte(uint8_t &val) override;
bool writeRecordEnd(); uint32_t position() override;
bool writeChar(const char val); bool writeBytes(const uint8_t *buf, size_t len) override;
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);
}; };
class ShadeConfigFile : public ConfigFile { class ShadeConfigFile : public ConfigFile {
protected: protected:
bool writeRepeaterRecord(SomfyShadeController *s); // A restore takes the rolling codes in the file (rollingCodeAfterLoad).
bool writeRoomRecord(SomfyRoom *room); bool restoring = false;
bool writeShadeRecord(SomfyShade *shade); bool writeModel(SomfyShadeController *s);
bool writeGroupRecord(SomfyGroup *group); void readRoom(SomfyRoom *room);
bool writeSettingsRecord(); void readShade(SomfyShade *shade);
bool writeNetRecord(); void readGroup(SomfyGroup *group);
bool writeTransRecord(transceiver_config_t &cfg); void readRepeaters(SomfyShadeController *s);
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);
public: public:
static bool exists(); static bool exists();
static bool load(SomfyShadeController *somfy, const char *filename = "/shades.cfg"); static bool load(SomfyShadeController *somfy, const char *filename = "/shades.cfg");

495
src/ConfigRecords.cpp Normal file
View file

@ -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<uint8_t>(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<int8_t>(atoi(buff));
return defVal;
}
uint8_t ConfigReader::readUInt8(const uint8_t defVal) {
char buff[4];
if(this->readString(buff, sizeof(buff))) return static_cast<uint8_t>(atoi(buff));
return defVal;
}
uint16_t ConfigReader::readUInt16(const uint16_t defVal) {
char buff[6];
if(this->readString(buff, sizeof(buff))) return static_cast<uint16_t>(atoi(buff));
return defVal;
}
uint32_t ConfigReader::readUInt32(const uint32_t defVal) {
char buff[11];
if(this->readString(buff, sizeof(buff))) return static_cast<uint32_t>(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<uint8_t>(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<const uint8_t *>(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<const uint8_t *>(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<unsigned>(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<uint8_t>(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<uint8_t>(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<int8_t>(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<uint8_t>(rec.shadeType));
w.writeUInt32(rec.remoteAddress);
w.writeString(rec.name, sizeof(rec.name));
w.writeUInt8(static_cast<uint8_t>(rec.tiltType));
w.writeUInt8(static_cast<uint8_t>(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<uint8_t>(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<shade_types>(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<tilt_types>(r.readUInt8(0));
if(ver > 6) rec.proto = static_cast<radio_proto>(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<float>(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<int8_t>(r.readUInt8(rec.shadeId - 1));
else rec.sortOrder = static_cast<int8_t>(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<uint8_t>(rec.groupType));
w.writeUInt32(rec.remoteAddress);
w.writeString(rec.name, sizeof(rec.name));
w.writeUInt8(static_cast<uint8_t>(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<uint8_t>(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<group_types>(r.readUInt8(0));
rec.remoteAddress = r.readUInt32(0);
r.readFixedString(rec.name, sizeof(rec.name));
rec.proto = static_cast<radio_proto>(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<int8_t>(r.readUInt8(rec.groupId - 1));
else rec.sortOrder = static_cast<int8_t>(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<uint8_t>(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<radio_proto>(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;
}

222
src/ConfigRecords.h Normal file
View file

@ -0,0 +1,222 @@
#ifndef CONFIG_RECORDS_H
#define CONFIG_RECORDS_H
#include <Arduino.h>
#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

View file

@ -226,36 +226,6 @@ void ConfigSettings::print() {
if(this->connType == conn_types_t::ethernet || this->connType == conn_types_t::ethernetpref) this->Ethernet.print(); if(this->connType == conn_types_t::ethernet || this->connType == conn_types_t::ethernetpref) this->Ethernet.print();
#endif #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() { bool MQTTSettings::begin() {
this->load(); this->load();
return true; return true;

View file

@ -186,8 +186,6 @@ class ConfigSettings: BaseSettings {
bool save(); bool save();
bool load(); bool load();
void print(); void print();
uint16_t calcSettingsRecSize();
uint16_t calcNetRecSize();
bool getAppVersion(); bool getAppVersion();
}; };
#endif #endif

View file

@ -0,0 +1,799 @@
#include <unity.h>
#include <fstream>
#include <sstream>
#include <string>
#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<const char *>(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<uint8_t>(text[pos++]);
return true;
}
uint32_t position() override { return static_cast<uint32_t>(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<uint8_t>(want.shadeType), static_cast<uint8_t>(got.shadeType));
TEST_ASSERT_EQUAL_HEX32(want.remoteAddress, got.remoteAddress);
TEST_ASSERT_EQUAL_STRING(want.name, got.name);
TEST_ASSERT_EQUAL_UINT8(static_cast<uint8_t>(want.tiltType), static_cast<uint8_t>(got.tiltType));
TEST_ASSERT_EQUAL_UINT8(static_cast<uint8_t>(want.proto), static_cast<uint8_t>(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<uint8_t>(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<uint8_t>(radio_proto::RTS), static_cast<uint8_t>(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<uint8_t>(tilt_types::none), static_cast<uint8_t>(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<uint8_t>(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=<path> 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<uint32_t>(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();
}