Save configuration to preferences in NVS
This commit is contained in:
@@ -35,17 +35,20 @@ struct ConfigDef {
|
|||||||
static const ConfigDef configdefs[] = {
|
static const ConfigDef configdefs[] = {
|
||||||
{"systemName", ConfigType::STRING, String("OBPkp61")},
|
{"systemName", ConfigType::STRING, String("OBPkp61")},
|
||||||
{"systemMode", ConfigType::CHAR, 'K'},
|
{"systemMode", ConfigType::CHAR, 'K'},
|
||||||
{"logLevel", ConfigType::BYTE, uint8_t(1)},
|
{"logLevel", ConfigType::BYTE, uint8_t(4)},
|
||||||
{"adminPassword", ConfigType::STRING, String("obpkp61")},
|
{"adminPassword", ConfigType::STRING, String("obpkp61")},
|
||||||
{"useAdminPass", ConfigType::BOOL, true},
|
{"useAdminPass", ConfigType::BOOL, true},
|
||||||
|
{"instDesc1", ConfigType::STRING, String("")},
|
||||||
|
{"instDesc2", ConfigType::STRING, String("")},
|
||||||
{"apEnable", ConfigType::BOOL, true},
|
{"apEnable", ConfigType::BOOL, true},
|
||||||
{"apPassword", ConfigType::STRING, String("obpkp61")},
|
{"apPassword", ConfigType::STRING, String("obpkp61")},
|
||||||
{"apIp", ConfigType::STRING, String("192.168.15.1")},
|
{"apIp", ConfigType::STRING, String("192.168.15.1")},
|
||||||
{"apMask", ConfigType::STRING, String("255.255.255.0")},
|
{"apMask", ConfigType::STRING, String("255.255.255.0")},
|
||||||
{"stopApTime", ConfigType::SHORT, int16_t(0)},
|
{"stopApTime", ConfigType::SHORT, int16_t(0)},
|
||||||
|
{"apHidden", ConfigType::BOOL, false},
|
||||||
{"cpuSpeed", ConfigType::SHORT, int16_t(160)},
|
{"cpuSpeed", ConfigType::SHORT, int16_t(160)},
|
||||||
{"ledBrightness", ConfigType::SHORT, int16_t(96)},
|
{"ledBrightness", ConfigType::SHORT, int16_t(96)},
|
||||||
{"ledRgbBrightness", ConfigType::SHORT, int16_t(96)},
|
{"rgbBrightness", ConfigType::SHORT, int16_t(96)},
|
||||||
{"tempFormat", ConfigType::CHAR, 'C'},
|
{"tempFormat", ConfigType::CHAR, 'C'},
|
||||||
{"switchBank", ConfigType::BYTE, uint8_t(0)},
|
{"switchBank", ConfigType::BYTE, uint8_t(0)},
|
||||||
{"key1", ConfigType::BYTE, uint8_t(1)},
|
{"key1", ConfigType::BYTE, uint8_t(1)},
|
||||||
@@ -75,15 +78,19 @@ private:
|
|||||||
Preferences& prefs;
|
Preferences& prefs;
|
||||||
std::map<String, ConfigValue> values;
|
std::map<String, ConfigValue> values;
|
||||||
std::map<String, const ConfigDef*> defs;
|
std::map<String, const ConfigDef*> defs;
|
||||||
|
void updateValue(const char* key, ConfigType type, ConfigValue newvalue);
|
||||||
[[noreturn]] void error_abort() const;
|
[[noreturn]] void error_abort() const;
|
||||||
public:
|
public:
|
||||||
explicit Config(Preferences& prefs);
|
explicit Config(Preferences& prefs);
|
||||||
void load();
|
void load();
|
||||||
bool save(JsonObject json);
|
void save();
|
||||||
void dump();
|
void dump();
|
||||||
|
|
||||||
|
bool hasKey(const char* key);
|
||||||
|
|
||||||
template<typename T>
|
template<typename T>
|
||||||
T get(const char* key) const;
|
T get(const char* key) const;
|
||||||
|
ConfigValue get(const char* key) const;
|
||||||
|
|
||||||
uint8_t getByte(const char* key) const;
|
uint8_t getByte(const char* key) const;
|
||||||
int16_t getShort(const char* key) const;
|
int16_t getShort(const char* key) const;
|
||||||
@@ -94,6 +101,8 @@ public:
|
|||||||
const String& getString(const char* key) const;
|
const String& getString(const char* key) const;
|
||||||
const char* getCString(const char* key) const;
|
const char* getCString(const char* key) const;
|
||||||
|
|
||||||
|
bool setValue(const char* key, const char* value);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
extern Config config;
|
extern Config config;
|
||||||
|
|||||||
@@ -5,5 +5,3 @@
|
|||||||
extern AsyncWebServer server;
|
extern AsyncWebServer server;
|
||||||
|
|
||||||
void webserver_init();
|
void webserver_init();
|
||||||
void send_embedded_file(String name, AsyncWebServerRequest *request);
|
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ bool Nmea2kTwai::CANSendFrame(unsigned long id, unsigned char len, const unsigne
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
txTimeouts = 0;
|
txTimeouts = 0;
|
||||||
LOGD(TAG, "twai transmit id %ld, len %d", LOGID(id), (int)len);
|
LOGV(TAG, "twai transmit id %ld, len %d", LOGID(id), (int)len);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ bool Nmea2kTwai::CANGetFrame(unsigned long &id, unsigned char &len, unsigned cha
|
|||||||
LOGD(TAG, "twai: received invalid message %lld, len %d", LOGID(id), len);
|
LOGD(TAG, "twai: received invalid message %lld, len %d", LOGID(id), len);
|
||||||
len = 8;
|
len = 8;
|
||||||
}
|
}
|
||||||
LOGD(TAG, "twai rcv id=%ld,len=%d, ext=%d",
|
LOGV(TAG, "twai rcv id=%ld,len=%d, ext=%d",
|
||||||
LOGID(message.identifier), message.data_length_code, message.extd);
|
LOGID(message.identifier), message.data_length_code, message.extd);
|
||||||
if (! message.rtr) {
|
if (! message.rtr) {
|
||||||
memcpy(buf, message.data, message.data_length_code);
|
memcpy(buf, message.data, message.data_length_code);
|
||||||
@@ -193,7 +193,7 @@ void Nmea2kTwai::loop()
|
|||||||
Nmea2kTwai::Status Nmea2kTwai::logStatus()
|
Nmea2kTwai::Status Nmea2kTwai::logStatus()
|
||||||
{
|
{
|
||||||
Status canState = getStatus();
|
Status canState = getStatus();
|
||||||
LOGI(TAG, "twai state %s, rxerr %d, txerr %d, txfail %d, txtimeout %d, rxmiss %d, rxoverrun %d",
|
LOGD(TAG, "twai state %s, rxerr %d, txerr %d, txfail %d, txtimeout %d, rxmiss %d, rxoverrun %d",
|
||||||
stateStr(canState.state),
|
stateStr(canState.state),
|
||||||
canState.rx_errors,
|
canState.rx_errors,
|
||||||
canState.tx_errors,
|
canState.tx_errors,
|
||||||
|
|||||||
237
src/config.cpp
237
src/config.cpp
@@ -14,7 +14,7 @@ Config::Config(Preferences& prefs)
|
|||||||
for (size_t i = 0; i < configdefsCount; ++i) {
|
for (size_t i = 0; i < configdefsCount; ++i) {
|
||||||
defs[configdefs[i].key] = &configdefs[i];
|
defs[configdefs[i].key] = &configdefs[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[[noreturn]] void Config::error_abort() const {
|
[[noreturn]] void Config::error_abort() const {
|
||||||
@@ -83,83 +83,70 @@ void Config::load() {
|
|||||||
prefs.end();
|
prefs.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Config::save(JsonObject json) {
|
void Config::updateValue(const char* key, ConfigType type, ConfigValue newvalue) {
|
||||||
|
switch (type) {
|
||||||
// TODO Not fail if one single value is wrong?
|
case ConfigType::BYTE:
|
||||||
|
prefs.putUChar(key, std::get<uint8_t>(newvalue));
|
||||||
|
break;
|
||||||
|
case ConfigType::SHORT:
|
||||||
|
prefs.putShort(key, std::get<int16_t>(newvalue));
|
||||||
|
break;
|
||||||
|
case ConfigType::INT:
|
||||||
|
prefs.putInt(key, std::get<int32_t>(newvalue));
|
||||||
|
break;
|
||||||
|
case ConfigType::BOOL:
|
||||||
|
prefs.putBool(key, std::get<bool>(newvalue));
|
||||||
|
break;
|
||||||
|
case ConfigType::FLOAT:
|
||||||
|
prefs.putFloat(key, std::get<float>(newvalue));
|
||||||
|
break;
|
||||||
|
case ConfigType::CHAR:
|
||||||
|
prefs.putChar(key, std::get<char>(newvalue));
|
||||||
|
break;
|
||||||
|
case ConfigType::STRING:
|
||||||
|
prefs.putString(key, std::get<String>(newvalue));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Config::save() {
|
||||||
|
ConfigValue oldval;
|
||||||
prefs.begin(PREF_NAME, false);
|
prefs.begin(PREF_NAME, false);
|
||||||
|
for (const auto& def : configdefs) {
|
||||||
int errors = 0;
|
if (prefs.isKey(def.key)) {
|
||||||
for (JsonPair kv : json) {
|
switch (def.type) {
|
||||||
const char* key = kv.key().c_str();
|
case ConfigType::BYTE:
|
||||||
JsonVariant v = kv.value();
|
oldval = (uint8_t)prefs.getUChar(def.key, std::get<uint8_t>(def.defval));
|
||||||
|
break;
|
||||||
auto it = values.find(key);
|
case ConfigType::SHORT:
|
||||||
if (it == values.end()) {
|
oldval = (int16_t)prefs.getShort(def.key, std::get<int16_t>(def.defval));
|
||||||
LOGE(TAG, "Unexpected missing key: %s", key);
|
break;
|
||||||
return false;
|
case ConfigType::INT:
|
||||||
}
|
oldval = prefs.getInt(def.key, std::get<int32_t>(def.defval));
|
||||||
|
break;
|
||||||
auto itdef = defs.find(key);
|
case ConfigType::BOOL:
|
||||||
if (itdef == defs.end()) {
|
oldval = prefs.getBool(def.key, std::get<bool>(def.defval));
|
||||||
LOGE(TAG, "Unexpected missing defs key: %s", key);
|
break;
|
||||||
return false;
|
case ConfigType::FLOAT:
|
||||||
}
|
oldval = prefs.getFloat(def.key, std::get<float>(def.defval));
|
||||||
|
break;
|
||||||
// const ConfigDef* def = itdef->second;
|
case ConfigType::CHAR:
|
||||||
|
oldval = (char)prefs.getChar(def.key, std::get<char>(def.defval));
|
||||||
switch (itdef->second->type) {
|
break;
|
||||||
case ConfigType::BYTE: {
|
case ConfigType::STRING:
|
||||||
if (!v.is<uint8_t>()) {
|
oldval = prefs.getString(def.key, std::get<String>(def.defval));
|
||||||
errors++;
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
uint8_t newval = v.as<uint8_t>();
|
|
||||||
uint8_t *curval = std::get_if<uint8_t>(&values[key]);
|
|
||||||
if (newval != *curval) {
|
|
||||||
values[key] = newval;
|
|
||||||
//prefs.putUChar(key, newval);
|
|
||||||
LOGI(TAG, "changing %s, replacing %d with %d", key, *curval, newval);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
/* case ConfigType::SHORT:
|
if (values[def.key] != oldval) {
|
||||||
if (!v.is<int16_t>()) return false;
|
LOGD(TAG, "NVS update needed for %s", def.key);
|
||||||
values[key] = v.as<int16_t>();
|
updateValue(def.key, def.type, values[def.key]);
|
||||||
prefs.putShort(key, v.as<int16_t>());
|
}
|
||||||
break;
|
} else {
|
||||||
case ConfigType::INT:
|
LOGD(TAG, "Key does not exist in NVS: %s, creating", def.key);
|
||||||
if (!v.is<int32_t>()) return false;
|
updateValue(def.key, def.type, values[def.key]);
|
||||||
values[key] = v.as<int32_t>();
|
|
||||||
prefs.putInt(key, v.as<int32_t>());
|
|
||||||
break;
|
|
||||||
case ConfigType::BOOL:
|
|
||||||
if (!v.is<bool>()) return false;
|
|
||||||
values[key] = v.as<bool>();
|
|
||||||
prefs.putBool(key, v.as<bool>());
|
|
||||||
break;
|
|
||||||
case ConfigType::FLOAT:
|
|
||||||
if (!v.is<float>()) return false;
|
|
||||||
values[key] = v.as<float>();
|
|
||||||
prefs.putFloat(key, v.as<float>());
|
|
||||||
break;
|
|
||||||
case ConfigType::CHAR:
|
|
||||||
if (!v.is<const char*>()) return false;
|
|
||||||
values[key] = v.as<const char*>()[0];
|
|
||||||
prefs.putChar(key, v.as<const char*>()[0]);
|
|
||||||
break;
|
|
||||||
case ConfigType::STRING:
|
|
||||||
if (!v.is<const char*>()) return false;
|
|
||||||
values[key] = String(v.as<const char*>());
|
|
||||||
prefs.putString(key, v.as<const char*>());
|
|
||||||
break; */
|
|
||||||
default:
|
|
||||||
ESP_LOGE("CFG", "Unsupported type for key %s", key);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
prefs.end();
|
prefs.end();
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Config::dump() {
|
void Config::dump() {
|
||||||
@@ -211,6 +198,16 @@ T Config::get(const char* key) const {
|
|||||||
return std::get<T>(values.at(key));
|
return std::get<T>(values.at(key));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ConfigValue Config::get(const char* key) const {
|
||||||
|
auto it = values.find(key);
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Config::hasKey(const char* key) {
|
||||||
|
auto it = values.find(key);
|
||||||
|
return ! (it == values.end());
|
||||||
|
}
|
||||||
|
|
||||||
uint8_t Config::getByte(const char* key) const {
|
uint8_t Config::getByte(const char* key) const {
|
||||||
auto it = values.find(key);
|
auto it = values.find(key);
|
||||||
if (it == values.end()) {
|
if (it == values.end()) {
|
||||||
@@ -249,13 +246,14 @@ int32_t Config::getInt(const char* key) const {
|
|||||||
|
|
||||||
bool Config::getBool(const char* key) const {
|
bool Config::getBool(const char* key) const {
|
||||||
auto it = values.find(key);
|
auto it = values.find(key);
|
||||||
if (it == values.end())
|
if (it == values.end()) {
|
||||||
LOGE(TAG, "Missing config key: %s", key);
|
LOGE(TAG, "Missing config key: %s", key);
|
||||||
abort();
|
error_abort();
|
||||||
|
}
|
||||||
if (auto v = std::get_if<bool>(&it->second))
|
if (auto v = std::get_if<bool>(&it->second))
|
||||||
return *v;
|
return *v;
|
||||||
LOGE(TAG, "Type mismatch for key: %s", key);
|
LOGE(TAG, "Type mismatch for key: %s", key);
|
||||||
abort();
|
error_abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
float Config::getFloat(const char* key) const {
|
float Config::getFloat(const char* key) const {
|
||||||
@@ -297,3 +295,94 @@ const String& Config::getString(const char* key) const {
|
|||||||
const char* Config::getCString(const char* key) const {
|
const char* Config::getCString(const char* key) const {
|
||||||
return getString(key).c_str();
|
return getString(key).c_str();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Config::setValue(const char* key, const char* value) {
|
||||||
|
// sets data associated to a key to a new value
|
||||||
|
// does not write back to preferences, that has to be done
|
||||||
|
// separately
|
||||||
|
LOGD(TAG, "setting: %s to %s", key, value);
|
||||||
|
bool success = false;
|
||||||
|
char* end;
|
||||||
|
unsigned long byte_temp;
|
||||||
|
long int_temp;
|
||||||
|
bool bool_temp;
|
||||||
|
float float_temp;
|
||||||
|
switch (defs[key]->type) {
|
||||||
|
case ConfigType::BYTE:
|
||||||
|
byte_temp = strtoul(value, &end, 10);
|
||||||
|
if (*end != '\0') {
|
||||||
|
LOGW(TAG, "not a number");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (temp > 255) {
|
||||||
|
LOGW(TAG, "byte out of range");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
values[key] = static_cast<uint8_t>(byte_temp);
|
||||||
|
LOGD(TAG, "converted to BYTE");
|
||||||
|
success = true;
|
||||||
|
break;
|
||||||
|
case ConfigType::SHORT:
|
||||||
|
int_temp = strtoul(value, &end, 10);
|
||||||
|
if (*end != '\0') {
|
||||||
|
LOGW(TAG, "not a number");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (temp < INT16_MIN || temp > INT16_MAX) {
|
||||||
|
LOGW(TAG, "short out of range");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
values[key] = static_cast<int16_t>(int_temp);
|
||||||
|
LOGD(TAG, "converted to SHORT");
|
||||||
|
success = true;
|
||||||
|
break;
|
||||||
|
case ConfigType::INT:
|
||||||
|
int_temp = strtoul(value, &end, 10);
|
||||||
|
if (*end != '\0') {
|
||||||
|
LOGW(TAG, "not a number");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (temp < INT32_MIN || temp > INT32_MAX) {
|
||||||
|
LOGW(TAG, "integer out of range");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
values[key] = static_cast<int16_t>(int_temp);
|
||||||
|
LOGD(TAG, "converted to INT");
|
||||||
|
success = true;
|
||||||
|
break;
|
||||||
|
case ConfigType::BOOL:
|
||||||
|
if ((value[0] == '1' && value[1] == '\0') || (strcasecmp(value, "true") == 0)) {
|
||||||
|
bool_temp = true;
|
||||||
|
} else if ((value[0] == '0' && value[1] == '\0') || (strcasecmp(value, "false") == 0)) {
|
||||||
|
bool_temp = false;
|
||||||
|
} else {
|
||||||
|
LOGW(TAG, "invalid value for boolean");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
values[key] = bool_temp;
|
||||||
|
LOGD(TAG, "converted to BOOL");
|
||||||
|
success = true;
|
||||||
|
break;
|
||||||
|
case ConfigType::FLOAT:
|
||||||
|
float_temp = strtof(value, &end);
|
||||||
|
if (*end != '\0') {
|
||||||
|
LOGW(TAG, "invalid float");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
values[key] = float_temp;
|
||||||
|
LOGD(TAG, "converted to FLOAT");
|
||||||
|
success = true;
|
||||||
|
break;
|
||||||
|
case ConfigType::CHAR:
|
||||||
|
values[key] = value[0];
|
||||||
|
LOGD(TAG, "converted to CHAR");
|
||||||
|
success = true;
|
||||||
|
break;
|
||||||
|
case ConfigType::STRING:
|
||||||
|
values[key] = String(value);
|
||||||
|
LOGD(TAG, "converted to STRING");
|
||||||
|
success = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|||||||
44
src/main.cpp
44
src/main.cpp
@@ -40,6 +40,7 @@ uint8_t loglevel = 5;
|
|||||||
const char* wifi_ssid = "OBPKP61";
|
const char* wifi_ssid = "OBPKP61";
|
||||||
const char* wifi_pass = "keypad61";
|
const char* wifi_pass = "keypad61";
|
||||||
bool ap_enabled = false;
|
bool ap_enabled = false;
|
||||||
|
bool ap_hidden = false;
|
||||||
|
|
||||||
unsigned long firstStart = 0;
|
unsigned long firstStart = 0;
|
||||||
unsigned long lastSensor = 0;
|
unsigned long lastSensor = 0;
|
||||||
@@ -248,33 +249,20 @@ void setup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// N2K basics
|
// N2K basics
|
||||||
nodeid = N2K_DEFAULT_NODEID;
|
LOGI(TAG, "N2K default node is %d", N2K_DEFAULT_NODEID);
|
||||||
LOGI(TAG, "N2K default node is %d", nodeid);
|
|
||||||
//preferences.begin(PREF_NAME, false);
|
|
||||||
//nodeid = preferences.getInt("LastNodeId", N2K_DEFAULT_NODEID);
|
|
||||||
//preferences.end();
|
|
||||||
nodeid = config.getByte("LastNodeId");
|
nodeid = config.getByte("LastNodeId");
|
||||||
LOGI(TAG, "N2K node id set to %d from preferences", nodeid);
|
LOGI(TAG, "N2K node id set to %d from preferences", nodeid);
|
||||||
|
|
||||||
//cpuspeed = preferences.getInt("cpuSpeed", 160);
|
// some other settings of interest
|
||||||
cpuspeed = config.getShort("cpuSpeed");
|
cpuspeed = config.getShort("cpuSpeed");
|
||||||
//int apstoptime = preferences.getInt("stopApTime", 0);
|
|
||||||
int16_t apstoptime = config.getShort("stopApTime");
|
int16_t apstoptime = config.getShort("stopApTime");
|
||||||
//String apip = preferences.getString("apIp", "192.168.15.1");
|
|
||||||
//String apmask = preferences.getString("apMask", "255.255.255.0");
|
|
||||||
String apip = config.getString("apIp");
|
String apip = config.getString("apIp");
|
||||||
String apmask = config.getString("apMask");
|
String apmask = config.getString("apMask");
|
||||||
//preferences.end();
|
|
||||||
|
|
||||||
// Setup webserver
|
// Setup webserver
|
||||||
WiFi.persistent(false);
|
WiFi.persistent(false);
|
||||||
WiFi.mode(WIFI_MODE_AP);
|
WiFi.mode(WIFI_MODE_AP);
|
||||||
|
|
||||||
int channel = WIFI_CHANNEL;
|
|
||||||
bool hidden = false;
|
|
||||||
// TODO do not enable if preferences apEnable = false
|
|
||||||
// but prepare later switch on by config key
|
|
||||||
WiFi.softAP(wifi_ssid, wifi_pass, channel, hidden, WIFI_MAX_STA);
|
|
||||||
|
|
||||||
// IPAddress ap_addr(192, 168, 15, 1);
|
// IPAddress ap_addr(192, 168, 15, 1);
|
||||||
IPAddress ap_addr;
|
IPAddress ap_addr;
|
||||||
@@ -287,7 +275,14 @@ void setup() {
|
|||||||
IPAddress ap_gateway(ap_addr);
|
IPAddress ap_gateway(ap_addr);
|
||||||
|
|
||||||
WiFi.softAPConfig(ap_addr, ap_gateway, ap_subnet);
|
WiFi.softAPConfig(ap_addr, ap_gateway, ap_subnet);
|
||||||
ap_enabled = true;
|
|
||||||
|
// Only enable if preferences apEnable is true
|
||||||
|
// But later switch on by config key is possible
|
||||||
|
ap_hidden = config.getBool("apHidden");
|
||||||
|
ap_enabled = config.getBool("apEnable");
|
||||||
|
if (ap_enabled) {
|
||||||
|
WiFi.softAP(wifi_ssid, wifi_pass, WIFI_CHANNEL, ap_hidden, WIFI_MAX_STA);
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize WebGUI
|
// Initialize WebGUI
|
||||||
webserver_init();
|
webserver_init();
|
||||||
@@ -309,12 +304,11 @@ void setup() {
|
|||||||
1 // LoadEquivalency (LEN)
|
1 // LoadEquivalency (LEN)
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO Read configuration information from preferences
|
|
||||||
// Manufacturer information is not changeable
|
// Manufacturer information is not changeable
|
||||||
NMEA2000.SetConfigurationInformation(
|
NMEA2000.SetConfigurationInformation(
|
||||||
"Open boat projects / NMEA2000 library", // Manufacturer
|
"Open Boat Projects / NMEA2000 library", // Manufacturer
|
||||||
"", // Info 1
|
config.getCString("instDesc1"), // Info 1
|
||||||
"" // Info 2
|
config.getCString("instDesc2") // Info 2
|
||||||
);
|
);
|
||||||
|
|
||||||
uint32_t uid; // ISO 21bit identity number devived from chipid (MAC)
|
uint32_t uid; // ISO 21bit identity number devived from chipid (MAC)
|
||||||
@@ -354,8 +348,6 @@ void setup() {
|
|||||||
// Debug: NMEA2000.EnableForward(true);
|
// Debug: NMEA2000.EnableForward(true);
|
||||||
NMEA2000.Open();
|
NMEA2000.Open();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
led_init();
|
led_init();
|
||||||
|
|
||||||
// Buzzer
|
// Buzzer
|
||||||
@@ -373,7 +365,7 @@ void setup() {
|
|||||||
digitalWrite(RGBLED_R, LOW); // boot status off
|
digitalWrite(RGBLED_R, LOW); // boot status off
|
||||||
delay(500);
|
delay(500);
|
||||||
led_test();
|
led_test();
|
||||||
|
|
||||||
// select current destination
|
// select current destination
|
||||||
switch (destination) {
|
switch (destination) {
|
||||||
case 'A':
|
case 'A':
|
||||||
@@ -388,8 +380,6 @@ void setup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// I²C
|
// I²C
|
||||||
// Serial.print("SHT31_LIB_VERSION: ");
|
|
||||||
// Serial.println(SHT31_LIB_VERSION);
|
|
||||||
LOGI(TAG, "SHT31_LIB_VERSION: %s", SHT31_LIB_VERSION);
|
LOGI(TAG, "SHT31_LIB_VERSION: %s", SHT31_LIB_VERSION);
|
||||||
Wire.begin(I2C_SDA, I2C_SCL);
|
Wire.begin(I2C_SDA, I2C_SCL);
|
||||||
Wire.setClock(I2C_SPEED);
|
Wire.setClock(I2C_SPEED);
|
||||||
@@ -613,7 +603,7 @@ void loop() {
|
|||||||
ap_enabled = false;
|
ap_enabled = false;
|
||||||
} else {
|
} else {
|
||||||
Serial.println("Enable Accesspoint");
|
Serial.println("Enable Accesspoint");
|
||||||
WiFi.softAP(wifi_ssid, wifi_pass);
|
WiFi.softAP(wifi_ssid, wifi_pass, WIFI_CHANNEL, ap_hidden, WIFI_MAX_STA);
|
||||||
ap_enabled = true;
|
ap_enabled = true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -638,7 +628,7 @@ void loop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NMEA2000.loop();
|
// NMEA2000.loop(); // not implemented yet
|
||||||
NMEA2000.ParseMessages();
|
NMEA2000.ParseMessages();
|
||||||
|
|
||||||
if ((millis() - lastSensor >= 5000) and sht_available) {
|
if ((millis() - lastSensor >= 5000) and sht_available) {
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ void webserver_init() {
|
|||||||
|
|
||||||
// API fast hack
|
// API fast hack
|
||||||
server.on("/api/capabilities", HTTP_GET, [](AsyncWebServerRequest *request) {
|
server.on("/api/capabilities", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||||
|
LOGD(TAG, "capabilities called");
|
||||||
StaticJsonDocument<100> doc;
|
StaticJsonDocument<100> doc;
|
||||||
doc["apPwChange"] = "true";
|
doc["apPwChange"] = "true";
|
||||||
String out;
|
String out;
|
||||||
@@ -91,6 +92,7 @@ void webserver_init() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
server.on("/api/checkpass", HTTP_GET, [](AsyncWebServerRequest *request) {
|
server.on("/api/checkpass", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||||
|
LOGD(TAG, "checkpass called");
|
||||||
StaticJsonDocument<100> doc;
|
StaticJsonDocument<100> doc;
|
||||||
doc["status"] = "FAILED";
|
doc["status"] = "FAILED";
|
||||||
String out;
|
String out;
|
||||||
@@ -99,24 +101,28 @@ void webserver_init() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
server.on("/api/config", HTTP_GET, [](AsyncWebServerRequest *request) {
|
server.on("/api/config", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||||
|
LOGD(TAG, "config called");
|
||||||
StaticJsonDocument<1024> doc;
|
StaticJsonDocument<1024> doc;
|
||||||
doc["systemName"] = config.getString("systemName");
|
doc["systemName"] = config.getString("systemName");
|
||||||
doc["systemMode"] = String(config.getChar("systemMode"));
|
doc["systemMode"] = String(config.getChar("systemMode"));
|
||||||
|
doc["instDesc1"] = config.getString("instDesc1");
|
||||||
|
doc["instDesc2"] = config.getString("instDesc2");
|
||||||
doc["logLevel"] = loglevel;
|
doc["logLevel"] = loglevel;
|
||||||
doc["version"] = VERSION;
|
doc["version"] = VERSION;
|
||||||
doc["fwtype"] = "unknown"; // TODO ?
|
doc["fwtype"] = "unknown"; // TODO ?
|
||||||
doc["salt"] = "secret";
|
doc["salt"] = "secret";
|
||||||
doc["AdminPassword"] = "********";
|
doc["AdminPassword"] = "********";
|
||||||
doc["useAdminPass"] = "false"; //config.getBool("useAdminPass") ? "true" : "false";
|
doc["useAdminPass"] = config.getBool("useAdminPass") ? "true" : "false";
|
||||||
doc["apEnable"] = "true";
|
doc["apEnable"] = config.getBool("apEnable") ? "true" : "false";
|
||||||
doc["apIp"] = config.getString("apIp");
|
doc["apIp"] = config.getString("apIp");
|
||||||
doc["apMask"] = config.getString("apMask");
|
doc["apMask"] = config.getString("apMask");
|
||||||
doc["apPassword"] = "********";
|
doc["apPassword"] = "********";
|
||||||
doc["stopApTime"] = config.getShort("stopApTime");
|
doc["stopApTime"] = config.getShort("stopApTime");
|
||||||
|
doc["apHidden"] = config.getBool("apHidden") ? "true" : "false";
|
||||||
doc["cpuSpeed"] = 160;
|
doc["cpuSpeed"] = 160;
|
||||||
doc["tempFormat"] = String(config.getChar("tempFormat"));
|
doc["tempFormat"] = String(config.getChar("tempFormat"));
|
||||||
doc["ledBrightness"] = led_brightness;
|
doc["ledBrightness"] = led_brightness;
|
||||||
doc["ledRgbBrightness"] = rgb_brightness;
|
doc["rgbBrightness"] = rgb_brightness;
|
||||||
doc["switchBank"] = config.getByte("switchBank");
|
doc["switchBank"] = config.getByte("switchBank");
|
||||||
doc["key1"] = keycode[BUTTON_1];
|
doc["key1"] = keycode[BUTTON_1];
|
||||||
doc["key2"] = keycode[BUTTON_2];
|
doc["key2"] = keycode[BUTTON_2];
|
||||||
@@ -140,6 +146,7 @@ void webserver_init() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
server.on("/api/resetconfig", HTTP_GET, [](AsyncWebServerRequest *request) {
|
server.on("/api/resetconfig", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||||
|
LOGD(TAG, "resetconfig called");
|
||||||
StaticJsonDocument<100> doc;
|
StaticJsonDocument<100> doc;
|
||||||
doc["status"] = "FAILED";
|
doc["status"] = "FAILED";
|
||||||
String out;
|
String out;
|
||||||
@@ -190,9 +197,37 @@ void webserver_init() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
server.on("/api/setconfig", HTTP_POST, [](AsyncWebServerRequest *request) {
|
server.on("/api/setconfig", HTTP_POST, [](AsyncWebServerRequest *request) {
|
||||||
LOGD(TAG, "API setconfig called");
|
LOGD(TAG, "setconfig called");
|
||||||
|
// TODO _hash must be first parameter
|
||||||
|
int count = request->params();
|
||||||
|
bool need_save = false;
|
||||||
|
LOGD(TAG, "setconfig Received %d params", count);
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
const AsyncWebParameter* p = request->getParam(i);
|
||||||
|
if (!config.hasKey(p->name().c_str())) {
|
||||||
|
LOGD(TAG, "POST %s=%s; key not found!", p->name(), p->value());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
LOGD(TAG, "POST %s=%s", p->name(), p->value());
|
||||||
|
// get old value for comparison
|
||||||
|
ConfigValue oldval = config.get(p->name().c_str());
|
||||||
|
config.setValue(p->name().c_str(), p->value().c_str());
|
||||||
|
// check if value was changed
|
||||||
|
if (config.get(p->name().c_str()) == oldval) {
|
||||||
|
// comparison of variants!
|
||||||
|
LOGD(TAG, "Values are equal. No change detected.");
|
||||||
|
} else {
|
||||||
|
need_save = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (need_save) {
|
||||||
|
LOGI(TAG, "Writing changed values to NVS");
|
||||||
|
config.save();
|
||||||
|
} else {
|
||||||
|
LOGI(TAG, "No changes, no action taken");
|
||||||
|
}
|
||||||
StaticJsonDocument<100> doc;
|
StaticJsonDocument<100> doc;
|
||||||
doc["status"] = "FAILED";
|
doc["status"] = "OK";
|
||||||
String out;
|
String out;
|
||||||
serializeJson(doc, out);
|
serializeJson(doc, out);
|
||||||
request->send(200, "application/json", out);
|
request->send(200, "application/json", out);
|
||||||
@@ -201,13 +236,13 @@ void webserver_init() {
|
|||||||
server.on("/api/update", HTTP_POST, [](AsyncWebServerRequest *request) {
|
server.on("/api/update", HTTP_POST, [](AsyncWebServerRequest *request) {
|
||||||
// the request handler is triggered after the upload has finished...
|
// the request handler is triggered after the upload has finished...
|
||||||
// create the response, add header, and send response
|
// create the response, add header, and send response
|
||||||
|
LOGD(TAG, "update called");
|
||||||
StaticJsonDocument<100> doc;
|
StaticJsonDocument<100> doc;
|
||||||
doc["status"] = "FAILED";
|
doc["status"] = "FAILED";
|
||||||
String out;
|
String out;
|
||||||
serializeJson(doc, out);
|
serializeJson(doc, out);
|
||||||
request->send(200, "application/json", out);
|
request->send(200, "application/json", out);
|
||||||
}, [](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final){
|
}, [](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
|
||||||
// this is the new image upload part
|
// this is the new image upload part
|
||||||
Serial.print("Retrieving firmware image named: ");
|
Serial.print("Retrieving firmware image named: ");
|
||||||
Serial.println(filename);
|
Serial.println(filename);
|
||||||
@@ -230,6 +265,7 @@ void webserver_init() {
|
|||||||
|
|
||||||
server.on("/api/devicelist", HTTP_GET, [](AsyncWebServerRequest *request) {
|
server.on("/api/devicelist", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||||
// NMEA2000 device list
|
// NMEA2000 device list
|
||||||
|
LOGD(TAG, "devicelist called");
|
||||||
AsyncResponseStream *response = request->beginResponseStream("application/json");
|
AsyncResponseStream *response = request->beginResponseStream("application/json");
|
||||||
response->print("[");
|
response->print("[");
|
||||||
bool first = true;
|
bool first = true;
|
||||||
|
|||||||
@@ -115,6 +115,14 @@
|
|||||||
"description": "Stop the access point after that many minutes if not used.\n1 to 60 minutes.\n\n'0' means that the access point is permanently enabled.",
|
"description": "Stop the access point after that many minutes if not used.\n1 to 60 minutes.\n\n'0' means that the access point is permanently enabled.",
|
||||||
"category": "Wifi"
|
"category": "Wifi"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "apHidden",
|
||||||
|
"label": "Hide SSID",
|
||||||
|
"type": "boolean",
|
||||||
|
"default": "false",
|
||||||
|
"description": "Hide Wifi SSID",
|
||||||
|
"category": "Wifi"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "cpuSpeed",
|
"name": "cpuSpeed",
|
||||||
"label": "CPU Speed [MHz]",
|
"label": "CPU Speed [MHz]",
|
||||||
@@ -139,7 +147,7 @@
|
|||||||
"category": "Hardware"
|
"category": "Hardware"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "ledRgbBrightness",
|
"name": "rgbBrightness",
|
||||||
"label": "RGB-LED brightness",
|
"label": "RGB-LED brightness",
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"default": 64,
|
"default": 64,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -14,14 +13,12 @@ body {
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin: 0.2em;
|
margin: 0.2em;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
#tabs {
|
#tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -31,53 +28,51 @@ body {
|
|||||||
#tabs .tab {
|
#tabs .tab {
|
||||||
background-color: lightgray;
|
background-color: lightgray;
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
/*border: 1px;
|
|
||||||
border-color: grey;
|
|
||||||
border-style: solid; */
|
|
||||||
border: 1px solid grey;
|
border: 1px solid grey;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
#tabs .tab.active {
|
#tabs .tab.active {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#tabPages {
|
#tabPages {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding-bottom: 1ex;
|
padding-bottom: 1ex;
|
||||||
border-bottom: 1px solid grey;
|
border-bottom: 1px solid grey;
|
||||||
}
|
}
|
||||||
|
hr {
|
||||||
|
border-top: 1px solid grey;
|
||||||
|
}
|
||||||
.configForm {
|
.configForm {
|
||||||
padding-bottom: 0.5em;
|
padding-bottom: 0.5em;
|
||||||
}
|
}
|
||||||
.configForm .buttons {
|
.configForm .buttons {
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
}
|
}
|
||||||
.configForm .content>div:nth-child(even) {
|
.configForm .content>div:nth-child(even) {
|
||||||
background-color: rgb(211 211 211 / 43%);
|
background-color: rgb(211 211 211 / 43%);
|
||||||
}
|
}
|
||||||
#statusPage .even {
|
#statusPage .even {
|
||||||
background-color: rgb(211 211 211 / 43%);
|
background-color: rgb(211 211 211 / 43%);
|
||||||
}
|
}
|
||||||
#statusPageContent {
|
#statusPageContent {
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
}
|
}
|
||||||
.counter-row .value{
|
.counter-row .value{
|
||||||
text-align: right;
|
text-align: right;
|
||||||
width: 6em;
|
width: 6em;
|
||||||
}
|
}
|
||||||
.icon-row .label{
|
.icon-row .label{
|
||||||
width: 8.7em;
|
width: 8.7em;
|
||||||
}
|
}
|
||||||
.category .title .label {
|
.category .title .label {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
margin-left: 1em;
|
margin-left: 1em;
|
||||||
}
|
}
|
||||||
.changed input{
|
.changed input{
|
||||||
color: green;
|
color: green;
|
||||||
}
|
}
|
||||||
.changed select{
|
.changed select{
|
||||||
color: green;
|
color: green;
|
||||||
}
|
}
|
||||||
.category.changed{
|
.category.changed{
|
||||||
color: green;
|
color: green;
|
||||||
@@ -160,7 +155,6 @@ button.addunassigned {
|
|||||||
display: flex;
|
display: flex;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overlay {
|
.overlay {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
@@ -214,7 +208,6 @@ h1 {
|
|||||||
.icon-eye.active{
|
.icon-eye.active{
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dash {
|
.dash {
|
||||||
width: 6.5em;
|
width: 6.5em;
|
||||||
height: 4em;
|
height: 4em;
|
||||||
@@ -242,10 +235,10 @@ div#dashboardPage {
|
|||||||
}
|
}
|
||||||
.dashValue.formatLatitude {
|
.dashValue.formatLatitude {
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
}
|
}
|
||||||
.dashValue.formatLongitude {
|
.dashValue.formatLongitude {
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
}
|
}
|
||||||
.dashValue.formatDate {
|
.dashValue.formatDate {
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
}
|
}
|
||||||
@@ -255,12 +248,12 @@ div#dashboardPage {
|
|||||||
padding: 0.1em;
|
padding: 0.1em;
|
||||||
background-color: lightgray;
|
background-color: lightgray;
|
||||||
font-size: 0.7em;
|
font-size: 0.7em;
|
||||||
}
|
|
||||||
.footer .unit{
|
|
||||||
}
|
}
|
||||||
.footer .source{
|
.footer .unit {
|
||||||
|
}
|
||||||
|
.footer .source {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
#adminPassInput {
|
#adminPassInput {
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
}
|
}
|
||||||
@@ -273,7 +266,6 @@ div#uploadProgress {
|
|||||||
max-width: 20em;
|
max-width: 20em;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
margin-left: 1em;
|
margin-left: 1em;
|
||||||
/* margin-right: auto; */
|
|
||||||
border: 1px solid grey;
|
border: 1px solid grey;
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
|
|||||||
@@ -198,7 +198,7 @@
|
|||||||
checkers.checkPort=function(v,allValues,def){
|
checkers.checkPort=function(v,allValues,def){
|
||||||
let parsed=parseInt(v);
|
let parsed=parseInt(v);
|
||||||
if (isNaN(parsed)) return "must be a number";
|
if (isNaN(parsed)) return "must be a number";
|
||||||
if (parsed <1 || parsed >= 65536) return "port must be in the range 1..65536";
|
if (parsed <1 || parsed >= 65535) return "port must be in the range 1..65535";
|
||||||
}
|
}
|
||||||
checkers.checkSystemName=function(v) {
|
checkers.checkSystemName=function(v) {
|
||||||
//2...32 characters for ssid
|
//2...32 characters for ssid
|
||||||
@@ -293,6 +293,7 @@
|
|||||||
let url = apiPrefix + "/api/setconfig"
|
let url = apiPrefix + "/api/setconfig"
|
||||||
let body = "_hash=" + encodeURIComponent(pass) + "&";
|
let body = "_hash=" + encodeURIComponent(pass) + "&";
|
||||||
let allValues = getAllConfigs();
|
let allValues = getAllConfigs();
|
||||||
|
console.log(allValues);
|
||||||
if (!allValues) return;
|
if (!allValues) return;
|
||||||
for (let name in allValues) {
|
for (let name in allValues) {
|
||||||
if (name == 'adminPassword') {
|
if (name == 'adminPassword') {
|
||||||
@@ -303,7 +304,6 @@
|
|||||||
fetch(url, {
|
fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
//'Content-Type': 'application/octet-stream' //we must lie here
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded'
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
},
|
},
|
||||||
body: body
|
body: body
|
||||||
@@ -323,6 +323,7 @@
|
|||||||
alert("unable to set config: " + status.status);
|
alert("unable to set config: " + status.status);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.catch(err => console.error(err));
|
||||||
})
|
})
|
||||||
.catch(function (e) { alert(e); })
|
.catch(function (e) { alert(e); })
|
||||||
}
|
}
|
||||||
@@ -633,6 +634,9 @@
|
|||||||
el = addEl('input', clazz, frame);
|
el = addEl('input', clazz, frame);
|
||||||
if (configItem.readOnly) el.setAttribute('disabled', true);
|
if (configItem.readOnly) el.setAttribute('disabled', true);
|
||||||
el.setAttribute('name', configItem.name)
|
el.setAttribute('name', configItem.name)
|
||||||
|
if (configItem.type === 'string') {
|
||||||
|
// TODO limit length
|
||||||
|
}
|
||||||
if (configItem.type === 'password') {
|
if (configItem.type === 'password') {
|
||||||
el.setAttribute('type', 'password');
|
el.setAttribute('type', 'password');
|
||||||
let vis = addEl('span', 'icon-eye icon', frame);
|
let vis = addEl('span', 'icon-eye icon', frame);
|
||||||
|
|||||||
Reference in New Issue
Block a user