void reset() {
messageModal(F("Сбрасываю настройки"));
+ delay(2000);
if (File f = LittleFS.open(F("/config.txt"),"w")) {
f.print(distConfig);
f.close();
}
}
+function downloadFile(url, name) {
+ const a = document.createElement('a')
+ a.href = url
+ a.download = name?name:url.split('/').pop()
+ document.body.appendChild(a)
+ a.click()
+ document.body.removeChild(a)
+}
+
+function uploadConfig() {
+ var elem = document.getElementById('_config_file')
+ if (elem.value) {
+ var data = elem.files[0]
+ console.log(data)
+ fetch('/config/put', {method:'PUT',body:data});
+ elem.value = null
+ }
+}
+
function elementHTML(element) {
var value
if (parameters[element.id] || !isNaN(parameters[element.id])) {
now.setMinutes(now.getMinutes() - now.getTimezoneOffset())
value = now.toISOString().slice(0, -1);
return '<div class="pure-u-1 pure-u-md-1-3"><label for="_ui_element_' + element.id + '">' + encode(element.label) + '</label>'
- +'<div class="timesetter"><input id="_ui_element_'+element.id+'" data-ui_class="timeset" class="inline-input" type="datetime-local" value="'+value+'">'
- + '<div class="send-button" onclick="sendTime(\''+element.id+'\')">-></div></div></div>'
+ +'<input id="_ui_element_'+element.id+'" data-ui_class="timeset" class="inline-input" type="datetime-local" value="'+value+'">'
+ + '<div class="send-button" onclick="sendTime(\''+element.id+'\')">-></div></div>'
case 'text':
return '<div class="pure-u-1 pure-u-md-1-3"><h2 id="_ui_element_'+ element.id +'" ' + (element.color?'style="color:'+ element.color+'" ':'')+ '>' + encode(value) + '</h2></div>'
case 'number':
+ '<input data-ui_class="range" type="range" '+ (!isNaN(element.min)?'min="'+element.min+'" ':'') + (!isNaN(element.max)?'max="'+element.max+'" ':'') + (!isNaN(element.step)?'step="'+element.step+'" ':'')
+ 'id="_ui_element_' + element.id + '" value="' + encode(value) +'"'+ pattern
+ ' class="pure-u-1" maxlength="99" oninput="sendUpdate(\'' + element.id + '\')" /></div>'
+ case 'config':
+ return '<div class="pure-u-1 pure-u-md-1-3"><label for="_ui_element_' + element.id + '">' + encode(element.label) + '</label>'
+ + '<div align="center"><input type="button" class="pure-button row-button" value="Сохранить..." onclick="downloadFile(\'/config/get\',\'config.json\')">'
+ + '<label for="_config_file" class="pure-button row-button">Восстановить...</label>'
+ + '<input type="button" class="pure-button row-button" value="Настройки по умолчанию" onclick="confirm(\'Вы точно хотите сбросить настройки?\')?sendAction(\'reset\'):console.log(\'Не надо так не надо...\')">'
+ + '<input type="file" id="_config_file" onchange="uploadConfig()" style="visibility:hidden">'
+ + '</div></div>'
case 'table':
default:
return '<div class="pure-u-1 pure-u-md-1-3"><table class="texttable" cellpadding="5" border="0" align="center"><tbody><tr><td class="value-name" align="right">'
function initES() {
if (!!window.EventSource) {
var source = new EventSource('/events');
+ openMsg('Соединение установлено')
source.onerror = function(e) {
if (source.readyState == 2) {
+ openMsg('Соединение прервано')
setTimeout(initES, 5000);
}
};
/* Time setter */
-.timesetter {
- display: inline;
-}
-
.inline-input.inline-input.inline-input {
display: inline-block;
width: calc(100% - 5em)
padding-right: 1em;
}
+/* Config */
+
+input[type=file]::file-selector-button {
+ display: none;
+}
+
+input[type=file]::-webkit-file-upload-button {
+ display: block;
+ width: 0;
+ height: 0;
+ margin-left: -100%;
+}
+
+input[type=file]::-ms-browse {
+ display: none;
+}
+
+.row-button.row-button.row-button {
+ display: inline-block;
+ margin: 0 2em 0 2em
+}
+
/* fonts */
@font-face {
void message(const char* str, int priority) {
if (priority > currentPriority) return;
+ currentPriority = priority;
utf8rus(str, msgBuf, 255);
Panel->displayClear();
Panel->setFont(RomanCyrillic);
void message(const __FlashStringHelper* str, int priority) {
if (priority > currentPriority) return;
+ currentPriority = priority;
char buf[256];
strncpy_P(buf, (PGM_P)str, 255);
utf8rus(buf, msgBuf, 255);
void messageModal(const char* str) {
message(str);
- while (!(Panel->displayAnimate())) { delay(50); }
+ while (!(Panel->displayAnimate())) { delay(10); }
}
void messageModal(const __FlashStringHelper* str) {
message(str);
- while (!(Panel->displayAnimate())) { delay(50); }
+ while (!(Panel->displayAnimate())) { delay(10); }
}
void scroll(const char* str, bool force) {
project:
name: WiFi Clock
- version: 0.1.2
+ version: 0.2.0
contacts:
- mailto:rvb@rvb.name
- tg:rvbglas
title: Погода
icon: ""
elements:
+ - id: enable_weather
+ label: Использовать погодный сервис
+ type: checkbox
- id: weather_url
label: URL погодного сервиса
type: input
type: button
label: Сменить пароль
- type: hr
+ - type: text
+ value: Конфигурация
+ - id: _config
+ type: config
+ label: Сохранение и восстановление настроек
+ - type: hr
- type: text
value: Синхронизация времени
- id: _timeset
#include <ESPAsyncWebServer.h>
#include <StreamString.h>
#include <Ticker.h>
+#include "ArduinoJson.h"
+#include "AsyncJson.h"
bool isApEnabled = false;
bool isWebStarted = false;
}
if(request->hasParam("name")) {
const char* action = request->getParam("name")->value().c_str();
- if (strcmp(action,"restart") == 0) {
+ if (strcmp(action,"reset") == 0) {
+ millisScheduled = millis();
+ actionScheduled = "reset";
+ request->send(200,"application/json", "{\"result\":\"OK\",\"message\":\"Сбрасываю настройки и перезагружаюсь\"}");
+ } else if (strcmp(action,"restart") == 0) {
if (pendingWiFi) {
request->send(200,"application/json", "{\"result\":\"FAILED\",\"message\":\"Не применены настройки WiFi\", \"page\":\"wifi\"}");
} else if (pendingAuth) {
unsigned long timestamp = atoi(request->getParam("timestamp")->value().c_str());
if (timestamp) {
timeval tv = { timestamp, 0 };
- settimeofday(&tv, nullptr);
+ settimeofday(&tv, nullptr);
if (isRTCEnabled) {
Serial.println(F("Время установлено вручную"));
RTC.adjust(DateTime(timestamp));
}
});
+ AsyncCallbackJsonWebHandler* configUploadHandler = new AsyncCallbackJsonWebHandler("/config/put", [](AsyncWebServerRequest *request, JsonVariant &json) {
+ cfg.clear();
+ // first - set values
+ for( JsonPair kv : json.as<JsonObject>() ) {
+ const char* name = kv.key().c_str();
+ if (kv.value().is<bool>()) {
+ cfg.setValue(name, kv.value().as<bool>());
+ } else if (kv.value().is<int>()) {
+ cfg.setValue(name, kv.value().as<int>());
+ } else if (kv.value().is<double>()) {
+ cfg.setValue(name, kv.value().as<double>());
+ } else if (kv.value().is<char*>()) {
+ cfg.setValue(name, kv.value().as<char*>());
+ } else {
+ Serial.print(F("Неопознанный тип значения параметра ")); Serial.print(name); Serial.print(": "); Serial.println(kv.value().as<String>().c_str());
+ cfg.clear();
+ setupConfig();
+ request->send(500, "text/plain", "Unknown parameter type");
+ }
+ }
+ // second - handle all changes
+ for( JsonPair kv : json.as<JsonObject>() ) {
+ apply(kv.key().c_str());
+ }
+ pendingWiFi = false;
+ pendingAuth = false;
+ saveConfig();
+ message(F("Применены сохраненные настройки"),5);
+ reportMessage(F("Применены сохраненные настройки"));
+ millisScheduled = millis() + 10000;
+ actionScheduled = "restart";
+ request->send(200,"application/json", "{\"result\":\"OK\",\"message\":\"Настройки восстановлены из резервной копии\"}");
+ });
+
+ server.addHandler(configUploadHandler).setAuthentication(auth_user,auth_pwd);
+
server.serveStatic("ui", LittleFS, "/ui.json").setAuthentication(auth_user,auth_pwd);
server.serveStatic("/", LittleFS, "/web/").setDefaultFile("index.html").setAuthentication(auth_user,auth_pwd);
if (actionScheduled && millis()>millisScheduled+300) {
Serial.print(F("Запланированная операция ")); Serial.println(actionScheduled);
//
- if (strcmp(actionScheduled,"restart") == 0) {
+ if (strcmp(actionScheduled,"reset") == 0) {
+ server.end();
+ reset();
+ } else if (strcmp(actionScheduled,"restart") == 0) {
server.end();
reboot();
} else if (strcmp(actionScheduled,"auth") == 0) {