", sizeof(s));
-
- // Check to see if we've been sent any arguments and instantly return if not
- if (webServer.args() == 0) {
- strlcat(s,
- "
Are you absolutely sure you want to erase all settings? Typing 'yes' will restart the System and you'll need to reconnect to the wifi "
- "Access Point called ems-esp.
",
- sizeof(s));
-
- strlcat(s, webResetAllPage_form, sizeof(s));
- strlcat(s, webCommonPage_end, sizeof(s));
- webServer.sendHeader("Content-Length", String(strlen(s)));
- webServer.send(200, "text/html", s);
- } else {
- // delete all settings
- if (webServer.arg("confirm") == "yes") {
- _fs_eraseConfig();
- delay(1000); // wait 1 sec
- resetESP();
- }
- }
-}
-
-// set up web server
-void MyESP::_webserver_setup() {
- webServer.on("/", [this]() { _webRootPage(); });
- webServer.on("/reset", [this]() { _webResetPage(); });
- webServer.on("/resetall", [this]() { _webResetAllPage(); });
-
- webServer.begin();
-
- myDebug_P(PSTR("[WEB] Web server started"));
-}
-
-// bootup sequence
-// quickly flash LED until we get a Wifi connection, or AP established
-// fast way is to use WRITE_PERI_REG(PERIPHS_GPIO_BASEADDR + (state ? 4 : 8), (1 << EMSESP_Status.led_gpio)); // 4 is on, 8 is off
-void MyESP::_bootupSequence() {
- uint8_t boot_status = getSystemBootStatus();
-
- if ((boot_status == MYESP_BOOTSTATUS_BOOTED) || (millis() <= MYESP_BOOTUP_DELAY)) {
- return; // already booted, or still starting up
- }
-
- // only kick in after a few seconds
- if (boot_status == MYESP_BOOTSTATUS_POWERON) {
- _setSystemBootStatus(MYESP_BOOTSTATUS_BOOTING);
- }
-
- static uint32_t last_bootupflash = 0;
-
- // flash LED quickly
- if ((millis() - last_bootupflash > MYESP_BOOTUP_FLASHDELAY)) {
- last_bootupflash = millis();
- int state = digitalRead(LED_BUILTIN);
- digitalWrite(LED_BUILTIN, !state);
- }
-
- if (isWifiConnected()) {
- _setSystemBootStatus(MYESP_BOOTSTATUS_BOOTED); // completed, reset flag
- digitalWrite(LED_BUILTIN, HIGH); // turn off LED
- }
-}
-
-// setup MyESP
-void MyESP::begin(const char * app_hostname, const char * app_name, const char * app_version) {
- _app_hostname = strdup(app_hostname);
- _app_name = strdup(app_name);
- _app_version = strdup(app_version);
-
- _telnet_setup(); // Telnet setup, called first to set Serial
-
- // print a welcome message
- myDebug_P(PSTR("\n\n* %s version %s"), _app_name, _app_version);
-
- // set up onboard LED
- pinMode(LED_BUILTIN, OUTPUT);
- digitalWrite(LED_BUILTIN, HIGH);
-
- _getInitialFreeHeap(); // get initial free mem
- _rtcmemSetup(); // rtc internal mem setup
-
- if (getSystemBootStatus() == MYESP_BOOTSTATUS_RESETNEEDED) {
- myDebug_P(PSTR("** resetting all settings"));
- _firstInstall = true; // flag as an initial install so the config file will be recreated
- }
-
- _eeprom_setup(); // set up EEPROM for storing crash data, if compiled with -DCRASH
- _fs_setup(); // SPIFFS setup, do this first to get values
- _wifi_setup(); // WIFI setup
- _ota_setup(); // init OTA
- _webserver_setup(); // init web server
-
- _setSystemCheck(false); // reset system check
- _heartbeatCheck(true); // force heartbeat check (not the MQTT one)
-
- SerialAndTelnet.flush();
-}
-
-/*
- * Loop. This is called as often as possible and it handles wifi, telnet, mqtt etc
- */
-void MyESP::loop() {
- jw.loop(); // WiFi
- ArduinoOTA.handle(); // OTA
-
- if (_ota_doing_update) {
- return; // quit if in the middle of an OTA update
- }
-
- _calculateLoad();
- _systemCheckLoop();
- _heartbeatCheck();
- _bootupSequence();
- webServer.handleClient();
- _telnetHandle();
- _mqttConnect();
-
- yield(); // ...and breath
-}
-
-MyESP myESP; // create instance
diff --git a/platformio.ini-example b/platformio.ini-example
index fec449cab..40424e9d5 100644
--- a/platformio.ini-example
+++ b/platformio.ini-example
@@ -8,40 +8,69 @@ default_envs = release
[common]
; -DMYESP_TIMESTAMP -DTESTS -DCRASH -DFORCE_SERIAL -DNO_GLOBAL_EEPROM -DLOGICANALYZER
-extra_flags = -DNO_GLOBAL_EEPROM
+;general_flags = -Wl,-Teagle.flash.4m2m.ld
+general_flags =
[env]
+;board = esp12e
board = d1_mini
-; board = nodemcuv2
-; board = d1_mini_pro
framework = arduino
platform = espressif8266
lib_deps =
CRC32
CircularBuffer
+ OneWire
JustWifi
AsyncMqttClient
ArduinoJson
- OneWire
EEPROM_rotate
+ ESP Async WebServer
+ ESPAsyncTCP
+ ESPAsyncUDP
upload_speed = 921600
monitor_speed = 115200
+
+; example ports for OSX
+;upload_port = /dev/cu.wchusbserial14403
+;upload_port = /dev/cu.usbserial-1440
+
; uncomment next 2 lines for OTA
;upload_protocol = espota
;upload_port = ems-esp.local
+[env:buildweb]
+extra_scripts = pre:scripts/buildweb.py
+
+[env:test]
+build_type = debug
+build_flags = ${common.general_flags} -DTESTS
+extra_scripts =
+ pre:scripts/rename_fw.py
+ pre:scripts/buildweb.py
+
+[env:crash]
+build_type = debug
+build_flags = ${common.general_flags} -DNO_GLOBAL_EEPROM -DCRASH
+extra_scripts =
+ pre:scripts/rename_fw.py
+ pre:scripts/buildweb.py
+
[env:debug]
build_type = debug
-build_flags = ${common.extra_flags} -DCRASH
-extra_scripts = pre:scripts/rename_fw.py
+build_flags = ${common.general_flags}
+extra_scripts =
+ pre:scripts/rename_fw.py
+ pre:scripts/buildweb.py
[env:clean]
extra_scripts = pre:scripts/clean_fw.py
[env:release]
-build_flags = ${common.extra_flags}
+build_flags = ${common.general_flags}
extra_scripts = pre:scripts/rename_fw.py
[env:checkcode]
-build_flags = ${common.extra_flags}
+build_type = debug
+build_flags = ${common.general_flags} -Wall
extra_scripts = scripts/checkcode.py
+
diff --git a/scripts/buildweb.py b/scripts/buildweb.py
new file mode 100644
index 000000000..93a75510c
--- /dev/null
+++ b/scripts/buildweb.py
@@ -0,0 +1,3 @@
+Import("env")
+
+env.Execute("node ./tools/webfilesbuilder/node_modules/gulp/bin/gulp.js --cwd ./tools/webfilesbuilder")
diff --git a/scripts/rename_fw.py b/scripts/rename_fw.py
index 42a11881a..7d2d1845e 100755
--- a/scripts/rename_fw.py
+++ b/scripts/rename_fw.py
@@ -1,7 +1,13 @@
#!/usr/bin/env python
+from subprocess import call
+import os
import re
Import("env")
+def build_web(source, target, env):
+ print("\n** Build web...")
+ call(["gulp", "-f", os.getcwd()+"/tools/webfilesbuilder/gulpfile.js"])
+
bag = {}
exprs = [
(re.compile(r'^#define APP_VERSION\s+"(\S+)"'), 'app_version'),
@@ -22,5 +28,9 @@ app_hostname = bag.get('app_hostname')
board = env['BOARD']
branch = env['PIOENV']
+# build the web files
+env.AddPreAction("buildprog", build_web)
+
# build filename, replacing . with _ for the version
env.Replace(PROGNAME="firmware_%s" % branch + "_" + app_version.replace(".", "_"))
+
diff --git a/src/MyESP.cpp b/src/MyESP.cpp
new file mode 100644
index 000000000..961828966
--- /dev/null
+++ b/src/MyESP.cpp
@@ -0,0 +1,2826 @@
+/*
+ * MyESP - my ESP helper class to handle WiFi, MQTT, Telnet, Web and other utils
+ *
+ * Paul Derbyshire - first revision: December 2018
+ *
+ * with ideas borrowed from Espurna https://github.com/xoseperez/espurna
+ * and web from https://github.com/esprfid/esp-rfid
+ */
+
+#include "MyESP.h"
+
+#ifdef CRASH
+EEPROM_Rotate EEPROMr;
+#endif
+
+union system_rtcmem_t {
+ struct {
+ uint8_t stability_counter;
+ uint8_t reset_reason;
+ uint8_t boot_status;
+ uint8_t _reserved_;
+ } parts;
+ uint32_t value;
+};
+
+// nasty global variables that are called from internal ws functions
+static char * _general_password = nullptr;
+static bool _shouldRestart = false;
+
+uint8_t RtcmemSize = (sizeof(RtcmemData) / 4u);
+auto Rtcmem = reinterpret_cast(RTCMEM_ADDR);
+
+// constructor
+MyESP::MyESP() {
+ _general_hostname = strdup("myesp");
+ _app_name = strdup("MyESP");
+ _app_version = strdup(MYESP_VERSION);
+ _app_url = nullptr;
+ _app_updateurl = nullptr;
+
+ // general
+ _timerequest = false;
+ _formatreq = false;
+ _suspendOutput = false;
+ _ota_pre_callback_f = nullptr;
+ _ota_post_callback_f = nullptr;
+ _load_average = 100; // calculated load average
+ _general_serial = true; // serial is set to on as default
+ _have_ntp_time = false;
+
+ // telnet
+ _command[0] = '\0';
+ _telnetcommand_callback_f = nullptr;
+ _telnet_callback_f = nullptr;
+
+ // fs
+ _fs_loadsave_callback_f = nullptr;
+ _fs_setlist_callback_f = nullptr;
+
+ // mqtt
+ _mqtt_ip = nullptr;
+ _mqtt_password = nullptr;
+ _mqtt_user = nullptr;
+ _mqtt_port = MQTT_PORT;
+ _mqtt_base = nullptr;
+ _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MIN;
+ _mqtt_last_connection = 0;
+ _mqtt_connecting = false;
+ _mqtt_enabled = false;
+ _mqtt_heartbeat = false;
+ _mqtt_keepalive = MQTT_KEEPALIVE;
+ _mqtt_qos = MQTT_QOS;
+ _mqtt_retain = MQTT_RETAIN;
+ _mqtt_will_topic = strdup(MQTT_WILL_TOPIC);
+ _mqtt_will_online_payload = strdup(MQTT_WILL_ONLINE_PAYLOAD);
+ _mqtt_will_offline_payload = strdup(MQTT_WILL_OFFLINE_PAYLOAD);
+
+ // network
+ _network_password = nullptr;
+ _network_ssid = nullptr;
+ _network_wmode = 1; // default AP
+
+ _wifi_callback_f = nullptr;
+ _wifi_connected = false;
+
+ // web
+ _web_callback_f = nullptr;
+ _webServer = new AsyncWebServer(80);
+ _ws = new AsyncWebSocket("/ws");
+ _http_username = strdup(MYESP_HTTP_PASSWORD);
+ _general_password = strdup(MYESP_HTTP_PASSWORD);
+
+ // system
+ _rtcmem_status = false;
+ _systemStable = true;
+
+ // ntp
+ _ntp_server = strdup(MYESP_NTP_SERVER);
+ ;
+ _ntp_interval = 60;
+ _ntp_enabled = false;
+
+ // get the build time
+ _buildTime = _getBuildTime();
+
+ // MQTT log
+ for (uint8_t i = 0; i < MYESP_MQTTLOG_MAX; i++) {
+ MQTT_log[i].timestamp = 0;
+ MQTT_log[i].topic = nullptr;
+ MQTT_log[i].payload = nullptr;
+ }
+}
+
+MyESP::~MyESP() {
+ end();
+}
+
+// end
+void MyESP::end() {
+ SPIFFS.end();
+ _ws->enable(false);
+ delete _webServer;
+ delete _ws;
+ SerialAndTelnet.end();
+ jw.disconnect();
+}
+
+// general debug to the telnet or serial channels
+void MyESP::myDebug(const char * format, ...) {
+ if (_suspendOutput)
+ return;
+
+ va_list args;
+ va_start(args, format);
+ char test[1];
+
+ int len = ets_vsnprintf(test, 1, format, args) + 1;
+
+ char * buffer = new char[len];
+ ets_vsnprintf(buffer, len, format, args);
+ va_end(args);
+
+ SerialAndTelnet.println(buffer);
+
+ delete[] buffer;
+}
+
+// for flashmemory. Must use PSTR()
+void MyESP::myDebug_P(PGM_P format_P, ...) {
+ if (_suspendOutput)
+ return;
+
+ char format[strlen_P(format_P) + 1];
+ memcpy_P(format, format_P, sizeof(format));
+
+ va_list args;
+ va_start(args, format_P);
+ char test[1];
+ int len = ets_vsnprintf(test, 1, format, args) + 1;
+
+ char * buffer = new char[len];
+ ets_vsnprintf(buffer, len, format, args);
+
+ va_end(args);
+
+#ifdef MYESP_TIMESTAMP
+ // capture & print timestamp
+ char timestamp[10] = {0};
+ snprintf_P(timestamp, sizeof(timestamp), PSTR("[%06lu] "), millis() % 1000000);
+ SerialAndTelnet.print(timestamp);
+#endif
+
+ SerialAndTelnet.println(buffer);
+
+ delete[] buffer;
+}
+
+// use Serial?
+bool MyESP::getUseSerial() {
+ return (_general_serial);
+}
+
+// heartbeat
+bool MyESP::getHeartbeat() {
+ return (_mqtt_heartbeat);
+}
+
+// init heap ram
+uint32_t MyESP::_getInitialFreeHeap() {
+ static uint32_t _heap = 0;
+
+ if (0 == _heap) {
+ _heap = ESP.getFreeHeap();
+ }
+
+ return _heap;
+}
+
+// called when WiFi is connected, and used to start OTA, MQTT
+void MyESP::_wifiCallback(justwifi_messages_t code, char * parameter) {
+ if ((code == MESSAGE_CONNECTED)) {
+ myDebug_P(PSTR("[WIFI] SSID %s"), WiFi.SSID().c_str());
+ myDebug_P(PSTR("[WIFI] CH %d"), WiFi.channel());
+ myDebug_P(PSTR("[WIFI] RSSI %d"), WiFi.RSSI());
+ myDebug_P(PSTR("[WIFI] IP %s"), WiFi.localIP().toString().c_str());
+ myDebug_P(PSTR("[WIFI] MAC %s"), WiFi.macAddress().c_str());
+ myDebug_P(PSTR("[WIFI] GW %s"), WiFi.gatewayIP().toString().c_str());
+ myDebug_P(PSTR("[WIFI] MASK %s"), WiFi.subnetMask().toString().c_str());
+ myDebug_P(PSTR("[WIFI] DNS %s"), WiFi.dnsIP().toString().c_str());
+ myDebug_P(PSTR("[WIFI] HOST %s"), _getESPhostname().c_str());
+
+ // start OTA
+ ArduinoOTA.begin(); // moved to support esp32
+ myDebug_P(PSTR("[OTA] listening to %s.local:%u"), ArduinoOTA.getHostname().c_str(), OTA_PORT);
+
+
+ myDebug_P(PSTR("[SYSTEM] Last reset info: %s"), (char *)ESP.getResetInfo().c_str()); // unconditionally show the last reset reason
+
+ // MQTT Setup
+ _mqtt_setup();
+
+ // if we don't want Serial anymore, turn it off
+ if (!_general_serial) {
+ myDebug_P(PSTR("[SYSTEM] Disabling serial port communication."));
+ SerialAndTelnet.flush(); // flush so all buffer is printed to serial
+ setUseSerial(false);
+ } else {
+ myDebug_P(PSTR("[SYSTEM] Serial port communication is enabled."));
+ }
+
+ _wifi_connected = true;
+
+ // NTP now that we have a WiFi connection
+ if (_ntp_enabled) {
+ NTP.Ntp(_ntp_server, _ntp_interval); // set up NTP server
+ myDebug_P(PSTR("[NTP] NTP internet time enabled via server %s"), _ntp_server);
+ }
+
+ // call any final custom stuff
+ if (_wifi_callback_f) {
+ _wifi_callback_f();
+ }
+
+ jw.enableAPFallback(false); // Disable AP mode after initial connect was successful
+ }
+
+ if (code == MESSAGE_ACCESSPOINT_CREATED) {
+ myDebug_P(PSTR("[WIFI] MODE AP"));
+ myDebug_P(PSTR("[WIFI] SSID %s"), jw.getAPSSID().c_str());
+ myDebug_P(PSTR("[WIFI] IP %s"), WiFi.softAPIP().toString().c_str());
+ myDebug_P(PSTR("[WIFI] MAC %s"), WiFi.softAPmacAddress().c_str());
+
+ // if we don't want Serial anymore, turn it off
+ if (!_general_serial) {
+ myDebug_P(PSTR("[SYSTEM] Disabling serial port communication."));
+ SerialAndTelnet.flush(); // flush so all buffer is printed to serial
+ setUseSerial(false);
+ } else {
+ myDebug_P(PSTR("[SYSTEM] Serial port communication is enabled."));
+ }
+
+ _wifi_connected = true;
+
+ // call any final custom stuff
+ if (_wifi_callback_f) {
+ _wifi_callback_f();
+ }
+ }
+
+ if (code == MESSAGE_CONNECTING) {
+ myDebug_P(PSTR("[WIFI] Connecting to %s"), parameter);
+ _wifi_connected = false;
+ }
+
+ if (code == MESSAGE_CONNECT_FAILED) {
+ myDebug_P(PSTR("[WIFI] Could not connect to %s"), parameter);
+ _wifi_connected = false;
+ }
+
+ if (code == MESSAGE_DISCONNECTED) {
+ myDebug_P(PSTR("[WIFI] Disconnected"));
+ _wifi_connected = false;
+ }
+
+ if (code == MESSAGE_SCANNING) {
+ myDebug_P(PSTR("[WIFI] Scanning"));
+ }
+
+ if (code == MESSAGE_SCAN_FAILED) {
+ myDebug_P(PSTR("[WIFI] Scan failed"));
+ }
+
+ if (code == MESSAGE_NO_NETWORKS) {
+ myDebug_P(PSTR("[WIFI] No networks found"));
+ }
+
+ if (code == MESSAGE_NO_KNOWN_NETWORKS) {
+ myDebug_P(PSTR("[WIFI] No known networks found"));
+ }
+
+ if (code == MESSAGE_FOUND_NETWORK) {
+ myDebug_P(PSTR("[WIFI] %s"), parameter);
+ }
+
+ if (code == MESSAGE_CONNECT_WAITING) {
+ // too much noise
+ }
+
+ if (code == MESSAGE_ACCESSPOINT_CREATING) {
+ myDebug_P(PSTR("[WIFI] Creating access point"));
+ }
+
+ if (code == MESSAGE_ACCESSPOINT_FAILED) {
+ myDebug_P(PSTR("[WIFI] Could not create access point"));
+ }
+}
+
+// return true if in WiFi AP mode
+// does not work after wifi reset on ESP32 yet. See https://github.com/espressif/arduino-esp32/issues/1306
+bool MyESP::isAPmode() {
+ return (WiFi.getMode() & WIFI_AP);
+}
+
+// received MQTT message
+// we send this to the call back function. Important to parse are the event strings such as MQTT_MESSAGE_EVENT and MQTT_CONNECT_EVENT
+void MyESP::_mqttOnMessage(char * topic, char * payload, size_t len) {
+ if (len == 0)
+ return;
+
+ char message[len + 1];
+ strlcpy(message, (char *)payload, len + 1);
+
+ // myDebug_P(PSTR("[MQTT] Received %s => %s"), topic, message); // enable for debugging
+
+ // topics are in format MQTT_BASE/HOSTNAME/TOPIC
+ char * topic_magnitude = strrchr(topic, '/'); // strip out everything until last /
+ if (topic_magnitude != nullptr) {
+ topic = topic_magnitude + 1;
+ }
+
+ // check for standard messages
+ // Restart the device
+ if (strcmp(topic, MQTT_TOPIC_RESTART) == 0) {
+ myDebug_P(PSTR("[MQTT] Received restart command"), message);
+ resetESP();
+ return;
+ }
+
+ // Send message event to custom service
+ (_mqtt_callback_f)(MQTT_MESSAGE_EVENT, topic, message);
+}
+
+// MQTT subscribe
+void MyESP::mqttSubscribe(const char * topic) {
+ if (mqttClient.connected() && (strlen(topic) > 0)) {
+ unsigned int packetId = mqttClient.subscribe(_mqttTopic(topic), _mqtt_qos);
+ myDebug_P(PSTR("[MQTT] Subscribing to %s (PID %d)"), _mqttTopic(topic), packetId);
+ }
+}
+
+// MQTT unsubscribe
+void MyESP::mqttUnsubscribe(const char * topic) {
+ if (mqttClient.connected() && (strlen(topic) > 0)) {
+ unsigned int packetId = mqttClient.unsubscribe(_mqttTopic(topic));
+ myDebug_P(PSTR("[MQTT] Unsubscribing to %s (PID %d)"), _mqttTopic(topic), packetId);
+ }
+}
+
+// MQTT Publish
+void MyESP::mqttPublish(const char * topic, const char * payload) {
+ // myDebug_P(PSTR("[MQTT] Sending pubish to %s with payload %s"), _mqttTopic(topic), payload); // for debugging
+ mqttClient.publish(_mqttTopic(topic), _mqtt_qos, _mqtt_retain, payload);
+
+ _addMQTTLog(topic, payload); // add to the log
+}
+
+// MQTT onConnect - when a connect is established
+void MyESP::_mqttOnConnect() {
+ myDebug_P(PSTR("[MQTT] is connected"));
+ _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MIN;
+
+ _mqtt_last_connection = millis();
+
+ // say we're alive to the Last Will topic
+ mqttClient.publish(_mqttTopic(_mqtt_will_topic), 1, true, _mqtt_will_online_payload); // qos=1, retain=true
+
+ // subscribe to general subs
+ mqttSubscribe(MQTT_TOPIC_RESTART);
+
+ // subscribe to a start message and send the first publish
+ mqttSubscribe(MQTT_TOPIC_START);
+ mqttPublish(MQTT_TOPIC_START, MQTT_TOPIC_START_PAYLOAD);
+
+ // send heartbeat if enabled
+ _heartbeatCheck(true);
+
+ // call custom function to handle mqtt receives
+ (_mqtt_callback_f)(MQTT_CONNECT_EVENT, nullptr, nullptr);
+}
+
+// MQTT setup
+void MyESP::_mqtt_setup() {
+ if (!_mqtt_enabled) {
+ myDebug_P(PSTR("[MQTT] is disabled"));
+ }
+
+ mqttClient.onConnect([this](bool sessionPresent) { _mqttOnConnect(); });
+
+ mqttClient.onDisconnect([this](AsyncMqttClientDisconnectReason reason) {
+ if (reason == AsyncMqttClientDisconnectReason::TCP_DISCONNECTED) {
+ myDebug_P(PSTR("[MQTT] TCP Disconnected"));
+ (_mqtt_callback_f)(MQTT_DISCONNECT_EVENT, nullptr, nullptr); // call callback with disconnect
+ }
+ if (reason == AsyncMqttClientDisconnectReason::MQTT_IDENTIFIER_REJECTED) {
+ myDebug_P(PSTR("[MQTT] Identifier Rejected"));
+ }
+ if (reason == AsyncMqttClientDisconnectReason::MQTT_SERVER_UNAVAILABLE) {
+ myDebug_P(PSTR("[MQTT] Server unavailable"));
+ }
+ if (reason == AsyncMqttClientDisconnectReason::MQTT_MALFORMED_CREDENTIALS) {
+ myDebug_P(PSTR("[MQTT] Malformed credentials"));
+ }
+ if (reason == AsyncMqttClientDisconnectReason::MQTT_NOT_AUTHORIZED) {
+ myDebug_P(PSTR("[MQTT] Not authorized"));
+ }
+
+ // Reset reconnection delay
+ _mqtt_last_connection = millis();
+ _mqtt_connecting = false;
+ });
+
+ //mqttClient.onSubscribe([this](uint16_t packetId, uint8_t qos) { myDebug_P(PSTR("[MQTT] Subscribe ACK for PID %d"), packetId); });
+ //mqttClient.onPublish([this](uint16_t packetId) { myDebug_P(PSTR("[MQTT] Publish ACK for PID %d"), packetId); });
+
+ mqttClient.onMessage([this](char * topic, char * payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) {
+ _mqttOnMessage(topic, payload, len);
+ });
+}
+
+// WiFI setup
+void MyESP::_wifi_setup() {
+ jw.setHostname(_general_hostname); // Set WIFI hostname
+ jw.subscribe([this](justwifi_messages_t code, char * parameter) { _wifiCallback(code, parameter); });
+ jw.setConnectTimeout(MYESP_WIFI_CONNECT_TIMEOUT);
+ jw.setReconnectTimeout(MYESP_WIFI_RECONNECT_INTERVAL);
+ jw.enableAPFallback(true); // AP mode only as fallback
+ jw.enableSTA(true); // Enable STA mode (connecting to a router)
+ jw.enableScan(false); // Configure it to scan available networks and connect in order of dBm
+ jw.cleanNetworks(); // Clean existing network configuration
+
+ /// wmode 1 is AP, 0 is client
+ if (_network_wmode == 1) {
+ jw.enableAP(true);
+ } else {
+ jw.enableAP(false);
+ jw.addNetwork(_network_ssid, _network_password); // Add a network
+ }
+
+#if defined(ESP8266)
+ WiFi.setSleepMode(WIFI_NONE_SLEEP); // added to possibly fix wifi dropouts in arduino core 2.5.0
+#endif
+}
+
+// set the callback function for the OTA onstart
+void MyESP::setOTA(ota_callback_f OTACallback_pre, ota_callback_f OTACallback_post) {
+ _ota_pre_callback_f = OTACallback_pre;
+ _ota_post_callback_f = OTACallback_post;
+}
+
+// OTA callback when the upload process starts
+void MyESP::_OTACallback() {
+ myDebug_P(PSTR("[OTA] Start"));
+
+#ifdef CRASH
+ // If we are not specifically reserving the sectors we are using as
+ // EEPROM in the memory layout then any OTA upgrade will overwrite
+ // all but the last one.
+ // Calling rotate(false) disables rotation so all writes will be done
+ // to the last sector. It also sets the dirty flag to true so the next commit()
+ // will actually persist current configuration to that last sector.
+ // Calling rotate(false) will also prevent any other EEPROM write
+ // to overwrite the OTA image.
+ // In case the OTA process fails, reenable rotation.
+ // See onError callback below.
+ EEPROMr.rotate(false);
+ EEPROMr.commit();
+#endif
+
+ // stop the web server
+ _ws->enable(false);
+
+ if (_ota_pre_callback_f) {
+ (_ota_pre_callback_f)(); // call custom function
+ }
+}
+
+// OTA Setup
+void MyESP::_ota_setup() {
+ if (!_network_ssid) {
+ return;
+ }
+
+ ArduinoOTA.setPort(OTA_PORT);
+ ArduinoOTA.setHostname(_general_hostname);
+
+ ArduinoOTA.onStart([this]() { _OTACallback(); });
+ ArduinoOTA.onEnd([this]() {
+ myDebug_P(PSTR("[OTA] Done, restarting..."));
+ _deferredReset(500, CUSTOM_RESET_OTA);
+ });
+
+ ArduinoOTA.onProgress([this](unsigned int progress, unsigned int total) {
+ static unsigned int _progOld;
+ unsigned int _prog = (progress / (total / 100));
+ if (_prog != _progOld) {
+ myDebug_P(PSTR("[OTA] Progress: %u%%"), _prog);
+ _progOld = _prog;
+ }
+ });
+
+ ArduinoOTA.onError([this](ota_error_t error) {
+ if (error == OTA_AUTH_ERROR)
+ myDebug_P(PSTR("[OTA] Auth Failed"));
+ else if (error == OTA_BEGIN_ERROR)
+ myDebug_P(PSTR("[OTA] Begin Failed"));
+ else if (error == OTA_CONNECT_ERROR)
+ myDebug_P(PSTR("[OTA] Connect Failed"));
+ else if (error == OTA_RECEIVE_ERROR)
+ myDebug_P(PSTR("[OTA] Receive Failed"));
+ else if (error == OTA_END_ERROR)
+ myDebug_P(PSTR("[OTA] End Failed"));
+
+#ifdef CRASH
+ // There's been an error, reenable eeprom rotation
+ EEPROMr.rotate(true);
+#endif
+ });
+}
+
+// eeprom
+void MyESP::_eeprom_setup() {
+#ifdef CRASH
+ EEPROMr.size(4);
+ EEPROMr.begin(SPI_FLASH_SEC_SIZE);
+#endif
+}
+
+// Set callback of sketch function to process project messages
+void MyESP::setTelnet(telnetcommand_callback_f callback_cmd, telnet_callback_f callback) {
+ _telnetcommand_callback_f = callback_cmd; // external function to handle commands
+ _telnet_callback_f = callback;
+}
+
+void MyESP::_telnetConnected() {
+ myDebug_P(PSTR("[TELNET] Telnet connection established"));
+ _consoleShowHelp(); // Show the initial message
+
+#ifdef CRASH
+ // show crash dump if just restarted after a fatal crash
+ uint32_t crash_time;
+ EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time);
+ if ((crash_time != 0) && (crash_time != 0xFFFFFFFF)) {
+ myDebug_P(PSTR("[SYSTEM] There is stack data available from the last system crash. Use 'crash dump' to view and 'crash clear' to reset"));
+ }
+#endif
+
+ // call callback
+ if (_telnet_callback_f) {
+ (_telnet_callback_f)(TELNET_EVENT_CONNECT);
+ }
+}
+
+void MyESP::_telnetDisconnected() {
+ myDebug_P(PSTR("[TELNET] Telnet connection closed"));
+ if (_telnet_callback_f) {
+ (_telnet_callback_f)(TELNET_EVENT_DISCONNECT); // call callback
+ }
+}
+
+// Initialize the telnet server
+void MyESP::_telnet_setup() {
+ SerialAndTelnet.setWelcomeMsg("");
+ SerialAndTelnet.setCallbackOnConnect([this]() { _telnetConnected(); });
+ SerialAndTelnet.setCallbackOnDisconnect([this]() { _telnetDisconnected(); });
+ SerialAndTelnet.setDebugOutput(false);
+ SerialAndTelnet.setPingTime(0); // default is 1500ms (1.5 seconds)
+ SerialAndTelnet.begin(TELNET_SERIAL_BAUD); // default baud is 115200
+
+ // init command buffer for console commands
+ memset(_command, 0, TELNET_MAX_COMMAND_LENGTH);
+}
+
+// Show help of commands
+void MyESP::_consoleShowHelp() {
+ myDebug_P(PSTR(""));
+ myDebug_P(PSTR("* Connected to: %s version %s"), _app_name, _app_version);
+
+ if (isAPmode()) {
+ myDebug_P(PSTR("* Device is in AP mode with SSID %s"), jw.getAPSSID().c_str());
+ } else {
+ myDebug_P(PSTR("* Hostname: %s (%s)"), _getESPhostname().c_str(), WiFi.localIP().toString().c_str());
+ myDebug_P(PSTR("* WiFi SSID: %s (signal %d%%)"), WiFi.SSID().c_str(), getWifiQuality());
+ if (isMQTTConnected()) {
+ myDebug_P(PSTR("* MQTT connected (heartbeat %s)"), getHeartbeat() ? "enabled" : "disabled");
+ } else {
+ myDebug_P(PSTR("* MQTT disconnected"));
+ }
+ }
+
+ myDebug_P(PSTR("*"));
+ myDebug_P(PSTR("* Commands:"));
+ myDebug_P(PSTR("* ?=help, CTRL-D/quit=exit telnet session"));
+ myDebug_P(PSTR("* set, system, restart, mqttlog"));
+#ifdef CRASH
+ myDebug_P(PSTR("* crash "));
+#endif
+
+ // call callback function
+ if (_telnet_callback_f) {
+ (_telnet_callback_f)(TELNET_EVENT_SHOWCMD);
+ }
+
+ myDebug_P(PSTR("")); // newline
+}
+
+// see if a char * string is empty. It could not be initialized yet.
+// return true if there is a value
+bool MyESP::_hasValue(char * s) {
+ if (s == nullptr) {
+ return false;
+ }
+ return (s[0] != '\0');
+}
+
+// print all set commands and current values
+void MyESP::_printSetCommands() {
+ myDebug_P(PSTR("\nThe following set commands are available:\n"));
+ myDebug_P(PSTR(" set erase"));
+ myDebug_P(PSTR(" set "));
+ myDebug_P(PSTR(" set [value]"));
+ myDebug_P(PSTR(" set mqtt_enabled "));
+ myDebug_P(PSTR(" set [value]"));
+ myDebug_P(PSTR(" set mqtt_heartbeat "));
+ myDebug_P(PSTR(" set mqtt_base [value]"));
+ myDebug_P(PSTR(" set mqtt_port [value]"));
+ myDebug_P(PSTR(" set ntp_enabled "));
+ myDebug_P(PSTR(" set serial "));
+
+ // call callback function
+ if (_telnet_callback_f) {
+ (_telnet_callback_f)(TELNET_EVENT_SHOWSET);
+ }
+
+ myDebug_P(PSTR("\nStored settings:\n"));
+
+ if (_network_wmode == 0) {
+ myDebug_P(PSTR(" wifi_mode=client"));
+ } else {
+ myDebug_P(PSTR(" wifi_mode=ap"));
+ }
+
+ if (_hasValue(_network_ssid)) {
+ myDebug_P(PSTR(" wifi_ssid=%s"), _network_ssid);
+ } else {
+ myDebug_P(PSTR(" wifi_ssid="));
+ }
+ SerialAndTelnet.print(FPSTR(" wifi_password="));
+ if (_hasValue(_network_password)) {
+ for (uint8_t i = 0; i < strlen(_network_password); i++) {
+ SerialAndTelnet.print(FPSTR("*"));
+ }
+ }
+ myDebug_P(PSTR(""));
+ myDebug_P(PSTR(" mqtt_enabled=%s"), (_mqtt_enabled) ? "on" : "off");
+ if (_hasValue(_mqtt_ip)) {
+ myDebug_P(PSTR(" mqtt_ip=%s"), _mqtt_ip);
+ } else {
+ myDebug_P(PSTR(" mqtt_ip="));
+ }
+ if (_hasValue(_mqtt_user)) {
+ myDebug_P(PSTR(" mqtt_username=%s"), _mqtt_user);
+ } else {
+ myDebug_P(PSTR(" mqtt_username="));
+ }
+ SerialAndTelnet.print(FPSTR(" mqtt_password="));
+ if (_hasValue(_mqtt_password)) {
+ for (uint8_t i = 0; i < strlen(_mqtt_password); i++) {
+ SerialAndTelnet.print(FPSTR("*"));
+ }
+ }
+ myDebug_P(PSTR(""));
+ if (_hasValue(_mqtt_base)) {
+ myDebug_P(PSTR(" mqtt_base=%s"), _mqtt_base);
+ } else {
+ myDebug_P(PSTR(" mqtt_base="));
+ }
+ myDebug_P(PSTR(" mqtt_port=%d"), _mqtt_port);
+ myDebug_P(PSTR(" mqtt_heartbeat=%s"), (_mqtt_heartbeat) ? "on" : "off");
+
+ myDebug_P(PSTR(" serial=%s"), (_general_serial) ? "on" : "off");
+ myDebug_P(PSTR(" ntp_enabled=%s"), (_ntp_enabled) ? "on" : "off");
+
+ // print any custom settings
+ if (_fs_setlist_callback_f) {
+ (_fs_setlist_callback_f)(MYESP_FSACTION_LIST, 0, nullptr, nullptr);
+ }
+
+ myDebug_P(PSTR("")); // newline
+}
+
+// reset / restart
+void MyESP::resetESP() {
+ myDebug_P(PSTR("* Restart ESP..."));
+ _deferredReset(500, CUSTOM_RESET_TERMINAL);
+ end();
+#if defined(ARDUINO_ARCH_ESP32)
+ ESP.restart();
+#else
+ ESP.restart();
+#endif
+}
+
+// read next word from string buffer
+// if parameter true then a word is only terminated by a newline
+char * MyESP::_telnet_readWord(bool allow_all_chars) {
+ if (allow_all_chars) {
+ return (strtok(nullptr, "\n")); // allow only newline
+ } else {
+ return (strtok(nullptr, ", \n")); // allow space and comma
+ }
+}
+
+// change settings - always as strings
+// messy code but effective since we don't have too many settings
+// wc is word count, number of parameters after the 'set' command
+bool MyESP::_changeSetting(uint8_t wc, const char * setting, const char * value) {
+ bool save_config = false;
+ bool save_custom_config = false;
+
+ // check for our internal commands first
+ if (strcmp(setting, "erase") == 0) {
+ _fs_eraseConfig();
+ return true;
+
+ } else if (strcmp(setting, "wifi_ssid") == 0) {
+ if (value) {
+ free(_network_ssid);
+ _network_ssid = strdup(value);
+ }
+ save_config = true;
+ //jw.enableSTA(false);
+ myDebug_P(PSTR("Note: please 'restart' to apply new WiFi settings"));
+ } else if (strcmp(setting, "wifi_password") == 0) {
+ if (value) {
+ free(_network_password);
+ _network_password = strdup(value);
+ }
+ save_config = true;
+ //jw.enableSTA(false);
+ myDebug_P(PSTR("Note: please 'restart' to apply new WiFi settings"));
+
+ } else if (strcmp(setting, "wifi_mode") == 0) {
+ if (value) {
+ if (strcmp(value, "ap") == 0) {
+ _network_wmode = 1;
+ save_config = true;
+ myDebug_P(PSTR("Note: please 'restart' to apply new WiFi settings"));
+ } else if (strcmp(value, "client") == 0) {
+ _network_wmode = 0;
+ save_config = true;
+ myDebug_P(PSTR("Note: please 'restart' to apply new WiFi settings"));
+ } else {
+ save_config = false;
+ }
+ }
+
+ } else if (strcmp(setting, "mqtt_ip") == 0) {
+ if (value) {
+ free(_mqtt_ip);
+ _mqtt_ip = strdup(value);
+ }
+ save_config = true;
+ } else if (strcmp(setting, "mqtt_username") == 0) {
+ if (value) {
+ free(_mqtt_user);
+ _mqtt_user = strdup(value);
+ }
+ save_config = true;
+ } else if (strcmp(setting, "mqtt_password") == 0) {
+ if (value) {
+ free(_mqtt_password);
+ _mqtt_password = strdup(value);
+ }
+ save_config = true;
+ } else if (strcmp(setting, "mqtt_base") == 0) {
+ if (value) {
+ free(_mqtt_base);
+ _mqtt_base = strdup(value);
+ }
+ save_config = true;
+ } else if (strcmp(setting, "mqtt_port") == 0) {
+ if (value) {
+ _mqtt_port = atoi(value);
+ }
+ save_config = true;
+ } else if (strcmp(setting, "mqtt_enabled") == 0) {
+ save_config = true;
+ if (value) {
+ if (strcmp(value, "on") == 0) {
+ _mqtt_enabled = true;
+ save_config = true;
+ } else if (strcmp(value, "off") == 0) {
+ _mqtt_enabled = false;
+ save_config = true;
+ } else {
+ save_config = false;
+ }
+ }
+
+ } else if (strcmp(setting, "serial") == 0) {
+ save_config = true;
+ if (value) {
+ if (strcmp(value, "on") == 0) {
+ _general_serial = true;
+ save_config = true;
+ myDebug_P(PSTR("Do a 'restart' to activate Serial mode."));
+ } else if (strcmp(value, "off") == 0) {
+ _general_serial = false;
+ save_config = true;
+ myDebug_P(PSTR("Do a 'restart' to deactivate Serial mode."));
+ } else {
+ save_config = false;
+ }
+ }
+
+ } else if (strcmp(setting, "mqtt_heartbeat") == 0) {
+ save_config = true;
+ if (value) {
+ if (strcmp(value, "on") == 0) {
+ _mqtt_heartbeat = true;
+ save_config = true;
+ myDebug_P(PSTR("Heartbeat on"));
+ } else if (strcmp(value, "off") == 0) {
+ _mqtt_heartbeat = false;
+ save_config = true;
+ myDebug_P(PSTR("Heartbeat off"));
+ } else {
+ save_config = false;
+ }
+ }
+ } else if (strcmp(setting, "ntp_enabled") == 0) {
+ save_config = true;
+ if (value) {
+ if (strcmp(value, "on") == 0) {
+ _ntp_enabled = true;
+ save_config = true;
+ myDebug_P(PSTR("NTP on"));
+ } else if (strcmp(value, "off") == 0) {
+ _ntp_enabled = false;
+ save_config = true;
+ myDebug_P(PSTR("NTP off"));
+ } else {
+ save_config = false;
+ }
+ }
+ } else {
+ // finally check for any custom commands
+ if (_fs_setlist_callback_f) {
+ save_custom_config = (_fs_setlist_callback_f)(MYESP_FSACTION_SET, wc, setting, value);
+ }
+ }
+
+ bool ok = false;
+
+ // if we were able to recognize the set command, continue
+ if ((save_config || save_custom_config)) {
+ // check for 2 params
+ if (value == nullptr) {
+ myDebug_P(PSTR("%s setting reset to its default value."), setting);
+ } else {
+ // must be 3 params
+ myDebug_P(PSTR("%s changed."), setting);
+ }
+ }
+
+ // now do the saving for system config if something has changed
+ if (save_config) {
+ ok = _fs_writeConfig();
+ }
+
+ // and see if we need to also save for custom config
+ if (save_custom_config) {
+ ok = _fs_createCustomConfig();
+ }
+
+ return ok;
+}
+
+// force the serial on/off
+void MyESP::setUseSerial(bool b) {
+ _general_serial = b;
+ SerialAndTelnet.setSerial(b ? &Serial : nullptr);
+}
+
+void MyESP::_telnetCommand(char * commandLine) {
+ char * str = commandLine;
+ bool state = false;
+
+ if (strlen(commandLine) == 0)
+ return;
+
+ // count the number of arguments
+ unsigned wc = 0;
+ while (*str) {
+ if (*str == ' ' || *str == '\n' || *str == '\t') {
+ state = false;
+ } else if (state == false) {
+ state = true;
+ ++wc;
+ }
+ ++str;
+ }
+
+ // check first for reserved commands
+ char * temp = strdup(commandLine); // because strotok kills original string buffer
+ char * ptrToCommandName = strtok((char *)temp, " \n"); // space and newline
+
+ // set command
+ if (strcmp(ptrToCommandName, "set") == 0) {
+ bool ok = false;
+ if (wc == 1) {
+ _printSetCommands();
+ ok = true;
+ } else if (wc == 2) { // set
+ char * setting = _telnet_readWord(false);
+ ok = _changeSetting(wc - 1, setting, nullptr);
+ } else { // set
+ char * setting = _telnet_readWord(false);
+ char * value = _telnet_readWord(true); // allow strange characters
+ ok = _changeSetting(wc - 1, setting, value);
+ }
+
+ if (!ok) {
+ myDebug_P(PSTR("\nInvalid parameter for set command."));
+ }
+
+ return;
+ }
+
+ // restart command
+ if ((strcmp(ptrToCommandName, "restart") == 0) && (wc == 1)) {
+ resetESP();
+ return;
+ }
+
+ // print mqtt log command
+ if ((strcmp(ptrToCommandName, "mqttlog") == 0) && (wc == 1)) {
+ _printMQTTLog();
+ return;
+ }
+
+ // show system stats
+ if ((strcmp(ptrToCommandName, "system") == 0) && (wc == 1)) {
+ showSystemStats();
+ return;
+ }
+
+ // show system stats
+ if ((strcmp(ptrToCommandName, "quit") == 0) && (wc == 1)) {
+ myDebug_P(PSTR("[TELNET] exiting telnet session"));
+ SerialAndTelnet.disconnectClient();
+ return;
+ }
+
+#ifdef CRASH
+ // crash command
+ if ((strcmp(ptrToCommandName, "crash") == 0) && (wc >= 2)) {
+ char * cmd = _telnet_readWord(false);
+ if (strcmp(cmd, "dump") == 0) {
+ crashDump();
+ } else if (strcmp(cmd, "clear") == 0) {
+ crashClear();
+ } else if ((strcmp(cmd, "test") == 0) && (wc == 3)) {
+ char * value = _telnet_readWord(false);
+ crashTest(atoi(value));
+ } else {
+ myDebug_P(PSTR("Error. Usage: crash "));
+ }
+ return; // don't call custom command line callback
+ }
+#endif
+
+ // call callback function
+ if (_telnetcommand_callback_f) {
+ (_telnetcommand_callback_f)(wc, commandLine);
+ }
+}
+
+// returns WiFi hostname as a String object
+String MyESP::_getESPhostname() {
+ String hostname;
+
+#if defined(ARDUINO_ARCH_ESP32)
+ hostname = String(WiFi.getHostname());
+#else
+ hostname = WiFi.hostname();
+#endif
+
+ return (hostname);
+}
+
+// takes the time from the gcc during compilation
+char * MyESP::_getBuildTime() {
+ const char time_now[] = __TIME__; // hh:mm:ss
+ uint8_t hour = atoi(&time_now[0]);
+ uint8_t minute = atoi(&time_now[3]);
+ uint8_t second = atoi(&time_now[6]);
+
+ const char date_now[] = __DATE__; // Mmm dd yyyy
+ const char * months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
+ uint8_t month = 0;
+ for (int i = 0; i < 12; i++) {
+ if (strncmp(date_now, months[i], 3) == 0) {
+ month = i + 1;
+ break;
+ }
+ }
+ uint8_t day = atoi(&date_now[3]);
+ uint16_t year = atoi(&date_now[7]);
+
+ char buffer[30];
+ snprintf_P(buffer, sizeof(buffer), PSTR("%04d-%02d-%02d %02d:%02d:%02d"), year, month, day, hour, minute, second);
+
+ return (strdup(buffer));
+}
+
+// returns system uptime in seconds
+unsigned long MyESP::_getUptime() {
+ static uint32_t last_uptime = 0;
+ static uint8_t uptime_overflows = 0;
+
+ if (millis() < last_uptime) {
+ ++uptime_overflows;
+ }
+ last_uptime = millis();
+ uint32_t uptime_seconds = uptime_overflows * (MYESP_UPTIME_OVERFLOW / 1000) + (last_uptime / 1000);
+
+ return uptime_seconds;
+}
+
+// init RTC mem
+void MyESP::_rtcmemInit() {
+ memset((uint32_t *)RTCMEM_ADDR, 0, sizeof(uint32_t) * RTCMEM_BLOCKS);
+ Rtcmem->magic = RTCMEM_MAGIC;
+}
+
+uint8_t MyESP::getSystemBootStatus() {
+ system_rtcmem_t data;
+ data.value = Rtcmem->sys;
+ return data.parts.boot_status;
+}
+
+void MyESP::_setSystemBootStatus(uint8_t status) {
+ system_rtcmem_t data;
+ data.value = Rtcmem->sys;
+ data.parts.boot_status = status;
+ Rtcmem->sys = data.value;
+ // myDebug("*** setting boot status to %d", data.parts.boot_status);
+}
+
+uint8_t MyESP::_getSystemStabilityCounter() {
+ system_rtcmem_t data;
+ data.value = Rtcmem->sys;
+ return data.parts.stability_counter;
+}
+
+void MyESP::_setSystemStabilityCounter(uint8_t counter) {
+ system_rtcmem_t data;
+ data.value = Rtcmem->sys;
+ data.parts.stability_counter = counter;
+ Rtcmem->sys = data.value;
+}
+
+uint8_t MyESP::_getSystemResetReason() {
+ system_rtcmem_t data;
+ data.value = Rtcmem->sys;
+ return data.parts.reset_reason;
+}
+
+void MyESP::_setSystemResetReason(uint8_t reason) {
+ system_rtcmem_t data;
+ data.value = Rtcmem->sys;
+ data.parts.reset_reason = reason;
+ Rtcmem->sys = data.value;
+}
+
+// system_get_rst_info() result is cached by the Core init for internal use
+uint32_t MyESP::getSystemResetReason() {
+ return resetInfo.reason;
+}
+
+void MyESP::_rtcmemSetup() {
+ _rtcmem_status = _rtcmemStatus();
+ if (!_rtcmem_status) {
+ _rtcmemInit();
+ }
+}
+
+void MyESP::_setCustomResetReason(uint8_t reason) {
+ _setSystemResetReason(reason);
+}
+
+// returns false if not set and needs to be intialized, causing all rtcmem data to be wiped
+bool MyESP::_rtcmemStatus() {
+ bool readable;
+
+ uint32_t reason = getSystemResetReason();
+
+ // the last reset could have been caused by manually pressing the reset button
+ // so before wiping, capture the boot sequence
+ if (reason == REASON_EXT_SYS_RST) { // external system reset
+ if (getSystemBootStatus() == MYESP_BOOTSTATUS_BOOTING) {
+ _setSystemBootStatus(MYESP_BOOTSTATUS_RESETNEEDED);
+ // _formatreq = true; // do a wipe next in the loop() - commented out for now because we use the web
+ } else {
+ _setSystemBootStatus(MYESP_BOOTSTATUS_POWERON);
+ }
+ }
+
+ switch (reason) {
+ //case REASON_EXT_SYS_RST: // external system reset
+ case REASON_WDT_RST: // hardware watch dog reset
+ case REASON_DEFAULT_RST: // normal startup by power on
+ case REASON_SOFT_WDT_RST: // Software watchdog
+ readable = false;
+ break;
+ default:
+ readable = true;
+ }
+
+ readable = readable and (RTCMEM_MAGIC == Rtcmem->magic);
+
+ return readable;
+}
+
+bool MyESP::_getRtcmemStatus() {
+ return _rtcmem_status;
+}
+
+uint8_t MyESP::_getCustomResetReason() {
+ static uint8_t status = 255;
+ if (status == 255) {
+ if (_rtcmemStatus())
+ status = _getSystemResetReason();
+ if (status > 0)
+ _setCustomResetReason(0);
+ if (status > CUSTOM_RESET_MAX)
+ status = 0;
+ }
+ return status;
+}
+
+void MyESP::_deferredReset(unsigned long delaytime, uint8_t reason) {
+ _setSystemBootStatus(MYESP_BOOTSTATUS_POWERON);
+ _setCustomResetReason(reason);
+ delay(delaytime);
+}
+
+// Call this method on boot with stable=true to reset the crash counter
+// Each call increments the counter
+// If the counter reaches MYESP_SYSTEM_CHECK_MAX then the system is flagged as unstable
+void MyESP::_setSystemCheck(bool stable) {
+ uint8_t value = 0;
+
+ if (stable) {
+ value = 0; // system is ok
+ } else {
+ if (!_getRtcmemStatus()) {
+ _setSystemStabilityCounter(1);
+ return;
+ }
+
+ value = _getSystemStabilityCounter();
+
+ if (++value > MYESP_SYSTEM_CHECK_MAX) {
+ _systemStable = false;
+ value = 0; // system is unstable
+ myDebug_P(PSTR("[SYSTEM] Warning, system UNSTABLE. Serial mode is enabled."));
+
+ // enable Serial again
+ if (!_general_serial) {
+ SerialAndTelnet.setSerial(&Serial);
+ _general_serial = true;
+ }
+ }
+ }
+
+ _setSystemStabilityCounter(value);
+}
+
+// return if system is stable (false=bad)
+bool MyESP::_getSystemCheck() {
+ return _systemStable;
+}
+
+// periodically check if system is stable
+void MyESP::_systemCheckLoop() {
+ static bool checked = false;
+ if (!checked && (millis() > MYESP_SYSTEM_CHECK_TIME)) {
+ _setSystemCheck(true); // Flag system as stable
+ checked = true;
+ }
+}
+
+// print out ESP system stats
+// for battery power is ESP.getVcc()
+void MyESP::showSystemStats() {
+#if defined(ESP8266)
+ myDebug_P(PSTR("%sESP8266 System stats:%s"), COLOR_BOLD_ON, COLOR_BOLD_OFF);
+#else
+ myDebug_P(PSTR("ESP32 System stats:"));
+#endif
+ myDebug_P(PSTR(""));
+
+ myDebug_P(PSTR(" [APP] %s version: %s"), _app_name, _app_version);
+ myDebug_P(PSTR(" [APP] MyESP version: %s"), MYESP_VERSION);
+ myDebug_P(PSTR(" [APP] Build timestamp: %s"), _buildTime);
+
+ // uptime
+ uint32_t t = _getUptime(); // seconds
+
+ uint32_t d = t / 86400L;
+ uint32_t h = ((t % 86400L) / 3600L) % 60;
+ uint32_t rem = t % 3600L;
+ uint8_t m = rem / 60;
+ uint8_t s = rem % 60;
+ myDebug_P(PSTR(" [APP] Uptime: %d day%s %d hour%s %d minute%s %d second%s"),
+ d,
+ (d == 1) ? "" : "s",
+ h,
+ (h == 1) ? "" : "s",
+ m,
+ (m == 1) ? "" : "s",
+ s,
+ (s == 1) ? "" : "s");
+
+ myDebug_P(PSTR(" [APP] System Load: %d%%"), getSystemLoadAverage());
+
+ if (!_getSystemCheck()) {
+ myDebug_P(PSTR(" [SYSTEM] Device is in SAFE MODE"));
+ }
+
+ if (isAPmode()) {
+ myDebug_P(PSTR(" [WIFI] Device is in AP mode with SSID %s"), jw.getAPSSID().c_str());
+ } else {
+ myDebug_P(PSTR(" [WIFI] WiFi Hostname: %s"), _getESPhostname().c_str());
+ myDebug_P(PSTR(" [WIFI] WiFi IP: %s"), WiFi.localIP().toString().c_str());
+ myDebug_P(PSTR(" [WIFI] WiFi signal strength: %d%%"), getWifiQuality());
+ }
+
+ myDebug_P(PSTR(" [WIFI] WiFi MAC: %s"), WiFi.macAddress().c_str());
+
+ if (isMQTTConnected()) {
+ myDebug_P(PSTR(" [MQTT] is connected (heartbeat %s)"), getHeartbeat() ? "enabled" : "disabled");
+ } else {
+ myDebug_P(PSTR(" [MQTT] is disconnected"));
+ }
+
+ if (_ntp_enabled) {
+ myDebug_P(PSTR(" [NTP] Time in UTC is %02d:%02d:%02d"), hour(now()), minute(now()), second(now()));
+ }
+
+#ifdef CRASH
+ char output_str[80] = {0};
+ char buffer[16] = {0};
+ myDebug_P(PSTR(" [EEPROM] EEPROM size: %u"), EEPROMr.reserved() * SPI_FLASH_SEC_SIZE);
+ strlcpy(output_str, " [EEPROM] EEPROM Sector pool size is ", sizeof(output_str));
+ strlcat(output_str, itoa(EEPROMr.size(), buffer, 10), sizeof(output_str));
+ strlcat(output_str, ", and in use are: ", sizeof(output_str));
+ for (uint32_t i = 0; i < EEPROMr.size(); i++) {
+ strlcat(output_str, itoa(EEPROMr.base() - i, buffer, 10), sizeof(output_str));
+ strlcat(output_str, " ", sizeof(output_str));
+ }
+ myDebug(output_str);
+#endif
+
+ myDebug_P(PSTR(" [SYSTEM] System is %s"), _getSystemCheck() ? "Stable" : "Unstable!");
+
+#ifdef ARDUINO_BOARD
+ myDebug_P(PSTR(" [SYSTEM] Board: %s"), ARDUINO_BOARD);
+#endif
+
+ myDebug_P(PSTR(" [SYSTEM] CPU frequency: %u MHz"), ESP.getCpuFreqMHz());
+ myDebug_P(PSTR(" [SYSTEM] SDK version: %s"), ESP.getSdkVersion());
+
+#if defined(ESP8266)
+ myDebug_P(PSTR(" [SYSTEM] CPU chip ID: 0x%06X"), ESP.getChipId());
+ myDebug_P(PSTR(" [SYSTEM] Core version: %s"), ESP.getCoreVersion().c_str());
+ myDebug_P(PSTR(" [SYSTEM] Boot version: %d"), ESP.getBootVersion());
+ myDebug_P(PSTR(" [SYSTEM] Boot mode: %d"), ESP.getBootMode());
+ unsigned char reason = _getCustomResetReason();
+ if (reason > 0) {
+ char buffer[32];
+ strcpy_P(buffer, custom_reset_string[reason - 1]);
+ myDebug_P(PSTR(" [SYSTEM] Last reset reason: %s"), buffer);
+ } else {
+ myDebug_P(PSTR(" [SYSTEM] Last reset reason: %s"), (char *)ESP.getResetReason().c_str());
+ myDebug_P(PSTR(" [SYSTEM] Last reset info: %s"), (char *)ESP.getResetInfo().c_str());
+ }
+ myDebug_P(PSTR(" [SYSTEM] Restart count: %d"), _getSystemStabilityCounter());
+
+ myDebug_P(PSTR(" [SYSTEM] rtcmem status: blocks:%u addr:0x%p"), RtcmemSize, Rtcmem);
+ for (uint8_t block = 0; block < RtcmemSize; ++block) {
+ myDebug_P(PSTR(" [SYSTEM] rtcmem %02u: %u"), block, reinterpret_cast(RTCMEM_ADDR)[block]);
+ }
+#endif
+
+ FlashMode_t mode = ESP.getFlashChipMode();
+#if defined(ESP8266)
+ myDebug_P(PSTR(" [FLASH] Flash chip ID: 0x%06X"), ESP.getFlashChipId());
+#endif
+ myDebug_P(PSTR(" [FLASH] Flash speed: %u Hz"), ESP.getFlashChipSpeed());
+ myDebug_P(PSTR(" [FLASH] Flash mode: %s"), mode == FM_QIO ? "QIO" : mode == FM_QOUT ? "QOUT" : mode == FM_DIO ? "DIO" : mode == FM_DOUT ? "DOUT" : "UNKNOWN");
+#if defined(ESP8266)
+ myDebug_P(PSTR(" [FLASH] Flash size (CHIP): %d"), ESP.getFlashChipRealSize());
+#endif
+ myDebug_P(PSTR(" [FLASH] Flash size (SDK): %d"), ESP.getFlashChipSize());
+ myDebug_P(PSTR(" [FLASH] Flash Reserved: %d"), 1 * SPI_FLASH_SEC_SIZE);
+ myDebug_P(PSTR(" [MEM] Firmware size: %d"), ESP.getSketchSize());
+ myDebug_P(PSTR(" [MEM] Max OTA size: %d"), (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000);
+ myDebug_P(PSTR(" [MEM] OTA Reserved: %d"), 4 * SPI_FLASH_SEC_SIZE);
+
+ uint32_t total_memory = _getInitialFreeHeap();
+ uint32_t free_memory = ESP.getFreeHeap();
+
+ myDebug(" [MEM] Free Heap: %d bytes initially | %d bytes used (%2u%%) | %d bytes free (%2u%%)",
+ total_memory,
+ total_memory - free_memory,
+ 100 * (total_memory - free_memory) / total_memory,
+ free_memory,
+ 100 * free_memory / total_memory);
+
+ myDebug_P(PSTR(""));
+}
+
+/*
+ * Send heartbeat via MQTT with all system data
+ */
+void MyESP::_heartbeatCheck(bool force = false) {
+ static uint32_t last_heartbeat = 0;
+
+ if ((millis() - last_heartbeat > MYESP_HEARTBEAT_INTERVAL) || force) {
+ last_heartbeat = millis();
+
+ // _printHeap("Heartbeat"); // for heartbeat debugging
+
+ if (!isMQTTConnected() || !(_mqtt_heartbeat)) {
+ return;
+ }
+
+ uint32_t total_memory = _getInitialFreeHeap();
+ uint32_t free_memory = ESP.getFreeHeap();
+ uint8_t mem_available = 100 * free_memory / total_memory; // as a %
+
+ char payload[300] = {0};
+ char s[10];
+ strlcpy(payload, "version=", sizeof(payload));
+ strlcat(payload, _app_version, sizeof(payload)); // version
+ strlcat(payload, ", IP=", sizeof(payload));
+ strlcat(payload, WiFi.localIP().toString().c_str(), sizeof(payload)); // IP address
+ strlcat(payload, ", rssid=", sizeof(payload));
+ strlcat(payload, itoa(getWifiQuality(), s, 10), sizeof(payload)); // rssi %
+ strlcat(payload, "%, load=", sizeof(payload));
+ strlcat(payload, ltoa(getSystemLoadAverage(), s, 10), sizeof(payload)); // load
+ strlcat(payload, "%, uptime=", sizeof(payload));
+ strlcat(payload, ltoa(_getUptime(), s, 10), sizeof(payload)); // uptime in secs
+ strlcat(payload, "secs, freemem=", sizeof(payload));
+ strlcat(payload, itoa(mem_available, s, 10), sizeof(payload)); // free mem as a %
+ strlcat(payload, "%", sizeof(payload));
+
+ mqttPublish(MQTT_TOPIC_HEARTBEAT, payload); // send to MQTT
+ }
+}
+
+// handler for Telnet
+void MyESP::_telnetHandle() {
+ SerialAndTelnet.handle();
+
+ static uint8_t charsRead = 0;
+ // read asynchronously until full command input
+ while (SerialAndTelnet.available()) {
+ char c = SerialAndTelnet.read();
+
+ if (c == 0)
+ return;
+
+ SerialAndTelnet.serialPrint(c); // echo to Serial (if connected)
+
+ switch (c) {
+ case '\r': // likely have full command in buffer now, commands are terminated by CR and/or LF
+ case '\n':
+ _command[charsRead] = '\0'; // null terminate our command char array
+
+ if (charsRead > 0) {
+ charsRead = 0; // is static, so have to reset
+ _suspendOutput = false;
+ if (_general_serial) {
+ SerialAndTelnet.serialPrint('\n'); // force newline if in Serial
+ }
+ _telnetCommand(_command);
+ }
+ break;
+ case '\b': // (^H)
+ case 0x7F: // (^?)
+ if (charsRead > 0) {
+ _command[--charsRead] = '\0';
+
+ SerialAndTelnet.write(' ');
+ SerialAndTelnet.write('\b');
+ }
+ break;
+ case '?':
+ if (!_suspendOutput) {
+ _consoleShowHelp();
+ } else {
+ _command[charsRead++] = c; // add it to buffer as its part of the string entered
+ }
+ break;
+ case 0x04: // EOT, CTRL-D
+ myDebug_P(PSTR("[TELNET] exiting telnet session"));
+ SerialAndTelnet.disconnectClient();
+ break;
+ default:
+ _suspendOutput = true;
+ if (charsRead < TELNET_MAX_COMMAND_LENGTH) {
+ _command[charsRead++] = c;
+ }
+ _command[charsRead] = '\0'; // just in case
+ break;
+ }
+ }
+}
+
+// make sure we have a connection to MQTT broker and the MQTT IP is set
+void MyESP::_mqttConnect() {
+ if ((!_mqtt_enabled) || (!_hasValue(_mqtt_ip))) {
+ return; // MQTT not enabled
+ }
+
+ // Do not connect if already connected or still trying to connect
+ if (mqttClient.connected() || _mqtt_connecting || (WiFi.status() != WL_CONNECTED)) {
+ return;
+ }
+
+ // Check reconnect interval
+ if (millis() - _mqtt_last_connection < _mqtt_reconnect_delay) {
+ return;
+ }
+
+ _mqtt_connecting = true; // we're doing a connection
+
+ // Increase the reconnect delay
+ _mqtt_reconnect_delay += MQTT_RECONNECT_DELAY_STEP;
+ if (_mqtt_reconnect_delay > MQTT_RECONNECT_DELAY_MAX) {
+ _mqtt_reconnect_delay = MQTT_RECONNECT_DELAY_MAX;
+ }
+
+ mqttClient.setServer(_mqtt_ip, _mqtt_port);
+ mqttClient.setClientId(_general_hostname);
+ mqttClient.setKeepAlive(_mqtt_keepalive);
+ mqttClient.setCleanSession(false);
+
+ // last will
+ if (_hasValue(_mqtt_will_topic)) {
+ //myDebug_P(PSTR("[MQTT] Setting last will topic %s"), _mqttTopic(_mqtt_will_topic));
+ mqttClient.setWill(_mqttTopic(_mqtt_will_topic), 1, true,
+ _mqtt_will_offline_payload); // retain always true
+ }
+
+ if (_hasValue(_mqtt_user)) {
+ myDebug_P(PSTR("[MQTT] Connecting to MQTT using user %s..."), _mqtt_user);
+ mqttClient.setCredentials(_mqtt_user, _mqtt_password);
+ } else {
+ myDebug_P(PSTR("[MQTT] Connecting to MQTT..."));
+ }
+
+ // Connect to the MQTT broker
+ mqttClient.connect();
+}
+
+// Setup everything we need
+void MyESP::setWIFI(wifi_callback_f callback) {
+ // callback
+ _wifi_callback_f = callback;
+}
+
+// init MQTT settings
+void MyESP::setMQTT(mqtt_callback_f callback) {
+ _mqtt_callback_f = callback; // callback
+}
+
+// builds up a topic by prefixing the base and hostname
+char * MyESP::_mqttTopic(const char * topic) {
+ static char buffer[MQTT_MAX_TOPIC_SIZE] = {0};
+
+ if (_hasValue(_mqtt_base)) {
+ strlcpy(buffer, _mqtt_base, sizeof(buffer));
+ strlcat(buffer, "/", sizeof(buffer));
+ strlcat(buffer, _general_hostname, sizeof(buffer));
+ } else {
+ strlcpy(buffer, _general_hostname, sizeof(buffer));
+ }
+
+ strlcat(buffer, "/", sizeof(buffer));
+ strlcat(buffer, topic, sizeof(buffer));
+
+ return buffer;
+}
+
+// print contents of file
+// assumes Serial is open
+void MyESP::_fs_printFile(const char * file) {
+ File configFile = SPIFFS.open(file, "r");
+ if (!configFile) {
+ myDebug_P(PSTR("[FS] Failed to read file %s for printing"), file);
+ return;
+ }
+
+ myDebug_P(PSTR("[FS] File: %s, Size: %d"), file, configFile.size());
+
+ while (configFile.available()) {
+ SerialAndTelnet.print((char)configFile.read());
+ }
+
+ myDebug_P(PSTR("[FS] end")); // newline
+
+ configFile.close();
+}
+
+// format File System
+void MyESP::_fs_eraseConfig() {
+ myDebug_P(PSTR("[FS] Doing a factory reset."));
+ _formatreq = true;
+}
+
+// custom callback for web info
+void MyESP::setWeb(web_callback_f callback_web) {
+ _web_callback_f = callback_web;
+}
+
+void MyESP::setSettings(fs_loadsave_callback_f loadsave, fs_setlist_callback_f setlist, bool useSerial) {
+ _fs_loadsave_callback_f = loadsave;
+ _fs_setlist_callback_f = setlist;
+ _general_serial = useSerial;
+}
+
+// load system config from SPIFFS
+// returns false on error or the file needs to be recreated
+bool MyESP::_fs_loadConfig() {
+ // see if old file exists and delete it
+ if (SPIFFS.exists("/config.json")) {
+ SPIFFS.remove("/config.json");
+ myDebug_P(PSTR("[FS] Removed old config version"));
+ }
+
+ File configFile = SPIFFS.open(MYESP_CONFIG_FILE, "r");
+ if (!configFile) {
+ configFile.close();
+ myDebug_P(PSTR("[FS] No system config found"));
+ return false;
+ }
+
+ // check size
+ size_t size = configFile.size();
+ if (size > MYESP_SPIFFS_MAXSIZE) {
+ configFile.close();
+ myDebug_P(PSTR("[FS] System config size is too large"));
+ return false;
+ } else if (size == 0) {
+ configFile.close();
+ myDebug_P(PSTR("[FS] Corrupted system config"));
+ return false;
+ }
+
+ // read file from SPIFFS into a char array
+ char json[MYESP_SPIFFS_MAXSIZE] = {0};
+ if (configFile.readBytes(json, size) != size) {
+ configFile.close();
+ myDebug_P(PSTR("[FS] Error, file sizes don't match with system config"));
+ return false;
+ }
+ configFile.close();
+
+ StaticJsonDocument doc;
+
+ DeserializationError error = deserializeJson(doc, json); // Deserialize the JSON document
+ if (error) {
+ myDebug_P(PSTR("[FS] Failed to deserialize json, error %s"), error.c_str());
+ configFile.close();
+ return false;
+ }
+
+ JsonObject network = doc["network"];
+ _network_ssid = strdup(network["ssid"] | "");
+ _network_password = strdup(network["password"] | "");
+ _network_wmode = network["wmode"]; // 0 is client, 1 is AP
+
+ JsonObject general = doc["general"];
+
+ _general_password = strdup(general["password"] | MYESP_HTTP_PASSWORD);
+ _ws->setAuthentication("admin", _general_password);
+ _general_hostname = strdup(general["hostname"]);
+
+ // serial is only on when booting
+#ifdef FORCE_SERIAL
+ myDebug_P(PSTR("[FS] Serial is forced"));
+ _general_serial = true;
+#else
+ _general_serial = general["serial"];
+#endif
+
+ JsonObject mqtt = doc["mqtt"];
+ _mqtt_enabled = mqtt["enabled"];
+ _mqtt_heartbeat = mqtt["heartbeat"];
+ _mqtt_ip = strdup(mqtt["ip"] | "");
+ _mqtt_user = strdup(mqtt["user"] | "");
+ _mqtt_port = mqtt["port"] | MQTT_PORT;
+ _mqtt_password = strdup(mqtt["password"] | "");
+ _mqtt_base = strdup(mqtt["base"] | MQTT_BASE_DEFAULT);
+
+ JsonObject ntp = doc["ntp"];
+ _ntp_server = strdup(ntp["server"] | "");
+ _ntp_interval = ntp["interval"] | 60;
+ if (_ntp_interval == 0)
+ _ntp_interval = 60;
+ _ntp_enabled = ntp["enabled"];
+
+ myDebug_P(PSTR("[FS] System settings loaded"));
+ // serializeJsonPretty(doc, Serial); // turn on for debugging
+
+ return true;
+}
+
+// load custom settings
+bool MyESP::_fs_loadCustomConfig() {
+ File configFile = SPIFFS.open(MYESP_CUSTOMCONFIG_FILE, "r");
+ if (!configFile) {
+ myDebug_P(PSTR("[FS] No custom config found"));
+ return false;
+ }
+
+ // check size
+ size_t size = configFile.size();
+ if (size > MYESP_SPIFFS_MAXSIZE) {
+ configFile.close();
+ myDebug_P(PSTR("[FS] Custom config size is too large"));
+ return false;
+ } else if (size == 0) {
+ configFile.close();
+ myDebug_P(PSTR("[FS] Corrupted custom config"));
+ return false;
+ }
+
+ // read file from SPIFFS into a char array
+ char data[MYESP_SPIFFS_MAXSIZE] = {0};
+ if (configFile.readBytes(data, size) != size) {
+ myDebug_P(PSTR("[FS] File sizes don't match with custom config"));
+ configFile.close();
+ return false;
+ }
+ configFile.close();
+
+ // create the JSON doc and pass it back to the callback function
+ StaticJsonDocument doc;
+ JsonObject json = doc.to(); // create empty object
+
+ DeserializationError error = deserializeJson(doc, data); // Deserialize the JSON document
+
+ if (error) {
+ myDebug_P(PSTR("[FS] Failed to deserialize json for custom config, error %s"), error.c_str());
+ configFile.close();
+ return false;
+ }
+
+ if (_fs_loadsave_callback_f) {
+ if (!(_fs_loadsave_callback_f)(MYESP_FSACTION_LOAD, json)) {
+ myDebug_P(PSTR("[FS] Error reading custom config"));
+ return false;
+ } else {
+ myDebug_P(PSTR("[FS] Custom config loaded"));
+ //serializeJsonPretty(doc, Serial); // added for debugging
+ }
+ }
+
+ return true;
+}
+
+// save custom config to spiffs
+bool MyESP::fs_saveCustomConfig(JsonObject root) {
+ bool ok = false;
+
+ // open for writing
+ File configFile = SPIFFS.open(MYESP_CUSTOMCONFIG_FILE, "w");
+ if (!configFile) {
+ myDebug_P(PSTR("[FS] Failed to open custom config for writing"));
+ return false;
+ }
+
+ // call any custom functions before handling SPIFFS
+ if (_ota_pre_callback_f) {
+ (_ota_pre_callback_f)();
+ }
+
+ // Serialize JSON to file
+ size_t n = serializeJson(root, configFile);
+ configFile.close();
+
+ if (n) {
+ // reload the settings
+ if (_fs_loadsave_callback_f) {
+ if (!(_fs_loadsave_callback_f)(MYESP_FSACTION_LOAD, root)) {
+ myDebug_P(PSTR("[FS] Error parsing custom config json"));
+ }
+ }
+
+ _writeEvent("INFO", "system", "Custom config stored in the SPIFFS", "");
+ myDebug_P(PSTR("[FS] custom config saved"));
+ ok = true;
+ }
+
+ if (_ota_pre_callback_f) {
+ (_ota_pre_callback_f)();
+ }
+
+ return ok;
+}
+
+// save system config to spiffs
+bool MyESP::fs_saveConfig(JsonObject root) {
+ bool ok = false;
+
+ // open for writing
+ File configFile = SPIFFS.open(MYESP_CONFIG_FILE, "w");
+ if (!configFile) {
+ myDebug_P(PSTR("[FS] Failed to open system config for writing"));
+ return false;
+ }
+
+ // call any custom functions before handling SPIFFS
+ if (_ota_pre_callback_f) {
+ (_ota_pre_callback_f)();
+ }
+
+ // Serialize JSON to file
+ size_t n = serializeJson(root, configFile);
+ configFile.close();
+
+ if (n) {
+ _writeEvent("INFO", "system", "System config stored in the SPIFFS", "");
+ myDebug_P(PSTR("[FS] system config saved"));
+ ok = true;
+ }
+
+ // serializeJsonPretty(root, Serial); // for debugging
+
+ if (_ota_pre_callback_f) {
+ (_ota_pre_callback_f)();
+ }
+
+ return ok;
+}
+
+// create an initial system config file using default settings
+bool MyESP::_fs_writeConfig() {
+ StaticJsonDocument doc;
+ JsonObject root = doc.to();
+
+ root["command"] = "configfile"; // header, important!
+
+ JsonObject network = doc.createNestedObject("network");
+ network["ssid"] = _network_ssid;
+ network["password"] = _network_password;
+ network["wmode"] = _network_wmode;
+
+ JsonObject general = doc.createNestedObject("general");
+ general["password"] = _general_password;
+ general["serial"] = _general_serial;
+ general["hostname"] = _general_hostname;
+
+ JsonObject mqtt = doc.createNestedObject("mqtt");
+ mqtt["enabled"] = _mqtt_enabled;
+ mqtt["heartbeat"] = _mqtt_heartbeat;
+ mqtt["ip"] = _mqtt_ip;
+ mqtt["user"] = _mqtt_user;
+ mqtt["port"] = _mqtt_port;
+ mqtt["password"] = _mqtt_password;
+ mqtt["base"] = _mqtt_base;
+
+ JsonObject ntp = doc.createNestedObject("ntp");
+ ntp["server"] = _ntp_server;
+ ntp["interval"] = _ntp_interval;
+ ntp["enabled"] = _ntp_enabled;
+
+ bool ok = fs_saveConfig(root); // save it
+
+ return ok;
+}
+
+// create an empty json doc for the custom config and call callback to populate it
+bool MyESP::_fs_createCustomConfig() {
+ StaticJsonDocument doc;
+ JsonObject json = doc.to();
+
+ json["command"] = "custom_configfile"; // header, important!
+
+ if (_fs_loadsave_callback_f) {
+ if (!(_fs_loadsave_callback_f)(MYESP_FSACTION_SAVE, json)) {
+ myDebug_P(PSTR("[FS] Error building custom config json"));
+ }
+ } else {
+ myDebug_P(PSTR("[FS] Created custom config"));
+ }
+
+ bool ok = fs_saveCustomConfig(json);
+
+ return ok;
+}
+
+
+// init the SPIFF file system and load the config
+// if it doesn't exist try and create it
+void MyESP::_fs_setup() {
+ if (!SPIFFS.begin()) {
+ myDebug_P(PSTR("[FS] Formatting filesystem..."));
+ if (SPIFFS.format()) {
+ _writeEvent("WARN", "system", "File system formatted", "");
+ } else {
+ myDebug_P(PSTR("[FS] Failed to format file system"));
+ }
+ }
+
+ // load the main system config file if we can. Otherwise create it and expect user to configure in web interface
+ if (!_fs_loadConfig()) {
+ myDebug_P(PSTR("[FS] Creating a new system config"));
+ _fs_writeConfig(); // create the initial config file
+ }
+
+ // load system and custom config
+ if (!_fs_loadCustomConfig()) {
+ _fs_createCustomConfig(); // create the initial config file
+ }
+}
+
+uint32_t MyESP::getSystemLoadAverage() {
+ return _load_average;
+}
+
+// calculate load average
+void MyESP::_calculateLoad() {
+ static uint32_t last_loadcheck = 0;
+ static uint32_t load_counter_temp = 0;
+ load_counter_temp++;
+
+ if (millis() - last_loadcheck > MYESP_LOADAVG_INTERVAL) {
+ static uint32_t load_counter = 0;
+ static uint32_t load_counter_max = 1;
+
+ load_counter = load_counter_temp;
+ load_counter_temp = 0;
+ if (load_counter > load_counter_max) {
+ load_counter_max = load_counter;
+ }
+ _load_average = 100 - (100 * load_counter / load_counter_max);
+ last_loadcheck = millis();
+ }
+}
+
+// returns true is MQTT is alive
+bool MyESP::isMQTTConnected() {
+ return mqttClient.connected();
+}
+
+// return true if wifi is connected (client or AP mode)
+bool MyESP::isWifiConnected() {
+ return (_wifi_connected);
+}
+
+/*
+ Return the quality (Received Signal Strength Indicator)
+ of the WiFi network.
+ Returns a number between 0 and 100 if WiFi is connected.
+ Returns -1 if WiFi is disconnected.
+
+ High quality: 90% ~= -55dBm
+ Medium quality: 50% ~= -75dBm
+ Low quality: 30% ~= -85dBm
+ Unusable quality: 8% ~= -96dBm
+*/
+int MyESP::getWifiQuality() {
+ if (WiFi.status() != WL_CONNECTED)
+ return -1;
+ int dBm = WiFi.RSSI();
+ if (dBm <= -100)
+ return 0;
+ if (dBm >= -50)
+ return 100;
+ return 2 * (dBm + 100);
+}
+
+#ifdef CRASH
+/**
+ * Save crash information in EEPROM
+ * This function is called automatically if ESP8266 suffers an exception
+ * It should be kept quick / consise to be able to execute before hardware wdt may kick in
+ */
+extern "C" void custom_crash_callback(struct rst_info * rst_info, uint32_t stack_start, uint32_t stack_end) {
+ // write crash time to EEPROM
+ uint32_t crash_time = millis();
+ EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time);
+
+ // write reset info to EEPROM
+ EEPROMr.write(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_RESTART_REASON, rst_info->reason);
+ EEPROMr.write(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCEPTION_CAUSE, rst_info->exccause);
+
+ // write epc1, epc2, epc3, excvaddr and depc to EEPROM
+ EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC1, rst_info->epc1);
+ EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC2, rst_info->epc2);
+ EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC3, rst_info->epc3);
+ EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCVADDR, rst_info->excvaddr);
+ EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_DEPC, rst_info->depc);
+
+ // write stack start and end address to EEPROM
+ EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_START, stack_start);
+ EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_END, stack_end);
+
+ // write stack trace to EEPROM and avoid overwriting settings
+ int16_t current_address = SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_TRACE;
+ for (uint32_t i = stack_start; i < stack_end; i++) {
+ byte * byteValue = (byte *)i;
+ EEPROMr.write(current_address++, *byteValue);
+ }
+
+ EEPROMr.commit();
+}
+
+/**
+ * Clears crash info
+ */
+void MyESP::crashClear() {
+ myDebug_P(PSTR("[CRASH] Clearing crash dump"));
+ uint32_t crash_time = 0xFFFFFFFF;
+ EEPROMr.put(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time);
+ EEPROMr.commit();
+}
+
+/**
+ * Print out crash information that has been previously saved in EEPROM
+ * Copied from https://github.com/krzychb/EspSaveCrash
+ */
+void MyESP::crashDump() {
+ uint32_t crash_time;
+ EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_CRASH_TIME, crash_time);
+ if ((crash_time == 0) || (crash_time == 0xFFFFFFFF)) {
+ myDebug_P(PSTR("[CRASH] No crash data captured."));
+ return;
+ }
+
+ uint32_t t = crash_time / 1000; // convert to seconds
+ uint32_t d = t / 86400L;
+ uint32_t h = (t / 3600L) % 60;
+ uint32_t rem = t % 3600L;
+ uint8_t m = rem / 60;
+ uint8_t s = rem % 60;
+ myDebug_P(PSTR("[CRASH] Last crash was %d days %d hours %d minutes %d seconds since boot time"), d, h, m, s);
+
+ // get reason and exception
+ // https://www.espressif.com/sites/default/files/documentation/esp8266_reset_causes_and_common_fatal_exception_causes_en.pdf
+ char buffer[80] = {0};
+ char ss[16] = {0};
+ strlcpy(buffer, "[CRASH] Reason of restart: ", sizeof(buffer));
+
+ uint8_t reason = EEPROMr.read(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_RESTART_REASON);
+ switch (reason) {
+ case REASON_WDT_RST:
+ strlcat(buffer, "1 - Hardware WDT reset", sizeof(buffer));
+ break;
+ case REASON_EXCEPTION_RST:
+ strlcat(buffer, "2 - Fatal exception", sizeof(buffer));
+ break;
+ case REASON_SOFT_WDT_RST:
+ strlcat(buffer, "3 - Software watchdog reset", sizeof(buffer));
+ break;
+ case REASON_EXT_SYS_RST:
+ strlcat(buffer, "6 - Hardware reset", sizeof(buffer));
+ break;
+ case REASON_SOFT_RESTART:
+ strlcat(buffer, "4 - Software reset", sizeof(buffer));
+ break;
+ default:
+ strlcat(buffer, itoa(reason, ss, 10), sizeof(buffer));
+ }
+ myDebug(buffer);
+
+ // check for exception
+ // see https://github.com/esp8266/Arduino/blob/master/doc/exception_causes.rst
+ if (reason == REASON_EXCEPTION_RST) {
+ // get exception cause
+ uint8_t cause = EEPROMr.read(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCEPTION_CAUSE);
+ strlcpy(buffer, "[CRASH] Exception cause: ", sizeof(buffer));
+ if (cause == 0) {
+ strlcat(buffer, "0 - IllegalInstructionCause", sizeof(buffer));
+ } else if (cause == 3) {
+ strlcat(buffer, "3 - LoadStoreErrorCause", sizeof(buffer));
+ } else if (cause == 6) {
+ strlcat(buffer, "6 - IntegerDivideByZeroCause", sizeof(buffer));
+ } else if (cause == 9) {
+ strlcat(buffer, "9 - LoadStoreAlignmentCause", sizeof(buffer));
+ } else {
+ strlcat(buffer, itoa(cause, ss, 10), sizeof(buffer));
+ }
+ }
+ myDebug(buffer);
+
+ uint32_t epc1, epc2, epc3, excvaddr, depc;
+ EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC1, epc1);
+ EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC2, epc2);
+ EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EPC3, epc3);
+ EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_EXCVADDR, excvaddr);
+ EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_DEPC, depc);
+
+ myDebug_P(PSTR("[CRASH] epc1=0x%08x epc2=0x%08x epc3=0x%08x"), epc1, epc2, epc3);
+ myDebug_P(PSTR("[CRASH] excvaddr=0x%08x depc=0x%08x"), excvaddr, depc);
+
+ uint32_t stack_start, stack_end;
+ EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_START, stack_start);
+ EEPROMr.get(SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_END, stack_end);
+
+ myDebug_P(PSTR("[CRASH] sp=0x%08x end=0x%08x"), stack_start, stack_end);
+
+ int16_t current_address = SAVE_CRASH_EEPROM_OFFSET + SAVE_CRASH_STACK_TRACE;
+ int16_t stack_len = stack_end - stack_start;
+
+ uint32_t stack_trace;
+
+ myDebug_P(PSTR(">>>stack>>>"));
+
+ for (int16_t i = 0; i < stack_len; i += 0x10) {
+ SerialAndTelnet.printf("%08x: ", stack_start + i);
+ for (byte j = 0; j < 4; j++) {
+ EEPROMr.get(current_address, stack_trace);
+ SerialAndTelnet.printf("%08x ", stack_trace);
+ current_address += 4;
+ }
+ SerialAndTelnet.println();
+ }
+ myDebug_P(PSTR("<< root;
+ root["type"] = type;
+ root["src"] = src;
+ root["desc"] = desc;
+ root["data"] = data;
+ root["time"] = now(); // is relative if we're not using NTP
+
+ // Serialize JSON to file
+ size_t n = serializeJson(root, eventlog);
+ eventlog.print("\n"); // this indicates end of the entry
+
+ if (!n) {
+ //Serial.println("[SYSTEM] Error writing to event log"); // for debugging
+ }
+
+ eventlog.close();
+}
+
+// send a paged list (10 items) to the ws
+void MyESP::_sendEventLog(uint8_t page) {
+ File eventlog = SPIFFS.open(MYESP_EVENTLOG_FILE, "r");
+ if (!eventlog) {
+ eventlog.close();
+ myDebug_P(PSTR("[WEB] Event log is missing"));
+ if (_ota_post_callback_f) {
+ (_ota_post_callback_f)(); // call custom function
+ }
+ return; // file can't be opened
+ }
+
+ if (_ota_pre_callback_f) {
+ (_ota_pre_callback_f)(); // call custom function
+ }
+
+ // the size of the json will be quite big so best not to use stack (StaticJsonDocument)
+ DynamicJsonDocument doc(MYESP_JSON_MAXSIZE);
+ JsonObject root = doc.to();
+ root["command"] = "eventlist";
+ root["page"] = page;
+
+ JsonArray list = doc.createNestedArray("list");
+
+ uint8_t first = ((page - 1) * 10) + 1;
+ uint8_t last = page * 10;
+ uint8_t char_count = 0;
+ uint8_t line_count = 0;
+ uint16_t read_count = 0;
+ bool abort = false;
+ char char_buffer[MYESP_JSON_LOG_MAXSIZE];
+
+ // if at start, start immediately recording
+ bool record = (first == 1) ? true : false;
+
+ // start at top and read until we find the page we want (sets of 10)
+ while (eventlog.available() && !abort) {
+ char c = eventlog.read();
+
+ // see if we've overrun, which means corrupt so ignore rest
+ if (read_count++ > MYESP_JSON_LOG_MAXSIZE - 1) {
+ abort = true;
+ }
+
+ // see if we have reached the end of the string
+ if (c == '\0' || c == '\n') {
+ line_count++;
+
+ // save line
+ if (record) {
+ char_buffer[char_count] = '\0';
+ list.add(char_buffer);
+ }
+
+ char_count = 0;
+ read_count = 0;
+ if (line_count == first - 1) { // have we come to the start position, start recording
+ record = true;
+ } else if (line_count == last) { // finish recording and exit loop
+ record = false;
+ }
+ } else {
+ // add the char to the buffer if recording
+ if (record && (char_count < MYESP_JSON_LOG_MAXSIZE)) {
+ char_buffer[char_count++] = c;
+ }
+ }
+ }
+ eventlog.close(); // close SPIFFS
+
+ float pages = line_count / 10.0;
+ root["haspages"] = ceil(pages);
+
+ char buffer[MYESP_JSON_MAXSIZE];
+ size_t len = serializeJson(root, buffer);
+
+ //Serial.printf("\nEVENTLOG: page %d\n", page); // turn on for debugging
+ //serializeJson(root, Serial); // turn on for debugging
+
+ _ws->textAll(buffer, len);
+ _ws->textAll("{\"command\":\"result\",\"resultof\":\"eventlist\",\"result\": true}");
+
+ if (_ota_post_callback_f) {
+ (_ota_post_callback_f)(); // call custom function
+ }
+}
+
+// Handles WebSocket Events
+void MyESP::_onWsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t * data, size_t len) {
+ if (type == WS_EVT_ERROR) {
+ myDebug("[WEB] WebSocket[%s][%u] error(%u): %s\r\n", server->url(), client->id(), *((uint16_t *)arg), (char *)data);
+ } else if (type == WS_EVT_DATA) {
+ AwsFrameInfo * info = (AwsFrameInfo *)arg;
+ uint64_t index = info->index;
+ uint64_t infolen = info->len;
+ if (info->final && info->index == 0 && infolen == len) {
+ // the whole message is in a single frame and we got all of it's data
+ client->_tempObject = malloc(len);
+ if (client->_tempObject != NULL) {
+ memcpy((uint8_t *)(client->_tempObject), data, len);
+ }
+ _procMsg(client, infolen);
+ } else {
+ // message is comprised of multiple frames or the frame is split into multiple packets
+ if (index == 0) {
+ if (info->num == 0 && client->_tempObject == NULL) {
+ client->_tempObject = malloc(infolen);
+ }
+ }
+ if (client->_tempObject != NULL) {
+ memcpy((uint8_t *)(client->_tempObject) + index, data, len);
+ }
+ if ((index + len) == infolen) {
+ if (info->final) {
+ _procMsg(client, infolen);
+ }
+ }
+ }
+ }
+}
+
+// handle ws from browser
+void MyESP::_procMsg(AsyncWebSocketClient * client, size_t sz) {
+ // We should always get a JSON object from browser, so parse it
+ StaticJsonDocument<400> doc;
+ char json[sz + 1];
+ memcpy(json, (char *)(client->_tempObject), sz);
+ json[sz] = '\0';
+
+ JsonObject root = doc.to(); // create empty object
+ DeserializationError error = deserializeJson(doc, json); // Deserialize the JSON document
+ if (error) {
+ myDebug_P(PSTR("[WEB] Couldn't parse WebSocket message, error %s"), error.c_str());
+ free(client->_tempObject);
+ client->_tempObject = NULL;
+ return;
+ }
+
+ const char * command = doc["command"];
+ // Serial.printf("*** Got command: %s\n", command); // turn on for debugging
+
+ // Check whatever the command is and act accordingly
+ if (strcmp(command, "configfile") == 0) {
+ if (_ota_pre_callback_f) {
+ (_ota_pre_callback_f)();
+ }
+ _shouldRestart = fs_saveConfig(root);
+ if (_ota_post_callback_f) {
+ (_ota_post_callback_f)();
+ }
+ } else if (strcmp(command, "custom_configfile") == 0) {
+ if (_ota_pre_callback_f) {
+ (_ota_pre_callback_f)();
+ }
+ (void)fs_saveCustomConfig(root);
+ if (_ota_post_callback_f) {
+ (_ota_post_callback_f)();
+ }
+ } else if (strcmp(command, "status") == 0) {
+ _sendStatus();
+ } else if (strcmp(command, "custom_status") == 0) {
+ _sendCustomStatus();
+ } else if (strcmp(command, "restart") == 0) {
+ _shouldRestart = true;
+ } else if (strcmp(command, "destroy") == 0) {
+ _formatreq = true;
+ } else if (strcmp(command, "forcentp") == 0) {
+ NTP.getNtpTime();
+ } else if (strcmp(command, "geteventlog") == 0) {
+ uint8_t page = doc["page"];
+ _sendEventLog(page);
+ } else if (strcmp(command, "clearevent") == 0) {
+ if (SPIFFS.remove(MYESP_EVENTLOG_FILE)) {
+ _writeEvent("WARN", "system", "Event log cleared", "");
+ } else {
+ myDebug_P(PSTR("[WEB] Couldn't clear log file"));
+ }
+ } else if (strcmp(command, "scan") == 0) {
+ WiFi.scanNetworksAsync(std::bind(&MyESP::_printScanResult, this, std::placeholders::_1), true);
+ } else if (strcmp(command, "gettime") == 0) {
+ _timerequest = true;
+ } else if (strcmp(command, "settime") == 0) {
+ time_t t = doc["epoch"];
+ setTime(t);
+ _timerequest = true;
+ } else if (strcmp(command, "getconf") == 0) {
+ _fs_sendConfig();
+ }
+
+ free(client->_tempObject);
+ client->_tempObject = NULL;
+}
+
+// read both system config and the custom config and send as json to web socket
+bool MyESP::_fs_sendConfig() {
+ File configFile;
+ size_t size;
+ char json[MYESP_SPIFFS_MAXSIZE] = {0};
+
+ configFile = SPIFFS.open(MYESP_CONFIG_FILE, "r");
+ if (!configFile) {
+ myDebug_P(PSTR("[FS] No system config found to load"));
+ return false;
+ }
+ size = configFile.size();
+
+ // read file from SPIFFS into a char array
+ if (configFile.readBytes(json, size) != size) {
+ configFile.close();
+ return false;
+ }
+ configFile.close();
+
+ //Serial.printf("_fs_sendConfig() sending system (%d): %s\n", size, json); // turn on for debugging
+ _ws->textAll(json, size);
+
+ configFile = SPIFFS.open(MYESP_CUSTOMCONFIG_FILE, "r");
+ if (!configFile) {
+ myDebug_P(PSTR("[FS] No custom config found to load"));
+ return false;
+ }
+ size = configFile.size();
+
+ // read file from SPIFFS into the same char array
+ memset(json, 0, MYESP_SPIFFS_MAXSIZE);
+ if (configFile.readBytes(json, size) != size) {
+ configFile.close();
+ return false;
+ }
+ configFile.close();
+
+ //Serial.printf("_fs_sendConfig() sending custom (%d): %s\n", size, json); // turn on for debugging
+ _ws->textAll(json, size);
+
+ return true;
+}
+
+// send custom status via ws
+void MyESP::_sendCustomStatus() {
+ // StaticJsonDocument<300> doc;
+ DynamicJsonDocument doc(MYESP_JSON_MAXSIZE);
+
+ JsonObject root = doc.to();
+
+ root["command"] = "custom_status";
+ root["version"] = _app_version;
+ root["customname"] = _app_name;
+ root["appurl"] = _app_url;
+ root["updateurl"] = _app_updateurl;
+
+ // add specific custom stuff
+ if (_web_callback_f) {
+ (_web_callback_f)(root);
+ }
+
+ char buffer[MYESP_JSON_MAXSIZE];
+ size_t len = serializeJson(root, buffer);
+ // Serial.printf("_sendCustomStatus() sending: %s\n", buffer); // turn on for debugging
+
+ _ws->textAll(buffer, len);
+}
+
+// send system status via ws
+void MyESP::_sendStatus() {
+ // capture memory before we stick in a huge json buffer on the heap!
+ uint32_t total_memory = _getInitialFreeHeap();
+ uint32_t free_memory = ESP.getFreeHeap();
+
+ DynamicJsonDocument doc(MQTT_MAX_PAYLOAD_SIZE_LARGE);
+ JsonObject root = doc.to();
+ root["command"] = "status";
+
+ FSInfo fsinfo;
+ if (!SPIFFS.info(fsinfo)) {
+ myDebug("[SYSTEM] Error getting info on SPIFFS");
+ } else {
+ root["availspiffs"] = (fsinfo.totalBytes - fsinfo.usedBytes) / 1000;
+ root["spiffssize"] = (fsinfo.totalBytes / 1000);
+ }
+
+ // all sizes in bytes converted to KB
+ root["initheap"] = total_memory;
+ root["heap"] = free_memory;
+ root["sketchsize"] = ESP.getSketchSize() / 1000;
+ root["availsize"] = ESP.getFreeSketchSpace() / 1000;
+
+ if (isAPmode()) {
+ root["ip"] = WiFi.softAPIP().toString();
+ root["ssid"] = jw.getAPSSID();
+ root["mac"] = WiFi.softAPmacAddress();
+ } else {
+ root["ip"] = WiFi.localIP().toString();
+ root["ssid"] = WiFi.SSID();
+ root["mac"] = WiFi.macAddress();
+ }
+
+ root["signalstr"] = getWifiQuality();
+ root["systemload"] = getSystemLoadAverage();
+
+ root["mqttconnected"] = isMQTTConnected();
+ root["mqttheartbeat"] = getHeartbeat();
+
+ char uptime[200];
+ uint32_t t = _getUptime(); // seconds
+ uint8_t d = t / 86400L;
+ uint8_t h = ((t % 86400L) / 3600L) % 60;
+ uint32_t rem = t % 3600L;
+ uint8_t m = rem / 60;
+ uint8_t sec = rem % 60;
+ sprintf(uptime, "%d day%s %d hour%s %d minute%s %d second%s", d, (d == 1) ? "" : "s", h, (h == 1) ? "" : "s", m, (m == 1) ? "" : "s", sec, (sec == 1) ? "" : "s");
+ root["uptime"] = uptime;
+
+ char topic_s[MQTT_MAX_TOPIC_SIZE] = {0};
+ if (_hasValue(_mqtt_base)) {
+ strlcpy(topic_s, _mqtt_base, sizeof(topic_s));
+ strlcat(topic_s, "/", sizeof(topic_s));
+ strlcat(topic_s, _general_hostname, sizeof(topic_s));
+ } else {
+ strlcpy(topic_s, _general_hostname, sizeof(topic_s));
+ }
+ strlcat(topic_s, "/", sizeof(topic_s));
+ root["mqttloghdr"] = topic_s;
+
+ // create MQTT log
+ JsonArray list = root.createNestedArray("mqttlog");
+
+ for (uint8_t i = 0; i < MYESP_MQTTLOG_MAX; i++) {
+ if (MQTT_log[i].topic != nullptr) {
+ JsonObject item = list.createNestedObject();
+ item["topic"] = MQTT_log[i].topic;
+ item["payload"] = MQTT_log[i].payload;
+ item["time"] = MQTT_log[i].timestamp;
+ }
+ }
+
+ char buffer[MQTT_MAX_PAYLOAD_SIZE_LARGE];
+ size_t len = serializeJson(root, buffer);
+
+ _ws->textAll(buffer, len);
+}
+
+// print top5 wifi
+void MyESP::_printScanResult(int networksFound) {
+ int n = networksFound;
+ int indices[n];
+ for (int i = 0; i < networksFound; i++) {
+ indices[i] = i;
+ }
+
+ // sort by RSSI
+ int skip[n];
+ for (int i = 0; i < networksFound; i++) {
+ for (int j = i + 1; j < networksFound; j++) {
+ if (WiFi.RSSI(indices[j]) > WiFi.RSSI(indices[i])) {
+ std::swap(indices[i], indices[j]);
+ std::swap(skip[i], skip[j]);
+ }
+ }
+ }
+
+ StaticJsonDocument<400> doc;
+ JsonObject root = doc.to();
+ root["command"] = "ssidlist";
+
+ JsonArray list = doc.createNestedArray("list");
+ for (int i = 0; i <= 5 && i < networksFound; ++i) {
+ JsonObject item = list.createNestedObject();
+ item["ssid"] = WiFi.SSID(indices[i]);
+ item["bssid"] = WiFi.BSSIDstr(indices[i]);
+ item["rssi"] = WiFi.RSSI(indices[i]);
+ }
+
+ char buffer[400];
+ size_t len = serializeJson(root, buffer);
+ _ws->textAll(buffer, len);
+}
+
+// set up web server
+void MyESP::_webserver_setup() {
+ _ws->onEvent(std::bind(&MyESP::_onWsEvent,
+ this,
+ std::placeholders::_1,
+ std::placeholders::_2,
+ std::placeholders::_3,
+ std::placeholders::_4,
+ std::placeholders::_5,
+ std::placeholders::_6));
+ _webServer->addHandler(_ws);
+
+ _webServer->onNotFound([](AsyncWebServerRequest * request) {
+ AsyncWebServerResponse * response = request->beginResponse(404, "text/plain", "Not found");
+ request->send(response);
+ });
+
+ _webServer->on("/update",
+ HTTP_POST,
+ [](AsyncWebServerRequest * request) {
+ AsyncWebServerResponse * response = request->beginResponse(200, "text/plain", _shouldRestart ? "OK" : "FAIL");
+ response->addHeader("Connection", "close");
+ request->send(response);
+ },
+ [](AsyncWebServerRequest * request, String filename, size_t index, uint8_t * data, size_t len, bool final) {
+ if (!request->authenticate(MYESP_HTTP_USERNAME, _general_password)) {
+ return;
+ }
+ if (!index) {
+ _writeEvent("INFO", "system", "Firmware update started", "");
+ //Serial.printf("[SYSTEM] Firmware update started: %s\n", filename.c_str()); // enable for debugging
+ Update.runAsync(true);
+ if (!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)) {
+ _writeEvent("ERRO", "system", "Not enough space to update", "");
+ //Update.printError(Serial); // enable for debugging
+ }
+ }
+ if (!Update.hasError()) {
+ if (Update.write(data, len) != len) {
+ _writeEvent("ERRO", "system", "Writing to flash failed", "");
+ //Update.printError(Serial); // enable for debugging
+ }
+ }
+ if (final) {
+ if (Update.end(true)) {
+ _writeEvent("INFO", "system", "Firmware update finished", "");
+ //Serial.printf("[SYSTEM] Firmware update finished: %uB\n", index + len); // enable for debugging
+ _shouldRestart = !Update.hasError();
+ } else {
+ _writeEvent("ERRO", "system", "Firmware update failed", "");
+ //Update.printError(Serial); // enable for debugging
+ }
+ }
+ });
+
+ _webServer->on("/fonts/glyphicons-halflings-regular.woff", HTTP_GET, [](AsyncWebServerRequest * request) {
+ AsyncWebServerResponse * response =
+ request->beginResponse_P(200, "font/woff", glyphicons_halflings_regular_woff_gz, glyphicons_halflings_regular_woff_gz_len);
+ response->addHeader("Content-Encoding", "gzip");
+ request->send(response);
+ });
+ _webServer->on("/css/required.css", HTTP_GET, [](AsyncWebServerRequest * request) {
+ AsyncWebServerResponse * response = request->beginResponse_P(200, "text/css", required_css_gz, required_css_gz_len);
+ response->addHeader("Content-Encoding", "gzip");
+ request->send(response);
+ });
+ _webServer->on("/js/required.js", HTTP_GET, [](AsyncWebServerRequest * request) {
+ AsyncWebServerResponse * response = request->beginResponse_P(200, "text/javascript", required_js_gz, required_js_gz_len);
+ response->addHeader("Content-Encoding", "gzip");
+ request->send(response);
+ });
+ _webServer->on("/js/myesp.js", HTTP_GET, [](AsyncWebServerRequest * request) {
+ AsyncWebServerResponse * response = request->beginResponse_P(200, "text/javascript", myesp_js_gz, myesp_js_gz_len);
+ response->addHeader("Content-Encoding", "gzip");
+ request->send(response);
+ });
+
+ _webServer->on("/index.html", HTTP_GET, [](AsyncWebServerRequest * request) {
+ AsyncWebServerResponse * response = request->beginResponse_P(200, "text/html", index_html_gz, index_html_gz_len);
+ response->addHeader("Content-Encoding", "gzip");
+ request->send(response);
+ });
+
+ _webServer->on("/myesp.html", HTTP_GET, [](AsyncWebServerRequest * request) {
+ AsyncWebServerResponse * response = request->beginResponse_P(200, "text/html", myesp_html_gz, myesp_html_gz_len);
+ response->addHeader("Content-Encoding", "gzip");
+ request->send(response);
+ });
+
+ _webServer->on("/login", HTTP_GET, [](AsyncWebServerRequest * request) {
+ //IPAddress address = request->client()->remoteIP();
+ //static String remoteIP = (String)address[0] + "." + (String)address[1] + "." + (String)address[2] + "." + (String)address[3];
+
+ if (!request->authenticate(MYESP_HTTP_USERNAME, _general_password)) {
+ //_writeEvent("WARN", "system", "New login attempt", remoteIP);
+ return request->requestAuthentication();
+ }
+ request->send(200, "text/plain", "Success");
+ // _writeEvent("INFO", "system", "Login successful", remoteIP);
+ });
+
+ _webServer->rewrite("/", "/index.html");
+ _webServer->begin();
+
+ myDebug_P(PSTR("[WEB] Web server started"));
+}
+
+// print memory
+void MyESP::_printHeap(const char * s) {
+ uint32_t total_memory = _getInitialFreeHeap();
+ uint32_t free_memory = ESP.getFreeHeap();
+
+ myDebug(" [%s] Free Heap: %d bytes initially | %d bytes used (%2u%%) | %d bytes free (%2u%%)",
+ s,
+ total_memory,
+ total_memory - free_memory,
+ 100 * (total_memory - free_memory) / total_memory,
+ free_memory,
+ 100 * free_memory / total_memory);
+}
+
+// print MQTT log - everything that was published last per topic
+void MyESP::_printMQTTLog() {
+ myDebug_P(PSTR("MQTT publish log:"));
+
+ for (uint8_t i = 0; i < MYESP_MQTTLOG_MAX; i++) {
+ if (MQTT_log[i].topic != nullptr) {
+ myDebug_P(PSTR("(%d) [%lu] Topic:%s Payload:%s"), i, MQTT_log[i].timestamp, MQTT_log[i].topic, MQTT_log[i].payload);
+ }
+ }
+
+ myDebug_P(PSTR("")); // newline
+}
+
+// add an MQTT log entry to our buffer
+void MyESP::_addMQTTLog(const char * topic, const char * payload) {
+ static uint8_t logCount = 0;
+ uint8_t logPointer = 0;
+ bool found = false;
+
+ // myDebug("Publish [#%d] %s (%d) %s (%d)", logCount, topic, strlen(topic), payload, strlen(payload)); // for debugging
+
+ // find the topic
+ while ((_hasValue(MQTT_log[logPointer].topic) && logPointer < MYESP_MQTTLOG_MAX)) {
+ if (strcmp(MQTT_log[logPointer].topic, topic) == 0) {
+ found = true;
+ break;
+ }
+ logPointer++;
+ }
+
+ // if not found add it and increment next free space pointer
+ if (!found) {
+ logPointer = logCount;
+ if (++logCount == MYESP_MQTTLOG_MAX) {
+ logCount = 0; // rotate
+ }
+ }
+
+ // delete old record
+ if (MQTT_log[logPointer].topic) {
+ free(MQTT_log[logPointer].topic);
+ }
+
+ if (MQTT_log[logPointer].payload) {
+ free(MQTT_log[logPointer].payload);
+ }
+
+ // add new record
+ MQTT_log[logPointer].topic = strdup(topic);
+ MQTT_log[logPointer].payload = strdup(payload);
+ MQTT_log[logPointer].timestamp = now();
+}
+
+// send UTC time via ws
+void MyESP::_sendTime() {
+ StaticJsonDocument<100> doc;
+ JsonObject root = doc.to();
+ root["command"] = "gettime";
+ root["epoch"] = now();
+
+ char buffer[100];
+ size_t len = serializeJson(root, buffer);
+ _ws->textAll(buffer, len);
+}
+
+// bootup sequence
+// quickly flash LED until we get a Wifi connection, or AP established
+// fast way is to use WRITE_PERI_REG(PERIPHS_GPIO_BASEADDR + (state ? 4 : 8), (1 << EMSESP_Status.led_gpio)); // 4 is on, 8 is off
+void MyESP::_bootupSequence() {
+ uint8_t boot_status = getSystemBootStatus();
+
+ // check if its booted
+ if (boot_status == MYESP_BOOTSTATUS_BOOTED) {
+ if ((_ntp_enabled) && (now() > 10000) && !_have_ntp_time) {
+ _have_ntp_time = true;
+ _writeEvent("INFO", "system", "System booted", "");
+ }
+ return;
+ }
+
+ // still starting up
+ if (millis() <= MYESP_BOOTUP_DELAY) {
+ return;
+ }
+
+ // only kick in after a few seconds
+ if (boot_status == MYESP_BOOTSTATUS_POWERON) {
+ _setSystemBootStatus(MYESP_BOOTSTATUS_BOOTING);
+ }
+
+ static uint32_t last_bootupflash = 0;
+
+ // flash LED quickly
+ if ((millis() - last_bootupflash > MYESP_BOOTUP_FLASHDELAY)) {
+ last_bootupflash = millis();
+ int state = digitalRead(LED_BUILTIN);
+ digitalWrite(LED_BUILTIN, !state);
+ }
+
+ if (isWifiConnected()) {
+ _setSystemBootStatus(MYESP_BOOTSTATUS_BOOTED); // completed, reset flag
+ digitalWrite(LED_BUILTIN, HIGH); // turn off LED, 1=OFF with LED_BULLETIN
+
+ // write a log message if we're not using NTP, otherwise wait for the internet time to arrive
+ if (!_ntp_enabled) {
+ _writeEvent("INFO", "system", "System booted", "");
+ }
+ }
+}
+
+// setup MyESP
+void MyESP::begin(const char * app_hostname, const char * app_name, const char * app_version, const char * app_url, const char * app_updateurl) {
+ _general_hostname = strdup(app_hostname);
+ _app_name = strdup(app_name);
+ _app_version = strdup(app_version);
+ _app_url = strdup(app_url);
+ _app_updateurl = strdup(app_updateurl);
+
+ _telnet_setup(); // Telnet setup, called first to set Serial
+
+ // _fs_printFile(MYESP_CONFIG_FILE); // for debugging
+ // _fs_printFile(MYESP_CUSTOMCONFIG_FILE); // for debugging
+ // _fs_printFile(MYESP_EVENTLOG_FILE); // for debugging
+
+ // print a welcome message
+ myDebug_P(PSTR("\n\n* %s version %s"), _app_name, _app_version);
+
+ // set up onboard LED
+ pinMode(LED_BUILTIN, OUTPUT);
+ digitalWrite(LED_BUILTIN, HIGH);
+
+ _getInitialFreeHeap(); // get initial free mem
+ _rtcmemSetup(); // rtc internal mem setup
+ _eeprom_setup(); // set up EEPROM for storing crash data, if compiled with -DCRASH
+ _fs_setup(); // SPIFFS setup, do this first to get values
+ _wifi_setup(); // WIFI setup
+ _ota_setup(); // init OTA
+ _webserver_setup(); // init web server
+
+ _setSystemCheck(false); // reset system check
+ _heartbeatCheck(true); // force heartbeat
+
+ SerialAndTelnet.flush();
+}
+
+/*
+ * Loop. This is called as often as possible and it handles wifi, telnet, mqtt etc
+ */
+void MyESP::loop() {
+ _calculateLoad();
+ _systemCheckLoop();
+ _heartbeatCheck();
+ _bootupSequence(); // see if a reset was pressed during bootup
+
+ jw.loop(); // WiFi
+ ArduinoOTA.handle(); // OTA
+
+ ESP.wdtFeed(); // feed the watchdog...
+ _telnetHandle(); // telnet
+ ESP.wdtFeed(); // feed the watchdog...
+
+ _mqttConnect(); // MQTT
+
+ if (_timerequest) {
+ _timerequest = false;
+ _sendTime();
+ }
+
+ if (_formatreq) {
+ myDebug("[SYSTEM] Factory reset initiated. Please wait. System will automatically restart when complete...");
+ SPIFFS.end();
+ _ws->enable(false);
+ SPIFFS.format();
+ _deferredReset(500, CUSTOM_RESET_FACTORY);
+ ESP.restart();
+ }
+
+ if (_shouldRestart) {
+ _writeEvent("INFO", "system", "System is restarting", "");
+ myDebug("[SYSTEM] Restarting...");
+ _deferredReset(500, CUSTOM_RESET_TERMINAL);
+ ESP.restart();
+ }
+
+ yield(); // ... and breath.
+}
+
+MyESP myESP;
diff --git a/lib/MyESP/MyESP.h b/src/MyESP.h
similarity index 59%
rename from lib/MyESP/MyESP.h
rename to src/MyESP.h
index 74dcc2a4a..eaf3d3993 100644
--- a/lib/MyESP/MyESP.h
+++ b/src/MyESP.h
@@ -1,7 +1,7 @@
/*
* MyESP.h
*
- * Paul Derbyshire - December 2018
+ * Paul Derbyshire - first version December 2018
*/
#pragma once
@@ -9,15 +9,18 @@
#ifndef MyESP_h
#define MyESP_h
-#define MYESP_VERSION "1.1.24"
+#define MYESP_VERSION "1.2.0"
#include
#include
#include // https://github.com/marvinroger/async-mqtt-client and for ESP32 see https://github.com/marvinroger/async-mqtt-client/issues/127
-#include
+#include
+#include
#include
-#include // https://github.com/xoseperez/justwifi
-#include // modified from https://github.com/yasheena/telnetspy
+#include // https://github.com/xoseperez/justwifi
+
+#include "Ntp.h"
+#include "TelnetSpy.h" // modified from https://github.com/yasheena/telnetspy
#ifdef CRASH
#include
@@ -30,7 +33,6 @@ extern struct rst_info resetInfo;
}
#if defined(ARDUINO_ARCH_ESP32)
-//#include
#include // added for ESP32
#define ets_vsnprintf vsnprintf // added for ESP32
#define OTA_PORT 3232
@@ -39,24 +41,54 @@ extern struct rst_info resetInfo;
#define OTA_PORT 8266
#endif
-#define MYEMS_CONFIG_FILE "/config.json"
+// web files
+// reference libs
+#include "webh/glyphicons-halflings-regular.woff.gz.h"
+#include "webh/required.css.gz.h"
+#include "webh/required.js.gz.h"
-#define LOADAVG_INTERVAL 30000 // Interval between calculating load average (in ms) = 30 seconds
+// custom stuff
+#include "webh/index.html.gz.h"
+#include "webh/myesp.html.gz.h"
+#include "webh/myesp.js.gz.h"
+
+#define MYESP_CONFIG_FILE "/myesp.json"
+#define MYESP_CUSTOMCONFIG_FILE "/customconfig.json"
+#define MYESP_EVENTLOG_FILE "/eventlog.json"
+
+#define MYESP_HTTP_USERNAME "admin" // HTTP username
+#define MYESP_HTTP_PASSWORD "admin" // default password
+
+#define MYESP_NTP_SERVER "pool.ntp.org" // default ntp server
+
+#define MYESP_LOADAVG_INTERVAL 30000 // Interval between calculating load average (in ms) = 30 seconds
// WIFI
-#define WIFI_CONNECT_TIMEOUT 10000 // Connecting timeout for WIFI in ms (10 seconds)
-#define WIFI_RECONNECT_INTERVAL 600000 // If could not connect to WIFI, retry after this time in ms. 10 minutes
+#define MYESP_WIFI_CONNECT_TIMEOUT 10000 // Connecting timeout for WIFI in ms (10 seconds)
+#define MYESP_WIFI_RECONNECT_INTERVAL 600000 // If could not connect to WIFI, retry after this time in ms. 10 minutes
// MQTT
#define MQTT_PORT 1883 // MQTT port
#define MQTT_RECONNECT_DELAY_MIN 2000 // Try to reconnect in 3 seconds upon disconnection
#define MQTT_RECONNECT_DELAY_STEP 3000 // Increase the reconnect delay in 3 seconds after each failed attempt
#define MQTT_RECONNECT_DELAY_MAX 120000 // Set reconnect time to 2 minutes at most
-#define MQTT_MAX_TOPIC_SIZE 50 // max length of MQTT topic
#define MQTT_TOPIC_START "start"
#define MQTT_TOPIC_HEARTBEAT "heartbeat"
#define MQTT_TOPIC_START_PAYLOAD "start"
#define MQTT_TOPIC_RESTART "restart"
+#define MQTT_WILL_ONLINE_PAYLOAD "online" // for last will & testament payload
+#define MQTT_WILL_OFFLINE_PAYLOAD "offline" // for last will & testament payload
+#define MQTT_BASE_DEFAULT "home" // default MQTT prefix to topics
+#define MQTT_RETAIN false
+#define MQTT_KEEPALIVE 60 // 1 minute
+#define MQTT_QOS 1
+#define MQTT_WILL_TOPIC "status" // for last will & testament topic name
+#define MQTT_MAX_TOPIC_SIZE 50 // max length of MQTT topic
+#define MQTT_MAX_PAYLOAD_SIZE 500 // max size of a JSON object. See https://arduinojson.org/v6/assistant/
+#define MQTT_MAX_PAYLOAD_SIZE_LARGE 2000 // max size of a large JSON object, like for sending MQTT log
+#define MYESP_JSON_MAXSIZE 2000 // for large Dynamic json files
+#define MYESP_MQTTLOG_MAX 20 // max number of log entries for MQTT publishes
+#define MYESP_JSON_LOG_MAXSIZE 300 // max size of an JSON log entry
// Internal MQTT events
#define MQTT_CONNECT_EVENT 0
@@ -94,18 +126,20 @@ extern struct rst_info resetInfo;
// reset reason codes
PROGMEM const char custom_reset_hardware[] = "Hardware button";
-PROGMEM const char custom_reset_terminal[] = "Reboot from terminal";
-PROGMEM const char custom_reset_mqtt[] = "Reboot from MQTT";
-PROGMEM const char custom_reset_ota[] = "Reboot after successful OTA update";
-PROGMEM const char * const custom_reset_string[] = {custom_reset_hardware, custom_reset_terminal, custom_reset_mqtt, custom_reset_ota};
+PROGMEM const char custom_reset_terminal[] = "Restart from terminal";
+PROGMEM const char custom_reset_mqtt[] = "Restart from MQTT";
+PROGMEM const char custom_reset_ota[] = "Restart after successful OTA update";
+PROGMEM const char custom_reset_factory[] = "Factory reset";
+PROGMEM const char * const custom_reset_string[] = {custom_reset_hardware, custom_reset_terminal, custom_reset_mqtt, custom_reset_ota, custom_reset_factory};
#define CUSTOM_RESET_HARDWARE 1 // Reset from hardware button
#define CUSTOM_RESET_TERMINAL 2 // Reset from terminal
#define CUSTOM_RESET_MQTT 3 // Reset via MQTT
#define CUSTOM_RESET_OTA 4 // Reset after successful OTA update
-#define CUSTOM_RESET_MAX 4
+#define CUSTOM_RESET_FACTORY 5 // Factory reset
+#define CUSTOM_RESET_MAX 5
// SPIFFS
-#define SPIFFS_MAXSIZE 800 // https://arduinojson.org/v6/assistant/
+#define MYESP_SPIFFS_MAXSIZE 800 // https://arduinojson.org/v6/assistant/
// CRASH
/**
@@ -157,9 +191,9 @@ struct RtcmemData {
static_assert(sizeof(RtcmemData) <= (RTCMEM_BLOCKS * 4u), "RTCMEM struct is too big");
-#define SYSTEM_CHECK_TIME 60000 // The system is considered stable after these many millis (1 minute)
-#define SYSTEM_CHECK_MAX 5 // After this many crashes on boot
-#define HEARTBEAT_INTERVAL 120000 // in milliseconds, how often the MQTT heartbeat is sent (2 mins)
+#define MYESP_SYSTEM_CHECK_TIME 60000 // The system is considered stable after these many millis (1 minute)
+#define MYESP_SYSTEM_CHECK_MAX 10 // After this many crashes on boot
+#define MYESP_HEARTBEAT_INTERVAL 120000 // in milliseconds, how often the MQTT heartbeat is sent (2 mins)
typedef struct {
bool set; // is it a set command
@@ -176,14 +210,21 @@ typedef enum {
MYESP_BOOTSTATUS_RESETNEEDED = 3
} MYESP_BOOTSTATUS; // boot messages
+// for storing all MQTT publish messages
+typedef struct {
+ char * topic;
+ char * payload;
+ time_t timestamp;
+} _MQTT_Log;
+
typedef std::function mqtt_callback_f;
typedef std::function wifi_callback_f;
typedef std::function ota_callback_f;
typedef std::function telnetcommand_callback_f;
typedef std::function telnet_callback_f;
-typedef std::function fs_callback_f;
-typedef std::function fs_settings_callback_f;
-typedef std::function web_callback_f;
+typedef std::function fs_loadsave_callback_f;
+typedef std::function fs_setlist_callback_f;
+typedef std::function web_callback_f;
// calculates size of an 2d array at compile time
template
@@ -191,58 +232,34 @@ constexpr size_t ArraySize(T (&)[N]) {
return N;
}
-template
-void PROGMEM_readAnything(const T * sce, T & dest) {
- memcpy_P(&dest, sce, sizeof(T));
-}
-
-#define UPTIME_OVERFLOW 4294967295 // Uptime overflow value
+#define MYESP_UPTIME_OVERFLOW 4294967295 // Uptime overflow value
// web min and max length of wifi ssid and password
-#define MAX_SSID_LEN 32
-#define MAX_PWD_LEN 64
+#define MYESP_MAX_STR_LEN 16
#define MYESP_BOOTUP_FLASHDELAY 50 // flash duration for LED at bootup sequence
#define MYESP_BOOTUP_DELAY 2000 // time before we open the window to reset. This is to stop resetting values when uploading firmware via USB
-// max size of char buffer for storing web page
-#define MYESP_MAXCHARBUFFER 800
-
-// Holds the admin webpage in the program memory
-const char webCommonPage_start[] = ""
- ""
- "";
-
-const char webCommonPage_start_body[] = "";
-
-const char webCommonPage_end[] = "";
-
-const char webResetPage_form[] = "";
-
-const char webResetPage_post[] =
- "
New wifi credentials set. System is now rebooting. Please wait a few seconds and then reconnect via telnet or browser to its new IP given address.
";
-
-const char webResetAllPage_form[] = "";
-
// class definition
class MyESP {
+ protected:
+ // webserver
+ AsyncWebServer * _webServer;
+ AsyncWebSocket * _ws;
+
+ // NTP
+ NtpClient NTP;
+
public:
MyESP();
~MyESP();
- ESP8266WebServer webServer; // Web server on port 80
+ // write event called from within lambda classs
+ static void _writeEvent(const char * type, const char * src, const char * desc, const char * data);
// wifi
void setWIFICallback(void (*callback)());
- void setWIFI(const char * wifi_ssid, const char * wifi_password, wifi_callback_f callback);
+ void setWIFI(wifi_callback_f callback);
bool isWifiConnected();
bool isAPmode();
@@ -251,17 +268,7 @@ class MyESP {
void mqttSubscribe(const char * topic);
void mqttUnsubscribe(const char * topic);
void mqttPublish(const char * topic, const char * payload);
- void setMQTT(const char * mqtt_host,
- const char * mqtt_username,
- const char * mqtt_password,
- const char * mqtt_base,
- unsigned long mqtt_keepalive,
- unsigned char mqtt_qos,
- bool mqtt_retain,
- const char * mqtt_will_topic,
- const char * mqtt_will_online_payload,
- const char * mqtt_will_offline_payload,
- mqtt_callback_f callback);
+ void setMQTT(mqtt_callback_f callback);
// OTA
void setOTA(ota_callback_f OTACallback_pre, ota_callback_f OTACallback_post);
@@ -274,8 +281,9 @@ class MyESP {
void setUseSerial(bool toggle);
// FS
- void setSettings(fs_callback_f callback, fs_settings_callback_f fs_settings_callback);
- bool fs_saveConfig();
+ void setSettings(fs_loadsave_callback_f loadsave, fs_setlist_callback_f setlist, bool useSerial = true);
+ bool fs_saveConfig(JsonObject root);
+ bool fs_saveCustomConfig(JsonObject root);
// Web
void setWeb(web_callback_f callback_web);
@@ -289,8 +297,7 @@ class MyESP {
// general
void end();
void loop();
- void begin(const char * app_hostname, const char * app_name, const char * app_version);
- void setBoottime(const char * boottime);
+ void begin(const char * app_hostname, const char * app_name, const char * app_version, const char * app_url, const char * app_updateurl);
void resetESP();
int getWifiQuality();
void showSystemStats();
@@ -298,48 +305,56 @@ class MyESP {
uint32_t getSystemLoadAverage();
uint32_t getSystemResetReason();
uint8_t getSystemBootStatus();
+ bool _have_ntp_time;
private:
// mqtt
- AsyncMqttClient mqttClient;
- unsigned long _mqtt_reconnect_delay;
- void _mqttOnMessage(char * topic, char * payload, size_t len);
- void _mqttConnect();
- void _mqtt_setup();
- mqtt_callback_f _mqtt_callback;
- void _mqttOnConnect();
- void _sendStart();
- char * _mqttTopic(const char * topic);
- char * _mqtt_host;
- char * _mqtt_username;
+ void _mqttOnMessage(char * topic, char * payload, size_t len);
+ void _mqttConnect();
+ void _mqtt_setup();
+ void _mqttOnConnect();
+ void _sendStart();
+ char * _mqttTopic(const char * topic);
+
+ // mqtt log
+ _MQTT_Log MQTT_log[MYESP_MQTTLOG_MAX]; // log for publish messages
+ void _printMQTTLog();
+ void _addMQTTLog(const char * topic, const char * payload);
+
+ AsyncMqttClient mqttClient; // the MQTT class
+ uint32_t _mqtt_reconnect_delay;
+ mqtt_callback_f _mqtt_callback_f;
+ char * _mqtt_ip;
+ char * _mqtt_user;
char * _mqtt_password;
+ int _mqtt_port;
char * _mqtt_base;
- unsigned long _mqtt_keepalive;
- unsigned char _mqtt_qos;
+ bool _mqtt_enabled;
+ uint32_t _mqtt_keepalive;
+ uint8_t _mqtt_qos;
bool _mqtt_retain;
char * _mqtt_will_topic;
char * _mqtt_will_online_payload;
char * _mqtt_will_offline_payload;
- char * _mqtt_topic;
- unsigned long _mqtt_last_connection;
+ uint32_t _mqtt_last_connection;
bool _mqtt_connecting;
- bool _rtcmem_status;
+ bool _mqtt_heartbeat;
// wifi
void _wifiCallback(justwifi_messages_t code, char * parameter);
void _wifi_setup();
- wifi_callback_f _wifi_callback;
- char * _wifi_ssid;
- char * _wifi_password;
+ wifi_callback_f _wifi_callback_f;
+ char * _network_ssid;
+ char * _network_password;
+ uint8_t _network_wmode;
bool _wifi_connected;
String _getESPhostname();
// ota
- ota_callback_f _ota_pre_callback;
- ota_callback_f _ota_post_callback;
+ ota_callback_f _ota_pre_callback_f;
+ ota_callback_f _ota_post_callback_f;
void _ota_setup();
void _OTACallback();
- bool _ota_doing_update;
// crash
void _eeprom_setup();
@@ -354,37 +369,42 @@ class MyESP {
void _telnet_setup();
char _command[TELNET_MAX_COMMAND_LENGTH]; // the input command from either Serial or Telnet
void _consoleShowHelp();
- telnetcommand_callback_f _telnetcommand_callback; // Callable for projects commands
- telnet_callback_f _telnet_callback; // callback for connect/disconnect
+ telnetcommand_callback_f _telnetcommand_callback_f; // Callable for projects commands
+ telnet_callback_f _telnet_callback_f; // callback for connect/disconnect
bool _changeSetting(uint8_t wc, const char * setting, const char * value);
- // fs
- void _fs_setup();
- bool _fs_loadConfig();
- void _fs_printConfig();
- void _fs_eraseConfig();
+ // fs and settings
+ void _fs_setup();
+ bool _fs_loadConfig();
+ bool _fs_loadCustomConfig();
+ void _fs_printFile(const char * file);
+ void _fs_eraseConfig();
+ bool _fs_writeConfig();
+ bool _fs_createCustomConfig();
+ bool _fs_sendConfig();
+ fs_loadsave_callback_f _fs_loadsave_callback_f;
+ fs_setlist_callback_f _fs_setlist_callback_f;
- // settings
- fs_callback_f _fs_callback;
- fs_settings_callback_f _fs_settings_callback;
- void _printSetCommands();
-
- // web
- web_callback_f _web_callback;
+ void _printSetCommands();
// general
- char * _app_hostname;
+ char * _general_hostname;
char * _app_name;
char * _app_version;
- char * _boottime;
+ char * _app_url;
+ char * _app_updateurl;
bool _suspendOutput;
- bool _serial;
- bool _heartbeat;
+ bool _general_serial;
unsigned long _getUptime();
- String _buildTime();
- bool _firstInstall;
+ char * _getBuildTime();
+ char * _buildTime;
+ bool _timerequest;
+ bool _formatreq;
+ bool _hasValue(char * s);
+ void _printHeap(const char * s);
// reset reason and rtcmem
+ bool _rtcmem_status;
bool _rtcmemStatus();
bool _getRtcmemStatus();
@@ -418,11 +438,29 @@ class MyESP {
// heartbeat
void _heartbeatCheck(bool force);
- // webserver
+ // web
+ web_callback_f _web_callback_f;
+ const char * _http_username;
+
+ // log
+ void _sendEventLog(uint8_t page);
+
+ // web
+ void _onWsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t * data, size_t len);
+ void _procMsg(AsyncWebSocketClient * client, size_t sz);
+ void _sendStatus();
+ void _sendCustomStatus();
+ void _printScanResult(int networksFound);
+ void _sendTime();
void _webserver_setup();
void _webRootPage();
void _webResetPage();
void _webResetAllPage();
+
+ // ntp
+ char * _ntp_server;
+ uint8_t _ntp_interval;
+ bool _ntp_enabled;
};
extern MyESP myESP;
diff --git a/src/Ntp.cpp b/src/Ntp.cpp
new file mode 100644
index 000000000..679bbae7b
--- /dev/null
+++ b/src/Ntp.cpp
@@ -0,0 +1,46 @@
+/*
+ * Ntp.cpp
+ */
+
+#include "Ntp.h"
+
+char * NtpClient::TimeServerName;
+time_t NtpClient::syncInterval;
+IPAddress NtpClient::timeServer;
+AsyncUDP NtpClient::udpListener;
+byte NtpClient::NTPpacket[NTP_PACKET_SIZE];
+
+void ICACHE_FLASH_ATTR NtpClient::Ntp(const char * server, time_t syncMins) {
+ TimeServerName = strdup(server);
+ syncInterval = syncMins * 60; // convert to seconds
+ WiFi.hostByName(TimeServerName, timeServer);
+ setSyncProvider(getNtpTime);
+ setSyncInterval(syncInterval);
+}
+
+ICACHE_FLASH_ATTR NtpClient::~NtpClient() {
+ udpListener.close();
+}
+
+// send an NTP request to the time server at the given address
+time_t ICACHE_FLASH_ATTR NtpClient::getNtpTime() {
+ memset(NTPpacket, 0, sizeof(NTPpacket));
+ NTPpacket[0] = 0b11100011;
+ NTPpacket[1] = 0;
+ NTPpacket[2] = 6;
+ NTPpacket[3] = 0xEC;
+ NTPpacket[12] = 49;
+ NTPpacket[13] = 0x4E;
+ NTPpacket[14] = 49;
+ NTPpacket[15] = 52;
+ if (udpListener.connect(timeServer, 123)) {
+ udpListener.onPacket([](AsyncUDPPacket packet) {
+ unsigned long highWord = word(packet.data()[40], packet.data()[41]);
+ unsigned long lowWord = word(packet.data()[42], packet.data()[43]);
+ time_t UnixUTCtime = (highWord << 16 | lowWord) - 2208988800UL;
+ setTime(UnixUTCtime);
+ });
+ }
+ udpListener.write(NTPpacket, sizeof(NTPpacket));
+ return 0;
+}
diff --git a/src/Ntp.h b/src/Ntp.h
new file mode 100644
index 000000000..7eca60b9e
--- /dev/null
+++ b/src/Ntp.h
@@ -0,0 +1,33 @@
+/*
+ * Ntp.h
+ *
+ */
+#pragma once
+
+#ifndef NTP_H_
+#define NTP_H_
+
+#include
+#include
+
+#include "TimeLib.h" // customized version of the time library
+
+#define NTP_PACKET_SIZE 48 // NTP time is in the first 48 bytes of message
+
+class NtpClient {
+ public:
+ void ICACHE_FLASH_ATTR Ntp(const char * server, time_t syncMins);
+ ICACHE_FLASH_ATTR virtual ~NtpClient();
+
+ static char * TimeServerName;
+ static IPAddress timeServer;
+ static time_t syncInterval;
+
+ static AsyncUDP udpListener;
+
+ static byte NTPpacket[NTP_PACKET_SIZE];
+
+ static ICACHE_FLASH_ATTR time_t getNtpTime();
+};
+
+#endif
diff --git a/lib/TelnetSpy/TelnetSpy.cpp b/src/TelnetSpy.cpp
similarity index 99%
rename from lib/TelnetSpy/TelnetSpy.cpp
rename to src/TelnetSpy.cpp
index 85716a0d8..6a44f83c0 100644
--- a/lib/TelnetSpy/TelnetSpy.cpp
+++ b/src/TelnetSpy.cpp
@@ -303,7 +303,7 @@ int TelnetSpy::available(void) {
}
int TelnetSpy::read(void) {
- int val;
+ int val = 0;
if (usedSer) {
val = usedSer->read();
if (val != -1) {
@@ -319,7 +319,7 @@ int TelnetSpy::read(void) {
}
int TelnetSpy::peek(void) {
- int val;
+ int val = 0;
if (usedSer) {
val = usedSer->peek();
if (val != -1) {
diff --git a/lib/TelnetSpy/TelnetSpy.h b/src/TelnetSpy.h
similarity index 100%
rename from lib/TelnetSpy/TelnetSpy.h
rename to src/TelnetSpy.h
diff --git a/src/TimeLib.cpp b/src/TimeLib.cpp
new file mode 100644
index 000000000..e56de4097
--- /dev/null
+++ b/src/TimeLib.cpp
@@ -0,0 +1,143 @@
+#include "TimeLib.h"
+
+static tmElements_t tm; // a cache of time elements
+static time_t cacheTime; // the time the cache was updated
+static uint32_t syncInterval = 300; // time sync will be attempted after this many seconds
+static uint32_t sysTime = 0;
+static uint32_t prevMillis = 0;
+static uint32_t nextSyncTime = 0;
+static timeStatus_t Status = timeNotSet;
+getExternalTime getTimePtr; // pointer to external sync function
+
+#define LEAP_YEAR(Y) (((1970 + (Y)) > 0) && !((1970 + (Y)) % 4) && (((1970 + (Y)) % 100) || !((1970 + (Y)) % 400)))
+static const uint8_t monthDays[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; // API starts months from 1, this array starts from 0
+
+time_t now() {
+ // calculate number of seconds passed since last call to now()
+ while (millis() - prevMillis >= 1000) {
+ // millis() and prevMillis are both unsigned ints thus the subtraction will always be the absolute value of the difference
+ sysTime++;
+ prevMillis += 1000;
+ }
+ if (nextSyncTime <= sysTime) {
+ if (getTimePtr != 0) {
+ time_t t = getTimePtr();
+ if (t != 0) {
+ setTime(t);
+ } else {
+ nextSyncTime = sysTime + syncInterval;
+ Status = (Status == timeNotSet) ? timeNotSet : timeNeedsSync;
+ }
+ }
+ }
+ return (time_t)sysTime;
+}
+
+void setSyncProvider(getExternalTime getTimeFunction) {
+ getTimePtr = getTimeFunction;
+ nextSyncTime = sysTime;
+ now(); // this will sync the clock
+}
+
+void setSyncInterval(time_t interval) { // set the number of seconds between re-sync
+ syncInterval = (uint32_t)interval;
+ nextSyncTime = sysTime + syncInterval;
+}
+
+void breakTime(time_t timeInput, tmElements_t & tm) {
+ // break the given time_t into time components
+ // this is a more compact version of the C library localtime function
+ // note that year is offset from 1970 !!!
+
+ uint8_t year;
+ uint8_t month, monthLength;
+ uint32_t time;
+ unsigned long days;
+
+ time = (uint32_t)timeInput;
+ tm.Second = time % 60;
+ time /= 60; // now it is minutes
+ tm.Minute = time % 60;
+ time /= 60; // now it is hours
+ tm.Hour = time % 24;
+ time /= 24; // now it is days
+ tm.Wday = ((time + 4) % 7) + 1; // Sunday is day 1
+
+ year = 0;
+ days = 0;
+ while ((unsigned)(days += (LEAP_YEAR(year) ? 366 : 365)) <= time) {
+ year++;
+ }
+ tm.Year = year; // year is offset from 1970
+
+ days -= LEAP_YEAR(year) ? 366 : 365;
+ time -= days; // now it is days in this year, starting at 0
+
+ days = 0;
+ month = 0;
+ monthLength = 0;
+ for (month = 0; month < 12; month++) {
+ if (month == 1) { // february
+ if (LEAP_YEAR(year)) {
+ monthLength = 29;
+ } else {
+ monthLength = 28;
+ }
+ } else {
+ monthLength = monthDays[month];
+ }
+
+ if (time >= monthLength) {
+ time -= monthLength;
+ } else {
+ break;
+ }
+ }
+ tm.Month = month + 1; // jan is month 1
+ tm.Day = time + 1; // day of month
+}
+
+void refreshCache(time_t t) {
+ if (t != cacheTime) {
+ breakTime(t, tm);
+ cacheTime = t;
+ }
+}
+
+int day(time_t t) { // the day for the given time (0-6)
+ refreshCache(t);
+ return tm.Day;
+}
+
+int month(time_t t) { // the month for the given time
+ refreshCache(t);
+ return tm.Month;
+}
+
+int second(time_t t) { // the second for the given time
+ refreshCache(t);
+ return tm.Second;
+}
+
+int minute(time_t t) { // the minute for the given time
+ refreshCache(t);
+ return tm.Minute;
+}
+
+int hour(time_t t) { // the hour for the given time
+ refreshCache(t);
+ return tm.Hour;
+}
+
+int year(time_t t) { // the year for the given time
+ refreshCache(t);
+ return tmYearToCalendar(tm.Year);
+}
+
+void setTime(time_t t) {
+ sysTime = (uint32_t)t;
+ nextSyncTime = (uint32_t)t + syncInterval;
+ Status = timeSet;
+ prevMillis = millis(); // restart counting from now (thanks to Korman for this fix)
+}
+
diff --git a/src/TimeLib.h b/src/TimeLib.h
new file mode 100644
index 000000000..a827e38f0
--- /dev/null
+++ b/src/TimeLib.h
@@ -0,0 +1,49 @@
+#ifndef _Time_h
+#define _Time_h
+
+#include
+
+#define SECS_PER_MIN ((time_t)(60UL))
+#define SECS_PER_HOUR ((time_t)(3600UL))
+#define SECS_PER_DAY ((time_t)(SECS_PER_HOUR * 24UL))
+#define tmYearToCalendar(Y) ((Y) + 1970) // full four digit year
+
+// This ugly hack allows us to define C++ overloaded functions, when included
+// from within an extern "C", as newlib's sys/stat.h does. Actually it is
+// intended to include "time.h" from the C library (on ARM, but AVR does not
+// have that file at all). On Mac and Windows, the compiler will find this
+// "Time.h" instead of the C library "time.h", so we may cause other weird
+// and unpredictable effects by conflicting with the C library header "time.h",
+// but at least this hack lets us define C++ functions as intended. Hopefully
+// nothing too terrible will result from overriding the C library header?!
+extern "C++" {
+typedef enum { timeNotSet, timeNeedsSync, timeSet } timeStatus_t;
+
+typedef struct {
+ uint8_t Second;
+ uint8_t Minute;
+ uint8_t Hour;
+ uint8_t Wday; // day of week, sunday is day 1
+ uint8_t Day;
+ uint8_t Month;
+ uint8_t Year; // offset from 1970;
+} tmElements_t, TimeElements, *tmElementsPtr_t;
+
+typedef time_t (*getExternalTime)();
+
+time_t now(); // return the current time as seconds since Jan 1 1970
+void setTime(time_t t);
+timeStatus_t timeStatus(); // indicates if time has been set and recently synchronized
+void setSyncProvider(getExternalTime getTimeFunction); // identify the external time provider
+void setSyncInterval(time_t interval); // set the number of seconds between re-sync
+time_t makeTime(const tmElements_t & tm); // convert time elements into time_t
+
+int hour(time_t t); // the hour for the given time
+int minute(time_t t); // the minute for the given time
+int second(time_t t); // the second for the given time
+int day(time_t t); // the day for the given time
+int month(time_t t); // the month for the given time
+int weekday(time_t t); // the weekday for the given time
+int year(time_t t); // the year for the given time
+}
+#endif
diff --git a/src/custom.htm b/src/custom.htm
new file mode 100644
index 000000000..01ddd4aed
--- /dev/null
+++ b/src/custom.htm
@@ -0,0 +1,275 @@
+
+
+
+
Please refer to the Help for configuration options
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Note: any setting marked with a requires a system restart after saving.
+
+
+
+
+
+
+
+
EMS Dashboard
+
Real-time values from the EMS-ESP device are shown here
+
+
+
+
+
EMS Bus Status
+
+
+
+
+
+
+
+
+
Discovered Devices:
+
+
+
+
+
+
+
+
+
+
Boiler:
+
+
+
Hot Tap Water:
+
+
Central Heating:
+
+
+
+
Selected Flow Temperature:
+
+
Current Flow Temperature:
+
+
+
+
Boiler Temperature:
+
+
Return Temperature:
+
+
+
+
+
+
+
Thermostat:
+
+
+
Setpoint Temperature:
+
+
Current Temperature:
+
+
+
+
Mode:
+
+
+
+
+
+
+
Solar Module:
+
+
+
Colector Temperature:
+
+
Bottom Temperature:
+
+
+
+
Pump Modulation:
+
+
Pump Active:
+
+
+
+
Energy Last Hour:
+
+
Energy Today:
+
+
Energy Total:
+
+
+
+
+
+
+
Heat Pump:
+
+
+
Pump Modulation:
+
+
Pump Speed:
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/custom.js b/src/custom.js
new file mode 100644
index 000000000..0c1bd2e8f
--- /dev/null
+++ b/src/custom.js
@@ -0,0 +1,169 @@
+var custom_config = {
+ "command": "custom_configfile",
+ "settings": {
+ "led": true,
+ "led_gpio": 2,
+ "dallas_gpio": 14,
+ "dallas_parasite": false,
+ "listen_mode": false,
+ "shower_timer": false,
+ "shower_alert": false,
+ "publish_time": 120,
+ "heating_circuit": 1
+ }
+}
+
+function custom_commit() {
+ websock.send(JSON.stringify(custom_config));
+}
+
+function listcustom() {
+
+ document.getElementById("led_gpio").value = custom_config.settings.led_gpio;
+ document.getElementById("dallas_gpio").value = custom_config.settings.dallas_gpio;
+ document.getElementById("publish_time").value = custom_config.settings.publish_time;
+ document.getElementById("heating_circuit").value = custom_config.settings.heating_circuit;
+ document.getElementById("tx_mode").value = custom_config.settings.tx_mode;
+
+ if (custom_config.settings.led) {
+ $("input[name=\"led\"][value=\"1\"]").prop("checked", true);
+ }
+ if (custom_config.settings.dallas_parasite) {
+ $("input[name=\"dallas_parasite\"][value=\"1\"]").prop("checked", true);
+ }
+ if (custom_config.settings.listen_mode) {
+ $("input[name=\"listen_mode\"][value=\"1\"]").prop("checked", true);
+ }
+ if (custom_config.settings.shower_timer) {
+ $("input[name=\"shower_timer\"][value=\"1\"]").prop("checked", true);
+ }
+ if (custom_config.settings.shower_alert) {
+ $("input[name=\"shower_alert\"][value=\"1\"]").prop("checked", true);
+ }
+}
+
+function savecustom() {
+ custom_config.settings.led_gpio = parseInt(document.getElementById("led_gpio").value);
+ custom_config.settings.dallas_gpio = parseInt(document.getElementById("dallas_gpio").value);
+
+ custom_config.settings.dallas_parasite = false;
+ if (parseInt($("input[name=\"dallas_parasite\"]:checked").val()) === 1) {
+ custom_config.settings.dallas_parasite = true;
+ }
+
+ custom_config.settings.listen_mode = false;
+ if (parseInt($("input[name=\"listen_mode\"]:checked").val()) === 1) {
+ custom_config.settings.listen_mode = true;
+ }
+
+ custom_config.settings.shower_timer = false;
+ if (parseInt($("input[name=\"shower_timer\"]:checked").val()) === 1) {
+ custom_config.settings.shower_timer = true;
+ }
+
+ custom_config.settings.shower_alert = false;
+ if (parseInt($("input[name=\"shower_alert\"]:checked").val()) === 1) {
+ custom_config.settings.shower_alert = true;
+ }
+
+ custom_config.settings.led = false;
+ if (parseInt($("input[name=\"led\"]:checked").val()) === 1) {
+ custom_config.settings.led = true;
+ }
+
+ custom_config.settings.publish_time = parseInt(document.getElementById("publish_time").value);
+ custom_config.settings.heating_circuit = parseInt(document.getElementById("heating_circuit").value);
+ custom_config.settings.tx_mode = parseInt(document.getElementById("tx_mode").value);
+
+ custom_uncommited();
+}
+
+function listCustomStats() {
+ document.getElementById("msg").innerHTML = ajaxobj.emsbus.msg;
+ if (ajaxobj.emsbus.ok) {
+ document.getElementById("msg").className = "alert alert-success";
+ } else {
+ document.getElementById("msg").className = "alert alert-danger";
+ document.getElementById("devicesshow").style.display = "none";
+ document.getElementById("thermostat_show").style.display = "none";
+ document.getElementById("boiler_show").style.display = "none";
+ return;
+ }
+
+ var list = document.getElementById("devices");
+ var obj = ajaxobj.emsbus.devices;
+
+ document.getElementById("devicesshow").style.display = "block";
+
+ for (var i = 0; i < obj.length; i++) {
+ var l = document.createElement("li");
+ var type = obj[i].type;
+ if (type == 1) {
+ var color = "list-group-item-success";
+ } else if (type == 2) {
+ var color = "list-group-item-info";
+ } else if (type == 3) {
+ var color = "list-group-item-warning";
+ } else if (type == 4) {
+ var color = "list-group-item-success";
+ } else {
+ var color = "";
+ }
+ l.innerHTML = obj[i].model + " (Version:" + obj[i].version + " ProductID:" + obj[i].productid + " DeviceID:0x" + obj[i].deviceid + ")";
+ l.className = "list-group-item " + color;
+ list.appendChild(l);
+ }
+
+ if (ajaxobj.boiler.ok) {
+ document.getElementById("boiler_show").style.display = "block";
+
+ document.getElementById("bm").innerHTML = ajaxobj.boiler.bm;
+ document.getElementById("b1").innerHTML = ajaxobj.boiler.b1;
+ document.getElementById("b2").innerHTML = ajaxobj.boiler.b2;
+ document.getElementById("b3").innerHTML = ajaxobj.boiler.b3 + " ℃";
+ document.getElementById("b4").innerHTML = ajaxobj.boiler.b4 + " ℃";
+ document.getElementById("b5").innerHTML = ajaxobj.boiler.b5 + " ℃";
+ document.getElementById("b6").innerHTML = ajaxobj.boiler.b6 + " ℃";
+ } else {
+ document.getElementById("boiler_show").style.display = "none";
+ }
+
+ if (ajaxobj.thermostat.ok) {
+ document.getElementById("thermostat_show").style.display = "block";
+
+ document.getElementById("tm").innerHTML = ajaxobj.thermostat.tm;
+ document.getElementById("ts").innerHTML = ajaxobj.thermostat.ts + " ℃";
+ document.getElementById("tc").innerHTML = ajaxobj.thermostat.tc + " ℃";
+ document.getElementById("tmode").innerHTML = ajaxobj.thermostat.tmode;
+ } else {
+ document.getElementById("thermostat_show").style.display = "none";
+ }
+
+ if (ajaxobj.sm.ok) {
+ document.getElementById("sm_show").style.display = "block";
+
+ document.getElementById("sm").innerHTML = ajaxobj.sm.sm;
+ document.getElementById("sm1").innerHTML = ajaxobj.sm.sm1 + " ℃";
+ document.getElementById("sm2").innerHTML = ajaxobj.sm.sm2 + " ℃";
+ document.getElementById("sm3").innerHTML = ajaxobj.sm.sm3 + " %";
+ document.getElementById("sm4").innerHTML = ajaxobj.sm.sm4;
+ document.getElementById("sm5").innerHTML = ajaxobj.sm.sm5 + " Wh";
+ document.getElementById("sm6").innerHTML = ajaxobj.sm.sm6 + " Wh";
+ document.getElementById("sm7").innerHTML = ajaxobj.sm.sm7 + " KWh";
+ } else {
+ document.getElementById("sm_show").style.display = "none";
+ }
+
+ if (ajaxobj.hp.ok) {
+ document.getElementById("hp_show").style.display = "block";
+
+ document.getElementById("hm").innerHTML = ajaxobj.hp.hm;
+ document.getElementById("hp1").innerHTML = ajaxobj.hp.hp1 + " %";
+ document.getElementById("hp2").innerHTML = ajaxobj.hp.hp2 + " %";
+ } else {
+ document.getElementById("hp_show").style.display = "none";
+ }
+
+
+}
+
diff --git a/src/ems-esp.cpp b/src/ems-esp.cpp
index adc3aca2e..5c46350c7 100644
--- a/src/ems-esp.cpp
+++ b/src/ems-esp.cpp
@@ -4,11 +4,11 @@
* Paul Derbyshire - https://github.com/proddy/EMS-ESP
*
* See ChangeLog.md for history
- * See README.md for Acknowledgments
+ * See wiki at https://github.com/proddy/EMS-ESP/Wiki for Acknowledgments
*/
// local libraries
-#include "ds18.h"
+#include "MyESP.h"
#include "ems.h"
#include "ems_devices.h"
#include "emsuart.h"
@@ -16,11 +16,9 @@
#include "version.h"
// Dallas external temp sensors
+#include "ds18.h"
DS18 ds18;
-// shared libraries
-#include
-
// public libraries
#include // https://github.com/bblanchon/ArduinoJson
#include // https://github.com/bakercp/CRC32
@@ -28,11 +26,18 @@ DS18 ds18;
// standard arduino libs
#include // https://github.com/esp8266/Arduino/tree/master/libraries/Ticker
+// default APP params
+#define APP_NAME "EMS-ESP"
+#define APP_HOSTNAME "ems-esp"
+#define APP_URL "https://github.com/proddy/EMS-ESP"
+#define APP_UPDATEURL "https://api.github.com/repos/proddy/EMS-ESP/releases/latest"
+
+// macros for easy debugging
#define myDebug(...) myESP.myDebug(__VA_ARGS__)
#define myDebug_P(...) myESP.myDebug_P(__VA_ARGS__)
// set to value >0 if the ESP is overheating or there are timing issues. Recommend a value of 1.
-#define EMSESP_DELAY 1 // initially set to 0 for no delay
+#define EMSESP_DELAY 0 // initially set to 0 for no delay. Change to 1 if getting WDT resets from wifi
#define DEFAULT_HEATINGCIRCUIT 1 // default to HC1 for thermostats that support multiple heating circuits like the RC35
@@ -69,6 +74,21 @@ Ticker showerColdShotStopTimer;
#define SHOWER_COLDSHOT_DURATION 10 // in seconds. 10 seconds for cold water before turning back hot water
#define SHOWER_MAX_DURATION 420000 // in ms. 7 minutes, before trigger a shot of cold water
+#ifdef LOGICANALYZER
+#define EMSESP_DALLAS_GPIO D1
+#define EMSESP_DALLAS_PARASITE false
+#else
+// set this if using an external temperature sensor like a DS18B20
+// D5 is the default on a bbqkees board
+#define EMSESP_DALLAS_GPIO D5
+#define EMSESP_DALLAS_PARASITE false
+#endif
+
+// Set LED pin used for showing the EMS bus connection status. Solid means EMS bus working, flashing is an error
+// can be either the onboard LED on the ESP8266 (LED_BULLETIN) or external via an external pull-up LED (e.g. D1 on a bbqkees' board)
+// can be enabled and disabled via the 'set led' command and pin set by 'set led_gpio'
+#define EMSESP_LED_GPIO LED_BUILTIN
+
typedef struct {
uint32_t timestamp; // for internal timings, via millis()
uint8_t dallas_sensors; // count of dallas sensors
@@ -83,6 +103,7 @@ typedef struct {
uint8_t dallas_gpio; // pin for attaching external dallas temperature sensors
bool dallas_parasite; // on/off is using parasite
uint8_t heating_circuit; // number of heating circuit, 1 or 2
+ uint8_t tx_mode; // TX mode 1,2 or 3
} _EMSESP_Status;
typedef struct {
@@ -98,15 +119,13 @@ static const command_t project_cmds[] PROGMEM = {
{true, "led ", "toggle status LED on/off"},
{true, "led_gpio ", "set the LED pin. Default is the onboard LED 2. For external D1 use 5"},
{true, "dallas_gpio ", "set the external Dallas temperature sensors pin. Default is 14 for D5"},
- {true, "dallas_parasite ", "set to on if powering Dallas sesnsors via parasite power"},
- {true, "thermostat_type ", "set the thermostat type ID (e.g. 10 for 0x10)"},
- {true, "boiler_type ", "set the boiler type ID (e.g. 8 for 0x08)"},
+ {true, "dallas_parasite ", "set to on if powering Dallas sensors via parasite power"},
{true, "listen_mode ", "when set to on all automatic Tx are disabled"},
{true, "shower_timer ", "send MQTT notification on all shower durations"},
{true, "shower_alert ", "stop hot water to send 3 cold burst warnings after max shower time is exceeded"},
{true, "publish_time ", "set frequency for publishing data to MQTT (0=off)"},
{true, "heating_circuit <1 | 2>", "set the main thermostat HC to work with (if using multiple heating circuits)"},
- {true, "tx_mode ", "changes Tx logic. 0=ems 1.0, 1=ems+, 2=generic (experimental!), 3=HT3"},
+ {true, "tx_mode ", "changes Tx logic. 1=ems generic, 2=ems+, 3=Junkers HT3"},
{false, "info", "show current captured on the devices"},
{false, "log ", "set logging mode to none, basic, thermostat only, raw or verbose"},
@@ -419,6 +438,8 @@ void showInfo() {
myDebug_P(PSTR(" System logging set to Thermostat only"));
} else if (sysLog == EMS_SYS_LOGGING_SOLARMODULE) {
myDebug_P(PSTR(" System logging set to Solar Module only"));
+ } else if (sysLog == EMS_SYS_LOGGING_JABBER) {
+ myDebug_P(PSTR(" System logging set to Jabber"));
} else {
myDebug_P(PSTR(" System logging set to None"));
}
@@ -509,7 +530,7 @@ void showInfo() {
_renderIntValue("Burner current power", "%", EMS_Boiler.curBurnPow);
_renderShortValue("Flame current", "uA", EMS_Boiler.flameCurr);
_renderIntValue("System pressure", "bar", EMS_Boiler.sysPress, 10);
- if (EMS_Boiler.serviceCode == EMS_VALUE_SHORT_NOTSET) {
+ if (EMS_Boiler.serviceCode == EMS_VALUE_USHORT_NOTSET) {
myDebug_P(PSTR(" System service code: %s"), EMS_Boiler.serviceCodeChar);
} else {
myDebug_P(PSTR(" System service code: %s (%d)"), EMS_Boiler.serviceCodeChar, EMS_Boiler.serviceCode);
@@ -550,7 +571,7 @@ void showInfo() {
if (ems_getSolarModuleEnabled()) {
myDebug_P(PSTR("")); // newline
myDebug_P(PSTR("%sSolar Module stats:%s"), COLOR_BOLD_ON, COLOR_BOLD_OFF);
- myDebug_P(PSTR(" Solar Module: %s"), ems_getSolarModuleDescription(buffer_type));
+ myDebug_P(PSTR(" Solar module: %s"), ems_getSolarModuleDescription(buffer_type));
_renderShortValue("Collector temperature", "C", EMS_SolarModule.collectorTemp);
_renderShortValue("Bottom temperature", "C", EMS_SolarModule.bottomTemp);
_renderIntValue("Pump modulation", "%", EMS_SolarModule.pumpModulation);
@@ -561,16 +582,16 @@ void showInfo() {
(EMS_SolarModule.pumpWorkMin % 1440) / 60,
EMS_SolarModule.pumpWorkMin % 60);
}
- _renderUShortValue("Energy Last Hour", "Wh", EMS_SolarModule.EnergyLastHour, 1); // *10
- _renderUShortValue("Energy Today", "Wh", EMS_SolarModule.EnergyToday, 0);
- _renderUShortValue("Energy Total", "kWH", EMS_SolarModule.EnergyTotal, 1); // *10
+ _renderUShortValue("Energy last hour", "Wh", EMS_SolarModule.EnergyLastHour, 1); // *10
+ _renderUShortValue("Energy today", "Wh", EMS_SolarModule.EnergyToday, 0);
+ _renderUShortValue("Energy total", "kWh", EMS_SolarModule.EnergyTotal, 1); // *10
}
// For HeatPumps
if (ems_getHeatPumpEnabled()) {
myDebug_P(PSTR("")); // newline
myDebug_P(PSTR("%sHeat Pump stats:%s"), COLOR_BOLD_ON, COLOR_BOLD_OFF);
- myDebug_P(PSTR(" Solar Module: %s"), ems_getHeatPumpDescription(buffer_type));
+ myDebug_P(PSTR(" Heat Pump module: %s"), ems_getHeatPumpDescription(buffer_type));
_renderIntValue("Pump modulation", "%", EMS_HeatPump.HPModulation);
_renderIntValue("Pump speed", "%", EMS_HeatPump.HPSpeed);
}
@@ -579,7 +600,7 @@ void showInfo() {
if (ems_getThermostatEnabled()) {
myDebug_P(PSTR("")); // newline
myDebug_P(PSTR("%sThermostat stats:%s"), COLOR_BOLD_ON, COLOR_BOLD_OFF);
- myDebug_P(PSTR(" Thermostat: %s"), ems_getThermostatDescription(buffer_type));
+ myDebug_P(PSTR(" Thermostat: %s"), ems_getThermostatDescription(buffer_type, false));
// Render Current & Setpoint Room Temperature
if (ems_getThermostatModel() == EMS_MODEL_EASY) {
@@ -684,23 +705,22 @@ void publishSensorValues() {
}
}
-
-
// send values via MQTT
// a json object is created for the boiler and one for the thermostat
// CRC check is done to see if there are changes in the values since the last send to avoid too much wifi traffic
// a check is done against the previous values and if there are changes only then they are published. Unless force=true
void publishValues(bool force) {
- // don't send if MQTT is connected
+ // don't send if MQTT is not connected
if (!myESP.isMQTTConnected()) {
return;
}
- char s[20] = {0}; // for formatting strings
- StaticJsonDocument doc;
- char data[MQTT_MAX_SIZE] = {0};
- CRC32 crc;
- uint32_t fchecksum;
+ char s[20] = {0}; // for formatting strings
+ StaticJsonDocument doc;
+ char data[MQTT_MAX_PAYLOAD_SIZE] = {0};
+ CRC32 crc;
+ uint32_t fchecksum;
+ uint8_t jsonSize;
static uint8_t last_boilerActive = 0xFF; // for remembering last setting of the tap water or heating on/off
static uint32_t previousBoilerPublishCRC = 0; // CRC check for boiler values
@@ -772,22 +792,28 @@ void publishValues(bool force) {
if (abs(EMS_Boiler.heatWorkMin) != EMS_VALUE_LONG_NOTSET)
rootBoiler["heatWorkMin"] = (double)EMS_Boiler.heatWorkMin;
- rootBoiler["ServiceCode"] = EMS_Boiler.serviceCodeChar;
- rootBoiler["ServiceCodeNumber"] = EMS_Boiler.serviceCode;
+ if (EMS_Boiler.serviceCode != EMS_VALUE_USHORT_NOTSET) {
+ rootBoiler["ServiceCode"] = EMS_Boiler.serviceCodeChar;
+ rootBoiler["ServiceCodeNumber"] = EMS_Boiler.serviceCode;
+ }
serializeJson(doc, data, sizeof(data));
- // calculate hash and send values if something has changed, to save unnecessary wifi traffic
- for (size_t i = 0; i < measureJson(doc) - 1; i++) {
- crc.update(data[i]);
- }
- fchecksum = crc.finalize();
- if ((previousBoilerPublishCRC != fchecksum) || force) {
- previousBoilerPublishCRC = fchecksum;
- myDebugLog("Publishing boiler data via MQTT");
+ // check for empty json
+ jsonSize = measureJson(doc);
+ if (jsonSize > 2) {
+ // calculate hash and send values if something has changed, to save unnecessary wifi traffic
+ for (uint8_t i = 0; i < (jsonSize - 1); i++) {
+ crc.update(data[i]);
+ }
+ fchecksum = crc.finalize();
+ if ((previousBoilerPublishCRC != fchecksum) || force) {
+ previousBoilerPublishCRC = fchecksum;
+ myDebugLog("Publishing boiler data via MQTT");
- // send values via MQTT
- myESP.mqttPublish(TOPIC_BOILER_DATA, data);
+ // send values via MQTT
+ myESP.mqttPublish(TOPIC_BOILER_DATA, data);
+ }
}
// see if the heating or hot tap water has changed, if so send
@@ -859,18 +885,22 @@ void publishValues(bool force) {
data[0] = '\0'; // reset data for next package
serializeJson(doc, data, sizeof(data));
- // calculate new CRC
- crc.reset();
- for (size_t i = 0; i < measureJson(doc) - 1; i++) {
- crc.update(data[i]);
- }
- fchecksum = crc.finalize();
- if ((previousThermostatPublishCRC != fchecksum) || force) {
- previousThermostatPublishCRC = fchecksum;
- myDebugLog("Publishing thermostat data via MQTT");
+ // check for empty json
+ jsonSize = measureJson(doc);
+ if (jsonSize > 2) {
+ // calculate new CRC
+ crc.reset();
+ for (uint8_t i = 0; i < (jsonSize - 1); i++) {
+ crc.update(data[i]);
+ }
+ fchecksum = crc.finalize();
+ if ((previousThermostatPublishCRC != fchecksum) || force) {
+ previousThermostatPublishCRC = fchecksum;
+ myDebugLog("Publishing thermostat data via MQTT");
- // send values via MQTT
- myESP.mqttPublish(TOPIC_THERMOSTAT_DATA, data);
+ // send values via MQTT
+ myESP.mqttPublish(TOPIC_THERMOSTAT_DATA, data);
+ }
}
}
@@ -909,18 +939,22 @@ void publishValues(bool force) {
data[0] = '\0'; // reset data for next package
serializeJson(doc, data, sizeof(data));
- // calculate new CRC
- crc.reset();
- for (size_t i = 0; i < measureJson(doc) - 1; i++) {
- crc.update(data[i]);
- }
- fchecksum = crc.finalize();
- if ((previousSMPublishCRC != fchecksum) || force) {
- previousSMPublishCRC = fchecksum;
- myDebugLog("Publishing SM data via MQTT");
+ // check for empty json
+ jsonSize = measureJson(doc);
+ if (jsonSize > 2) {
+ // calculate new CRC
+ crc.reset();
+ for (uint8_t i = 0; i < (jsonSize - 1); i++) {
+ crc.update(data[i]);
+ }
+ fchecksum = crc.finalize();
+ if ((previousSMPublishCRC != fchecksum) || force) {
+ previousSMPublishCRC = fchecksum;
+ myDebugLog("Publishing SM data via MQTT");
- // send values via MQTT
- myESP.mqttPublish(TOPIC_SM_DATA, data);
+ // send values via MQTT
+ myESP.mqttPublish(TOPIC_SM_DATA, data);
+ }
}
}
@@ -1045,8 +1079,6 @@ void do_regularUpdates() {
ems_getThermostatValues();
ems_getBoilerValues();
ems_getSolarModuleValues();
- } else {
- myDebugLog("System is either not connect to the EMS bus or listen_mode is enabled");
}
}
@@ -1133,44 +1165,43 @@ void runUnitTest(uint8_t test_num) {
}
// callback for loading/saving settings to the file system (SPIFFS)
-bool FSCallback(MYESP_FSACTION action, const JsonObject json) {
+bool LoadSaveCallback(MYESP_FSACTION action, JsonObject json) {
if (action == MYESP_FSACTION_LOAD) {
- EMSESP_Status.led = json["led"];
- EMSESP_Status.led_gpio = json["led_gpio"] | EMSESP_LED_GPIO;
- EMSESP_Status.dallas_gpio = json["dallas_gpio"] | EMSESP_DALLAS_GPIO;
- EMSESP_Status.dallas_parasite = json["dallas_parasite"] | EMSESP_DALLAS_PARASITE;
+ const JsonObject & settings = json["settings"];
- EMS_Thermostat.device_id = json["thermostat_type"] | EMSESP_THERMOSTAT_TYPE;
- EMS_Boiler.device_id = json["boiler_type"] | EMSESP_BOILER_TYPE;
+ EMSESP_Status.led = settings["led"];
+ EMSESP_Status.led_gpio = settings["led_gpio"] | EMSESP_LED_GPIO;
+ EMSESP_Status.dallas_gpio = settings["dallas_gpio"] | EMSESP_DALLAS_GPIO;
+ EMSESP_Status.dallas_parasite = settings["dallas_parasite"] | EMSESP_DALLAS_PARASITE;
+ EMSESP_Status.shower_timer = settings["shower_timer"];
+ EMSESP_Status.shower_alert = settings["shower_alert"];
+ EMSESP_Status.publish_time = settings["publish_time"] | DEFAULT_PUBLISHTIME;
- EMSESP_Status.shower_timer = json["shower_timer"];
- EMSESP_Status.shower_alert = json["shower_alert"];
- EMSESP_Status.publish_time = json["publish_time"] | DEFAULT_PUBLISHTIME;
-
- ems_setTxMode(json["tx_mode"]);
-
- EMSESP_Status.listen_mode = json["listen_mode"];
+ EMSESP_Status.listen_mode = settings["listen_mode"];
ems_setTxDisabled(EMSESP_Status.listen_mode);
- EMSESP_Status.heating_circuit = json["heating_circuit"] | DEFAULT_HEATINGCIRCUIT;
+ EMSESP_Status.heating_circuit = settings["heating_circuit"] | DEFAULT_HEATINGCIRCUIT;
ems_setThermostatHC(EMSESP_Status.heating_circuit);
- return true; // return false if some settings are missing and we need to rebuild the file
+ EMSESP_Status.tx_mode = settings["tx_mode"] | 1; // default to 1 (generic)
+ ems_setTxMode(EMSESP_Status.tx_mode);
+
+ return true;
}
if (action == MYESP_FSACTION_SAVE) {
- json["thermostat_type"] = EMS_Thermostat.device_id;
- json["boiler_type"] = EMS_Boiler.device_id;
- json["led"] = EMSESP_Status.led;
- json["led_gpio"] = EMSESP_Status.led_gpio;
- json["dallas_gpio"] = EMSESP_Status.dallas_gpio;
- json["dallas_parasite"] = EMSESP_Status.dallas_parasite;
- json["listen_mode"] = EMSESP_Status.listen_mode;
- json["shower_timer"] = EMSESP_Status.shower_timer;
- json["shower_alert"] = EMSESP_Status.shower_alert;
- json["publish_time"] = EMSESP_Status.publish_time;
- json["heating_circuit"] = EMSESP_Status.heating_circuit;
- json["tx_mode"] = ems_getTxMode();
+ JsonObject settings = json.createNestedObject("settings");
+
+ settings["led"] = EMSESP_Status.led;
+ settings["led_gpio"] = EMSESP_Status.led_gpio;
+ settings["dallas_gpio"] = EMSESP_Status.dallas_gpio;
+ settings["dallas_parasite"] = EMSESP_Status.dallas_parasite;
+ settings["listen_mode"] = EMSESP_Status.listen_mode;
+ settings["shower_timer"] = EMSESP_Status.shower_timer;
+ settings["shower_alert"] = EMSESP_Status.shower_alert;
+ settings["publish_time"] = EMSESP_Status.publish_time;
+ settings["heating_circuit"] = EMSESP_Status.heating_circuit;
+ settings["tx_mode"] = EMSESP_Status.tx_mode;
return true;
}
@@ -1181,7 +1212,7 @@ bool FSCallback(MYESP_FSACTION action, const JsonObject json) {
// callback for custom settings when showing Stored Settings with the 'set' command
// wc is number of arguments after the 'set' command
// returns true if the setting was recognized and changed and should be saved back to SPIFFs
-bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, const char * value) {
+bool SetListCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, const char * value) {
bool ok = false;
if (action == MYESP_FSACTION_SET) {
@@ -1245,18 +1276,6 @@ bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, c
}
}
- // thermostat_type
- if (strcmp(setting, "thermostat_type") == 0) {
- EMS_Thermostat.device_id = ((wc == 2) ? (uint8_t)strtol(value, 0, 16) : EMS_ID_NONE);
- ok = true;
- }
-
- // boiler_type
- if (strcmp(setting, "boiler_type") == 0) {
- EMS_Boiler.device_id = ((wc == 2) ? (uint8_t)strtol(value, 0, 16) : EMS_ID_NONE);
- ok = true;
- }
-
// shower timer
if ((strcmp(setting, "shower_timer") == 0) && (wc == 2)) {
if (strcmp(value, "on") == 0) {
@@ -1301,10 +1320,16 @@ bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, c
}
}
- // tx delay/ tx mode
- if (((strcmp(setting, "tx_mode") == 0) || (strcmp(setting, "tx_delay") == 0)) && (wc == 2)) {
- ems_setTxMode(atoi(value));
- ok = true;
+ // tx_mode
+ if ((strcmp(setting, "tx_mode") == 0) && (wc == 2)) {
+ uint8_t mode = atoi(value);
+ if ((mode >= 1) && (mode <= 3)) {
+ EMSESP_Status.tx_mode = mode;
+ ems_setTxMode(mode);
+ ok = true;
+ } else {
+ myDebug_P(PSTR("Error. Usage: set tx_mode <1 | 2 | 3>"));
+ }
}
}
@@ -1313,26 +1338,12 @@ bool SettingsCallback(MYESP_FSACTION action, uint8_t wc, const char * setting, c
myDebug_P(PSTR(" led_gpio=%d"), EMSESP_Status.led_gpio);
myDebug_P(PSTR(" dallas_gpio=%d"), EMSESP_Status.dallas_gpio);
myDebug_P(PSTR(" dallas_parasite=%s"), EMSESP_Status.dallas_parasite ? "on" : "off");
-
- if (EMS_Thermostat.device_id == EMS_ID_NONE) {
- myDebug_P(PSTR(" thermostat_type="));
- } else {
- myDebug_P(PSTR(" thermostat_type=%02X"), EMS_Thermostat.device_id);
- }
-
myDebug_P(PSTR(" heating_circuit=%d"), EMSESP_Status.heating_circuit);
-
- if (EMS_Boiler.device_id == EMS_ID_NONE) {
- myDebug_P(PSTR(" boiler_type="));
- } else {
- myDebug_P(PSTR(" boiler_type=%02X"), EMS_Boiler.device_id);
- }
-
+ myDebug_P(PSTR(" tx_mode=%d"), EMSESP_Status.tx_mode);
myDebug_P(PSTR(" listen_mode=%s"), EMSESP_Status.listen_mode ? "on" : "off");
myDebug_P(PSTR(" shower_timer=%s"), EMSESP_Status.shower_timer ? "on" : "off");
myDebug_P(PSTR(" shower_alert=%s"), EMSESP_Status.shower_alert ? "on" : "off");
myDebug_P(PSTR(" publish_time=%d"), EMSESP_Status.publish_time);
- myDebug_P(PSTR(" tx_mode=%d"), ems_getTxMode());
}
return ok;
@@ -1469,6 +1480,9 @@ void TelnetCommandCallback(uint8_t wc, const char * commandLine) {
} else if (strcmp(second_cmd, "n") == 0) {
ems_setLogging(EMS_SYS_LOGGING_NONE);
ok = true;
+ } else if (strcmp(second_cmd, "j") == 0) {
+ ems_setLogging(EMS_SYS_LOGGING_JABBER);
+ ok = true;
}
}
@@ -1562,18 +1576,20 @@ void MQTTCallback(unsigned int type, const char * topic, const char * message) {
if (type == MQTT_CONNECT_EVENT) {
myESP.mqttSubscribe(TOPIC_THERMOSTAT_CMD_TEMP);
myESP.mqttSubscribe(TOPIC_THERMOSTAT_CMD_MODE);
- myESP.mqttSubscribe(TOPIC_BOILER_WWACTIVATED);
- myESP.mqttSubscribe(TOPIC_BOILER_CMD_WWTEMP);
- myESP.mqttSubscribe(TOPIC_BOILER_CMD_COMFORT);
- myESP.mqttSubscribe(TOPIC_BOILER_CMD_FLOWTEMP);
- myESP.mqttSubscribe(TOPIC_SHOWER_TIMER);
- myESP.mqttSubscribe(TOPIC_SHOWER_ALERT);
- myESP.mqttSubscribe(TOPIC_SHOWER_COLDSHOT);
myESP.mqttSubscribe(TOPIC_THERMOSTAT_CMD_HC);
myESP.mqttSubscribe(TOPIC_THERMOSTAT_CMD_DAYTEMP);
myESP.mqttSubscribe(TOPIC_THERMOSTAT_CMD_NIGHTTEMP);
myESP.mqttSubscribe(TOPIC_THERMOSTAT_CMD_HOLIDAYTEMP);
+ myESP.mqttSubscribe(TOPIC_BOILER_CMD_WWACTIVATED);
+ myESP.mqttSubscribe(TOPIC_BOILER_CMD_WWTEMP);
+ myESP.mqttSubscribe(TOPIC_BOILER_CMD_COMFORT);
+ myESP.mqttSubscribe(TOPIC_BOILER_CMD_FLOWTEMP);
+
+ myESP.mqttSubscribe(TOPIC_SHOWER_TIMER);
+ myESP.mqttSubscribe(TOPIC_SHOWER_ALERT);
+ myESP.mqttSubscribe(TOPIC_SHOWER_COLDSHOT);
+
// publish the status of the Shower parameters
myESP.mqttPublish(TOPIC_SHOWER_TIMER, EMSESP_Status.shower_timer ? "1" : "0");
myESP.mqttPublish(TOPIC_SHOWER_ALERT, EMSESP_Status.shower_alert ? "1" : "0");
@@ -1637,7 +1653,7 @@ void MQTTCallback(unsigned int type, const char * topic, const char * message) {
}
// wwActivated
- if (strcmp(topic, TOPIC_BOILER_WWACTIVATED) == 0) {
+ if (strcmp(topic, TOPIC_BOILER_CMD_WWACTIVATED) == 0) {
if ((message[0] == '1' || strcmp(message, "on") == 0) || (strcmp(message, "auto") == 0)) {
ems_setWarmWaterActivated(true);
} else if (message[0] == '0' || strcmp(message, "off") == 0) {
@@ -1699,58 +1715,175 @@ void MQTTCallback(unsigned int type, const char * topic, const char * message) {
}
}
-// web information for diagnostics
-void WebCallback(char * body) {
- strlcpy(body, "EMS stats: ", MYESP_MAXCHARBUFFER);
-
- if (ems_getBusConnected()) {
- char s[10];
- strlcat(body, "EMS Bus is connected ", MYESP_MAXCHARBUFFER);
- strlcat(body, "Rx: # successful read requests=", MYESP_MAXCHARBUFFER);
- strlcat(body, itoa(EMS_Sys_Status.emsRxPgks, s, 10), MYESP_MAXCHARBUFFER);
- strlcat(body, ", # CRC errors=", MYESP_MAXCHARBUFFER);
- strlcat(body, itoa(EMS_Sys_Status.emxCrcErr, s, 10), MYESP_MAXCHARBUFFER);
- if (ems_getTxCapable()) {
- strlcat(body, " Tx: # successful write requests=", MYESP_MAXCHARBUFFER);
- strlcat(body, itoa(EMS_Sys_Status.emsTxPkgs, s, 10), MYESP_MAXCHARBUFFER);
- } else {
- strlcat(body, " Tx: no signal
", MYESP_MAXCHARBUFFER);
- }
-
- // show device list
- strlcpy(body, "EMS devices found: ", MYESP_MAXCHARBUFFER);
-
- char buffer[MYESP_MAXCHARBUFFER] = {0};
- uint8_t num_devices = ems_printDevices_s(buffer, MYESP_MAXCHARBUFFER);
- if (num_devices == 0) {
- strlcat(body, "(any detected and compatible EMS devices will show up here)", MYESP_MAXCHARBUFFER);
- } else {
- strlcat(body, buffer, MYESP_MAXCHARBUFFER);
- }
-
- } else {
- strlcat(body, "Unable to establish a connection to the EMS Bus.", MYESP_MAXCHARBUFFER);
- }
-}
-
// Init callback, which is used to set functions and call methods after a wifi connection has been established
void WIFICallback() {
// This is where we enable the UART service to scan the incoming serial Tx/Rx bus signals
// This is done after we have a WiFi signal to avoid any resource conflicts
- system_uart_swap(); // TODO check
+ // TODO see if EMS bus is blocked during startup and whether we still need to delay the UART with the swap below?
+ // system_uart_swap();
+}
+
+// web information for diagnostics
+void WebCallback(JsonObject root) {
+ JsonObject emsbus = root.createNestedObject("emsbus");
- /*
if (myESP.getUseSerial()) {
- myDebug_P(PSTR("Warning! EMS bus communication disabled when Serial mode enabled. Use 'set serial off' to start communication."));
+ emsbus["ok"] = false;
+ emsbus["msg"] = "EMS Bus is disabled when in Serial mode. Check Settings->General Settings->Serial Port";
} else {
- emsuart_init();
- myDebug_P(PSTR("[UART] Opened Rx/Tx connection"));
- if (!EMSESP_Status.listen_mode) {
- // go and find the boiler and thermostat types, if not in listen mode
- ems_discoverModels();
+ if (ems_getBusConnected()) {
+ if (ems_getTxDisabled()) {
+ emsbus["ok"] = false;
+ emsbus["msg"] = "EMS Bus Connected with Rx active but Tx has been disabled (in listen only mode).";
+ } else if (ems_getTxCapable()) {
+ emsbus["ok"] = true;
+ emsbus["msg"] = "EMS Bus Connected with both Rx and Tx active.";
+ } else {
+ emsbus["ok"] = false;
+ emsbus["msg"] = "EMS Bus Connected but Tx is not working.";
+ }
+ } else {
+ emsbus["ok"] = false;
+ emsbus["msg"] = "EMS Bus is not connected. Check event logs for errors.";
}
}
- */
+
+ JsonArray list = emsbus.createNestedArray("devices");
+
+ for (std::list<_Generic_Device>::iterator it = Devices.begin(); it != Devices.end(); it++) {
+ JsonObject item = list.createNestedObject();
+ item["type"] = (it)->model_type;
+ item["model"] = (it)->model_string;
+ item["version"] = (it)->version;
+ item["productid"] = (it)->product_id;
+
+ char s[10];
+ itoa((it)->device_id, s, 16);
+ item["deviceid"] = s; // convert to hex
+ }
+
+ JsonObject thermostat = root.createNestedObject("thermostat");
+
+ if (ems_getThermostatEnabled()) {
+ thermostat["ok"] = true;
+
+ char buffer[200];
+ thermostat["tm"] = ems_getThermostatDescription(buffer, true);
+
+ // Render Current & Setpoint Room Temperature
+ if (ems_getThermostatModel() == EMS_MODEL_EASY) {
+ if (EMS_Thermostat.setpoint_roomTemp != EMS_VALUE_SHORT_NOTSET)
+ thermostat["ts"] = (double)EMS_Thermostat.setpoint_roomTemp / 100;
+ if (EMS_Thermostat.curr_roomTemp != EMS_VALUE_SHORT_NOTSET)
+ thermostat["tc"] = (double)EMS_Thermostat.curr_roomTemp / 100;
+ } else if ((ems_getThermostatModel() == EMS_MODEL_FR10) || (ems_getThermostatModel() == EMS_MODEL_FW100)
+ || (ems_getThermostatModel() == EMS_MODEL_FW120)) {
+ if (EMS_Thermostat.setpoint_roomTemp != EMS_VALUE_SHORT_NOTSET)
+ thermostat["ts"] = (double)EMS_Thermostat.setpoint_roomTemp / 10;
+ if (EMS_Thermostat.curr_roomTemp != EMS_VALUE_SHORT_NOTSET)
+ thermostat["tc"] = (double)EMS_Thermostat.curr_roomTemp / 10;
+ } else {
+ if (EMS_Thermostat.setpoint_roomTemp != EMS_VALUE_SHORT_NOTSET)
+ thermostat["ts"] = (double)EMS_Thermostat.setpoint_roomTemp / 2;
+ if (EMS_Thermostat.curr_roomTemp != EMS_VALUE_SHORT_NOTSET)
+ thermostat["tc"] = (double)EMS_Thermostat.curr_roomTemp / 10;
+ }
+
+ // Render Termostat Mode, if we have a mode
+ uint8_t thermoMode = _getThermostatMode(); // 0xFF=unknown, 0=low, 1=manual, 2=auto, 3=night, 4=day
+ if (thermoMode == 0) {
+ thermostat["tmode"] = "low";
+ } else if (thermoMode == 1) {
+ thermostat["tmode"] = "manual";
+ } else if (thermoMode == 2) {
+ thermostat["tmode"] = "auto";
+ } else if (thermoMode == 3) {
+ thermostat["tmode"] = "night";
+ } else if (thermoMode == 4) {
+ thermostat["tmode"] = "day";
+ }
+ } else {
+ thermostat["ok"] = false;
+ }
+
+ JsonObject boiler = root.createNestedObject("boiler");
+ if (ems_getBoilerEnabled()) {
+ boiler["ok"] = true;
+
+ char buffer[200];
+ boiler["bm"] = ems_getBoilerDescription(buffer, true);
+
+ boiler["b1"] = (EMS_Boiler.tapwaterActive ? "running" : "off");
+ boiler["b2"] = (EMS_Boiler.heatingActive ? "active" : "off");
+
+ if (EMS_Boiler.selFlowTemp != EMS_VALUE_INT_NOTSET)
+ boiler["b3"] = EMS_Boiler.selFlowTemp;
+
+ if (EMS_Boiler.curFlowTemp != EMS_VALUE_INT_NOTSET)
+ boiler["b4"] = EMS_Boiler.curFlowTemp / 10;
+
+ if (EMS_Boiler.boilTemp != EMS_VALUE_USHORT_NOTSET)
+ boiler["b5"] = (double)EMS_Boiler.boilTemp / 10;
+
+ if (EMS_Boiler.retTemp != EMS_VALUE_USHORT_NOTSET)
+ boiler["b6"] = (double)EMS_Boiler.retTemp / 10;
+
+ } else {
+ boiler["ok"] = false;
+ }
+
+ // For SM10/SM100 Solar Module
+ JsonObject sm = root.createNestedObject("sm");
+ if (ems_getSolarModuleEnabled()) {
+ sm["ok"] = true;
+
+ char buffer[200];
+ sm["sm"] = ems_getSolarModuleDescription(buffer, true);
+
+ if (EMS_SolarModule.collectorTemp != EMS_VALUE_SHORT_NOTSET)
+ sm["sm1"] = (double)EMS_SolarModule.collectorTemp / 10; // Collector temperature oC
+
+ if (EMS_SolarModule.bottomTemp != EMS_VALUE_SHORT_NOTSET)
+ sm["sm2"] = (double)EMS_SolarModule.bottomTemp / 10; // Bottom temperature oC
+
+ if (EMS_SolarModule.pumpModulation != EMS_VALUE_INT_NOTSET)
+ sm["sm3"] = EMS_SolarModule.pumpModulation; // Pump modulation %
+
+ if (EMS_SolarModule.pump != EMS_VALUE_INT_NOTSET) {
+ char s[10];
+ sm["sm4"] = _bool_to_char(s, EMS_SolarModule.pump); // Pump active on/off
+ }
+
+ if (EMS_SolarModule.EnergyLastHour != EMS_VALUE_USHORT_NOTSET)
+ sm["sm5"] = (double)EMS_SolarModule.EnergyLastHour / 10; // Energy last hour Wh
+
+ if (EMS_SolarModule.EnergyToday != EMS_VALUE_USHORT_NOTSET) // Energy today Wh
+ sm["sm6"] = EMS_SolarModule.EnergyToday;
+
+ if (EMS_SolarModule.EnergyTotal != EMS_VALUE_USHORT_NOTSET) // Energy total KWh
+ sm["sm7"] = (double)EMS_SolarModule.EnergyTotal / 10;
+ } else {
+ sm["ok"] = false;
+ }
+
+ // For HeatPumps
+ JsonObject hp = root.createNestedObject("hp");
+ if (ems_getHeatPumpEnabled()) {
+ hp["ok"] = true;
+ char buffer[200];
+ hp["hm"] = ems_getHeatPumpDescription(buffer, true);
+
+ if (EMS_HeatPump.HPModulation != EMS_VALUE_INT_NOTSET)
+ hp["hp1"] = EMS_HeatPump.HPModulation; // Pump modulation %
+
+ if (EMS_HeatPump.HPSpeed != EMS_VALUE_INT_NOTSET)
+ hp["hp2"] = EMS_HeatPump.HPSpeed; // Pump speed %
+ } else {
+ hp["ok"] = false;
+ }
+
+
+ // serializeJsonPretty(root, Serial); // turn on for debugging
}
// Initialize the boiler settings and shower settings
@@ -1864,25 +1997,14 @@ void setup() {
systemCheckTimer.attach(SYSTEMCHECK_TIME, do_systemCheck); // check if EMS is reachable
- // set up myESP for Wifi, MQTT, MDNS and Telnet
- myESP.setTelnet(TelnetCommandCallback, TelnetCallback); // set up Telnet commands
- myESP.setWIFI(NULL, NULL, WIFICallback); // empty ssid and password as we take this from the config file
-
- // MQTT host, username and password taken from the SPIFFS settings
- myESP.setMQTT(
- NULL, NULL, NULL, MQTT_BASE, MQTT_KEEPALIVE, MQTT_QOS, MQTT_RETAIN, MQTT_WILL_TOPIC, MQTT_WILL_ONLINE_PAYLOAD, MQTT_WILL_OFFLINE_PAYLOAD, MQTTCallback);
-
- // OTA callback which is called when OTA is starting and stopping
- myESP.setOTA(OTACallback_pre, OTACallback_post);
-
- // custom settings in SPIFFS
- myESP.setSettings(FSCallback, SettingsCallback);
-
- // web custom settings
- myESP.setWeb(WebCallback);
-
- // start up all the services
- myESP.begin(APP_HOSTNAME, APP_NAME, APP_VERSION);
+ // set up myESP for Wifi, MQTT, MDNS and Telnet callbacks
+ myESP.setTelnet(TelnetCommandCallback, TelnetCallback); // set up Telnet commands
+ myESP.setWIFI(WIFICallback); // wifi callback
+ myESP.setMQTT(MQTTCallback); // MQTT ip, username and password taken from the SPIFFS settings
+ myESP.setSettings(LoadSaveCallback, SetListCallback, false); // default is Serial off
+ myESP.setWeb(WebCallback); // web custom settings
+ myESP.setOTA(OTACallback_pre, OTACallback_post); // OTA callback which is called when OTA is starting and stopping
+ myESP.begin(APP_HOSTNAME, APP_NAME, APP_VERSION, APP_URL, APP_UPDATEURL);
// at this point we have all the settings from our internall SPIFFS config file
// fire up the UART now
@@ -1891,7 +2013,8 @@ void setup() {
} else {
Serial.println("Note: Serial output will now be disabled. Please use Telnet.");
Serial.flush();
- emsuart_init();
+ myESP.setUseSerial(false);
+ emsuart_init(); // start EMS bus transmissions
myDebug_P(PSTR("[UART] Opened Rx/Tx connection"));
if (!EMSESP_Status.listen_mode) {
// go and find the boiler and thermostat types, if not in listen mode
diff --git a/src/ems.cpp b/src/ems.cpp
index b33c6c597..2493fb495 100644
--- a/src/ems.cpp
+++ b/src/ems.cpp
@@ -7,19 +7,17 @@
*/
#include "ems.h"
+#include "MyESP.h"
#include "ems_devices.h"
#include "emsuart.h"
-#include
#include // https://github.com/rlogiacco/CircularBuffer
-#include
-#include // std::list
#ifdef TESTS
#include "test_data.h"
uint8_t _TEST_DATA_max = ArraySize(TEST_DATA);
#endif
-// myESP for logging to telnet and serial
+// MyESP class for logging to telnet and serial
#define myDebug(...) myESP.myDebug(__VA_ARGS__)
#define myDebug_P(...) myESP.myDebug_P(__VA_ARGS__)
@@ -28,7 +26,7 @@ _EMS_Sys_Status EMS_Sys_Status; // EMS Status
CircularBuffer<_EMS_TxTelegram, EMS_TX_TELEGRAM_QUEUE_MAX> EMS_TxQueue; // FIFO queue for Tx send buffer
// for storing all detected EMS devices
-std::list<_Generic_Type> Devices;
+std::list<_Generic_Device> Devices;
// macros used in the _process* functions
#define _toByte(i) (EMS_RxTelegram->data[i])
@@ -95,8 +93,6 @@ void _process_EasyStatusMessage(_EMS_RxTelegram * EMS_RxTelegram);
// RC1010, RC300, RC310
void _process_RCPLUSStatusMessage(_EMS_RxTelegram * EMS_RxTelegram);
void _process_RCPLUSSetMessage(_EMS_RxTelegram * EMS_RxTelegram);
-void _process_RCPLUSStatusHeating(_EMS_RxTelegram * EMS_RxTelegram);
-void _process_RCPLUSStatusHeating(_EMS_RxTelegram * EMS_RxTelegram);
void _process_RCPLUSStatusMode(_EMS_RxTelegram * EMS_RxTelegram);
// Junkers FR10 & FW100
@@ -176,7 +172,6 @@ const _EMS_Type EMS_Types[] = {
// Nefit 1010, RC300, RC310 (EMS Plus)
{EMS_MODEL_ALL, EMS_TYPE_RCPLUSStatusMessage, "RCPLUSStatusMessage", _process_RCPLUSStatusMessage},
{EMS_MODEL_ALL, EMS_TYPE_RCPLUSSet, "RCPLUSSetMessage", _process_RCPLUSSetMessage},
- {EMS_MODEL_ALL, EMS_TYPE_RCPLUSStatusHeating, "RCPLUSStatusHeating", _process_RCPLUSStatusHeating},
{EMS_MODEL_ALL, EMS_TYPE_RCPLUSStatusMode, "RCPLUSStatusMode", _process_RCPLUSStatusMode},
// Junkers FR10
@@ -186,12 +181,12 @@ const _EMS_Type EMS_Types[] = {
};
// calculate sizes of arrays at compile
-uint8_t _EMS_Types_max = ArraySize(EMS_Types); // number of defined types
-uint8_t _Boiler_Types_max = ArraySize(Boiler_Types); // number of boiler models
-uint8_t _Solar_Module_Types_max = ArraySize(SolarModule_Types); // number of solar module types
-uint8_t _Other_Types_max = ArraySize(Other_Types); // number of other ems devices
-uint8_t _Thermostat_Types_max = ArraySize(Thermostat_Types); // number of defined thermostat types
-uint8_t _HeatPump_Types_max = ArraySize(HeatPump_Types); // number of defined heatpuimp types
+uint8_t _EMS_Types_max = ArraySize(EMS_Types); // number of defined types
+uint8_t _Boiler_Devices_max = ArraySize(Boiler_Devices); // number of boiler models
+uint8_t _SolarModule_Types_max = ArraySize(SolarModule_Devices); // number of solar module types
+uint8_t _Other_Devices_max = ArraySize(Other_Devices); // number of other ems devices
+uint8_t _Thermostat_Devices_max = ArraySize(Thermostat_Devices); // number of defined thermostat types
+uint8_t _HeatPump_Devices_max = ArraySize(HeatPump_Devices); // number of defined heatpuimp types
// these structs contain the data we store from the specific EMS devices
_EMS_Boiler EMS_Boiler; // for boiler
@@ -226,7 +221,7 @@ void ems_init() {
EMS_Sys_Status.emsTxPkgs = 0;
EMS_Sys_Status.emxCrcErr = 0;
EMS_Sys_Status.emsRxStatus = EMS_RX_STATUS_IDLE;
- EMS_Sys_Status.emsTxStatus = EMS_TX_STATUS_IDLE;
+ EMS_Sys_Status.emsTxStatus = EMS_TX_REV_DETECT;
EMS_Sys_Status.emsRefreshed = false;
EMS_Sys_Status.emsPollEnabled = false; // start up with Poll disabled
EMS_Sys_Status.emsBusConnected = false;
@@ -235,8 +230,8 @@ void ems_init() {
EMS_Sys_Status.emsTxDisabled = false;
EMS_Sys_Status.emsPollFrequency = 0;
EMS_Sys_Status.txRetryCount = 0;
- EMS_Sys_Status.emsReverse = false;
- EMS_Sys_Status.emsTxMode = 0;
+ EMS_Sys_Status.emsIDMask = 0x00;
+ EMS_Sys_Status.emsPollAck[0] = EMS_ID_ME;
// thermostat
EMS_Thermostat.setpoint_roomTemp = EMS_VALUE_SHORT_NOTSET;
@@ -278,10 +273,10 @@ void ems_init() {
EMS_Boiler.wWCirc = EMS_VALUE_INT_NOTSET; // Circulation on/off
EMS_Boiler.selBurnPow = EMS_VALUE_INT_NOTSET; // Burner max power
EMS_Boiler.curBurnPow = EMS_VALUE_INT_NOTSET; // Burner current power
- EMS_Boiler.flameCurr = EMS_VALUE_SHORT_NOTSET; // Flame current in micro amps
+ EMS_Boiler.flameCurr = EMS_VALUE_USHORT_NOTSET; // Flame current in micro amps
EMS_Boiler.sysPress = EMS_VALUE_INT_NOTSET; // System pressure
strlcpy(EMS_Boiler.serviceCodeChar, "??", sizeof(EMS_Boiler.serviceCodeChar));
- EMS_Boiler.serviceCode = EMS_VALUE_SHORT_NOTSET;
+ EMS_Boiler.serviceCode = EMS_VALUE_USHORT_NOTSET;
// UBAMonitorSlow
EMS_Boiler.extTemp = EMS_VALUE_SHORT_NOTSET; // Outside temperature
@@ -340,7 +335,6 @@ void ems_init() {
EMS_Thermostat.product_id = EMS_ID_NONE;
strlcpy(EMS_Thermostat.version, "?", sizeof(EMS_Thermostat.version));
-
// default logging is none
ems_setLogging(EMS_SYS_LOGGING_DEFAULT);
}
@@ -355,23 +349,6 @@ bool ems_getPoll() {
return EMS_Sys_Status.emsPollEnabled;
}
-void ems_setTxMode(uint8_t mode) {
- EMS_Sys_Status.emsTxMode = mode;
-
- // special case for Junkers. If tx_mode is 3 then set the reverse poll flag
- // https://github.com/proddy/EMS-ESP/issues/103#issuecomment-495945850
- if (mode == 3) {
- EMS_Sys_Status.emsReverse = true;
- myDebug_P(PSTR("Forcing emsReverse for Junkers"));
- } else {
- EMS_Sys_Status.emsReverse = false;
- }
-}
-
-uint8_t ems_getTxMode() {
- return EMS_Sys_Status.emsTxMode;
-}
-
bool ems_getEmsRefreshed() {
return EMS_Sys_Status.emsRefreshed;
}
@@ -439,7 +416,7 @@ _EMS_SYS_LOGGING ems_getLogging() {
}
void ems_setLogging(_EMS_SYS_LOGGING loglevel) {
- if (loglevel <= EMS_SYS_LOGGING_VERBOSE) {
+ if (loglevel <= EMS_SYS_LOGGING_JABBER) {
EMS_Sys_Status.emsLogging = loglevel;
if (loglevel == EMS_SYS_LOGGING_NONE) {
myDebug_P(PSTR("System Logging set to None"));
@@ -453,10 +430,19 @@ void ems_setLogging(_EMS_SYS_LOGGING loglevel) {
myDebug_P(PSTR("System Logging set to Solar Module only"));
} else if (loglevel == EMS_SYS_LOGGING_RAW) {
myDebug_P(PSTR("System Logging set to Raw mode"));
+ } else if (loglevel == EMS_SYS_LOGGING_JABBER) {
+ myDebug_P(PSTR("System Logging set to Jabber mode"));
}
}
}
+/**
+ * send a poll acknowledge
+ */
+void ems_tx_pollAck() {
+ emsuart_tx_buffer(&EMS_Sys_Status.emsPollAck[0], 1);
+}
+
/**
* Calculate CRC checksum using lookup table for speed
* len is length of all the data in bytes (including the header & CRC byte at end)
@@ -522,6 +508,11 @@ int _ems_findType(uint16_t type) {
return (typeFound ? i : -1);
}
+void ems_setTxMode(uint8_t mode) {
+ EMS_Sys_Status.emsTxMode = mode;
+}
+
+
/**
* debug print a telegram to telnet/serial including the CRC
*/
@@ -556,18 +547,14 @@ void _debugPrintTelegram(const char * prefix, _EMS_RxTelegram * EMS_RxTelegram,
strlcat(output_str, " ", sizeof(output_str)); // add space
}
- if (raw) {
- strlcat(output_str, _hextoa(data[length - 1], buffer), sizeof(output_str));
- } else {
- strlcat(output_str, "(CRC=", sizeof(output_str));
- strlcat(output_str, _hextoa(data[length - 1], buffer), sizeof(output_str));
- strlcat(output_str, ")", sizeof(output_str));
+ strlcat(output_str, "(CRC=", sizeof(output_str));
+ strlcat(output_str, _hextoa(data[length - 1], buffer), sizeof(output_str));
+ strlcat(output_str, ")", sizeof(output_str));
- // print number of data bytes only if its a valid telegram
- if (data_len) {
- strlcat(output_str, " #data=", sizeof(output_str));
- strlcat(output_str, itoa(data_len, buffer, 10), sizeof(output_str));
- }
+ // print number of data bytes only if its a valid telegram
+ if (data_len) {
+ strlcat(output_str, " #data=", sizeof(output_str));
+ strlcat(output_str, itoa(data_len, buffer, 10), sizeof(output_str));
}
strlcat(output_str, COLOR_RESET, sizeof(output_str));
@@ -586,7 +573,7 @@ void _ems_sendTelegram() {
}
// if we're preventing all outbound traffic, quit
- if (EMS_Sys_Status.emsTxDisabled) {
+ if (ems_getTxDisabled()) {
EMS_TxQueue.shift(); // remove from queue
if (ems_getLogging() != EMS_SYS_LOGGING_NONE) {
myDebug_P(PSTR("in Listen Mode. All Tx is disabled."));
@@ -606,6 +593,8 @@ void _ems_sendTelegram() {
// if we're in raw mode just fire and forget
if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_RAW) {
+ EMS_TxTelegram.data[EMS_TxTelegram.length - 1] = _crcCalculator(EMS_TxTelegram.data, EMS_TxTelegram.length); // add the CRC
+
if (EMS_Sys_Status.emsLogging != EMS_SYS_LOGGING_NONE) {
_EMS_RxTelegram EMS_RxTelegram; // create new Rx object
EMS_RxTelegram.length = EMS_TxTelegram.length; // full length of telegram
@@ -614,32 +603,46 @@ void _ems_sendTelegram() {
_debugPrintTelegram("Sending raw: ", &EMS_RxTelegram, COLOR_CYAN, true);
}
- EMS_TxTelegram.data[EMS_TxTelegram.length - 1] = _crcCalculator(EMS_TxTelegram.data, EMS_TxTelegram.length); // add the CRC
- emsuart_tx_buffer(EMS_TxTelegram.data, EMS_TxTelegram.length); // send the telegram to the UART Tx
- EMS_TxQueue.shift(); // and remove from queue
+ _EMS_TX_STATUS _txStatus = emsuart_tx_buffer(EMS_TxTelegram.data, EMS_TxTelegram.length); // send the telegram to the UART Tx
+ if (EMS_TX_BRK_DETECT == _txStatus || EMS_TX_WTD_TIMEOUT == _txStatus) {
+ // Tx Error!
+ if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_VERBOSE) {
+ myDebug_P(PSTR("** error sending buffer: %s"), _txStatus == EMS_TX_BRK_DETECT ? "BRK" : "WDTO");
+ }
+ // EMS_Sys_Status.emsTxStatus = EMS_TX_STATUS_IDLE;
+ }
+ EMS_TxQueue.shift(); // and remove from queue
return;
}
// create the header
- EMS_TxTelegram.data[0] = (EMS_Sys_Status.emsReverse) ? EMS_ID_ME | 0x80 : EMS_ID_ME; // src
+ EMS_TxTelegram.data[0] = EMS_ID_ME ^ EMS_Sys_Status.emsIDMask; // src
// dest
if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_WRITE) {
- EMS_TxTelegram.data[1] = EMS_TxTelegram.dest;
+ EMS_TxTelegram.data[1] = EMS_TxTelegram.dest ^ EMS_Sys_Status.emsIDMask;
} else {
// for a READ or VALIDATE
- EMS_TxTelegram.data[1] = EMS_TxTelegram.dest | 0x80; // read has 8th bit set
+ EMS_TxTelegram.data[1] = (EMS_TxTelegram.dest ^ 0x80 ^ EMS_Sys_Status.emsIDMask); // read has 8th bit set
}
// complete the rest of the header depending on EMS or EMS+
if (EMS_TxTelegram.type > 0xFF) {
- // EMS 2.0 / emsplus
+ // EMS 2.0 / EMS+
EMS_TxTelegram.data[2] = 0xFF; // fixed value indicating an extended message
EMS_TxTelegram.data[3] = EMS_TxTelegram.offset;
- EMS_TxTelegram.data[4] = EMS_TxTelegram.dataValue; // for read its #bytes to return, for write it the value to set
- EMS_TxTelegram.data[5] = EMS_TxTelegram.type >> 8; // type, 1st byte
- EMS_TxTelegram.data[6] = EMS_TxTelegram.type & 0xFF; // type, 2nd byte
- EMS_TxTelegram.length += 2; // add 2 bytes to length to compensate the extra FF and byte for the type
+ EMS_TxTelegram.length += 2; // add 2 bytes to length to compensate the extra FF and byte for the type
+
+ // EMS+ has different format for read and write. See https://github.com/proddy/EMS-ESP/wiki/RC3xx-Thermostats
+ if ((EMS_TxTelegram.action == EMS_TX_TELEGRAM_READ) || (EMS_TxTelegram.action == EMS_TX_TELEGRAM_VALIDATE)) {
+ EMS_TxTelegram.data[4] = EMS_TxTelegram.dataValue; // for read its #bytes to return
+ EMS_TxTelegram.data[5] = EMS_TxTelegram.type >> 8; // type, 1st byte
+ EMS_TxTelegram.data[6] = EMS_TxTelegram.type & 0xFF; // type, 2nd byte
+ } else if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_WRITE) {
+ EMS_TxTelegram.data[4] = EMS_TxTelegram.type >> 8; // type, 1st byte
+ EMS_TxTelegram.data[5] = EMS_TxTelegram.type & 0xFF; // type, 2nd byte
+ EMS_TxTelegram.data[6] = EMS_TxTelegram.dataValue; // for write it the value to set
+ }
} else {
// EMS 1.0
EMS_TxTelegram.data[2] = EMS_TxTelegram.type; // type
@@ -656,30 +659,38 @@ void _ems_sendTelegram() {
if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_VERBOSE) {
char s[64] = {0};
if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_WRITE) {
- snprintf(s, sizeof(s), "Sending write of type 0x%02X to 0x%02X:", EMS_TxTelegram.type, EMS_TxTelegram.dest & 0x7F);
+ snprintf(s, sizeof(s), "Sending write of type 0x%02X to 0x%02X, ", EMS_TxTelegram.type, EMS_TxTelegram.dest & 0x7F);
} else if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_READ) {
- snprintf(s, sizeof(s), "Sending read of type 0x%02X to 0x%02X:", EMS_TxTelegram.type, EMS_TxTelegram.dest & 0x7F);
+ snprintf(s, sizeof(s), "Sending read of type 0x%02X to 0x%02X, ", EMS_TxTelegram.type, EMS_TxTelegram.dest & 0x7F);
} else if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_VALIDATE) {
- snprintf(s, sizeof(s), "Sending validate of type 0x%02X to 0x%02X:", EMS_TxTelegram.type, EMS_TxTelegram.dest & 0x7F);
+ snprintf(s, sizeof(s), "Sending validate of type 0x%02X to 0x%02X, ", EMS_TxTelegram.type, EMS_TxTelegram.dest & 0x7F);
}
_EMS_RxTelegram EMS_RxTelegram;
- EMS_RxTelegram.length = EMS_TxTelegram.length; // complete length of telegram
- EMS_RxTelegram.telegram = EMS_TxTelegram.data;
- EMS_RxTelegram.timestamp = millis(); // now
+ EMS_RxTelegram.length = EMS_TxTelegram.length; // complete length of telegram incl CRC
+ EMS_RxTelegram.data_length = 0; // ignore the data length for read and writes. only used for incoming.
+ EMS_RxTelegram.telegram = EMS_TxTelegram.data;
+ EMS_RxTelegram.timestamp = millis(); // now
_debugPrintTelegram(s, &EMS_RxTelegram, COLOR_CYAN);
}
// send the telegram to the UART Tx
- emsuart_tx_buffer(EMS_TxTelegram.data, EMS_TxTelegram.length);
-
- EMS_Sys_Status.emsTxStatus = EMS_TX_STATUS_WAIT;
+ _EMS_TX_STATUS _txStatus = emsuart_tx_buffer(EMS_TxTelegram.data, EMS_TxTelegram.length); // send the telegram to the UART Tx
+ if (EMS_TX_STATUS_OK == _txStatus || EMS_TX_STATUS_IDLE == _txStatus)
+ EMS_Sys_Status.emsTxStatus = EMS_TX_STATUS_WAIT;
+ else {
+ // Tx Error!
+ if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_VERBOSE) {
+ myDebug_P(PSTR("** error sending buffer: %s"), _txStatus == EMS_TX_BRK_DETECT ? "BRK" : "WDTO");
+ }
+ EMS_Sys_Status.emsTxStatus = EMS_TX_STATUS_IDLE;
+ }
}
/**
* Takes the last write command and turns into a validate request
- * placing it on the queue
+ * placing it on the Tx queue
*/
void _createValidate() {
if (EMS_TxQueue.isEmpty()) {
@@ -703,9 +714,10 @@ void _createValidate() {
new_EMS_TxTelegram.action = EMS_TX_TELEGRAM_VALIDATE;
// copy old Write record
- new_EMS_TxTelegram.type_validate = EMS_TxTelegram.type_validate;
+ new_EMS_TxTelegram.type_validate = EMS_TxTelegram.type; // save the original type in the type_validate, increase we need to re-try
+ new_EMS_TxTelegram.type = EMS_TxTelegram.type_validate; // new type is the validate type
+
new_EMS_TxTelegram.dest = EMS_TxTelegram.dest;
- new_EMS_TxTelegram.type = EMS_TxTelegram.type;
new_EMS_TxTelegram.comparisonValue = EMS_TxTelegram.comparisonValue;
new_EMS_TxTelegram.comparisonPostRead = EMS_TxTelegram.comparisonPostRead;
new_EMS_TxTelegram.comparisonOffset = EMS_TxTelegram.comparisonOffset;
@@ -720,6 +732,57 @@ void _createValidate() {
EMS_TxQueue.unshift(new_EMS_TxTelegram); // add back to queue making it first to be picked up next (FIFO)
}
+/**
+ * dump a UART Tx or Rx buffer to console...
+ */
+void ems_dumpBuffer(const char * prefix, uint8_t * telegram, uint8_t length) {
+ uint32_t timestamp = millis();
+ static char output_str[200] = {0};
+ static char buffer[16] = {0};
+
+ /*
+ // we only care about known devices
+ if (length) {
+ uint8_t dev = telegram[0] & 0x7F;
+ if (!((dev == 0x04)||(dev == 0x08)||(dev == 0x09)||(dev == 0x0a)
+ ||(dev == 0x01)||(dev == 0x0b)||(dev == 0x10)))
+ return;
+ }
+*/
+
+ strlcpy(output_str, "(", sizeof(output_str));
+ strlcat(output_str, COLOR_CYAN, sizeof(output_str));
+ strlcat(output_str, _smallitoa((uint8_t)((timestamp / 3600000) % 24), buffer), sizeof(output_str));
+ strlcat(output_str, ":", sizeof(output_str));
+ strlcat(output_str, _smallitoa((uint8_t)((timestamp / 60000) % 60), buffer), sizeof(output_str));
+ strlcat(output_str, ":", sizeof(output_str));
+ strlcat(output_str, _smallitoa((uint8_t)((timestamp / 1000) % 60), buffer), sizeof(output_str));
+ strlcat(output_str, ".", sizeof(output_str));
+ strlcat(output_str, _smallitoa3(timestamp % 1000, buffer), sizeof(output_str));
+ strlcat(output_str, COLOR_RESET, sizeof(output_str));
+ strlcat(output_str, ") ", sizeof(output_str));
+
+ strlcat(output_str, COLOR_YELLOW, sizeof(output_str));
+ strlcat(output_str, prefix, sizeof(output_str));
+
+ // show some EMS_Sys_Status entries
+ strlcat(output_str, _hextoa(EMS_Sys_Status.emsRxStatus, buffer), sizeof(output_str));
+ strlcat(output_str, " ", sizeof(output_str));
+ strlcat(output_str, _hextoa(EMS_Sys_Status.emsTxStatus, buffer), sizeof(output_str));
+ strlcat(output_str, ": ", sizeof(output_str));
+
+
+ // print whole buffer, don't interpret any data
+ for (int i = 0; i < (length); i++) {
+ strlcat(output_str, _hextoa(telegram[i], buffer), sizeof(output_str));
+ strlcat(output_str, " ", sizeof(output_str));
+ }
+
+ strlcat(output_str, COLOR_RESET, sizeof(output_str));
+
+ myDebug(output_str);
+}
+
/**
* Entry point triggered by an interrupt in emsuart.cpp
* length is the number of all the telegram bytes up to and including the CRC at the end
@@ -727,46 +790,61 @@ void _createValidate() {
* When a telegram is processed we forcefully erase it from the stack to prevent overflow
*/
void ems_parseTelegram(uint8_t * telegram, uint8_t length) {
- static uint32_t _last_emsPollFrequency = 0;
+ if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_JABBER) {
+ ems_dumpBuffer("ems_parseTelegram: ", telegram, length);
+ }
+
+ /*
+ * Detect the EMS bus type - Buderus or Junkers - and set emsIDMask accordingly.
+ * we wait for the first valid telegram and look at the SourceID.
+ * If Bit 7 is set we have a Buderus, otherwise a Junkers
+ */
+ if (EMS_Sys_Status.emsTxStatus == EMS_TX_REV_DETECT) {
+ if ((length >= 5) && (telegram[length - 1] == _crcCalculator(telegram, length))) {
+ EMS_Sys_Status.emsTxStatus = EMS_TX_STATUS_IDLE;
+ EMS_Sys_Status.emsIDMask = telegram[0] & 0x80;
+ EMS_Sys_Status.emsPollAck[0] = EMS_ID_ME ^ EMS_Sys_Status.emsIDMask;
+ } else
+ return; // ignore the whole telegram Rx Telegram while in DETECT mode
+ }
+
+ /*
+ * It may happen that we where interrupted (for instance by WIFI activity) and the
+ * buffer isn't valid anymore, so we must not answer at all...
+ */
+ if (EMS_Sys_Status.emsRxStatus != EMS_RX_STATUS_IDLE) {
+ if (EMS_Sys_Status.emsLogging > EMS_SYS_LOGGING_NONE) {
+ myDebug_P(PSTR("** [DEBUG MODE] Warning, we missed the bus - Rx non-idle!"));
+ }
+ return;
+ }
/*
* check if we just received a single byte
* it could well be a Poll request from the boiler for us, which will have a value of 0x8B (0x0B | 0x80)
* or either a return code like 0x01 or 0x04 from the last Write command
- * Roger Wilco: we have different types here:
- * EMS_ID_ME && length == 1 && EMS_TX_STATUS_IDLE && EMS_RX_STATUS_IDLE: polling request
- * EMS_ID_ME && length > 1 && EMS_TX_STATUS_IDLE && EMS_RX_STATUS_IDLE: direct telegram
- * (EMS_TX_SUCCESS || EMS_TX_ERROR) && EMS_TX_STATUS_WAIT: response, free the EMS bus
- *
- * In addition, it may happen that we where interrupted (f.e. by WIFI activity) and the
- * buffer isn't valid anymore, so we must not answer at all...
*/
- if (EMS_Sys_Status.emsRxStatus != EMS_RX_STATUS_IDLE) {
- if (EMS_Sys_Status.emsLogging > EMS_SYS_LOGGING_NONE) {
- myDebug_P(PSTR("** [DEBUG MODE] We missed the bus - Rx non-idle!")); //TODO tidy up error logging
- }
- return;
- }
-
if (length == 1) {
- uint8_t value = telegram[0]; // 1st byte of data package
+ uint8_t value = telegram[0]; // 1st byte of data package
+ static uint32_t _last_emsPollFrequency = 0;
+ static uint8_t delay_tx = 0; // TODO remove, experimental
// check first for a Poll for us
- // the poll has the MSB set - seems to work on both EMS and Junkers
- if ((value & 0x7F) == EMS_ID_ME) {
- EMS_Sys_Status.emsTxCapable = true;
+ if ((value ^ 0x80 ^ EMS_Sys_Status.emsIDMask) == EMS_ID_ME) {
uint32_t timenow_microsecs = micros();
EMS_Sys_Status.emsPollFrequency = (timenow_microsecs - _last_emsPollFrequency);
_last_emsPollFrequency = timenow_microsecs;
// do we have something to send thats waiting in the Tx queue?
// if so send it if the Queue is not in a wait state
- if ((!EMS_TxQueue.isEmpty()) && (EMS_Sys_Status.emsTxStatus == EMS_TX_STATUS_IDLE)) {
+ // TODO: but only do this at defined rate otherwise it'll be too quick, so skip a few polls
+ if ((!EMS_TxQueue.isEmpty()) && (EMS_Sys_Status.emsTxStatus == EMS_TX_STATUS_IDLE) && (++delay_tx == 2)) {
+ delay_tx = 0;
_ems_sendTelegram(); // perform the read/write command immediately
} else {
// nothing to send so just send a poll acknowledgement back
if (EMS_Sys_Status.emsPollEnabled) {
- emsuart_tx_poll();
+ ems_tx_pollAck();
}
}
} else if (EMS_Sys_Status.emsTxStatus == EMS_TX_STATUS_WAIT) {
@@ -774,15 +852,15 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) {
if (value == EMS_TX_SUCCESS) {
EMS_Sys_Status.emsTxPkgs++;
// got a success 01. Send a validate to check the value of the last write
- emsuart_tx_poll(); // send a poll to free the EMS bus
+ ems_tx_pollAck(); // send a poll to free the EMS bus
_createValidate(); // create a validate Tx request (if needed)
} else if (value == EMS_TX_ERROR) {
// last write failed (04), delete it from queue and dont bother to retry
if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_VERBOSE) {
- myDebug_P(PSTR("** Write command failed from host"));
+ myDebug_P(PSTR("-> Error: Write command failed from host"));
}
- emsuart_tx_poll(); // send a poll to free the EMS bus
- _removeTxQueue(); // remove from queue
+ ems_tx_pollAck(); // send a poll to free the EMS bus
+ _removeTxQueue(); // remove from queue
}
}
@@ -790,9 +868,9 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) {
}
// ignore anything that doesn't resemble a proper telegram package
- // minimal is 5 bytes, excluding CRC at the end
+ // minimal is 5 bytes, excluding CRC at the end (for EMS1.0)
if (length <= 4) {
- //_debugPrintTelegram("Noisy data:", &EMS_RxTelegram COLOR_RED);
+ // _debugPrintTelegram("Noisy data:", &EMS_RxTelegram, COLOR_RED);
return;
}
@@ -805,12 +883,13 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) {
EMS_RxTelegram.dest = telegram[1] & 0x7F; // remove 8th bit (don't care if read or write)
EMS_RxTelegram.offset = telegram[3]; // offset is always 4th byte
- // determing if its normal ems or ems plus
+ // determing if its normal ems or ems plus, check for marker
if (telegram[2] >= 0xF0) {
// its EMS plus / EMS 2.0
- EMS_RxTelegram.emsplus = true;
+ EMS_RxTelegram.emsplus = true;
+ EMS_RxTelegram.emsplus_type = telegram[2]; // 0xFF, 0xF7 or 0xF9
- if (telegram[2] == 0xFF) {
+ if (EMS_RxTelegram.emsplus_type == 0xFF) {
EMS_RxTelegram.type = (telegram[4] << 8) + telegram[5]; // is a long in bytes 5 & 6
EMS_RxTelegram.data = telegram + 6;
@@ -840,7 +919,7 @@ void ems_parseTelegram(uint8_t * telegram, uint8_t length) {
// if we are in raw logging mode then just print out the telegram as it is
// but still continue to process it
- if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_RAW) {
+ if ((EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_RAW)) {
_debugPrintTelegram("", &EMS_RxTelegram, COLOR_WHITE, true);
}
@@ -1026,7 +1105,7 @@ void _removeTxQueue() {
}
/**
- * deciphers the telegram packet, which has already been checked for valid CRC and has a complete header (min of 5 bytes)
+ * deciphers the telegram packet, which has already been checked for valid CRC and has a complete header
* length is only data bytes, excluding the BRK
* We only remove from the Tx queue if the read or write was successful
*/
@@ -1035,7 +1114,8 @@ void _processType(_EMS_RxTelegram * EMS_RxTelegram) {
// if its an echo of ourselves from the master UBA, ignore. This should never happen mind you
if (EMS_RxTelegram->src == EMS_ID_ME) {
- // _debugPrintTelegram("echo:", EMS_RxTelegram, COLOR_WHITE);
+ if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_JABBER)
+ _debugPrintTelegram("echo:", EMS_RxTelegram, COLOR_WHITE);
return;
}
@@ -1048,8 +1128,8 @@ void _processType(_EMS_RxTelegram * EMS_RxTelegram) {
// release the lock on the TxQueue
EMS_Sys_Status.emsTxStatus = EMS_TX_STATUS_IDLE;
- // at this point we can assume Txstatus is EMS_TX_STATUS_WAIT so we just sent a read/write/validate
- // for READ, WRITE or VALIDATE the dest (telegram[1]) is always us, so check for this
+ // at this point we can assume TxStatus is EMS_TX_STATUS_WAIT as we just sent a read or validate
+ // for READ or VALIDATE the dest (telegram[1]) is always us, so check for this
// and if not we probably didn't get any response so remove the last Tx from the queue and process the telegram anyway
if ((telegram[1] & 0x7F) != EMS_ID_ME) {
_removeTxQueue();
@@ -1057,7 +1137,7 @@ void _processType(_EMS_RxTelegram * EMS_RxTelegram) {
return;
}
- // first double check we actually have something in the queue
+ // first double check we actually have something in the Tx queue that we're waiting upon
if (EMS_TxQueue.isEmpty()) {
_ems_processTelegram(EMS_RxTelegram);
return;
@@ -1067,7 +1147,7 @@ void _processType(_EMS_RxTelegram * EMS_RxTelegram) {
_EMS_TxTelegram EMS_TxTelegram = EMS_TxQueue.first();
// check action
- // if READ, match the current inbound telegram to what we sent
+ // if READ, match the current inbound telegram to what we just sent
// if WRITE, should not happen
// if VALIDATE, check the contents
if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_READ) {
@@ -1076,10 +1156,10 @@ void _processType(_EMS_RxTelegram * EMS_RxTelegram) {
// all checks out, read was successful, remove tx from queue and continue to process telegram
_removeTxQueue();
EMS_Sys_Status.emsRxPgks++; // increment Rx happy counter
+ EMS_Sys_Status.emsTxCapable = true; // we're able to transmit a telegram on the Tx
ems_setEmsRefreshed(EMS_TxTelegram.forceRefresh); // does mqtt need refreshing?
} else {
- // read not OK, we didn't get back a telegram we expected
-
+ // read not OK, we didn't get back a telegram we expected.
// first see if we got a response back from the sender saying its an unknown command
if (EMS_RxTelegram->data_length == 0) {
_removeTxQueue();
@@ -1089,12 +1169,12 @@ void _processType(_EMS_RxTelegram * EMS_RxTelegram) {
// if retried too many times, give up and remove it
if (EMS_Sys_Status.txRetryCount >= TX_WRITE_TIMEOUT_COUNT) {
if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) {
- myDebug_P(PSTR("Read failed. Giving up, removing from queue"));
+ myDebug_P(PSTR("-> Read failed. Giving up and removing write from queue"));
}
_removeTxQueue();
} else {
if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) {
- myDebug_P(PSTR("Read failed. Retrying attempt %d/%d..."), EMS_Sys_Status.txRetryCount, TX_WRITE_TIMEOUT_COUNT);
+ myDebug_P(PSTR("-> Read failed. Retrying (%d/%d)..."), EMS_Sys_Status.txRetryCount, TX_WRITE_TIMEOUT_COUNT);
}
}
}
@@ -1104,49 +1184,53 @@ void _processType(_EMS_RxTelegram * EMS_RxTelegram) {
if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_WRITE) {
// should not get here, since this is handled earlier receiving a 01 or 04
- myDebug_P(PSTR("** Error! Write - should not be here"));
+ myDebug_P(PSTR("-> Write error - panic! should never get here"));
}
if (EMS_TxTelegram.action == EMS_TX_TELEGRAM_VALIDATE) {
// this is a read telegram which we use to validate the last write
- uint8_t * data = telegram + 4; // data block starts at position 5
- uint8_t dataReceived = data[0]; // only a single byte is returned after a read
+
+ // data block starts at position 5 for EMS1.0 and 6 for EMS2.0.
+ // See https://github.com/proddy/EMS-ESP/wiki/RC3xx-Thermostats
+ uint8_t dataReceived = (EMS_RxTelegram->emsplus) ? telegram[6] : telegram[4];
+
if (EMS_TxTelegram.comparisonValue == dataReceived) {
// validate was successful, the write changed the value
_removeTxQueue(); // now we can remove the Tx validate command the queue
if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) {
- myDebug_P(PSTR("Write to 0x%02X was successful"), EMS_TxTelegram.dest);
+ myDebug_P(PSTR("-> Validate confirmed, last Write to 0x%02X was successful"), EMS_TxTelegram.dest);
}
// follow up with the post read command
ems_doReadCommand(EMS_TxTelegram.comparisonPostRead, EMS_TxTelegram.dest, true);
} else {
// write failed
if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) {
- myDebug_P(PSTR("Last write failed. Compared set value 0x%02X with received value 0x%02X"), EMS_TxTelegram.comparisonValue, dataReceived);
+ myDebug_P(PSTR("-> Write failed. Compared set value 0x%02X with received value of 0x%02X"), EMS_TxTelegram.comparisonValue, dataReceived);
}
if (++EMS_Sys_Status.txRetryCount > TX_WRITE_TIMEOUT_COUNT) {
if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) {
- myDebug_P(PSTR("Write failed. Giving up, removing from queue"));
+ myDebug_P(PSTR("-> Write failed. Giving up, removing from queue"));
}
_removeTxQueue();
} else {
// retry, turn the validate back into a write and try again
if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) {
- myDebug_P(PSTR("...Retrying write. Attempt %d/%d..."), EMS_Sys_Status.txRetryCount, TX_WRITE_TIMEOUT_COUNT);
+ myDebug_P(PSTR("-> Write didn't work, retrying (%d/%d)..."), EMS_Sys_Status.txRetryCount, TX_WRITE_TIMEOUT_COUNT);
}
EMS_TxTelegram.action = EMS_TX_TELEGRAM_WRITE;
EMS_TxTelegram.dataValue = EMS_TxTelegram.comparisonValue; // restore old value
EMS_TxTelegram.offset = EMS_TxTelegram.comparisonOffset; // restore old value
- EMS_TxQueue.shift(); // remove validate from queue
- EMS_TxQueue.unshift(EMS_TxTelegram); // add back to queue making it next in line
+ EMS_TxTelegram.type = EMS_TxTelegram.type_validate; // restore old value, we swapped them to save the original type
+
+ EMS_TxQueue.shift(); // remove validate from queue
+ EMS_TxQueue.unshift(EMS_TxTelegram); // add back to queue making it next in line
}
}
}
- emsuart_tx_poll(); // send Acknowledgement back to free the EMS bus since we have the telegram
+ ems_tx_pollAck(); // send Acknowledgement back to free the EMS bus since we have the telegram
}
-
/**
* Check if hot tap water or heating is active
* using a quick hack for checking the heating. Selected Flow Temp >= 70
@@ -1306,7 +1390,7 @@ void _process_RC35StatusMessage(_EMS_RxTelegram * EMS_RxTelegram) {
} else {
EMS_Thermostat.curr_roomTemp = _toShort(EMS_OFFSET_RC35StatusMessage_curr);
}
- EMS_Thermostat.day_mode = _bitRead(EMS_OFFSET_RC35Get_mode_day, 1); // get day mode flag
+ EMS_Thermostat.day_mode = _bitRead(EMS_OFFSET_RC35StatusMessage_mode, 1); // get day mode flag
EMS_Thermostat.circuitcalctemp = _toByte(EMS_OFFSET_RC35Set_circuitcalctemp); // 0x48 calculated temperature
@@ -1325,48 +1409,41 @@ void _process_EasyStatusMessage(_EMS_RxTelegram * EMS_RxTelegram) {
}
/**
- * type 0x01A5 - data from the Nefit RC1010 thermostat (0x18) and RC300/310s on 0x10
+ * type 0x01A5 - data from the Nefit RC1010/3000 thermostat (0x18) and RC300/310s on 0x10
* EMS+ messages may come in with different offsets so handle them here
*/
void _process_RCPLUSStatusMessage(_EMS_RxTelegram * EMS_RxTelegram) {
- if (EMS_RxTelegram->offset == 0) {
+ // handle single data values
+ if (EMS_RxTelegram->data_length == 1) {
+ switch (EMS_RxTelegram->offset) {
+ case EMS_OFFSET_RCPLUSStatusMessage_curr: // setpoint target temp
+ EMS_Thermostat.curr_roomTemp = _toShort(0); // value is * 10
+ break;
+ case EMS_OFFSET_RCPLUSStatusMessage_setpoint: // current target temp
+ EMS_Thermostat.setpoint_roomTemp = _toByte(0); // value is * 2
+ break;
+ case EMS_OFFSET_RCPLUSStatusMessage_currsetpoint: // current setpoint temp, e.g. Thermostat -> all, telegram: 10 00 FF 06 01 A5 22
+ EMS_Thermostat.setpoint_roomTemp = _toByte(0); // value is * 2
+ break;
+ case EMS_OFFSET_RCPLUSStatusMessage_mode: // thermostat mode auto/manual
+ // manual : 10 00 FF 0A 01 A5 02
+ // auto : Thermostat -> all, type 0x01A5 telegram: 10 00 FF 0A 01 A5 03
+ EMS_Thermostat.mode = _bitRead(0, 0); // bit 1, mode (auto=1 or manual=0)
+ EMS_Thermostat.day_mode = _bitRead(0, 1); // get day mode flag
+
+ break;
+ }
+ } else if (EMS_RxTelegram->data_length > 20) {
// the whole telegram
// e.g. Thermostat -> all, telegram: 10 00 FF 00 01 A5 00 D7 21 00 00 00 00 30 01 84 01 01 03 01 84 01 F1 00 00 11 01 00 08 63 00
// 10 00 FF 00 01 A5 80 00 01 30 28 00 30 28 01 54 03 03 01 01 54 02 A8 00 00 11 01 03 FF FF 00
EMS_Thermostat.curr_roomTemp = _toShort(EMS_OFFSET_RCPLUSStatusMessage_curr); // value is * 10
EMS_Thermostat.setpoint_roomTemp = _toByte(EMS_OFFSET_RCPLUSStatusMessage_setpoint); // value is * 2
-
- EMS_Thermostat.day_mode = _bitRead(EMS_OFFSET_RCPLUSGet_mode_day, 1); // get day mode flag
-
- EMS_Thermostat.mode = _bitRead(EMS_OFFSET_RCPLUSStatusMessage_mode, 0); // bit 1, mode (auto=1 or manual=0)
+ EMS_Thermostat.day_mode = _bitRead(EMS_OFFSET_RCPLUSStatusMessage_mode, 1); // get day mode flag
+ EMS_Thermostat.mode = _bitRead(EMS_OFFSET_RCPLUSStatusMessage_mode, 0); // bit 1, mode (auto=1 or manual=0)
}
- // actual set point
- // e.g. Thermostat -> all, telegram: 10 00 FF 07 01 A5 32
- if (EMS_RxTelegram->offset == 7) {
- // to add...
- }
-
- // next set point
- // e.g. Thermostat -> all, telegram: 18 00 FF 06 01 A5 22
- if (EMS_RxTelegram->offset == 6) {
- // to add...
- }
-
- // thermostat mode auto/manual, examples:
- // manual : 10 00 FF 0A 01 A5 02 (CRC=16) #data=1
- // auto : Thermostat -> all, type 0x01A5 telegram: 10 00 FF 0A 01 A5 03 (CRC=17) #data=1
- if (EMS_RxTelegram->offset == EMS_OFFSET_RCPLUSStatusMessage_mode) {
- EMS_Thermostat.mode = _bitRead(0, 0); // bit 0
- }
-}
-
-/**
- * type 0x01B9 - heating data from the Nefit RC1010 thermostat (0x18) and RC300/310s on 0x10
- */
-void _process_RCPLUSStatusHeating(_EMS_RxTelegram * EMS_RxTelegram) {
- // see wiki
- // operation mode, comfort levels 1,2,3, eco level
+ EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT
}
/**
@@ -1389,10 +1466,39 @@ void _process_JunkersStatusMessage(_EMS_RxTelegram * EMS_RxTelegram) {
}
/**
- * to complete....
+ * type 0x01B9 EMS+ for reading the mode from RC300/RC310 thermostat
*/
void _process_RCPLUSSetMessage(_EMS_RxTelegram * EMS_RxTelegram) {
- // to complete
+ // ignore F7 and F9
+ if (EMS_RxTelegram->emsplus_type != 0xFF) {
+ return;
+ }
+
+ // check for single values
+ // but ignore values of 0xFF, e.g. 10 00 FF 08 01 B9 FF
+ if ((EMS_RxTelegram->data_length == 1) && (_toByte(0) != 0xFF)) {
+ // check for setpoint temps, e.g. Thermostat -> all, type 0x01B9, telegram: 10 00 FF 08 01 B9 26 (CRC=1A) #data=1
+
+ if ((EMS_RxTelegram->offset == EMS_OFFSET_RCPLUSSet_temp_setpoint) || (EMS_RxTelegram->offset == EMS_OFFSET_RCPLUSSet_manual_setpoint)) {
+ EMS_Thermostat.setpoint_roomTemp = _toByte(0); // value is * 2
+ EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT
+
+ // check for mode, eg. 10 00 FF 08 01 B9 FF
+ } else if (EMS_RxTelegram->offset == EMS_OFFSET_RCPLUSSet_mode) {
+ EMS_Thermostat.mode = (_toByte(0) == 0xFF); // Auto = xFF, Manual = x00 (auto=1 or manual=0)
+ EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT
+ }
+
+ return; // quit
+ }
+
+ // check for long broadcasts
+ if (EMS_RxTelegram->offset == 0) {
+ EMS_Thermostat.mode = _toByte(EMS_OFFSET_RCPLUSSet_mode);
+ EMS_Thermostat.daytemp = _toByte(EMS_OFFSET_RCPLUSSet_temp_comfort2); // is * 2
+ EMS_Thermostat.nighttemp = _toByte(EMS_OFFSET_RCPLUSSet_temp_eco); // is * 2
+ EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back via MQTT
+ }
}
/**
@@ -1617,17 +1723,19 @@ void ems_clearDeviceList() {
/*
* add an EMS device to our list of detected devices
+ * model_type : 1=boiler, 2=thermostat, 3=sm, 4=other, 5=unknown
*/
-void _addDevice(uint8_t product_id, uint8_t device_id, char * version, const char * model_string) {
- _Generic_Type device;
+void _addDevice(uint8_t model_type, uint8_t product_id, uint8_t device_id, char * version, const char * model_string) {
+ _Generic_Device device;
// if its a duplicate don't add
bool found = false;
- for (std::list<_Generic_Type>::iterator it = Devices.begin(); it != Devices.end(); it++) {
+ for (std::list<_Generic_Device>::iterator it = Devices.begin(); it != Devices.end(); it++) {
if (((it)->product_id == product_id) && ((it)->device_id == device_id)) {
found = true;
}
}
if (!found) {
+ device.model_type = model_type;
device.product_id = product_id;
device.device_id = device_id;
strlcpy(device.version, version, sizeof(device.version));
@@ -1654,8 +1762,8 @@ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) {
// see if its a known boiler
int i = 0;
bool typeFound = false;
- while (i < _Boiler_Types_max) {
- if ((Boiler_Types[i].product_id == product_id) && ((EMS_RxTelegram->src & 0x7F) == EMS_ID_BOILER)) {
+ while (i < _Boiler_Devices_max) {
+ if ((Boiler_Devices[i].product_id == product_id) && ((EMS_RxTelegram->src & 0x7F) == EMS_ID_BOILER)) {
typeFound = true; // we have a matching product id. i is the index.
break;
}
@@ -1664,31 +1772,30 @@ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) {
if (typeFound) {
// its a boiler
- myDebug_P(PSTR("Boiler found: %s (DeviceID:0x%02X ProductID:%d Version:%s)"), Boiler_Types[i].model_string, EMS_ID_BOILER, product_id, version);
+ myDebug_P(PSTR("Boiler found: %s (DeviceID:0x%02X ProductID:%d Version:%s)"), Boiler_Devices[i].model_string, EMS_ID_BOILER, product_id, version);
// add to list
- _addDevice(product_id, EMS_ID_BOILER, version, Boiler_Types[i].model_string);
+ _addDevice(EMS_MODELTYPE_BOILER, product_id, EMS_ID_BOILER, version, Boiler_Devices[i].model_string); // type 1 = boiler
// if its a boiler set it, unless it already has been set by checking for a productID
// it will take the first one found in the list
if ((EMS_Boiler.device_id == EMS_ID_NONE) || ((EMS_Boiler.device_id == EMS_ID_BOILER) && EMS_Boiler.product_id == EMS_ID_NONE)) {
myDebug_P(PSTR("* Setting Boiler to model %s (DeviceID:0x%02X ProductID:%d Version:%s)"),
- Boiler_Types[i].model_string,
+ Boiler_Devices[i].model_string,
EMS_ID_BOILER,
product_id,
version);
EMS_Boiler.device_id = EMS_ID_BOILER;
- EMS_Boiler.product_id = Boiler_Types[i].product_id;
+ EMS_Boiler.product_id = Boiler_Devices[i].product_id;
strlcpy(EMS_Boiler.version, version, sizeof(EMS_Boiler.version));
// check to see if its a Junkers Heatronic3, which has a different poll'ing logic
if (EMS_Boiler.product_id == EMS_PRODUCTID_HEATRONICS) {
- EMS_Sys_Status.emsReverse = true;
+ EMS_Sys_Status.emsIDMask = 0x80;
+ EMS_Sys_Status.emsPollAck[0] = EMS_ID_ME ^ EMS_Sys_Status.emsIDMask;
}
- myESP.fs_saveConfig(); // save config to SPIFFS
-
ems_getBoilerValues(); // get Boiler values that we would usually have to wait for
}
return;
@@ -1696,8 +1803,8 @@ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) {
// its not a boiler, maybe its a known thermostat?
i = 0;
- while (i < _Thermostat_Types_max) {
- if (Thermostat_Types[i].product_id == product_id) {
+ while (i < _Thermostat_Devices_max) {
+ if (Thermostat_Devices[i].product_id == product_id) {
typeFound = true; // we have a matching product id. i is the index.
break;
}
@@ -1708,33 +1815,31 @@ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) {
// its a known thermostat
if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) {
myDebug_P(PSTR("Thermostat found: %s (DeviceID:0x%02X ProductID:%d Version:%s)"),
- Thermostat_Types[i].model_string,
- Thermostat_Types[i].device_id,
+ Thermostat_Devices[i].model_string,
+ Thermostat_Devices[i].device_id,
product_id,
version);
}
// add to list
- _addDevice(product_id, Thermostat_Types[i].device_id, version, Thermostat_Types[i].model_string);
+ _addDevice(EMS_MODELTYPE_THERMOSTAT, product_id, Thermostat_Devices[i].device_id, version, Thermostat_Devices[i].model_string); // type 2 = thermostat
// if we don't have a thermostat set, use this one
if (((EMS_Thermostat.device_id == EMS_ID_NONE) || (EMS_Thermostat.model_id == EMS_MODEL_NONE)
- || (EMS_Thermostat.device_id == Thermostat_Types[i].device_id))
+ || (EMS_Thermostat.device_id == Thermostat_Devices[i].device_id))
&& EMS_Thermostat.product_id == EMS_ID_NONE) {
myDebug_P(PSTR("* Setting Thermostat to %s (DeviceID:0x%02X ProductID:%d Version:%s)"),
- Thermostat_Types[i].model_string,
- Thermostat_Types[i].device_id,
+ Thermostat_Devices[i].model_string,
+ Thermostat_Devices[i].device_id,
product_id,
version);
- EMS_Thermostat.model_id = Thermostat_Types[i].model_id;
- EMS_Thermostat.device_id = Thermostat_Types[i].device_id;
- EMS_Thermostat.write_supported = Thermostat_Types[i].write_supported;
+ EMS_Thermostat.model_id = Thermostat_Devices[i].model_id;
+ EMS_Thermostat.device_id = Thermostat_Devices[i].device_id;
+ EMS_Thermostat.write_supported = Thermostat_Devices[i].write_supported;
EMS_Thermostat.product_id = product_id;
strlcpy(EMS_Thermostat.version, version, sizeof(EMS_Thermostat.version));
- myESP.fs_saveConfig(); // save config to SPIFFS
-
// get Thermostat values (if supported)
ems_getThermostatValues();
}
@@ -1744,8 +1849,8 @@ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) {
// look for Solar Modules
i = 0;
- while (i < _Solar_Module_Types_max) {
- if (SolarModule_Types[i].product_id == product_id) {
+ while (i < _SolarModule_Types_max) {
+ if (SolarModule_Devices[i].product_id == product_id) {
typeFound = true; // we have a matching product id. i is the index.
break;
}
@@ -1754,16 +1859,16 @@ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) {
if (typeFound) {
myDebug_P(PSTR("Solar Module found: %s (DeviceID:0x%02X ProductID:%d Version:%s)"),
- SolarModule_Types[i].model_string,
- SolarModule_Types[i].device_id,
+ SolarModule_Devices[i].model_string,
+ SolarModule_Devices[i].device_id,
product_id,
version);
// add to list
- _addDevice(product_id, SolarModule_Types[i].device_id, version, SolarModule_Types[i].model_string);
+ _addDevice(EMS_MODELTYPE_SM, product_id, SolarModule_Devices[i].device_id, version, SolarModule_Devices[i].model_string); // type 3 = other
myDebug_P(PSTR("Solar Module support enabled."));
- EMS_SolarModule.device_id = SolarModule_Types[i].device_id;
+ EMS_SolarModule.device_id = SolarModule_Devices[i].device_id;
EMS_SolarModule.product_id = product_id;
strlcpy(EMS_SolarModule.version, version, sizeof(EMS_SolarModule.version));
@@ -1774,8 +1879,8 @@ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) {
// look for heatpumps
i = 0;
- while (i < _HeatPump_Types_max) {
- if (HeatPump_Types[i].product_id == product_id) {
+ while (i < _HeatPump_Devices_max) {
+ if (HeatPump_Devices[i].product_id == product_id) {
typeFound = true; // we have a matching product id. i is the index.
break;
}
@@ -1784,16 +1889,16 @@ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) {
if (typeFound) {
myDebug_P(PSTR("Heat Pump found: %s (DeviceID:0x%02X ProductID:%d Version:%s)"),
- HeatPump_Types[i].model_string,
- HeatPump_Types[i].device_id,
+ HeatPump_Devices[i].model_string,
+ HeatPump_Devices[i].device_id,
product_id,
version);
// add to list
- _addDevice(product_id, HeatPump_Types[i].device_id, version, HeatPump_Types[i].model_string);
+ _addDevice(EMS_MODELTYPE_HP, product_id, HeatPump_Devices[i].device_id, version, HeatPump_Devices[i].model_string); // type 3 = other
myDebug_P(PSTR("Heat Pump support enabled."));
- EMS_HeatPump.device_id = SolarModule_Types[i].device_id;
+ EMS_HeatPump.device_id = SolarModule_Devices[i].device_id;
EMS_HeatPump.product_id = product_id;
strlcpy(EMS_HeatPump.version, version, sizeof(EMS_HeatPump.version));
return;
@@ -1801,8 +1906,8 @@ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) {
// finally look for the other EMS devices
i = 0;
- while (i < _Other_Types_max) {
- if (Other_Types[i].product_id == product_id) {
+ while (i < _Other_Devices_max) {
+ if (Other_Devices[i].product_id == product_id) {
typeFound = true; // we have a matching product id. i is the index.
break;
}
@@ -1810,15 +1915,15 @@ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) {
}
if (typeFound) {
- myDebug_P(PSTR("Device found: %s (DeviceID:0x%02X ProductID:%d Version:%s)"), Other_Types[i].model_string, Other_Types[i].device_id, product_id, version);
+ myDebug_P(PSTR("Device found: %s (DeviceID:0x%02X ProductID:%d Version:%s)"), Other_Devices[i].model_string, Other_Devices[i].device_id, product_id, version);
// add to list
- _addDevice(product_id, Other_Types[i].device_id, version, Other_Types[i].model_string);
+ _addDevice(EMS_MODELTYPE_OTHER, product_id, Other_Devices[i].device_id, version, Other_Devices[i].model_string); // type 3 = other
return;
} else {
myDebug_P(PSTR("Unrecognized device found: %s (DeviceID:0x%02X ProductID:%d Version:%s)"), EMS_RxTelegram->src, product_id, version);
// add to list
- _addDevice(product_id, EMS_RxTelegram->src, version, "unknown?");
+ _addDevice(EMS_MODELTYPE_OTHER, product_id, EMS_RxTelegram->src, version, "unknown?"); // type 4 = unknown
}
}
@@ -1827,9 +1932,11 @@ void _process_Version(_EMS_RxTelegram * EMS_RxTelegram) {
* Do a read command for the version with the src having the MSB set
*/
void _ems_detectJunkers() {
+#ifdef JUNKERS_DETECT
char s[30] = {0};
snprintf(s, sizeof(s), "%02X %02X %02X 00 %02X", (EMS_ID_ME | 0x80), (EMS_ID_BOILER | 0x080), EMS_TYPE_Version, EMS_MAX_TELEGRAM_LENGTH);
ems_sendRawTelegram(s);
+#endif
}
/*
@@ -1921,39 +2028,39 @@ void ems_getThermostatValues() {
return;
}
- uint8_t model_id = EMS_Thermostat.model_id;
- uint8_t type = EMS_Thermostat.device_id;
- uint8_t hc = EMS_Thermostat.hc;
+ uint8_t model_id = EMS_Thermostat.model_id;
+ uint8_t device_id = EMS_Thermostat.device_id;
+ uint8_t hc = EMS_Thermostat.hc;
switch (model_id) {
case EMS_MODEL_RC20:
- ems_doReadCommand(EMS_TYPE_RC20StatusMessage, type); // to get the temps
- ems_doReadCommand(EMS_TYPE_RC20Set, type); // to get the mode
+ ems_doReadCommand(EMS_TYPE_RC20StatusMessage, device_id); // to get the temps
+ ems_doReadCommand(EMS_TYPE_RC20Set, device_id); // to get the mode
break;
case EMS_MODEL_RC30:
- ems_doReadCommand(EMS_TYPE_RC30StatusMessage, type); // to get the temps
- ems_doReadCommand(EMS_TYPE_RC30Set, type); // to get the mode
+ ems_doReadCommand(EMS_TYPE_RC30StatusMessage, device_id); // to get the temps
+ ems_doReadCommand(EMS_TYPE_RC30Set, device_id); // to get the mode
break;
case EMS_MODEL_EASY:
- ems_doReadCommand(EMS_TYPE_EasyStatusMessage, type);
+ ems_doReadCommand(EMS_TYPE_EasyStatusMessage, device_id);
break;
case EMS_MODEL_RC35:
case EMS_MODEL_ES73:
if (hc == 1) {
- ems_doReadCommand(EMS_TYPE_RC35StatusMessage_HC1, type); // to get the temps
- ems_doReadCommand(EMS_TYPE_RC35Set_HC1, type); // to get the mode
+ ems_doReadCommand(EMS_TYPE_RC35StatusMessage_HC1, device_id); // to get the temps
+ ems_doReadCommand(EMS_TYPE_RC35Set_HC1, device_id); // to get the mode
} else if (hc == 2) {
- ems_doReadCommand(EMS_TYPE_RC35StatusMessage_HC2, type); // to get the temps
- ems_doReadCommand(EMS_TYPE_RC35Set_HC2, type); // to get the mode
+ ems_doReadCommand(EMS_TYPE_RC35StatusMessage_HC2, device_id); // to get the temps
+ ems_doReadCommand(EMS_TYPE_RC35Set_HC2, device_id); // to get the mode
}
break;
case EMS_MODEL_RC300:
- ems_doReadCommand(EMS_TYPE_RCPLUSStatusMessage, type);
+ ems_doReadCommand(EMS_TYPE_RCPLUSStatusMessage, device_id);
default:
break;
}
- ems_doReadCommand(EMS_TYPE_RCTime, type); // get Thermostat time
+ ems_doReadCommand(EMS_TYPE_RCTime, device_id); // get Thermostat time
}
/**
@@ -1986,7 +2093,7 @@ void ems_getSolarModuleValues() {
* returns current thermostat type as a string
* by looking up the product_id
*/
-char * ems_getThermostatDescription(char * buffer) {
+char * ems_getThermostatDescription(char * buffer, bool name_only) {
uint8_t size = 128;
if (!ems_getThermostatEnabled()) {
strlcpy(buffer, "", size);
@@ -1996,8 +2103,8 @@ char * ems_getThermostatDescription(char * buffer) {
char tmp[6] = {0};
// scan through known ID types
- while (i < _Thermostat_Types_max) {
- if (Thermostat_Types[i].product_id == EMS_Thermostat.product_id) {
+ while (i < _Thermostat_Devices_max) {
+ if (Thermostat_Devices[i].product_id == EMS_Thermostat.product_id) {
found = true; // we have a match
break;
}
@@ -2005,7 +2112,10 @@ char * ems_getThermostatDescription(char * buffer) {
}
if (found) {
- strlcpy(buffer, Thermostat_Types[i].model_string, size);
+ strlcpy(buffer, Thermostat_Devices[i].model_string, size);
+ if (name_only) {
+ return buffer; // only interested in the model name
+ }
} else {
strlcpy(buffer, "DeviceID: 0x", size);
strlcat(buffer, _hextoa(EMS_Thermostat.device_id, tmp), size);
@@ -2028,7 +2138,7 @@ char * ems_getThermostatDescription(char * buffer) {
/**
* returns current boiler type as a string
*/
-char * ems_getBoilerDescription(char * buffer) {
+char * ems_getBoilerDescription(char * buffer, bool name_only) {
uint8_t size = 128;
if (!ems_getBoilerEnabled()) {
strlcpy(buffer, "", size);
@@ -2038,15 +2148,18 @@ char * ems_getBoilerDescription(char * buffer) {
char tmp[6] = {0};
// scan through known ID types
- while (i < _Boiler_Types_max) {
- if (Boiler_Types[i].product_id == EMS_Boiler.product_id) {
+ while (i < _Boiler_Devices_max) {
+ if (Boiler_Devices[i].product_id == EMS_Boiler.product_id) {
found = true; // we have a match
break;
}
i++;
}
if (found) {
- strlcpy(buffer, Boiler_Types[i].model_string, size);
+ strlcpy(buffer, Boiler_Devices[i].model_string, size);
+ if (name_only) {
+ return buffer; // only interested in the model name
+ }
} else {
strlcpy(buffer, "DeviceID: 0x", size);
strlcat(buffer, _hextoa(EMS_Boiler.device_id, tmp), size);
@@ -2069,7 +2182,7 @@ char * ems_getBoilerDescription(char * buffer) {
/**
* returns current Solar Module type as a string
*/
-char * ems_getSolarModuleDescription(char * buffer) {
+char * ems_getSolarModuleDescription(char * buffer, bool name_only) {
uint8_t size = 128;
if (!ems_getSolarModuleEnabled()) {
strlcpy(buffer, "", size);
@@ -2079,15 +2192,18 @@ char * ems_getSolarModuleDescription(char * buffer) {
char tmp[6] = {0};
// scan through known ID types
- while (i < _Solar_Module_Types_max) {
- if (SolarModule_Types[i].product_id == EMS_SolarModule.product_id) {
+ while (i < _SolarModule_Types_max) {
+ if (SolarModule_Devices[i].product_id == EMS_SolarModule.product_id) {
found = true; // we have a match
break;
}
i++;
}
if (found) {
- strlcpy(buffer, SolarModule_Types[i].model_string, size);
+ strlcpy(buffer, SolarModule_Devices[i].model_string, size);
+ if (name_only) {
+ return buffer; // only interested in the model name
+ }
} else {
strlcpy(buffer, "DeviceID: 0x", size);
strlcat(buffer, _hextoa(EMS_SolarModule.device_id, tmp), size);
@@ -2110,7 +2226,7 @@ char * ems_getSolarModuleDescription(char * buffer) {
/**
* returns current Heat Pump type as a string
*/
-char * ems_getHeatPumpDescription(char * buffer) {
+char * ems_getHeatPumpDescription(char * buffer, bool name_only) {
uint8_t size = 128;
if (!ems_getHeatPumpEnabled()) {
strlcpy(buffer, "", size);
@@ -2120,15 +2236,18 @@ char * ems_getHeatPumpDescription(char * buffer) {
char tmp[6] = {0};
// scan through known ID types
- while (i < _HeatPump_Types_max) {
- if (HeatPump_Types[i].product_id == EMS_HeatPump.product_id) {
+ while (i < _HeatPump_Devices_max) {
+ if (HeatPump_Devices[i].product_id == EMS_HeatPump.product_id) {
found = true; // we have a match
break;
}
i++;
}
if (found) {
- strlcpy(buffer, HeatPump_Types[i].model_string, size);
+ strlcpy(buffer, HeatPump_Devices[i].model_string, size);
+ if (name_only) {
+ return buffer; // only interested in the model name
+ }
} else {
strlcpy(buffer, "DeviceID: 0x", size);
strlcat(buffer, _hextoa(EMS_HeatPump.device_id, tmp), size);
@@ -2160,17 +2279,17 @@ void ems_scanDevices() {
Device_Ids.push_back(EMS_ID_BOILER);
// copy over thermostats
- for (_Thermostat_Type tt : Thermostat_Types) {
+ for (_Thermostat_Device tt : Thermostat_Devices) {
Device_Ids.push_back(tt.device_id);
}
// copy over solar modules
- for (_SolarModule_Type sm : SolarModule_Types) {
+ for (_SolarModule_Device sm : SolarModule_Devices) {
Device_Ids.push_back(sm.device_id);
}
// copy over others
- for (_Other_Type ot : Other_Types) {
+ for (_Other_Device ot : Other_Devices) {
Device_Ids.push_back(ot.device_id);
}
@@ -2194,34 +2313,34 @@ void ems_scanDevices() {
void ems_printAllDevices() {
uint8_t i;
- myDebug_P(PSTR("\nThese %d devices are supported as boiler units:"), _Boiler_Types_max);
- for (i = 0; i < _Boiler_Types_max; i++) {
+ myDebug_P(PSTR("\nThese %d devices are supported as boiler units:"), _Boiler_Devices_max);
+ for (i = 0; i < _Boiler_Devices_max; i++) {
myDebug_P(PSTR(" %s%s%s (DeviceID:0x%02X ProductID:%d)"),
COLOR_BOLD_ON,
- Boiler_Types[i].model_string,
+ Boiler_Devices[i].model_string,
COLOR_BOLD_OFF,
EMS_ID_BOILER,
- Boiler_Types[i].product_id);
+ Boiler_Devices[i].product_id);
}
- myDebug_P(PSTR("\nThese %d devices are supported as solar module devices:"), _Solar_Module_Types_max);
- for (i = 0; i < _Solar_Module_Types_max; i++) {
+ myDebug_P(PSTR("\nThese %d devices are supported as solar module devices:"), _SolarModule_Types_max);
+ for (i = 0; i < _SolarModule_Types_max; i++) {
myDebug_P(PSTR(" %s%s%s (DeviceID:0x%02X ProductID:%d)"),
COLOR_BOLD_ON,
- SolarModule_Types[i].model_string,
+ SolarModule_Devices[i].model_string,
COLOR_BOLD_OFF,
- SolarModule_Types[i].device_id,
- SolarModule_Types[i].product_id);
+ SolarModule_Devices[i].device_id,
+ SolarModule_Devices[i].product_id);
}
- myDebug_P(PSTR("\nThese %d devices are supported as other known EMS devices:"), _Other_Types_max);
- for (i = 0; i < _Other_Types_max; i++) {
+ myDebug_P(PSTR("\nThese %d devices are supported as other known EMS devices:"), _Other_Devices_max);
+ for (i = 0; i < _Other_Devices_max; i++) {
myDebug_P(PSTR(" %s%s%s (DeviceID:0x%02X ProductID:%d)"),
COLOR_BOLD_ON,
- Other_Types[i].model_string,
+ Other_Devices[i].model_string,
COLOR_BOLD_OFF,
- Other_Types[i].device_id,
- Other_Types[i].product_id);
+ Other_Devices[i].device_id,
+ Other_Devices[i].product_id);
}
myDebug_P(PSTR("\nThe following telegram type IDs are supported:"));
@@ -2231,15 +2350,15 @@ void ems_printAllDevices() {
}
}
- myDebug_P(PSTR("\nThese %d thermostat devices are supported:"), _Thermostat_Types_max);
- for (i = 0; i < _Thermostat_Types_max; i++) {
+ myDebug_P(PSTR("\nThese %d thermostat devices are supported:"), _Thermostat_Devices_max);
+ for (i = 0; i < _Thermostat_Devices_max; i++) {
myDebug_P(PSTR(" %s%s%s (DeviceID:0x%02X ProductID:%d) can write:%c"),
COLOR_BOLD_ON,
- Thermostat_Types[i].model_string,
+ Thermostat_Devices[i].model_string,
COLOR_BOLD_OFF,
- Thermostat_Types[i].device_id,
- Thermostat_Types[i].product_id,
- (Thermostat_Types[i].write_supported) ? 'y' : 'n');
+ Thermostat_Devices[i].device_id,
+ Thermostat_Devices[i].product_id,
+ (Thermostat_Devices[i].write_supported) ? 'y' : 'n');
}
// print out known devices
@@ -2254,7 +2373,7 @@ void ems_printAllDevices() {
void ems_printDevices() {
if (Devices.size() != 0) {
myDebug_P(PSTR("\nThese %d EMS devices were detected:"), Devices.size());
- for (std::list<_Generic_Type>::iterator it = Devices.begin(); it != Devices.end(); it++) {
+ for (std::list<_Generic_Device>::iterator it = Devices.begin(); it != Devices.end(); it++) {
myDebug_P(PSTR(" %s%s%s (DeviceID:0x%02X ProductID:%d Version:%s)"),
COLOR_BOLD_ON,
(it)->model_string,
@@ -2269,23 +2388,6 @@ void ems_printDevices() {
}
}
-/*
- * prints the device list to a string for html parsing
- */
-uint8_t ems_printDevices_s(char * buffer, uint16_t len) {
- if (Devices.size() == 0) {
- return 0;
- }
-
- char s[100];
- for (std::list<_Generic_Type>::iterator it = Devices.begin(); it != Devices.end(); it++) {
- sprintf(s, "%s (DeviceID:0x%02X ProductID:%d Version:%s) ", (it)->model_string, (it)->device_id, (it)->product_id, (it)->version);
- strlcat(buffer, s, len);
- }
-
- return Devices.size();
-}
-
/**
* Send a command to UART Tx to Read from another device
* Read commands when sent must respond by the destination (target) immediately (or within 10ms)
@@ -2342,10 +2444,6 @@ void ems_sendRawTelegram(char * telegram) {
char * p;
char value[10] = {0};
- if (EMS_Sys_Status.emsTxDisabled) {
- return; // user has disabled all Tx
- }
-
_EMS_TxTelegram EMS_TxTelegram = EMS_TX_TELEGRAM_NEW; // create new Tx
EMS_TxTelegram.timestamp = millis(); // set timestamp
EMS_Sys_Status.txRetryCount = 0; // reset retry counter
@@ -2375,7 +2473,6 @@ void ems_sendRawTelegram(char * telegram) {
return; // nothing to send
}
- // calculate length including header and CRC
EMS_TxTelegram.length = count + 2;
EMS_TxTelegram.type_validate = EMS_ID_NONE;
EMS_TxTelegram.action = EMS_TX_TELEGRAM_RAW;
@@ -2394,7 +2491,7 @@ void ems_setThermostatTemp(float temperature, uint8_t temptype) {
}
if (!EMS_Thermostat.write_supported) {
- myDebug_P(PSTR("Write not supported for this model Thermostat"));
+ myDebug_P(PSTR("Write not supported for this Thermostat model"));
return;
}
@@ -2402,29 +2499,47 @@ void ems_setThermostatTemp(float temperature, uint8_t temptype) {
EMS_TxTelegram.timestamp = millis(); // set timestamp
EMS_Sys_Status.txRetryCount = 0; // reset retry counter
- uint8_t model_id = EMS_Thermostat.model_id;
- uint8_t type = EMS_Thermostat.device_id;
- uint8_t hc = EMS_Thermostat.hc; // heating circuit
+ uint8_t model_id = EMS_Thermostat.model_id;
+ uint8_t device_id = EMS_Thermostat.device_id;
+ uint8_t hc = EMS_Thermostat.hc; // heating circuit
EMS_TxTelegram.action = EMS_TX_TELEGRAM_WRITE;
- EMS_TxTelegram.dest = type;
+ EMS_TxTelegram.dest = device_id;
myDebug_P(PSTR("Setting new thermostat temperature"));
- // when doing a comparison to validate the new temperature we call a different type
-
if (model_id == EMS_MODEL_RC20) {
EMS_TxTelegram.type = EMS_TYPE_RC20Set;
EMS_TxTelegram.offset = EMS_OFFSET_RC20Set_temp;
EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC20StatusMessage;
+ EMS_TxTelegram.type_validate = EMS_TxTelegram.type;
+
} else if (model_id == EMS_MODEL_RC10) {
EMS_TxTelegram.type = EMS_TYPE_RC10Set;
EMS_TxTelegram.offset = EMS_OFFSET_RC10Set_temp;
EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC10StatusMessage;
+ EMS_TxTelegram.type_validate = EMS_TxTelegram.type;
+
} else if (model_id == EMS_MODEL_RC30) {
EMS_TxTelegram.type = EMS_TYPE_RC30Set;
EMS_TxTelegram.offset = EMS_OFFSET_RC30Set_temp;
EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC30StatusMessage;
+ EMS_TxTelegram.type_validate = EMS_TxTelegram.type;
+
+ } else if (model_id == EMS_MODEL_RC300) {
+ EMS_TxTelegram.type = EMS_TYPE_RCPLUSSet; // for 3000 and 1010, e.g. 0B 10 FF (0A | 08) 01 89 2B
+ // check mode
+ if (EMS_Thermostat.mode == 1) { // auto
+ EMS_TxTelegram.offset = 0x08; // auto offset
+ } else if (EMS_Thermostat.mode == 0) { // manuaL
+ EMS_TxTelegram.offset = 0x0A; // manual offset
+ }
+ // TODO: commented out because it's too fast
+ // EMS_TxTelegram.type_validate = EMS_TYPE_RCPLUSStatusMessage; // validate by reading from a different telegram
+ EMS_TxTelegram.type_validate = EMS_ID_NONE; // validate by reading from a different telegram
+
+ EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RCPLUSStatusMessage; // after write, do a full fetch of all values
+
} else if ((model_id == EMS_MODEL_RC35) || (model_id == EMS_MODEL_ES73)) {
switch (temptype) {
case 1: // change the night temp
@@ -2445,21 +2560,21 @@ void ems_setThermostatTemp(float temperature, uint8_t temptype) {
}
break;
}
-
if (hc == 1) {
// heating circuit 1
EMS_TxTelegram.type = EMS_TYPE_RC35Set_HC1;
EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC35StatusMessage_HC1;
+ EMS_TxTelegram.type_validate = EMS_TxTelegram.type;
} else {
// heating circuit 2
EMS_TxTelegram.type = EMS_TYPE_RC35Set_HC2;
EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC35StatusMessage_HC2;
+ EMS_TxTelegram.type_validate = EMS_TxTelegram.type;
}
}
EMS_TxTelegram.length = EMS_MIN_TELEGRAM_LENGTH;
- EMS_TxTelegram.dataValue = (uint8_t)((float)temperature * (float)2); // value
- EMS_TxTelegram.type_validate = EMS_TxTelegram.type;
+ EMS_TxTelegram.dataValue = (uint8_t)((float)temperature * (float)2); // value * 2
EMS_TxTelegram.comparisonOffset = EMS_TxTelegram.offset;
EMS_TxTelegram.comparisonValue = EMS_TxTelegram.dataValue;
@@ -2468,8 +2583,9 @@ void ems_setThermostatTemp(float temperature, uint8_t temptype) {
}
/**
- * Set the thermostat working mode (0=low/night, 1=manual/day, 2=auto/clock)
- * 0xA8 on a RC20 and 0xA7 on RC30
+ * Set the thermostat working mode
+ * (0=low/night, 1=manual/day, 2=auto/clock), 0xA8 on a RC20 and 0xA7 on RC30
+ * 0x01B9 for EMS+ 300/1000/3000, Auto=0xFF Manual=0x00. See https://github.com/proddy/EMS-ESP/wiki/RC3xx-Thermostats
*/
void ems_setThermostatMode(uint8_t mode) {
if (!ems_getThermostatEnabled()) {
@@ -2481,9 +2597,18 @@ void ems_setThermostatMode(uint8_t mode) {
return;
}
- uint8_t model_id = EMS_Thermostat.model_id;
- uint8_t type = EMS_Thermostat.device_id;
- uint8_t hc = EMS_Thermostat.hc;
+ uint8_t model_id = EMS_Thermostat.model_id;
+ uint8_t device_id = EMS_Thermostat.device_id;
+ uint8_t hc = EMS_Thermostat.hc;
+
+ // RC300/1000/3000 have different settings
+ if (model_id == EMS_MODEL_RC300) {
+ if (mode == 1) {
+ mode = 0; // manual
+ } else {
+ mode = 0xFF; // auto
+ }
+ }
myDebug_P(PSTR("Setting thermostat mode to %d"), mode);
@@ -2492,27 +2617,46 @@ void ems_setThermostatMode(uint8_t mode) {
EMS_Sys_Status.txRetryCount = 0; // reset retry counter
EMS_TxTelegram.action = EMS_TX_TELEGRAM_WRITE;
- EMS_TxTelegram.dest = type;
+ EMS_TxTelegram.dest = device_id;
EMS_TxTelegram.length = EMS_MIN_TELEGRAM_LENGTH;
EMS_TxTelegram.dataValue = mode;
// handle different thermostat types
if (model_id == EMS_MODEL_RC20) {
- EMS_TxTelegram.type = EMS_TYPE_RC20Set;
- EMS_TxTelegram.offset = EMS_OFFSET_RC20Set_mode;
+ EMS_TxTelegram.type = EMS_TYPE_RC20Set;
+ EMS_TxTelegram.offset = EMS_OFFSET_RC20Set_mode;
+ EMS_TxTelegram.type_validate = EMS_TxTelegram.type;
+ EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC20StatusMessage;
+
} else if (model_id == EMS_MODEL_RC30) {
- EMS_TxTelegram.type = EMS_TYPE_RC30Set;
- EMS_TxTelegram.offset = EMS_OFFSET_RC30Set_mode;
+ EMS_TxTelegram.type = EMS_TYPE_RC30Set;
+ EMS_TxTelegram.offset = EMS_OFFSET_RC30Set_mode;
+ EMS_TxTelegram.type_validate = EMS_TxTelegram.type;
+ EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC30StatusMessage;
+
} else if ((model_id == EMS_MODEL_RC35) || (model_id == EMS_MODEL_ES73)) {
- EMS_TxTelegram.type = (hc == 2) ? EMS_TYPE_RC35Set_HC2 : EMS_TYPE_RC35Set_HC1;
- EMS_TxTelegram.offset = EMS_OFFSET_RC35Set_mode;
+ EMS_TxTelegram.type = (hc == 2) ? EMS_TYPE_RC35Set_HC2 : EMS_TYPE_RC35Set_HC1;
+ EMS_TxTelegram.offset = EMS_OFFSET_RC35Set_mode;
+ EMS_TxTelegram.type_validate = EMS_TxTelegram.type;
+ if (hc == 1) {
+ EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC35StatusMessage_HC1;
+ } else {
+ EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC35StatusMessage_HC2;
+ }
+
+ } else if (model_id == EMS_MODEL_RC300) {
+ EMS_TxTelegram.type = EMS_TYPE_RCPLUSSet; // for 3000 and 1010, e.g. 48 10 FF 00 01 B9 00 for manual
+ EMS_TxTelegram.offset = EMS_OFFSET_RCPLUSSet_mode;
+
+ // TODO: commented out because it's too fast
+ // EMS_TxTelegram.type_validate = EMS_TYPE_RCPLUSStatusMessage; // validate by reading from a different telegram
+ EMS_TxTelegram.type_validate = EMS_ID_NONE; // don't validate after the write
+ EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RCPLUSStatusMessage; // after write, do a full fetch of all values
}
- EMS_TxTelegram.type_validate = EMS_TxTelegram.type; // callback to EMS_TYPE_RC30Temperature to fetch temps
- EMS_TxTelegram.comparisonOffset = EMS_TxTelegram.offset;
- EMS_TxTelegram.comparisonValue = EMS_TxTelegram.dataValue;
- EMS_TxTelegram.comparisonPostRead = EMS_TxTelegram.type;
- EMS_TxTelegram.forceRefresh = false; // send to MQTT is done automatically in 0xA8 process
+ EMS_TxTelegram.comparisonOffset = EMS_TxTelegram.offset;
+ EMS_TxTelegram.comparisonValue = EMS_TxTelegram.dataValue;
+ EMS_TxTelegram.forceRefresh = false; // send to MQTT is done automatically in 0xA8 process
EMS_TxQueue.push(EMS_TxTelegram);
}
@@ -2684,10 +2828,10 @@ void ems_setWarmTapWaterActivated(bool activated) {
/**
* Start up sequence for UBA Master, hopefully to initialize a handshake
- * Still experimental
+ * Still experimental and not used yet!
*/
void ems_startupTelegrams() {
- if ((EMS_Sys_Status.emsTxDisabled) || (!EMS_Sys_Status.emsBusConnected)) {
+ if ((ems_getTxDisabled()) || (!ems_getBusConnected())) {
myDebug_P(PSTR("Unable to send startup sequence when in listen mode or the bus is disabled"));
}
diff --git a/src/ems.h b/src/ems.h
index 800577200..d1e5a12a0 100644
--- a/src/ems.h
+++ b/src/ems.h
@@ -11,12 +11,14 @@
#pragma once
#include
+#include // std::list
/* debug helper for logic analyzer
* create marker puls on GPIOx
* ° for Rx, we use GPIO14
* ° for Tx, we use GPIO12
*/
+
// clang-format off
#ifdef LOGICANALYZER
#define RX_MARK_PIN 14
@@ -29,39 +31,39 @@
#define GPIO_H(mask) (GPIO_REG_WRITE(GPIO_OUT_W1TS_ADDRESS, (mask)))
#define GPIO_L(mask) (GPIO_REG_WRITE(GPIO_OUT_W1TC_ADDRESS, (mask)))
-#define RX_PULSE(pulse) \
- do { \
- GPIO_H(RX_MARK_MASK); \
- delayMicroseconds(pulse); \
- GPIO_L(RX_MARK_MASK); \
+#define RX_PULSE(pulse) \
+ do { \
+ GPIO_H(RX_MARK_MASK); \
+ delayMicroseconds(pulse); \
+ GPIO_L(RX_MARK_MASK); \
} while (0)
-#define TX_PULSE(pulse) \
- do { \
- GPIO_H(TX_MARK_MASK); \
- delayMicroseconds(pulse); \
- GPIO_L(TX_MARK_MASK); \
+#define TX_PULSE(pulse) \
+ do { \
+ GPIO_H(TX_MARK_MASK); \
+ delayMicroseconds(pulse); \
+ GPIO_L(TX_MARK_MASK); \
} while (0)
-#define LA_PULSE(pulse) \
- do { \
- GPIO_H(MARKERS_MASK); \
- delayMicroseconds(pulse); \
- GPIO_L(MARKERS_MASK); \
+#define LA_PULSE(pulse) \
+ do { \
+ GPIO_H(MARKERS_MASK); \
+ delayMicroseconds(pulse); \
+ GPIO_L(MARKERS_MASK); \
} while (0)
-#define INIT_MARKERS(void) \
- do { \
- pinMode(RX_MARK_PIN, OUTPUT); \
- pinMode(TX_MARK_PIN, OUTPUT); \
- GPIO_L(MARKERS_MASK); \
+#define INIT_MARKERS(void) \
+ do { \
+ pinMode(RX_MARK_PIN, OUTPUT); \
+ pinMode(TX_MARK_PIN, OUTPUT); \
+ GPIO_L(MARKERS_MASK); \
} while (0)
#else
-#define RX_PULSE(pulse) \
+#define RX_PULSE(pulse) \
{}
-#define TX_PULSE(pulse) \
+#define TX_PULSE(pulse) \
{}
-#define LA_PULSE(pulse) \
+#define LA_PULSE(pulse) \
{}
-#define INIT_MARKERS(void) \
+#define INIT_MARKERS(void) \
{}
#define RX_MARK_MASK
#define TX_MARK_MASK
@@ -113,6 +115,13 @@
//#define EMS_SYS_LOGGING_DEFAULT EMS_SYS_LOGGING_VERBOSE
#define EMS_SYS_LOGGING_DEFAULT EMS_SYS_LOGGING_NONE
+// define the model types which get rendered to html colors in the web interface
+#define EMS_MODELTYPE_BOILER 1 // success color
+#define EMS_MODELTYPE_THERMOSTAT 2 // info color
+#define EMS_MODELTYPE_SM 3 // warning color
+#define EMS_MODELTYPE_HP 4 // success color
+#define EMS_MODELTYPE_OTHER 5 // no color
+
/* EMS UART transfer status */
typedef enum {
EMS_RX_STATUS_IDLE,
@@ -124,7 +133,8 @@ typedef enum {
EMS_TX_STATUS_IDLE, // ready
EMS_TX_STATUS_WAIT, // waiting for response from last Tx
EMS_TX_WTD_TIMEOUT, // watchdog timeout during send
- EMS_TX_BRK_DETECT // incoming BRK during Tx
+ EMS_TX_BRK_DETECT, // incoming BRK during Tx
+ EMS_TX_REV_DETECT // waiting to detect reverse bit
} _EMS_TX_STATUS;
#define EMS_TX_SUCCESS 0x01 // EMS single byte after a Tx Write indicating a success
@@ -145,15 +155,16 @@ typedef enum {
EMS_SYS_LOGGING_BASIC, // only basic read/write messages
EMS_SYS_LOGGING_THERMOSTAT, // only telegrams sent from thermostat
EMS_SYS_LOGGING_SOLARMODULE, // only telegrams sent from thermostat
- EMS_SYS_LOGGING_VERBOSE // everything
+ EMS_SYS_LOGGING_VERBOSE, // everything
+ EMS_SYS_LOGGING_JABBER // lots of debug output...
} _EMS_SYS_LOGGING;
// status/counters since last power on
typedef struct {
_EMS_RX_STATUS emsRxStatus;
_EMS_TX_STATUS emsTxStatus;
- uint16_t emsRxPgks; // received
- uint16_t emsTxPkgs; // sent
+ uint16_t emsRxPgks; // # successfull received
+ uint16_t emsTxPkgs; // # successfull sent
uint16_t emxCrcErr; // CRC errors
bool emsPollEnabled; // flag enable the response to poll messages
_EMS_SYS_LOGGING emsLogging; // logging
@@ -164,8 +175,9 @@ typedef struct {
bool emsTxCapable; // able to send via Tx
bool emsTxDisabled; // true to prevent all Tx
uint8_t txRetryCount; // # times the last Tx was re-sent
- bool emsReverse; // if true, poll logic is reversed
- uint8_t emsTxMode; // handles Tx logic
+ uint8_t emsIDMask; // Buderus: 0x00, Junkers: 0x80
+ uint8_t emsPollAck[1]; // acknowledge buffer for Poll
+ uint8_t emsTxMode; // Tx mode 1, 2 or 3
} _EMS_Sys_Status;
// The Tx send package
@@ -174,12 +186,12 @@ typedef struct {
uint8_t dest;
uint16_t type;
uint8_t offset;
- uint8_t length; // full length of complete telegram
+ uint8_t length; // full length of complete telegram, including CRC
uint8_t dataValue; // value to validate against
uint16_t type_validate; // type to call after a successful Write command
- uint8_t comparisonValue; // value to compare against during a validate
- uint8_t comparisonOffset; // offset of where the byte is we want to compare too later
- uint16_t comparisonPostRead; // after a successful write call this to read from this type ID
+ uint8_t comparisonValue; // value to compare against during a validate command
+ uint8_t comparisonOffset; // offset of where the byte is we want to compare too during validation
+ uint16_t comparisonPostRead; // after a successful write, do a read from this type ID
bool forceRefresh; // should we send to MQTT after a successful Tx?
uint32_t timestamp; // when created
uint8_t data[EMS_MAX_TELEGRAM_LENGTH];
@@ -187,19 +199,20 @@ typedef struct {
// The Rx receive package
typedef struct {
- uint32_t timestamp; // timestamp from millis()
- uint8_t * telegram; // the full data package
- uint8_t data_length; // length in bytes of the data
- uint8_t length; // full length of the complete telegram
- uint8_t src; // source ID
- uint8_t dest; // destination ID
- uint16_t type; // type ID as a double byte to support EMS+
- uint8_t offset; // offset
- uint8_t * data; // pointer to where telegram data starts
- bool emsplus; // true if ems+/ems 2.0
+ uint32_t timestamp; // timestamp from millis()
+ uint8_t * telegram; // the full data package
+ uint8_t data_length; // length in bytes of the data
+ uint8_t length; // full length of the complete telegram
+ uint8_t src; // source ID
+ uint8_t dest; // destination ID
+ uint16_t type; // type ID as a double byte to support EMS+
+ uint8_t offset; // offset
+ uint8_t * data; // pointer to where telegram data starts
+ bool emsplus; // true if ems+/ems 2.0
+ uint8_t emsplus_type; // FF, F7 or F9
} _EMS_RxTelegram;
-// default empty Tx
+// default empty Tx, must match struct
const _EMS_TxTelegram EMS_TX_TELEGRAM_NEW = {
EMS_TX_TELEGRAM_INIT, // action
EMS_ID_NONE, // dest
@@ -220,25 +233,25 @@ const _EMS_TxTelegram EMS_TX_TELEGRAM_NEW = {
typedef struct {
uint8_t product_id;
char model_string[50];
-} _Boiler_Type;
+} _Boiler_Device;
typedef struct {
uint8_t product_id;
uint8_t device_id;
char model_string[50];
-} _SolarModule_Type;
+} _SolarModule_Device;
typedef struct {
uint8_t product_id;
uint8_t device_id;
char model_string[50];
-} _Other_Type;
+} _Other_Device;
typedef struct {
uint8_t product_id;
uint8_t device_id;
char model_string[50];
-} _HeatPump_Type;
+} _HeatPump_Device;
typedef struct {
uint8_t model_id;
@@ -246,15 +259,16 @@ typedef struct {
uint8_t device_id;
char model_string[50];
bool write_supported;
-} _Thermostat_Type;
+} _Thermostat_Device;
// for consolidating all types
typedef struct {
+ uint8_t model_type; // 1=boiler, 2=thermostat, 3=sm, 4=other, 5=unknown
uint8_t product_id;
uint8_t device_id;
char version[10];
char model_string[50];
-} _Generic_Type;
+} _Generic_Device;
/*
* Telegram package defintions
@@ -390,6 +404,7 @@ typedef struct {
} _EMS_Type;
// function definitions
+extern void ems_dumpBuffer(const char * prefix, uint8_t * telegram, uint8_t length);
extern void ems_parseTelegram(uint8_t * telegram, uint8_t len);
void ems_init();
void ems_doReadCommand(uint16_t type, uint8_t dest, bool forceRefresh = false);
@@ -403,28 +418,27 @@ void ems_testTelegram(uint8_t test_num);
void ems_startupTelegrams();
bool ems_checkEMSBUSAlive();
void ems_clearDeviceList();
-void ems_setTxMode(uint8_t mode);
-void ems_setThermostatTemp(float temperature, uint8_t temptype = 0);
-void ems_setThermostatMode(uint8_t mode);
-void ems_setThermostatHC(uint8_t hc);
-void ems_setWarmWaterTemp(uint8_t temperature);
-void ems_setFlowTemp(uint8_t temperature);
-void ems_setWarmWaterActivated(bool activated);
-void ems_setWarmTapWaterActivated(bool activated);
-void ems_setPoll(bool b);
-void ems_setLogging(_EMS_SYS_LOGGING loglevel);
-void ems_setEmsRefreshed(bool b);
-void ems_setWarmWaterModeComfort(uint8_t comfort);
-void ems_setModels();
-void ems_setTxDisabled(bool b);
-bool ems_getTxDisabled();
-uint8_t ems_getTxMode();
+void ems_setThermostatTemp(float temperature, uint8_t temptype = 0);
+void ems_setThermostatMode(uint8_t mode);
+void ems_setThermostatHC(uint8_t hc);
+void ems_setWarmWaterTemp(uint8_t temperature);
+void ems_setFlowTemp(uint8_t temperature);
+void ems_setWarmWaterActivated(bool activated);
+void ems_setWarmTapWaterActivated(bool activated);
+void ems_setPoll(bool b);
+void ems_setLogging(_EMS_SYS_LOGGING loglevel);
+void ems_setEmsRefreshed(bool b);
+void ems_setWarmWaterModeComfort(uint8_t comfort);
+void ems_setModels();
+void ems_setTxDisabled(bool b);
+bool ems_getTxDisabled();
+void ems_setTxMode(uint8_t mode);
-char * ems_getThermostatDescription(char * buffer);
-char * ems_getBoilerDescription(char * buffer);
-char * ems_getSolarModuleDescription(char * buffer);
-char * ems_getHeatPumpDescription(char * buffer);
+char * ems_getThermostatDescription(char * buffer, bool name_only = false);
+char * ems_getBoilerDescription(char * buffer, bool name_only = false);
+char * ems_getSolarModuleDescription(char * buffer, bool name_only = false);
+char * ems_getHeatPumpDescription(char * buffer, bool name_only = false);
void ems_getThermostatValues();
void ems_getBoilerValues();
void ems_getSolarModuleValues();
@@ -457,3 +471,5 @@ extern _EMS_Thermostat EMS_Thermostat;
extern _EMS_SolarModule EMS_SolarModule;
extern _EMS_HeatPump EMS_HeatPump;
extern _EMS_Other EMS_Other;
+
+extern std::list<_Generic_Device> Devices;
diff --git a/src/ems_devices.h b/src/ems_devices.h
index 40c1d8a07..85273de02 100644
--- a/src/ems_devices.h
+++ b/src/ems_devices.h
@@ -13,12 +13,12 @@
#include "ems.h"
/*
- * Common
+ * Common Type
*/
#define EMS_TYPE_Version 0x02
/*
- * Boiler...
+ * Boiler Telegram Types...
*/
#define EMS_TYPE_UBAMonitorFast 0x18 // is an automatic monitor broadcast
#define EMS_TYPE_UBAMonitorSlow 0x19 // is an automatic monitor broadcast
@@ -40,7 +40,7 @@
#define EMS_OFFSET_UBASetPoints_flowtemp 0 // flow temp
-// Other
+// SM and HP Types
#define EMS_TYPE_SM10Monitor 0x97 // SM10Monitor
#define EMS_TYPE_SM100Monitor 0x0262 // SM100Monitor
#define EMS_TYPE_SM100Status 0x0264 // SM100Status
@@ -54,7 +54,7 @@
#define EMS_OFFSET_ISM1Set_MaxBoilerTemp 6 // position of max boiler temp e.g. 50 in the following example: 90 30 FF 06 00 01 50 (CRC=2C)
/*
- * Thermostats...
+ * Thermostat Types
*/
// Common for all thermostats
@@ -63,41 +63,46 @@
// RC10 specific
#define EMS_TYPE_RC10StatusMessage 0xB1 // is an automatic thermostat broadcast giving us temps
-#define EMS_TYPE_RC10Set 0xB0 // for setting values like temp and mode
-#define EMS_OFFSET_RC10Set_temp 4 // position of thermostat setpoint temperature
#define EMS_OFFSET_RC10StatusMessage_setpoint 1 // setpoint temp
#define EMS_OFFSET_RC10StatusMessage_curr 2 // current temp
+#define EMS_TYPE_RC10Set 0xB0 // for setting values like temp and mode
+#define EMS_OFFSET_RC10Set_temp 4 // position of thermostat setpoint temperature
+
// RC20 specific
#define EMS_TYPE_RC20StatusMessage 0x91 // is an automatic thermostat broadcast giving us temps
-#define EMS_TYPE_RC20Set 0xA8 // for setting values like temp and mode
-#define EMS_OFFSET_RC20Set_mode 23 // position of thermostat mode
-#define EMS_OFFSET_RC20Set_temp 28 // position of thermostat setpoint temperature
#define EMS_OFFSET_RC20StatusMessage_setpoint 1 // setpoint temp
#define EMS_OFFSET_RC20StatusMessage_curr 2 // current temp
+#define EMS_TYPE_RC20Set 0xA8 // for setting values like temp and mode
+#define EMS_OFFSET_RC20Set_mode 23 // position of thermostat mode
+#define EMS_OFFSET_RC20Set_temp 28 // position of thermostat setpoint temperature
+
// RC30 specific
#define EMS_TYPE_RC30StatusMessage 0x41 // is an automatic thermostat broadcast giving us temps
-#define EMS_TYPE_RC30Set 0xA7 // for setting values like temp and mode
-#define EMS_OFFSET_RC30Set_mode 23 // position of thermostat mode
-#define EMS_OFFSET_RC30Set_temp 28 // position of thermostat setpoint temperature
#define EMS_OFFSET_RC30StatusMessage_setpoint 1 // setpoint temp
#define EMS_OFFSET_RC30StatusMessage_curr 2 // current temp
+#define EMS_TYPE_RC30Set 0xA7 // for setting values like temp and mode
+#define EMS_OFFSET_RC30Set_mode 23 // position of thermostat mode
+#define EMS_OFFSET_RC30Set_temp 28 // position of thermostat setpoint temperature
+
+
// RC35 specific
#define EMS_TYPE_RC35StatusMessage_HC1 0x3E // is an automatic thermostat broadcast giving us temps on HC1
#define EMS_TYPE_RC35StatusMessage_HC2 0x48 // is an automatic thermostat broadcast giving us temps on HC2
-#define EMS_TYPE_RC35Set_HC1 0x3D // for setting values like temp and mode (Working mode HC1)
-#define EMS_TYPE_RC35Set_HC2 0x47 // for setting values like temp and mode (Working mode HC2)
#define EMS_OFFSET_RC35StatusMessage_setpoint 2 // desired temp
#define EMS_OFFSET_RC35StatusMessage_curr 3 // current temp
-#define EMS_OFFSET_RC35Set_mode 7 // position of thermostat mode
-#define EMS_OFFSET_RC35Set_temp_day 2 // position of thermostat setpoint temperature for day time
-#define EMS_OFFSET_RC35Set_temp_night 1 // position of thermostat setpoint temperature for night time
-#define EMS_OFFSET_RC35Get_mode_day 1 // position of thermostat day mode
-#define EMS_OFFSET_RC35Set_temp_holiday 3 // temp during holiday 0x47
-#define EMS_OFFSET_RC35Set_heatingtype 0 // floor heating = 3 0x47
-#define EMS_OFFSET_RC35Set_circuitcalctemp 14 // calculated circuit temperature 0x48
+#define EMS_OFFSET_RC35StatusMessage_mode 1 //day mode
+
+#define EMS_TYPE_RC35Set_HC1 0x3D // for setting values like temp and mode (Working mode HC1)
+#define EMS_TYPE_RC35Set_HC2 0x47 // for setting values like temp and mode (Working mode HC2)
+#define EMS_OFFSET_RC35Set_mode 7 // position of thermostat mode
+#define EMS_OFFSET_RC35Set_temp_day 2 // position of thermostat setpoint temperature for day time
+#define EMS_OFFSET_RC35Set_temp_night 1 // position of thermostat setpoint temperature for night time
+#define EMS_OFFSET_RC35Set_temp_holiday 3 // temp during holiday 0x47
+#define EMS_OFFSET_RC35Set_heatingtype 0 // floor heating = 3 0x47
+#define EMS_OFFSET_RC35Set_circuitcalctemp 14 // calculated circuit temperature 0x48
// Easy specific
#define EMS_TYPE_EasyStatusMessage 0x0A // reading values on an Easy Thermostat
@@ -105,14 +110,21 @@
#define EMS_OFFSET_EasyStatusMessage_curr 8 // current temp
// RC1010, RC310 and RC300 specific (EMS Plus)
-#define EMS_TYPE_RCPLUSStatusMessage 0x01A5 // is an automatic thermostat broadcast giving us temps
-#define EMS_TYPE_RCPLUSStatusHeating 0x01B9 // heating mode
-#define EMS_TYPE_RCPLUSStatusMode 0x1AF // summer/winter mode
-#define EMS_TYPE_RCPLUSSet 0x03 // setpoint temp message
-#define EMS_OFFSET_RCPLUSStatusMessage_setpoint 3 // setpoint temp
-#define EMS_OFFSET_RCPLUSStatusMessage_curr 0 // current temp
-#define EMS_OFFSET_RCPLUSGet_mode_day 8 // day/night mode
-#define EMS_OFFSET_RCPLUSStatusMessage_mode 0x0A // thermostat mode (auto, manual)
+#define EMS_TYPE_RCPLUSStatusMessage 0x01A5 // is an automatic thermostat broadcast giving us temps, also reading
+#define EMS_TYPE_RCPLUSStatusMode 0x1AF // summer/winter mode
+#define EMS_OFFSET_RCPLUSStatusMessage_mode 10 // thermostat mode (auto, manual)
+#define EMS_OFFSET_RCPLUSStatusMessage_setpoint 3 // setpoint temp
+#define EMS_OFFSET_RCPLUSStatusMessage_curr 0 // current temp
+#define EMS_OFFSET_RCPLUSStatusMessage_currsetpoint 6 // target setpoint temp
+
+#define EMS_TYPE_RCPLUSSet 0x01B9 // setpoint temp message and mode
+#define EMS_OFFSET_RCPLUSSet_mode 0 // operation mode(Auto=0xFF, Manual=0x00)
+#define EMS_OFFSET_RCPLUSSet_temp_comfort3 1 // comfort3 level
+#define EMS_OFFSET_RCPLUSSet_temp_comfort2 2 // comfort2 level
+#define EMS_OFFSET_RCPLUSSet_temp_comfort1 3 // comfort1 level
+#define EMS_OFFSET_RCPLUSSet_temp_eco 4 // eco level
+#define EMS_OFFSET_RCPLUSSet_temp_setpoint 8 // temp setpoint, when changing of templevel (in auto) value is reset and set to FF
+#define EMS_OFFSET_RCPLUSSet_manual_setpoint 10 // manual setpoint
// Junkers FR10, FW100 (EMS Plus)
#define EMS_TYPE_JunkersStatusMessage 0x6F // is an automatic thermostat broadcast giving us temps
@@ -120,7 +132,7 @@
#define EMS_OFFSET_JunkersStatusMessage_curr 4 // current temp
-// Known EMS types
+// Known EMS devices
typedef enum {
EMS_MODEL_NONE, // unset
EMS_MODEL_ALL, // common for all devices
@@ -157,7 +169,7 @@ typedef enum {
// EMS types for known boilers. This list will be extended when new devices are recognized.
// The device_id is always 0x08
// format is PRODUCT ID, DESCRIPTION
-const _Boiler_Type Boiler_Types[] = {
+const _Boiler_Device Boiler_Devices[] = {
{72, "MC10 Module"},
{123, "Buderus GB172/Nefit Trendline/Junkers Cerapur"},
@@ -175,7 +187,7 @@ const _Boiler_Type Boiler_Types[] = {
* Known Solar Module types
* format is PRODUCT ID, DEVICE ID, DESCRIPTION
*/
-const _SolarModule_Type SolarModule_Types[] = {
+const _SolarModule_Device SolarModule_Devices[] = {
{EMS_PRODUCTID_SM10, EMS_ID_SM, "SM10 Solar Module"},
{EMS_PRODUCTID_SM100, EMS_ID_SM, "SM100 Solar Module"},
@@ -185,7 +197,7 @@ const _SolarModule_Type SolarModule_Types[] = {
// Other EMS devices which are not considered boilers, thermostats or solar modules
// format is PRODUCT ID, DEVICE ID, DESCRIPTION
-const _Other_Type Other_Types[] = {
+const _Other_Device Other_Devices[] = {
{69, 0x21, "MM10 Mixer Module"},
{71, 0x11, "WM10 Switch Module"},
@@ -207,13 +219,13 @@ const _Other_Type Other_Types[] = {
// heatpump
// format is PRODUCT ID, DEVICE ID, DESCRIPTION
-const _HeatPump_Type HeatPump_Types[] = {{252, EMS_ID_HP, "HeatPump Module"}};
+const _HeatPump_Device HeatPump_Devices[] = {{252, EMS_ID_HP, "HeatPump Module"}};
/*
* Known thermostat types and their capabilities
* format is MODEL_ID, PRODUCT ID, DEVICE ID, DESCRIPTION
*/
-const _Thermostat_Type Thermostat_Types[] = {
+const _Thermostat_Device Thermostat_Devices[] = {
// Easy devices - not currently supporting write operations
{EMS_MODEL_EASY, 202, 0x18, "Logamatic TC100/Nefit Moduline Easy", EMS_THERMOSTAT_WRITE_NO},
diff --git a/src/emsuart.cpp b/src/emsuart.cpp
index a701d25c3..7ecfe6351 100644
--- a/src/emsuart.cpp
+++ b/src/emsuart.cpp
@@ -7,12 +7,12 @@
#include "emsuart.h"
#include "ems.h"
-#include
#include
_EMSRxBuf * pEMSRxBuf;
_EMSRxBuf * paEMSRxBuf[EMS_MAXBUFFERS];
-uint8_t emsRxBufIdx = 0;
+uint8_t emsRxBufIdx = 0;
+uint8_t phantomBreak = 0;
os_event_t recvTaskQueue[EMSUART_recvTaskQueueLen]; // our Rx queue
@@ -22,7 +22,7 @@ os_event_t recvTaskQueue[EMSUART_recvTaskQueueLen]; // our Rx queue
//
static void emsuart_rx_intr_handler(void * para) {
static uint8_t length;
- static uint8_t uart_buffer[EMS_MAXBUFFERSIZE];
+ static uint8_t uart_buffer[EMS_MAXBUFFERSIZE + 2];
// is a new buffer? if so init the thing for a new telegram
if (EMS_Sys_Status.emsRxStatus == EMS_RX_STATUS_IDLE) {
@@ -33,7 +33,9 @@ static void emsuart_rx_intr_handler(void * para) {
// fill IRQ buffer, by emptying Rx FIFO
if (USIS(EMSUART_UART) & ((1 << UIFF) | (1 << UITO) | (1 << UIBD))) {
while ((USS(EMSUART_UART) >> USRXC) & 0xFF) {
- uart_buffer[length++] = USF(EMSUART_UART);
+ uint8_t rx = USF(EMSUART_UART);
+ if (length < EMS_MAXBUFFERSIZE)
+ uart_buffer[length++] = rx;
}
// clear Rx FIFO full and Rx FIFO timeout interrupts
@@ -46,10 +48,11 @@ static void emsuart_rx_intr_handler(void * para) {
ETS_UART_INTR_DISABLE(); // disable all interrupts and clear them
USIC(EMSUART_UART) = (1 << UIBD); // INT clear the BREAK detect interrupt
- pEMSRxBuf->length = length;
- os_memcpy((void *)pEMSRxBuf->buffer, (void *)&uart_buffer, length); // copy data into transfer buffer, including the BRK 0x00 at the end
- EMS_Sys_Status.emsRxStatus = EMS_RX_STATUS_IDLE; // set the status flag stating BRK has been received and we can start a new package
- ETS_UART_INTR_ENABLE(); // re-enable UART interrupts
+ pEMSRxBuf->length = (length > EMS_MAXBUFFERSIZE) ? EMS_MAXBUFFERSIZE : length;
+ os_memcpy((void *)pEMSRxBuf->buffer, (void *)&uart_buffer, pEMSRxBuf->length); // copy data into transfer buffer, including the BRK 0x00 at the end
+ length = 0;
+ EMS_Sys_Status.emsRxStatus = EMS_RX_STATUS_IDLE; // set the status flag stating BRK has been received and we can start a new package
+ ETS_UART_INTR_ENABLE(); // re-enable UART interrupts
system_os_post(EMSUART_recvTaskPrio, 0, 0); // call emsuart_recvTask() at next opportunity
RX_PULSE(EMSUART_BIT_TIME / 2);
@@ -67,18 +70,21 @@ static void ICACHE_FLASH_ATTR emsuart_recvTask(os_event_t * events) {
uint8_t length = pCurrent->length; // number of bytes including the BRK at the end
pCurrent->length = 0;
- // validate and transmit the EMS buffer, excluding the BRK
+ if (phantomBreak) {
+ phantomBreak = 0;
+ length--; // remove phantom break from Rx buffer
+ }
+
if (length == 2) {
RX_PULSE(20);
// it's a poll or status code, single byte and ok to send on
ems_parseTelegram((uint8_t *)pCurrent->buffer, 1);
- } else if ((length > 4) && (length <= EMS_MAXBUFFERSIZE + 1) && (pCurrent->buffer[length - 2] != 0x00)) {
+ } else if ((length > 4) && (length <= EMS_MAXBUFFERSIZE + 1)) {
// ignore double BRK at the end, possibly from the Tx loopback
// also telegrams with no data value
RX_PULSE(40);
ems_parseTelegram((uint8_t *)pCurrent->buffer, length - 1); // transmit EMS buffer, excluding the BRK
}
- // memset(pCurrent->buffer, 0x00, EMS_MAXBUFFERSIZE); // wipe memory just to be safe
}
/*
@@ -122,10 +128,10 @@ void ICACHE_FLASH_ATTR emsuart_init() {
// UCFFT = RX FIFO Full Threshold (7 bit) = want this to be 31 for 32 bytes of buffer (default was 127)
// see https://www.espressif.com/sites/default/files/documentation/esp8266-technical_reference_en.pdf
//
- // change: we set UCFFT to 1 to get an immediate indicator about incoming trafffic.
+ // change: we set UCFFT to 1 to get an immediate indicator about incoming traffic.
// Otherwise, we're only noticed by UCTOT or RxBRK!
USC1(EMSUART_UART) = 0; // reset config first
- USC1(EMSUART_UART) = (0x01 << UCFFT) | (0x01 << UCTOT) | (1 << UCTOE); // enable interupts
+ USC1(EMSUART_UART) = (0x01 << UCFFT) | (0x01 << UCTOT) | (0 << UCTOE); // enable interupts
// set interrupts for triggers
USIC(EMSUART_UART) = 0xFFFF; // clear all interupts
@@ -133,7 +139,8 @@ void ICACHE_FLASH_ATTR emsuart_init() {
// enable rx break, fifo full and timeout.
// but not frame error UIFR (because they are too frequent) or overflow UIOF because our buffer is only max 32 bytes
- USIE(EMSUART_UART) = (1 << UIBD) | (1 << UIFF) | (1 << UITO);
+ // change: we don't care about Rx Timeout - it may lead to wrong readouts
+ USIE(EMSUART_UART) = (1 << UIBD) | (1 << UIFF) | (0 << UITO);
// set up interrupt callbacks for Rx
system_os_task(emsuart_recvTask, EMSUART_recvTaskPrio, recvTaskQueue, EMSUART_recvTaskQueueLen);
@@ -142,7 +149,7 @@ void ICACHE_FLASH_ATTR emsuart_init() {
system_set_os_print(0);
// swap Rx and Tx pins to use GPIO13 (D7) and GPIO15 (D8) respectively
- //system_uart_swap();
+ system_uart_swap();
ETS_UART_INTR_ATTACH(emsuart_rx_intr_handler, nullptr);
ETS_UART_INTR_ENABLE();
@@ -199,16 +206,15 @@ void ICACHE_FLASH_ATTR emsuart_tx_brk() {
*/
_EMS_TX_STATUS ICACHE_FLASH_ATTR emsuart_tx_buffer(uint8_t * buf, uint8_t len) {
_EMS_TX_STATUS result = EMS_TX_STATUS_OK;
+
+ if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_JABBER) {
+ ems_dumpBuffer("emsuart_tx_buffer: ", buf, len); // validate and transmit the EMS buffer, excluding the BRK
+ }
+
if (len) {
LA_PULSE(50);
- // temp code until we get mode 2 working without resets
- if (EMS_Sys_Status.emsTxMode == 0) { // classic mode logic
- for (uint8_t i = 0; i < len; i++) {
- TX_PULSE(EMSUART_BIT_TIME / 4);
- USF(EMSUART_UART) = buf[i];
- }
- emsuart_tx_brk(); // send
- } else if (EMS_Sys_Status.emsTxMode == 1) { // With extra tx delay for EMS+
+
+ if (EMS_Sys_Status.emsTxMode == 2) { // With extra tx delay for EMS+
for (uint8_t i = 0; i < len; i++) {
TX_PULSE(EMSUART_BIT_TIME / 4);
USF(EMSUART_UART) = buf[i];
@@ -228,9 +234,8 @@ _EMS_TX_STATUS ICACHE_FLASH_ATTR emsuart_tx_buffer(uint8_t * buf, uint8_t len) {
delayMicroseconds(EMSUART_TX_WAIT_BYTE - EMSUART_TX_LAG + EMSUART_TX_WAIT_GAP);
}
emsuart_tx_brk(); // send
- } else if (EMS_Sys_Status.emsTxMode == 2) {
+ } else if (EMS_Sys_Status.emsTxMode == 1) {
/*
- *
* based on code from https://github.com/proddy/EMS-ESP/issues/103 by @susisstrolch
* we emit the whole telegram, with Rx interrupt disabled, collecting busmaster response in FIFO.
* after sending the last char we poll the Rx status until either
@@ -254,9 +259,9 @@ _EMS_TX_STATUS ICACHE_FLASH_ATTR emsuart_tx_buffer(uint8_t * buf, uint8_t len) {
// shorter busy poll...
#define EMSUART_BUSY_WAIT (EMSUART_BIT_TIME / 8)
-#define EMS_TX_TO_COUNT ((20 + 10000 / EMSUART_BIT_TIME) * 8)
+#define EMS_TX_TO_CHARS (2 + 20)
+#define EMS_TX_TO_COUNT ((EMS_TX_TO_CHARS)*10 * 8)
uint16_t wdc = EMS_TX_TO_COUNT;
-
ETS_UART_INTR_DISABLE(); // disable rx interrupt
// clear Rx status register
@@ -272,7 +277,6 @@ _EMS_TX_STATUS ICACHE_FLASH_ATTR emsuart_tx_buffer(uint8_t * buf, uint8_t len) {
USF(EMSUART_UART) = buf[i++]; // send each Tx byte
// wait for echo from busmaster
GPIO_L(TX_MARK_MASK);
-
while (((USS(EMSUART_UART) >> USRXC) & 0xFF) == _usrxc) {
delayMicroseconds(EMSUART_BUSY_WAIT); // burn CPU cycles...
if (--wdc == 0) {
@@ -301,11 +305,13 @@ _EMS_TX_STATUS ICACHE_FLASH_ATTR emsuart_tx_buffer(uint8_t * buf, uint8_t len) {
// wait until BRK detected...
while (!(USIR(EMSUART_UART) & (1 << UIBD))) {
- delayMicroseconds(EMSUART_BUSY_WAIT);
+ // delayMicroseconds(EMSUART_BUSY_WAIT);
+ delayMicroseconds(EMSUART_BIT_TIME);
}
USC0(EMSUART_UART) &= ~((1 << UCBRK) | (1 << UCLBE)); // disable loopback & clear
USIC(EMSUART_UART) = (1 << UIBD); // clear BRK detect IRQ
+ phantomBreak = 1;
}
GPIO_L(TX_MARK_MASK);
}
@@ -314,16 +320,3 @@ _EMS_TX_STATUS ICACHE_FLASH_ATTR emsuart_tx_buffer(uint8_t * buf, uint8_t len) {
}
return result;
}
-
-/*
- * Send the Poll (our own ID) to Tx as a single byte and end with a
- */
-void ICACHE_FLASH_ATTR emsuart_tx_poll() {
- static uint8_t buf[1];
- if (EMS_Sys_Status.emsReverse) {
- buf[0] = {EMS_ID_ME | 0x80};
- } else {
- buf[0] = {EMS_ID_ME};
- }
- emsuart_tx_buffer(buf, 1);
-}
diff --git a/src/emsuart.h b/src/emsuart.h
index 0777dcfd5..0859aaf2f 100644
--- a/src/emsuart.h
+++ b/src/emsuart.h
@@ -7,7 +7,6 @@
*/
#pragma once
-#include
#include
#define EMSUART_UART 0 // UART 0
@@ -37,4 +36,3 @@ void ICACHE_FLASH_ATTR emsuart_init();
void ICACHE_FLASH_ATTR emsuart_stop();
void ICACHE_FLASH_ATTR emsuart_start();
_EMS_TX_STATUS ICACHE_FLASH_ATTR emsuart_tx_buffer(uint8_t * buf, uint8_t len);
-void ICACHE_FLASH_ATTR emsuart_tx_poll();
diff --git a/src/my_config.h b/src/my_config.h
index 1dc9c16b4..15f06b4d2 100644
--- a/src/my_config.h
+++ b/src/my_config.h
@@ -10,26 +10,14 @@
#include "ems.h"
-// MQTT base name
-#define MQTT_BASE "home" // all MQTT topics are prefix with this string, in the format //
-
-// MQTT general settings
-#define MQTT_WILL_TOPIC "status" // for last will & testament topic name
-#define MQTT_WILL_ONLINE_PAYLOAD "online" // for last will & testament payload
-#define MQTT_WILL_OFFLINE_PAYLOAD "offline" // for last will & testament payload
-#define MQTT_RETAIN false
-#define MQTT_KEEPALIVE 120 // 2 minutes
-#define MQTT_QOS 1
-#define MQTT_MAX_SIZE 700 // max size of a JSON object. See https://arduinojson.org/v6/assistant/
-
// MQTT for thermostat
#define TOPIC_THERMOSTAT_DATA "thermostat_data" // for sending thermostat values to MQTT
#define TOPIC_THERMOSTAT_CMD_TEMP "thermostat_cmd_temp" // for received thermostat temp changes via MQTT
#define TOPIC_THERMOSTAT_CMD_MODE "thermostat_cmd_mode" // for received thermostat mode changes via MQTT
#define TOPIC_THERMOSTAT_CMD_HC "thermostat_cmd_hc" // for received thermostat hc number changes via MQTT
-#define TOPIC_THERMOSTAT_CMD_DAYTEMP "thermostat_daytemp" // RC35 specific
-#define TOPIC_THERMOSTAT_CMD_NIGHTTEMP "thermostat_nighttemp" // RC35 specific
-#define TOPIC_THERMOSTAT_CMD_HOLIDAYTEMP "thermostat_holidayttemp" // RC35 specific
+#define TOPIC_THERMOSTAT_CMD_DAYTEMP "thermostat_daytemp" // for received thermostat day temp (RC35 specific)
+#define TOPIC_THERMOSTAT_CMD_NIGHTTEMP "thermostat_nighttemp" // for received thermostat night temp (RC35 specific)
+#define TOPIC_THERMOSTAT_CMD_HOLIDAYTEMP "thermostat_holidayttemp" // for received thermostat holiday temp (RC35 specific)
#define THERMOSTAT_CURRTEMP "thermostat_currtemp" // current temperature
#define THERMOSTAT_SELTEMP "thermostat_seltemp" // selected temperature
#define THERMOSTAT_HC "thermostat_hc" // which heating circuit number
@@ -41,13 +29,13 @@
#define THERMOSTAT_CIRCUITCALCTEMP "thermostat_circuitcalctemp" // RC35 specific
// MQTT for boiler
-#define TOPIC_BOILER_DATA "boiler_data" // for sending boiler values to MQTT
-#define TOPIC_BOILER_TAPWATER_ACTIVE "tapwater_active" // if hot tap water is running
-#define TOPIC_BOILER_HEATING_ACTIVE "heating_active" // if heating is on
-#define TOPIC_BOILER_WWACTIVATED "wwactivated" // for receiving MQTT message to change water on/off
-#define TOPIC_BOILER_CMD_WWTEMP "boiler_cmd_wwtemp" // for received boiler wwtemp changes via MQTT
-#define TOPIC_BOILER_CMD_COMFORT "boiler_cmd_comfort" // for received boiler ww comfort setting via MQTT
-#define TOPIC_BOILER_CMD_FLOWTEMP "boiler_cmd_flowtemp" // for received boiler flowtemp value via MQTT
+#define TOPIC_BOILER_DATA "boiler_data" // for sending boiler values to MQTT
+#define TOPIC_BOILER_TAPWATER_ACTIVE "tapwater_active" // if hot tap water is running
+#define TOPIC_BOILER_HEATING_ACTIVE "heating_active" // if heating is on
+#define TOPIC_BOILER_CMD_WWACTIVATED "boiler_cmd_wwactivated" // for received message to change water on/off
+#define TOPIC_BOILER_CMD_WWTEMP "boiler_cmd_wwtemp" // for received boiler wwtemp changes via MQTT
+#define TOPIC_BOILER_CMD_COMFORT "boiler_cmd_comfort" // for received boiler ww comfort setting via MQTT
+#define TOPIC_BOILER_CMD_FLOWTEMP "boiler_cmd_flowtemp" // for received boiler flowtemp value via MQTT
// MQTT for SM10/SM100 Solar Module
#define TOPIC_SM_DATA "sm_data" // topic name
@@ -74,28 +62,3 @@
// MQTT for EXTERNAL SENSORS
#define TOPIC_EXTERNAL_SENSORS "sensors" // for sending sensor values to MQTT
#define PAYLOAD_EXTERNAL_SENSORS "temp_%d" // for formatting the payload for each external dallas sensor
-
-
-////////////////////////////////////////////////////////////////////////////////////////////////////
-// THESE DEFAULT VALUES CAN ALSO BE SET AND STORED WITHTIN THE APPLICATION (see 'set' command) //
-////////////////////////////////////////////////////////////////////////////////////////////////////
-
-// Set LED pin used for showing the EMS bus connection status. Solid means EMS bus working, flashing is an error
-// can be either the onboard LED on the ESP8266 (LED_BULLETIN) or external via an external pull-up LED (e.g. D1 on a bbqkees' board)
-// can be enabled and disabled via the 'set led' command and pin set by 'set led_gpio'
-#define EMSESP_LED_GPIO LED_BUILTIN
-
-#ifdef LOGICANALYZER
-#define EMSESP_DALLAS_GPIO D1
-#define EMSESP_DALLAS_PARASITE false
-#else
-// set this if using an external temperature sensor like a DS18B20
-// D5 is the default on a bbqkees board
-#define EMSESP_DALLAS_GPIO D5
-#define EMSESP_DALLAS_PARASITE false
-#endif
-
-// By default the EMS bus will be scanned for known devices based on the product ids in ems_devices.h
-// You can override the Thermostat and Boiler types here
-#define EMSESP_BOILER_TYPE EMS_ID_NONE
-#define EMSESP_THERMOSTAT_TYPE EMS_ID_NONE
diff --git a/src/test_data.h b/src/test_data.h
index 0f9edf3b7..c47805977 100644
--- a/src/test_data.h
+++ b/src/test_data.h
@@ -52,7 +52,9 @@ static const char * TEST_DATA[] = {
"38 10 FF 00 03 2B 00 C7 07 C3 01", // test 47 - heatpump Enviline
"08 0B 19 00 00 F7 80 00 80 00 00 00 00 00 03 58 97 0C 7B 1F 00 00 00 06 C4 DF 02 64 48 80 00", // test 48 - outdoor temp check
"88 00 19 00 00 DC 80 00 80 00 FF FF 00 00 00 21 9A 06 E1 7C 00 00 00 06 C2 13 00 1E 90 80 00", // test 49 - check max length
- "30 00 FF 00 02 8E 00 00 41 82 00 00 28 36 00 00 82 21" // test 50 - SM100
+ "30 00 FF 00 02 8E 00 00 41 82 00 00 28 36 00 00 82 21", // test 50 - SM100
+ "10 00 FF 08 01 B9 26", // test 51 - EMS+ 0x1B9 set temp
+ "10 00 F7 00 FF 01 B9 21 E9" // test 52 - EMS+ 0x1B9 F7 test
};
diff --git a/src/version.h b/src/version.h
index 8163ddfed..64e2e8623 100644
--- a/src/version.h
+++ b/src/version.h
@@ -1,10 +1 @@
-/**
- *
- * Paul Derbyshire - https://github.com/proddy/EMS-ESP
- */
-
-#pragma once
-
-#define APP_NAME "EMS-ESP"
-#define APP_VERSION "1.8.3"
-#define APP_HOSTNAME "ems-esp"
+#define APP_VERSION "1.9.0"
diff --git a/src/webh/.gitkeep b/src/webh/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/websrc/3rdparty/css/bootstrap-3.3.7.min.css b/src/websrc/3rdparty/css/bootstrap-3.3.7.min.css
new file mode 100644
index 000000000..ed3905e0e
--- /dev/null
+++ b/src/websrc/3rdparty/css/bootstrap-3.3.7.min.css
@@ -0,0 +1,6 @@
+/*!
+ * Bootstrap v3.3.7 (http://getbootstrap.com)
+ * Copyright 2011-2016 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff2) format('woff2'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;max-width:100%;height:auto;padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:focus,a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:focus,a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ol,ul{margin-top:0;margin-bottom:10px}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;margin-left:-5px;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dd,dt{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{margin-bottom:0}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote .small:before,blockquote footer:before,blockquote small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:''}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:'\00A0 \2014'}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control::-ms-expand{background-color:transparent;border:0}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-top:4px\9;margin-left:-20px}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{cursor:not-allowed}.checkbox.disabled label,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .radio label{cursor:not-allowed}.form-control-static{min-height:34px;padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none;opacity:.65}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default.focus,.btn-default:focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#398439;border-color:#255625}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#337ab7;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-property:height,visibility;-o-transition-property:height,visibility;transition-property:height,visibility}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown,.dropup{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;-webkit-overflow-scrolling:touch;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1)}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#337ab7;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:2;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:3;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:middle;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron .h1,.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{padding-right:15px;padding-left:15px;border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-right:auto;margin-left:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{color:#555;text-decoration:none;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#777;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-right:15px;padding-left:15px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{-webkit-appearance:none;padding:0;cursor:pointer;background:0 0;border:0}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5)}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:12px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;filter:alpha(opacity=0);opacity:0;line-break:auto}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);line-break:auto}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.item.active.right,.carousel-inner>.item.next{left:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{left:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{left:0;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);background-color:rgba(0,0,0,0);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;filter:alpha(opacity=90);outline:0;opacity:.9}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-10px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000\9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{display:table;content:" "}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.modal-header:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-lg,.visible-md,.visible-sm,.visible-xs{display:none!important}.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}}
+/*# sourceMappingURL=bootstrap.min.css.map */
\ No newline at end of file
diff --git a/src/websrc/3rdparty/css/footable.bootstrap-3.1.6.min.css b/src/websrc/3rdparty/css/footable.bootstrap-3.1.6.min.css
new file mode 100644
index 000000000..87841b27a
--- /dev/null
+++ b/src/websrc/3rdparty/css/footable.bootstrap-3.1.6.min.css
@@ -0,0 +1 @@
+table.footable-details,table.footable>thead>tr.footable-filtering>th div.form-group{margin-bottom:0}table.footable,table.footable-details{position:relative;width:100%;border-spacing:0;border-collapse:collapse}table.footable-hide-fouc{display:none}table>tbody>tr>td>span.footable-toggle{margin-right:8px;opacity:.3}table>tbody>tr>td>span.footable-toggle.last-column{margin-left:8px;float:right}table.table-condensed>tbody>tr>td>span.footable-toggle{margin-right:5px}table.footable-details>tbody>tr>th:nth-child(1){min-width:40px;width:120px}table.footable-details>tbody>tr>td:nth-child(2){word-break:break-all}table.footable-details>tbody>tr:first-child>td,table.footable-details>tbody>tr:first-child>th,table.footable-details>tfoot>tr:first-child>td,table.footable-details>tfoot>tr:first-child>th,table.footable-details>thead>tr:first-child>td,table.footable-details>thead>tr:first-child>th{border-top-width:0}table.footable-details.table-bordered>tbody>tr:first-child>td,table.footable-details.table-bordered>tbody>tr:first-child>th,table.footable-details.table-bordered>tfoot>tr:first-child>td,table.footable-details.table-bordered>tfoot>tr:first-child>th,table.footable-details.table-bordered>thead>tr:first-child>td,table.footable-details.table-bordered>thead>tr:first-child>th{border-top-width:1px}div.footable-loader{vertical-align:middle;text-align:center;height:300px;position:relative}div.footable-loader>span.fooicon{display:inline-block;opacity:.3;font-size:30px;line-height:32px;width:32px;height:32px;margin-top:-16px;margin-left:-16px;position:absolute;top:50%;left:50%;-webkit-animation:fooicon-spin-r 2s infinite linear;animation:fooicon-spin-r 2s infinite linear}table.footable>tbody>tr.footable-empty>td{vertical-align:middle;text-align:center;font-size:30px}table.footable>tbody>tr>td,table.footable>tbody>tr>th{display:none}table.footable>tbody>tr.footable-detail-row>td,table.footable>tbody>tr.footable-detail-row>th,table.footable>tbody>tr.footable-empty>td,table.footable>tbody>tr.footable-empty>th{display:table-cell}@-webkit-keyframes fooicon-spin-r{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fooicon-spin-r{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fooicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings'!important;font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fooicon:after,.fooicon:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.fooicon-loader:before{content:"\e030"}.fooicon-plus:before{content:"\2b"}.fooicon-minus:before{content:"\2212"}.fooicon-search:before{content:"\e003"}.fooicon-remove:before{content:"\e014"}.fooicon-sort:before{content:"\e150"}.fooicon-sort-asc:before{content:"\e155"}.fooicon-sort-desc:before{content:"\e156"}.fooicon-pencil:before{content:"\270f"}.fooicon-trash:before{content:"\e020"}.fooicon-eye-close:before{content:"\e106"}.fooicon-flash:before{content:"\e162"}.fooicon-cog:before{content:"\e019"}.fooicon-stats:before{content:"\e185"}table.footable>thead>tr.footable-filtering>th{border-bottom-width:1px;font-weight:400}.footable-filtering-external.footable-filtering-right,table.footable.footable-filtering-right>thead>tr.footable-filtering>th,table.footable>thead>tr.footable-filtering>th{text-align:right}.footable-filtering-external.footable-filtering-left,table.footable.footable-filtering-left>thead>tr.footable-filtering>th{text-align:left}.footable-filtering-external.footable-filtering-center,.footable-paging-external.footable-paging-center,table.footable-paging-center>tfoot>tr.footable-paging>td,table.footable.footable-filtering-center>thead>tr.footable-filtering>th,table.footable>tfoot>tr.footable-paging>td{text-align:center}table.footable>thead>tr.footable-filtering>th div.form-group+div.form-group{margin-top:5px}table.footable>thead>tr.footable-filtering>th div.input-group{width:100%}.footable-filtering-external ul.dropdown-menu>li>a.checkbox,table.footable>thead>tr.footable-filtering>th ul.dropdown-menu>li>a.checkbox{margin:0;display:block;position:relative}.footable-filtering-external ul.dropdown-menu>li>a.checkbox>label,table.footable>thead>tr.footable-filtering>th ul.dropdown-menu>li>a.checkbox>label{display:block;padding-left:20px}.footable-filtering-external ul.dropdown-menu>li>a.checkbox input[type=checkbox],table.footable>thead>tr.footable-filtering>th ul.dropdown-menu>li>a.checkbox input[type=checkbox]{position:absolute;margin-left:-20px}@media (min-width:768px){table.footable>thead>tr.footable-filtering>th div.input-group{width:auto}table.footable>thead>tr.footable-filtering>th div.form-group{margin-left:2px;margin-right:2px}table.footable>thead>tr.footable-filtering>th div.form-group+div.form-group{margin-top:0}}table.footable>tbody>tr>td.footable-sortable,table.footable>tbody>tr>th.footable-sortable,table.footable>tfoot>tr>td.footable-sortable,table.footable>tfoot>tr>th.footable-sortable,table.footable>thead>tr>td.footable-sortable,table.footable>thead>tr>th.footable-sortable{position:relative;padding-right:30px;cursor:pointer}td.footable-sortable>span.fooicon,th.footable-sortable>span.fooicon{position:absolute;right:6px;top:50%;margin-top:-7px;opacity:0;transition:opacity .3s ease-in}td.footable-sortable.footable-asc>span.fooicon,td.footable-sortable.footable-desc>span.fooicon,td.footable-sortable:hover>span.fooicon,th.footable-sortable.footable-asc>span.fooicon,th.footable-sortable.footable-desc>span.fooicon,th.footable-sortable:hover>span.fooicon{opacity:1}table.footable-sorting-disabled td.footable-sortable.footable-asc>span.fooicon,table.footable-sorting-disabled td.footable-sortable.footable-desc>span.fooicon,table.footable-sorting-disabled td.footable-sortable:hover>span.fooicon,table.footable-sorting-disabled th.footable-sortable.footable-asc>span.fooicon,table.footable-sorting-disabled th.footable-sortable.footable-desc>span.fooicon,table.footable-sorting-disabled th.footable-sortable:hover>span.fooicon{opacity:0;visibility:hidden}.footable-paging-external ul.pagination,table.footable>tfoot>tr.footable-paging>td>ul.pagination{margin:10px 0 0}.footable-paging-external span.label,table.footable>tfoot>tr.footable-paging>td>span.label{display:inline-block;margin:0 0 10px;padding:4px 10px}.footable-paging-external.footable-paging-left,table.footable-paging-left>tfoot>tr.footable-paging>td{text-align:left}.footable-paging-external.footable-paging-right,table.footable-editing-right td.footable-editing,table.footable-editing-right tr.footable-editing,table.footable-paging-right>tfoot>tr.footable-paging>td{text-align:right}ul.pagination>li.footable-page{display:none}ul.pagination>li.footable-page.visible{display:inline}td.footable-editing{width:90px;max-width:90px}table.footable-editing-no-delete td.footable-editing,table.footable-editing-no-edit td.footable-editing,table.footable-editing-no-view td.footable-editing{width:70px;max-width:70px}table.footable-editing-no-delete.footable-editing-no-view td.footable-editing,table.footable-editing-no-edit.footable-editing-no-delete td.footable-editing,table.footable-editing-no-edit.footable-editing-no-view td.footable-editing{width:50px;max-width:50px}table.footable-editing-no-edit.footable-editing-no-delete.footable-editing-no-view td.footable-editing,table.footable-editing-no-edit.footable-editing-no-delete.footable-editing-no-view th.footable-editing{width:0;max-width:0;display:none!important}table.footable-editing-left td.footable-editing,table.footable-editing-left tr.footable-editing{text-align:left}table.footable-editing button.footable-add,table.footable-editing button.footable-hide,table.footable-editing-show button.footable-show,table.footable-editing.footable-editing-always-show button.footable-hide,table.footable-editing.footable-editing-always-show button.footable-show,table.footable-editing.footable-editing-always-show.footable-editing-no-add tr.footable-editing{display:none}table.footable-editing.footable-editing-always-show button.footable-add,table.footable-editing.footable-editing-show button.footable-add,table.footable-editing.footable-editing-show button.footable-hide{display:inline-block}
\ No newline at end of file
diff --git a/src/websrc/3rdparty/css/sidebar.css b/src/websrc/3rdparty/css/sidebar.css
new file mode 100644
index 000000000..6a2f2345f
--- /dev/null
+++ b/src/websrc/3rdparty/css/sidebar.css
@@ -0,0 +1 @@
+html {position: relative;overflow: scroll;overflow-x: hidden;min-height: 100% }::-webkit-scrollbar {width: 0px;background: transparent;}::-webkit-scrollbar-thumb {background: #e8e8e8;}body {background: #f1f3f6;margin-bottom: 60px }p {font-size: 1.1em;font-weight: 300;line-height: 1.7em;color: #999 }a, a:focus, a:hover {color: inherit;text-decoration: none;transition: all .3s }.navbar {padding: 15px 10px;background: #fff;border: none;border-radius: 0;margin-bottom: 40px;box-shadow: 1px 1px 3px rgba(0, 0, 0, .1) }#dismiss, #sidebar {background: #337ab7 }#content.navbar-btn {box-shadow: none;outline: 0;border: none }.line {width: 100%;height: 1px;border-bottom: 1px dashed #ddd;margin: 40px 0 }#sidebar {width: 250px;position: fixed;top: 0;left: -250px;height: 100vh;z-index: 999;color: #fff;transition: all .3s;overflow-y: auto;box-shadow: 3px 3px 3px rgba(0, 0, 0, .2) }@media screen and (min-width:768px) {#sidebar {left: 0 }.footer {margin-left: 250px }#ajaxcontent {margin-left: 250px }#dismiss, .navbar-btn {display: none }}#sidebar.active {left: 0 }#dismiss {width: 35px;height: 35px;line-height: 35px;text-align: center;position: absolute;top: 10px;right: 10px;cursor: pointer;-webkit-transition: all .3s;-o-transition: all .3s;transition: all .3s }#dismiss:hover {background: #fff;color: #337ab7 }.overlay {position: fixed;width: 100vw;height: 100vh;background: rgba(0, 0, 0, .7);z-index: 998;display: none }#sidebar .sidebar-header {padding: 20px;background: #337ab7 }#sidebar ul.components {padding: 20px 0;border-bottom: 1px solid #47748b }#content, ul.CTAs {padding: 20px }#sidebar ul p {color: #fff;padding: 10px }#sidebar ul li a {padding: 10px;font-size: 1.1em;display: block }#sidebar ul li a:hover {color: #337ab7;background: #fff }#sidebar ul li.active>a, a[aria-expanded=true] {color: #fff;background: #2e6da4 }a[data-toggle=collapse] {position: relative }a[aria-expanded=false]::before, a[aria-expanded=true]::before {content: '\e259';display: block;position: absolute;right: 20px;font-family: 'Glyphicons Halflings';font-size: .6em }#sidebar ul ul a, ul.CTAs a {font-size: .9em }a[aria-expanded=true]::before {content: '\e260' }#sidebar ul ul a {padding-left: 30px;background: #2e6da4 }ul.CTAs a {text-align: center;display: block;border-radius: 5px;margin-bottom: 5px }a.download {background: #fff;color: #337ab7 }#sidebar a.article, a.article:hover {background: #2e6da4;color: #fff }#content {width: 100%;min-height: 100vh;transition: all .3s;position: absolute;top: 0;right: 0 }.footer {position: fixed;bottom: 0;width: 100%;height: 45px;background-color: #f1f3f6 }i {margin-right: 1em }
\ No newline at end of file
diff --git a/src/websrc/3rdparty/fonts/glyphicons-halflings-regular.woff b/src/websrc/3rdparty/fonts/glyphicons-halflings-regular.woff
new file mode 100644
index 000000000..9e612858f
Binary files /dev/null and b/src/websrc/3rdparty/fonts/glyphicons-halflings-regular.woff differ
diff --git a/src/websrc/3rdparty/js/bootstrap-3.3.7.min.js b/src/websrc/3rdparty/js/bootstrap-3.3.7.min.js
new file mode 100644
index 000000000..9bcd2fcca
--- /dev/null
+++ b/src/websrc/3rdparty/js/bootstrap-3.3.7.min.js
@@ -0,0 +1,7 @@
+/*!
+ * Bootstrap v3.3.7 (http://getbootstrap.com)
+ * Copyright 2011-2016 Twitter, Inc.
+ * Licensed under the MIT license
+ */
+if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1||b[0]>3)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher, but lower than version 4")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){if(a(b.target).is(this))return b.handleObj.handler.apply(this,arguments)}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.7",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a("#"===f?[]:f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.7",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c).prop(c,!0)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c).prop(c,!1))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")?(c.prop("checked")&&(a=!1),b.find(".active").removeClass("active"),this.$element.addClass("active")):"checkbox"==c.prop("type")&&(c.prop("checked")!==this.$element.hasClass("active")&&(a=!1),this.$element.toggleClass("active")),c.prop("checked",this.$element.hasClass("active")),a&&c.trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active")),this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target).closest(".btn");b.call(d,"toggle"),a(c.target).is('input[type="radio"], input[type="checkbox"]')||(c.preventDefault(),d.is("input,button")?d.trigger("focus"):d.find("input:visible,button:visible").first().trigger("focus"))}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=null,this.sliding=null,this.interval=null,this.$active=null,this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.7",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c=this.getItemIndex(b),d="prev"==a&&0===c||"next"==a&&c==this.$items.length-1;if(d&&!this.options.wrap)return b;var e="prev"==a?-1:1,f=(c+e)%this.$items.length;return this.$items.eq(f)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));if(!(a>this.$items.length-1||a<0))return this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){if(!this.sliding)return this.slide("next")},c.prototype.prev=function(){if(!this.sliding)return this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i=this;if(f.hasClass("active"))return this.sliding=!1;var j=f[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:h});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(f)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(m)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&/show|hide/.test(b)&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a('[data-toggle="collapse"][href="#'+b.id+'"],[data-toggle="collapse"][data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.7",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":e.data();c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function c(c){c&&3===c.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=b(d),f={relatedTarget:this};e.hasClass("open")&&(c&&"click"==c.type&&/input|textarea/i.test(c.target.tagName)&&a.contains(e[0],c.target)||(e.trigger(c=a.Event("hide.bs.dropdown",f)),c.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger(a.Event("hidden.bs.dropdown",f)))))}))}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.7",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=b(e),g=f.hasClass("open");if(c(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(document.createElement("div")).addClass("dropdown-backdrop").insertAfter(a(this)).on("click",c);var h={relatedTarget:this};if(f.trigger(d=a.Event("show.bs.dropdown",h)),d.isDefaultPrevented())return;e.trigger("focus").attr("aria-expanded","true"),f.toggleClass("open").trigger(a.Event("shown.bs.dropdown",h))}return!1}},g.prototype.keydown=function(c){if(/(38|40|27|32)/.test(c.which)&&!/input|textarea/i.test(c.target.tagName)){var d=a(this);if(c.preventDefault(),c.stopPropagation(),!d.is(".disabled, :disabled")){var e=b(d),g=e.hasClass("open");if(!g&&27!=c.which||g&&27==c.which)return 27==c.which&&e.find(f).trigger("focus"),d.trigger("click");var h=" li:not(.disabled):visible a",i=e.find(".dropdown-menu"+h);if(i.length){var j=i.index(c.target);38==c.which&&j>0&&j--,40==c.which&&jdocument.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&a?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!a?this.scrollbarWidth:""})},c.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},c.prototype.checkScrollbar=function(){var a=window.innerWidth;if(!a){var b=document.documentElement.getBoundingClientRect();a=b.right-Math.abs(b.left)}this.bodyIsOverflowing=document.body.clientWidth',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){if(this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(a.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusin"==b.type?"focus":"hover"]=!0),c.tip().hasClass("in")||"in"==c.hoverState?void(c.hoverState="in"):(clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.isInStateTrue=function(){for(var a in this.inState)if(this.inState[a])return!0;return!1},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);if(c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusout"==b.type?"focus":"hover"]=!1),!c.isInStateTrue())return clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide()},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.getPosition(this.$viewport);h="bottom"==h&&k.bottom+m>o.bottom?"top":"top"==h&&k.top-mo.width?"left":"left"==h&&k.left-lg.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;jg.right&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){if(!this.$tip&&(this.$tip=a(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),b?(c.inState.click=!c.inState.click,c.isInStateTrue()?c.enter(c):c.leave(c)):c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type),a.$tip&&a.$tip.detach(),a.$tip=null,a.$arrow=null,a.$viewport=null,a.$element=null})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;!e&&/destroy|hide/.test(b)||(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.7",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:'