From 0763f5b44604a4a1688bcc0c14566f75b0b8a803 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Konstantin=20L=C3=A1ska?= Date: Fri, 26 Jun 2026 09:27:26 +0200 Subject: [PATCH] update --- SW/ESPlan_RS485_Relay/ESPlan_RS485_Relay.ino | 141 +++ SW/ESPlan_RS485_Relay/cfg.h | 132 +++ SW/ESPlan_RS485_Relay/i18n_str.h | 187 ++++ SW/ESPlan_RS485_Relay/mb_client.cpp | 158 +++ SW/ESPlan_RS485_Relay/mb_client.h | 54 + SW/ESPlan_RS485_Relay/relay.cpp | 114 +++ SW/ESPlan_RS485_Relay/relay.h | 40 + SW/ESPlan_RS485_Relay/ws_server.cpp | 661 +++++++++++++ SW/ESPlan_RS485_Relay/ws_server.h | 28 + .../ESPlan_WeatherStation.ino | 125 +++ SW/ESPlan_WeatherStation/cfg.h | 140 +++ SW/ESPlan_WeatherStation/i18n_str.h | 244 +++++ SW/ESPlan_WeatherStation/mb_client.cpp | 152 +++ SW/ESPlan_WeatherStation/mb_client.h | 54 + SW/ESPlan_WeatherStation/snr.cpp | 266 +++++ SW/ESPlan_WeatherStation/snr.h | 14 + SW/ESPlan_WeatherStation/ws_server.cpp | 710 +++++++++++++ SW/ESPlan_WeatherStation/ws_server.h | 28 + .../ESPlan_ZTS3000_WindSensor.ino | 132 +++ SW/ESPlan_ZTS3000_WindSensor/cfg.h | 120 +++ SW/ESPlan_ZTS3000_WindSensor/i18n_str.h | 158 +++ SW/ESPlan_ZTS3000_WindSensor/mb_client.cpp | 152 +++ SW/ESPlan_ZTS3000_WindSensor/mb_client.h | 54 + SW/ESPlan_ZTS3000_WindSensor/snr.cpp | 85 ++ SW/ESPlan_ZTS3000_WindSensor/snr.h | 14 + SW/ESPlan_ZTS3000_WindSensor/ws_server.cpp | 592 +++++++++++ SW/ESPlan_ZTS3000_WindSensor/ws_server.h | 29 + SW/esplan_v2_renke_weather_station/Config.h | 43 - SW/esplan_v2_renke_weather_station/Lang.h | 262 ----- .../WeatherStation.cpp | 531 ---------- .../WeatherStation.h | 112 --- SW/esplan_v2_renke_weather_station/WebUi.cpp | 645 ------------ SW/esplan_v2_renke_weather_station/WebUi.h | 13 - .../esplan_v2_renke_weather_station.ino | 455 --------- .../logo_web.cpp | 936 ------------------ SW/esplan_v2_renke_weather_station/logo_web.h | 7 - 36 files changed, 4584 insertions(+), 3004 deletions(-) create mode 100755 SW/ESPlan_RS485_Relay/ESPlan_RS485_Relay.ino create mode 100755 SW/ESPlan_RS485_Relay/cfg.h create mode 100755 SW/ESPlan_RS485_Relay/i18n_str.h create mode 100755 SW/ESPlan_RS485_Relay/mb_client.cpp create mode 100755 SW/ESPlan_RS485_Relay/mb_client.h create mode 100755 SW/ESPlan_RS485_Relay/relay.cpp create mode 100755 SW/ESPlan_RS485_Relay/relay.h create mode 100755 SW/ESPlan_RS485_Relay/ws_server.cpp create mode 100755 SW/ESPlan_RS485_Relay/ws_server.h create mode 100755 SW/ESPlan_WeatherStation/ESPlan_WeatherStation.ino create mode 100755 SW/ESPlan_WeatherStation/cfg.h create mode 100755 SW/ESPlan_WeatherStation/i18n_str.h create mode 100755 SW/ESPlan_WeatherStation/mb_client.cpp create mode 100755 SW/ESPlan_WeatherStation/mb_client.h create mode 100755 SW/ESPlan_WeatherStation/snr.cpp create mode 100755 SW/ESPlan_WeatherStation/snr.h create mode 100755 SW/ESPlan_WeatherStation/ws_server.cpp create mode 100755 SW/ESPlan_WeatherStation/ws_server.h create mode 100755 SW/ESPlan_ZTS3000_WindSensor/ESPlan_ZTS3000_WindSensor.ino create mode 100755 SW/ESPlan_ZTS3000_WindSensor/cfg.h create mode 100755 SW/ESPlan_ZTS3000_WindSensor/i18n_str.h create mode 100755 SW/ESPlan_ZTS3000_WindSensor/mb_client.cpp create mode 100755 SW/ESPlan_ZTS3000_WindSensor/mb_client.h create mode 100755 SW/ESPlan_ZTS3000_WindSensor/snr.cpp create mode 100755 SW/ESPlan_ZTS3000_WindSensor/snr.h create mode 100755 SW/ESPlan_ZTS3000_WindSensor/ws_server.cpp create mode 100755 SW/ESPlan_ZTS3000_WindSensor/ws_server.h delete mode 100755 SW/esplan_v2_renke_weather_station/Config.h delete mode 100755 SW/esplan_v2_renke_weather_station/Lang.h delete mode 100755 SW/esplan_v2_renke_weather_station/WeatherStation.cpp delete mode 100755 SW/esplan_v2_renke_weather_station/WeatherStation.h delete mode 100755 SW/esplan_v2_renke_weather_station/WebUi.cpp delete mode 100755 SW/esplan_v2_renke_weather_station/WebUi.h delete mode 100755 SW/esplan_v2_renke_weather_station/esplan_v2_renke_weather_station.ino delete mode 100755 SW/esplan_v2_renke_weather_station/logo_web.cpp delete mode 100755 SW/esplan_v2_renke_weather_station/logo_web.h diff --git a/SW/ESPlan_RS485_Relay/ESPlan_RS485_Relay.ino b/SW/ESPlan_RS485_Relay/ESPlan_RS485_Relay.ino new file mode 100755 index 0000000..a7847fc --- /dev/null +++ b/SW/ESPlan_RS485_Relay/ESPlan_RS485_Relay.ino @@ -0,0 +1,141 @@ +/* + * ESPlan_RS485_Relay.ino + * 8 / 16-kanalovy RS485 rele modul (Modbus-RTU) + * + * Hardware : LaskaKit ESPlan (ESP32 + LAN8720) + * Modul : 8-ch / 16-ch 12V rele 250VAC/10A RS485 DIN + * https://www.laskakit.cz/8-kanalu-12v-rele-modul-250vac-10a--rs485--din/ + * + * Soubory projektu: + * cfg.h - HW konstanty, registry, datove struktury + * mb_client.h/cpp - Modbus RTU klient (FC03, FC06) + * relay.h/cpp - Ovladani rele (set/toggle/latch/momentary/delay/all/read) + * i18n_str.h - Preklady CZ / EN + * ws_server.h/cpp - HTTP webserver + * + * Modbus protokol (datasheet 8ch_rele_rs485_protocols.pdf): + * FC 0x06 (Write Single Register), 8N1 + * Adresa modulu se nastavuje DIP prepinaci A0-A5 (rozsah 0-47) + * Baudrate (HW pady M1/M2): 9600 (default) / 2400 / 4800 / 19200 + * + * reg 0x0001..0x0010 - rele 1..16 (jednotlive kanaly) + * val 0x0100 = Open (sepne) + * val 0x0200 = Close (rozepne) + * val 0x0300 = Toggle (self-locking) + * val 0x0400 = Latch (inter-locking) + * val 0x0500 = Momentary (1s puls) + * val 0x06xx = Delay xx s (0-255) + * reg 0x0000 - vsechny kanaly + * val 0x0700 = Vsechny ON + * val 0x0800 = Vsechny OFF + * + * FC 0x03 cteni stavu: reg 0x0001..0x0010, vrati 0x0001=ON / 0x0000=OFF + */ + +#include +#include +#include +#include + +#include "cfg.h" +#include "mb_client.h" +#include "relay.h" +#include "ws_server.h" + +// ─── Globalni objekty ───────────────────────────────────────────────────────── +Preferences prefs; +RelayState relayState; +RelayConfig relayCfg; +ModbusClient modbusClient; + +static bool ethGotIP = false; +static uint32_t lastReadMs = 0; +static const uint32_t READ_INTERVAL_MS = 5000; // 5s pravidelna sync z modulu + +// ─── ETH udalosti ───────────────────────────────────────────────────────────── +void onEthEvent(WiFiEvent_t event) { + switch (event) { + case ARDUINO_EVENT_ETH_START: + Serial.println(F("[ETH] Inicializace...")); + ETH.setHostname("espplan-relay"); + break; + case ARDUINO_EVENT_ETH_CONNECTED: + Serial.println(F("[ETH] Pripojen (fyzicka vrstva)")); + break; + case ARDUINO_EVENT_ETH_GOT_IP: + Serial.print(F("[ETH] IP: ")); + Serial.println(ETH.localIP()); + ethGotIP = true; + break; + case ARDUINO_EVENT_ETH_DISCONNECTED: + Serial.println(F("[ETH] Odpojeno")); + ethGotIP = false; + break; + case ARDUINO_EVENT_ETH_STOP: + Serial.println(F("[ETH] Zastaveno")); + break; + default: + break; + } +} + +// ─── Setup ──────────────────────────────────────────────────────────────────── +void setup() { + Serial.begin(115200); + delay(500); + Serial.println(F("\n=== ESPlan RS485 Relay 8/16ch ===")); + Serial.println(F(" LaskaKit 8/16-ch Modbus-RTU ")); + Serial.println(F("=============================")); + + // Nacteni konfigurace z NVS + prefs.begin("rs485rel", true); + loadConfig(prefs, relayCfg); + prefs.end(); + + Serial.printf("[CFG] Modbus addr=%d baud=%d\n", + relayCfg.modbusAddr, + relayCfg.baudRate); + + // RS485 / Modbus + modbusClient.begin(relayCfg.baudRate, relayCfg.modbusAddr); + + // ETH + WiFi.onEvent(onEthEvent); + ETH.begin(ETH_PHY_TYPE, + ETH_PHY_ADDR, + ETH_PHY_MDC, + ETH_PHY_MDIO, + ETH_PHY_POWER, + ETH_CLK_MODE); + + uint32_t t0 = millis(); + while (!ethGotIP && (millis() - t0 < 10000)) { + delay(100); + } + if (!ethGotIP) { + Serial.println(F("[ETH] VAROVANI: IP neziskano - web nebude dostupny!")); + } + + // Webserver + webServerBegin(relayState, relayCfg, prefs, modbusClient); + + // Pokus o nacteni stavu (pokud modul podporuje FC03) + Serial.println(F("[REL] Pokus o sync stavu z modulu...")); + relayReadAll(modbusClient, relayState, relayCfg); + + Serial.println(F("[SYS] Inicializace dokoncena.")); +} + +// ─── Loop ───────────────────────────────────────────────────────────────────── +void loop() { + webServerHandle(); + + // Periodicky pokus o aktualizaci stavu (volitelne) + uint32_t now = millis(); + if (now - lastReadMs >= READ_INTERVAL_MS) { + lastReadMs = now; + relayReadAll(modbusClient, relayState, relayCfg); + } + + delay(5); +} diff --git a/SW/ESPlan_RS485_Relay/cfg.h b/SW/ESPlan_RS485_Relay/cfg.h new file mode 100755 index 0000000..2d8e5d2 --- /dev/null +++ b/SW/ESPlan_RS485_Relay/cfg.h @@ -0,0 +1,132 @@ +#ifndef RL_CFG_H +#define RL_CFG_H +/* + * cfg.h – Konfigurace HW a datove struktury + * + * Zarizeni: 8 / 16-kanalovy RS485 rele modul (Modbus-RTU) + * https://www.laskakit.cz/8-kanalu-12v-rele-modul-250vac-10a--rs485--din/ + * + * Protokol (datasheet 8ch_rele_rs485_protocols.pdf): + * FC 0x06 (Write Single Register), 8N1 + * Adresa: 0x00-0x2F (0-47) – nastavena DIP prepinaci A0-A5 (HW) + * Baudrate: 9600 / 2400 / 4800 / 19200 (vychozi 9600, nastavi se M1/M2 pady) + * + * Jednotlivy kanal (reg 0x0001 .. 0x000N): + * val_hi = funkce, val_lo = parametr/delay + * + * 0x0100 Open (sepne) + * 0x0200 Close (rozepne) + * 0x0300 Toggle (self-locking) + * 0x0400 Latch (inter-locking; jen tento kanal ON, ostatni OFF) + * 0x0500 Momentary (non-locking, 1s puls) + * 0x06xx Delay XX s (xx = delka v sekundach 0x00..0xFF) + * + * Vsechny kanaly (reg 0x0000): + * 0x0700 All ON + * 0x0800 All OFF + * + * Cteni stavu (FC 0x03): + * reg 0x0001 .. 0x000N, vrati 0x0001 (ON) / 0x0000 (OFF) + * + * Adresa zarizeni se NEMENI Modbusem – nastavi DIP prepinacem! + */ + +#include +#include + +// ─── ETH (LAN8720 na ESPlan) ────────────────────────────────────────────────── +#define ETH_PHY_TYPE ETH_PHY_LAN8720 +#define ETH_PHY_ADDR 0 +#define ETH_PHY_MDC 23 +#define ETH_PHY_MDIO 18 +#define ETH_PHY_POWER -1 +#define ETH_CLK_MODE ETH_CLOCK_GPIO17_OUT + +// ─── RS485 / UART ───────────────────────────────────────────────────────────── +#define PIN_RS485_RX 36 +#define PIN_RS485_TX 4 + +// ─── Modbus vychozi hodnoty ─────────────────────────────────────────────────── +#define DEFAULT_MODBUS_ADDR 1 +#define DEFAULT_BAUD_RATE 9600 +#define DEFAULT_CHANNEL_COUNT 8 // 8 nebo 16 +#define MAX_CHANNEL_COUNT 16 +#define MODBUS_ADDR_MIN 0 +#define MODBUS_ADDR_MAX 47 // 0x2F dle datasheetu (DIP A0-A5) + +// ─── Registry rele modulu ──────────────────────────────────────────────────── +#define REG_ALL_CHANNELS 0x0000 +#define REG_CHANNEL_BASE 0x0001 // reg 0x0001 = ch1, az 0x0010 = ch16 + +// ─── Funkcni kody (val_hi) ─────────────────────────────────────────────────── +#define FN_OPEN 0x01 +#define FN_CLOSE 0x02 +#define FN_TOGGLE 0x03 +#define FN_LATCH 0x04 +#define FN_MOMENTARY 0x05 +#define FN_DELAY 0x06 // val_lo = delka v s (0-255) +#define FN_ALL_ON 0x07 // jen s reg 0x0000 +#define FN_ALL_OFF 0x08 // jen s reg 0x0000 + +// helper na slozeni val pole pro Modbus zapis +inline uint16_t makeVal(uint8_t fn, uint8_t param = 0) { + return ((uint16_t)fn << 8) | param; +} + +// ─── Stav rele modulu ──────────────────────────────────────────────────────── +struct RelayState { + bool channels[MAX_CHANNEL_COUNT] = { false }; // SW shadow (true = ON) + uint32_t lastUpdateMs = 0; + uint32_t lastCmdMs = 0; + bool commError = false; + String errorMsg = ""; + String lastAction = ""; // pro UI +}; + +// ─── Konfigurace (uklada se do NVS) ────────────────────────────────────────── +struct RelayConfig { + uint8_t modbusAddr = DEFAULT_MODBUS_ADDR; + uint32_t baudRate = DEFAULT_BAUD_RATE; + uint8_t channelCount = DEFAULT_CHANNEL_COUNT; // 8 nebo 16 + uint8_t defaultDelay = 10; // pro tlacitko Delay (sekundy) + + // Nazvy kanalu (max 15 znaku + \0) + char names[MAX_CHANNEL_COUNT][16] = { + "Rele 1", "Rele 2", "Rele 3", "Rele 4", + "Rele 5", "Rele 6", "Rele 7", "Rele 8", + "Rele 9", "Rele 10", "Rele 11", "Rele 12", + "Rele 13", "Rele 14", "Rele 15", "Rele 16" + }; +}; + +// ─── NVS load / save ───────────────────────────────────────────────────────── +inline void loadConfig(Preferences &p, RelayConfig &c) { + c.modbusAddr = p.getUChar("mbAddr", DEFAULT_MODBUS_ADDR); + c.baudRate = p.getUInt ("baud", DEFAULT_BAUD_RATE); + c.channelCount = p.getUChar("chCount", DEFAULT_CHANNEL_COUNT); + if (c.channelCount != 8 && c.channelCount != 16) c.channelCount = 8; + c.defaultDelay = p.getUChar("defDelay", 10); + + for (uint8_t i = 0; i < MAX_CHANNEL_COUNT; i++) { + char key[8]; + snprintf(key, sizeof(key), "n%u", i); + String def = String("Rele ") + (i + 1); + String v = p.getString(key, def); + strncpy(c.names[i], v.c_str(), 15); + c.names[i][15] = 0; + } +} + +inline void saveConfig(Preferences &p, const RelayConfig &c) { + p.putUChar("mbAddr", c.modbusAddr); + p.putUInt ("baud", c.baudRate); + p.putUChar("chCount", c.channelCount); + p.putUChar("defDelay", c.defaultDelay); + for (uint8_t i = 0; i < MAX_CHANNEL_COUNT; i++) { + char key[8]; + snprintf(key, sizeof(key), "n%u", i); + p.putString(key, c.names[i]); + } +} + +#endif // RL_CFG_H diff --git a/SW/ESPlan_RS485_Relay/i18n_str.h b/SW/ESPlan_RS485_Relay/i18n_str.h new file mode 100755 index 0000000..7f945cd --- /dev/null +++ b/SW/ESPlan_RS485_Relay/i18n_str.h @@ -0,0 +1,187 @@ +#ifndef RL_I18N_STR_H +#define RL_I18N_STR_H +/* + * i18n_str.h – Preklady webove rozhrani (CZ / EN) + * pro 8/16-kanalovy RS485 rele modul + */ + +#include + +struct I18nStrings { + // Navigace + const char* title; + const char* navStatus; + const char* navConfig; + const char* langSwitch; + + // Stavova stranka + const char* pageStatusTitle; + const char* sectionRelays; + const char* sectionActions; + const char* sectionAdvanced; + const char* colChannel; + const char* colName; + const char* colState; + const char* colCtrl; + const char* stateOn; + const char* stateOff; + const char* btnOn; + const char* btnOff; + const char* btnToggle; + const char* btnLatch; + const char* btnMomentary; + const char* btnDelay; + const char* btnAllOn; + const char* btnAllOff; + const char* commError; + const char* lastAction; + const char* labelDelay; + const char* labelDelaySec; + const char* labelLatchHelp; + const char* labelMomentaryHelp; + const char* labelDelayHelp; + + // Konfigurace + const char* pageConfigTitle; + const char* sectionComm; + const char* cfgAddr; + const char* cfgAddrHelp; + const char* cfgBaud; + const char* cfgBaudHelp; + const char* cfgChannels; + const char* cfgChannelsHelp; + const char* cfgDefDelay; + const char* cfgDefDelayHelp; + + const char* sectionNames; + const char* cfgNameHelp; + + const char* sectionInfo; + const char* infoDip; + + const char* btnSave; + + const char* sectionRaw; +}; + +// ─── Cestina ────────────────────────────────────────────────────────────────── +static const I18nStrings STR_CZ = { + "ESPlan RS485 rele", + "Ovladani", + "Konfigurace", + "English", + + "Ovladani rele", + "Stav kanalu", + "Hromadne ovladani", + "Specialni rezimy", + "#", + "Nazev", + "Stav", + "Akce", + "ON", + "OFF", + "ON", + "OFF", + "Toggle", + "Latch", + "Pulse", + "Delay", + "Vsechny ON", + "Vsechny OFF", + "⚠ Chyba komunikace", + "Posledni akce", + "Delay (sekundy)", + "s", + "Latch (inter-locking) sepne jen vybrany kanal a ostatni rozepne.", + "Pulse (momentary) sepne kanal na 1 sekundu, pak automaticky rozepne.", + "Delay sepne kanal, po zadanem case ho automaticky rozepne (0-255 s).", + + "Konfigurace", + "Komunikacni parametry", + "Modbus adresa", + "Adresa modulu nastavena DIP prepinaci A0-A5 na desce (0-47).", + "Prenosova rychlost", + "Baudrate (nastavi se HW pady M1/M2): 9600 (default), 2400, 4800, 19200.", + "Pocet kanalu", + "8-kanalova nebo 16-kanalova varianta modulu.", + "Vychozi Delay", + "Pocatecni hodnota Delay tlacitka (0-255 s).", + + "Nazvy kanalu", + "Pojmenovani jednotlivych kanalu (max 15 znaku)", + + "Informace o zarizeni", + "Adresa Modbus modulu se nastavuje DIP prepinaci A0-A5 (binarne, 6 bitu). " + "Baudrate se nastavi propojkami M1/M2: 9600 / 2400 / 4800 / 19200. " + "Modul take podporuje AT prikazy pres UART, pokud zkratite pady M0 - v tom rezimu " + "Slave ID nepouziva, a tento ESP32 program ho neumi (pouziva pouze Modbus-RTU).", + + "Ulozit konfiguraci", + + "RS485 komunikace (raw)" +}; + +// ─── Anglictina ─────────────────────────────────────────────────────────────── +static const I18nStrings STR_EN = { + "ESPlan RS485 relay", + "Control", + "Configuration", + "Cesky", + + "Relay control", + "Channel state", + "Bulk control", + "Advanced modes", + "#", + "Name", + "State", + "Action", + "ON", + "OFF", + "ON", + "OFF", + "Toggle", + "Latch", + "Pulse", + "Delay", + "All ON", + "All OFF", + "⚠ Communication error", + "Last action", + "Delay (seconds)", + "s", + "Latch (inter-locking) turns ON only the selected channel and turns OFF all others.", + "Pulse (momentary) closes the channel for 1 second, then opens automatically.", + "Delay closes the channel and reopens it after the given time (0-255 s).", + + "Configuration", + "Communication Settings", + "Modbus address", + "Module address is set by DIP switches A0-A5 on the board (0-47).", + "Baud rate", + "Baud rate (set by HW pads M1/M2): 9600 (default), 2400, 4800, 19200.", + "Channel count", + "8-channel or 16-channel variant.", + "Default Delay", + "Initial value for the Delay button (0-255 s).", + + "Channel names", + "Name each channel (max 15 chars)", + + "Device info", + "Module Modbus address is set by DIP switches A0-A5 (6-bit binary). " + "Baud rate is selected by jumper pads M1/M2: 9600 / 2400 / 4800 / 19200. " + "The module also supports AT commands over UART when M0 pads are shorted - " + "Slave ID is then unused, and this firmware does not use that mode (Modbus-RTU only).", + + "Save configuration", + + "RS485 Communication (raw)" +}; + +inline const I18nStrings& getStrings(bool isCz) { + return isCz ? STR_CZ : STR_EN; +} + +#endif // RL_I18N_STR_H diff --git a/SW/ESPlan_RS485_Relay/mb_client.cpp b/SW/ESPlan_RS485_Relay/mb_client.cpp new file mode 100755 index 0000000..5dc9ed7 --- /dev/null +++ b/SW/ESPlan_RS485_Relay/mb_client.cpp @@ -0,0 +1,158 @@ +/* + * mb_client.cpp – Implementace Modbus RTU klienta + */ + +#include "mb_client.h" + +uint16_t ModbusClient::crc16(const uint8_t* data, uint8_t len) { + uint16_t crc = 0xFFFF; + for (uint8_t i = 0; i < len; i++) { + crc ^= (uint16_t)data[i]; + for (uint8_t b = 0; b < 8; b++) { + if (crc & 0x0001) crc = (crc >> 1) ^ 0xA001; + else crc >>= 1; + } + } + return crc; +} + +void ModbusClient::flushRx() { + while (_serial->available()) _serial->read(); +} + +void ModbusClient::begin(uint32_t baud, uint8_t /*addr*/) { + _baud = baud; + _serial = &Serial2; + _serial->begin(_baud, SERIAL_8N1, PIN_RS485_RX, PIN_RS485_TX); + Serial.printf("[MOD] RS485 init: %d baud, 8N1, RX=%d TX=%d\n", + _baud, PIN_RS485_RX, PIN_RS485_TX); +} + +void ModbusClient::setBaudRate(uint32_t baud) { + _baud = baud; + _serial->end(); + _serial->begin(_baud, SERIAL_8N1, PIN_RS485_RX, PIN_RS485_TX); + Serial.printf("[MOD] Baudrate zmenen na: %d\n", _baud); +} + +ModbusStatus ModbusClient::readRegisters(uint8_t slaveAddr, uint16_t startReg, + uint8_t count, uint16_t* out) { + if (!_serial || count == 0 || count > 64) return ModbusStatus::FRAME_ERROR; + + uint8_t req[8]; + req[0] = slaveAddr; + req[1] = 0x03; + req[2] = (startReg >> 8) & 0xFF; + req[3] = startReg & 0xFF; + req[4] = 0x00; + req[5] = count; + uint16_t c = crc16(req, 6); + req[6] = c & 0xFF; + req[7] = (c >> 8) & 0xFF; + + memcpy(lastTx, req, 8); + lastTxLen = 8; + + flushRx(); + _serial->write(req, 8); + _serial->flush(); + + uint8_t expectedLen = 3 + count * 2 + 2; + uint8_t buf[160]; + uint8_t rxLen = 0; + uint32_t t0 = millis(); + + while (rxLen < expectedLen && (millis() - t0 < TIMEOUT_MS)) { + if (_serial->available()) buf[rxLen++] = _serial->read(); + yield(); + } + + uint8_t copyLen = (rxLen < sizeof(lastRx)) ? rxLen : sizeof(lastRx); + memcpy(lastRx, buf, copyLen); + lastRxLen = copyLen; + + if (rxLen < expectedLen) { + lastResult = ModbusStatus::TIMEOUT; + return ModbusStatus::TIMEOUT; + } + + uint16_t rxCrc = (uint16_t)buf[rxLen-1] << 8 | buf[rxLen-2]; + uint16_t calcCrc = crc16(buf, rxLen - 2); + if (rxCrc != calcCrc) { + lastResult = ModbusStatus::CRC_ERROR; + return ModbusStatus::CRC_ERROR; + } + + if (buf[1] & 0x80) { + lastResult = ModbusStatus::EXCEPTION; + return ModbusStatus::EXCEPTION; + } + + lastResult = ModbusStatus::OK; + for (uint8_t i = 0; i < count; i++) { + out[i] = (uint16_t)buf[3 + i*2] << 8 | buf[4 + i*2]; + } + return ModbusStatus::OK; +} + +ModbusStatus ModbusClient::writeRegister(uint8_t slaveAddr, uint16_t reg, uint16_t value) { + if (!_serial) return ModbusStatus::FRAME_ERROR; + + uint8_t req[8]; + req[0] = slaveAddr; + req[1] = 0x06; + req[2] = (reg >> 8) & 0xFF; + req[3] = reg & 0xFF; + req[4] = (value >> 8) & 0xFF; + req[5] = value & 0xFF; + uint16_t c = crc16(req, 6); + req[6] = c & 0xFF; + req[7] = (c >> 8) & 0xFF; + + memcpy(lastTx, req, 8); + lastTxLen = 8; + + flushRx(); + _serial->write(req, 8); + _serial->flush(); + + // Pri broadcast adrese 0x00 nepriche odpoved + if (slaveAddr == 0x00) { + lastRxLen = 0; + lastResult = ModbusStatus::OK; + delay(50); + return ModbusStatus::OK; + } + + uint8_t buf[8]; + uint8_t rxLen = 0; + uint32_t t0 = millis(); + + while (rxLen < 8 && (millis() - t0 < TIMEOUT_MS)) { + if (_serial->available()) buf[rxLen++] = _serial->read(); + yield(); + } + + memcpy(lastRx, buf, rxLen); + lastRxLen = rxLen; + + if (rxLen < 8) { lastResult = ModbusStatus::TIMEOUT; return ModbusStatus::TIMEOUT; } + + uint16_t rxCrc = (uint16_t)buf[7] << 8 | buf[6]; + uint16_t calcCrc = crc16(buf, 6); + if (rxCrc != calcCrc) { lastResult = ModbusStatus::CRC_ERROR; return ModbusStatus::CRC_ERROR; } + + lastResult = ModbusStatus::OK; + return ModbusStatus::OK; +} + +const char* ModbusClient::statusStr(ModbusStatus s) { + switch (s) { + case ModbusStatus::OK: return "OK"; + case ModbusStatus::TIMEOUT: return "TIMEOUT"; + case ModbusStatus::CRC_ERROR: return "CRC_ERROR"; + case ModbusStatus::EXCEPTION: return "EXCEPTION"; + case ModbusStatus::FRAME_ERROR: return "FRAME_ERROR"; + default: return "UNKNOWN"; + } +} diff --git a/SW/ESPlan_RS485_Relay/mb_client.h b/SW/ESPlan_RS485_Relay/mb_client.h new file mode 100755 index 0000000..a00e832 --- /dev/null +++ b/SW/ESPlan_RS485_Relay/mb_client.h @@ -0,0 +1,54 @@ +#ifndef RL_MB_CLIENT_H +#define RL_MB_CLIENT_H +/* + * mb_client.h – Modbus RTU klient pres RS485 / HardwareSerial + * + * FC03 – Read Holding Registers + * FC06 – Write Single Register + * + * DE pin rizen hardwarove – nevyzaduje softwarove prepinani. + */ + +#include +#include "cfg.h" + +enum class ModbusStatus : uint8_t { + OK = 0, + TIMEOUT, + CRC_ERROR, + EXCEPTION, + FRAME_ERROR +}; + +class ModbusClient { +public: + ModbusClient() = default; + + void begin(uint32_t baud = 9600, uint8_t addr = 1); + + ModbusStatus readRegisters(uint8_t slaveAddr, uint16_t startReg, + uint8_t count, uint16_t* out); + + ModbusStatus writeRegister(uint8_t slaveAddr, uint16_t reg, uint16_t value); + + void setBaudRate(uint32_t baud); + + const char* statusStr(ModbusStatus s); + + // ── Posledni TX/RX frame ──────────────────────────────────────────── + uint8_t lastTx[16]; + uint8_t lastTxLen = 0; + uint8_t lastRx[80]; + uint8_t lastRxLen = 0; + ModbusStatus lastResult = ModbusStatus::OK; + +private: + HardwareSerial* _serial = nullptr; + uint32_t _baud = 9600; + static const uint16_t TIMEOUT_MS = 500; + + uint16_t crc16(const uint8_t* data, uint8_t len); + void flushRx(); +}; + +#endif // RL_MB_CLIENT_H diff --git a/SW/ESPlan_RS485_Relay/relay.cpp b/SW/ESPlan_RS485_Relay/relay.cpp new file mode 100755 index 0000000..17d43e3 --- /dev/null +++ b/SW/ESPlan_RS485_Relay/relay.cpp @@ -0,0 +1,114 @@ +/* + * relay.cpp – Ovladani RS485 rele modulu (8 / 16 kanalu) + */ + +#include "relay.h" + +static bool sendCmd(ModbusClient& mb, RelayState& st, uint8_t addr, + uint16_t reg, uint16_t val, const String& actionDesc) { + ModbusStatus s = mb.writeRegister(addr, reg, val); + st.lastCmdMs = millis(); + st.lastAction = actionDesc; + if (s == ModbusStatus::OK) { + st.commError = false; + st.errorMsg = ""; + Serial.printf("[REL] %s : OK\n", actionDesc.c_str()); + return true; + } else { + st.commError = true; + st.errorMsg = mb.statusStr(s); + Serial.printf("[REL] %s : %s\n", actionDesc.c_str(), mb.statusStr(s)); + return false; + } +} + +static inline bool validCh(uint8_t channel, const RelayConfig& cfg) { + return (channel >= 1 && channel <= cfg.channelCount); +} + +bool relaySet(ModbusClient& mb, RelayState& st, const RelayConfig& cfg, + uint8_t channel, bool on) { + if (!validCh(channel, cfg)) return false; + uint16_t reg = REG_CHANNEL_BASE + (channel - 1); + uint16_t val = makeVal(on ? FN_OPEN : FN_CLOSE); + String d = String("ch") + channel + (on ? " ON" : " OFF"); + bool ok = sendCmd(mb, st, cfg.modbusAddr, reg, val, d); + if (ok) st.channels[channel - 1] = on; + return ok; +} + +bool relayToggle(ModbusClient& mb, RelayState& st, const RelayConfig& cfg, + uint8_t channel) { + if (!validCh(channel, cfg)) return false; + uint16_t reg = REG_CHANNEL_BASE + (channel - 1); + uint16_t val = makeVal(FN_TOGGLE); + String d = String("ch") + channel + " TOGGLE"; + bool ok = sendCmd(mb, st, cfg.modbusAddr, reg, val, d); + if (ok) st.channels[channel - 1] = !st.channels[channel - 1]; + return ok; +} + +bool relayLatch(ModbusClient& mb, RelayState& st, const RelayConfig& cfg, + uint8_t channel) { + if (!validCh(channel, cfg)) return false; + uint16_t reg = REG_CHANNEL_BASE + (channel - 1); + uint16_t val = makeVal(FN_LATCH); + String d = String("ch") + channel + " LATCH"; + bool ok = sendCmd(mb, st, cfg.modbusAddr, reg, val, d); + if (ok) { + // Inter-locking: jen tento kanal ON, ostatni OFF + for (uint8_t i = 0; i < cfg.channelCount; i++) { + st.channels[i] = (i == (channel - 1)); + } + } + return ok; +} + +bool relayMomentary(ModbusClient& mb, RelayState& st, const RelayConfig& cfg, + uint8_t channel) { + if (!validCh(channel, cfg)) return false; + uint16_t reg = REG_CHANNEL_BASE + (channel - 1); + uint16_t val = makeVal(FN_MOMENTARY); + String d = String("ch") + channel + " MOMENTARY (1s)"; + // Pri momentary se rele po 1s vrati do OFF. SW shadow nemenime – + // dalsi periodicky FC03 sync stav doplni. + return sendCmd(mb, st, cfg.modbusAddr, reg, val, d); +} + +bool relayDelay(ModbusClient& mb, RelayState& st, const RelayConfig& cfg, + uint8_t channel, uint8_t seconds) { + if (!validCh(channel, cfg)) return false; + uint16_t reg = REG_CHANNEL_BASE + (channel - 1); + uint16_t val = makeVal(FN_DELAY, seconds); + String d = String("ch") + channel + " DELAY " + seconds + "s"; + bool ok = sendCmd(mb, st, cfg.modbusAddr, reg, val, d); + if (ok) st.channels[channel - 1] = true; // okamzite ON, pak FC03 zjisti OFF + return ok; +} + +bool relayAll(ModbusClient& mb, RelayState& st, const RelayConfig& cfg, bool on) { + uint16_t val = makeVal(on ? FN_ALL_ON : FN_ALL_OFF); + String d = on ? "ALL ON" : "ALL OFF"; + bool ok = sendCmd(mb, st, cfg.modbusAddr, REG_ALL_CHANNELS, val, d); + if (ok) { + for (uint8_t i = 0; i < cfg.channelCount; i++) st.channels[i] = on; + } + return ok; +} + +bool relayReadAll(ModbusClient& mb, RelayState& st, const RelayConfig& cfg) { + uint16_t r[MAX_CHANNEL_COUNT] = {0}; + ModbusStatus s = mb.readRegisters(cfg.modbusAddr, + REG_CHANNEL_BASE, cfg.channelCount, r); + st.lastUpdateMs = millis(); + if (s != ModbusStatus::OK) { + Serial.printf("[REL] Cteni stavu: %s (shadow zachovan)\n", + mb.statusStr(s)); + return false; + } + // Dle datasheetu: 0x0001 = open(ON), 0x0000 = close(OFF) + for (uint8_t i = 0; i < cfg.channelCount; i++) { + st.channels[i] = (r[i] != 0); + } + return true; +} diff --git a/SW/ESPlan_RS485_Relay/relay.h b/SW/ESPlan_RS485_Relay/relay.h new file mode 100755 index 0000000..f67831d --- /dev/null +++ b/SW/ESPlan_RS485_Relay/relay.h @@ -0,0 +1,40 @@ +#ifndef RL_RELAY_H +#define RL_RELAY_H +/* + * relay.h – Ovladani RS485 rele modulu (8 / 16 kanalu) + * + * Vsechny funkce vraci true pri OK odpovedi z modulu. SW stav + * v RelayState.channels[] se aktualizuje pouze pri OK. + */ + +#include "cfg.h" +#include "mb_client.h" + +// Open / Close (sepne / rozepne) +bool relaySet (ModbusClient& mb, RelayState& st, const RelayConfig& cfg, + uint8_t channel /*1..N*/, bool on); + +// Toggle (self-locking) – prepne stav +bool relayToggle (ModbusClient& mb, RelayState& st, const RelayConfig& cfg, + uint8_t channel); + +// Latch (inter-locking) – jen tento kanal ON, ostatni OFF +bool relayLatch (ModbusClient& mb, RelayState& st, const RelayConfig& cfg, + uint8_t channel); + +// Momentary (non-locking) – 1s puls +bool relayMomentary(ModbusClient& mb, RelayState& st, const RelayConfig& cfg, + uint8_t channel); + +// Delay – sepne na zadany pocet sekund (0-255), pak automaticky rozepne +bool relayDelay (ModbusClient& mb, RelayState& st, const RelayConfig& cfg, + uint8_t channel, uint8_t seconds); + +// Vsechny kanaly ON / OFF +bool relayAll (ModbusClient& mb, RelayState& st, const RelayConfig& cfg, + bool on); + +// Cteni stavu vsech aktivnich kanalu pres FC03 +bool relayReadAll (ModbusClient& mb, RelayState& st, const RelayConfig& cfg); + +#endif // RL_RELAY_H diff --git a/SW/ESPlan_RS485_Relay/ws_server.cpp b/SW/ESPlan_RS485_Relay/ws_server.cpp new file mode 100755 index 0000000..a8b4f26 --- /dev/null +++ b/SW/ESPlan_RS485_Relay/ws_server.cpp @@ -0,0 +1,661 @@ +/* + * ws_server.cpp – HTTP webserver pro RS485 rele modul (8 / 16 kanalu) + */ + +#include "ws_server.h" +#include + +// ─── Globalni pointery ──────────────────────────────────────────────────────── +static WebServer* gServer = nullptr; +static RelayState* gState = nullptr; +static RelayConfig* gCfg = nullptr; +static Preferences* gPrefs = nullptr; +static ModbusClient* gMb = nullptr; + +// ─── Pomocne ───────────────────────────────────────────────────────────────── +static bool isLangCz() { + if (gServer->hasArg("lang")) { + String l = gServer->arg("lang"); + return !(l == "en"); + } + if (gServer->hasHeader("Cookie")) { + String c = gServer->header("Cookie"); + if (c.indexOf("lang=en") >= 0) return false; + } + return true; +} + +static void sendLangCookie(bool cz) { + gServer->sendHeader("Set-Cookie", + cz ? "lang=cs; Path=/; Max-Age=31536000" + : "lang=en; Path=/; Max-Age=31536000"); +} + +// ─── CSS ───────────────────────────────────────────────────────────────────── +static const char CSS[] PROGMEM = R"CSS( +:root{ + --bg:#0d1117;--bg2:#161b22;--bg3:#21262d; + --border:#30363d;--border2:#484f58; + --text:#e6edf3;--text2:#8b949e;--text3:#6e7681; + --accent:#58a6ff;--accent2:#1f6feb; + --green:#3fb950;--red:#f85149;--yellow:#d29922;--purple:#a371f7; +} +*{box-sizing:border-box;margin:0;padding:0} +body{background:var(--bg);color:var(--text);font-family:'Segoe UI',system-ui,sans-serif; + min-height:100vh;font-size:14px;line-height:1.5} +a{color:var(--accent);text-decoration:none} +a:hover{text-decoration:underline} +.topbar{background:var(--bg2);border-bottom:1px solid var(--border); + padding:0 24px;display:flex;align-items:center;height:52px; + position:sticky;top:0;z-index:100} +.logo{display:flex;align-items:center;gap:10px;font-weight:700;font-size:15px; + color:var(--text);margin-right:32px;white-space:nowrap} +.logo-icon{width:28px;height:28px;background:#1f6feb;border-radius:6px; + display:flex;align-items:center;justify-content:center;flex-shrink:0;color:#fff;font-weight:700} +.nav-links{display:flex;gap:2px;flex:1} +.nav-link{padding:6px 14px;border-radius:6px;font-size:13px; + color:var(--text2);transition:background .15s,color .15s;white-space:nowrap} +.nav-link:hover{background:var(--bg3);color:var(--text);text-decoration:none} +.nav-link.active{background:var(--accent2);color:#fff} +.lang-btn{margin-left:auto;padding:5px 12px;border-radius:20px; + background:var(--bg3);border:1px solid var(--border2); + color:var(--text2);font-size:12px;white-space:nowrap} +.lang-btn:hover{background:var(--border);color:var(--text);text-decoration:none} +.main{max-width:1100px;margin:0 auto;padding:28px 20px} +h1{font-size:20px;font-weight:600;margin-bottom:20px} +h2{font-size:12px;font-weight:600;color:var(--text3);text-transform:uppercase; + letter-spacing:.08em;margin:28px 0 12px;padding-bottom:8px; + border-bottom:1px solid var(--border)} +h2:first-of-type{margin-top:0} +.card{background:var(--bg2);border:1px solid var(--border); + border-radius:10px;padding:16px 20px;margin-bottom:16px} +table{width:100%;border-collapse:collapse;font-size:13px} +th{padding:8px 12px;text-align:left;font-weight:600;font-size:11px; + color:var(--text3);text-transform:uppercase;letter-spacing:.06em; + border-bottom:1px solid var(--border);white-space:nowrap} +td{padding:9px 12px;border-bottom:1px solid var(--border);vertical-align:middle} +tr:last-child td{border-bottom:none} +tr:hover td{background:rgba(255,255,255,.02)} +.ch-num{font-family:'Courier New',monospace;color:var(--text3);width:32px;text-align:center} +.state-on{display:inline-flex;align-items:center;gap:6px;color:var(--green);font-weight:600;font-size:12px} +.state-off{display:inline-flex;align-items:center;gap:6px;color:var(--text3);font-size:12px} +.dot{width:10px;height:10px;border-radius:50%;flex-shrink:0} +.dot-g{background:var(--green);box-shadow:0 0 8px var(--green)} +.dot-x{background:var(--text3)} +.err-banner{background:rgba(248,81,73,.1);border:1px solid var(--red); + border-radius:8px;padding:10px 16px;color:var(--red);font-size:13px; + margin-bottom:16px;display:flex;align-items:center;gap:8px} +.info-banner{background:rgba(88,166,255,.08);border:1px solid var(--border); + border-radius:8px;padding:12px 16px;color:var(--text2);font-size:12px; + margin-bottom:16px;line-height:1.6} +.ip-bar{display:flex;align-items:center;gap:20px;padding:10px 16px; + background:var(--bg3);border:1px solid var(--border);border-radius:8px; + font-family:'Courier New',monospace;font-size:12px;color:var(--text2); + margin-bottom:20px;flex-wrap:wrap} +.ip-lbl{color:var(--text3)} +.btn{display:inline-flex;align-items:center;gap:6px;padding:6px 12px; + border-radius:6px;font-size:12px;cursor:pointer;border:1px solid transparent; + font-family:inherit;transition:background .15s;text-decoration:none} +.btn:active{opacity:.8} +.btn-p{background:var(--accent2);color:#fff;border-color:var(--accent2)} +.btn-p:hover{background:#388bfd;text-decoration:none;color:#fff} +.btn-d{background:var(--bg3);color:var(--text);border-color:var(--border2)} +.btn-d:hover{background:var(--border);text-decoration:none} +.btn-w{background:transparent;color:var(--yellow);border-color:var(--yellow)} +.btn-w:hover{background:rgba(210,153,34,.1);text-decoration:none} +.btn-pp{background:transparent;color:var(--purple);border-color:var(--purple)} +.btn-pp:hover{background:rgba(163,113,247,.1);text-decoration:none;color:var(--purple)} +.btn-g{background:var(--green);color:#fff;border-color:var(--green)} +.btn-g:hover{background:#46c95a;text-decoration:none;color:#fff} +.btn-r{background:var(--red);color:#fff;border-color:var(--red)} +.btn-r:hover{background:#ff5e54;text-decoration:none;color:#fff} +.btn-sm{padding:4px 10px;font-size:11px} +.actions-row{display:flex;gap:5px;flex-wrap:wrap} +.bulk-row{display:flex;gap:12px;flex-wrap:wrap} +.fg{margin-bottom:14px} +label{display:block;font-size:13px;font-weight:500;color:var(--text2);margin-bottom:5px} +.ht{font-size:11px;color:var(--text3);margin-top:4px} +input[type=number],input[type=text],select{ + background:var(--bg3);border:1px solid var(--border2);border-radius:6px; + padding:7px 10px;color:var(--text);font-size:13px;width:100%; + max-width:280px;outline:none;transition:border-color .15s;font-family:inherit} +input[type=number]{max-width:120px} +input:focus,select:focus{border-color:var(--accent)} +select option{background:var(--bg3)} +.frow{display:flex;gap:16px;flex-wrap:wrap} +.frow .fg{flex:1;min-width:200px} +.name-input{max-width:180px;font-size:12px;padding:5px 8px} +.adv-row{display:flex;align-items:end;gap:8px;flex-wrap:wrap} +.adv-row .fg{margin-bottom:0;min-width:0} +.adv-inline{display:inline-flex;align-items:center;gap:6px} +.adv-inline input[type=number]{width:80px;max-width:80px;padding:5px 8px;font-size:12px} +.smeta{font-size:12px;color:var(--text3);font-family:'Courier New',monospace} +.raw-panel{background:var(--bg2);border:1px solid var(--border);border-radius:10px; + padding:14px 20px;margin-bottom:16px;font-family:'Courier New',monospace; + font-size:12px;line-height:1.8} +.raw-tx{color:var(--yellow)} +.raw-rx{color:var(--green)} +.raw-err{color:var(--red)} +.raw-label{color:var(--text3);display:inline-block;width:28px} +.raw-status{display:inline-block;padding:1px 6px;border-radius:3px;font-size:10px; + font-weight:600;text-transform:uppercase;margin-left:8px} +.raw-status.ok{background:rgba(63,185,80,.15);color:var(--green)} +.raw-status.err{background:rgba(248,81,73,.15);color:var(--red)} +@media(max-width:640px){ + .topbar{padding:0 12px} + .logo{margin-right:12px} + .main{padding:16px 12px} + th,td{padding:7px 6px} + .actions-row{gap:3px} + .btn{padding:5px 8px;font-size:11px} + input,select{max-width:100%} +} +)CSS"; + +// ─── HTML head + nav ───────────────────────────────────────────────────────── +static String htmlHead(const I18nStrings& s, bool cz, const char* page) { + String langUrl = cz ? "?lang=en" : "?lang=cs"; + String out; + out.reserve(3000); + + out += F("" + "" + ""); + out += s.title; + out += F("" + "
"); + return out; +} + +static String htmlFoot() { + return F("
"); +} + +// ─── Stranka: ovladani ──────────────────────────────────────────────────────── +static void handleRoot() { + bool cz = isLangCz(); + sendLangCookie(cz); + const I18nStrings& s = getStrings(cz); + + String html = htmlHead(s, cz, "status"); + + // IP bar + html += F("
" + "IP: "); + html += ETH.localIP().toString(); + html += F("MAC: "); + html += ETH.macAddress(); + html += F("Modbus: "); + html += gCfg->modbusAddr; + html += F(" @ "); + html += gCfg->baudRate; + html += F(" bpsCH: "); + html += gCfg->channelCount; + html += F("
"); + + if (gState->commError) { + html += F("
⚠ "); + html += s.commError; + html += F(": "); + html += gState->errorMsg; + html += F("
"); + } + + // Tabulka kanalu + html += F("

"); + html += s.sectionRelays; + html += F("

" + ""); + + String langInp = cz ? "cs" : "en"; + + for (uint8_t i = 0; i < gCfg->channelCount; i++) { + bool on = gState->channels[i]; + html += F(""); + } + html += F("
"); + html += s.colChannel; + html += F(""); + html += s.colName; + html += F(""); + html += s.colState; + html += F(""); + html += s.colCtrl; + html += F("
"); + html += (i + 1); + html += F(""); + html += gCfg->names[i]; + html += F(""); + if (on) { + html += F(""); + html += s.stateOn; + html += F(""); + } else { + html += F(""); + html += s.stateOff; + html += F(""); + } + html += F("
"); + + auto addBtn = [&](const char* act, const char* cls, const char* label) { + html += F("
" + "
"); + }; + + addBtn("on", "btn-g", s.btnOn); + addBtn("off", "btn-r", s.btnOff); + addBtn("toggle", "btn-d", s.btnToggle); + + html += F("
"); + + // Specialni rezimy (Latch / Momentary / Delay) ako kompaktni panel + html += F("

"); + html += s.sectionAdvanced; + html += F("

"); + html += F("
"); + html += s.labelLatchHelp; + html += F(" — "); + html += s.labelMomentaryHelp; + html += F(" — "); + html += s.labelDelayHelp; + html += F("
"); + + // Latch / Momentary / Delay – pro kazdy kanal radek + html += F(""); + for (uint8_t i = 0; i < gCfg->channelCount; i++) { + html += F(""); + } + html += F("
"); + html += s.colChannel; + html += F(""); + html += s.colName; + html += F(""); + html += s.colCtrl; + html += F("
"); + html += (i + 1); + html += F(""); + html += gCfg->names[i]; + html += F("
"); + + // Latch + html += F("
" + "" + "
"); + + // Momentary (Pulse) + html += F("
" + "" + "
"); + + // Delay s inputem sekund + html += F("
" + "" + "" + "
"); + + html += F("
"); + html += F("
"); + + // Hromadne ovladani + html += F("

"); + html += s.sectionActions; + html += F("

" + "
" + "" + "
" + "
" + "" + "
" + "
"); + if (gState->lastAction.length() > 0) { + html += F("
"); + html += s.lastAction; + html += F(": "); + html += gState->lastAction; + html += F("
"); + } + html += F("
"); + + // Raw RS485 panel + html += F("

"); + html += s.sectionRaw; + html += F("

" + "
TX: ..." + "
" + "
RX: ...
" + "
"); + + // JS: refresh stavu + raw panelu + html += F(""); + + html += htmlFoot(); + gServer->send(200, "text/html; charset=UTF-8", html); +} + +// ─── POST /relay ───────────────────────────────────────────────────────────── +static void handleRelay() { + bool cz = gServer->hasArg("lang") ? (gServer->arg("lang") != "en") : true; + String act = gServer->arg("act"); + uint8_t ch = (uint8_t)gServer->arg("ch").toInt(); + + if (act == "on" && ch >= 1 && ch <= gCfg->channelCount) { + relaySet(*gMb, *gState, *gCfg, ch, true); + } else if (act == "off" && ch >= 1 && ch <= gCfg->channelCount) { + relaySet(*gMb, *gState, *gCfg, ch, false); + } else if (act == "toggle" && ch >= 1 && ch <= gCfg->channelCount) { + relayToggle(*gMb, *gState, *gCfg, ch); + } else if (act == "latch" && ch >= 1 && ch <= gCfg->channelCount) { + relayLatch(*gMb, *gState, *gCfg, ch); + } else if (act == "momentary" && ch >= 1 && ch <= gCfg->channelCount) { + relayMomentary(*gMb, *gState, *gCfg, ch); + } else if (act == "delay" && ch >= 1 && ch <= gCfg->channelCount) { + uint8_t sec = (uint8_t)constrain(gServer->arg("sec").toInt(), 0, 255); + relayDelay(*gMb, *gState, *gCfg, ch, sec); + } else if (act == "all_on") { + relayAll(*gMb, *gState, *gCfg, true); + } else if (act == "all_off") { + relayAll(*gMb, *gState, *gCfg, false); + } + + sendLangCookie(cz); + gServer->sendHeader("Location", cz ? "/?lang=cs" : "/?lang=en"); + gServer->send(303); +} + +// ─── GET /config ────────────────────────────────────────────────────────────── +static void handleConfig() { + bool cz = isLangCz(); + sendLangCookie(cz); + const I18nStrings& s = getStrings(cz); + + String html = htmlHead(s, cz, "config"); + html += F("

"); + html += s.pageConfigTitle; + html += F("

"); + + // Info banner o DIP + html += F("
"); + html += s.infoDip; + html += F("
"); + + html += F("
"); + html += F(""); + + // Komunikace + html += F("

"); + html += s.sectionComm; + html += F("

"); + + html += F("
"); + html += s.cfgAddrHelp; + html += F("
"); + + html += F("
"); + html += s.cfgBaudHelp; + html += F("
"); + + html += F("
"); + html += s.cfgChannelsHelp; + html += F("
"); + + html += F("
"); + html += s.cfgDefDelayHelp; + html += F("
"); + + html += F("
"); + + // Nazvy kanalu + html += F("

"); + html += s.sectionNames; + html += F("

"); + html += s.cfgNameHelp; + html += F("
"); + for (uint8_t i = 0; i < gCfg->channelCount; i++) { + html += F("
"); + } + html += F("
"); + + html += F("
" + "
"); + + html += htmlFoot(); + gServer->send(200, "text/html; charset=UTF-8", html); +} + +// ─── POST /config ───────────────────────────────────────────────────────────── +static void handleConfigPost() { + bool cz = gServer->hasArg("lang") ? (gServer->arg("lang") != "en") : true; + + if (gServer->hasArg("addr")) { + gCfg->modbusAddr = (uint8_t)constrain(gServer->arg("addr").toInt(), + MODBUS_ADDR_MIN, MODBUS_ADDR_MAX); + } + if (gServer->hasArg("baud")) { + uint32_t b = (uint32_t)gServer->arg("baud").toInt(); + if (b != gCfg->baudRate) { gCfg->baudRate = b; gMb->setBaudRate(b); } + } + if (gServer->hasArg("chCount")) { + uint8_t c = (uint8_t)gServer->arg("chCount").toInt(); + gCfg->channelCount = (c == 16) ? 16 : 8; + } + if (gServer->hasArg("defDelay")) { + gCfg->defaultDelay = (uint8_t)constrain(gServer->arg("defDelay").toInt(), 0, 255); + } + for (uint8_t i = 0; i < MAX_CHANNEL_COUNT; i++) { + String key = String("n") + i; + if (gServer->hasArg(key.c_str())) { + String v = gServer->arg(key.c_str()); + strncpy(gCfg->names[i], v.c_str(), 15); + gCfg->names[i][15] = 0; + } + } + + gPrefs->begin("rs485rel", false); + saveConfig(*gPrefs, *gCfg); + gPrefs->end(); + Serial.println(F("[CFG] Konfigurace ulozena.")); + + sendLangCookie(cz); + gServer->sendHeader("Location", cz ? "/config?lang=cs" : "/config?lang=en"); + gServer->send(303); +} + +// ─── GET /api/state ────────────────────────────────────────────────────────── +static void handleApiState() { + String json = "{"; + json += "\"ts\":" + String(gState->lastUpdateMs); + json += ",\"err\":" + String(gState->commError ? "true" : "false"); + json += ",\"ch\":["; + for (uint8_t i = 0; i < gCfg->channelCount; i++) { + if (i > 0) json += ","; + json += gState->channels[i] ? "true" : "false"; + } + json += "],\"last\":\"" + gState->lastAction + "\""; + json += "}"; + + gServer->sendHeader("Access-Control-Allow-Origin", "*"); + gServer->send(200, "application/json", json); +} + +// ─── GET /api/raw ──────────────────────────────────────────────────────────── +static String hexBuf(const uint8_t* buf, uint8_t len) { + String s; + s.reserve(len * 3); + for (uint8_t i = 0; i < len; i++) { + char h[4]; + snprintf(h, sizeof(h), "%02X ", buf[i]); + s += h; + } + if (s.length() > 0) s.remove(s.length() - 1); + return s; +} + +static void handleApiRaw() { + String json = "{"; + json += "\"tx\":\"" + hexBuf(gMb->lastTx, gMb->lastTxLen) + "\""; + json += ",\"rx\":\"" + hexBuf(gMb->lastRx, gMb->lastRxLen) + "\""; + json += ",\"s\":\"" + String(gMb->statusStr(gMb->lastResult)) + "\""; + json += "}"; + gServer->sendHeader("Access-Control-Allow-Origin", "*"); + gServer->send(200, "application/json", json); +} + +// ─── Inicializace ───────────────────────────────────────────────────────────── +void webServerBegin(RelayState& state, RelayConfig& cfg, + Preferences& prefs, ModbusClient& mb) { + gState = &state; + gCfg = &cfg; + gPrefs = &prefs; + gMb = &mb; + + gServer = new WebServer(80); + static const char* hdrs[] = {"Cookie"}; + gServer->collectHeaders(hdrs, 1); + + gServer->on("/", HTTP_GET, handleRoot); + gServer->on("/relay", HTTP_POST, handleRelay); + gServer->on("/config", HTTP_GET, handleConfig); + gServer->on("/config", HTTP_POST, handleConfigPost); + gServer->on("/api/state", HTTP_GET, handleApiState); + gServer->on("/api/raw", HTTP_GET, handleApiRaw); + gServer->onNotFound([]() { + gServer->send(404, "text/plain", "Not found"); + }); + + gServer->begin(); + Serial.println(F("[WEB] HTTP server spusten na portu 80")); +} + +void webServerHandle() { + if (gServer) gServer->handleClient(); +} diff --git a/SW/ESPlan_RS485_Relay/ws_server.h b/SW/ESPlan_RS485_Relay/ws_server.h new file mode 100755 index 0000000..2d169ba --- /dev/null +++ b/SW/ESPlan_RS485_Relay/ws_server.h @@ -0,0 +1,28 @@ +#ifndef RL_WS_SERVER_H +#define RL_WS_SERVER_H +/* + * ws_server.h – HTTP webserver pro rele modul + * + * Stranky: + * GET / Ovladani rele (8 kanalu + hromadne) + * GET /config Konfigurace + * POST /config Ulozeni konfigurace + * POST /relay Akce: on/off/toggle/all_on/all_off + * POST /action Zmena adresy modulu + * GET /api/state JSON se stavem + * GET /api/raw JSON s poslednim RS485 TX/RX + */ + +#include +#include +#include "cfg.h" +#include "mb_client.h" +#include "relay.h" +#include "i18n_str.h" + +void webServerBegin(RelayState& state, RelayConfig& cfg, + Preferences& prefs, ModbusClient& mb); + +void webServerHandle(); + +#endif // RL_WS_SERVER_H diff --git a/SW/ESPlan_WeatherStation/ESPlan_WeatherStation.ino b/SW/ESPlan_WeatherStation/ESPlan_WeatherStation.ino new file mode 100755 index 0000000..ef2d1d5 --- /dev/null +++ b/SW/ESPlan_WeatherStation/ESPlan_WeatherStation.ino @@ -0,0 +1,125 @@ +/* + * ESPlan_WeatherStation.ino + * RS-FSXCS-N01-* Ultrazvuková meteorologická stanice + * + * Hardware : LaskaKit ESPlan (ESP32 + LAN8720) + * Senzor : RS-FSXCS-N01-* (Modbus RTU / RS485) + * Komunikace: pouze ETH (LAN8720) + * + * Soubory projektu: + * cfg.h – HW konstanty, registry, datové struktury + * mb_client.h/cpp– Modbus RTU klient (FC03, FC06) + * snr.h/cpp – Detekce variant a čtení snímače + * i18n_str.h – Překlady CZ / EN + * ws_server.h/cpp– HTTP webserver (stav + konfigurace) + */ + +#include +#include // nutné pro WiFiEvent_t a WiFi.onEvent +#include +#include + +#include "cfg.h" +#include "mb_client.h" +#include "snr.h" +#include "ws_server.h" + +// ─── Globální objekty ───────────────────────────────────────────────────────── +Preferences prefs; +SensorData sensorData; +SensorConfig sensorConfig; +ModbusClient modbusClient; + +static bool ethGotIP = false; +static uint32_t lastPollMs = 0; + +// ─── ETH události ───────────────────────────────────────────────────────────── +void onEthEvent(WiFiEvent_t event) { + switch (event) { + case ARDUINO_EVENT_ETH_START: + Serial.println(F("[ETH] Inicializace...")); + ETH.setHostname("espplan-weather"); + break; + case ARDUINO_EVENT_ETH_CONNECTED: + Serial.println(F("[ETH] Pripojen (fyzicka vrstva)")); + break; + case ARDUINO_EVENT_ETH_GOT_IP: + Serial.print(F("[ETH] IP: ")); + Serial.println(ETH.localIP()); + ethGotIP = true; + break; + case ARDUINO_EVENT_ETH_DISCONNECTED: + Serial.println(F("[ETH] Odpojeno")); + ethGotIP = false; + break; + case ARDUINO_EVENT_ETH_STOP: + Serial.println(F("[ETH] Zastaveno")); + break; + default: + break; + } +} + +// ─── Setup ──────────────────────────────────────────────────────────────────── +void setup() { + Serial.begin(115200); + delay(500); + Serial.println(F("\n=== ESPlan Weather Station ===")); + Serial.println(F(" RS-FSXCS-N01-* Modbus RTU ")); + Serial.println(F("==============================")); + + // Načtení konfigurace z NVS + prefs.begin("weather", true); + loadConfig(prefs, sensorConfig); + prefs.end(); + + Serial.printf("[CFG] Modbus addr=%d baud=%d poll=%ds\n", + sensorConfig.modbusAddr, + sensorConfig.baudRate, + sensorConfig.pollInterval); + + // RS485 / Modbus + modbusClient.begin(sensorConfig.baudRate, sensorConfig.modbusAddr); + + // ETH + WiFi.onEvent(onEthEvent); + ETH.begin(ETH_PHY_TYPE, + ETH_PHY_ADDR, + ETH_PHY_MDC, + ETH_PHY_MDIO, + ETH_PHY_POWER, + ETH_CLK_MODE); + + // Čekáme na IP (max 10 s) + uint32_t t0 = millis(); + while (!ethGotIP && (millis() - t0 < 10000)) { + delay(100); + } + + if (!ethGotIP) { + Serial.println(F("[ETH] VAROVANI: IP neziskano – web nebude dostupny!")); + } + + // Webserver + webServerBegin(sensorData, sensorConfig, prefs, modbusClient); + + // První detekce + Serial.println(F("[SNS] Detekuji senzory...")); + detectSensors(modbusClient, sensorData, sensorConfig); + printSensorStatus(sensorData); + + Serial.println(F("[SYS] Inicializace dokoncena.")); +} + +// ─── Loop ───────────────────────────────────────────────────────────────────── +void loop() { + webServerHandle(); + + uint32_t now = millis(); + if (now - lastPollMs >= (uint32_t)sensorConfig.pollInterval * 1000UL) { + lastPollMs = now; + readAllSensors(modbusClient, sensorData, sensorConfig); + } + + delay(5); +} diff --git a/SW/ESPlan_WeatherStation/cfg.h b/SW/ESPlan_WeatherStation/cfg.h new file mode 100755 index 0000000..47bd39f --- /dev/null +++ b/SW/ESPlan_WeatherStation/cfg.h @@ -0,0 +1,140 @@ +#ifndef WS_CFG_H +#define WS_CFG_H +/* + * cfg.h – Konfigurace hardwaru a datové struktury + * + * POZOR: Konfigurace HW pinů je pevná (nelze měnit přes web). + * Přes webové rozhraní lze měnit pouze komunikační parametry + * a kalibrační registry snímače. + */ + +#include +#include + +// ─── ETH (LAN8720 na ESPlan) ────────────────────────────────────────────────── +#define ETH_PHY_TYPE ETH_PHY_LAN8720 +#define ETH_PHY_ADDR 0 +#define ETH_PHY_MDC 23 +#define ETH_PHY_MDIO 18 +#define ETH_PHY_POWER -1 +#define ETH_CLK_MODE ETH_CLOCK_GPIO17_OUT + +// ─── RS485 / UART ───────────────────────────────────────────────────────────── +#define PIN_RS485_RX 36 +#define PIN_RS485_TX 4 +// DE pin je řízen hardwarově – nepotřebujeme softwarové ovládání + +// ─── Modbus výchozí hodnoty ─────────────────────────────────────────────────── +#define DEFAULT_MODBUS_ADDR 1 +#define DEFAULT_BAUD_RATE 4800 +#define DEFAULT_POLL_INTERVAL 5 // sekundy + +// ─── Modbus registry snímače (RS-FSXCS-N01-*) ──────────────────────────────── +// Čtecí registry (FC03 / FC04), base address 500 (0x01F4) +#define REG_WIND_SPEED 500 +#define REG_WIND_FORCE 501 +#define REG_WIND_DIR_8 502 +#define REG_WIND_DIR_360 503 +#define REG_HUMIDITY 504 +#define REG_TEMPERATURE 505 +#define REG_NOISE 506 +#define REG_PM25_CO2 507 +#define REG_PM10_CO2 508 +#define REG_PRESSURE 509 +#define REG_LUX_HIGH 510 +#define REG_LUX_LOW 511 +#define REG_LUX_100 512 +#define REG_RAINFALL 513 +#define REG_COMPASS 514 +#define REG_SOLAR 515 + +// Kalibrační / konfigurační registry (FC06) +#define REG_WIND_DIR_OFFSET 0x6000 +#define REG_WIND_ZERO 0x6001 +#define REG_RAIN_ZERO 0x6002 +#define REG_RAIN_SENS 0x6003 + +// ─── Flags přítomnosti senzorů ──────────────────────────────────────────────── +struct SensorPresence { + bool wind = false; + bool tempHum = false; + bool pressure = false; + bool noise = false; + bool pm25 = false; + bool pm10 = false; + bool co2 = false; + bool light = false; + bool rainfall = false; + bool compass = false; + bool solar = false; +}; + +// ─── Naměřené hodnoty ───────────────────────────────────────────────────────── +struct SensorData { + SensorPresence present; + + float windSpeed = 0.0f; + uint16_t windForce = 0; + uint8_t windDir8 = 0; + uint16_t windDir360 = 0; + + float temperature = 0.0f; + float humidity = 0.0f; + float pressure = 0.0f; + float noise = 0.0f; + + uint16_t pm25 = 0; + uint16_t pm10 = 0; + uint16_t co2 = 0; + + uint32_t luxFull = 0; + uint32_t lux100 = 0; + + float rainfall = 0.0f; + float compass = 0.0f; + uint16_t solar = 0; + + uint32_t lastUpdateMs = 0; + bool commError = false; + String errorMsg = ""; +}; + +// ─── Konfigurace snímače (ukládá se do NVS) ─────────────────────────────────── +struct SensorConfig { + uint8_t modbusAddr = DEFAULT_MODBUS_ADDR; + uint32_t baudRate = DEFAULT_BAUD_RATE; + uint8_t pollInterval = DEFAULT_POLL_INTERVAL; + + uint8_t windDirOffset = 0; + uint8_t rainSensitivity = 0x11; + uint8_t slot507Mode = 0; // 0=auto, 1=PM, 2=CO2 + uint8_t slot508Mode = 0; +}; + +// ─── NVS load / save ────────────────────────────────────────────────────────── +inline void loadConfig(Preferences &p, SensorConfig &c) { + c.modbusAddr = p.getUChar("mbAddr", DEFAULT_MODBUS_ADDR); + c.baudRate = p.getUInt ("baud", DEFAULT_BAUD_RATE); + c.pollInterval = p.getUChar("poll", DEFAULT_POLL_INTERVAL); + c.windDirOffset = p.getUChar("wdOffset", 0); + c.rainSensitivity = p.getUChar("rainSens", 0x11); + c.slot507Mode = p.getUChar("slot507", 0); + c.slot508Mode = p.getUChar("slot508", 0); +} + +inline void saveConfig(Preferences &p, const SensorConfig &c) { + p.putUChar("mbAddr", c.modbusAddr); + p.putUInt ("baud", c.baudRate); + p.putUChar("poll", c.pollInterval); + p.putUChar("wdOffset", c.windDirOffset); + p.putUChar("rainSens", c.rainSensitivity); + p.putUChar("slot507", c.slot507Mode); + p.putUChar("slot508", c.slot508Mode); +} + +inline const char* windDir8Name(uint8_t d) { + static const char* names[] = {"N","NE","E","SE","S","SW","W","NW"}; + return (d < 8) ? names[d] : "?"; +} + +#endif // WS_CFG_H diff --git a/SW/ESPlan_WeatherStation/i18n_str.h b/SW/ESPlan_WeatherStation/i18n_str.h new file mode 100755 index 0000000..5b1fce0 --- /dev/null +++ b/SW/ESPlan_WeatherStation/i18n_str.h @@ -0,0 +1,244 @@ +#ifndef WS_I18N_STR_H +#define WS_I18N_STR_H +/* + * i18n_str.h – Překlady webového rozhraní (CZ / EN) + */ + +#include + +struct I18nStrings { + // Navigace + const char* title; + const char* navStatus; + const char* navConfig; + const char* langSwitch; + + // Stavová stránka + const char* pageStatusTitle; + const char* sectionSensors; + const char* sectionValues; + const char* colSensor; + const char* colPresent; + const char* colValue; + const char* colUnit; + const char* yes; + const char* no; + const char* lastUpdate; + const char* noData; + const char* commError; + const char* btnRefresh; + const char* btnDetect; + const char* autoRefresh; + + // Názvy senzorů + const char* snsWind; + const char* snsWindSpeed; + const char* snsWindForce; + const char* snsWindDir; + const char* snsWindDir360; + const char* snsTempHum; + const char* snsTemp; + const char* snsHum; + const char* snsPressure; + const char* snsNoise; + const char* snsPM25; + const char* snsPM10; + const char* snsCO2; + const char* snsLight; + const char* snsRainfall; + const char* snsCompass; + const char* snsSolar; + + // Konfigurace + const char* pageConfigTitle; + const char* sectionComm; + const char* cfgAddr; + const char* cfgAddrHelp; + const char* cfgBaud; + const char* cfgBaudHelp; + const char* cfgPoll; + const char* cfgPollHelp; + + const char* sectionCalib; + const char* cfgWindDirOffset; + const char* cfgWindDirOffsetHelp; + const char* cfgWindDirOff0; + const char* cfgWindDirOff1; + const char* cfgRainSens; + const char* cfgRainSensHelp; + const char* cfgSlot507; + const char* cfgSlot507Help; + const char* cfgSlotAuto; + const char* cfgSlotPM; + const char* cfgSlotCO2; + + const char* sectionActions; + const char* btnWindZero; + const char* btnWindZeroHelp; + const char* btnRainZero; + const char* btnRainZeroHelp; + + const char* btnSave; + + const char* sectionRaw; + + const char* windDirNames[8]; +}; + +// ─── Čeština ────────────────────────────────────────────────────────────────── +static const I18nStrings STR_CZ = { + "ESPlan Meteostanice", + "Stav cidel", + "Konfigurace", + "English", + + "Stav a hodnoty snimacu", + "Detekce senzoru", + "Namerene hodnoty", + "Senzor", + "Pritomnost", + "Hodnota", + "Jednotka", + "✓ Pritomno", + "— Nepritomno", + "Posledni aktualizace", + "—", + "⚠ Chyba komunikace", + "Obnovit", + "Znovu detekovat", + "Auto obnoveni", + + "Vitr", + "Rychlost vetru", + "Sila vetru (Beaufort)", + "Smer vetru (0-7)", + "Smer vetru (0-360°)", + "Teplota / Vlhkost", + "Teplota", + "Relativni vlhkost", + "Atmosfericky tlak", + "Hladina hluku", + "Prach PM2.5", + "Prach PM10", + "CO₂", + "Osvit", + "Srazky", + "El. kompas", + "Celkove sol. zareni", + + "Konfigurace snimacu", + "Komunikacni parametry", + "Adresa Modbus", + "Adresa snimacu na sbernici RS485 (1-247, vychozi: 1)", + "Prenosova rychlost", + "Baudrate RS485 (vychozi: 4800 bps)", + "Interval cteni", + "Interval periodickeho cteni dat v sekundach (1-60)", + + "Kalibrace a nastaveni", + "Korekce smeru vetru", + "Otoceni smeru o 180 stupnu", + "Normalni (0°)", + "Otoceni 180°", + "Citlivost destoveho snimacu", + "Hex hodnota registru 0x6003 (vychozi: 0x11). Mensi = citlivejsi.", + "Typ snimacu v registrech 507/508", + "Auto detekce nebo rucni urceni PM2.5/PM10 nebo CO2", + "Automaticka detekce", + "PM2.5 + PM10", + "CO₂", + + "Akce snimacu", + "Vynulovat rychlost vetru", + "Zapise 0xAA do reg. 0x6001. Pockejte 10 s.", + "Vynulovat srazkomery", + "Zapise 0x5A do reg. 0x6002.", + + "Ulozit konfiguraci", + + "RS485 komunikace (raw)", + + {"S", "SV", "V", "JV", "J", "JZ", "Z", "SZ"} +}; + +// ─── Angličtina ─────────────────────────────────────────────────────────────── +static const I18nStrings STR_EN = { + "ESPlan Weather Station", + "Sensor Status", + "Configuration", + "Cesky", + + "Sensor Status & Readings", + "Sensor Detection", + "Live Readings", + "Sensor", + "Presence", + "Value", + "Unit", + "✓ Present", + "— Not present", + "Last update", + "—", + "⚠ Communication error", + "Refresh", + "Re-detect sensors", + "Auto refresh", + + "Wind", + "Wind speed", + "Wind force (Beaufort)", + "Wind direction (0-7)", + "Wind direction (0-360°)", + "Temperature / Humidity", + "Temperature", + "Relative humidity", + "Atmospheric pressure", + "Noise level", + "Particulate PM2.5", + "Particulate PM10", + "CO₂", + "Illuminance", + "Rainfall", + "E-compass", + "Total solar radiation", + + "Sensor Configuration", + "Communication Settings", + "Modbus Address", + "Sensor address on RS485 bus (1-247, default: 1)", + "Baud Rate", + "RS485 baud rate (default: 4800 bps)", + "Poll Interval", + "Data reading interval in seconds (1-60)", + + "Calibration & Settings", + "Wind direction offset", + "Rotate direction by 180 degrees", + "Normal (0°)", + "Rotated 180°", + "Rain sensor sensitivity", + "Hex value of register 0x6003 (default: 0x11). Lower = more sensitive.", + "Sensor type in registers 507/508", + "Auto-detect or manually specify PM2.5/PM10 or CO2", + "Auto detect", + "PM2.5 + PM10", + "CO₂", + + "Sensor Actions", + "Zero wind speed", + "Writes 0xAA to reg 0x6001. Wait 10 s.", + "Reset rainfall counter", + "Writes 0x5A to reg 0x6002.", + + "Save configuration", + + "RS485 Communication (raw)", + + {"N", "NE", "E", "SE", "S", "SW", "W", "NW"} +}; + +inline const I18nStrings& getStrings(bool isCz) { + return isCz ? STR_CZ : STR_EN; +} + +#endif // WS_I18N_STR_H diff --git a/SW/ESPlan_WeatherStation/mb_client.cpp b/SW/ESPlan_WeatherStation/mb_client.cpp new file mode 100755 index 0000000..6613faa --- /dev/null +++ b/SW/ESPlan_WeatherStation/mb_client.cpp @@ -0,0 +1,152 @@ +/* + * mb_client.cpp – Implementace Modbus RTU klienta + */ + +#include "mb_client.h" + +uint16_t ModbusClient::crc16(const uint8_t* data, uint8_t len) { + uint16_t crc = 0xFFFF; + for (uint8_t i = 0; i < len; i++) { + crc ^= (uint16_t)data[i]; + for (uint8_t b = 0; b < 8; b++) { + if (crc & 0x0001) crc = (crc >> 1) ^ 0xA001; + else crc >>= 1; + } + } + return crc; +} + +void ModbusClient::flushRx() { + while (_serial->available()) _serial->read(); +} + +void ModbusClient::begin(uint32_t baud, uint8_t /*addr*/) { + _baud = baud; + _serial = &Serial2; + _serial->begin(_baud, SERIAL_8N1, PIN_RS485_RX, PIN_RS485_TX); + Serial.printf("[MOD] RS485 init: %d baud, 8N1, RX=%d TX=%d\n", + _baud, PIN_RS485_RX, PIN_RS485_TX); +} + +void ModbusClient::setBaudRate(uint32_t baud) { + _baud = baud; + _serial->end(); + _serial->begin(_baud, SERIAL_8N1, PIN_RS485_RX, PIN_RS485_TX); + Serial.printf("[MOD] Baudrate zmenen na: %d\n", _baud); +} + +ModbusStatus ModbusClient::readRegisters(uint8_t slaveAddr, uint16_t startReg, + uint8_t count, uint16_t* out) { + if (!_serial || count == 0 || count > 64) return ModbusStatus::FRAME_ERROR; + + uint8_t req[8]; + req[0] = slaveAddr; + req[1] = 0x03; + req[2] = (startReg >> 8) & 0xFF; + req[3] = startReg & 0xFF; + req[4] = 0x00; + req[5] = count; + uint16_t c = crc16(req, 6); + req[6] = c & 0xFF; + req[7] = (c >> 8) & 0xFF; + + // Uložit TX + memcpy(lastTx, req, 8); + lastTxLen = 8; + + flushRx(); + _serial->write(req, 8); + _serial->flush(); + + uint8_t expectedLen = 3 + count * 2 + 2; + uint8_t buf[160]; + uint8_t rxLen = 0; + uint32_t t0 = millis(); + + while (rxLen < expectedLen && (millis() - t0 < TIMEOUT_MS)) { + if (_serial->available()) buf[rxLen++] = _serial->read(); + yield(); + } + + // Uložit RX + uint8_t copyLen = (rxLen < sizeof(lastRx)) ? rxLen : sizeof(lastRx); + memcpy(lastRx, buf, copyLen); + lastRxLen = copyLen; + + if (rxLen < expectedLen) { + lastResult = ModbusStatus::TIMEOUT; + return ModbusStatus::TIMEOUT; + } + + uint16_t rxCrc = (uint16_t)buf[rxLen-1] << 8 | buf[rxLen-2]; + uint16_t calcCrc = crc16(buf, rxLen - 2); + if (rxCrc != calcCrc) { + lastResult = ModbusStatus::CRC_ERROR; + return ModbusStatus::CRC_ERROR; + } + + if (buf[1] & 0x80) { + lastResult = ModbusStatus::EXCEPTION; + return ModbusStatus::EXCEPTION; + } + + lastResult = ModbusStatus::OK; + for (uint8_t i = 0; i < count; i++) { + out[i] = (uint16_t)buf[3 + i*2] << 8 | buf[4 + i*2]; + } + return ModbusStatus::OK; +} + +ModbusStatus ModbusClient::writeRegister(uint8_t slaveAddr, uint16_t reg, uint16_t value) { + if (!_serial) return ModbusStatus::FRAME_ERROR; + + uint8_t req[8]; + req[0] = slaveAddr; + req[1] = 0x06; + req[2] = (reg >> 8) & 0xFF; + req[3] = reg & 0xFF; + req[4] = (value >> 8) & 0xFF; + req[5] = value & 0xFF; + uint16_t c = crc16(req, 6); + req[6] = c & 0xFF; + req[7] = (c >> 8) & 0xFF; + + memcpy(lastTx, req, 8); + lastTxLen = 8; + + flushRx(); + _serial->write(req, 8); + _serial->flush(); + + uint8_t buf[8]; + uint8_t rxLen = 0; + uint32_t t0 = millis(); + + while (rxLen < 8 && (millis() - t0 < TIMEOUT_MS)) { + if (_serial->available()) buf[rxLen++] = _serial->read(); + yield(); + } + + memcpy(lastRx, buf, rxLen); + lastRxLen = rxLen; + + if (rxLen < 8) { lastResult = ModbusStatus::TIMEOUT; return ModbusStatus::TIMEOUT; } + + uint16_t rxCrc = (uint16_t)buf[7] << 8 | buf[6]; + uint16_t calcCrc = crc16(buf, 6); + if (rxCrc != calcCrc) { lastResult = ModbusStatus::CRC_ERROR; return ModbusStatus::CRC_ERROR; } + + lastResult = ModbusStatus::OK; + return ModbusStatus::OK; +} + +const char* ModbusClient::statusStr(ModbusStatus s) { + switch (s) { + case ModbusStatus::OK: return "OK"; + case ModbusStatus::TIMEOUT: return "TIMEOUT"; + case ModbusStatus::CRC_ERROR: return "CRC_ERROR"; + case ModbusStatus::EXCEPTION: return "EXCEPTION"; + case ModbusStatus::FRAME_ERROR: return "FRAME_ERROR"; + default: return "UNKNOWN"; + } +} diff --git a/SW/ESPlan_WeatherStation/mb_client.h b/SW/ESPlan_WeatherStation/mb_client.h new file mode 100755 index 0000000..b8065b9 --- /dev/null +++ b/SW/ESPlan_WeatherStation/mb_client.h @@ -0,0 +1,54 @@ +#ifndef WS_MB_CLIENT_H +#define WS_MB_CLIENT_H +/* + * mb_client.h – Modbus RTU klient přes RS485 / HardwareSerial + * + * FC03 – Read Holding Registers + * FC06 – Write Single Register + * + * DE pin (direction enable) řízen HW – nevyžaduje softwarové přepínání. + */ + +#include +#include "cfg.h" + +enum class ModbusStatus : uint8_t { + OK = 0, + TIMEOUT, + CRC_ERROR, + EXCEPTION, + FRAME_ERROR +}; + +class ModbusClient { +public: + ModbusClient() = default; + + void begin(uint32_t baud = 4800, uint8_t addr = 1); + + ModbusStatus readRegisters(uint8_t slaveAddr, uint16_t startReg, + uint8_t count, uint16_t* out); + + ModbusStatus writeRegister(uint8_t slaveAddr, uint16_t reg, uint16_t value); + + void setBaudRate(uint32_t baud); + + const char* statusStr(ModbusStatus s); + + // ── Poslední TX/RX frame ──────────────────────────────────────────── + uint8_t lastTx[16]; + uint8_t lastTxLen = 0; + uint8_t lastRx[80]; + uint8_t lastRxLen = 0; + ModbusStatus lastResult = ModbusStatus::OK; + +private: + HardwareSerial* _serial = nullptr; + uint32_t _baud = 4800; + static const uint16_t TIMEOUT_MS = 500; + + uint16_t crc16(const uint8_t* data, uint8_t len); + void flushRx(); +}; + +#endif // WS_MB_CLIENT_H diff --git a/SW/ESPlan_WeatherStation/snr.cpp b/SW/ESPlan_WeatherStation/snr.cpp new file mode 100755 index 0000000..4df47e3 --- /dev/null +++ b/SW/ESPlan_WeatherStation/snr.cpp @@ -0,0 +1,266 @@ +/* + * snr.cpp – Detekce a čtení snímače RS-FSXCS-N01-* + * + * STRATEGIE DETEKCE: + * Snímač RS-FSXCS-N01-* existuje v různých variantách. Neosazené + * senzory NEVRACÍ Modbus exception – vrátí 0x0000. Proto: + * + * 1. Registry čteme PO SKUPINÁCH + * 2. Čteme DVAKRÁT s 2s pauzou + * 3. Podmínky přítomnosti: + * - Teplota/vlhkost: přítomno pokud alespoň jedna hodnota != 0 + * (obě přesně 0.0 je fyzicky extrémně nepravděpodobné) + * - Tlak: přítomno pokud > 0 (0 kPa je na Zemi nemožné) + * - Hluk: přítomno pokud >= 300 (minimum 30 dB, reg ×10) + * - Vítr: přítomno pokud se alespoň jeden reg změní, nebo + * je nenulový aspoň v jednom čtení + * - PM/CO2: přítomno pokud alespoň jedno čtení != 0 + * - Osvit: přítomno pokud alespoň jedno čtení != 0 + * - Srážky: vždy 0 pokud neprší – ověřujeme zápisem do + * kalibračního reg 0x6002 (pokud exception → neosazeno) + * - Kompas: přítomno pokud se hodnota mění, nebo je nenulová + * - Solar: přítomno pokud alespoň jedno čtení != 0 + */ + +#include "snr.h" + +static float rawToTemp(uint16_t raw) { + return (float)(int16_t)raw / 10.0f; +} + +// ─── Detekce ────────────────────────────────────────────────────────────────── +void detectSensors(ModbusClient& mb, SensorData& data, const SensorConfig& cfg) { + SensorPresence& p = data.present; + uint8_t addr = cfg.modbusAddr; + + // Resetujeme vše na nepřítomno + p = SensorPresence(); + + Serial.println(F("[SNS] === Detekce senzoru (2x cteni) ===")); + + // Čtení #1: celý blok 500–515 + uint16_t r1[16] = {0}; + ModbusStatus st1 = mb.readRegisters(addr, REG_WIND_SPEED, 16, r1); + if (st1 != ModbusStatus::OK) { + Serial.printf("[SNS] Chyba 1. cteni: %s\n", mb.statusStr(st1)); + data.commError = true; + data.errorMsg = mb.statusStr(st1); + return; + } + + Serial.print(F("[SNS] Cteni #1: ")); + for (int i = 0; i < 16; i++) { Serial.printf("%04X ", r1[i]); } + Serial.println(); + + // Pauza 2 s – dáme senzorům čas vrátit různé hodnoty + delay(2000); + + // Čtení #2 + uint16_t r2[16] = {0}; + ModbusStatus st2 = mb.readRegisters(addr, REG_WIND_SPEED, 16, r2); + if (st2 != ModbusStatus::OK) { + Serial.printf("[SNS] Chyba 2. cteni: %s\n", mb.statusStr(st2)); + data.commError = true; + data.errorMsg = mb.statusStr(st2); + return; + } + + Serial.print(F("[SNS] Cteni #2: ")); + for (int i = 0; i < 16; i++) { Serial.printf("%04X ", r2[i]); } + Serial.println(); + + data.commError = false; + data.errorMsg = ""; + + // ── VÍTR (reg 500–503): windSpeed, windForce, windDir8, windDir360 ─── + // Vítr (ultrazvukový) je ZÁKLADNÍ senzor, přítomný na VŠECH variantách + // RS-FSXCS-N01-*. Při bezvětří vrací 0,0,0,0 což je validní stav. + // Proto: pokud čtení bloku proběhlo OK, vítr je vždy přítomný. + { + p.wind = true; + Serial.printf("[SNS] Vitr: r1=[%d,%d,%d,%d] r2=[%d,%d,%d,%d] => ANO (zakladni senzor)\n", + r1[0], r1[1], r1[2], r1[3], + r2[0], r2[1], r2[2], r2[3]); + } + + // ── TEPLOTA + VLHKOST (reg 504–505) ────────────────────────────────── + // 504=vlhkost×10, 505=teplota×10 (signed) + // Obě přesně 0 je fyzicky téměř nemožné (0.0°C a 0.0%RH) + { + bool ok1 = (r1[4] != 0 || r1[5] != 0); + bool ok2 = (r2[4] != 0 || r2[5] != 0); + p.tempHum = ok1 || ok2; + Serial.printf("[SNS] Temp/RH: r1=[%d,%d] r2=[%d,%d] => %s\n", + r1[4], r1[5], r2[4], r2[5], p.tempHum?"ANO":"NE"); + } + + // ── HLUK (reg 506) ─────────────────────────────────────────────────── + // Rozsah snímače 30–120 dB → registr 300–1200 (×10). + // POZOR: neosazený senzor vrací konstantně 300 (= 30.0 dB). + // Přítomno pokud: hodnota > 300, NEBO se mění mezi čteními. + // Přesně 300 v obou čteních = pravděpodobně neosazeno. + { + bool inRange1 = (r1[6] >= 300 && r1[6] <= 1200); + bool inRange2 = (r2[6] >= 300 && r2[6] <= 1200); + bool changed = (r1[6] != r2[6]); + bool aboveMin = (r1[6] > 300 || r2[6] > 300); + + // Přítomno pokud je v rozsahu A (hodnota nad minimem NEBO se mění) + p.noise = (inRange1 || inRange2) && (aboveMin || changed); + Serial.printf("[SNS] Hluk: r1=%d r2=%d chg=%d abv=%d => %s\n", + r1[6], r2[6], changed, aboveMin, p.noise?"ANO":"NE"); + } + + // ── PM2.5/PM10 nebo CO2 (reg 507–508) ─────────────────────────────── + // Přítomno pokud alespoň v jednom čtení != 0 + { + bool anyNZ1 = (r1[7] != 0 || r1[8] != 0); + bool anyNZ2 = (r2[7] != 0 || r2[8] != 0); + bool present = anyNZ1 || anyNZ2; + + if (present) { + uint8_t mode = cfg.slot507Mode; + if (mode == 0) { + // Auto: CO2 typicky 400–5000 ppm + uint16_t v = (r2[7] != 0) ? r2[7] : r1[7]; + mode = (v >= 400 && v <= 5000) ? 2 : 1; + } + if (mode == 2) { + p.co2 = true; + } else { + p.pm25 = true; + p.pm10 = true; + } + } + Serial.printf("[SNS] PM/CO2: r1=[%d,%d] r2=[%d,%d] => pm25=%s pm10=%s co2=%s\n", + r1[7], r1[8], r2[7], r2[8], + p.pm25?"ANO":"NE", p.pm10?"ANO":"NE", p.co2?"ANO":"NE"); + } + + // ── TLAK (reg 509) ────────────────────────────────────────────────── + // 0 kPa je na Zemi fyzicky nemožné. Normální rozsah 85–108 kPa. + { + p.pressure = (r1[9] > 0 || r2[9] > 0); + Serial.printf("[SNS] Tlak: r1=%d r2=%d => %s\n", + r1[9], r2[9], p.pressure?"ANO":"NE"); + } + + // ── OSVIT (reg 510–512): luxH, luxL, lux/100 ─────────────────────── + // 0 lux = noc/tma, validní. Ale pokud ALL 0 v obou čteních = neosazeno. + { + bool allZ1 = (r1[10]==0 && r1[11]==0 && r1[12]==0); + bool allZ2 = (r2[10]==0 && r2[11]==0 && r2[12]==0); + p.light = !(allZ1 && allZ2); + Serial.printf("[SNS] Osvit: r1=[%d,%d,%d] r2=[%d,%d,%d] => %s\n", + r1[10],r1[11],r1[12], r2[10],r2[11],r2[12], + p.light?"ANO":"NE"); + } + + // ── SRÁŽKY (reg 513) ──────────────────────────────────────────────── + // 0 mm je validní (neprší). Hodnota je kumulativní. + // Ověříme zápisem do kalibračního reg: pokud snímač nemá + // srážkoměr, zápis do REG_RAIN_ZERO (0x6002) vrátí exception. + { + if (r1[13] > 0 || r2[13] > 0) { + p.rainfall = true; + } else { + // Hodnota je 0 – pokusíme se ověřit přes kalibrační registr + // Čteme reg 0x6003 (rain sensitivity) – pokud OK, srážkoměr je + uint16_t dummy = 0; + ModbusStatus st = mb.readRegisters(addr, REG_RAIN_SENS, 1, &dummy); + p.rainfall = (st == ModbusStatus::OK && dummy != 0 && dummy != 0xFFFF); + Serial.printf("[SNS] Srazky: kalib.reg test: %s (val=0x%04X)\n", + mb.statusStr(st), dummy); + } + Serial.printf("[SNS] Srazky: r1=%d r2=%d => %s\n", + r1[13], r2[13], p.rainfall?"ANO":"NE"); + } + + // ── KOMPAS (reg 514) ──────────────────────────────────────────────── + // 0 = sever, validní. Ale neosazený kompas vrací stále 0. + // Přítomno pokud nenulový, nebo se mění. + { + bool nonZero = (r1[14] != 0 || r2[14] != 0); + bool changed = (r1[14] != r2[14]); + p.compass = nonZero || changed; + Serial.printf("[SNS] Kompas: r1=%d r2=%d => %s\n", + r1[14], r2[14], p.compass?"ANO":"NE"); + } + + // ── SOLÁRNÍ ZÁŘENÍ (reg 515) ──────────────────────────────────────── + // 0 W/m² validní (noc). Přítomno pokud alespoň jednou != 0. + { + p.solar = (r1[15] != 0 || r2[15] != 0); + Serial.printf("[SNS] Solar: r1=%d r2=%d => %s\n", + r1[15], r2[15], p.solar?"ANO":"NE"); + } + + Serial.println(F("[SNS] === Detekce dokoncena ===")); +} + +// ─── Čtení všech snímačů (bulk) ───────────────────────────────────────────── +void readAllSensors(ModbusClient& mb, SensorData& data, const SensorConfig& cfg) { + uint16_t buf[16]; + ModbusStatus st = mb.readRegisters(cfg.modbusAddr, REG_WIND_SPEED, 16, buf); + + if (st != ModbusStatus::OK) { + data.commError = true; + data.errorMsg = mb.statusStr(st); + Serial.printf("[SNS] Chyba cteni: %s\n", mb.statusStr(st)); + return; + } + + data.commError = false; + data.errorMsg = ""; + data.lastUpdateMs = millis(); + + const SensorPresence& p = data.present; + + if (p.wind) { + data.windSpeed = buf[0] / 10.0f; + data.windForce = buf[1]; + data.windDir8 = (uint8_t)(buf[2] & 0x07); + data.windDir360 = buf[3]; + } + + if (p.tempHum) { + data.humidity = buf[4] / 10.0f; + data.temperature = rawToTemp(buf[5]); + } + + if (p.noise) data.noise = buf[6] / 10.0f; + if (p.pressure) data.pressure = buf[9] / 10.0f; + + // PM / CO2 + if (p.co2) data.co2 = buf[7]; + if (p.pm25) data.pm25 = buf[7]; + if (p.pm10) data.pm10 = buf[8]; + + if (p.light) { + data.luxFull = ((uint32_t)buf[10] << 16) | buf[11]; + data.lux100 = buf[12]; + } + if (p.rainfall) data.rainfall = buf[13] / 10.0f; + if (p.compass) data.compass = buf[14] / 100.0f; + if (p.solar) data.solar = buf[15]; +} + +// ─── Výpis stavu ───────────────────────────────────────────────────────────── +void printSensorStatus(const SensorData& data) { + const SensorPresence& p = data.present; + Serial.println(F("[SNS] +--------------------------+")); + Serial.println(F("[SNS] | Detekce senzoru |")); + Serial.println(F("[SNS] +--------------------------+")); + Serial.printf("[SNS] | Vitr: %-3s |\n", p.wind ?"ANO":"NE"); + Serial.printf("[SNS] | Teplota/RH: %-3s |\n", p.tempHum ?"ANO":"NE"); + Serial.printf("[SNS] | Tlak: %-3s |\n", p.pressure ?"ANO":"NE"); + Serial.printf("[SNS] | Hluk: %-3s |\n", p.noise ?"ANO":"NE"); + Serial.printf("[SNS] | PM2.5: %-3s |\n", p.pm25 ?"ANO":"NE"); + Serial.printf("[SNS] | PM10: %-3s |\n", p.pm10 ?"ANO":"NE"); + Serial.printf("[SNS] | CO2: %-3s |\n", p.co2 ?"ANO":"NE"); + Serial.printf("[SNS] | Osvit: %-3s |\n", p.light ?"ANO":"NE"); + Serial.printf("[SNS] | Srazky: %-3s |\n", p.rainfall ?"ANO":"NE"); + Serial.printf("[SNS] | Kompas: %-3s |\n", p.compass ?"ANO":"NE"); + Serial.printf("[SNS] | Solar: %-3s |\n", p.solar ?"ANO":"NE"); + Serial.println(F("[SNS] +--------------------------+")); +} \ No newline at end of file diff --git a/SW/ESPlan_WeatherStation/snr.h b/SW/ESPlan_WeatherStation/snr.h new file mode 100755 index 0000000..14bff82 --- /dev/null +++ b/SW/ESPlan_WeatherStation/snr.h @@ -0,0 +1,14 @@ +#ifndef WS_SNR_H +#define WS_SNR_H +/* + * snr.h – Detekce variant snímače a čtení dat + */ + +#include "cfg.h" +#include "mb_client.h" + +void detectSensors(ModbusClient& mb, SensorData& data, const SensorConfig& cfg); +void readAllSensors(ModbusClient& mb, SensorData& data, const SensorConfig& cfg); +void printSensorStatus(const SensorData& data); + +#endif // WS_SNR_H diff --git a/SW/ESPlan_WeatherStation/ws_server.cpp b/SW/ESPlan_WeatherStation/ws_server.cpp new file mode 100755 index 0000000..bb16000 --- /dev/null +++ b/SW/ESPlan_WeatherStation/ws_server.cpp @@ -0,0 +1,710 @@ +/* + * ws_server.cpp – HTTP webserver implementace + */ + +#include "ws_server.h" +#include + +// ─── Globální pointery ──────────────────────────────────────────────────────── +static WebServer* gServer = nullptr; +static SensorData* gData = nullptr; +static SensorConfig* gCfg = nullptr; +static Preferences* gPrefs = nullptr; +static ModbusClient* gMb = nullptr; + +// ─── Pomocné ───────────────────────────────────────────────────────────────── +static bool isLangCz() { + if (gServer->hasArg("lang")) { + String l = gServer->arg("lang"); + return !(l == "en"); + } + if (gServer->hasHeader("Cookie")) { + String c = gServer->header("Cookie"); + if (c.indexOf("lang=en") >= 0) return false; + } + return true; +} + +static void sendLangCookie(bool cz) { + gServer->sendHeader("Set-Cookie", + cz ? "lang=cs; Path=/; Max-Age=31536000" + : "lang=en; Path=/; Max-Age=31536000"); +} + +static String fmtFloat(float v, int dec = 1) { + char buf[20]; + snprintf(buf, sizeof(buf), "%.*f", dec, v); + return String(buf); +} + +// ─── CSS ───────────────────────────────────────────────────────────────────── +static const char CSS[] PROGMEM = R"CSS( +:root{ + --bg:#0d1117;--bg2:#161b22;--bg3:#21262d; + --border:#30363d;--border2:#484f58; + --text:#e6edf3;--text2:#8b949e;--text3:#6e7681; + --accent:#58a6ff;--accent2:#1f6feb; + --green:#3fb950;--red:#f85149;--yellow:#d29922; +} +*{box-sizing:border-box;margin:0;padding:0} +body{background:var(--bg);color:var(--text);font-family:'Segoe UI',system-ui,sans-serif; + min-height:100vh;font-size:14px;line-height:1.5} +a{color:var(--accent);text-decoration:none} +a:hover{text-decoration:underline} +.topbar{background:var(--bg2);border-bottom:1px solid var(--border); + padding:0 24px;display:flex;align-items:center;height:52px; + position:sticky;top:0;z-index:100} +.logo{display:flex;align-items:center;gap:10px;font-weight:700;font-size:15px; + color:var(--text);margin-right:32px;white-space:nowrap} +.logo-icon{width:28px;height:28px;background:#1f6feb;border-radius:6px; + display:flex;align-items:center;justify-content:center;flex-shrink:0} +.nav-links{display:flex;gap:2px;flex:1} +.nav-link{padding:6px 14px;border-radius:6px;font-size:13px; + color:var(--text2);transition:background .15s,color .15s;white-space:nowrap} +.nav-link:hover{background:var(--bg3);color:var(--text);text-decoration:none} +.nav-link.active{background:var(--accent2);color:#fff} +.lang-btn{margin-left:auto;padding:5px 12px;border-radius:20px; + background:var(--bg3);border:1px solid var(--border2); + color:var(--text2);font-size:12px;white-space:nowrap} +.lang-btn:hover{background:var(--border);color:var(--text);text-decoration:none} +.main{max-width:1100px;margin:0 auto;padding:28px 20px} +h1{font-size:20px;font-weight:600;margin-bottom:20px} +h2{font-size:12px;font-weight:600;color:var(--text3);text-transform:uppercase; + letter-spacing:.08em;margin:28px 0 12px;padding-bottom:8px; + border-bottom:1px solid var(--border)} +h2:first-of-type{margin-top:0} +.card{background:var(--bg2);border:1px solid var(--border); + border-radius:10px;padding:16px 20px;margin-bottom:16px} +table{width:100%;border-collapse:collapse;font-size:13px} +th{padding:8px 12px;text-align:left;font-weight:600;font-size:11px; + color:var(--text3);text-transform:uppercase;letter-spacing:.06em; + border-bottom:1px solid var(--border);white-space:nowrap} +td{padding:9px 12px;border-bottom:1px solid var(--border);vertical-align:middle} +tr:last-child td{border-bottom:none} +tr:hover td{background:rgba(255,255,255,.02)} +.val{font-family:'Courier New',monospace;font-size:13px;color:var(--text)} +.unit{color:var(--text3);font-size:12px} +.badge-yes{display:inline-flex;align-items:center;gap:6px;color:var(--green);font-size:12px} +.badge-no{display:inline-flex;align-items:center;gap:6px;color:var(--text3);font-size:12px} +.dot{width:8px;height:8px;border-radius:50%;flex-shrink:0} +.dot-g{background:var(--green);box-shadow:0 0 6px var(--green); + animation:pulse 2s ease-in-out infinite} +.dot-x{background:var(--text3)} +@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}} +.toolbar{display:flex;align-items:center;gap:8px;margin-bottom:16px;flex-wrap:wrap} +.smeta{margin-left:auto;font-size:12px;color:var(--text3);font-family:'Courier New',monospace} +.err-banner{background:rgba(248,81,73,.1);border:1px solid var(--red); + border-radius:8px;padding:10px 16px;color:var(--red);font-size:13px; + margin-bottom:16px;display:flex;align-items:center;gap:8px} +.ip-bar{display:flex;align-items:center;gap:20px;padding:10px 16px; + background:var(--bg3);border:1px solid var(--border);border-radius:8px; + font-family:'Courier New',monospace;font-size:12px;color:var(--text2); + margin-bottom:20px;flex-wrap:wrap} +.ip-lbl{color:var(--text3)} +.btn{display:inline-flex;align-items:center;gap:6px;padding:7px 14px; + border-radius:6px;font-size:13px;cursor:pointer;border:1px solid transparent; + font-family:inherit;transition:background .15s;text-decoration:none} +.btn:active{opacity:.8} +.btn-p{background:var(--accent2);color:#fff;border-color:var(--accent2)} +.btn-p:hover{background:#388bfd;text-decoration:none;color:#fff} +.btn-d{background:var(--bg3);color:var(--text);border-color:var(--border2)} +.btn-d:hover{background:var(--border);text-decoration:none} +.btn-w{background:transparent;color:var(--yellow);border-color:var(--yellow)} +.btn-w:hover{background:rgba(210,153,34,.1);text-decoration:none} +.btn-sm{padding:4px 10px;font-size:12px} +.fg{margin-bottom:18px} +label{display:block;font-size:13px;font-weight:500;color:var(--text2);margin-bottom:5px} +.ht{font-size:11px;color:var(--text3);margin-top:4px} +input[type=number],input[type=text],select{ + background:var(--bg3);border:1px solid var(--border2);border-radius:6px; + padding:7px 10px;color:var(--text);font-size:13px;width:100%; + max-width:280px;outline:none;transition:border-color .15s;font-family:inherit} +input:focus,select:focus{border-color:var(--accent)} +select option{background:var(--bg3)} +.frow{display:flex;gap:16px;flex-wrap:wrap} +.frow .fg{flex:1;min-width:200px} +.act-row{display:flex;gap:16px;flex-wrap:wrap;align-items:flex-start} +.act-item{flex:1;min-width:200px} +.live-dot{width:8px;height:8px;border-radius:50%;background:var(--green); + box-shadow:0 0 6px var(--green);animation:pulse 2s ease-in-out infinite; + flex-shrink:0;margin-left:auto} +.val.flash{animation:valflash .4s ease-out} +@keyframes valflash{0%{color:var(--accent)}100%{color:var(--text)}} +.raw-panel{background:var(--bg2);border:1px solid var(--border);border-radius:10px; + padding:14px 20px;margin-bottom:16px;font-family:'Courier New',monospace; + font-size:12px;line-height:1.8} +.raw-tx{color:var(--yellow)} +.raw-rx{color:var(--green)} +.raw-err{color:var(--red)} +.raw-label{color:var(--text3);display:inline-block;width:28px} +.raw-status{display:inline-block;padding:1px 6px;border-radius:3px;font-size:10px; + font-weight:600;text-transform:uppercase;margin-left:8px} +.raw-status.ok{background:rgba(63,185,80,.15);color:var(--green)} +.raw-status.err{background:rgba(248,81,73,.15);color:var(--red)} +@media(max-width:640px){ + .topbar{padding:0 12px} + .logo{margin-right:12px} + .main{padding:16px 12px} + th,td{padding:7px 8px} + input,select{max-width:100%} + .smeta{margin-left:0} +} +)CSS"; + +// ─── HTML head + nav ───────────────────────────────────────────────────────── +static String htmlHead(const I18nStrings& s, bool cz, const char* page) { + String langUrl = cz ? "?lang=en" : "?lang=cs"; + String out; + out.reserve(3000); + + out += F("" + "" + ""); + out += s.title; + out += F("" + "
"); + return out; +} + +static String htmlFoot() { + return F("
"); +} + +// ─── Stránka: Stav ──────────────────────────────────────────────────────────── +static void handleRoot() { + bool cz = isLangCz(); + sendLangCookie(cz); + const I18nStrings& s = getStrings(cz); + + String html = htmlHead(s, cz, "status"); + + // IP bar + html += F("
" + "IP: "); + html += ETH.localIP().toString(); + html += F("MAC: "); + html += ETH.macAddress(); + html += F("ETH: "); + html += ETH.linkSpeed(); + html += F(" Mbit/s
"); + + if (gData->commError) { + html += F("
⚠ "); + html += s.commError; + html += F(": "); + html += gData->errorMsg; + html += F("
"); + } + + // Toolbar + html += F("
" + "
" + "
" + "" + ""); + html += F("
"); + + // Tabulka detekce + html += F("

"); + html += s.sectionSensors; + html += F("

" + ""); + + auto row = [&](const char* name, bool present) { + html += F(""); + }; + + const SensorPresence& p = gData->present; + row(s.snsWind, p.wind); + row(s.snsTempHum, p.tempHum); + row(s.snsPressure, p.pressure); + row(s.snsNoise, p.noise); + row(s.snsPM25, p.pm25); + row(s.snsPM10, p.pm10); + row(s.snsCO2, p.co2); + row(s.snsLight, p.light); + row(s.snsRainfall, p.rainfall); + row(s.snsCompass, p.compass); + row(s.snsSolar, p.solar); + html += F("
"); + html += s.colSensor; + html += F(""); + html += s.colPresent; + html += F("
"); + html += name; + html += F(""); + if (present) { + html += F(""); + html += s.yes; + } else { + html += F(""); + html += s.no; + } + html += F("
"); + + // Tabulka hodnot + html += F("

"); + html += s.sectionValues; + html += F("

"); + + auto valRow = [&](const char* id, const char* name, bool present, + const String& val, const char* unit) { + html += F(""); + }; + + const SensorData& d = *gData; + if (p.wind) { + valRow("ws", s.snsWindSpeed, true, fmtFloat(d.windSpeed), "m/s"); + valRow("wf", s.snsWindForce, true, String(d.windForce), "Bft"); + String dirTxt = String(d.windDir8) + " (" + + s.windDirNames[d.windDir8 < 8 ? d.windDir8 : 0] + ")"; + valRow("wd", s.snsWindDir, true, dirTxt, ""); + valRow("w3", s.snsWindDir360, true, String(d.windDir360), "°"); + } + if (p.tempHum) { + valRow("te", s.snsTemp, true, fmtFloat(d.temperature), "°C"); + valRow("hu", s.snsHum, true, fmtFloat(d.humidity), "%RH"); + } + if (p.pressure) valRow("pr", s.snsPressure, true, fmtFloat(d.pressure,2), "kPa"); + if (p.noise) valRow("no", s.snsNoise, true, fmtFloat(d.noise), "dB"); + if (p.pm25) valRow("p2", s.snsPM25, true, String(d.pm25), "µg/m³"); + if (p.pm10) valRow("p1", s.snsPM10, true, String(d.pm10), "µg/m³"); + if (p.co2) valRow("co", s.snsCO2, true, String(d.co2), "ppm"); + if (p.light) valRow("lx", s.snsLight, true, String(d.luxFull), "lux"); + if (p.rainfall) valRow("rn", s.snsRainfall, true, fmtFloat(d.rainfall), "mm"); + if (p.compass) valRow("cp", s.snsCompass, true, fmtFloat(d.compass,1), "°"); + if (p.solar) valRow("sl", s.snsSolar, true, String(d.solar), "W/m²"); + + html += F("
"); + html += s.colSensor; + html += F(""); + html += s.colValue; + html += F(""); + html += s.colUnit; + html += F("
"); + html += name; + html += F(""); + html += present ? val : s.noData; + html += F(""); + html += present ? unit : ""; + html += F("
"); + + // ── Raw RS485 panel ──────────────────────────────────────────────────── + html += F("

"); + html += s.sectionRaw; + html += F("

" + "
TX: ..." + "
" + "
RX: ...
" + "
"); + + // Inline JavaScript – AJAX auto-refresh + html += F(""); + + html += htmlFoot(); + gServer->send(200, "text/html; charset=UTF-8", html); +} + +// ─── POST /detect ───────────────────────────────────────────────────────────── +static void handleDetect() { + detectSensors(*gMb, *gData, *gCfg); + bool cz = isLangCz(); + sendLangCookie(cz); + gServer->sendHeader("Location", cz ? "/?lang=cs" : "/?lang=en"); + gServer->send(303); +} + +// ─── GET /config ────────────────────────────────────────────────────────────── +static void handleConfig() { + bool cz = isLangCz(); + sendLangCookie(cz); + const I18nStrings& s = getStrings(cz); + + String html = htmlHead(s, cz, "config"); + html += F("

"); + html += s.pageConfigTitle; + html += F("

"); + html += F(""); + + // Komunikace + html += F("

"); + html += s.sectionComm; + html += F("

"); + + html += F("
"); + html += s.cfgAddrHelp; + html += F("
"); + + html += F("
"); + html += s.cfgBaudHelp; + html += F("
"); + + html += F("
"); + html += s.cfgPollHelp; + html += F("
"); + + // Kalibrace + html += F("

"); + html += s.sectionCalib; + html += F("

"); + + html += F("
"); + html += s.cfgWindDirOffsetHelp; + html += F("
"); + + char rainHex[8]; + snprintf(rainHex, sizeof(rainHex), "0x%02X", gCfg->rainSensitivity); + html += F("
"); + html += s.cfgRainSensHelp; + html += F("
"); + + html += F("
"); + html += s.cfgSlot507Help; + html += F("
"); + + html += F("
" + "
"); + + // Akce + html += F("

"); + html += s.sectionActions; + html += F("

"); + + html += F("
" + "
" + "" + "
"); + html += s.btnWindZeroHelp; + html += F("
"); + + html += F("
" + "
" + "" + "
"); + html += s.btnRainZeroHelp; + html += F("
"); + + html += htmlFoot(); + gServer->send(200, "text/html; charset=UTF-8", html); +} + +// ─── POST /config ───────────────────────────────────────────────────────────── +static void handleConfigPost() { + bool cz = gServer->hasArg("lang") ? (gServer->arg("lang") != "en") : true; + + if (gServer->hasArg("addr")) { + gCfg->modbusAddr = (uint8_t)constrain(gServer->arg("addr").toInt(), 1, 247); + } + if (gServer->hasArg("baud")) { + uint32_t b = (uint32_t)gServer->arg("baud").toInt(); + if (b != gCfg->baudRate) { gCfg->baudRate = b; gMb->setBaudRate(b); } + } + if (gServer->hasArg("poll")) { + gCfg->pollInterval = (uint8_t)constrain(gServer->arg("poll").toInt(), 1, 60); + } + if (gServer->hasArg("wdOffset")) { + uint8_t v = (uint8_t)(gServer->arg("wdOffset").toInt() & 0x01); + if (v != gCfg->windDirOffset) { + gCfg->windDirOffset = v; + gMb->writeRegister(gCfg->modbusAddr, REG_WIND_DIR_OFFSET, v); + } + } + if (gServer->hasArg("rainSens")) { + uint8_t v = (uint8_t)strtol(gServer->arg("rainSens").c_str(), nullptr, 16); + if (v != gCfg->rainSensitivity) { + gCfg->rainSensitivity = v; + gMb->writeRegister(gCfg->modbusAddr, REG_RAIN_SENS, v); + } + } + if (gServer->hasArg("slot507")) { + gCfg->slot507Mode = (uint8_t)constrain(gServer->arg("slot507").toInt(), 0, 2); + } + + gPrefs->begin("weather", false); + saveConfig(*gPrefs, *gCfg); + gPrefs->end(); + Serial.println(F("[CFG] Konfigurace ulozena.")); + + sendLangCookie(cz); + gServer->sendHeader("Location", cz ? "/config?lang=cs" : "/config?lang=en"); + gServer->send(303); +} + +// ─── POST /action ───────────────────────────────────────────────────────────── +static void handleAction() { + bool cz = gServer->hasArg("lang") ? (gServer->arg("lang") != "en") : true; + String act = gServer->arg("act"); + + if (act == "wind_zero") { + ModbusStatus st = gMb->writeRegister(gCfg->modbusAddr, REG_WIND_ZERO, 0xAA); + Serial.printf("[ACT] Wind zero: %s\n", gMb->statusStr(st)); + } else if (act == "rain_zero") { + ModbusStatus st = gMb->writeRegister(gCfg->modbusAddr, REG_RAIN_ZERO, 0x5A); + Serial.printf("[ACT] Rain zero: %s\n", gMb->statusStr(st)); + } + + sendLangCookie(cz); + gServer->sendHeader("Location", cz ? "/config?lang=cs" : "/config?lang=en"); + gServer->send(303); +} + +// ─── GET /api/data ──────────────────────────────────────────────────────────── +static void handleApiData() { + const SensorData& d = *gData; + String json = "{"; + json += "\"ts\":" + String(d.lastUpdateMs) + ","; + json += "\"err\":" + String(d.commError ? "true" : "false"); + + if (d.present.wind) { + json += ",\"windSpeed\":" + fmtFloat(d.windSpeed); + json += ",\"windForce\":" + String(d.windForce); + json += ",\"windDir8\":" + String(d.windDir8); + json += ",\"windDir360\":" + String(d.windDir360); + } + if (d.present.tempHum) { + json += ",\"temp\":" + fmtFloat(d.temperature); + json += ",\"hum\":" + fmtFloat(d.humidity); + } + if (d.present.pressure) json += ",\"pres\":" + fmtFloat(d.pressure,2); + if (d.present.noise) json += ",\"noise\":" + fmtFloat(d.noise); + if (d.present.pm25) json += ",\"pm25\":" + String(d.pm25); + if (d.present.pm10) json += ",\"pm10\":" + String(d.pm10); + if (d.present.co2) json += ",\"co2\":" + String(d.co2); + if (d.present.light) json += ",\"lux\":" + String(d.luxFull); + if (d.present.rainfall) json += ",\"rain\":" + fmtFloat(d.rainfall); + if (d.present.compass) json += ",\"compass\":" + fmtFloat(d.compass); + if (d.present.solar) json += ",\"solar\":" + String(d.solar); + json += "}"; + + gServer->sendHeader("Access-Control-Allow-Origin", "*"); + gServer->send(200, "application/json", json); +} + +// ─── GET /api/status ────────────────────────────────────────────────────────── +static void handleApiStatus() { + const SensorPresence& p = gData->present; + String json = "{"; + json += "\"wind\":" + String(p.wind ?"true":"false"); + json += ",\"tempHum\":" + String(p.tempHum ?"true":"false"); + json += ",\"pressure\":" + String(p.pressure?"true":"false"); + json += ",\"noise\":" + String(p.noise ?"true":"false"); + json += ",\"pm25\":" + String(p.pm25 ?"true":"false"); + json += ",\"pm10\":" + String(p.pm10 ?"true":"false"); + json += ",\"co2\":" + String(p.co2 ?"true":"false"); + json += ",\"light\":" + String(p.light ?"true":"false"); + json += ",\"rainfall\":" + String(p.rainfall?"true":"false"); + json += ",\"compass\":" + String(p.compass ?"true":"false"); + json += ",\"solar\":" + String(p.solar ?"true":"false"); + json += "}"; + + gServer->sendHeader("Access-Control-Allow-Origin", "*"); + gServer->send(200, "application/json", json); +} + +// ─── GET /api/raw – Poslední TX/RX frame ───────────────────────────────────── +static String hexBuf(const uint8_t* buf, uint8_t len) { + String s; + s.reserve(len * 3); + for (uint8_t i = 0; i < len; i++) { + char h[4]; + snprintf(h, sizeof(h), "%02X ", buf[i]); + s += h; + } + if (s.length() > 0) s.remove(s.length() - 1); + return s; +} + +static void handleApiRaw() { + String json = "{"; + json += "\"tx\":\"" + hexBuf(gMb->lastTx, gMb->lastTxLen) + "\""; + json += ",\"rx\":\"" + hexBuf(gMb->lastRx, gMb->lastRxLen) + "\""; + json += ",\"s\":\"" + String(gMb->statusStr(gMb->lastResult)) + "\""; + json += "}"; + gServer->sendHeader("Access-Control-Allow-Origin", "*"); + gServer->send(200, "application/json", json); +} + +// ─── Inicializace ───────────────────────────────────────────────────────────── +void webServerBegin(SensorData& data, SensorConfig& cfg, + Preferences& prefs, ModbusClient& mb) { + gData = &data; + gCfg = &cfg; + gPrefs = &prefs; + gMb = &mb; + + gServer = new WebServer(80); + static const char* hdrs[] = {"Cookie"}; + gServer->collectHeaders(hdrs, 1); + + gServer->on("/", HTTP_GET, handleRoot); + gServer->on("/detect", HTTP_POST, handleDetect); + gServer->on("/config", HTTP_GET, handleConfig); + gServer->on("/config", HTTP_POST, handleConfigPost); + gServer->on("/action", HTTP_POST, handleAction); + gServer->on("/api/data", HTTP_GET, handleApiData); + gServer->on("/api/status", HTTP_GET, handleApiStatus); + gServer->on("/api/raw", HTTP_GET, handleApiRaw); + gServer->onNotFound([]() { + gServer->send(404, "text/plain", "Not found"); + }); + + gServer->begin(); + Serial.println(F("[WEB] HTTP server spusten na portu 80")); +} + +void webServerHandle() { + if (gServer) gServer->handleClient(); +} diff --git a/SW/ESPlan_WeatherStation/ws_server.h b/SW/ESPlan_WeatherStation/ws_server.h new file mode 100755 index 0000000..0b86dd8 --- /dev/null +++ b/SW/ESPlan_WeatherStation/ws_server.h @@ -0,0 +1,28 @@ +#ifndef WS_SERVER_H +#define WS_SERVER_H +/* + * ws_server.h – HTTP webserver + * + * Stránky: + * GET / Stav snímačů + živá data + * GET /config Konfigurace snímače + * POST /config Uložení konfigurace + * POST /detect Re-detekce snímačů + * POST /action Akce: wind_zero, rain_zero + * GET /api/data JSON s aktuálními daty + * GET /api/status JSON se stavem detekce + */ + +#include +#include +#include "cfg.h" +#include "mb_client.h" +#include "snr.h" +#include "i18n_str.h" + +void webServerBegin(SensorData& data, SensorConfig& cfg, + Preferences& prefs, ModbusClient& mb); + +void webServerHandle(); + +#endif // WS_SERVER_H diff --git a/SW/ESPlan_ZTS3000_WindSensor/ESPlan_ZTS3000_WindSensor.ino b/SW/ESPlan_ZTS3000_WindSensor/ESPlan_ZTS3000_WindSensor.ino new file mode 100755 index 0000000..0bfc973 --- /dev/null +++ b/SW/ESPlan_ZTS3000_WindSensor/ESPlan_ZTS3000_WindSensor.ino @@ -0,0 +1,132 @@ +/* + * ESPlan_ZTS3000_WindSensor.ino + * ZTS-3000-FSJT-N01 - Polykarbonatovy 3-poharovy anemometr + * + * Hardware : LaskaKit ESPlan (ESP32 + LAN8720) + * Snimac : ZTS-3000-FSJT-N01 (Modbus RTU / RS485) + * Komunikace: pouze ETH (LAN8720) + * + * Soubory projektu: + * cfg.h - HW konstanty, registry, datove struktury + * mb_client.h/cpp - Modbus RTU klient (FC03, FC06) + * snr.h/cpp - Detekce a cteni snimace + * i18n_str.h - Preklady CZ / EN + * ws_server.h/cpp - HTTP webserver (stav + konfigurace) + * + * Modbus-RTU parametry snimace (datasheet): + * adresa: 0x01 (vychozi), bps: 4800, 8N1 + * FC03 cteni 2 registru od 0x0000: + * 0x0000 = rychlost vetru x10 (m/s) + * 0x0001 = sila vetru (Beaufort 0-17) + * FC06 zapis adresy do reg 0x07D0 + */ + +#include +#include // nutne pro WiFiEvent_t a WiFi.onEvent +#include +#include + +#include "cfg.h" +#include "mb_client.h" +#include "snr.h" +#include "ws_server.h" + +// ─── Globalni objekty ───────────────────────────────────────────────────────── +Preferences prefs; +SensorData sensorData; +SensorConfig sensorConfig; +ModbusClient modbusClient; + +static bool ethGotIP = false; +static uint32_t lastPollMs = 0; + +// ─── ETH udalosti ───────────────────────────────────────────────────────────── +void onEthEvent(WiFiEvent_t event) { + switch (event) { + case ARDUINO_EVENT_ETH_START: + Serial.println(F("[ETH] Inicializace...")); + ETH.setHostname("espplan-zts3000"); + break; + case ARDUINO_EVENT_ETH_CONNECTED: + Serial.println(F("[ETH] Pripojen (fyzicka vrstva)")); + break; + case ARDUINO_EVENT_ETH_GOT_IP: + Serial.print(F("[ETH] IP: ")); + Serial.println(ETH.localIP()); + ethGotIP = true; + break; + case ARDUINO_EVENT_ETH_DISCONNECTED: + Serial.println(F("[ETH] Odpojeno")); + ethGotIP = false; + break; + case ARDUINO_EVENT_ETH_STOP: + Serial.println(F("[ETH] Zastaveno")); + break; + default: + break; + } +} + +// ─── Setup ──────────────────────────────────────────────────────────────────── +void setup() { + Serial.begin(115200); + delay(500); + Serial.println(F("\n=== ESPlan ZTS-3000 Anemometr ===")); + Serial.println(F(" ZTS-3000-FSJT-N01 Modbus RTU ")); + Serial.println(F("================================")); + + // Nacteni konfigurace z NVS + prefs.begin("zts3000", true); + loadConfig(prefs, sensorConfig); + prefs.end(); + + Serial.printf("[CFG] Modbus addr=%d baud=%d poll=%ds\n", + sensorConfig.modbusAddr, + sensorConfig.baudRate, + sensorConfig.pollInterval); + + // RS485 / Modbus + modbusClient.begin(sensorConfig.baudRate, sensorConfig.modbusAddr); + + // ETH + WiFi.onEvent(onEthEvent); + ETH.begin(ETH_PHY_TYPE, + ETH_PHY_ADDR, + ETH_PHY_MDC, + ETH_PHY_MDIO, + ETH_PHY_POWER, + ETH_CLK_MODE); + + // Cekame na IP (max 10 s) + uint32_t t0 = millis(); + while (!ethGotIP && (millis() - t0 < 10000)) { + delay(100); + } + + if (!ethGotIP) { + Serial.println(F("[ETH] VAROVANI: IP neziskano - web nebude dostupny!")); + } + + // Webserver + webServerBegin(sensorData, sensorConfig, prefs, modbusClient); + + // Prvni detekce + Serial.println(F("[SNS] Detekuji snimac...")); + detectSensors(modbusClient, sensorData, sensorConfig); + printSensorStatus(sensorData); + + Serial.println(F("[SYS] Inicializace dokoncena.")); +} + +// ─── Loop ───────────────────────────────────────────────────────────────────── +void loop() { + webServerHandle(); + + uint32_t now = millis(); + if (now - lastPollMs >= (uint32_t)sensorConfig.pollInterval * 1000UL) { + lastPollMs = now; + readAllSensors(modbusClient, sensorData, sensorConfig); + } + + delay(5); +} diff --git a/SW/ESPlan_ZTS3000_WindSensor/cfg.h b/SW/ESPlan_ZTS3000_WindSensor/cfg.h new file mode 100755 index 0000000..28cbd94 --- /dev/null +++ b/SW/ESPlan_ZTS3000_WindSensor/cfg.h @@ -0,0 +1,120 @@ +#ifndef WS_CFG_H +#define WS_CFG_H +/* + * cfg.h – Konfigurace HW a datove struktury + * + * Snimac: ZTS-3000-FSJT-N01 (polykarbonatovy 3-pohárový anemometr) + * - RS485, Modbus-RTU, 8N1, vychozi 4800 bps + * - Vychozi adresa: 0x01 (1) + * - 2 vstupni registry (FC 0x03): + * reg 0x0000 = rychlost vetru x10 (napr. 0x0024 = 36 = 3.6 m/s) + * reg 0x0001 = sila vetru (Beaufort 0-17) + * - Zmena adresy: zapis (FC 0x06) do reg 0x07D0 + * + * POZOR: Konfigurace HW pinu je pevna (nelze menit pres web). + * Pres webove rozhrani lze menit pouze komunikacni parametry + * a adresu snimace. + */ + +#include +#include + +// ─── ETH (LAN8720 na ESPlan) ────────────────────────────────────────────────── +#define ETH_PHY_TYPE ETH_PHY_LAN8720 +#define ETH_PHY_ADDR 0 +#define ETH_PHY_MDC 23 +#define ETH_PHY_MDIO 18 +#define ETH_PHY_POWER -1 +#define ETH_CLK_MODE ETH_CLOCK_GPIO17_OUT + +// ─── RS485 / UART ───────────────────────────────────────────────────────────── +#define PIN_RS485_RX 36 +#define PIN_RS485_TX 4 +// DE pin je rizen hardwarove – nepotrebujeme softwarove ovladani + +// ─── Modbus vychozi hodnoty ─────────────────────────────────────────────────── +#define DEFAULT_MODBUS_ADDR 1 +#define DEFAULT_BAUD_RATE 4800 // datasheet ZTS-3000-FSJT-N01 +#define DEFAULT_POLL_INTERVAL 5 // sekundy + +// ─── Modbus registry snimace (ZTS-3000-FSJT-N01) ───────────────────────────── +// Cteni (FC03): +#define REG_WIND_SPEED 0x0000 // rychlost vetru * 10 (m/s) +#define REG_WIND_LEVEL 0x0001 // sila vetru (Beaufort 0-17) +#define REG_BLOCK_COUNT 2 // celkem 2 registry + +// Zapis (FC06): +#define REG_DEVICE_ADDR 0x07D0 // adresa snimace (1-247) + +// ─── Flag pritomnosti snimace ──────────────────────────────────────────────── +struct SensorPresence { + bool wind = false; +}; + +// ─── Namerene hodnoty ───────────────────────────────────────────────────────── +struct SensorData { + SensorPresence present; + + float windSpeed = 0.0f; // m/s + uint16_t windLevel = 0; // Beaufort 0-17 + + uint32_t lastUpdateMs = 0; + bool commError = false; + String errorMsg = ""; +}; + +// ─── Konfigurace snimace (uklada se do NVS) ─────────────────────────────────── +struct SensorConfig { + uint8_t modbusAddr = DEFAULT_MODBUS_ADDR; + uint32_t baudRate = DEFAULT_BAUD_RATE; + uint8_t pollInterval = DEFAULT_POLL_INTERVAL; +}; + +// ─── NVS load / save ────────────────────────────────────────────────────────── +inline void loadConfig(Preferences &p, SensorConfig &c) { + c.modbusAddr = p.getUChar("mbAddr", DEFAULT_MODBUS_ADDR); + c.baudRate = p.getUInt ("baud", DEFAULT_BAUD_RATE); + c.pollInterval = p.getUChar("poll", DEFAULT_POLL_INTERVAL); +} + +inline void saveConfig(Preferences &p, const SensorConfig &c) { + p.putUChar("mbAddr", c.modbusAddr); + p.putUInt ("baud", c.baudRate); + p.putUChar("poll", c.pollInterval); +} + +// ─── Tabulka Beaufortovy stupnice (cz/en nazev a popis) ────────────────────── +// Podle datasheetu ZTS-3000 (0..17). Hodnoty rozsahu m/s a popisy lze ziskat +// pres funkce nize. Tento snimac ovsem rovnou vraci stupen v reg 0x0001. +struct BeaufortInfo { + uint8_t level; + const char* nameCz; // cesky nazev + const char* nameEn; // anglicky nazev +}; + +inline const BeaufortInfo& beaufortInfo(uint8_t lvl) { + static const BeaufortInfo TBL[] = { + { 0, "Bezvetri", "Calm" }, + { 1, "Vanek", "Light air" }, + { 2, "Slaby vitr", "Light breeze" }, + { 3, "Mirny vitr", "Gentle breeze" }, + { 4, "Dosti silny vitr", "Moderate breeze" }, + { 5, "Cerstvy vitr", "Fresh breeze" }, + { 6, "Silny vitr", "Strong breeze" }, + { 7, "Prudky vitr", "Near gale" }, + { 8, "Bourlivy vitr", "Gale" }, + { 9, "Vichrice", "Strong gale" }, + {10, "Silna vichrice", "Storm" }, + {11, "Mohutna vichrice", "Violent storm" }, + {12, "Orkan", "Hurricane" }, + {13, "Orkan", "Hurricane" }, + {14, "Orkan", "Hurricane" }, + {15, "Orkan", "Hurricane" }, + {16, "Orkan", "Hurricane" }, + {17, "Orkan", "Hurricane" }, + }; + if (lvl > 17) lvl = 17; + return TBL[lvl]; +} + +#endif // WS_CFG_H diff --git a/SW/ESPlan_ZTS3000_WindSensor/i18n_str.h b/SW/ESPlan_ZTS3000_WindSensor/i18n_str.h new file mode 100755 index 0000000..28b2cd4 --- /dev/null +++ b/SW/ESPlan_ZTS3000_WindSensor/i18n_str.h @@ -0,0 +1,158 @@ +#ifndef WS_I18N_STR_H +#define WS_I18N_STR_H +/* + * i18n_str.h – Preklady webove rozhrani (CZ / EN) + * pro snimac ZTS-3000-FSJT-N01 + */ + +#include + +struct I18nStrings { + // Navigace + const char* title; + const char* navStatus; + const char* navConfig; + const char* langSwitch; + + // Stavova stranka + const char* pageStatusTitle; + const char* sectionSensors; + const char* sectionValues; + const char* colSensor; + const char* colPresent; + const char* colValue; + const char* colUnit; + const char* yes; + const char* no; + const char* lastUpdate; + const char* noData; + const char* commError; + const char* btnDetect; + + // Nazvy mereni + const char* snsWind; + const char* snsWindSpeed; + const char* snsWindLevel; + const char* snsWindLevelName; + const char* unitMs; + const char* unitBft; + + // Konfigurace + const char* pageConfigTitle; + const char* sectionComm; + const char* cfgAddr; + const char* cfgAddrHelp; + const char* cfgBaud; + const char* cfgBaudHelp; + const char* cfgPoll; + const char* cfgPollHelp; + + const char* sectionActions; + const char* btnChangeAddr; + const char* btnChangeAddrHelp; + const char* cfgNewAddr; + + const char* btnSave; + + const char* sectionRaw; +}; + +// ─── Cestina ────────────────────────────────────────────────────────────────── +static const I18nStrings STR_CZ = { + "ESPlan ZTS-3000 Anemometr", + "Stav snimace", + "Konfigurace", + "English", + + "Stav a hodnoty snimace", + "Detekce snimace", + "Namerene hodnoty", + "Velicina", + "Pritomnost", + "Hodnota", + "Jednotka", + "✓ Pritomno", + "— Nepritomno", + "Posledni aktualizace", + "—", + "⚠ Chyba komunikace", + "Znovu detekovat", + + "Anemometr ZTS-3000", + "Rychlost vetru", + "Sila vetru (Beaufort)", + "Slovni popis", + "m/s", + "Bft", + + "Konfigurace snimace", + "Komunikacni parametry", + "Modbus adresa", + "Aktualni adresa snimace na sbernici RS485 (1-247, vychozi: 1)", + "Prenosova rychlost", + "Baudrate RS485 (datasheet: 4800 bps)", + "Interval cteni", + "Interval periodickeho cteni dat v sekundach (1-60)", + + "Akce snimace", + "Zmenit adresu snimace", + "Zapise novou adresu do reg. 0x07D0 snimace (FC06). Pote upravte adresu vyse.", + "Nova adresa", + + "Ulozit konfiguraci", + + "RS485 komunikace (raw)" +}; + +// ─── Anglictina ─────────────────────────────────────────────────────────────── +static const I18nStrings STR_EN = { + "ESPlan ZTS-3000 Anemometer", + "Sensor Status", + "Configuration", + "Cesky", + + "Sensor Status & Readings", + "Sensor Detection", + "Live Readings", + "Quantity", + "Presence", + "Value", + "Unit", + "✓ Present", + "— Not present", + "Last update", + "—", + "⚠ Communication error", + "Re-detect sensor", + + "ZTS-3000 anemometer", + "Wind speed", + "Wind force (Beaufort)", + "Description", + "m/s", + "Bft", + + "Sensor Configuration", + "Communication Settings", + "Modbus address", + "Current sensor address on the RS485 bus (1-247, default: 1)", + "Baud rate", + "RS485 baud rate (datasheet: 4800 bps)", + "Poll interval", + "Data polling interval in seconds (1-60)", + + "Sensor Actions", + "Change sensor address", + "Writes the new address into register 0x07D0 (FC06). Update the address above afterwards.", + "New address", + + "Save configuration", + + "RS485 Communication (raw)" +}; + +inline const I18nStrings& getStrings(bool isCz) { + return isCz ? STR_CZ : STR_EN; +} + +#endif // WS_I18N_STR_H diff --git a/SW/ESPlan_ZTS3000_WindSensor/mb_client.cpp b/SW/ESPlan_ZTS3000_WindSensor/mb_client.cpp new file mode 100755 index 0000000..1fd0534 --- /dev/null +++ b/SW/ESPlan_ZTS3000_WindSensor/mb_client.cpp @@ -0,0 +1,152 @@ +/* + * mb_client.cpp – Implementace Modbus RTU klienta + */ + +#include "mb_client.h" + +uint16_t ModbusClient::crc16(const uint8_t* data, uint8_t len) { + uint16_t crc = 0xFFFF; + for (uint8_t i = 0; i < len; i++) { + crc ^= (uint16_t)data[i]; + for (uint8_t b = 0; b < 8; b++) { + if (crc & 0x0001) crc = (crc >> 1) ^ 0xA001; + else crc >>= 1; + } + } + return crc; +} + +void ModbusClient::flushRx() { + while (_serial->available()) _serial->read(); +} + +void ModbusClient::begin(uint32_t baud, uint8_t /*addr*/) { + _baud = baud; + _serial = &Serial2; + _serial->begin(_baud, SERIAL_8N1, PIN_RS485_RX, PIN_RS485_TX); + Serial.printf("[MOD] RS485 init: %d baud, 8N1, RX=%d TX=%d\n", + _baud, PIN_RS485_RX, PIN_RS485_TX); +} + +void ModbusClient::setBaudRate(uint32_t baud) { + _baud = baud; + _serial->end(); + _serial->begin(_baud, SERIAL_8N1, PIN_RS485_RX, PIN_RS485_TX); + Serial.printf("[MOD] Baudrate zmenen na: %d\n", _baud); +} + +ModbusStatus ModbusClient::readRegisters(uint8_t slaveAddr, uint16_t startReg, + uint8_t count, uint16_t* out) { + if (!_serial || count == 0 || count > 64) return ModbusStatus::FRAME_ERROR; + + uint8_t req[8]; + req[0] = slaveAddr; + req[1] = 0x03; + req[2] = (startReg >> 8) & 0xFF; + req[3] = startReg & 0xFF; + req[4] = 0x00; + req[5] = count; + uint16_t c = crc16(req, 6); + req[6] = c & 0xFF; + req[7] = (c >> 8) & 0xFF; + + // Ulozit TX + memcpy(lastTx, req, 8); + lastTxLen = 8; + + flushRx(); + _serial->write(req, 8); + _serial->flush(); + + uint8_t expectedLen = 3 + count * 2 + 2; + uint8_t buf[160]; + uint8_t rxLen = 0; + uint32_t t0 = millis(); + + while (rxLen < expectedLen && (millis() - t0 < TIMEOUT_MS)) { + if (_serial->available()) buf[rxLen++] = _serial->read(); + yield(); + } + + // Ulozit RX + uint8_t copyLen = (rxLen < sizeof(lastRx)) ? rxLen : sizeof(lastRx); + memcpy(lastRx, buf, copyLen); + lastRxLen = copyLen; + + if (rxLen < expectedLen) { + lastResult = ModbusStatus::TIMEOUT; + return ModbusStatus::TIMEOUT; + } + + uint16_t rxCrc = (uint16_t)buf[rxLen-1] << 8 | buf[rxLen-2]; + uint16_t calcCrc = crc16(buf, rxLen - 2); + if (rxCrc != calcCrc) { + lastResult = ModbusStatus::CRC_ERROR; + return ModbusStatus::CRC_ERROR; + } + + if (buf[1] & 0x80) { + lastResult = ModbusStatus::EXCEPTION; + return ModbusStatus::EXCEPTION; + } + + lastResult = ModbusStatus::OK; + for (uint8_t i = 0; i < count; i++) { + out[i] = (uint16_t)buf[3 + i*2] << 8 | buf[4 + i*2]; + } + return ModbusStatus::OK; +} + +ModbusStatus ModbusClient::writeRegister(uint8_t slaveAddr, uint16_t reg, uint16_t value) { + if (!_serial) return ModbusStatus::FRAME_ERROR; + + uint8_t req[8]; + req[0] = slaveAddr; + req[1] = 0x06; + req[2] = (reg >> 8) & 0xFF; + req[3] = reg & 0xFF; + req[4] = (value >> 8) & 0xFF; + req[5] = value & 0xFF; + uint16_t c = crc16(req, 6); + req[6] = c & 0xFF; + req[7] = (c >> 8) & 0xFF; + + memcpy(lastTx, req, 8); + lastTxLen = 8; + + flushRx(); + _serial->write(req, 8); + _serial->flush(); + + uint8_t buf[8]; + uint8_t rxLen = 0; + uint32_t t0 = millis(); + + while (rxLen < 8 && (millis() - t0 < TIMEOUT_MS)) { + if (_serial->available()) buf[rxLen++] = _serial->read(); + yield(); + } + + memcpy(lastRx, buf, rxLen); + lastRxLen = rxLen; + + if (rxLen < 8) { lastResult = ModbusStatus::TIMEOUT; return ModbusStatus::TIMEOUT; } + + uint16_t rxCrc = (uint16_t)buf[7] << 8 | buf[6]; + uint16_t calcCrc = crc16(buf, 6); + if (rxCrc != calcCrc) { lastResult = ModbusStatus::CRC_ERROR; return ModbusStatus::CRC_ERROR; } + + lastResult = ModbusStatus::OK; + return ModbusStatus::OK; +} + +const char* ModbusClient::statusStr(ModbusStatus s) { + switch (s) { + case ModbusStatus::OK: return "OK"; + case ModbusStatus::TIMEOUT: return "TIMEOUT"; + case ModbusStatus::CRC_ERROR: return "CRC_ERROR"; + case ModbusStatus::EXCEPTION: return "EXCEPTION"; + case ModbusStatus::FRAME_ERROR: return "FRAME_ERROR"; + default: return "UNKNOWN"; + } +} diff --git a/SW/ESPlan_ZTS3000_WindSensor/mb_client.h b/SW/ESPlan_ZTS3000_WindSensor/mb_client.h new file mode 100755 index 0000000..12bfc45 --- /dev/null +++ b/SW/ESPlan_ZTS3000_WindSensor/mb_client.h @@ -0,0 +1,54 @@ +#ifndef WS_MB_CLIENT_H +#define WS_MB_CLIENT_H +/* + * mb_client.h – Modbus RTU klient pres RS485 / HardwareSerial + * + * FC03 – Read Holding Registers + * FC06 – Write Single Register + * + * DE pin (direction enable) rizen HW – nevyzaduje softwarove prepinani. + */ + +#include +#include "cfg.h" + +enum class ModbusStatus : uint8_t { + OK = 0, + TIMEOUT, + CRC_ERROR, + EXCEPTION, + FRAME_ERROR +}; + +class ModbusClient { +public: + ModbusClient() = default; + + void begin(uint32_t baud = 4800, uint8_t addr = 1); + + ModbusStatus readRegisters(uint8_t slaveAddr, uint16_t startReg, + uint8_t count, uint16_t* out); + + ModbusStatus writeRegister(uint8_t slaveAddr, uint16_t reg, uint16_t value); + + void setBaudRate(uint32_t baud); + + const char* statusStr(ModbusStatus s); + + // ── Posledni TX/RX frame ──────────────────────────────────────────── + uint8_t lastTx[16]; + uint8_t lastTxLen = 0; + uint8_t lastRx[80]; + uint8_t lastRxLen = 0; + ModbusStatus lastResult = ModbusStatus::OK; + +private: + HardwareSerial* _serial = nullptr; + uint32_t _baud = 4800; + static const uint16_t TIMEOUT_MS = 500; + + uint16_t crc16(const uint8_t* data, uint8_t len); + void flushRx(); +}; + +#endif // WS_MB_CLIENT_H diff --git a/SW/ESPlan_ZTS3000_WindSensor/snr.cpp b/SW/ESPlan_ZTS3000_WindSensor/snr.cpp new file mode 100755 index 0000000..8b4cc80 --- /dev/null +++ b/SW/ESPlan_ZTS3000_WindSensor/snr.cpp @@ -0,0 +1,85 @@ +/* + * snr.cpp – Detekce a cteni snimace ZTS-3000-FSJT-N01 + * + * Snimac ma 2 registry pristupne pres FC03: + * reg 0x0000 = rychlost vetru x10 (m/s) + * reg 0x0001 = sila vetru (Beaufort 0-17) + * + * Detekce: pokud cteni 2 registru projde (CRC OK, zadna exception), + * snimac povazujeme za pritomny. Bezvetri (0,0) je validni stav. + */ + +#include "snr.h" + +// ─── Detekce ────────────────────────────────────────────────────────────────── +void detectSensors(ModbusClient& mb, SensorData& data, const SensorConfig& cfg) { + SensorPresence& p = data.present; + uint8_t addr = cfg.modbusAddr; + + p = SensorPresence(); // reset + + Serial.println(F("[SNS] === Detekce ZTS-3000-FSJT-N01 ===")); + + uint16_t r[REG_BLOCK_COUNT] = {0}; + ModbusStatus st = mb.readRegisters(addr, REG_WIND_SPEED, REG_BLOCK_COUNT, r); + + if (st != ModbusStatus::OK) { + Serial.printf("[SNS] Chyba cteni: %s\n", mb.statusStr(st)); + data.commError = true; + data.errorMsg = mb.statusStr(st); + return; + } + + Serial.printf("[SNS] Cteni: speed_raw=%u (=%.1f m/s) level=%u\n", + r[0], r[0] / 10.0f, r[1]); + + data.commError = false; + data.errorMsg = ""; + p.wind = true; + + // rovnou ulozime hodnoty + data.windSpeed = r[0] / 10.0f; + data.windLevel = r[1]; + data.lastUpdateMs = millis(); + + Serial.println(F("[SNS] === Detekce dokoncena: snimac pritomny ===")); +} + +// ─── Cteni snimace ──────────────────────────────────────────────────────────── +void readAllSensors(ModbusClient& mb, SensorData& data, const SensorConfig& cfg) { + uint16_t r[REG_BLOCK_COUNT] = {0}; + ModbusStatus st = mb.readRegisters(cfg.modbusAddr, REG_WIND_SPEED, + REG_BLOCK_COUNT, r); + + if (st != ModbusStatus::OK) { + data.commError = true; + data.errorMsg = mb.statusStr(st); + Serial.printf("[SNS] Chyba cteni: %s\n", mb.statusStr(st)); + return; + } + + data.commError = false; + data.errorMsg = ""; + data.lastUpdateMs = millis(); + + // Pokud predtim selhala detekce, oznacime snimac jako pritomny + if (!data.present.wind) data.present.wind = true; + + data.windSpeed = r[0] / 10.0f; + data.windLevel = r[1]; +} + +// ─── Vypis stavu ───────────────────────────────────────────────────────────── +void printSensorStatus(const SensorData& data) { + const SensorPresence& p = data.present; + Serial.println(F("[SNS] +--------------------------+")); + Serial.println(F("[SNS] | ZTS-3000-FSJT-N01 |")); + Serial.println(F("[SNS] +--------------------------+")); + Serial.printf ("[SNS] | Snimac pritomny: %-3s |\n", p.wind ? "ANO" : "NE"); + if (p.wind) { + const BeaufortInfo& bi = beaufortInfo(data.windLevel); + Serial.printf("[SNS] | Rychlost: %5.1f m/s |\n", data.windSpeed); + Serial.printf("[SNS] | Sila: %u (%s) |\n", data.windLevel, bi.nameCz); + } + Serial.println(F("[SNS] +--------------------------+")); +} diff --git a/SW/ESPlan_ZTS3000_WindSensor/snr.h b/SW/ESPlan_ZTS3000_WindSensor/snr.h new file mode 100755 index 0000000..377a484 --- /dev/null +++ b/SW/ESPlan_ZTS3000_WindSensor/snr.h @@ -0,0 +1,14 @@ +#ifndef WS_SNR_H +#define WS_SNR_H +/* + * snr.h – Detekce a cteni snimace ZTS-3000-FSJT-N01 + */ + +#include "cfg.h" +#include "mb_client.h" + +void detectSensors(ModbusClient& mb, SensorData& data, const SensorConfig& cfg); +void readAllSensors(ModbusClient& mb, SensorData& data, const SensorConfig& cfg); +void printSensorStatus(const SensorData& data); + +#endif // WS_SNR_H diff --git a/SW/ESPlan_ZTS3000_WindSensor/ws_server.cpp b/SW/ESPlan_ZTS3000_WindSensor/ws_server.cpp new file mode 100755 index 0000000..e3016b0 --- /dev/null +++ b/SW/ESPlan_ZTS3000_WindSensor/ws_server.cpp @@ -0,0 +1,592 @@ +/* + * ws_server.cpp – HTTP webserver implementace + * ZTS-3000-FSJT-N01 + */ + +#include "ws_server.h" +#include + +// ─── Globalni pointery ──────────────────────────────────────────────────────── +static WebServer* gServer = nullptr; +static SensorData* gData = nullptr; +static SensorConfig* gCfg = nullptr; +static Preferences* gPrefs = nullptr; +static ModbusClient* gMb = nullptr; + +// ─── Pomocne ───────────────────────────────────────────────────────────────── +static bool isLangCz() { + if (gServer->hasArg("lang")) { + String l = gServer->arg("lang"); + return !(l == "en"); + } + if (gServer->hasHeader("Cookie")) { + String c = gServer->header("Cookie"); + if (c.indexOf("lang=en") >= 0) return false; + } + return true; +} + +static void sendLangCookie(bool cz) { + gServer->sendHeader("Set-Cookie", + cz ? "lang=cs; Path=/; Max-Age=31536000" + : "lang=en; Path=/; Max-Age=31536000"); +} + +static String fmtFloat(float v, int dec = 1) { + char buf[20]; + snprintf(buf, sizeof(buf), "%.*f", dec, v); + return String(buf); +} + +// ─── CSS ───────────────────────────────────────────────────────────────────── +static const char CSS[] PROGMEM = R"CSS( +:root{ + --bg:#0d1117;--bg2:#161b22;--bg3:#21262d; + --border:#30363d;--border2:#484f58; + --text:#e6edf3;--text2:#8b949e;--text3:#6e7681; + --accent:#58a6ff;--accent2:#1f6feb; + --green:#3fb950;--red:#f85149;--yellow:#d29922; +} +*{box-sizing:border-box;margin:0;padding:0} +body{background:var(--bg);color:var(--text);font-family:'Segoe UI',system-ui,sans-serif; + min-height:100vh;font-size:14px;line-height:1.5} +a{color:var(--accent);text-decoration:none} +a:hover{text-decoration:underline} +.topbar{background:var(--bg2);border-bottom:1px solid var(--border); + padding:0 24px;display:flex;align-items:center;height:52px; + position:sticky;top:0;z-index:100} +.logo{display:flex;align-items:center;gap:10px;font-weight:700;font-size:15px; + color:var(--text);margin-right:32px;white-space:nowrap} +.logo-icon{width:28px;height:28px;background:#1f6feb;border-radius:6px; + display:flex;align-items:center;justify-content:center;flex-shrink:0} +.nav-links{display:flex;gap:2px;flex:1} +.nav-link{padding:6px 14px;border-radius:6px;font-size:13px; + color:var(--text2);transition:background .15s,color .15s;white-space:nowrap} +.nav-link:hover{background:var(--bg3);color:var(--text);text-decoration:none} +.nav-link.active{background:var(--accent2);color:#fff} +.lang-btn{margin-left:auto;padding:5px 12px;border-radius:20px; + background:var(--bg3);border:1px solid var(--border2); + color:var(--text2);font-size:12px;white-space:nowrap} +.lang-btn:hover{background:var(--border);color:var(--text);text-decoration:none} +.main{max-width:1100px;margin:0 auto;padding:28px 20px} +h1{font-size:20px;font-weight:600;margin-bottom:20px} +h2{font-size:12px;font-weight:600;color:var(--text3);text-transform:uppercase; + letter-spacing:.08em;margin:28px 0 12px;padding-bottom:8px; + border-bottom:1px solid var(--border)} +h2:first-of-type{margin-top:0} +.card{background:var(--bg2);border:1px solid var(--border); + border-radius:10px;padding:16px 20px;margin-bottom:16px} +table{width:100%;border-collapse:collapse;font-size:13px} +th{padding:8px 12px;text-align:left;font-weight:600;font-size:11px; + color:var(--text3);text-transform:uppercase;letter-spacing:.06em; + border-bottom:1px solid var(--border);white-space:nowrap} +td{padding:9px 12px;border-bottom:1px solid var(--border);vertical-align:middle} +tr:last-child td{border-bottom:none} +tr:hover td{background:rgba(255,255,255,.02)} +.val{font-family:'Courier New',monospace;font-size:13px;color:var(--text)} +.unit{color:var(--text3);font-size:12px} +.badge-yes{display:inline-flex;align-items:center;gap:6px;color:var(--green);font-size:12px} +.badge-no{display:inline-flex;align-items:center;gap:6px;color:var(--text3);font-size:12px} +.dot{width:8px;height:8px;border-radius:50%;flex-shrink:0} +.dot-g{background:var(--green);box-shadow:0 0 6px var(--green); + animation:pulse 2s ease-in-out infinite} +.dot-x{background:var(--text3)} +@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}} +.toolbar{display:flex;align-items:center;gap:8px;margin-bottom:16px;flex-wrap:wrap} +.smeta{margin-left:auto;font-size:12px;color:var(--text3);font-family:'Courier New',monospace} +.err-banner{background:rgba(248,81,73,.1);border:1px solid var(--red); + border-radius:8px;padding:10px 16px;color:var(--red);font-size:13px; + margin-bottom:16px;display:flex;align-items:center;gap:8px} +.ip-bar{display:flex;align-items:center;gap:20px;padding:10px 16px; + background:var(--bg3);border:1px solid var(--border);border-radius:8px; + font-family:'Courier New',monospace;font-size:12px;color:var(--text2); + margin-bottom:20px;flex-wrap:wrap} +.ip-lbl{color:var(--text3)} +.btn{display:inline-flex;align-items:center;gap:6px;padding:7px 14px; + border-radius:6px;font-size:13px;cursor:pointer;border:1px solid transparent; + font-family:inherit;transition:background .15s;text-decoration:none} +.btn:active{opacity:.8} +.btn-p{background:var(--accent2);color:#fff;border-color:var(--accent2)} +.btn-p:hover{background:#388bfd;text-decoration:none;color:#fff} +.btn-d{background:var(--bg3);color:var(--text);border-color:var(--border2)} +.btn-d:hover{background:var(--border);text-decoration:none} +.btn-w{background:transparent;color:var(--yellow);border-color:var(--yellow)} +.btn-w:hover{background:rgba(210,153,34,.1);text-decoration:none} +.btn-sm{padding:4px 10px;font-size:12px} +.fg{margin-bottom:18px} +label{display:block;font-size:13px;font-weight:500;color:var(--text2);margin-bottom:5px} +.ht{font-size:11px;color:var(--text3);margin-top:4px} +input[type=number],input[type=text],select{ + background:var(--bg3);border:1px solid var(--border2);border-radius:6px; + padding:7px 10px;color:var(--text);font-size:13px;width:100%; + max-width:280px;outline:none;transition:border-color .15s;font-family:inherit} +input:focus,select:focus{border-color:var(--accent)} +select option{background:var(--bg3)} +.frow{display:flex;gap:16px;flex-wrap:wrap} +.frow .fg{flex:1;min-width:200px} +.act-row{display:flex;gap:16px;flex-wrap:wrap;align-items:flex-start} +.act-item{flex:1;min-width:200px} +.live-dot{width:8px;height:8px;border-radius:50%;background:var(--green); + box-shadow:0 0 6px var(--green);animation:pulse 2s ease-in-out infinite; + flex-shrink:0;margin-left:auto} +.val.flash{animation:valflash .4s ease-out} +@keyframes valflash{0%{color:var(--accent)}100%{color:var(--text)}} +.raw-panel{background:var(--bg2);border:1px solid var(--border);border-radius:10px; + padding:14px 20px;margin-bottom:16px;font-family:'Courier New',monospace; + font-size:12px;line-height:1.8} +.raw-tx{color:var(--yellow)} +.raw-rx{color:var(--green)} +.raw-err{color:var(--red)} +.raw-label{color:var(--text3);display:inline-block;width:28px} +.raw-status{display:inline-block;padding:1px 6px;border-radius:3px;font-size:10px; + font-weight:600;text-transform:uppercase;margin-left:8px} +.raw-status.ok{background:rgba(63,185,80,.15);color:var(--green)} +.raw-status.err{background:rgba(248,81,73,.15);color:var(--red)} +.bigval{display:flex;align-items:baseline;gap:8px;margin:4px 0} +.bigval .num{font-size:34px;font-weight:700;color:var(--accent);font-family:'Courier New',monospace} +.bigval .un{font-size:14px;color:var(--text2)} +@media(max-width:640px){ + .topbar{padding:0 12px} + .logo{margin-right:12px} + .main{padding:16px 12px} + th,td{padding:7px 8px} + input,select{max-width:100%} + .smeta{margin-left:0} +} +)CSS"; + +// ─── HTML head + nav ───────────────────────────────────────────────────────── +static String htmlHead(const I18nStrings& s, bool cz, const char* page) { + String langUrl = cz ? "?lang=en" : "?lang=cs"; + String out; + out.reserve(3000); + + out += F("" + "" + ""); + out += s.title; + out += F("" + "
"); + return out; +} + +static String htmlFoot() { + return F("
"); +} + +// ─── Stranka: Stav ──────────────────────────────────────────────────────────── +static void handleRoot() { + bool cz = isLangCz(); + sendLangCookie(cz); + const I18nStrings& s = getStrings(cz); + + String html = htmlHead(s, cz, "status"); + + // IP bar + html += F("
" + "IP: "); + html += ETH.localIP().toString(); + html += F("MAC: "); + html += ETH.macAddress(); + html += F("ETH: "); + html += ETH.linkSpeed(); + html += F(" Mbit/s
"); + + if (gData->commError) { + html += F("
⚠ "); + html += s.commError; + html += F(": "); + html += gData->errorMsg; + html += F("
"); + } + + // Toolbar + html += F("
" + "
" + "
" + "" + ""); + html += F("
"); + + // Tabulka detekce + html += F("

"); + html += s.sectionSensors; + html += F("

" + ""); + + const SensorPresence& p = gData->present; + html += F(""); + html += F("
"); + html += s.colSensor; + html += F(""); + html += s.colPresent; + html += F("
"); + html += s.snsWind; + html += F(""); + if (p.wind) { + html += F(""); + html += s.yes; + } else { + html += F(""); + html += s.no; + } + html += F("
"); + + // Tabulka hodnot + html += F("

"); + html += s.sectionValues; + html += F("

"); + + const SensorData& d = *gData; + const BeaufortInfo& bi = beaufortInfo(d.windLevel); + const char* bName = cz ? bi.nameCz : bi.nameEn; + + // Rychlost vetru + html += F(""); + + // Sila vetru + html += F(""); + + // Slovni popis + html += F(""); + + html += F("
"); + html += s.colSensor; + html += F(""); + html += s.colValue; + html += F(""); + html += s.colUnit; + html += F("
"); + html += s.snsWindSpeed; + html += F(""); + html += p.wind ? fmtFloat(d.windSpeed) : String(s.noData); + html += F(""); + html += p.wind ? s.unitMs : ""; + html += F("
"); + html += s.snsWindLevel; + html += F(""); + html += p.wind ? String(d.windLevel) : String(s.noData); + html += F(""); + html += p.wind ? s.unitBft : ""; + html += F("
"); + html += s.snsWindLevelName; + html += F(""); + html += p.wind ? bName : s.noData; + html += F("
"); + + // ── Raw RS485 panel ──────────────────────────────────────────────────── + html += F("

"); + html += s.sectionRaw; + html += F("

" + "
TX: ..." + "
" + "
RX: ...
" + "
"); + + // Inline JavaScript – AJAX auto-refresh + html += F(""); + + html += htmlFoot(); + gServer->send(200, "text/html; charset=UTF-8", html); +} + +// ─── POST /detect ───────────────────────────────────────────────────────────── +static void handleDetect() { + detectSensors(*gMb, *gData, *gCfg); + bool cz = isLangCz(); + sendLangCookie(cz); + gServer->sendHeader("Location", cz ? "/?lang=cs" : "/?lang=en"); + gServer->send(303); +} + +// ─── GET /config ────────────────────────────────────────────────────────────── +static void handleConfig() { + bool cz = isLangCz(); + sendLangCookie(cz); + const I18nStrings& s = getStrings(cz); + + String html = htmlHead(s, cz, "config"); + html += F("

"); + html += s.pageConfigTitle; + html += F("

"); + html += F(""); + + // Komunikace + html += F("

"); + html += s.sectionComm; + html += F("

"); + + html += F("
"); + html += s.cfgAddrHelp; + html += F("
"); + + html += F("
"); + html += s.cfgBaudHelp; + html += F("
"); + + html += F("
"); + html += s.cfgPollHelp; + html += F("
"); + + html += F("
" + "
"); + + // Akce – zmena adresy snimace + html += F("

"); + html += s.sectionActions; + html += F("

" + "
" + "" + "
" + "
"); + html += s.btnChangeAddrHelp; + html += F("
"); + + html += htmlFoot(); + gServer->send(200, "text/html; charset=UTF-8", html); +} + +// ─── POST /config ───────────────────────────────────────────────────────────── +static void handleConfigPost() { + bool cz = gServer->hasArg("lang") ? (gServer->arg("lang") != "en") : true; + + if (gServer->hasArg("addr")) { + gCfg->modbusAddr = (uint8_t)constrain(gServer->arg("addr").toInt(), 1, 247); + } + if (gServer->hasArg("baud")) { + uint32_t b = (uint32_t)gServer->arg("baud").toInt(); + if (b != gCfg->baudRate) { gCfg->baudRate = b; gMb->setBaudRate(b); } + } + if (gServer->hasArg("poll")) { + gCfg->pollInterval = (uint8_t)constrain(gServer->arg("poll").toInt(), 1, 60); + } + + gPrefs->begin("zts3000", false); + saveConfig(*gPrefs, *gCfg); + gPrefs->end(); + Serial.println(F("[CFG] Konfigurace ulozena.")); + + sendLangCookie(cz); + gServer->sendHeader("Location", cz ? "/config?lang=cs" : "/config?lang=en"); + gServer->send(303); +} + +// ─── POST /action ───────────────────────────────────────────────────────────── +static void handleAction() { + bool cz = gServer->hasArg("lang") ? (gServer->arg("lang") != "en") : true; + String act = gServer->arg("act"); + + if (act == "change_addr" && gServer->hasArg("newAddr")) { + uint8_t newAddr = (uint8_t)constrain(gServer->arg("newAddr").toInt(), 1, 247); + ModbusStatus st = gMb->writeRegister(gCfg->modbusAddr, REG_DEVICE_ADDR, newAddr); + Serial.printf("[ACT] Change addr %u -> %u : %s\n", + gCfg->modbusAddr, newAddr, gMb->statusStr(st)); + if (st == ModbusStatus::OK) { + gCfg->modbusAddr = newAddr; + gPrefs->begin("zts3000", false); + saveConfig(*gPrefs, *gCfg); + gPrefs->end(); + } + } + + sendLangCookie(cz); + gServer->sendHeader("Location", cz ? "/config?lang=cs" : "/config?lang=en"); + gServer->send(303); +} + +// ─── GET /api/data ──────────────────────────────────────────────────────────── +static void handleApiData() { + const SensorData& d = *gData; + String json = "{"; + json += "\"ts\":" + String(d.lastUpdateMs) + ","; + json += "\"err\":" + String(d.commError ? "true" : "false"); + + if (d.present.wind) { + json += ",\"windSpeed\":" + fmtFloat(d.windSpeed); + json += ",\"windLevel\":" + String(d.windLevel); + } + json += "}"; + + gServer->sendHeader("Access-Control-Allow-Origin", "*"); + gServer->send(200, "application/json", json); +} + +// ─── GET /api/status ────────────────────────────────────────────────────────── +static void handleApiStatus() { + const SensorPresence& p = gData->present; + String json = "{"; + json += "\"wind\":" + String(p.wind ? "true" : "false"); + json += "}"; + + gServer->sendHeader("Access-Control-Allow-Origin", "*"); + gServer->send(200, "application/json", json); +} + +// ─── GET /api/raw – Posledni TX/RX frame ───────────────────────────────────── +static String hexBuf(const uint8_t* buf, uint8_t len) { + String s; + s.reserve(len * 3); + for (uint8_t i = 0; i < len; i++) { + char h[4]; + snprintf(h, sizeof(h), "%02X ", buf[i]); + s += h; + } + if (s.length() > 0) s.remove(s.length() - 1); + return s; +} + +static void handleApiRaw() { + String json = "{"; + json += "\"tx\":\"" + hexBuf(gMb->lastTx, gMb->lastTxLen) + "\""; + json += ",\"rx\":\"" + hexBuf(gMb->lastRx, gMb->lastRxLen) + "\""; + json += ",\"s\":\"" + String(gMb->statusStr(gMb->lastResult)) + "\""; + json += "}"; + gServer->sendHeader("Access-Control-Allow-Origin", "*"); + gServer->send(200, "application/json", json); +} + +// ─── Inicializace ───────────────────────────────────────────────────────────── +void webServerBegin(SensorData& data, SensorConfig& cfg, + Preferences& prefs, ModbusClient& mb) { + gData = &data; + gCfg = &cfg; + gPrefs = &prefs; + gMb = &mb; + + gServer = new WebServer(80); + static const char* hdrs[] = {"Cookie"}; + gServer->collectHeaders(hdrs, 1); + + gServer->on("/", HTTP_GET, handleRoot); + gServer->on("/detect", HTTP_POST, handleDetect); + gServer->on("/config", HTTP_GET, handleConfig); + gServer->on("/config", HTTP_POST, handleConfigPost); + gServer->on("/action", HTTP_POST, handleAction); + gServer->on("/api/data", HTTP_GET, handleApiData); + gServer->on("/api/status", HTTP_GET, handleApiStatus); + gServer->on("/api/raw", HTTP_GET, handleApiRaw); + gServer->onNotFound([]() { + gServer->send(404, "text/plain", "Not found"); + }); + + gServer->begin(); + Serial.println(F("[WEB] HTTP server spusten na portu 80")); +} + +void webServerHandle() { + if (gServer) gServer->handleClient(); +} diff --git a/SW/ESPlan_ZTS3000_WindSensor/ws_server.h b/SW/ESPlan_ZTS3000_WindSensor/ws_server.h new file mode 100755 index 0000000..11afbdb --- /dev/null +++ b/SW/ESPlan_ZTS3000_WindSensor/ws_server.h @@ -0,0 +1,29 @@ +#ifndef WS_SERVER_H +#define WS_SERVER_H +/* + * ws_server.h – HTTP webserver + * + * Stranky: + * GET / Stav snimace + ziva data (wind speed + Beaufort) + * GET /config Konfigurace snimace + * POST /config Ulozeni konfigurace + * POST /detect Re-detekce snimace + * POST /action Akce: change_addr + * GET /api/data JSON s aktualnimi daty + * GET /api/status JSON se stavem detekce + * GET /api/raw Posledni RS485 TX/RX + */ + +#include +#include +#include "cfg.h" +#include "mb_client.h" +#include "snr.h" +#include "i18n_str.h" + +void webServerBegin(SensorData& data, SensorConfig& cfg, + Preferences& prefs, ModbusClient& mb); + +void webServerHandle(); + +#endif // WS_SERVER_H diff --git a/SW/esplan_v2_renke_weather_station/Config.h b/SW/esplan_v2_renke_weather_station/Config.h deleted file mode 100755 index 361fb3c..0000000 --- a/SW/esplan_v2_renke_weather_station/Config.h +++ /dev/null @@ -1,43 +0,0 @@ -#pragma once - -#include -#include - -#define ETH_PHY_TYPE ETH_PHY_LAN8720 -#define ETH_PHY_ADDR 0 -#define ETH_PHY_MDC 23 -#define ETH_PHY_MDIO 18 -#define ETH_PHY_POWER -1 -#define ETH_CLK_MODE ETH_CLOCK_GPIO17_OUT - -static constexpr int PIN_RS485_RX = 36; -static constexpr int PIN_RS485_TX = 4; - -static constexpr uint8_t DEFAULT_MODBUS_ADDR = 0x01; -static constexpr uint32_t DEFAULT_MODBUS_BAUD = 4800; - -static constexpr uint16_t REG_DATA_START = 500; -static constexpr uint16_t REG_DATA_COUNT = 16; - -static constexpr uint16_t REG_CFG_WIND_DIR_OFFSET = 0x6000; -static constexpr uint16_t REG_CFG_RAIN_SENSITIVITY = 0x6003; - -// Wind zero command prefix without CRC. CRC is appended automatically in software. -// User-requested raw frame prefix: 01 AB 01 06 01 02 00 5A A9 CD -static constexpr const char* CMD_WIND_ZERO = "01AB01060102005AA9CD"; - -static constexpr uint16_t REG_CFG_RAIN_ZERO = 0x0104; -static constexpr uint16_t CFG_RAIN_ZERO = 0x005A; - -static constexpr unsigned long SENSOR_POLL_MS = 2000; -static constexpr unsigned long DETECT_RECHECK_MS = 30000; -static constexpr unsigned long ETH_LOG_MS = 10000; - -static constexpr size_t HTTP_RESERVE_LARGE = 32000; -static constexpr size_t HTTP_RESERVE_MED = 12000; - -enum class AirQualityMode : uint8_t { - Auto = 0, - PM = 1, - CO2 = 2 -}; diff --git a/SW/esplan_v2_renke_weather_station/Lang.h b/SW/esplan_v2_renke_weather_station/Lang.h deleted file mode 100755 index 5b7f1b6..0000000 --- a/SW/esplan_v2_renke_weather_station/Lang.h +++ /dev/null @@ -1,262 +0,0 @@ -#pragma once - -#include - -struct Texts { - const char* appTitle; - const char* tabMain; - const char* tabConfig; - const char* language; - const char* sensorStatus; - const char* values; - const char* present; - const char* absent; - const char* pollAge; - const char* seconds; - const char* windSpeed; - const char* beaufort; - const char* windDir8; - const char* windDirDeg; - const char* humidity; - const char* temperature; - const char* noise; - const char* pressure; - const char* lightHigh; - const char* lightLow; - const char* lightLux100; - const char* rain; - const char* compass; - const char* solarRadiation; - const char* sensorWind; - const char* sensorHumidityTemp; - const char* sensorNoise; - const char* sensorAir; - const char* sensorPressure; - const char* sensorLight; - const char* sensorRain; - const char* sensorCompass; - const char* sensorSolar; - const char* configTitle; - const char* address; - const char* baudrate; - const char* save; - const char* writeOk; - const char* writeFail; - const char* execute; - const char* windOffset; - const char* rainSensitivity; - const char* commandWindZero; - const char* commandRainZero; - const char* tableItem; - const char* tableSetting; - const char* tableNote; - const char* modbusLog; - const char* modbusFrames; - const char* modbusLastStatus; - const char* modbusAction; - const char* modbusDuration; - const char* modbusCounters; - const char* modbusError; - const char* modbusTx; - const char* modbusRx; - const char* noteAddress; - const char* noteBaudrate; - const char* noteWindOffset; - const char* noteRainSensitivity; - const char* noteWindZero; - const char* noteRainZero; - const char* rawRegisters; - const char* manualCommand; - const char* manualAddress; - const char* manualRegisterHex; - const char* manualValueHex; - const char* manualSend; - const char* manualPreview; - const char* manualNote; - const char* rawHexTitle; - const char* rawHexInput; - const char* rawHexSend; - const char* rawHexNote; - const char* noResult; - const char* statusOkShort; - const char* statusErrorShort; -}; - -enum class UiLang : uint8_t { - CZ = 0, - EN = 1 -}; - -inline UiLang parseLang(const String& v) { - String s = v; - s.toLowerCase(); - return (s == "en") ? UiLang::EN : UiLang::CZ; -} - -inline const char* langCode(UiLang lang) { - return (lang == UiLang::EN) ? "en" : "cz"; -} - -inline const Texts& T(UiLang lang) { - static const Texts cz = { - "ESPlan Meteostanice RS-FSXCS-N01", - "Hlavní stránka", - "Konfigurace", - "Jazyk", - "Stav čidel", - "Aktuální hodnoty", - "Přítomno", - "Nepřítomno", - "Stáří dat", - "s", - "Rychlost větru", - "Síla větru", - "Směr větru 0-7", - "Směr větru", - "Vlhkost", - "Teplota", - "Hluk", - "Tlak", - "Osvětlení high", - "Osvětlení low", - "Osvětlení", - "Déšť", - "Kompas", - "Sluneční radiace", - "Vítr", - "Vlhkost / Teplota", - "Hluk", - "Kvalita vzduchu", - "Tlak", - "Světlo", - "Déšť", - "Kompas", - "Solární", - "Konfigurace snímače", - "Adresa (ESP32)", - "Baudrate (ESP32)", - "Uložit", - "Zápis OK", - "Zápis CHYBA", - "Provést", - "Offset směru větru", - "Citlivost optického deště", - "Nulování větru", - "Nulování deště", - "Položka", - "Nastavení", - "Poznámka", - "Modbus log", - "Modbus rámce", - "Stav", - "Akce", - "Doba", - "Počítadla", - "Chyba", - "TX", - "RX", - "Toto nastavení mění pouze komunikaci ESP32, ne konfiguraci samotného snímače.", - "Toto nastavení mění pouze komunikaci ESP32, ne konfiguraci samotného snímače.", - "Registr 0x6000, HEX hodnota.", - "Registr 0x6003, HEX hodnota.", - "Zapíše kalibrační příkaz do snímače.", - "Zapíše nulovací příkaz do snímače.", - "Surové registry", - "Ruční Modbus příkaz", - "Adresa", - "Registr (HEX)", - "Hodnota (HEX)", - "Odeslat", - "Rámec s CRC", - "Odešle se funkce 0x06. Adresa je decimálně, registr a hodnota jsou HEX. CRC se počítá automaticky.", - "Celý HEX příkaz", - "HEX rámec", - "Odeslat HEX", - "Zadej celý rámec v HEX bez CRC(dopočítá se automaticky). Neověřuje se správnost rámce, odesílá se tak jak je zadán.", - "Zatím bez výsledku", - "OK", - "CHYBA" - }; - - static const Texts en = { - "ESPlan Weather Station RS-FSXCS-N01", - "Main page", - "Configuration", - "Language", - "Sensor status", - "Current values", - "Present", - "Absent", - "Data age", - "s", - "Wind speed", - "Beaufort", - "Wind direction 0-7", - "Wind direction", - "Humidity", - "Temperature", - "Noise", - "Pressure", - "Light high", - "Light low", - "Light", - "Rain", - "Compass", - "Solar radiation", - "Wind", - "Humidity / Temperature", - "Noise", - "Air quality", - "Pressure", - "Light", - "Rain", - "Compass", - "Solar", - "Sensor configuration", - "Address (ESP32)", - "Baudrate (ESP32)", - "Save", - "Write OK", - "Write FAIL", - "Execute", - "Wind direction offset", - "Optical rain sensitivity", - "Zero wind", - "Zero rain", - "Item", - "Setting", - "Note", - "Modbus log", - "Modbus frames", - "Status", - "Action", - "Duration", - "Counters", - "Error", - "TX", - "RX", - "This setting affects only ESP32 communication, not the sensor configuration itself.", - "This setting affects only ESP32 communication, not the sensor configuration itself.", - "Register 0x6000, HEX value.", - "Register 0x6003, HEX value.", - "Writes the calibration command to the sensor.", - "Writes the reset command to the sensor.", - "Raw registers", - "Manual Modbus command", - "Address", - "Register (HEX)", - "Value (HEX)", - "Send", - "Frame with CRC", - "Function 0x06 will be sent. Address is decimal, register and value are HEX. CRC is calculated automatically.", - "Full HEX command", - "HEX frame", - "Send HEX", - "Enter the entire frame in HEX without CRC (it will be calculated automatically). The frame is not verified for correctness, it is sent as entered.", - "No result yet", - "OK", - "ERROR" - }; - - return (lang == UiLang::EN) ? en : cz; -} diff --git a/SW/esplan_v2_renke_weather_station/WeatherStation.cpp b/SW/esplan_v2_renke_weather_station/WeatherStation.cpp deleted file mode 100755 index 3b9f5e6..0000000 --- a/SW/esplan_v2_renke_weather_station/WeatherStation.cpp +++ /dev/null @@ -1,531 +0,0 @@ -#include "WeatherStation.h" -#include -#include - -static bool parseHexStringLocal(const String& input, uint8_t* out, size_t& outLen, size_t maxLen) { - String s = input; - s.trim(); - s.replace(" ", ""); - s.replace("\t", ""); - s.replace("\r", ""); - s.replace("\n", ""); - - if (s.length() == 0 || (s.length() % 2) != 0) { - outLen = 0; - return false; - } - - const size_t bytes = s.length() / 2; - if (bytes > maxLen) { - outLen = 0; - return false; - } - - outLen = 0; - for (size_t i = 0; i < bytes; i++) { - const String tok = s.substring(i * 2, i * 2 + 2); - char* endptr = nullptr; - const unsigned long v = strtoul(tok.c_str(), &endptr, 16); - if (*endptr != '\0' || v > 0xFFUL) { - outLen = 0; - return false; - } - out[outLen++] = static_cast(v); - } - - return true; -} - -WeatherStation::WeatherStation(HardwareSerial& serial) - : _serial(serial) {} - -void WeatherStation::begin() { - _serial.begin(_config.baudrate, SERIAL_8N1, PIN_RS485_RX, PIN_RS485_TX); -} - -bool WeatherStation::setBaudrate(uint32_t baud) { - _config.baudrate = baud; - _serial.updateBaudRate(baud); - return true; -} - -uint16_t WeatherStation::crc16(const uint8_t* data, size_t len) const { - uint16_t crc = 0xFFFF; - for (size_t pos = 0; pos < len; pos++) { - crc ^= static_cast(data[pos]); - for (uint8_t i = 0; i < 8; i++) { - if (crc & 0x0001) { - crc >>= 1; - crc ^= 0xA001; - } else { - crc >>= 1; - } - } - } - return crc; -} - -int16_t WeatherStation::toSigned(uint16_t v) { - return static_cast(v); -} - -String WeatherStation::toHex(const uint8_t* data, size_t len) { - static const char* HEX_CHARS = "0123456789ABCDEF"; - String out; - out.reserve(len * 3); - for (size_t i = 0; i < len; i++) { - if (i) out += ' '; - out += HEX_CHARS[(data[i] >> 4) & 0x0F]; - out += HEX_CHARS[data[i] & 0x0F]; - } - return out; -} - -const __FlashStringHelper* WeatherStation::exceptionText(uint8_t code) { - switch (code) { - case 0x01: return F("illegal function"); - case 0x02: return F("illegal data address"); - case 0x03: return F("illegal data value"); - case 0x04: return F("slave device failure"); - case 0x05: return F("acknowledge"); - case 0x06: return F("slave device busy"); - case 0x08: return F("memory parity error"); - case 0x0A: return F("gateway path unavailable"); - case 0x0B: return F("gateway target failed"); - default: return F("modbus exception"); - } -} - -void WeatherStation::setSimpleError(const String& error) { - _commLog.lastOk = false; - _commLog.lastDurationMs = 0; - _commLog.lastAction = String(); - _commLog.lastRequestHex = String(); - _commLog.lastResponseHex = String(); - _commLog.lastError = error; - _commLog.errorCount++; -} - -void WeatherStation::setCommResult( - bool ok, - uint32_t durationMs, - const String& action, - const uint8_t* req, - size_t reqLen, - const uint8_t* resp, - size_t respLen, - const String& error) { - _commLog.lastOk = ok; - _commLog.lastDurationMs = durationMs; - _commLog.lastAction = action; - _commLog.lastRequestHex = req ? toHex(req, reqLen) : String(); - _commLog.lastResponseHex = resp ? toHex(resp, respLen) : String(); - _commLog.lastError = error; - if (ok) { - _commLog.successCount++; - } else { - _commLog.errorCount++; - } -} - -bool WeatherStation::transact( - const uint8_t* request, - size_t reqLen, - uint8_t* response, - size_t responseCap, - uint32_t timeoutMs, - const String& action, - size_t* actualLen) { - while (_serial.available()) { - _serial.read(); - } - - const uint32_t t0 = millis(); - _serial.write(request, reqLen); - _serial.flush(); - - size_t got = 0; - uint32_t lastByteAt = 0; - bool seenAnyByte = false; - - while ((millis() - t0) < timeoutMs && got < responseCap) { - bool gotByteNow = false; - - while (_serial.available() && got < responseCap) { - response[got++] = static_cast(_serial.read()); - lastByteAt = millis(); - seenAnyByte = true; - gotByteNow = true; - } - - if (seenAnyByte && !gotByteNow && (millis() - lastByteAt) >= 20) { - break; - } - delay(1); - } - - if (actualLen) { - *actualLen = got; - } - - if (!seenAnyByte) { - setCommResult(false, millis() - t0, action, request, reqLen, response, 0, F("timeout")); - return false; - } - - setCommResult(true, millis() - t0, action, request, reqLen, response, got, String()); - return true; -} - -bool WeatherStation::sendRawFrame(const uint8_t* request, size_t reqLen, uint8_t* response, size_t responseCapacity, uint32_t timeoutMs, const String& action) { - if (request == nullptr || reqLen == 0) { - setSimpleError(F("invalid args")); - return false; - } - - uint8_t frame[260] = {0}; - if (reqLen > sizeof(frame)) { - setSimpleError(F("raw frame too long")); - return false; - } - - memcpy(frame, request, reqLen); - size_t frameLen = reqLen; - - bool hasValidCrc = false; - if (frameLen >= 4) { - const uint16_t crcProvided = static_cast(frame[frameLen - 2]) | (static_cast(frame[frameLen - 1]) << 8); - const uint16_t crcCalculated = crc16(frame, frameLen - 2); - hasValidCrc = (crcProvided == crcCalculated); - } - - if (!hasValidCrc) { - if ((frameLen + 2) > sizeof(frame)) { - setSimpleError(F("raw frame too long")); - return false; - } - const uint16_t crc = crc16(frame, frameLen); - frame[frameLen++] = static_cast(crc & 0xFF); - frame[frameLen++] = static_cast((crc >> 8) & 0xFF); - } - - size_t actualLen = 0; - const uint32_t effectiveTimeoutMs = timeoutMs < 2000 ? 2000 : timeoutMs; - return transact(frame, frameLen, response, responseCapacity, effectiveTimeoutMs, action, &actualLen); -} - -bool WeatherStation::sendRawFrame(const uint8_t* request, size_t reqLen, uint32_t timeoutMs, const String& action) { - uint8_t resp[128] = {0}; - return sendRawFrame(request, reqLen, resp, sizeof(resp), timeoutMs, action); -} - -bool WeatherStation::readHoldingRegisters(uint16_t startReg, uint16_t count, uint16_t* out) { - if (count == 0 || out == nullptr) { - setSimpleError(F("invalid args")); - return false; - } - - uint8_t req[8]; - req[0] = _config.address; - req[1] = 0x03; - req[2] = static_cast((startReg >> 8) & 0xFF); - req[3] = static_cast(startReg & 0xFF); - req[4] = static_cast((count >> 8) & 0xFF); - req[5] = static_cast(count & 0xFF); - const uint16_t crc = crc16(req, 6); - req[6] = static_cast(crc & 0xFF); - req[7] = static_cast((crc >> 8) & 0xFF); - - uint8_t resp[64] = {0}; - size_t respLen = 0; - - char regHex[5]; - snprintf(regHex, sizeof(regHex), "%04X", startReg); - String action = F("read 0x"); - action += regHex; - action += F(" + "); - action += String(count); - - if (!transact(req, sizeof(req), resp, sizeof(resp), 2000, action, &respLen)) { - return false; - } - - if (respLen < 5) { - setCommResult(false, _commLog.lastDurationMs, action, req, sizeof(req), resp, respLen, F("short response")); - return false; - } - - if (resp[0] != _config.address) { - setCommResult(false, _commLog.lastDurationMs, action, req, sizeof(req), resp, respLen, F("wrong slave address")); - return false; - } - - const uint16_t crcResp = static_cast(resp[respLen - 2]) | (static_cast(resp[respLen - 1]) << 8); - const uint16_t crcCalc = crc16(resp, respLen - 2); - if (crcResp != crcCalc) { - setCommResult(false, _commLog.lastDurationMs, action, req, sizeof(req), resp, respLen, F("crc mismatch")); - return false; - } - - if (resp[1] == static_cast(0x03 | 0x80)) { - String err = F("modbus exception: "); - err += String(exceptionText(resp[2])); - setCommResult(false, _commLog.lastDurationMs, action, req, sizeof(req), resp, respLen, err); - return false; - } - - if (resp[1] != 0x03) { - setCommResult(false, _commLog.lastDurationMs, action, req, sizeof(req), resp, respLen, F("bad function")); - return false; - } - - const size_t expectedLen = 5 + static_cast(count) * 2; - if (respLen != expectedLen) { - setCommResult(false, _commLog.lastDurationMs, action, req, sizeof(req), resp, respLen, F("bad length")); - return false; - } - - if (resp[2] != count * 2) { - setCommResult(false, _commLog.lastDurationMs, action, req, sizeof(req), resp, respLen, F("bad byte count")); - return false; - } - - for (uint16_t i = 0; i < count; i++) { - out[i] = (static_cast(resp[3 + i * 2]) << 8) | static_cast(resp[4 + i * 2]); - } - - _commLog.lastOk = true; - _commLog.lastError = String(); - return true; -} - -bool WeatherStation::writeRegister(uint16_t reg, uint16_t value) { - uint8_t req[8]; - req[0] = _config.address; - req[1] = 0x06; - req[2] = static_cast((reg >> 8) & 0xFF); - req[3] = static_cast(reg & 0xFF); - req[4] = static_cast((value >> 8) & 0xFF); - req[5] = static_cast(value & 0xFF); - const uint16_t crc = crc16(req, 6); - req[6] = static_cast(crc & 0xFF); - req[7] = static_cast((crc >> 8) & 0xFF); - - uint8_t resp[16] = {0}; - size_t respLen = 0; - - char regHex[5]; - char valHex[5]; - snprintf(regHex, sizeof(regHex), "%04X", reg); - snprintf(valHex, sizeof(valHex), "%04X", value); - String action = F("write 0x"); - action += regHex; - action += F(" = 0x"); - action += valHex; - - if (!transact(req, sizeof(req), resp, sizeof(resp), 2000, action, &respLen)) { - return false; - } - - if (respLen < 5) { - setCommResult(false, _commLog.lastDurationMs, action, req, sizeof(req), resp, respLen, F("short response")); - return false; - } - - if (resp[0] != _config.address) { - setCommResult(false, _commLog.lastDurationMs, action, req, sizeof(req), resp, respLen, F("wrong slave address")); - return false; - } - - const uint16_t crcResp = static_cast(resp[respLen - 2]) | (static_cast(resp[respLen - 1]) << 8); - const uint16_t crcCalc = crc16(resp, respLen - 2); - if (crcResp != crcCalc) { - setCommResult(false, _commLog.lastDurationMs, action, req, sizeof(req), resp, respLen, F("crc mismatch")); - return false; - } - - if (resp[1] == static_cast(0x06 | 0x80)) { - String err = F("modbus exception: "); - err += String(exceptionText(resp[2])); - setCommResult(false, _commLog.lastDurationMs, action, req, sizeof(req), resp, respLen, err); - return false; - } - - if (resp[1] != 0x06) { - setCommResult(false, _commLog.lastDurationMs, action, req, sizeof(req), resp, respLen, F("bad function")); - return false; - } - - if (respLen != 8) { - setCommResult(false, _commLog.lastDurationMs, action, req, sizeof(req), resp, respLen, F("bad length")); - return false; - } - - if (memcmp(req, resp, 6) != 0) { - setCommResult(false, _commLog.lastDurationMs, action, req, sizeof(req), resp, respLen, F("write echo mismatch")); - return false; - } - - _commLog.lastOk = true; - _commLog.lastError = String(); - return true; -} - -bool WeatherStation::readAll() { - uint16_t regs[REG_DATA_COUNT] = {0}; - if (!readHoldingRegisters(REG_DATA_START, REG_DATA_COUNT, regs)) { - _values.registersValid = false; - return false; - } - - for (uint8_t i = 0; i < REG_DATA_COUNT; i++) { - _values.raw[i] = regs[i]; - } - - _values.registersValid = true; - _values.lastReadMs = millis(); - decodeRegisters(); - detectPresence(); - return true; -} - -bool WeatherStation::readSensorConfig() { - uint16_t regs[4] = {0}; - if (!readHoldingRegisters(REG_CFG_WIND_DIR_OFFSET, 4, regs)) { - _config.sensorConfigReadOk = false; - return false; - } - - _config.windOffset = regs[0]; - _config.rainSensitivity = regs[3]; - _config.sensorConfigReadOk = true; - return true; -} - -void WeatherStation::decodeRegisters() { - _values.windSpeed = _values.raw[0] / 10.0f; - _values.windForce = static_cast(_values.raw[1]); - _values.windDir8 = static_cast(_values.raw[2]); - _values.windDirDeg = static_cast(_values.raw[3]); - _values.humidity = _values.raw[4] / 10.0f; - _values.temperature = toSigned(_values.raw[5]) / 10.0f; - _values.noise = _values.raw[6] / 10.0f; - _values.air1 = static_cast(_values.raw[7]); - _values.air2 = static_cast(_values.raw[8]); - _values.pressureKpa = _values.raw[9] / 10.0f; - _values.lightHigh = _values.raw[10]; - _values.lightLow = _values.raw[11]; - _values.lightLux = _values.raw[12]; - _values.rain = _values.raw[13] / 10.0f; - _values.compassDeg = _values.raw[14] / 100.0f; - _values.solarRadiation = static_cast(_values.raw[15]); -} - -void WeatherStation::detectPresence() { - _presence.wind = - (_values.windSpeed >= 0.0f) && - (_values.windDirDeg >= 0) && - (_values.windDirDeg <= 359); - - _presence.humidityTemp = - isfinite(_values.humidity) && - isfinite(_values.temperature) && - (_values.humidity >= 0.0f && _values.humidity <= 100.0f) && - (_values.temperature > -50.0f && _values.temperature < 90.0f); - - if (isfinite(_values.noise) && - _values.noise >= 29.5f && - _values.noise <= 30.5f) { - _presence.noise = false; - _values.noise = NAN; - } else { - _presence.noise = isfinite(_values.noise) && (_values.noise > 0.0f) && (_values.noise <= 150.0f); - } - - _presence.air = (_values.air1 > 0 || _values.air2 > 0); - if (!_presence.air) { - _values.air1 = -1; - _values.air2 = -1; - } - - _presence.pressure = isfinite(_values.pressureKpa) && (_values.pressureKpa > 10.0f); - if (!_presence.pressure) _values.pressureKpa = NAN; - - _presence.light = (_values.lightLux > 0) || (_values.lightHigh > 0) || (_values.lightLow > 0); - if (!_presence.light) { - _values.lightLux = 0; - _values.lightHigh = 0; - _values.lightLow = 0; - } - - _presence.rain = isfinite(_values.rain) && (_values.rain >= 0.0f); - if (!_presence.rain) _values.rain = NAN; - - _presence.compass = isfinite(_values.compassDeg) && (_values.compassDeg > 0.01f); - if (!_presence.compass) _values.compassDeg = NAN; - - _presence.solar = (_values.solarRadiation > 0); - if (!_presence.solar) _values.solarRadiation = -1; -} - -bool WeatherStation::commandZeroWind() { - uint8_t req[64]; - size_t reqLen = 0; - - if (!parseHexStringLocal(String(CMD_WIND_ZERO), req, reqLen, sizeof(req))) { - setSimpleError(F("bad wind zero prefix")); - return false; - } - - if (reqLen + 2 > sizeof(req)) { - setSimpleError(F("wind zero frame too long")); - return false; - } - - const uint16_t crc = crc16(req, reqLen); - req[reqLen++] = static_cast(crc & 0xFF); - req[reqLen++] = static_cast((crc >> 8) & 0xFF); - - uint8_t resp[32] = {0}; - size_t respLen = 0; - - if (!transact(req, reqLen, resp, sizeof(resp), 2000, F("wind zero raw"), &respLen)) { - return false; - } - - if (respLen == 0) { - setCommResult(false, _commLog.lastDurationMs, F("wind zero raw"), req, reqLen, resp, respLen, F("no response")); - return false; - } - - _commLog.lastOk = true; - _commLog.lastError = String(); - return true; -} - -bool WeatherStation::commandZeroRain() { - return writeRegister(REG_CFG_RAIN_ZERO, CFG_RAIN_ZERO); -} - -void WeatherStation::pushConfigLog() { - _lastConfigLog = _commLog; - _hasConfigLog = true; -} - -const CommLog& WeatherStation::configLogAt(size_t reverseIndex) const { - static const CommLog empty; - if (!_hasConfigLog || reverseIndex != 0) { - return empty; - } - return _lastConfigLog; -} - -String WeatherStation::airLabel1() const { - if (_config.airMode == AirQualityMode::CO2) return F("CO2"); - return F("PM2.5"); -} - -String WeatherStation::airLabel2() const { - if (_config.airMode == AirQualityMode::CO2) return F("CO2"); - return F("PM10"); -} \ No newline at end of file diff --git a/SW/esplan_v2_renke_weather_station/WeatherStation.h b/SW/esplan_v2_renke_weather_station/WeatherStation.h deleted file mode 100755 index 9fbbf7d..0000000 --- a/SW/esplan_v2_renke_weather_station/WeatherStation.h +++ /dev/null @@ -1,112 +0,0 @@ -#pragma once - -#include -#include -#include "Config.h" - -struct VariantPresence { - bool wind = false; - bool humidityTemp = false; - bool noise = false; - bool air = false; - bool pressure = false; - bool light = false; - bool rain = false; - bool compass = false; - bool solar = false; -}; - -struct WeatherValues { - uint16_t raw[REG_DATA_COUNT] = {0}; - bool registersValid = false; - uint32_t lastReadMs = 0; - - float windSpeed = NAN; - int windForce = -1; - int windDir8 = -1; - int windDirDeg = -1; - float humidity = NAN; - float temperature = NAN; - float noise = NAN; - int air1 = -1; - int air2 = -1; - float pressureKpa = NAN; - uint16_t lightHigh = 0; - uint16_t lightLow = 0; - uint32_t lightLux = 0; - float rain = NAN; - float compassDeg = NAN; - int solarRadiation = -1; -}; - -struct RuntimeConfig { - uint8_t address = DEFAULT_MODBUS_ADDR; - uint32_t baudrate = DEFAULT_MODBUS_BAUD; - AirQualityMode airMode = AirQualityMode::Auto; - uint16_t windOffset = 0; - uint16_t rainSensitivity = 0x0011; - bool sensorConfigReadOk = false; -}; - -struct CommLog { - bool lastOk = false; - uint32_t lastDurationMs = 0; - uint32_t successCount = 0; - uint32_t errorCount = 0; - String lastAction; - String lastRequestHex; - String lastResponseHex; - String lastError; -}; - -class WeatherStation { -public: - explicit WeatherStation(HardwareSerial& serial); - - void begin(); - bool setBaudrate(uint32_t baud); - - bool readAll(); - bool readSensorConfig(); - bool writeRegister(uint16_t reg, uint16_t value); - - bool commandZeroWind(); - bool commandZeroRain(); - - bool sendRawFrame(const uint8_t* request, size_t reqLen, uint8_t* response, size_t responseCapacity, uint32_t timeoutMs, const String& action); - bool sendRawFrame(const uint8_t* request, size_t reqLen, uint32_t timeoutMs, const String& action); - - void pushConfigLog(); - size_t configLogCount() const { return _hasConfigLog ? 1 : 0; } - const CommLog& configLogAt(size_t reverseIndex) const; - - const WeatherValues& values() const { return _values; } - const VariantPresence& presence() const { return _presence; } - RuntimeConfig& config() { return _config; } - const RuntimeConfig& config() const { return _config; } - const CommLog& commLog() const { return _commLog; } - - String airLabel1() const; - String airLabel2() const; - -private: - HardwareSerial& _serial; - WeatherValues _values; - VariantPresence _presence; - RuntimeConfig _config; - CommLog _commLog; - - CommLog _lastConfigLog; - bool _hasConfigLog = false; - - uint16_t crc16(const uint8_t* data, size_t len) const; - bool transact(const uint8_t* request, size_t reqLen, uint8_t* response, size_t responseCap, uint32_t timeoutMs, const String& action, size_t* actualLen = nullptr); - bool readHoldingRegisters(uint16_t startReg, uint16_t count, uint16_t* out); - void decodeRegisters(); - void detectPresence(); - void setCommResult(bool ok, uint32_t durationMs, const String& action, const uint8_t* req, size_t reqLen, const uint8_t* resp, size_t respLen, const String& error); - void setSimpleError(const String& error); - static int16_t toSigned(uint16_t v); - static String toHex(const uint8_t* data, size_t len); - static const __FlashStringHelper* exceptionText(uint8_t code); -}; \ No newline at end of file diff --git a/SW/esplan_v2_renke_weather_station/WebUi.cpp b/SW/esplan_v2_renke_weather_station/WebUi.cpp deleted file mode 100755 index 4a32895..0000000 --- a/SW/esplan_v2_renke_weather_station/WebUi.cpp +++ /dev/null @@ -1,645 +0,0 @@ -#include "WebUi.h" -#include "logo_web.h" - -String htmlEscape(const String& in) { - String out; - out.reserve(in.length() + 16); - for (size_t i = 0; i < in.length(); i++) { - switch (in[i]) { - case '&': out += F("&"); break; - case '<': out += F("<"); break; - case '>': out += F(">"); break; - case '"': out += F("""); break; - case '\'': out += F("'"); break; - default: out += in[i]; break; - } - } - return out; -} - -String fmtFloat(float v, uint8_t decimals) { - if (isnan(v) || !isfinite(v)) return F("-"); - return String(static_cast(v), static_cast(decimals)); -} - -static String jsonFloat(float v, uint8_t decimals = 1) { - if (isnan(v) || !isfinite(v)) return F("null"); - return String(static_cast(v), static_cast(decimals)); -} - -String jsonString(const String& in) { - String out; - out.reserve(in.length() + 8); - - for (size_t i = 0; i < in.length(); i++) { - char c = in[i]; - switch (c) { - case '\"': out += F("\\\""); break; - case '\\': out += F("\\\\"); break; - case '\n': out += F("\\n"); break; - case '\r': out += F("\\r"); break; - case '\t': out += F("\\t"); break; - case '\b': out += F("\\b"); break; - case '\f': out += F("\\f"); break; - default: - if ((uint8_t)c < 32) { - out += '?'; - } else { - out += c; - } - } - } - return out; -} - -static String hex16(uint16_t value) { - char buf[5]; - snprintf(buf, sizeof(buf), "%04X", value); - return String(buf); -} - -static String statusBadge(bool present, const Texts& t) { - String s; - s.reserve(64); - s += F("") : F("bad'>"); - s += present ? t.present : t.absent; - s += F(""); - return s; -} - -String buildStyle(const Texts&, UiLang) { - return F( - ""); -} - -static void appendTop(String& html, UiLang lang, const Texts& t, const char* active) { - html += F("

"); - html += t.appTitle; - html += F("

"); - - html += F("
"); - html += F("
"); - - html += F(""); -} - -static void appendRow(String& html, const String& id, const String& label, const String& value) { - html += F(""); - html += label; - html += F(""); - html += value; - html += F(""); -} - -static String buildConfigHistoryBox(const Texts& t, const WeatherStation& ws) { - String html; - html.reserve(1800); - - html += F("

"); - html += t.modbusLog; - html += F("

"); - - if (ws.configLogCount() == 0) { - html += F("
"); - html += t.noResult; - html += F("
"); - return html; - } - - const CommLog& c = ws.configLogAt(0); - - html += F(""); - appendRow(html, F("cfg-log-state"), t.modbusLastStatus, c.lastOk ? String(F("")) + t.statusOkShort + F("") : String(F("")) + t.statusErrorShort + F("")); - appendRow(html, F("cfg-log-action"), t.modbusAction, htmlEscape(c.lastAction)); - appendRow(html, F("cfg-log-duration"), t.modbusDuration, String(c.lastDurationMs) + F(" ms")); - appendRow(html, F("cfg-log-error"), t.modbusError, c.lastError.length() ? htmlEscape(c.lastError) : F("-")); - html += F("
"); - - html += F("
"); - html += t.modbusTx; - html += F("
"); - html += htmlEscape(c.lastRequestHex); - html += F("
"); - - html += F("
"); - html += t.modbusRx; - html += F("
"); - html += htmlEscape(c.lastResponseHex); - html += F("
"); - - html += F(""); - return html; -} - -static void appendMainCards(String& html, const Texts& t, const WeatherStation& ws, bool ethUp, const String& ip, const String& mac) { - (void)ethUp; - (void)ip; - (void)mac; - - const auto& p = ws.presence(); - const auto& v = ws.values(); - const auto& c = ws.commLog(); - - html += F("
"); - - html += F("

"); - html += t.sensorStatus; - html += F("

"); - appendRow(html, F("st-wind"), t.sensorWind, statusBadge(p.wind, t)); - appendRow(html, F("st-th"), t.sensorHumidityTemp, statusBadge(p.humidityTemp, t)); - appendRow(html, F("st-noise"), t.sensorNoise, statusBadge(p.noise, t)); - appendRow(html, F("st-air"), t.sensorAir, statusBadge(p.air, t)); - appendRow(html, F("st-pressure"), t.sensorPressure, statusBadge(p.pressure, t)); - appendRow(html, F("st-light"), t.sensorLight, statusBadge(p.light, t)); - appendRow(html, F("st-rain"), t.sensorRain, statusBadge(p.rain, t)); - appendRow(html, F("st-compass"), t.sensorCompass, statusBadge(p.compass, t)); - appendRow(html, F("st-solar"), t.sensorSolar, statusBadge(p.solar, t)); - html += F("
"); - - html += F("

"); - html += t.values; - html += F("

"); - appendRow(html, F("v-wind-short"), t.windSpeed, fmtFloat(v.windSpeed, 1)); - appendRow(html, F("v-beaufort-short"), t.beaufort, (v.windForce >= 0 ? String(v.windForce) : F("-"))); - appendRow(html, F("v-dir8-short"), t.windDir8, (v.windDir8 >= 0 ? String(v.windDir8) : F("-"))); - appendRow(html, F("v-dirdeg-short"), t.windDirDeg, (v.windDirDeg >= 0 ? String(v.windDirDeg) : F("-"))); - appendRow(html, F("v-hum-short"), t.humidity, fmtFloat(v.humidity, 1)); - appendRow(html, F("v-temp-short"), t.temperature, fmtFloat(v.temperature, 1)); - appendRow(html, F("v-noise-short"), t.noise, fmtFloat(v.noise, 1)); - appendRow(html, F("v-air1-short"), ws.airLabel1(), (v.air1 >= 0 ? String(v.air1) : F("-"))); - appendRow(html, F("v-air2-short"), ws.airLabel2(), (v.air2 >= 0 ? String(v.air2) : F("-"))); - appendRow(html, F("v-pressure-short"), t.pressure, fmtFloat(v.pressureKpa, 1)); - appendRow(html, F("v-luxh-short"), t.lightHigh, String(v.lightHigh)); - appendRow(html, F("v-luxl-short"), t.lightLow, String(v.lightLow)); - appendRow(html, F("v-lux-short"), t.lightLux100, String(v.lightLux)); - appendRow(html, F("v-rain-short"), t.rain, fmtFloat(v.rain, 1)); - appendRow(html, F("v-compass-short"), t.compass, fmtFloat(v.compassDeg, 2)); - appendRow(html, F("v-solar-short"), t.solarRadiation, (v.solarRadiation >= 0 ? String(v.solarRadiation) : F("-"))); - appendRow(html, F("data-age"), t.pollAge, String((millis() - v.lastReadMs) / 1000UL) + String(F(" ")) + t.seconds); - html += F("
"); - - html += F("

"); - html += t.modbusLog; - html += F("

"); - appendRow(html, F("mb-state"), t.modbusLastStatus, c.lastOk ? String(F("")) + t.statusOkShort + F("") : String(F("")) + t.statusErrorShort + F("")); - appendRow(html, F("mb-action"), t.modbusAction, htmlEscape(c.lastAction)); - appendRow(html, F("mb-dur"), t.modbusDuration, String(c.lastDurationMs) + F(" ms")); - appendRow(html, F("mb-stats"), t.modbusCounters, String(c.successCount) + F(" / ") + String(c.errorCount)); - appendRow(html, F("mb-err"), t.modbusError, c.lastError.length() ? htmlEscape(c.lastError) : F("-")); - html += F("
"); - - html += F("

"); - html += t.modbusFrames; - html += F("

"); - html += F("
"); - html += t.modbusTx; - html += F("
"); - html += htmlEscape(c.lastRequestHex); - html += F("
"); - html += F("
"); - html += t.modbusRx; - html += F("
"); - html += htmlEscape(c.lastResponseHex); - html += F("
"); - - html += F("
"); - html += t.rawRegisters; - html += F("
"); - for (uint8_t i = 0; i < REG_DATA_COUNT; i++) { - html += F("R"); - html += String(REG_DATA_START + i); - html += F(" = "); - html += String(v.raw[i]); - if (i + 1 < REG_DATA_COUNT) html += '\n'; - } - html += F("
"); - - html += F("
"); -} - -static String buildMainScript(UiLang lang, const Texts& t) { - String s; - s.reserve(9000); - - s += F(""); - return s; -} - -static String buildManualCommandScript() { - String s; - s.reserve(2400); - s += F(""); - return s; -} - -String buildMainPage(UiLang lang, const WeatherStation& ws, bool ethUp, const String& ip, const String& mac) { - const auto& t = T(lang); - String html; - html.reserve(24000); - html += F(""); - html += t.appTitle; - html += F(""); - html += buildStyle(t, lang); - html += F("
"); - appendTop(html, lang, t, "main"); - appendMainCards(html, t, ws, ethUp, ip, mac); - html += buildMainScript(lang, t); - html += F("
"); - return html; -} - -String buildConfigPage(UiLang lang, const WeatherStation& ws, const String& flashMsg) { - const auto& t = T(lang); - String html; - html.reserve(24000); - - (void)flashMsg; - - html += F(""); - html += t.tabConfig; - html += F(""); - html += buildStyle(t, lang); - html += F("
"); - appendTop(html, lang, t, "config"); - - html += F("
"); - html += buildConfigHistoryBox(t, ws); - - html += F("

"); - html += t.manualCommand; - html += F("

"); - html += t.manualNote; - html += F("
"); - - html += F("
"); - - html += F("
"); - - html += F("
"); - - html += F("
"); - - html += F("
"); - - html += F("
-
"); - - html += F("
"); - - html += F("
"); - - html += F("

"); - html += t.rawHexTitle; - html += F("

"); - html += t.rawHexNote; - html += F("
"); - - html += F("
"); - html += F(""); - html += F("
"); - - html += F("

"); - html += t.configTitle; - html += F("

"); - - html += F(""); - html += F(""); - - html += F(""); - - html += F(""); - - html += F(""); - - html += F(""); - - html += F(""); - - html += F(""); - - html += F("
"); - html += t.tableItem; - html += F(""); - html += t.tableSetting; - html += F(""); - html += t.tableNote; - html += F("
"); - html += t.address; - html += F(""); - html += F("
"); - html += F(""); - html += F(""); - html += F(""); - html += F("
"); - html += t.noteAddress; - html += F("
"); - html += t.baudrate; - html += F(""); - html += F("
"); - html += F(""); - html += F(""); - html += F(""); - html += F("
"); - html += t.noteBaudrate; - html += F("
"); - html += t.windOffset; - html += F(""); - html += F("
"); - html += F(""); - html += F(""); - html += F(""); - html += F("
"); - html += t.noteWindOffset; - html += F("
"); - html += t.rainSensitivity; - html += F(""); - html += F("
"); - html += F(""); - html += F(""); - html += F(""); - html += F("
"); - html += t.noteRainSensitivity; - html += F("
"); - html += t.commandWindZero; - html += F("
"); - html += t.noteWindZero; - html += F("
"); - html += t.commandRainZero; - html += F("
"); - html += t.noteRainZero; - html += F("
"); - - html += F("
"); - html += buildManualCommandScript(); - html += F("
"); - return html; -} - -String buildMainDataJson(const WeatherStation& ws, bool ethUp, const String& ip, const String& mac) { - const auto& v = ws.values(); - const auto& p = ws.presence(); - const auto& c = ws.commLog(); - - String json; - json.reserve(2400); - - json += F("{"); - json += F("\"eth\":"); json += ethUp ? F("true") : F("false"); - json += F(",\"ip\":\""); json += jsonString(ip); json += F("\""); - json += F(",\"mac\":\""); json += jsonString(mac); json += F("\""); - json += F(",\"age\":"); json += String((millis() - v.lastReadMs) / 1000UL); - - json += F(",\"wind\":"); json += jsonFloat(v.windSpeed); - json += F(",\"beaufort\":"); json += (v.windForce >= 0 ? String(v.windForce) : F("-1")); - json += F(",\"dir8\":"); json += (v.windDir8 >= 0 ? String(v.windDir8) : F("-1")); - json += F(",\"dirdeg\":"); json += (v.windDirDeg >= 0 ? String(v.windDirDeg) : F("-1")); - json += F(",\"temp\":"); json += jsonFloat(v.temperature); - json += F(",\"hum\":"); json += jsonFloat(v.humidity); - json += F(",\"noise\":"); json += jsonFloat(v.noise); - json += F(",\"air1\":"); json += String(v.air1); - json += F(",\"air2\":"); json += String(v.air2); - json += F(",\"press\":"); json += jsonFloat(v.pressureKpa); - json += F(",\"rain\":"); json += jsonFloat(v.rain); - json += F(",\"solar\":"); json += String(v.solarRadiation); - json += F(",\"light\":"); json += String(v.lightLux); - json += F(",\"lightHigh\":"); json += String(v.lightHigh); - json += F(",\"lightLow\":"); json += String(v.lightLow); - json += F(",\"compass\":"); json += jsonFloat(v.compassDeg, 2); - - json += F(",\"pwind\":"); json += p.wind ? F("true") : F("false"); - json += F(",\"pth\":"); json += p.humidityTemp ? F("true") : F("false"); - json += F(",\"pnoise\":"); json += p.noise ? F("true") : F("false"); - json += F(",\"pair\":"); json += p.air ? F("true") : F("false"); - json += F(",\"ppress\":"); json += p.pressure ? F("true") : F("false"); - json += F(",\"plight\":"); json += p.light ? F("true") : F("false"); - json += F(",\"prain\":"); json += p.rain ? F("true") : F("false"); - json += F(",\"pcomp\":"); json += p.compass ? F("true") : F("false"); - json += F(",\"psolar\":"); json += p.solar ? F("true") : F("false"); - - json += F(",\"mbOk\":"); json += c.lastOk ? F("true") : F("false"); - json += F(",\"mbDuration\":"); json += String(c.lastDurationMs); - json += F(",\"mbOkCount\":"); json += String(c.successCount); - json += F(",\"mbErrCount\":"); json += String(c.errorCount); - json += F(",\"mbAction\":\""); json += jsonString(c.lastAction); json += F("\""); - json += F(",\"mbError\":\""); json += jsonString(c.lastError); json += F("\""); - json += F(",\"mbTx\":\""); json += jsonString(c.lastRequestHex); json += F("\""); - json += F(",\"mbRx\":\""); json += jsonString(c.lastResponseHex); json += F("\""); - - json += F(",\"raw\":["); - for (uint8_t i = 0; i < REG_DATA_COUNT; i++) { - if (i) json += ','; - json += String(v.raw[i]); - } - json += F("]}"); - - return json; -} \ No newline at end of file diff --git a/SW/esplan_v2_renke_weather_station/WebUi.h b/SW/esplan_v2_renke_weather_station/WebUi.h deleted file mode 100755 index 8b45ab1..0000000 --- a/SW/esplan_v2_renke_weather_station/WebUi.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -#include -#include -#include "Lang.h" -#include "WeatherStation.h" - -String htmlEscape(const String& in); -String fmtFloat(float v, uint8_t decimals = 1); -String buildMainPage(UiLang lang, const WeatherStation& ws, bool ethUp, const String& ip, const String& mac); -String buildConfigPage(UiLang lang, const WeatherStation& ws, const String& flashMsg); -String buildMainDataJson(const WeatherStation& ws, bool ethUp, const String& ip, const String& mac); -String buildStyle(const Texts& t, UiLang lang); diff --git a/SW/esplan_v2_renke_weather_station/esplan_v2_renke_weather_station.ino b/SW/esplan_v2_renke_weather_station/esplan_v2_renke_weather_station.ino deleted file mode 100755 index 77fc910..0000000 --- a/SW/esplan_v2_renke_weather_station/esplan_v2_renke_weather_station.ino +++ /dev/null @@ -1,455 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include "Config.h" -#include "Lang.h" -#include "WeatherStation.h" -#include "WebUi.h" -#include "logo_web.h" - -Preferences prefs; -WebServer server(80); -HardwareSerial RS485Serial(2); -WeatherStation station(RS485Serial); - -bool ethConnected = false; -bool ethGotIp = false; -unsigned long lastPollMs = 0; -unsigned long lastDetectRecheckMs = 0; -bool uiOnConfigPage = false; -String flashMessage; - -static void fwLog(const String& msg) { - Serial.println(String(F("[FW] ")) + msg); -} - - -static void sendNoCacheHeaders() { - server.sendHeader(F("Cache-Control"), F("no-store, no-cache, must-revalidate, max-age=0")); - server.sendHeader(F("Pragma"), F("no-cache")); - server.sendHeader(F("Expires"), F("0")); -} - -static void loadPreferences() { - prefs.begin("meteo", true); - station.config().address = prefs.getUChar("addr", DEFAULT_MODBUS_ADDR); - station.config().baudrate = prefs.getUInt("baud", DEFAULT_MODBUS_BAUD); - station.config().airMode = static_cast(prefs.getUChar("airMode", 0)); - prefs.end(); -} - -static void savePreferences() { - prefs.begin("meteo", false); - prefs.putUChar("addr", station.config().address); - prefs.putUInt("baud", station.config().baudrate); - prefs.putUChar("airMode", static_cast(station.config().airMode)); - prefs.end(); -} - -void WiFiEvent(WiFiEvent_t event) { - switch (event) { - case ARDUINO_EVENT_ETH_START: - ETH.setHostname("esplan-weather"); - break; - case ARDUINO_EVENT_ETH_CONNECTED: - ethConnected = true; - fwLog(F("ETH link connected")); - break; - case ARDUINO_EVENT_ETH_GOT_IP: - ethConnected = true; - ethGotIp = true; - fwLog(String(F("ETH IP acquired: ")) + ETH.localIP().toString()); - break; - case ARDUINO_EVENT_ETH_DISCONNECTED: - ethConnected = false; - ethGotIp = false; - fwLog(F("ETH link disconnected")); - break; - case ARDUINO_EVENT_ETH_STOP: - ethConnected = false; - ethGotIp = false; - fwLog(F("ETH stopped")); - break; - default: - break; - } -} - -static UiLang currentLang() { - if (server.hasArg("lang")) return parseLang(server.arg("lang")); - return UiLang::CZ; -} - -static String currentIp() { - return ethGotIp ? ETH.localIP().toString() : String("0.0.0.0"); -} - -static String currentMac() { - return ETH.macAddress(); -} - -static void redirectWithMessage(const String& url, UiLang lang, const String& msg) { - flashMessage = msg; - const String target = url + (url.indexOf('?') >= 0 ? '&' : '?') + String(F("lang=")) + langCode(lang); - server.sendHeader("Location", target, true); - server.send(302, "text/plain", ""); -} - -static String buildSimpleMessage(UiLang lang, const String& title, const String& detail, bool ok) { - const auto& t = T(lang); - String msg; - msg.reserve(320); - msg += F("
"); - msg += htmlEscape(title); - msg += F(": "); - msg += ok ? String(F("")) + t.statusOkShort + F("") : String(F("")) + t.statusErrorShort + F(""); - if (detail.length()) { - msg += F("
"); - msg += htmlEscape(detail); - } - msg += F("
"); - return msg; -} - -static String buildConfigResultMessage(UiLang lang, const WeatherStation& ws, const String& title) { - const auto& t = T(lang); - const auto& c = ws.commLog(); - - String msg; - msg.reserve(900); - msg += F("
"); - msg += htmlEscape(title); - msg += F(": "); - msg += c.lastOk ? String(F("")) + t.statusOkShort + F("") : String(F("")) + t.statusErrorShort + F(""); - msg += F("
"); - msg += htmlEscape(t.modbusAction); - msg += F(": "); - msg += htmlEscape(c.lastAction.length() ? c.lastAction : String("-")); - msg += F("
"); - msg += htmlEscape(t.modbusDuration); - msg += F(": "); - msg += String(c.lastDurationMs); - msg += F(" ms"); - msg += F("
"); - msg += htmlEscape(t.modbusError); - msg += F(": "); - msg += (c.lastError.length() ? htmlEscape(c.lastError) : F("-")); - msg += F("
"); - return msg; -} - -static bool parseHex16(const String& s, uint16_t& out) { - String v = s; - v.trim(); - v.replace("0x", ""); - v.replace("0X", ""); - if (v.length() == 0 || v.length() > 4) return false; - char* endptr = nullptr; - unsigned long n = strtoul(v.c_str(), &endptr, 16); - if (*endptr != '\0' || n > 0xFFFFUL) return false; - out = static_cast(n); - return true; -} - -static bool parseUint32Strict(const String& s, uint32_t& out) { - String v = s; - v.trim(); - if (v.length() == 0) return false; - char* endptr = nullptr; - unsigned long n = strtoul(v.c_str(), &endptr, 10); - if (*endptr != '\0') return false; - out = static_cast(n); - return true; -} - -static bool parseHexBytesLoose(const String& input, uint8_t* out, size_t& outLen, size_t maxLen) { - String s = input; - s.trim(); - s.replace(" ", ""); - s.replace("\t", ""); - s.replace("\r", ""); - s.replace("\n", ""); - if (s.length() == 0 || (s.length() % 2) != 0) { - outLen = 0; - return false; - } - size_t bytes = s.length() / 2; - if (bytes > maxLen) { - outLen = 0; - return false; - } - outLen = 0; - for (size_t i = 0; i < bytes; i++) { - String tok = s.substring(i * 2, i * 2 + 2); - char* endptr = nullptr; - unsigned long v = strtoul(tok.c_str(), &endptr, 16); - if (*endptr != '\0' || v > 0xFFUL) { - outLen = 0; - return false; - } - out[outLen++] = static_cast(v); - } - return true; -} - -static bool writeRegisterToAddress(uint8_t slaveAddr, uint16_t reg, uint16_t value) { - const uint8_t oldAddr = station.config().address; - station.config().address = slaveAddr; - const bool ok = station.writeRegister(reg, value); - station.config().address = oldAddr; - return ok; -} - -static void handleRoot() { - uiOnConfigPage = false; - UiLang lang = currentLang(); - sendNoCacheHeaders(); - server.send(200, "text/html; charset=utf-8", buildMainPage(lang, station, ethConnected && ethGotIp, currentIp(), currentMac())); -} - -static void handleConfig() { - uiOnConfigPage = true; - UiLang lang = currentLang(); - const String msg = flashMessage; - flashMessage = String(); - sendNoCacheHeaders(); - server.send(200, "text/html; charset=utf-8", buildConfigPage(lang, station, msg)); -} - -static void handleLogo() { - server.send_P(200, "image/png", reinterpret_cast(logo_web_png), logo_web_png_len); -} - -static void handleJson() { - sendNoCacheHeaders(); - server.send(200, "application/json; charset=utf-8", buildMainDataJson(station, ethConnected && ethGotIp, currentIp(), currentMac())); -} - -static bool applyRuntimeComm(UiLang lang, String& errorText, bool& changed) { - changed = false; - - if (server.hasArg("address")) { - uint32_t addr = 0; - if (!parseUint32Strict(server.arg("address"), addr) || addr < 1 || addr > 247) { - errorText = T(lang).address; - return false; - } - if (station.config().address != static_cast(addr)) { - station.config().address = static_cast(addr); - changed = true; - } - } - - if (server.hasArg("baudrate")) { - uint32_t baud = 0; - if (!parseUint32Strict(server.arg("baudrate"), baud)) { - errorText = T(lang).baudrate; - return false; - } - switch (baud) { - case 1200: - case 2400: - case 4800: - case 9600: - case 19200: - case 38400: - case 57600: - case 115200: - if (station.config().baudrate != baud) { - station.setBaudrate(baud); - changed = true; - } - break; - default: - errorText = T(lang).baudrate; - return false; - } - } - - if (changed) savePreferences(); - return true; -} - -static void handleConfigSave() { - UiLang lang = currentLang(); - const auto& t = T(lang); - - String runtimeError; - bool runtimeChanged = false; - if (!applyRuntimeComm(lang, runtimeError, runtimeChanged)) { - redirectWithMessage("/config", lang, buildSimpleMessage(lang, t.save, runtimeError, false)); - return; - } - - bool didWrite = false; - bool ok = true; - String firstValidationError; - - const bool hasWindOffset = server.hasArg("windOffset") && server.arg("windOffset").length() > 0; - const bool hasRainSensitivity = server.hasArg("rainSensitivity") && server.arg("rainSensitivity").length() > 0; - - if (hasWindOffset) { - uint16_t v = 0; - if (!parseHex16(server.arg("windOffset"), v)) { - ok = false; - firstValidationError = t.windOffset; - } else { - ok = station.writeRegister(REG_CFG_WIND_DIR_OFFSET, v) && ok; - station.pushConfigLog(); - didWrite = true; - if (ok) station.config().windOffset = v; - } - } - - if (hasRainSensitivity) { - uint16_t v = 0; - if (!parseHex16(server.arg("rainSensitivity"), v)) { - ok = false; - if (!firstValidationError.length()) firstValidationError = t.rainSensitivity; - } else { - ok = station.writeRegister(REG_CFG_RAIN_SENSITIVITY, v) && ok; - station.pushConfigLog(); - didWrite = true; - if (ok) station.config().rainSensitivity = v; - } - } - - if (!didWrite) { - const String detail = runtimeChanged ? String() : firstValidationError; - redirectWithMessage("/config", lang, buildSimpleMessage(lang, t.save, detail, runtimeChanged || ok)); - return; - } - - if (!ok && firstValidationError.length()) { - String msg = buildConfigResultMessage(lang, station, t.save); - msg += F("
"); - msg += htmlEscape(firstValidationError); - msg += F("
"); - redirectWithMessage("/config", lang, msg); - return; - } - - redirectWithMessage("/config", lang, buildConfigResultMessage(lang, station, t.save)); -} - -static void handleCmdWindZero() { - UiLang lang = currentLang(); - station.commandZeroWind(); - station.pushConfigLog(); - redirectWithMessage("/config", lang, buildConfigResultMessage(lang, station, T(lang).commandWindZero)); -} - -static void handleCmdRainZero() { - UiLang lang = currentLang(); - station.commandZeroRain(); - station.pushConfigLog(); - redirectWithMessage("/config", lang, buildConfigResultMessage(lang, station, T(lang).commandRainZero)); -} - -static void handleCmdManual() { - UiLang lang = currentLang(); - const auto& t = T(lang); - if (!server.hasArg("address") || !server.hasArg("reg") || !server.hasArg("value")) { - redirectWithMessage("/config", lang, buildSimpleMessage(lang, t.manualCommand, t.writeFail, false)); - return; - } - - uint32_t addrParsed = 0; - uint16_t regIn = 0; - uint16_t valIn = 0; - if (!parseUint32Strict(server.arg("address"), addrParsed) || addrParsed < 1 || addrParsed > 247 || - !parseHex16(server.arg("reg"), regIn) || !parseHex16(server.arg("value"), valIn)) { - redirectWithMessage("/config", lang, buildSimpleMessage(lang, t.manualCommand, t.writeFail, false)); - return; - } - - writeRegisterToAddress(static_cast(addrParsed), regIn, valIn); - station.pushConfigLog(); - redirectWithMessage("/config", lang, buildConfigResultMessage(lang, station, t.manualCommand)); -} - -static void handleCmdRawHex() { - UiLang lang = currentLang(); - const auto& t = T(lang); - if (!server.hasArg("rawhex")) { - redirectWithMessage("/config", lang, buildSimpleMessage(lang, t.rawHexTitle, t.writeFail, false)); - return; - } - - uint8_t frame[128]; - size_t frameLen = 0; - if (!parseHexBytesLoose(server.arg("rawhex"), frame, frameLen, sizeof(frame))) { - redirectWithMessage("/config", lang, buildSimpleMessage(lang, t.rawHexTitle, t.writeFail, false)); - return; - } - - uint8_t resp[128] = {0}; - station.sendRawFrame(frame, frameLen, resp, sizeof(resp), 300, String(t.rawHexTitle)); - station.pushConfigLog(); - redirectWithMessage("/config", lang, buildConfigResultMessage(lang, station, t.rawHexTitle)); -} - -static void handleNotFound() { - sendNoCacheHeaders(); - server.send(404, "text/plain; charset=utf-8", "404 Not Found"); -} - -static void beginEthernet() { - WiFi.onEvent(WiFiEvent); - ETH.begin(ETH_PHY_TYPE, ETH_PHY_ADDR, ETH_PHY_MDC, ETH_PHY_MDIO, ETH_PHY_POWER, ETH_CLK_MODE); -} - -static void beginWeb() { - server.on("/", HTTP_GET, handleRoot); - server.on("/config", HTTP_GET, handleConfig); - server.on("/data.json", HTTP_GET, handleJson); - server.on("/logo.png", HTTP_GET, handleLogo); - server.on("/config-save", HTTP_POST, handleConfigSave); - server.on("/cmd-wind-zero", HTTP_POST, handleCmdWindZero); - server.on("/cmd-rain-zero", HTTP_POST, handleCmdRainZero); - server.on("/cmd-manual", HTTP_POST, handleCmdManual); - server.on("/cmd-rawhex", HTTP_POST, handleCmdRawHex); - server.onNotFound(handleNotFound); - server.begin(); -} - -void setup() { - Serial.begin(115200); - delay(300); - Serial.println(); - Serial.println("ESPlan Weather Station start"); - fwLog(F("Boot")); - - loadPreferences(); - station.begin(); - station.readSensorConfig(); - station.readAll(); - - beginEthernet(); - beginWeb(); - fwLog(F("HTTP server started")); -} - -void loop() { - server.handleClient(); - - const unsigned long now = millis(); - if (!uiOnConfigPage && (now - lastPollMs >= SENSOR_POLL_MS)) { - lastPollMs = now; - const bool ok = station.readAll(); - if (!ok) { - fwLog(String(F("Modbus read failed: ")) + station.commLog().lastError); - } - } - - if (!uiOnConfigPage && (now - lastDetectRecheckMs >= DETECT_RECHECK_MS)) { - lastDetectRecheckMs = now; - const bool ok = station.readSensorConfig(); - if (!ok) { - fwLog(String(F("Sensor config refresh failed: ")) + station.commLog().lastError); - } - } -} diff --git a/SW/esplan_v2_renke_weather_station/logo_web.cpp b/SW/esplan_v2_renke_weather_station/logo_web.cpp deleted file mode 100755 index fb24c40..0000000 --- a/SW/esplan_v2_renke_weather_station/logo_web.cpp +++ /dev/null @@ -1,936 +0,0 @@ -#include "logo_web.h" -#include -#include - -const uint8_t logo_web_png[] PROGMEM = { - 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, - 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x01, 0x90, 0x00, 0x00, 0x00, 0x66, - 0x08, 0x06, 0x00, 0x00, 0x00, 0xE5, 0x03, 0xC7, 0x7C, 0x00, 0x00, 0x00, - 0x01, 0x73, 0x52, 0x47, 0x42, 0x01, 0xD9, 0xC9, 0x2C, 0x7F, 0x00, 0x00, - 0x00, 0x04, 0x67, 0x41, 0x4D, 0x41, 0x00, 0x00, 0xB1, 0x8F, 0x0B, 0xFC, - 0x61, 0x05, 0x00, 0x00, 0x00, 0x20, 0x63, 0x48, 0x52, 0x4D, 0x00, 0x00, - 0x7A, 0x26, 0x00, 0x00, 0x80, 0x84, 0x00, 0x00, 0xFA, 0x00, 0x00, 0x00, - 0x80, 0xE8, 0x00, 0x00, 0x75, 0x30, 0x00, 0x00, 0xEA, 0x60, 0x00, 0x00, - 0x3A, 0x98, 0x00, 0x00, 0x17, 0x70, 0x9C, 0xBA, 0x51, 0x3C, 0x00, 0x00, - 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x2C, 0x4B, 0x00, 0x00, - 0x2C, 0x4B, 0x01, 0xA5, 0x3D, 0x96, 0xA9, 0x00, 0x00, 0x20, 0x00, 0x49, - 0x44, 0x41, 0x54, 0x78, 0xDA, 0xED, 0x9D, 0x79, 0x98, 0x14, 0xC5, 0xF9, - 0xC7, 0x3F, 0x35, 0xBB, 0x0B, 0x28, 0x82, 0x88, 0x12, 0x4F, 0xBC, 0xEF, - 0xA8, 0xA8, 0x98, 0xA8, 0xC9, 0xA0, 0x02, 0x2A, 0x06, 0xD4, 0x78, 0xD1, - 0x6A, 0x34, 0xDE, 0x6D, 0xBC, 0x30, 0xC6, 0xC4, 0xC4, 0xF1, 0xF8, 0x99, - 0x78, 0x0D, 0x78, 0xDF, 0x1A, 0x6B, 0xA3, 0x46, 0x45, 0x4D, 0x71, 0x28, - 0x06, 0x24, 0x9A, 0xC4, 0x73, 0x8C, 0x07, 0x2A, 0x5E, 0xA8, 0xC8, 0x65, - 0xF0, 0x8A, 0x72, 0xDF, 0xC7, 0xEE, 0x4E, 0xFD, 0xFE, 0xA8, 0x1A, 0x76, - 0x58, 0x66, 0x76, 0xBB, 0x7B, 0x66, 0x76, 0x66, 0x77, 0xEB, 0xFB, 0x3C, - 0x3C, 0xC0, 0x6E, 0x77, 0x75, 0x75, 0x55, 0xF5, 0xFB, 0xAD, 0xF7, 0xAD, - 0xF7, 0x10, 0x94, 0x18, 0x42, 0xAA, 0xAE, 0x1A, 0x76, 0x02, 0x76, 0x04, - 0x7A, 0x01, 0xEB, 0x03, 0x31, 0xFB, 0xEB, 0xE5, 0xC0, 0x7C, 0x60, 0x96, - 0x80, 0xCF, 0xB5, 0xEF, 0xCD, 0xA7, 0x1D, 0x42, 0x48, 0xD5, 0x43, 0xC3, - 0xCE, 0xC0, 0xF6, 0xC0, 0xC6, 0x76, 0x0C, 0x00, 0x34, 0xB0, 0x0C, 0x98, - 0x0B, 0xCC, 0xB0, 0x63, 0xB0, 0x14, 0x07, 0x07, 0x07, 0x87, 0xB6, 0x20, - 0xDB, 0x4A, 0x20, 0x2C, 0x37, 0xD0, 0x70, 0x18, 0x30, 0x08, 0xE8, 0x07, - 0xEC, 0x02, 0x54, 0x05, 0xB8, 0x35, 0x0D, 0x7C, 0x09, 0xBC, 0x0E, 0xBC, - 0x00, 0x4C, 0xC4, 0xF7, 0xBE, 0x6F, 0xA3, 0x84, 0xB1, 0x89, 0x86, 0x23, - 0x80, 0xC3, 0x81, 0x38, 0xB0, 0x4D, 0x16, 0x69, 0x36, 0x87, 0x06, 0x60, - 0x1A, 0x90, 0x02, 0xFE, 0x21, 0xE0, 0x05, 0xED, 0x7B, 0x4B, 0xDC, 0x32, - 0x75, 0x70, 0x70, 0x68, 0xB7, 0x04, 0xD2, 0xB3, 0x76, 0x94, 0x98, 0xAF, - 0x75, 0x1C, 0xF8, 0x15, 0x70, 0x0C, 0xD0, 0xB5, 0x08, 0xCD, 0xD6, 0x03, - 0xFF, 0x02, 0x6A, 0xAB, 0x60, 0x5C, 0x83, 0xEF, 0xD5, 0x57, 0xF2, 0x40, - 0x56, 0x4B, 0x55, 0x55, 0x0F, 0x47, 0x02, 0x3E, 0x86, 0x40, 0x3B, 0x15, - 0xA1, 0xD9, 0xE5, 0xC0, 0xB3, 0xC0, 0x83, 0xDD, 0x85, 0x78, 0x79, 0xF1, - 0x39, 0x43, 0xB5, 0x5B, 0xB2, 0x0E, 0x0E, 0x0E, 0x19, 0xE8, 0x64, 0x7C, - 0xB0, 0x95, 0xBB, 0xB9, 0xF0, 0x85, 0x48, 0xA4, 0x7E, 0x5D, 0xD9, 0x04, - 0x22, 0xD5, 0x60, 0xE0, 0x1A, 0xE0, 0xC7, 0x25, 0xEC, 0xE7, 0x2C, 0x60, - 0x44, 0x15, 0x3C, 0xD4, 0xE0, 0x7B, 0x75, 0x15, 0x48, 0x1C, 0xA7, 0x03, - 0x09, 0x8C, 0x99, 0xAE, 0x54, 0x98, 0x0C, 0x5C, 0xDB, 0x4D, 0x88, 0x71, - 0x4B, 0x1C, 0x91, 0x38, 0x38, 0x38, 0x18, 0x02, 0xB9, 0x00, 0xB8, 0x37, - 0xCF, 0xAF, 0x3F, 0x12, 0x89, 0xD4, 0x5E, 0xA5, 0x7C, 0x7E, 0x2C, 0xF2, - 0x9D, 0x52, 0xED, 0x8E, 0x54, 0xFF, 0x06, 0x26, 0x94, 0x98, 0x3C, 0x00, - 0xB6, 0x03, 0x1E, 0x68, 0x80, 0x0F, 0x91, 0x6A, 0x50, 0xC5, 0xCC, 0x9E, - 0x54, 0xFD, 0xEB, 0xE1, 0x7D, 0xE0, 0x2F, 0x25, 0x26, 0x0F, 0x80, 0x7D, - 0x80, 0xA7, 0x97, 0x68, 0xFD, 0x0A, 0x52, 0xF5, 0x71, 0x9F, 0x8E, 0x83, - 0x83, 0x43, 0xD9, 0x37, 0xD0, 0x61, 0x6F, 0xA8, 0x91, 0xAA, 0xAA, 0x0E, - 0xFE, 0x00, 0xFC, 0x1F, 0xD0, 0xB9, 0x95, 0xFB, 0xBB, 0x2B, 0xF0, 0x0F, - 0xA4, 0x7A, 0x44, 0xC0, 0xAF, 0xB5, 0xEF, 0x2D, 0x2E, 0xC7, 0xA0, 0xD9, - 0x73, 0x9E, 0x5B, 0x31, 0xE6, 0x2A, 0xD1, 0xCA, 0x8F, 0xEF, 0x07, 0x4C, - 0x42, 0xAA, 0x1B, 0xAB, 0xE0, 0xFA, 0x4A, 0x37, 0xED, 0x39, 0x38, 0x38, - 0xB4, 0x5F, 0x84, 0xD2, 0x40, 0x84, 0x54, 0xBD, 0xEA, 0xE0, 0x79, 0xE0, - 0x86, 0x32, 0x90, 0x47, 0x36, 0xCE, 0xD0, 0xF0, 0x2E, 0x52, 0xED, 0xD5, - 0xEA, 0x4F, 0x96, 0x6A, 0x37, 0x0D, 0x6F, 0x03, 0xE7, 0x96, 0x81, 0x3C, - 0xD6, 0xF0, 0x38, 0x70, 0x4D, 0x03, 0xBC, 0x88, 0x54, 0x9B, 0xB9, 0x65, - 0xEC, 0xE0, 0xE0, 0x50, 0x96, 0xCD, 0x74, 0x08, 0xC1, 0xB9, 0x2B, 0xC6, - 0x5C, 0xB5, 0x7D, 0x05, 0xF5, 0x7F, 0x29, 0x70, 0x22, 0xBE, 0xF7, 0x5C, - 0x2B, 0x91, 0xC7, 0xA1, 0xC0, 0x68, 0x60, 0xC3, 0x0A, 0x1A, 0x83, 0x2F, - 0x81, 0x21, 0xF8, 0xDE, 0x47, 0x6E, 0x39, 0x3B, 0x38, 0xB4, 0x5F, 0xE8, - 0x64, 0xFC, 0xA7, 0x40, 0xF7, 0x26, 0x3F, 0x3E, 0x12, 0xB8, 0x20, 0xCF, - 0x2D, 0x5F, 0xE4, 0xF9, 0xDD, 0x1B, 0x22, 0x91, 0x5A, 0xD8, 0x7A, 0x04, - 0x62, 0x6C, 0xEE, 0xFF, 0xC4, 0xC4, 0x71, 0x54, 0x1A, 0xEA, 0x80, 0xD3, - 0xF1, 0xBD, 0x27, 0x4B, 0x4C, 0x1E, 0xC7, 0x01, 0x4F, 0x94, 0x59, 0xF3, - 0xCA, 0x87, 0xF9, 0xC0, 0x60, 0x7C, 0xEF, 0x2D, 0xF7, 0x99, 0x39, 0x38, - 0xB4, 0x5B, 0x02, 0x99, 0x0C, 0xEC, 0x5D, 0x84, 0xA6, 0x0E, 0x14, 0x89, - 0xD4, 0x9B, 0xC5, 0xE8, 0x53, 0xCB, 0x26, 0x2C, 0xA9, 0x76, 0xAF, 0x60, - 0xF2, 0x00, 0x63, 0xCE, 0x79, 0x14, 0xA9, 0x4E, 0x2A, 0x21, 0x79, 0x1C, - 0x03, 0x3C, 0x55, 0xA1, 0xE4, 0x01, 0xD0, 0x13, 0x73, 0x36, 0xB4, 0xB7, - 0xFB, 0xCC, 0x1C, 0x1C, 0x1C, 0x5A, 0x0B, 0xB1, 0x16, 0x04, 0xE7, 0x66, - 0xC0, 0xC4, 0x0A, 0x26, 0x8F, 0x0C, 0xAA, 0x81, 0xC7, 0x4A, 0x42, 0x22, - 0x52, 0x1D, 0x69, 0xC9, 0xA3, 0xA6, 0xC2, 0xC7, 0xA0, 0x07, 0xF0, 0x1C, - 0x52, 0xF5, 0x76, 0xCB, 0xDA, 0xC1, 0xC1, 0xA1, 0xB5, 0x04, 0x6F, 0x6E, - 0x66, 0x91, 0xAA, 0x53, 0xDA, 0xD8, 0xFB, 0xB7, 0x2E, 0xC2, 0x73, 0xA6, - 0x03, 0xEF, 0x00, 0x9F, 0x02, 0x73, 0x80, 0xD5, 0x98, 0xE8, 0xF4, 0x8D, - 0x80, 0x1D, 0x80, 0xBE, 0xC0, 0x5E, 0x44, 0xF0, 0x0A, 0xCB, 0x41, 0x22, - 0x1A, 0xDF, 0xFB, 0x5B, 0x91, 0xC8, 0x63, 0x08, 0x66, 0x0C, 0x0A, 0xD5, - 0x3C, 0xD2, 0xC0, 0x47, 0x76, 0x0C, 0xA6, 0x63, 0x4C, 0x4E, 0x0D, 0x96, - 0x94, 0x7A, 0x61, 0xBC, 0xCB, 0xF6, 0xC3, 0xA4, 0x3B, 0x29, 0x04, 0x9B, - 0x03, 0x63, 0x63, 0x52, 0xF5, 0x4B, 0xFB, 0xDE, 0x4A, 0xB7, 0xBC, 0x73, - 0xCE, 0xE9, 0xCD, 0xC0, 0xA6, 0x45, 0x68, 0xE9, 0x0F, 0xF8, 0xDE, 0xB7, - 0x6E, 0x40, 0x1D, 0x1C, 0x81, 0xE4, 0x96, 0x78, 0x7F, 0x02, 0x7E, 0x5A, - 0x40, 0xDB, 0xB3, 0x00, 0x09, 0x28, 0x7C, 0x6F, 0x46, 0x4B, 0x17, 0x0B, - 0xA9, 0x36, 0xD6, 0x70, 0x34, 0x70, 0x0E, 0xF0, 0x93, 0x02, 0xDE, 0xE7, - 0x71, 0x4B, 0x22, 0xAA, 0x08, 0xE4, 0x31, 0xA6, 0x40, 0xF2, 0x78, 0x1B, - 0xA8, 0x15, 0x30, 0x4E, 0x07, 0x49, 0xCB, 0x22, 0xD5, 0xB6, 0x80, 0x87, - 0x71, 0x0F, 0x8E, 0x1A, 0x57, 0xB2, 0x5F, 0x1A, 0x86, 0x03, 0x97, 0xB8, - 0xE5, 0x9D, 0x13, 0x3F, 0xC7, 0xE4, 0x66, 0x2B, 0x14, 0x37, 0x02, 0x8E, - 0x40, 0x1C, 0x3A, 0x34, 0x44, 0x1E, 0x41, 0xB6, 0x3F, 0x26, 0x27, 0x55, - 0x55, 0x84, 0x36, 0xBF, 0x02, 0xAE, 0xAE, 0x82, 0xC7, 0x23, 0xC7, 0x28, - 0x48, 0xD5, 0x0F, 0xB8, 0x19, 0xD8, 0x3F, 0xE2, 0x7B, 0xD5, 0x03, 0xA7, - 0x44, 0x26, 0x11, 0x23, 0xC8, 0x3F, 0x05, 0xBA, 0x44, 0x7C, 0xFE, 0x7B, - 0xC0, 0x65, 0x9B, 0x08, 0xF1, 0xD2, 0xDC, 0x08, 0x51, 0xE3, 0x36, 0xD6, - 0xC6, 0x03, 0x92, 0x98, 0x3C, 0x5A, 0x51, 0x34, 0x9E, 0x01, 0xF8, 0xDE, - 0x2B, 0x6E, 0x89, 0xAF, 0x33, 0xB7, 0x9F, 0x17, 0x89, 0x40, 0x76, 0xC3, - 0xF7, 0x3E, 0x73, 0x03, 0xEA, 0xD0, 0x5A, 0xA8, 0xC4, 0x43, 0xF4, 0x75, - 0x08, 0xC4, 0xA6, 0xE6, 0x98, 0x84, 0x89, 0x7C, 0x0E, 0x8B, 0x5A, 0x01, - 0x97, 0x16, 0x23, 0x01, 0x60, 0x27, 0xA9, 0x62, 0xAB, 0xE1, 0x42, 0xE0, - 0xA6, 0x88, 0x82, 0xBC, 0xCE, 0x92, 0xC8, 0xA8, 0x88, 0x82, 0xE6, 0xB7, - 0x96, 0xC4, 0xC2, 0xC4, 0x7A, 0xAC, 0x06, 0xAE, 0xAC, 0x86, 0xDB, 0xEB, - 0x7D, 0xAF, 0xA1, 0xE0, 0xC9, 0x31, 0x99, 0x8C, 0x47, 0x60, 0x5C, 0xF1, - 0xC2, 0xC6, 0x9C, 0x7C, 0x1A, 0x83, 0x3E, 0xE9, 0x0A, 0x4A, 0xFD, 0xA2, - 0x93, 0xF1, 0x0D, 0x03, 0xBE, 0x47, 0x5A, 0x24, 0x52, 0xA5, 0x09, 0x12, - 0x6D, 0xC7, 0x04, 0xA2, 0x93, 0xF1, 0xEE, 0x04, 0x8B, 0xED, 0xD2, 0x22, - 0x91, 0x5A, 0xD4, 0xA1, 0x77, 0xCE, 0x52, 0xF5, 0x08, 0xBA, 0x19, 0x2B, - 0x57, 0xC0, 0x72, 0x8E, 0xF9, 0x7D, 0x03, 0x63, 0xEA, 0x6F, 0x6A, 0x75, - 0xE9, 0xD4, 0xCC, 0x46, 0x32, 0x97, 0x29, 0xBB, 0xBF, 0x48, 0xA4, 0xDE, - 0x2E, 0x46, 0x9F, 0xAA, 0x73, 0x6C, 0xDD, 0x4F, 0x8B, 0x40, 0x1E, 0xAB, - 0x81, 0xF3, 0xF0, 0xBD, 0x87, 0x8B, 0x95, 0xA4, 0x69, 0xB5, 0xEF, 0xA5, - 0x81, 0xBB, 0x91, 0xEA, 0x0D, 0x4C, 0x42, 0xC1, 0xCD, 0xC3, 0x6E, 0xE4, - 0x81, 0x27, 0x90, 0x8A, 0x48, 0x24, 0xE2, 0x7B, 0xB7, 0x22, 0x95, 0xB0, - 0x24, 0x12, 0x04, 0xDF, 0x03, 0xC7, 0xE1, 0x7B, 0xAF, 0x17, 0x2B, 0x34, - 0x5C, 0xFB, 0xDE, 0x32, 0xE0, 0x22, 0xA4, 0xFA, 0x0F, 0xF0, 0x10, 0xE1, - 0xCC, 0x69, 0xBB, 0xA5, 0x8D, 0x29, 0xEC, 0xBE, 0x0A, 0xFA, 0x6E, 0xBF, - 0x01, 0xD6, 0x0B, 0x70, 0xDD, 0x4C, 0x4A, 0x9F, 0x1A, 0xA6, 0x3D, 0xE2, - 0x63, 0x60, 0xAB, 0x00, 0xD7, 0x2D, 0xD1, 0xC3, 0xFB, 0x6D, 0x24, 0x2E, - 0x7F, 0x2D, 0xDD, 0x61, 0x77, 0xF3, 0xE6, 0x1C, 0x92, 0xB6, 0xB5, 0x16, - 0xC5, 0x4F, 0xD7, 0xDD, 0x80, 0xE9, 0xF3, 0x81, 0xBB, 0xF3, 0xDC, 0x30, - 0x05, 0xC4, 0xBA, 0xB2, 0x5C, 0x54, 0x15, 0x6D, 0xDE, 0xD7, 0x22, 0x90, - 0x2A, 0xA9, 0xAA, 0x1B, 0xE0, 0xEA, 0x08, 0xE4, 0x71, 0x3C, 0xBE, 0x37, - 0xBE, 0x24, 0x63, 0xE6, 0x7B, 0xEF, 0x58, 0x93, 0xD6, 0x4B, 0x40, 0xEF, - 0x08, 0xEF, 0x37, 0xD2, 0x9E, 0x89, 0x8C, 0x8E, 0xF0, 0xEC, 0x5B, 0x90, - 0x8A, 0x00, 0x24, 0xF2, 0x35, 0x30, 0x10, 0xDF, 0x9B, 0x5A, 0xA2, 0x31, - 0x78, 0x02, 0xA9, 0xE6, 0x00, 0xE3, 0x02, 0x0A, 0xE0, 0x0C, 0xAE, 0x8C, - 0x49, 0xF5, 0x97, 0xB4, 0xEF, 0xAD, 0xAA, 0x90, 0xEF, 0x36, 0x16, 0x50, - 0x03, 0x89, 0xE1, 0x10, 0xD5, 0xA2, 0xE0, 0xC6, 0x37, 0xA2, 0xF5, 0xA5, - 0xC0, 0xEB, 0x4A, 0xDF, 0xE1, 0xC4, 0xBA, 0x84, 0xAF, 0x93, 0xF1, 0x74, - 0x0B, 0xF7, 0x34, 0x94, 0xFA, 0x83, 0x5E, 0x83, 0x06, 0x38, 0x01, 0x93, - 0xB8, 0x30, 0x04, 0x91, 0x73, 0x66, 0xC9, 0xC8, 0xA3, 0x51, 0x80, 0xCE, - 0x00, 0xFA, 0x63, 0xA2, 0xAE, 0xC3, 0x22, 0xA3, 0x89, 0x9C, 0x10, 0xF1, - 0xD9, 0xB7, 0x00, 0xBF, 0x6F, 0x61, 0x57, 0x3D, 0xA0, 0x64, 0xE4, 0xD1, - 0xD8, 0x8F, 0x7F, 0x02, 0xA7, 0x5A, 0xB5, 0x34, 0x28, 0xB6, 0x48, 0x9B, - 0x7B, 0x1C, 0x1C, 0x1C, 0x1C, 0x8A, 0x8E, 0xA6, 0x26, 0xAC, 0xF3, 0x43, - 0xDE, 0x7F, 0x3B, 0xBE, 0xF7, 0x44, 0xAB, 0xF4, 0xD4, 0xF7, 0x66, 0x20, - 0xD5, 0x00, 0xE0, 0xC5, 0x08, 0x9A, 0x48, 0x86, 0x44, 0x34, 0xBE, 0x37, - 0x26, 0xC2, 0xB3, 0x6F, 0xB6, 0xE6, 0xAC, 0x11, 0x79, 0xC8, 0xE3, 0xF3, - 0x56, 0x1A, 0x83, 0xB1, 0x48, 0x75, 0x23, 0x70, 0x55, 0x88, 0xBB, 0xCE, - 0xC3, 0x64, 0x0B, 0x76, 0x30, 0x78, 0x94, 0xE2, 0xC4, 0x35, 0x2D, 0x70, - 0x43, 0xE9, 0xE0, 0x08, 0x24, 0x03, 0xA9, 0xB6, 0xC3, 0x64, 0x7A, 0x0D, - 0x8A, 0x4F, 0x63, 0x70, 0x65, 0xAB, 0x1A, 0x51, 0x7D, 0x6F, 0xBA, 0x25, - 0x91, 0x97, 0x08, 0x66, 0xEB, 0x6D, 0x4A, 0x22, 0x4F, 0x22, 0xD5, 0xC9, - 0x11, 0x49, 0xE4, 0x26, 0x6B, 0xCE, 0x1A, 0x6E, 0xD5, 0xDA, 0xD6, 0xD1, - 0x3C, 0x9A, 0xA0, 0x0A, 0xAE, 0x6D, 0x80, 0xC1, 0xC0, 0xBE, 0x01, 0x6F, - 0xE9, 0x8B, 0x54, 0xBB, 0xE3, 0x7B, 0x9F, 0xB8, 0xE5, 0x0E, 0xF8, 0xDE, - 0xF5, 0x6E, 0x10, 0x1C, 0x1C, 0x8A, 0xAF, 0x81, 0x9C, 0x40, 0x38, 0x7B, - 0xDF, 0x6F, 0xCA, 0x12, 0xAC, 0x66, 0x48, 0xA4, 0x7F, 0x81, 0x24, 0x72, - 0x12, 0xBE, 0x37, 0x36, 0x22, 0x89, 0x08, 0xE0, 0xE2, 0x72, 0x90, 0x07, - 0x40, 0x83, 0xEF, 0xD5, 0x21, 0xD5, 0x25, 0xC0, 0x2B, 0x01, 0xE7, 0x4B, - 0x00, 0xC7, 0x03, 0x8E, 0x40, 0x1C, 0x1C, 0xDA, 0x1F, 0x5E, 0x02, 0xCE, - 0x0E, 0xA4, 0x25, 0x4B, 0x75, 0x16, 0x30, 0x20, 0x80, 0xC0, 0xB8, 0x54, - 0x07, 0x2C, 0x27, 0x9E, 0x4D, 0x20, 0x47, 0x84, 0xE8, 0xF4, 0x1B, 0xF8, - 0xDE, 0xF3, 0x65, 0xDC, 0x45, 0x4E, 0xCF, 0x32, 0x67, 0x15, 0xA2, 0x89, - 0x44, 0x21, 0x91, 0x11, 0x42, 0x2A, 0xA9, 0x7D, 0x6F, 0x7E, 0x19, 0xDF, - 0xFF, 0x35, 0x5B, 0xCC, 0xEB, 0xD0, 0x80, 0x77, 0x0C, 0x06, 0xAE, 0x73, - 0xDF, 0x9A, 0x83, 0x43, 0xFB, 0x82, 0x48, 0xA4, 0x3E, 0xC5, 0xC4, 0xAC, - 0x05, 0xC1, 0xFE, 0xC0, 0x29, 0x2D, 0x5D, 0xA4, 0x4D, 0xAD, 0xA7, 0x40, - 0x04, 0x12, 0x03, 0x88, 0x49, 0x55, 0x03, 0x1C, 0x10, 0xA2, 0xDF, 0x77, - 0x57, 0x80, 0x29, 0x62, 0x9A, 0x65, 0xD3, 0xAF, 0x23, 0xDC, 0xDD, 0x09, - 0x78, 0x0A, 0xA9, 0x8E, 0x8D, 0xF2, 0xE8, 0xB2, 0x92, 0x47, 0xB4, 0x39, - 0xE8, 0x2B, 0xA4, 0xEA, 0xEA, 0x3E, 0x37, 0x07, 0x07, 0x87, 0x62, 0x22, - 0x06, 0x90, 0x36, 0xC1, 0x29, 0xEB, 0x07, 0xBC, 0x67, 0xA9, 0x80, 0x67, - 0x2A, 0xA2, 0xF7, 0x86, 0x44, 0xFA, 0x47, 0x24, 0x91, 0x9A, 0x42, 0x48, - 0xA4, 0xDC, 0xA8, 0x32, 0x49, 0x2E, 0x83, 0x12, 0x59, 0x8D, 0x06, 0x57, - 0x06, 0xD7, 0xC1, 0xC1, 0xA1, 0xF8, 0x04, 0x82, 0x49, 0xE6, 0x17, 0x14, - 0xAF, 0x68, 0xDF, 0x5B, 0x51, 0x31, 0x6F, 0x50, 0x1C, 0x4D, 0xE4, 0x98, - 0xB6, 0x36, 0x71, 0x0D, 0x26, 0xC2, 0xFC, 0x9F, 0x21, 0x6E, 0xD9, 0xC3, - 0x2D, 0x77, 0x07, 0x07, 0x87, 0x52, 0x10, 0x48, 0x98, 0xD4, 0x0E, 0xFF, - 0xA9, 0xB8, 0xB7, 0x30, 0x6E, 0xB4, 0x03, 0x30, 0x9E, 0x51, 0x51, 0x48, - 0xE4, 0x6F, 0x6D, 0x91, 0x44, 0x80, 0x30, 0x05, 0xA4, 0x76, 0x72, 0xCB, - 0xDD, 0xC1, 0xC1, 0xA1, 0x98, 0xC8, 0x1C, 0xA2, 0x6F, 0x11, 0xE2, 0x9E, - 0x29, 0x15, 0xF9, 0x26, 0xBE, 0xF7, 0x79, 0xD6, 0xC1, 0xFA, 0x16, 0x21, - 0xEF, 0xCE, 0x90, 0x88, 0x87, 0xEF, 0x8D, 0x6B, 0x43, 0xF3, 0xF7, 0x61, - 0x88, 0x6B, 0x7F, 0xD0, 0xD1, 0x16, 0xB7, 0x4E, 0xC6, 0xBB, 0x61, 0x62, - 0x3E, 0x7A, 0x60, 0xD2, 0xC0, 0xD4, 0x03, 0xAB, 0x80, 0x79, 0xC0, 0x5C, - 0x91, 0x48, 0xAD, 0x2A, 0x43, 0x9F, 0xBA, 0xDB, 0x3E, 0x6D, 0x88, 0xC9, - 0xF1, 0x56, 0x87, 0xC9, 0x57, 0x34, 0x1F, 0x98, 0x27, 0x12, 0xA9, 0x0E, - 0x9B, 0x86, 0x5F, 0x27, 0xE3, 0x5D, 0x80, 0x4D, 0x80, 0x8D, 0xED, 0xD8, - 0xC4, 0xEC, 0xD8, 0x2C, 0xB0, 0xF3, 0xB5, 0xD4, 0x89, 0xEC, 0xCA, 0x24, - 0x90, 0xEE, 0x21, 0xEE, 0x99, 0x5D, 0xB1, 0x6F, 0xE3, 0x7B, 0x53, 0x0B, - 0x24, 0x11, 0xD5, 0xC6, 0x48, 0x24, 0x4C, 0x3A, 0xF1, 0xEE, 0xED, 0x79, - 0x21, 0xEB, 0x64, 0xBC, 0x07, 0x70, 0xB0, 0xFD, 0xB3, 0x37, 0xB0, 0x27, - 0xA6, 0x52, 0x63, 0xBE, 0xB4, 0x1D, 0x75, 0x3A, 0x19, 0x9F, 0x01, 0xBC, - 0x8B, 0x31, 0x05, 0x4E, 0x10, 0x89, 0xD4, 0xDC, 0xA2, 0xF6, 0x69, 0xC4, - 0x21, 0x82, 0x74, 0xFD, 0xBE, 0xC0, 0x10, 0x4C, 0x89, 0x82, 0x7D, 0xAD, - 0x70, 0xCC, 0xD7, 0xA7, 0x7A, 0x9D, 0x8C, 0xFF, 0x0F, 0x53, 0x3B, 0xE6, - 0x03, 0xE0, 0x35, 0xE0, 0xD5, 0x72, 0x0A, 0x4E, 0x9D, 0x8C, 0x1F, 0x82, - 0xA9, 0xD7, 0x13, 0x06, 0xE3, 0x44, 0x22, 0x35, 0xBD, 0xF9, 0x76, 0xFB, - 0xD5, 0x80, 0x3E, 0x10, 0xC8, 0xB4, 0xDF, 0x07, 0xD8, 0x92, 0xFC, 0x25, - 0x26, 0x1A, 0x74, 0x32, 0xFE, 0x9D, 0x9D, 0xAF, 0x57, 0x81, 0xBF, 0x8B, - 0x44, 0x6A, 0x6A, 0x45, 0x2D, 0x42, 0x53, 0x7C, 0x6E, 0x97, 0x90, 0x77, - 0x8D, 0xC4, 0xF7, 0xFE, 0xD7, 0xA4, 0x9D, 0xCD, 0x08, 0xE0, 0x2D, 0x65, - 0xF1, 0x09, 0xBE, 0x37, 0x31, 0x4F, 0x7F, 0x9A, 0xB6, 0xB3, 0x57, 0xC0, - 0x36, 0xCF, 0x41, 0xAA, 0x79, 0xB9, 0x7E, 0x21, 0xE0, 0xB1, 0x6C, 0x17, - 0xDF, 0xEA, 0x26, 0x7F, 0x07, 0x41, 0x65, 0xEF, 0x02, 0x8A, 0x43, 0x22, - 0x43, 0xF1, 0xBD, 0x67, 0xDB, 0x80, 0xDC, 0x0C, 0x73, 0x16, 0xD5, 0xEE, - 0xBC, 0xB0, 0x74, 0x32, 0xDE, 0x19, 0x38, 0x16, 0x93, 0x00, 0x74, 0x20, - 0xF9, 0xB3, 0x92, 0xE6, 0x42, 0x0D, 0xE6, 0xEC, 0x6F, 0x57, 0xFB, 0x91, - 0xAD, 0xD2, 0xC9, 0xF8, 0x68, 0xE0, 0x7A, 0x91, 0x48, 0x7D, 0x56, 0x60, - 0xBF, 0x6A, 0x80, 0x33, 0x49, 0xD7, 0xFF, 0x1A, 0xD8, 0x3D, 0xE4, 0x86, - 0x6E, 0x2B, 0xFB, 0xE7, 0x67, 0xC0, 0xE5, 0xC0, 0x0A, 0x9D, 0x8C, 0x3F, - 0x0F, 0x3C, 0x0C, 0x62, 0x42, 0xA9, 0x73, 0x1B, 0x35, 0x79, 0x8F, 0x13, - 0x80, 0x91, 0x21, 0xC7, 0xF5, 0x21, 0x44, 0xEC, 0xF6, 0x66, 0xDA, 0x8C, - 0x03, 0x67, 0x81, 0x3E, 0x06, 0x53, 0x50, 0x2E, 0x28, 0xAA, 0xEC, 0xF7, - 0xBC, 0x05, 0x70, 0x14, 0x70, 0x93, 0x4E, 0xC6, 0x53, 0xC0, 0x8D, 0x22, - 0x91, 0xFA, 0x47, 0x05, 0x90, 0xC7, 0x05, 0x18, 0xCF, 0xC8, 0x30, 0x79, - 0xC6, 0x6E, 0x58, 0x87, 0x3C, 0x0C, 0x7A, 0x03, 0xB7, 0x04, 0x6C, 0xE3, - 0x31, 0x8C, 0x43, 0x4D, 0x2E, 0x6C, 0x1D, 0xA2, 0x9D, 0x6C, 0x24, 0xF2, - 0xCE, 0x9F, 0x91, 0xAB, 0x6B, 0x08, 0x24, 0x4A, 0x52, 0xB5, 0xCA, 0x4F, - 0xC4, 0x66, 0x02, 0xFC, 0x06, 0x12, 0xAD, 0xE0, 0x4F, 0x86, 0x44, 0x8E, - 0x6E, 0x03, 0x32, 0x34, 0x4C, 0xBD, 0x96, 0x06, 0xDA, 0x09, 0x74, 0x32, - 0x5E, 0xA3, 0x93, 0xF1, 0x8B, 0x30, 0xD5, 0x1D, 0x9F, 0xB4, 0xC2, 0xB6, - 0x53, 0x81, 0xCD, 0x76, 0xB6, 0x44, 0xF2, 0xA1, 0x4E, 0xC6, 0xAF, 0xD5, - 0xC9, 0x7E, 0xD5, 0x11, 0xFB, 0xB6, 0x3F, 0xC6, 0xB4, 0xF8, 0xE7, 0x90, - 0xE4, 0x91, 0x0F, 0xEB, 0x01, 0xC7, 0x00, 0xE3, 0x40, 0xFF, 0xA3, 0x15, - 0xC7, 0xF8, 0x44, 0xE0, 0x89, 0x90, 0xE3, 0xFA, 0x04, 0x88, 0x73, 0xC5, - 0xE5, 0xAF, 0xE6, 0x4A, 0xFA, 0x77, 0x98, 0x4E, 0xC6, 0xDF, 0xB6, 0x5A, - 0xD5, 0x99, 0x21, 0xC9, 0x23, 0xCF, 0x66, 0x98, 0x7E, 0xC0, 0x44, 0x9D, - 0x8C, 0x8F, 0xD3, 0xC9, 0x78, 0xF9, 0xCA, 0x6E, 0x4B, 0x75, 0x31, 0x70, - 0x4F, 0x48, 0xD9, 0x78, 0x6B, 0xAF, 0x98, 0xB8, 0xBA, 0xAD, 0x7F, 0x8B, - 0x99, 0x17, 0x0E, 0x63, 0x0B, 0xDE, 0xB0, 0x4D, 0xBC, 0x99, 0xA9, 0xD5, - 0x30, 0x20, 0x22, 0x89, 0x74, 0x06, 0x46, 0x21, 0xD5, 0x51, 0x15, 0xFE, - 0x96, 0x61, 0xCC, 0x52, 0xCB, 0xDA, 0x91, 0xF2, 0x51, 0x6B, 0x77, 0x7B, - 0x5B, 0x95, 0xA0, 0xED, 0x1A, 0xE0, 0x6A, 0xD0, 0xCF, 0x58, 0x9B, 0x7C, - 0x18, 0xA1, 0x7B, 0x8A, 0x35, 0xAF, 0xEC, 0x5A, 0xA2, 0xF7, 0x7E, 0xB9, - 0x95, 0xC8, 0xE3, 0x14, 0xAB, 0x79, 0xD4, 0x84, 0xB8, 0x6D, 0x0C, 0x88, - 0xD3, 0x73, 0x69, 0x48, 0xB6, 0xBD, 0x17, 0x80, 0x1F, 0x95, 0xA8, 0xCB, - 0x47, 0x03, 0x6F, 0xE9, 0x64, 0xBC, 0xF5, 0x1D, 0x45, 0xA4, 0xBA, 0x14, - 0xB8, 0x83, 0x70, 0x59, 0x3C, 0xEE, 0x5B, 0x4F, 0x88, 0xCB, 0xE6, 0x9C, - 0x3D, 0x54, 0xB7, 0xF5, 0x0F, 0x31, 0x43, 0x20, 0x61, 0x8A, 0xCB, 0x6C, - 0xD7, 0x66, 0xDE, 0xCE, 0x90, 0x48, 0x21, 0x9A, 0x48, 0xA5, 0x93, 0x48, - 0x98, 0x7A, 0xF5, 0x0B, 0xDB, 0x11, 0x81, 0xB4, 0x46, 0x02, 0xCF, 0x21, - 0xC0, 0xE3, 0x7A, 0xF8, 0x41, 0x81, 0x04, 0x83, 0x4E, 0xC6, 0x8F, 0x06, - 0xFE, 0x5A, 0x04, 0x4D, 0x28, 0x1F, 0x56, 0x58, 0xAD, 0xA6, 0xD4, 0xE4, - 0x71, 0x06, 0x26, 0xE1, 0x64, 0x18, 0xED, 0x76, 0x3C, 0x70, 0x8A, 0x48, - 0xBC, 0x96, 0xAF, 0x14, 0xCE, 0x84, 0x56, 0x58, 0x7F, 0xDB, 0x01, 0xCF, - 0xB7, 0xAA, 0x26, 0x22, 0xD5, 0x15, 0xC0, 0xAD, 0x21, 0xC9, 0xE3, 0xA1, - 0x4E, 0x30, 0x6C, 0xC5, 0x39, 0x6D, 0x9F, 0x3C, 0xB2, 0x09, 0x24, 0xCC, - 0xC1, 0xF8, 0x9E, 0x6D, 0xEA, 0x0D, 0x7D, 0xEF, 0x53, 0x4B, 0x22, 0xFF, - 0x2B, 0x40, 0x13, 0x39, 0xB2, 0x42, 0xDF, 0x2E, 0x4C, 0x70, 0xE0, 0x37, - 0xB4, 0x13, 0x88, 0x44, 0xEA, 0x79, 0x4C, 0xBD, 0xF9, 0x52, 0xE3, 0x78, - 0x74, 0xFA, 0xBC, 0x00, 0x42, 0xB7, 0x17, 0xA6, 0xE0, 0x57, 0x55, 0x09, - 0xFB, 0xF2, 0x58, 0xB1, 0x0F, 0xF9, 0x73, 0xBC, 0xC7, 0xB9, 0x56, 0xBB, - 0x0B, 0x63, 0x8A, 0xF9, 0x27, 0xE0, 0x35, 0xE7, 0xD1, 0x26, 0x12, 0xA9, - 0x85, 0xC0, 0xBD, 0xAD, 0x30, 0x5F, 0xDB, 0x01, 0x0F, 0xB6, 0x12, 0x79, - 0x5C, 0x03, 0xDC, 0x10, 0x76, 0xE3, 0x53, 0x0D, 0xE7, 0xDA, 0x62, 0x79, - 0xED, 0x02, 0x99, 0x85, 0x12, 0xC6, 0x9B, 0xE1, 0xA0, 0x36, 0xF7, 0x96, - 0x86, 0x44, 0x06, 0x14, 0x40, 0x22, 0xA3, 0x2B, 0x94, 0x44, 0xE2, 0x21, - 0xAE, 0x6D, 0x6F, 0xF5, 0xBB, 0xAF, 0x6D, 0xA5, 0xE7, 0x5C, 0x6F, 0x3D, - 0xBC, 0x9A, 0xC3, 0xEF, 0x30, 0xDE, 0x55, 0xA5, 0x42, 0x1A, 0xB8, 0xB3, - 0xC4, 0xE4, 0x71, 0x21, 0xF0, 0x40, 0x48, 0x12, 0x7C, 0x05, 0x38, 0x56, - 0x24, 0x52, 0x41, 0x9C, 0x39, 0x6E, 0x07, 0x5A, 0xA3, 0x34, 0xEC, 0x31, - 0x3A, 0x19, 0x1F, 0x58, 0x62, 0xF2, 0xB8, 0x0E, 0xF8, 0x63, 0xC8, 0xBB, - 0xC6, 0xC4, 0xE0, 0x8C, 0x62, 0x94, 0xBA, 0xAE, 0x44, 0x02, 0x09, 0x13, - 0xDB, 0x71, 0x80, 0x90, 0x6A, 0x93, 0x36, 0x4A, 0x22, 0x85, 0x68, 0x22, - 0xA3, 0x91, 0x6A, 0x48, 0xC5, 0xEC, 0xC2, 0xA5, 0xEA, 0x1E, 0x92, 0x40, - 0x3E, 0x6E, 0x57, 0xF4, 0x21, 0x62, 0xCF, 0x01, 0xEF, 0x04, 0x95, 0x8F, - 0x05, 0x3C, 0xA9, 0x27, 0x70, 0x56, 0xDE, 0x86, 0x87, 0xF7, 0x8B, 0x11, - 0xAE, 0x68, 0xD7, 0x3C, 0x4C, 0x89, 0xE6, 0xBB, 0x2C, 0x09, 0xDE, 0x00, - 0xDC, 0x0F, 0xFC, 0x1D, 0x98, 0x95, 0xA7, 0xAF, 0x2F, 0x88, 0x44, 0xAA, - 0x64, 0xD9, 0x94, 0x75, 0x32, 0xFE, 0x6B, 0xCC, 0x99, 0x52, 0x18, 0x53, - 0xCC, 0x1B, 0xC0, 0xD1, 0x22, 0x91, 0x0A, 0x74, 0xB6, 0x26, 0x12, 0xA9, - 0x79, 0x21, 0xB4, 0x90, 0x42, 0xCD, 0x3B, 0xBF, 0x2B, 0xC5, 0x38, 0x6D, - 0x52, 0x3B, 0x4A, 0x20, 0xD5, 0x70, 0xC2, 0xD5, 0xE3, 0x01, 0x18, 0x1F, - 0x83, 0x53, 0xD2, 0x26, 0x7B, 0x44, 0xBB, 0x42, 0x35, 0xC0, 0xFA, 0x42, - 0x4C, 0x5B, 0xAE, 0xF5, 0x5C, 0x4C, 0x10, 0x4F, 0x4B, 0xA8, 0xD1, 0xF0, - 0x0B, 0xFB, 0x01, 0xB4, 0x35, 0x12, 0xF9, 0x04, 0xA9, 0x06, 0x02, 0xFF, - 0x06, 0x36, 0x8B, 0x40, 0x22, 0x63, 0x90, 0xEA, 0x78, 0x7C, 0x6F, 0x42, - 0xB9, 0x5F, 0x45, 0x83, 0x87, 0x09, 0xB6, 0x0A, 0x82, 0x45, 0xD5, 0x30, - 0xA5, 0xBE, 0x1D, 0x2D, 0x5C, 0x71, 0xF9, 0xAB, 0x5A, 0x27, 0xE3, 0x7F, - 0xB2, 0x82, 0x37, 0x1B, 0x33, 0x30, 0x36, 0xF9, 0x37, 0xED, 0xC6, 0x68, - 0x2E, 0x26, 0x80, 0xB0, 0x3B, 0xE6, 0x70, 0xFB, 0x70, 0xE0, 0x74, 0xC2, - 0x39, 0x83, 0x9C, 0x02, 0xDC, 0x96, 0x7B, 0x22, 0xF4, 0x0E, 0x04, 0x77, - 0x17, 0xBF, 0x01, 0xB8, 0xAE, 0x39, 0x73, 0x8F, 0x4E, 0xC6, 0xB7, 0xC4, - 0x9C, 0xBF, 0x9C, 0x82, 0xF1, 0x32, 0x12, 0x98, 0x43, 0xDA, 0x52, 0x91, - 0xC7, 0x65, 0xC0, 0x4D, 0x21, 0x6F, 0x7B, 0x17, 0x18, 0x22, 0x12, 0xA9, - 0xB0, 0x1A, 0xC5, 0xAD, 0xC0, 0x45, 0x40, 0xB7, 0xAC, 0x9F, 0x2D, 0x00, - 0x9E, 0xC3, 0x78, 0x67, 0xBD, 0x8F, 0x31, 0xB5, 0x2E, 0xC7, 0xB8, 0x9D, - 0xF7, 0xC6, 0xC4, 0xF5, 0x9C, 0x05, 0xEC, 0x10, 0xE2, 0x39, 0x87, 0xE9, - 0x64, 0x7C, 0x53, 0x91, 0x48, 0x7D, 0x57, 0xAC, 0x71, 0xEA, 0x5E, 0x3B, - 0x4A, 0xCC, 0xD5, 0xFA, 0x36, 0xE0, 0x92, 0x90, 0xB7, 0xFE, 0x53, 0x80, - 0x57, 0xA6, 0xB2, 0xD2, 0xF3, 0x80, 0xA7, 0xB2, 0xFE, 0xFF, 0xA3, 0x80, - 0xE3, 0x38, 0x9E, 0xFC, 0xE1, 0x1A, 0x6B, 0xA5, 0x88, 0x17, 0x59, 0x6A, - 0xD9, 0x28, 0x4C, 0x4D, 0x90, 0x20, 0x98, 0x56, 0x05, 0xBB, 0x37, 0xF8, - 0x5E, 0xDB, 0x94, 0x49, 0x52, 0xED, 0x8E, 0xF1, 0x67, 0xDE, 0x34, 0xC2, - 0xDD, 0x2B, 0x31, 0x35, 0xE0, 0x9F, 0x2B, 0x57, 0xF7, 0x3B, 0x4B, 0x15, - 0x5B, 0x65, 0x3E, 0xB6, 0xA0, 0xE7, 0x51, 0xE3, 0xF1, 0xBD, 0xB2, 0x3B, - 0x03, 0xE8, 0x64, 0x7C, 0x45, 0x40, 0xD2, 0x9B, 0x25, 0x12, 0xA9, 0xED, - 0x5B, 0x6C, 0x6F, 0x44, 0x7F, 0x41, 0xBA, 0x6E, 0x12, 0x26, 0x40, 0x6F, - 0x22, 0x30, 0x1C, 0x51, 0x95, 0x12, 0x97, 0xBF, 0xA2, 0x5B, 0xE8, 0xC7, - 0x66, 0xC0, 0x68, 0xE0, 0xA7, 0x21, 0x4C, 0x48, 0x9B, 0x89, 0x44, 0x6A, - 0x4E, 0x8E, 0xB6, 0x0E, 0x26, 0x98, 0x77, 0xD4, 0x1C, 0x44, 0xF5, 0xA6, - 0xE2, 0xF2, 0x97, 0x75, 0x88, 0xF1, 0xDA, 0x1B, 0x38, 0x03, 0x11, 0xBB, - 0x34, 0x97, 0x6B, 0x6C, 0x8E, 0xEB, 0xBF, 0x24, 0x98, 0x67, 0xDA, 0x52, - 0x84, 0xD8, 0x10, 0xAD, 0x13, 0x40, 0xD8, 0x02, 0x5B, 0x1F, 0x01, 0x03, - 0xA2, 0x9E, 0xC7, 0xE8, 0x64, 0x7C, 0x04, 0xA6, 0x44, 0xF4, 0x27, 0xC0, - 0x8D, 0xC0, 0xE8, 0x96, 0x32, 0x02, 0xE8, 0x64, 0xBC, 0x13, 0x70, 0x33, - 0xA6, 0x0E, 0x4F, 0x60, 0xD2, 0x17, 0x89, 0xD4, 0x13, 0xCD, 0xC8, 0x80, - 0xA0, 0xF3, 0x30, 0x73, 0x03, 0x21, 0x76, 0x5C, 0xAA, 0xF5, 0x5D, 0x96, - 0xFC, 0xC2, 0xE0, 0x55, 0x01, 0x43, 0xB4, 0xEF, 0x2D, 0x0D, 0x29, 0x9F, - 0x7E, 0x44, 0xF0, 0x33, 0xBE, 0xC7, 0xF0, 0xBD, 0xD3, 0x02, 0xB6, 0xFB, - 0x67, 0xE0, 0xDC, 0x00, 0x57, 0xEE, 0x80, 0xEF, 0xCD, 0x0C, 0x63, 0xC2, - 0xC2, 0xEE, 0x02, 0x82, 0x62, 0xA7, 0x06, 0x38, 0xA3, 0xCD, 0x6E, 0x5F, - 0x4D, 0x75, 0xBE, 0x01, 0x40, 0x94, 0x1D, 0x4A, 0x17, 0x60, 0x2C, 0x52, - 0xFD, 0xAC, 0x5C, 0xDD, 0x5F, 0x05, 0x27, 0x12, 0xCE, 0x99, 0x61, 0x3C, - 0xED, 0x10, 0xE2, 0x0F, 0x2F, 0x69, 0xE0, 0x4A, 0xE0, 0x28, 0x91, 0x48, - 0x0D, 0x11, 0x89, 0xD4, 0x6B, 0x2D, 0x91, 0x87, 0x35, 0xA7, 0xFC, 0x0F, - 0x13, 0x5B, 0x11, 0x54, 0x08, 0xC6, 0x80, 0xFD, 0xF2, 0xFC, 0x2E, 0xE8, - 0x99, 0xC1, 0xC6, 0xE8, 0xFA, 0xC3, 0x43, 0xBD, 0x5F, 0x22, 0xF5, 0xBE, - 0x48, 0xA4, 0x2E, 0x09, 0x42, 0x1E, 0xE1, 0x25, 0xB9, 0xFE, 0x63, 0x04, - 0xF2, 0xF8, 0x0C, 0x18, 0x54, 0xE0, 0x61, 0xFE, 0xAD, 0xC0, 0xE5, 0x20, - 0xFA, 0x88, 0x44, 0x6A, 0x64, 0x90, 0x74, 0x32, 0x22, 0x91, 0x5A, 0x8D, - 0xA8, 0xBA, 0x04, 0x08, 0x53, 0x83, 0x68, 0xDF, 0x22, 0x8D, 0x54, 0x6C, - 0xA9, 0xD6, 0xF7, 0x45, 0x20, 0x8F, 0x37, 0x05, 0x1C, 0x15, 0x9A, 0x3C, - 0xDA, 0x18, 0x62, 0x59, 0xAA, 0xC8, 0x38, 0xC2, 0xC5, 0x83, 0xDC, 0x28, - 0xA4, 0xEA, 0xD5, 0x66, 0xDF, 0xDC, 0x90, 0xC8, 0xC0, 0x88, 0x24, 0xD2, - 0xB9, 0x5C, 0x24, 0x22, 0xA4, 0xDA, 0x88, 0x70, 0xD1, 0xA5, 0x75, 0x02, - 0xC6, 0xB6, 0xD7, 0x05, 0x2C, 0x12, 0xA9, 0xE7, 0x45, 0x22, 0x35, 0x21, - 0xC2, 0x7D, 0x73, 0x31, 0x9E, 0x53, 0x41, 0x91, 0xCF, 0x7D, 0x3D, 0x0C, - 0x09, 0x4D, 0xD4, 0xC9, 0xF8, 0xCB, 0x3A, 0x19, 0xFF, 0x83, 0x4E, 0xC6, - 0x07, 0xE9, 0x64, 0x7C, 0x0B, 0x9D, 0xEC, 0x57, 0x55, 0x86, 0x61, 0xDB, - 0x00, 0x08, 0x1B, 0xC4, 0x36, 0x03, 0x38, 0x4C, 0x24, 0x52, 0xDF, 0x16, - 0x38, 0x5F, 0xDF, 0x8B, 0x44, 0x6A, 0x44, 0x33, 0x2E, 0xBF, 0xB9, 0xEF, - 0x33, 0x1B, 0x83, 0x5B, 0x43, 0xDC, 0xB2, 0x4B, 0x91, 0xC6, 0x6A, 0x5B, - 0xE0, 0xBC, 0x90, 0xF7, 0xBC, 0x67, 0x35, 0x8F, 0xC5, 0xB4, 0x73, 0xAC, - 0x89, 0xB6, 0xD5, 0xBE, 0x37, 0x1F, 0xA9, 0xC6, 0x61, 0x6C, 0xEB, 0x41, - 0xD0, 0x4B, 0xC3, 0x5F, 0xD6, 0xAB, 0x55, 0x3F, 0x5F, 0x71, 0x8E, 0xD7, - 0x36, 0x7D, 0x9A, 0x7D, 0x6F, 0x8A, 0x3D, 0x13, 0x79, 0x91, 0xF0, 0xC9, - 0x06, 0x33, 0x9A, 0xC8, 0xB1, 0xF8, 0x5E, 0xAB, 0x44, 0x08, 0x77, 0xAF, - 0x1D, 0x25, 0x16, 0x6B, 0xFD, 0x00, 0xE1, 0x52, 0xB4, 0x8C, 0xD7, 0xBE, - 0x37, 0x87, 0x0E, 0x08, 0x3D, 0xFC, 0x20, 0x81, 0x4E, 0x77, 0x26, 0x7F, - 0xAA, 0x9E, 0x30, 0xCE, 0x23, 0x5B, 0xE6, 0xF9, 0xF9, 0x34, 0x8C, 0x59, - 0x33, 0x88, 0x69, 0x4E, 0xD0, 0x98, 0xAF, 0x2B, 0xD3, 0xCB, 0x65, 0x3A, - 0x19, 0x9F, 0x66, 0x77, 0xF7, 0x53, 0x80, 0x49, 0xC0, 0x9B, 0x22, 0x91, - 0x5A, 0x54, 0x41, 0x43, 0x39, 0xDB, 0x92, 0xC7, 0x57, 0xAD, 0x30, 0x5F, - 0xD5, 0x76, 0x83, 0x96, 0x0B, 0x61, 0x92, 0x87, 0x6E, 0x5E, 0xA6, 0xB1, - 0xFA, 0x48, 0xC0, 0x11, 0x15, 0x52, 0x74, 0xAE, 0xF5, 0x08, 0xC4, 0xE2, - 0xEE, 0x10, 0x04, 0x02, 0x70, 0xD4, 0x0A, 0xCD, 0x0D, 0xC0, 0x15, 0x6D, - 0x58, 0x13, 0x99, 0x92, 0x75, 0xB0, 0x1E, 0x85, 0x44, 0x9E, 0x6E, 0x2D, - 0x12, 0x59, 0xAC, 0xF5, 0x95, 0x21, 0xE7, 0x27, 0x33, 0xA7, 0xED, 0x9F, - 0x2C, 0x92, 0xF1, 0xDD, 0x30, 0x07, 0xE4, 0xFB, 0x01, 0x3F, 0x04, 0x36, - 0x47, 0xA7, 0x37, 0x26, 0x5C, 0x34, 0x75, 0xB3, 0xFC, 0x9D, 0x67, 0x47, - 0xBD, 0x42, 0x27, 0xE3, 0x13, 0x31, 0x39, 0xB9, 0xA2, 0xA0, 0x2B, 0x26, - 0xF9, 0xE3, 0xDE, 0xD9, 0x5A, 0xA3, 0x4E, 0xC6, 0x5F, 0xC7, 0x44, 0x83, - 0x3F, 0x21, 0x12, 0xA9, 0xE5, 0x65, 0x1E, 0xDE, 0x53, 0x45, 0x22, 0x35, - 0xAB, 0xC8, 0xF3, 0xB5, 0x29, 0x30, 0x08, 0x53, 0x66, 0xB5, 0x0F, 0xD0, - 0x1B, 0x9D, 0xDE, 0x84, 0xE0, 0x85, 0xED, 0x82, 0x7C, 0x9B, 0xAD, 0x8D, - 0x34, 0x70, 0x7C, 0x47, 0xDA, 0xB0, 0xAD, 0x15, 0x30, 0xB4, 0xB1, 0x10, - 0xAF, 0x03, 0xAF, 0x87, 0x6C, 0x23, 0x81, 0x54, 0xBF, 0x6F, 0xD3, 0xA3, - 0xE0, 0x7B, 0x1F, 0x63, 0xCC, 0x59, 0x73, 0x23, 0x2E, 0xD4, 0xB1, 0x25, - 0x77, 0xF1, 0x35, 0x29, 0x13, 0xC2, 0xC6, 0x3E, 0xBC, 0xD5, 0x53, 0x88, - 0x97, 0xDB, 0x2F, 0x69, 0xF4, 0xAB, 0xD2, 0xC9, 0xF8, 0x99, 0x3A, 0x19, - 0xFF, 0x00, 0x73, 0x28, 0x7B, 0x07, 0xC6, 0xA5, 0x76, 0x1F, 0x8C, 0x97, - 0x5D, 0x4D, 0x11, 0x1F, 0xD7, 0x9C, 0x8B, 0xEB, 0x70, 0x2B, 0x3C, 0x8A, - 0x85, 0x1A, 0x4C, 0x96, 0x5A, 0x09, 0xCC, 0xD0, 0xC9, 0xF8, 0xE9, 0x65, - 0x1E, 0xEA, 0xC8, 0xB9, 0xC1, 0x72, 0x10, 0x47, 0x3F, 0x9D, 0x8C, 0x4F, - 0xC0, 0x14, 0x80, 0xFB, 0x2B, 0x70, 0x01, 0xC6, 0x99, 0x61, 0xEB, 0x22, - 0x92, 0x47, 0x39, 0xE5, 0xE9, 0x75, 0x3D, 0x6A, 0x47, 0x09, 0x3A, 0x08, - 0xD6, 0x22, 0x90, 0x79, 0x26, 0xBC, 0xFE, 0xAA, 0x08, 0xED, 0x0C, 0x47, - 0xAA, 0xCB, 0xDA, 0x01, 0x89, 0x1C, 0x4E, 0xF0, 0x32, 0xB1, 0xD9, 0x58, - 0xCF, 0x6A, 0x22, 0xE7, 0x15, 0xBB, 0x5B, 0x55, 0x52, 0x55, 0x23, 0xD5, - 0xAD, 0x98, 0x73, 0x8F, 0xB0, 0x0B, 0xF3, 0xBA, 0xF9, 0xED, 0x24, 0x65, - 0x42, 0x0E, 0x41, 0xB4, 0x23, 0xE8, 0xB7, 0x30, 0xE7, 0x18, 0x7B, 0x95, - 0xB3, 0x2F, 0x22, 0x91, 0x7A, 0x1B, 0xB8, 0xAE, 0x44, 0xCD, 0x6F, 0x06, - 0x3C, 0xA2, 0x93, 0x71, 0x59, 0xA6, 0xF3, 0x12, 0x0C, 0x99, 0xE9, 0x11, - 0x05, 0xCE, 0x57, 0x57, 0x9D, 0x8C, 0x3F, 0x82, 0x09, 0x3E, 0x1C, 0x4C, - 0x69, 0xA3, 0xF6, 0xCB, 0x89, 0x13, 0x17, 0x6A, 0xFD, 0x9B, 0x0E, 0x49, - 0x20, 0x56, 0x90, 0xBE, 0x0C, 0x8C, 0x89, 0xB0, 0x3B, 0x1B, 0x81, 0x54, - 0xBF, 0x6B, 0xD3, 0xA3, 0xE1, 0x7B, 0x93, 0x81, 0xC3, 0x22, 0x92, 0x48, - 0x0D, 0x70, 0x3F, 0x52, 0x3D, 0x25, 0xA4, 0x2A, 0x4E, 0xF1, 0x26, 0xA9, - 0x76, 0x69, 0x30, 0x2E, 0xA2, 0x97, 0x46, 0x20, 0x8F, 0x7F, 0x55, 0x42, - 0xBC, 0x4A, 0x89, 0xC8, 0x63, 0x37, 0x4C, 0x20, 0x5B, 0xDF, 0x4A, 0xE9, - 0x93, 0x48, 0xA4, 0xFE, 0x88, 0x39, 0x98, 0x2E, 0x55, 0xA4, 0xF1, 0x39, - 0xA0, 0xAF, 0x2B, 0xE3, 0x2B, 0xFE, 0xC6, 0x66, 0xE8, 0x8D, 0x32, 0x5F, - 0xEB, 0x63, 0xDC, 0xAC, 0x4F, 0x8F, 0xB0, 0x8E, 0xDB, 0x22, 0x46, 0x20, - 0xD5, 0x21, 0x1D, 0x93, 0x40, 0x0C, 0x2E, 0x21, 0x7C, 0xF2, 0x33, 0x01, - 0xDC, 0xD4, 0x0E, 0x48, 0xE4, 0xBD, 0x02, 0x48, 0x04, 0xE0, 0x44, 0x0D, - 0x9F, 0x21, 0xD5, 0xEF, 0x6D, 0xB4, 0x78, 0x14, 0xE2, 0xD8, 0xDC, 0x6A, - 0x1D, 0x1F, 0x10, 0x3C, 0x56, 0x21, 0x1B, 0x2B, 0x08, 0xEF, 0x76, 0xD8, - 0x56, 0xC8, 0xA3, 0x13, 0x26, 0x86, 0xA3, 0xE2, 0xB2, 0x21, 0x88, 0x44, - 0xEA, 0x7A, 0xE0, 0x40, 0x4C, 0x7E, 0xA8, 0x52, 0x68, 0x7E, 0xBF, 0xB7, - 0xE4, 0x59, 0x96, 0xD7, 0x03, 0x6A, 0x75, 0x32, 0xBE, 0x47, 0x84, 0x7B, - 0x6F, 0xC1, 0x04, 0x45, 0x76, 0x14, 0x54, 0x03, 0x4F, 0x21, 0xD5, 0x56, - 0x1D, 0xE1, 0x45, 0x73, 0x09, 0xD1, 0xAF, 0x90, 0xEA, 0x22, 0xE0, 0xF1, - 0x88, 0x24, 0xA2, 0xF1, 0xBD, 0x5B, 0xDB, 0xEC, 0xA8, 0xF8, 0xDE, 0x7B, - 0x48, 0x75, 0x38, 0x26, 0x05, 0x75, 0xCF, 0x08, 0x2D, 0x6C, 0x04, 0x8C, - 0xD0, 0x70, 0x05, 0x52, 0xFD, 0x0D, 0x18, 0x2B, 0xE0, 0x35, 0xED, 0x7B, - 0x79, 0x0F, 0x43, 0x85, 0x54, 0x1B, 0x69, 0x73, 0x0E, 0xE3, 0x61, 0xD2, - 0x53, 0x77, 0x2E, 0xE0, 0x0D, 0x12, 0xB6, 0x26, 0x4A, 0x7B, 0xC4, 0x99, - 0x84, 0xAB, 0xB3, 0xB1, 0x18, 0xE3, 0xD9, 0xF4, 0x55, 0x0E, 0xED, 0xA0, - 0xB7, 0xDD, 0x2C, 0x14, 0x93, 0x44, 0x26, 0x01, 0x87, 0xEB, 0x64, 0x7C, - 0x17, 0x3B, 0x97, 0x87, 0x5B, 0x4D, 0x69, 0xBD, 0x22, 0x34, 0x5F, 0x85, - 0x71, 0x29, 0xFD, 0x75, 0x99, 0xC6, 0x7E, 0x03, 0x60, 0xB4, 0x4E, 0xC6, - 0xF7, 0x0F, 0xEA, 0x25, 0x66, 0x4C, 0x8D, 0x81, 0x82, 0xD7, 0x32, 0xA8, - 0x03, 0x26, 0x63, 0x6A, 0xBD, 0xAC, 0xCC, 0x21, 0x5F, 0x4E, 0xA7, 0x2D, - 0xD4, 0x24, 0x32, 0x41, 0xCA, 0xA3, 0x62, 0x52, 0x1D, 0x9C, 0xF6, 0xBD, - 0xD5, 0x1D, 0x8B, 0x40, 0x8C, 0x10, 0x1D, 0x89, 0x54, 0x07, 0x85, 0x9C, - 0xFC, 0xCC, 0x24, 0xDF, 0x6C, 0x49, 0xE4, 0xB6, 0x36, 0x4C, 0x22, 0xEF, - 0x5A, 0x12, 0xF9, 0x27, 0xD1, 0x8B, 0xDF, 0x6C, 0x68, 0xC7, 0xEF, 0x5C, - 0x0D, 0x75, 0x48, 0xF5, 0x89, 0xFD, 0x30, 0x16, 0x58, 0x61, 0x56, 0x63, - 0x77, 0xD2, 0xBB, 0x6A, 0x93, 0x62, 0xA0, 0x18, 0x76, 0xE1, 0xA7, 0x37, - 0x14, 0xE2, 0xAE, 0x45, 0xB4, 0x5B, 0x9C, 0x16, 0xF0, 0xBA, 0x34, 0xC6, - 0x3B, 0xF0, 0x8E, 0x7C, 0xC1, 0x6A, 0x3A, 0x19, 0x1F, 0x52, 0x6C, 0x02, - 0xC9, 0x22, 0x92, 0xA9, 0x98, 0x73, 0x91, 0xEB, 0x6C, 0x85, 0xC2, 0xDD, - 0x2C, 0xF1, 0x6D, 0x0F, 0xEC, 0x8C, 0x89, 0x2B, 0xD9, 0x05, 0xE3, 0xF9, - 0x17, 0xC6, 0xAC, 0x73, 0x78, 0x99, 0xC7, 0x7F, 0x17, 0xE0, 0x11, 0x3D, - 0xFC, 0xA0, 0xE3, 0x03, 0x06, 0x38, 0x9E, 0x1A, 0x62, 0x5D, 0x8F, 0x02, - 0x86, 0xE5, 0x4B, 0x41, 0x62, 0xCE, 0x80, 0xF4, 0xA9, 0x6D, 0x84, 0x40, - 0x00, 0x0E, 0x48, 0x1B, 0xC7, 0x8E, 0x0B, 0x3A, 0x1E, 0x81, 0x98, 0x59, - 0x1A, 0x96, 0x36, 0x8B, 0xFD, 0x90, 0x08, 0x24, 0x72, 0x0B, 0x52, 0xD1, - 0x0E, 0x48, 0xE4, 0xB0, 0x02, 0x49, 0x24, 0x83, 0x1A, 0x8C, 0xBB, 0x62, - 0x9F, 0x12, 0xF6, 0xF8, 0x7D, 0x01, 0xA7, 0x2D, 0x6A, 0xBF, 0x07, 0xE7, - 0x9D, 0x09, 0x5E, 0x94, 0xE8, 0x71, 0x91, 0x48, 0x8D, 0xA8, 0x84, 0x7E, - 0x8B, 0x44, 0xAA, 0x0E, 0x13, 0xC3, 0xF0, 0x61, 0x8E, 0x77, 0xEA, 0x0D, - 0x1C, 0x61, 0xC9, 0x6E, 0xDB, 0x00, 0xCD, 0x6D, 0xA3, 0x93, 0xFD, 0xAA, - 0xC3, 0x06, 0xE2, 0x35, 0x83, 0xAF, 0x31, 0xF5, 0x72, 0xF6, 0x0B, 0x71, - 0xCF, 0x31, 0xE8, 0xF4, 0xE5, 0x98, 0x54, 0x24, 0x2D, 0xE1, 0xC0, 0x80, - 0x6D, 0xCE, 0xC6, 0xB8, 0x0B, 0x57, 0xF2, 0x6E, 0xFD, 0x33, 0x4B, 0x5E, - 0x3B, 0x87, 0xB8, 0xE7, 0x7C, 0xA4, 0x7A, 0x1B, 0xDF, 0x7B, 0xA4, 0x0D, - 0x7D, 0x6A, 0x81, 0xF3, 0xC4, 0x35, 0xCB, 0xE4, 0x69, 0xDF, 0x5B, 0x2D, - 0x8C, 0x7F, 0xFB, 0x7B, 0x51, 0xBE, 0x1B, 0x4B, 0x22, 0x6D, 0xDB, 0x23, - 0xC1, 0xF7, 0xDE, 0xB5, 0xBB, 0xBE, 0x05, 0x15, 0xDE, 0xD3, 0xE9, 0xC0, - 0xE0, 0x76, 0x9E, 0x3A, 0xE1, 0x07, 0x04, 0x77, 0xCD, 0x7D, 0x29, 0xC0, - 0x35, 0x9B, 0x15, 0xAB, 0x63, 0x7A, 0xC4, 0x21, 0x42, 0x27, 0xE3, 0x23, - 0x75, 0x32, 0x3E, 0x50, 0x8F, 0x38, 0x24, 0xB0, 0x46, 0x21, 0x12, 0xA9, - 0x2F, 0x45, 0x22, 0x25, 0x31, 0x09, 0x14, 0x83, 0xA0, 0x33, 0xE8, 0x62, - 0x79, 0x30, 0x7D, 0x87, 0x49, 0xE9, 0x73, 0x0C, 0xE1, 0xB3, 0x54, 0x5F, - 0xAB, 0x93, 0xF1, 0x20, 0xDA, 0x50, 0xD0, 0xA2, 0x67, 0x6F, 0xB6, 0x4C, - 0x1E, 0x7A, 0x73, 0xCA, 0xE7, 0xBD, 0x35, 0xDD, 0x8E, 0xD5, 0x71, 0xC0, - 0x92, 0x90, 0xF7, 0xDE, 0x87, 0x54, 0xFB, 0x56, 0xC0, 0xF7, 0xB3, 0x32, - 0xE0, 0x75, 0x41, 0x73, 0x22, 0xB6, 0xAC, 0x0A, 0x6A, 0xDF, 0x5B, 0x28, - 0x4C, 0xC0, 0xCF, 0x07, 0x11, 0x49, 0xE4, 0xD6, 0x76, 0x40, 0x22, 0xEF, - 0x10, 0xDD, 0xC5, 0xB7, 0xB5, 0x76, 0x46, 0x03, 0xF1, 0xBD, 0x6F, 0x69, - 0xDF, 0x08, 0x23, 0x3C, 0x36, 0x6F, 0x41, 0x9B, 0x59, 0x1F, 0x28, 0x9E, - 0xEB, 0x79, 0xBA, 0xFE, 0x50, 0x4C, 0x96, 0xEA, 0x7F, 0x91, 0xAE, 0x9F, - 0xA4, 0x93, 0xF1, 0x5F, 0x85, 0xAC, 0x8E, 0xD7, 0x23, 0xE0, 0x75, 0xAB, - 0x41, 0x14, 0xC3, 0xD3, 0xEB, 0x7B, 0x60, 0xA0, 0x48, 0xA4, 0x3E, 0x17, - 0x89, 0xD4, 0xD7, 0xC0, 0x50, 0xD3, 0x76, 0xA8, 0xB9, 0x78, 0x42, 0x27, - 0xE3, 0x2D, 0x69, 0x4D, 0x41, 0x2B, 0x34, 0x06, 0x49, 0x6C, 0xFA, 0x47, - 0xCA, 0x63, 0xBE, 0x9A, 0x01, 0x0C, 0xC0, 0xF7, 0xBE, 0xC5, 0xF7, 0xA6, - 0x60, 0xF2, 0x00, 0x86, 0xD1, 0xF2, 0xD7, 0x03, 0x46, 0x0B, 0xA9, 0x36, - 0x2E, 0xF3, 0xF7, 0x13, 0xB4, 0xA8, 0xDC, 0xEF, 0x90, 0xEA, 0x17, 0xD9, - 0x3F, 0xA8, 0x96, 0xAA, 0x0A, 0xA9, 0xBC, 0xA6, 0xA5, 0x3C, 0x02, 0x4D, - 0x86, 0xF6, 0xBD, 0xB9, 0xC2, 0xD8, 0x8A, 0x3F, 0x8C, 0xD0, 0xE9, 0x0C, - 0x89, 0x5C, 0x42, 0x5B, 0x86, 0x21, 0x91, 0x7E, 0x98, 0x9A, 0x0D, 0x95, - 0x84, 0x37, 0x04, 0x1C, 0x84, 0xEF, 0xCD, 0xA6, 0xFD, 0x63, 0x61, 0x88, - 0x0F, 0xF7, 0x02, 0x9B, 0x75, 0x37, 0x17, 0x79, 0x74, 0x03, 0xFE, 0x46, - 0xF1, 0xF2, 0x25, 0xC1, 0xDA, 0x69, 0xBE, 0xFB, 0x62, 0x8A, 0x33, 0x7D, - 0xAB, 0x93, 0xF1, 0x37, 0x75, 0x32, 0x7E, 0xB3, 0x4E, 0xC6, 0x4F, 0xD5, - 0xC9, 0xF8, 0xFE, 0x3A, 0x19, 0xDF, 0x5C, 0x27, 0xE3, 0x1B, 0xD8, 0xB8, - 0x88, 0x5E, 0xF6, 0x67, 0xBF, 0x07, 0x9E, 0x09, 0x2A, 0xCC, 0x8A, 0x60, - 0xBE, 0xAA, 0x03, 0x0E, 0x15, 0x89, 0xD4, 0x94, 0x2C, 0x4D, 0x28, 0x45, - 0xF8, 0x54, 0xE5, 0x1B, 0x63, 0x0E, 0xD5, 0x9B, 0x73, 0x10, 0x08, 0x1A, - 0x95, 0xDD, 0x2F, 0x5F, 0x21, 0x28, 0x3D, 0xFC, 0x20, 0xA1, 0x93, 0xF1, - 0x2B, 0x80, 0xB3, 0xCB, 0xB0, 0xE6, 0x16, 0x5B, 0xF2, 0xF8, 0x32, 0x4B, - 0x16, 0x8C, 0x05, 0x92, 0x21, 0xDB, 0xD9, 0x4E, 0xC3, 0xC8, 0x1A, 0xA9, - 0xCA, 0x19, 0xFF, 0x12, 0x34, 0x75, 0x4F, 0x27, 0x60, 0x24, 0x52, 0x7D, - 0x8A, 0x54, 0x63, 0x90, 0x6A, 0x62, 0xBD, 0x31, 0x75, 0xFE, 0x4D, 0x37, - 0xC9, 0x84, 0x11, 0x38, 0xBA, 0x54, 0xFB, 0xDE, 0x1C, 0x21, 0xD5, 0xA1, - 0x1A, 0xFE, 0x45, 0xF8, 0xC0, 0x2D, 0x01, 0xDC, 0x66, 0x0F, 0xD6, 0xEF, - 0x6C, 0xB3, 0xE2, 0xCB, 0xF7, 0x3E, 0x11, 0x52, 0xFD, 0x58, 0x1B, 0xEF, - 0xB4, 0x41, 0x15, 0xD0, 0x23, 0x19, 0x83, 0x61, 0x65, 0xAA, 0x35, 0xD0, - 0xEA, 0x10, 0x89, 0xD4, 0x42, 0x9D, 0x8C, 0xCF, 0xC2, 0x1C, 0x44, 0xB7, - 0x84, 0xAD, 0x80, 0x77, 0x74, 0x32, 0x7E, 0x0B, 0x90, 0xC2, 0xD4, 0x37, - 0xE8, 0x09, 0xF4, 0xC7, 0x78, 0x32, 0x15, 0xCD, 0xC5, 0xD2, 0x7A, 0x5C, - 0x1D, 0x91, 0x67, 0x97, 0xBE, 0xBF, 0xFD, 0xB3, 0xD6, 0x2D, 0xF6, 0x4F, - 0x94, 0xDD, 0x74, 0x31, 0x62, 0x7B, 0x56, 0x21, 0xC4, 0x94, 0x1C, 0xE3, - 0x7B, 0xBF, 0x4E, 0xC6, 0xFB, 0x86, 0x14, 0xD4, 0x7D, 0x81, 0x7B, 0x9A, - 0xB9, 0x67, 0x72, 0x8E, 0xF7, 0xCF, 0x67, 0x0D, 0x99, 0xA0, 0x93, 0xF1, - 0x7B, 0x31, 0x89, 0x5D, 0xBF, 0xB7, 0x3B, 0xF7, 0x7D, 0xD1, 0xE9, 0x73, - 0x81, 0x1F, 0x97, 0x69, 0xD9, 0xCD, 0xCD, 0xB5, 0x39, 0xAB, 0x81, 0xAB, - 0xEB, 0x4C, 0xFA, 0x99, 0xC1, 0x21, 0xDA, 0x1A, 0x54, 0x67, 0xB4, 0xA8, - 0xAB, 0xCB, 0xF2, 0xFD, 0xC0, 0xDB, 0xDA, 0x38, 0x97, 0x04, 0x5D, 0x77, - 0xBB, 0xDA, 0x3F, 0xD9, 0xF8, 0x05, 0x70, 0x5F, 0x28, 0x0D, 0x64, 0x2D, - 0x12, 0x81, 0x43, 0x31, 0x35, 0x01, 0xA2, 0x68, 0x22, 0xB7, 0x23, 0xD5, - 0xC5, 0x6D, 0x59, 0x88, 0x69, 0xDF, 0x9B, 0xDB, 0xD9, 0x2C, 0x9A, 0xDF, - 0x00, 0xCB, 0xCA, 0xD4, 0x8D, 0x39, 0x80, 0x87, 0xEF, 0x9D, 0xDB, 0x51, - 0xC8, 0x23, 0x0B, 0x7F, 0x0F, 0x71, 0xED, 0x96, 0x98, 0x52, 0xAA, 0x93, - 0x80, 0x4F, 0x31, 0x69, 0x7A, 0xAE, 0x2F, 0x26, 0x79, 0x58, 0x5C, 0x1C, - 0xF2, 0x5B, 0x12, 0x11, 0xC9, 0x63, 0x15, 0xA6, 0x7A, 0x61, 0x29, 0x71, - 0x11, 0xE1, 0xEB, 0xCD, 0x9F, 0xA5, 0x93, 0xF1, 0x5F, 0x15, 0x61, 0xBE, - 0x3A, 0x63, 0x82, 0x66, 0x5F, 0xB1, 0xF3, 0xF5, 0x1E, 0xA6, 0x46, 0xFB, - 0x8F, 0x2B, 0x6D, 0x11, 0xD6, 0xF9, 0x5E, 0x5A, 0x18, 0x0F, 0xB3, 0xE9, - 0x21, 0x6F, 0xBD, 0x02, 0xA9, 0x8E, 0x2E, 0x93, 0xEC, 0xFA, 0xDE, 0x6E, - 0xA6, 0x0A, 0xC1, 0x4F, 0x90, 0x6A, 0xDB, 0x48, 0x04, 0x52, 0x24, 0x12, - 0xB9, 0xA3, 0xAD, 0x93, 0xC8, 0x2A, 0xDF, 0x4B, 0xE3, 0x7B, 0x77, 0x60, - 0x6A, 0x72, 0x8C, 0xA1, 0x34, 0x41, 0x63, 0xB9, 0xD0, 0x00, 0x3C, 0x20, - 0x60, 0x77, 0x7C, 0x6F, 0x14, 0x1D, 0x13, 0xF7, 0x5A, 0x13, 0x4C, 0x65, - 0x6C, 0x28, 0x92, 0xF1, 0x8D, 0x08, 0xEE, 0x5A, 0x5C, 0x28, 0x6E, 0x10, - 0x89, 0xD4, 0x17, 0x25, 0xD6, 0xF2, 0x4C, 0xC1, 0xB4, 0xF0, 0x87, 0xEA, - 0x77, 0xEA, 0x64, 0x3C, 0x87, 0xA6, 0x21, 0x9E, 0xB7, 0x64, 0xD0, 0xEE, - 0xA0, 0x7D, 0x6F, 0x01, 0xC6, 0xC9, 0x28, 0x8C, 0xE3, 0x4A, 0x0C, 0xF8, - 0x2B, 0x52, 0xED, 0x5C, 0xA6, 0x6E, 0x17, 0x5A, 0x49, 0x56, 0x00, 0x27, - 0x47, 0x26, 0x90, 0x0C, 0x93, 0x15, 0x81, 0x44, 0x86, 0xB5, 0xF9, 0x15, - 0xE4, 0x7B, 0xB3, 0xF0, 0xBD, 0x13, 0x30, 0xAE, 0x8A, 0xE3, 0x29, 0x6E, - 0x42, 0xBD, 0xB5, 0x36, 0x3C, 0x98, 0xCC, 0xAC, 0x7B, 0xE2, 0x7B, 0xE7, - 0x6B, 0xDF, 0x9B, 0x4B, 0x07, 0x85, 0x48, 0xA4, 0xA6, 0x11, 0xDE, 0xFE, - 0xDC, 0x1C, 0x66, 0x14, 0x78, 0xFF, 0x39, 0x98, 0x00, 0xBB, 0x52, 0x43, - 0x81, 0xB8, 0xB1, 0x95, 0xC6, 0xF8, 0x2B, 0x4C, 0xD1, 0xB2, 0x30, 0x44, - 0xDD, 0x19, 0x18, 0xA5, 0x93, 0xF1, 0x1F, 0xAC, 0xDD, 0xD6, 0x6B, 0x0D, - 0xC0, 0xF9, 0x14, 0x2F, 0xC5, 0xCB, 0xCC, 0x0A, 0x93, 0x01, 0x1F, 0x63, - 0x4A, 0xEE, 0x86, 0xD9, 0x44, 0xF6, 0xC0, 0x1C, 0xAA, 0x77, 0x6D, 0xED, - 0xEE, 0x76, 0x11, 0x3C, 0x4D, 0xB0, 0x0A, 0x9A, 0xCD, 0xA1, 0x30, 0x02, - 0xC9, 0x52, 0x87, 0x0E, 0x05, 0x3E, 0x8E, 0x48, 0x22, 0x77, 0xDA, 0x68, - 0xF7, 0xB6, 0x0F, 0xDF, 0x7B, 0xCB, 0x96, 0x8C, 0xDD, 0x0D, 0x18, 0x81, - 0xF1, 0x69, 0x2F, 0x06, 0xA6, 0x02, 0xFF, 0x07, 0x6C, 0x8F, 0xEF, 0x9D, - 0x8A, 0xEF, 0xB5, 0xCB, 0x9D, 0x5C, 0x84, 0xE5, 0x73, 0x2D, 0x6B, 0xD7, - 0x7A, 0x8E, 0x8A, 0xD1, 0xC0, 0xD3, 0x05, 0xB6, 0x71, 0x40, 0xA9, 0x37, - 0xBA, 0xC0, 0xED, 0x20, 0x4E, 0xB1, 0xC2, 0xB8, 0xB5, 0x88, 0xFA, 0x55, - 0x8C, 0x39, 0x29, 0x0C, 0x7A, 0x03, 0x4F, 0x35, 0xCD, 0xDC, 0x2B, 0x12, - 0xA9, 0x57, 0x80, 0x0B, 0x8B, 0xB0, 0xC1, 0xFA, 0x82, 0x4A, 0x2C, 0x1D, - 0x61, 0xAC, 0x01, 0x61, 0x63, 0x8E, 0xF6, 0xD4, 0x50, 0xBB, 0x49, 0x2B, - 0x67, 0xEE, 0x5D, 0x79, 0x8E, 0x97, 0x06, 0x4E, 0xB1, 0x63, 0x19, 0x15, - 0x7B, 0x22, 0xD5, 0x9E, 0x05, 0x11, 0x88, 0x1D, 0xB8, 0xEF, 0x31, 0xE9, - 0x37, 0xA2, 0x92, 0xC8, 0x5D, 0xED, 0x86, 0x44, 0xCC, 0x78, 0x7C, 0x8E, - 0xEF, 0x5D, 0xDE, 0xD9, 0x44, 0x19, 0xF7, 0x05, 0x2E, 0xC7, 0x1C, 0x7A, - 0x7E, 0x15, 0x50, 0x50, 0xCC, 0xB2, 0x02, 0xED, 0x37, 0xC0, 0x0F, 0x7B, - 0x0A, 0xB1, 0x1B, 0xBE, 0x77, 0x1D, 0xBE, 0xF7, 0x15, 0x0E, 0x4D, 0x76, - 0xB5, 0xE2, 0x54, 0xE0, 0x06, 0x20, 0xAA, 0x47, 0xD2, 0x18, 0xE0, 0x97, - 0x14, 0x6A, 0x7E, 0x14, 0xB1, 0xA1, 0xC0, 0xCF, 0x31, 0x45, 0xC9, 0x8A, - 0xAD, 0x81, 0xBE, 0x8B, 0xA9, 0x3F, 0x7E, 0x69, 0x11, 0x03, 0x07, 0xC3, - 0x90, 0xC8, 0x3D, 0xC0, 0x23, 0x21, 0x6F, 0xEB, 0x0F, 0x3A, 0x99, 0xA3, - 0xAD, 0x3F, 0x63, 0x4C, 0x63, 0xF3, 0x0A, 0xD8, 0x4C, 0x1D, 0x56, 0xC0, - 0xFD, 0x25, 0x45, 0xB5, 0xC9, 0x62, 0xFE, 0x7C, 0xC8, 0xDB, 0x4E, 0x9A, - 0xAB, 0x75, 0xEB, 0xA7, 0xA5, 0xF1, 0xBD, 0x6F, 0x30, 0x45, 0xCD, 0xDE, - 0x2E, 0xA0, 0x95, 0x5F, 0x14, 0x4E, 0x20, 0x6B, 0x93, 0xC8, 0x94, 0x28, - 0x6B, 0xD4, 0x92, 0xC8, 0x85, 0xED, 0x49, 0xC0, 0xD9, 0x33, 0x92, 0xF7, - 0xF0, 0xBD, 0x11, 0xF8, 0xDE, 0x91, 0xF8, 0x5E, 0x6F, 0x61, 0xD4, 0xD6, - 0x3E, 0x98, 0xA8, 0xFE, 0xA3, 0x30, 0x7E, 0xF7, 0x47, 0x02, 0x07, 0x01, - 0x7B, 0x08, 0xE8, 0x86, 0xEF, 0x6D, 0x8F, 0xEF, 0x1D, 0x87, 0xEF, 0xDD, - 0x81, 0xEF, 0x7D, 0xD2, 0x5E, 0x53, 0xB1, 0x17, 0x8B, 0x44, 0x44, 0x22, - 0x75, 0x15, 0xA6, 0xF6, 0xC7, 0x93, 0x04, 0x2F, 0xC7, 0x3C, 0x15, 0x38, - 0x0D, 0x51, 0x35, 0xD4, 0xDA, 0xFB, 0x0B, 0xEB, 0xC7, 0xE5, 0xAF, 0xA6, - 0x45, 0x22, 0xF5, 0xAC, 0x48, 0xA4, 0x06, 0x62, 0xBC, 0xC3, 0x2E, 0xC1, - 0x98, 0x33, 0xA3, 0x66, 0x93, 0x99, 0x07, 0x3C, 0x0A, 0x0C, 0x44, 0x54, - 0xFF, 0x48, 0x24, 0x52, 0x2F, 0x97, 0x79, 0xA8, 0xCF, 0xC7, 0x38, 0x21, - 0x84, 0xC1, 0x6F, 0x75, 0x32, 0x3E, 0x34, 0x07, 0x89, 0x3C, 0x83, 0x71, - 0x9D, 0x1E, 0x4E, 0x70, 0xF7, 0xDE, 0x05, 0x98, 0x94, 0x30, 0x7D, 0x45, - 0x22, 0x35, 0xBD, 0x52, 0xD7, 0x63, 0xBD, 0xEF, 0x35, 0x08, 0x23, 0x54, - 0xC3, 0x9A, 0x44, 0x6F, 0x42, 0xAA, 0x83, 0xCB, 0x40, 0x22, 0xB3, 0xAB, - 0x4C, 0xA2, 0xD6, 0x73, 0x09, 0x77, 0x46, 0xB5, 0x04, 0xE3, 0xD8, 0xF0, - 0x60, 0x46, 0x80, 0x17, 0x07, 0x52, 0x6D, 0x8A, 0xA9, 0xEA, 0xF7, 0xC3, - 0x88, 0x6A, 0xFA, 0x30, 0x7C, 0xEF, 0x5E, 0x27, 0x1A, 0xDB, 0x2F, 0x74, - 0x32, 0x7E, 0x60, 0xC0, 0x4D, 0xCB, 0x4A, 0x91, 0x48, 0xBD, 0x1B, 0xB2, - 0xED, 0xEE, 0x76, 0x23, 0xB3, 0x2F, 0x26, 0xD5, 0x44, 0xA6, 0x1A, 0xE1, - 0x0A, 0xCC, 0x81, 0xF0, 0x47, 0xC0, 0x6B, 0x88, 0xD8, 0x3B, 0xD9, 0x39, - 0x9C, 0x6C, 0x20, 0xDC, 0x96, 0x01, 0x1E, 0xF1, 0xAD, 0x48, 0xA4, 0x66, - 0x06, 0xEF, 0x4F, 0xBF, 0x2A, 0xD0, 0x3B, 0xD8, 0x4D, 0xC3, 0x76, 0x98, - 0x34, 0x25, 0x3F, 0xC0, 0x54, 0x36, 0xEC, 0x6C, 0xB5, 0x95, 0xE5, 0x96, - 0x30, 0x66, 0x63, 0x4A, 0xE3, 0x4E, 0x06, 0x31, 0x25, 0xAA, 0xA9, 0x4A, - 0x27, 0xE3, 0xFB, 0x11, 0x2C, 0x09, 0x67, 0x83, 0x48, 0xA4, 0xDE, 0x0C, - 0xD1, 0xEE, 0x26, 0x84, 0x8F, 0x99, 0x59, 0x26, 0x12, 0xA9, 0xF7, 0x9B, - 0x19, 0x9F, 0x1A, 0xD0, 0x3F, 0xC1, 0xB8, 0xF8, 0xEE, 0x8E, 0x09, 0x24, - 0x5C, 0x1F, 0x73, 0xEE, 0x32, 0x17, 0x13, 0x1C, 0xFB, 0x36, 0xF0, 0x62, - 0x36, 0xD1, 0xEB, 0x64, 0xBC, 0x47, 0x40, 0x19, 0xB3, 0x5C, 0x24, 0x52, - 0x93, 0x9B, 0x91, 0x57, 0x41, 0xB3, 0x5C, 0xAF, 0xB4, 0xD9, 0x28, 0x82, - 0xCA, 0xC1, 0x2D, 0xEC, 0x7C, 0x87, 0xC1, 0xFC, 0x6C, 0xF3, 0xB4, 0x90, - 0x6A, 0x03, 0x1D, 0x3C, 0xE5, 0xD1, 0xF7, 0xF8, 0xDE, 0xB4, 0x42, 0xBE, - 0xCB, 0x6E, 0xB5, 0xA3, 0xC4, 0x12, 0xAD, 0x7F, 0x08, 0xC4, 0x81, 0x3D, - 0x30, 0xA6, 0xC8, 0x6E, 0xF6, 0x5B, 0x5D, 0x6E, 0xBF, 0x9F, 0xCF, 0x80, - 0xC9, 0x02, 0x52, 0xDA, 0xF7, 0x56, 0x64, 0x6B, 0x00, 0xC5, 0x43, 0xE1, - 0x24, 0x72, 0x11, 0xBE, 0x77, 0x9F, 0x13, 0xB5, 0x0E, 0x0E, 0x0E, 0x0E, - 0x6D, 0xC0, 0x12, 0x50, 0xF4, 0x16, 0x0D, 0x89, 0xBC, 0x48, 0xB8, 0x94, - 0xDB, 0xD9, 0x24, 0x72, 0x21, 0xBE, 0x77, 0xBF, 0x9B, 0x1A, 0x07, 0x07, - 0x07, 0x87, 0xCA, 0x46, 0xF1, 0xF3, 0xCA, 0xF8, 0xDE, 0x77, 0xD6, 0x94, - 0xF0, 0x49, 0x44, 0x42, 0xBB, 0x17, 0xA9, 0xCE, 0x77, 0x53, 0xE3, 0xE0, - 0xE0, 0xE0, 0xD0, 0xD1, 0x34, 0x90, 0x46, 0x4D, 0x64, 0x33, 0xAB, 0x89, - 0x44, 0xA9, 0xA0, 0xA6, 0x81, 0x0B, 0xF0, 0xBD, 0x07, 0xDC, 0x14, 0x39, - 0x38, 0x38, 0x38, 0x74, 0x34, 0x02, 0x71, 0x24, 0xE2, 0xE0, 0xE0, 0xE0, - 0xE0, 0x08, 0xA4, 0x8C, 0x24, 0x92, 0xB6, 0x24, 0xF2, 0x67, 0x37, 0x55, - 0x0E, 0x0E, 0x0E, 0x0E, 0x1D, 0x8D, 0x40, 0x0C, 0x89, 0x6C, 0x6E, 0x49, - 0x64, 0x57, 0x47, 0x22, 0x0E, 0x0E, 0x0E, 0x0E, 0x8E, 0x40, 0x5A, 0x9B, - 0x44, 0xCE, 0xC7, 0xF7, 0x1E, 0xAC, 0xC8, 0x51, 0x94, 0xEA, 0x2C, 0x4C, - 0xF4, 0xF8, 0xB3, 0xC0, 0xF3, 0x31, 0x78, 0x3B, 0xED, 0x7B, 0x2B, 0xDD, - 0xF2, 0x72, 0x70, 0x70, 0x70, 0x04, 0x52, 0x5C, 0x12, 0x79, 0x89, 0x68, - 0x85, 0x7C, 0xD2, 0xC0, 0x79, 0xF8, 0x9E, 0xAC, 0x30, 0xF2, 0x38, 0x13, - 0x13, 0x99, 0x99, 0xED, 0xD1, 0xB6, 0x12, 0x78, 0x07, 0x53, 0xC5, 0xF1, - 0x03, 0x4C, 0xDE, 0x99, 0xD9, 0x02, 0xBE, 0x8E, 0xC1, 0xCA, 0x2A, 0x48, - 0xC7, 0x04, 0x54, 0x23, 0xF4, 0x52, 0x17, 0x6D, 0xEE, 0xE0, 0xE0, 0xE0, - 0x08, 0x24, 0xB0, 0xC0, 0xDD, 0xC2, 0x6A, 0x22, 0x6D, 0x9F, 0x44, 0x72, - 0x93, 0x47, 0x4B, 0x58, 0x85, 0x29, 0x1D, 0x3A, 0x15, 0x38, 0xD2, 0xBA, - 0x3D, 0x3B, 0x38, 0x38, 0x38, 0x38, 0x02, 0x69, 0x25, 0x12, 0xF9, 0x15, - 0xBE, 0x57, 0x5B, 0x66, 0xF2, 0x38, 0xC3, 0x92, 0x47, 0x94, 0x12, 0x95, - 0xD3, 0x81, 0xFE, 0x2E, 0x49, 0xA2, 0x83, 0x83, 0x83, 0x23, 0x90, 0xE8, - 0x24, 0xF2, 0x12, 0x26, 0x6F, 0x51, 0xDB, 0x22, 0x11, 0x53, 0x70, 0xFE, - 0x31, 0xA2, 0x05, 0x62, 0x4E, 0xA7, 0x69, 0x8D, 0x65, 0x07, 0x07, 0x07, - 0x87, 0x36, 0x88, 0x58, 0xD9, 0x9E, 0x6C, 0x52, 0x0A, 0xF7, 0x07, 0x3E, - 0x8F, 0xD8, 0xEF, 0x3F, 0x23, 0xD5, 0xD9, 0xAD, 0xDE, 0x6F, 0xA9, 0x4E, - 0x02, 0xFE, 0x1A, 0x71, 0xEC, 0x66, 0x38, 0xF2, 0x70, 0x68, 0x6F, 0xA8, - 0x92, 0xAA, 0x7A, 0xE3, 0x56, 0xAE, 0x6B, 0xE1, 0xD0, 0xD1, 0x35, 0x90, - 0x46, 0x81, 0xBC, 0x25, 0xC6, 0x9C, 0x15, 0x55, 0x13, 0xF1, 0xF1, 0xBD, - 0x87, 0x5A, 0xA9, 0xAF, 0x27, 0x02, 0x8F, 0x03, 0xD5, 0x11, 0xC9, 0xA3, - 0x7F, 0x87, 0x26, 0x0F, 0xA9, 0x3E, 0xC4, 0x64, 0x8B, 0x7D, 0x0A, 0xDF, - 0xBB, 0xA6, 0x03, 0xBE, 0xFF, 0x79, 0x18, 0x6F, 0x3D, 0x11, 0x83, 0x3D, - 0xD2, 0xBE, 0xB7, 0xBA, 0xCD, 0xEE, 0x3C, 0xA5, 0xEA, 0x94, 0x36, 0x35, - 0x30, 0x4E, 0x07, 0x36, 0xC7, 0x94, 0x75, 0x7D, 0x14, 0xDF, 0xBB, 0xA4, - 0x03, 0xCE, 0x6B, 0x3F, 0x8C, 0x39, 0x1B, 0xE0, 0x64, 0x7C, 0xEF, 0x3D, - 0xA7, 0x81, 0xB4, 0x9E, 0x26, 0xF2, 0x35, 0x30, 0xA0, 0x00, 0x4D, 0x44, - 0x5A, 0x37, 0x5A, 0x47, 0x1E, 0x95, 0x8F, 0x9D, 0xEC, 0x46, 0x61, 0xD3, - 0x0E, 0xFA, 0xFE, 0x3D, 0xED, 0xFB, 0xEF, 0x54, 0x11, 0x9B, 0xB7, 0x02, - 0x90, 0x36, 0xB5, 0xE9, 0xAF, 0x06, 0xB6, 0xC6, 0xA4, 0xCD, 0xDF, 0x88, - 0x68, 0xE7, 0x81, 0xED, 0x01, 0x5D, 0xED, 0xBC, 0xEE, 0x0C, 0x74, 0xE9, - 0x48, 0x2F, 0x1E, 0xAB, 0x88, 0x5E, 0x34, 0x92, 0xC8, 0xB4, 0x88, 0xEF, - 0x20, 0xAD, 0x47, 0x54, 0xA9, 0xC8, 0xC3, 0x2B, 0x80, 0x3C, 0x66, 0xE2, - 0xCC, 0x56, 0x0E, 0xED, 0xC9, 0x6C, 0x21, 0x55, 0x4F, 0xE0, 0x0C, 0xFB, - 0xDF, 0x31, 0x98, 0x7A, 0x2A, 0x7D, 0x81, 0x3B, 0xDD, 0xE8, 0x74, 0x2C, - 0xC4, 0x2A, 0xA6, 0x27, 0x85, 0x93, 0x48, 0x6D, 0x49, 0x48, 0xC4, 0x90, - 0xC7, 0xC8, 0x02, 0xC8, 0xA3, 0x3F, 0xBE, 0x37, 0xDB, 0x2D, 0x35, 0x87, - 0xF6, 0x02, 0x0D, 0x3B, 0x66, 0x7D, 0x0F, 0x0F, 0xE0, 0x7B, 0xDF, 0xD8, - 0x0A, 0x9C, 0xD3, 0xDD, 0xE8, 0x74, 0x2C, 0x54, 0x57, 0x54, 0x6F, 0x7C, - 0xEF, 0x2B, 0xA4, 0x1A, 0x80, 0x39, 0x13, 0xD9, 0x29, 0x22, 0x89, 0x68, - 0x7C, 0xEF, 0x91, 0x22, 0x91, 0xC7, 0xD0, 0x02, 0x35, 0x0F, 0x47, 0x1E, - 0xF9, 0xC7, 0x76, 0x0F, 0xE0, 0x02, 0x60, 0x6F, 0x8C, 0x39, 0xE7, 0x53, - 0x60, 0x24, 0xBE, 0xF7, 0xEF, 0xAC, 0x6B, 0xEE, 0x03, 0x7A, 0x01, 0x53, - 0xF1, 0xBD, 0xAB, 0xF2, 0xB4, 0x73, 0x13, 0xA6, 0x02, 0x5C, 0x0A, 0xDF, - 0xBB, 0xB3, 0x99, 0xE7, 0xDD, 0x85, 0xB1, 0xD5, 0xFF, 0x03, 0x78, 0xDD, - 0x3E, 0xBB, 0xAF, 0x5D, 0x37, 0x1F, 0x03, 0xF7, 0xE1, 0x7B, 0x93, 0x63, - 0x52, 0xD5, 0xA4, 0xE1, 0x54, 0x4C, 0x9D, 0xF3, 0x2D, 0x80, 0x65, 0xC0, - 0xCB, 0x02, 0xEE, 0xD6, 0xBE, 0x37, 0x7F, 0xAD, 0x05, 0x67, 0xAE, 0x3D, - 0x1A, 0x18, 0x84, 0xC9, 0xB0, 0xB0, 0x1E, 0xA6, 0x94, 0xED, 0x1B, 0xC0, - 0x83, 0xA1, 0xB4, 0x4E, 0xA9, 0xB6, 0x07, 0x6E, 0xC4, 0x98, 0x81, 0xE6, - 0x08, 0xF8, 0x6D, 0xA6, 0xF2, 0x9B, 0x90, 0xAA, 0x9B, 0x36, 0x3B, 0xFE, - 0x41, 0x18, 0xF3, 0xDF, 0x62, 0xE0, 0x55, 0xFB, 0x8C, 0x6F, 0x9B, 0xB4, - 0x33, 0x18, 0x38, 0x13, 0x58, 0x19, 0x83, 0xB3, 0xD3, 0x70, 0x1A, 0xA6, - 0x8F, 0x73, 0xF1, 0xBD, 0xB3, 0x00, 0x6A, 0xA4, 0xAA, 0xAA, 0x83, 0xE3, - 0x80, 0x63, 0x30, 0x65, 0x78, 0x63, 0x98, 0x12, 0xB3, 0x93, 0x80, 0x67, - 0xF0, 0xBD, 0x0F, 0x72, 0x75, 0x71, 0xA3, 0xDA, 0x51, 0x62, 0x81, 0xD6, - 0xF7, 0xD8, 0x71, 0xC9, 0x60, 0x98, 0xFD, 0x4E, 0x00, 0xAE, 0xCB, 0xB8, - 0xA6, 0x57, 0x49, 0x55, 0xD3, 0x00, 0x9E, 0x1D, 0xC7, 0x6D, 0x80, 0x06, - 0xE0, 0x43, 0xCC, 0xF9, 0xD7, 0xCB, 0x39, 0xDE, 0x5F, 0x62, 0xCA, 0x3E, - 0x3F, 0x6D, 0xFB, 0x71, 0x1E, 0xF0, 0x23, 0xE0, 0x2A, 0x7C, 0xEF, 0xD5, - 0x66, 0xC6, 0xED, 0x12, 0x4C, 0x69, 0xD6, 0xA9, 0x02, 0xEE, 0xD2, 0x70, - 0x21, 0xA6, 0xDE, 0xF7, 0xFA, 0xC0, 0x2C, 0xE0, 0x11, 0x7C, 0x6F, 0x62, - 0xCF, 0xDA, 0x51, 0x62, 0xBE, 0xD6, 0x3F, 0xC3, 0x94, 0x9D, 0xDD, 0xD1, - 0xF6, 0x67, 0x12, 0x70, 0x4F, 0x53, 0xE2, 0xB3, 0x15, 0xFA, 0x0E, 0xB6, - 0xE3, 0xD6, 0x07, 0x53, 0x45, 0x72, 0x05, 0xF0, 0x3E, 0xF0, 0x30, 0xBE, - 0x37, 0x39, 0x84, 0xB6, 0xD6, 0x4D, 0xC3, 0x2D, 0x18, 0xF3, 0x65, 0x1D, - 0x70, 0x99, 0xDD, 0x2C, 0x53, 0x25, 0x55, 0x75, 0x83, 0x29, 0x6D, 0x7D, - 0xAC, 0x1D, 0xA3, 0xD5, 0x98, 0x00, 0xE4, 0x87, 0xF0, 0xBD, 0x8F, 0x9A, - 0xBC, 0xE7, 0xAE, 0x98, 0xD2, 0xBE, 0x60, 0xCA, 0x26, 0xEF, 0x63, 0xDF, - 0x65, 0xCB, 0x1A, 0xE8, 0x5F, 0xE7, 0x7B, 0x69, 0x7B, 0xDD, 0xC1, 0xC0, - 0xC9, 0x98, 0xA2, 0x7E, 0x5D, 0x80, 0x85, 0x98, 0x60, 0xE6, 0x89, 0xEB, - 0x09, 0xF1, 0xE2, 0x8A, 0x22, 0x07, 0x2E, 0x8B, 0x0A, 0x15, 0x2E, 0x5B, - 0x61, 0x5C, 0x7C, 0x77, 0x8C, 0x70, 0x77, 0x1A, 0x38, 0xBB, 0x60, 0x12, - 0x91, 0xEA, 0x04, 0x4C, 0xAD, 0xED, 0x28, 0xE4, 0x31, 0xCB, 0x92, 0xC7, - 0x7F, 0x1D, 0x53, 0xAC, 0x35, 0xA6, 0x2B, 0xEC, 0xA2, 0xFE, 0x1C, 0x53, - 0xE2, 0xB5, 0x53, 0x8E, 0xAB, 0x1E, 0xAF, 0x82, 0xB3, 0x1A, 0x7C, 0xAF, - 0x0E, 0xA9, 0xAE, 0x07, 0xAE, 0xB4, 0x1F, 0xFB, 0x2E, 0xF8, 0xDE, 0x8C, - 0x26, 0xED, 0xED, 0x6E, 0x85, 0xBF, 0x00, 0x06, 0xE3, 0x7B, 0x13, 0x9B, - 0x79, 0xF6, 0x54, 0x8C, 0x8D, 0xFA, 0x23, 0xBB, 0x39, 0x69, 0x6A, 0xAB, - 0xAE, 0xC3, 0xD4, 0xFF, 0x3E, 0x0F, 0xD8, 0x2F, 0x47, 0x0B, 0xFF, 0x05, - 0x7E, 0x62, 0xBD, 0x07, 0x89, 0x49, 0xD5, 0x39, 0x0D, 0x93, 0xC9, 0x9F, - 0x24, 0x74, 0x31, 0x26, 0x50, 0xF4, 0xB5, 0xAC, 0x3E, 0x5C, 0x01, 0xDC, - 0x60, 0x77, 0x3B, 0x5D, 0xD2, 0xBE, 0xB7, 0xCA, 0xFE, 0x7C, 0x4F, 0x4B, - 0x6C, 0x5B, 0x58, 0x41, 0x3E, 0x70, 0x8D, 0x00, 0x91, 0x6A, 0x1F, 0xE0, - 0x19, 0xCC, 0x59, 0x43, 0xAE, 0x67, 0xFC, 0x02, 0xDF, 0x9B, 0x90, 0xF5, - 0x8C, 0x8B, 0xAD, 0x29, 0x69, 0xB9, 0x25, 0xB2, 0x81, 0xF6, 0x37, 0x6F, - 0xE2, 0x7B, 0x07, 0xC6, 0xA4, 0xEA, 0x92, 0x36, 0x29, 0x77, 0x0E, 0xCB, - 0xD3, 0xEF, 0xFF, 0xC5, 0x60, 0xDB, 0x35, 0x7D, 0xCB, 0x42, 0xD7, 0xDA, - 0x51, 0x62, 0x99, 0xD6, 0x2B, 0xC8, 0x5F, 0x36, 0xB7, 0x0F, 0xBE, 0xF7, - 0x21, 0x52, 0x6D, 0x6B, 0xFB, 0x9C, 0xAF, 0x2C, 0xEB, 0x53, 0x02, 0xCE, - 0xD4, 0xD9, 0xE9, 0x7E, 0xA4, 0xFA, 0x16, 0xD8, 0x0C, 0x78, 0xD9, 0x12, - 0x47, 0x57, 0xFB, 0x9B, 0x9F, 0xE3, 0x7B, 0xCF, 0x36, 0x33, 0xAF, 0x4F, - 0x02, 0x27, 0xD9, 0xF9, 0xE9, 0x6C, 0xDB, 0x68, 0x8A, 0xEB, 0xED, 0x9C, - 0x9F, 0x98, 0xE3, 0x77, 0x4B, 0x81, 0x41, 0xF8, 0xDE, 0x7F, 0xB2, 0xDA, - 0x54, 0x18, 0xC1, 0x9E, 0x0B, 0x0D, 0x98, 0xDC, 0x7C, 0x0F, 0x66, 0x5D, - 0x7F, 0x04, 0x90, 0x59, 0x7B, 0x3F, 0xCD, 0xB4, 0x25, 0xA4, 0xDA, 0x44, - 0xC3, 0x73, 0xF6, 0x7D, 0x56, 0x03, 0x27, 0xE2, 0x7B, 0xCF, 0xD8, 0x7B, - 0x36, 0x05, 0xC6, 0x61, 0x4A, 0xFA, 0xE6, 0x7A, 0x46, 0x02, 0xDF, 0xBB, - 0x39, 0xEB, 0x19, 0x71, 0x20, 0xB3, 0x96, 0x9E, 0xB5, 0xE4, 0x06, 0xB0, - 0xA2, 0x1A, 0xBA, 0xD5, 0xFB, 0x5E, 0x03, 0x52, 0xDD, 0x6E, 0xC9, 0x25, - 0x5F, 0xBF, 0x7F, 0x88, 0xEF, 0x4D, 0x6D, 0x9F, 0x26, 0xAC, 0xA6, 0x9A, - 0x88, 0x71, 0xF1, 0x9D, 0x1E, 0xF1, 0x9D, 0x6A, 0x6D, 0xA0, 0x5F, 0x21, - 0xE4, 0xF1, 0x84, 0x23, 0x8F, 0x92, 0x61, 0x67, 0x2B, 0xF4, 0x25, 0x70, - 0x0E, 0x90, 0xC0, 0x38, 0x1A, 0x00, 0x9C, 0xDA, 0x60, 0x85, 0x2C, 0x70, - 0xBF, 0xFD, 0xF0, 0xAA, 0xAC, 0x70, 0x6F, 0x8A, 0x8B, 0x6C, 0x3B, 0x53, - 0x3B, 0xC1, 0xF3, 0x01, 0x9F, 0xBD, 0xA7, 0xB1, 0xC2, 0x70, 0x3F, 0xC6, - 0x23, 0xEA, 0x71, 0xFB, 0xFF, 0x1A, 0x8C, 0x27, 0xCD, 0x7E, 0x98, 0x1A, - 0xE5, 0xD7, 0x02, 0xBF, 0x07, 0x32, 0xB5, 0xC3, 0xB7, 0xB1, 0x1A, 0x82, - 0xD9, 0xA5, 0x18, 0x01, 0xFB, 0x1F, 0x4C, 0x4D, 0xF3, 0xBB, 0x30, 0xDE, - 0x48, 0x27, 0x03, 0xF7, 0x01, 0xF5, 0x76, 0xD7, 0xFA, 0x70, 0x8D, 0x54, - 0xB1, 0x16, 0xD6, 0xDA, 0x81, 0x56, 0x68, 0x66, 0xC8, 0xE3, 0xD0, 0x0C, - 0x79, 0x08, 0xA9, 0x7A, 0x61, 0x04, 0xD0, 0xD6, 0xC0, 0xF7, 0xC0, 0xA5, - 0x76, 0x77, 0x7D, 0xA6, 0x25, 0xCE, 0xEE, 0xC0, 0x28, 0xBB, 0x3B, 0x6D, - 0x8A, 0xF5, 0x2D, 0x79, 0xCC, 0x01, 0x1E, 0xC1, 0x9C, 0x55, 0x90, 0x36, - 0xE3, 0x7D, 0x58, 0xD6, 0xF8, 0x1E, 0x69, 0xFF, 0x3F, 0xCC, 0xF6, 0xE3, - 0x96, 0x5C, 0xE4, 0x01, 0xB0, 0xBE, 0xD9, 0x6E, 0x26, 0x80, 0xBB, 0xB3, - 0x09, 0x1F, 0xB8, 0x0C, 0xB8, 0x4C, 0xC0, 0xB7, 0x42, 0xAA, 0xF5, 0x6C, - 0x9F, 0xFB, 0xD8, 0x71, 0x1D, 0x0D, 0xFC, 0xCA, 0xF6, 0x7D, 0x92, 0xBD, - 0xE7, 0x24, 0x6D, 0x0E, 0xE1, 0x73, 0xE1, 0x10, 0x4B, 0x1E, 0xFF, 0xC1, - 0x5C, 0xF3, 0x45, 0xC0, 0x79, 0xDD, 0x06, 0x53, 0x73, 0x5E, 0xD9, 0x67, - 0x65, 0x48, 0x14, 0x8C, 0xB7, 0xD8, 0x89, 0x96, 0x70, 0x6F, 0xB3, 0xBF, - 0x7F, 0xC6, 0xFE, 0x6E, 0x03, 0xE0, 0x81, 0x0D, 0xD6, 0x76, 0x43, 0x7E, - 0x09, 0x93, 0x8E, 0xE8, 0x31, 0xDB, 0xF7, 0xA1, 0x76, 0x3D, 0x2C, 0xB2, - 0x6B, 0xF1, 0x2E, 0x1B, 0xC7, 0xD6, 0xDC, 0xBC, 0xF6, 0xD6, 0xF0, 0x8A, - 0x25, 0x8F, 0xBA, 0x6C, 0xF2, 0xB0, 0x6B, 0x62, 0xAC, 0x25, 0x8F, 0xD9, - 0x83, 0x5D, 0x9A, 0x00, 0x00, 0x0A, 0xDD, 0x49, 0x44, 0x41, 0x54, 0x55, - 0xC0, 0x70, 0x3B, 0x57, 0xC7, 0x03, 0x2F, 0xD8, 0x67, 0x8C, 0x40, 0xAA, - 0xE3, 0xF2, 0xB4, 0x7E, 0xB4, 0xD5, 0x88, 0x14, 0xF0, 0xE7, 0x2A, 0xD0, - 0x48, 0x75, 0x40, 0x16, 0x79, 0x4C, 0x00, 0x4E, 0xC0, 0x1C, 0x09, 0x9C, - 0x05, 0x8C, 0x02, 0x9E, 0x28, 0x36, 0x79, 0x54, 0x9E, 0x09, 0x2B, 0xB7, - 0x39, 0xEB, 0x25, 0x60, 0x87, 0x90, 0x77, 0x57, 0x65, 0x99, 0xB3, 0xFE, - 0x1A, 0x92, 0x3C, 0x8E, 0xB7, 0xE4, 0x51, 0x13, 0xA1, 0xD7, 0x5F, 0x38, - 0xF2, 0x08, 0x04, 0x0D, 0x1C, 0x83, 0xEF, 0x3D, 0x97, 0xA5, 0xEA, 0xDF, - 0xA7, 0xCD, 0x0E, 0x6B, 0x2F, 0xE0, 0x62, 0x21, 0xD5, 0x4D, 0xDA, 0xF7, - 0xBE, 0x46, 0xAA, 0x31, 0x56, 0x30, 0x9F, 0x29, 0xA4, 0xBA, 0x46, 0xFB, - 0xDE, 0x32, 0x7B, 0x7D, 0x0F, 0x0D, 0xBF, 0xB4, 0xB7, 0xDF, 0xBD, 0x3A, - 0xA3, 0xC2, 0x07, 0xC3, 0xB1, 0xF8, 0xDE, 0xF3, 0x59, 0x73, 0x3E, 0xC7, - 0x92, 0x09, 0xC0, 0x37, 0x02, 0xFA, 0x6A, 0xDF, 0x9B, 0x6B, 0x35, 0x8D, - 0x3B, 0xAD, 0xA6, 0xB1, 0x3B, 0x70, 0xD4, 0x46, 0xB5, 0xA3, 0xC4, 0x82, - 0x46, 0x33, 0xC0, 0x95, 0x02, 0x2E, 0xD3, 0xBE, 0xB7, 0x20, 0x7B, 0x77, - 0x8D, 0x54, 0xDF, 0x03, 0x7F, 0x04, 0x76, 0xA8, 0x33, 0x84, 0xF5, 0x41, - 0x9E, 0xB5, 0x76, 0xB8, 0x15, 0xB0, 0xDD, 0xB2, 0xC8, 0xE3, 0xC3, 0xAC, - 0x41, 0xBA, 0xD4, 0xEE, 0xA8, 0x57, 0x00, 0x07, 0xE3, 0x7B, 0x9F, 0xD9, - 0x5F, 0xBD, 0x2A, 0xA4, 0x1A, 0xAB, 0xE1, 0x5D, 0xAB, 0xA5, 0x5F, 0x4E, - 0xE3, 0xA1, 0x76, 0x36, 0x26, 0x0B, 0x18, 0xA0, 0x7D, 0x6F, 0x61, 0xD6, - 0xCF, 0xF6, 0xB6, 0x7F, 0x7F, 0xD7, 0x5D, 0x88, 0x0B, 0x17, 0x37, 0xBE, - 0xCB, 0xBF, 0x80, 0x7B, 0x3A, 0x4B, 0x15, 0x5B, 0x95, 0x67, 0xD0, 0xE6, - 0x9C, 0x3D, 0x54, 0x03, 0xB7, 0x23, 0xD5, 0xFE, 0x96, 0x70, 0x00, 0xFE, - 0x8E, 0xEF, 0xA9, 0xCC, 0xA4, 0x22, 0xD5, 0x45, 0x59, 0x5A, 0xD9, 0xEF, - 0xF0, 0xBD, 0xDB, 0xD6, 0x7C, 0x94, 0x52, 0xDD, 0xDD, 0x60, 0x88, 0xEC, - 0x68, 0xE0, 0x4C, 0xA4, 0xBA, 0x0D, 0xDF, 0x9B, 0x92, 0xE3, 0x51, 0xC3, - 0xF0, 0xBD, 0x7B, 0x22, 0xAC, 0xAB, 0xAB, 0xF1, 0xBD, 0x1B, 0xB3, 0xC6, - 0xF7, 0x6D, 0xCC, 0xF9, 0x25, 0x96, 0xD4, 0xFB, 0x67, 0xB9, 0xD8, 0xDE, - 0x8E, 0x54, 0x4F, 0x59, 0x62, 0xD9, 0x73, 0xA9, 0xD6, 0xDB, 0x67, 0x36, - 0x31, 0x31, 0xF8, 0x4B, 0x1A, 0x5E, 0x68, 0xA2, 0xF1, 0x8E, 0x46, 0xAA, - 0x49, 0xC0, 0xDF, 0xAD, 0xA6, 0x73, 0x04, 0xF0, 0x50, 0x9E, 0x79, 0xDD, - 0xC9, 0x12, 0xC1, 0xB6, 0xF6, 0xB9, 0x27, 0xAF, 0xD1, 0x3C, 0x0C, 0x9B, - 0x1C, 0x03, 0xFC, 0xC4, 0xFE, 0xF7, 0x14, 0x7C, 0x6F, 0x4C, 0xE6, 0x77, - 0x5D, 0x6A, 0xD5, 0x33, 0x2B, 0x35, 0xCF, 0x02, 0x43, 0xAC, 0xC9, 0x6A, - 0x6C, 0x8E, 0x27, 0xCC, 0xB5, 0x9A, 0xCE, 0xE7, 0x58, 0x06, 0x42, 0xAA, - 0x7D, 0xB3, 0x7E, 0x7F, 0x41, 0x13, 0xD3, 0xF9, 0xC3, 0x9D, 0xA4, 0x8A, - 0x95, 0xC2, 0x67, 0xBC, 0xBA, 0xA2, 0xC5, 0x8C, 0xEF, 0x7D, 0x89, 0x54, - 0xFD, 0x0B, 0x20, 0x91, 0xBF, 0x58, 0x12, 0x79, 0x34, 0x04, 0x79, 0x3C, - 0xE9, 0xC8, 0xA3, 0xE4, 0x78, 0x31, 0x9B, 0x3C, 0x00, 0xB4, 0xEF, 0x2D, - 0x46, 0xAA, 0xEB, 0xEC, 0x6E, 0xA9, 0xB3, 0x36, 0x1A, 0xE8, 0x28, 0xBB, - 0x93, 0x3C, 0x19, 0xE8, 0xA9, 0x8D, 0xCD, 0x57, 0x5A, 0x61, 0x75, 0xA6, - 0xDD, 0x3D, 0x2E, 0x12, 0xF0, 0x68, 0x08, 0xC3, 0xEE, 0x94, 0xB5, 0xC8, - 0x23, 0x23, 0xF4, 0x1B, 0x09, 0xE4, 0xAF, 0x19, 0xF2, 0xB0, 0x9A, 0xC6, - 0x6A, 0xA4, 0x7A, 0xDA, 0x12, 0x48, 0xCF, 0x85, 0x5A, 0x6F, 0x68, 0xED, - 0xCA, 0xE0, 0x7B, 0xDF, 0x59, 0xA1, 0xB9, 0x85, 0x15, 0xF4, 0x19, 0x6D, - 0x23, 0x3B, 0x45, 0x4D, 0xEF, 0x5C, 0x04, 0x92, 0x36, 0x3B, 0xC4, 0xBF, - 0x58, 0x61, 0xB4, 0xB8, 0x29, 0x79, 0x58, 0x0C, 0xB6, 0x7F, 0xFF, 0x07, - 0xA8, 0xB6, 0xE7, 0x46, 0x6B, 0x18, 0xD8, 0xEE, 0xF4, 0x2F, 0x06, 0x7E, - 0xD6, 0xB5, 0x76, 0x94, 0x58, 0xB6, 0xAE, 0x7D, 0xFB, 0xEA, 0x26, 0xE4, - 0x41, 0xD6, 0x8E, 0x7E, 0xD3, 0xC5, 0x5A, 0x4F, 0x44, 0xAA, 0x67, 0x2C, - 0xD1, 0x7C, 0xAA, 0x7D, 0x6F, 0xF1, 0xAA, 0x70, 0x44, 0x9C, 0x6F, 0x77, - 0x0C, 0xF0, 0x4D, 0x35, 0xDC, 0x59, 0x9F, 0x6D, 0x43, 0xF1, 0xBD, 0x7A, - 0xA4, 0x4A, 0xD8, 0x6B, 0x84, 0x7D, 0xBF, 0xA6, 0x04, 0x32, 0x75, 0x63, - 0x21, 0xEE, 0x9D, 0x17, 0xFE, 0xB9, 0xF5, 0xF6, 0x0C, 0x64, 0x0D, 0x04, - 0x8C, 0xD5, 0x46, 0x83, 0xED, 0x64, 0xD7, 0xDC, 0x7B, 0x39, 0xE6, 0x3D, - 0x63, 0xD6, 0x5A, 0x43, 0x20, 0x36, 0x3E, 0x67, 0x86, 0x90, 0x6A, 0x23, - 0xDD, 0xE8, 0xA6, 0x0C, 0x30, 0xDF, 0x0E, 0xBD, 0xB0, 0xF3, 0x9A, 0x0B, - 0xFB, 0x60, 0xDC, 0x9B, 0x33, 0xAE, 0xEA, 0x67, 0x64, 0x13, 0x84, 0xC5, - 0x91, 0xF6, 0xEF, 0x6F, 0x81, 0xCF, 0xB3, 0xE7, 0x75, 0xA5, 0x5E, 0x63, - 0xA2, 0x1A, 0x02, 0xEC, 0x8E, 0x54, 0x5B, 0xE7, 0x38, 0x47, 0xBD, 0x27, - 0x43, 0x1E, 0x59, 0x98, 0x99, 0xF5, 0xEF, 0x71, 0x48, 0x35, 0x12, 0x73, - 0x9E, 0xF2, 0x09, 0xBE, 0xF7, 0xFD, 0xEA, 0xC2, 0xE7, 0xB5, 0x0D, 0x12, - 0x48, 0x71, 0x48, 0xE4, 0x21, 0xA4, 0xA2, 0x45, 0x12, 0x31, 0xEA, 0x62, - 0xA1, 0xE4, 0xF1, 0x05, 0x0E, 0x41, 0xF0, 0x49, 0x5E, 0xE1, 0xDE, 0x88, - 0x2D, 0xED, 0xFC, 0xBF, 0x85, 0x54, 0x6F, 0x02, 0x07, 0x00, 0x17, 0xF5, - 0xAC, 0x1D, 0x55, 0xBB, 0x54, 0x6B, 0xB1, 0xDA, 0x1C, 0x96, 0x02, 0x3C, - 0xA4, 0x7D, 0x6F, 0x49, 0x88, 0x67, 0xCF, 0xCC, 0xF1, 0xB3, 0xC5, 0x4D, - 0x4C, 0x90, 0x4D, 0x91, 0xAD, 0x61, 0xD4, 0x64, 0xAD, 0x99, 0x83, 0xAC, - 0xF9, 0xAA, 0x4F, 0x33, 0xCF, 0xCB, 0xB7, 0x9E, 0x1E, 0xCD, 0x22, 0x9C, - 0x6E, 0x76, 0xB7, 0xDA, 0x94, 0x40, 0xB6, 0xB4, 0x7F, 0x0F, 0xC4, 0x9C, - 0xDD, 0xE4, 0x43, 0xCF, 0x15, 0x5A, 0xD7, 0x58, 0x73, 0x5F, 0x36, 0xDE, - 0x6D, 0x7A, 0xA1, 0x80, 0xFB, 0xB4, 0x39, 0x40, 0xDF, 0x07, 0x73, 0x28, - 0x3F, 0xC8, 0x12, 0x52, 0x03, 0x52, 0xA5, 0x30, 0x87, 0xD6, 0xA9, 0x02, - 0xE6, 0x36, 0x63, 0xDA, 0xF9, 0xA8, 0xDE, 0xF7, 0x1A, 0x72, 0x08, 0x9C, - 0xA9, 0xF5, 0x46, 0xA3, 0x5A, 0xCF, 0xBE, 0x73, 0x53, 0x7C, 0x30, 0x2F, - 0xDA, 0x41, 0xEF, 0x1C, 0xED, 0x7B, 0x4B, 0x9B, 0x0C, 0xFC, 0xEA, 0xD5, - 0x8D, 0x04, 0x92, 0x6B, 0x5E, 0x17, 0x35, 0x91, 0x15, 0x99, 0x79, 0xDD, - 0x01, 0x33, 0x4E, 0x87, 0x92, 0xDF, 0xCC, 0x9F, 0x4F, 0x76, 0xDE, 0xD5, - 0xE4, 0x9E, 0x5C, 0x01, 0xD2, 0x99, 0x79, 0xDD, 0x3C, 0xC7, 0x9C, 0x37, - 0xC5, 0xA6, 0xD6, 0xA4, 0xDA, 0xEC, 0xBC, 0xD6, 0xC0, 0x0B, 0x75, 0x66, - 0xC3, 0x35, 0xD4, 0x6A, 0x99, 0x7B, 0xAF, 0xD9, 0x6B, 0x48, 0xF5, 0x31, - 0x30, 0x02, 0xDF, 0x1B, 0xD9, 0xF1, 0x08, 0xA4, 0x91, 0x44, 0x32, 0xE6, - 0xAC, 0xED, 0x23, 0x92, 0x88, 0xC6, 0xF7, 0x1E, 0x6B, 0x86, 0x3C, 0x9E, - 0x72, 0xE4, 0xD1, 0x6A, 0xE8, 0x91, 0xE7, 0xE7, 0x1B, 0x65, 0xFD, 0x7B, - 0x59, 0x93, 0x8F, 0xF2, 0x00, 0x60, 0xAF, 0xF9, 0x5A, 0xF7, 0xB3, 0x02, - 0x77, 0x07, 0xB3, 0x91, 0x27, 0xAC, 0xA9, 0xA3, 0xA1, 0x85, 0xDF, 0x07, - 0xDB, 0xA9, 0x49, 0xD5, 0xDB, 0x6A, 0x00, 0x5D, 0xAD, 0x70, 0xFA, 0x9B, - 0xDD, 0xA1, 0x02, 0x6C, 0x65, 0x35, 0x83, 0xE6, 0x10, 0xB3, 0xC2, 0x63, - 0x3B, 0xFB, 0x3E, 0x8F, 0x22, 0xD5, 0x8F, 0x9B, 0xEC, 0x2C, 0x97, 0x02, - 0x1B, 0x63, 0x3C, 0xD4, 0xC6, 0x35, 0xD7, 0xE7, 0x18, 0x34, 0xA4, 0xD7, - 0x25, 0x8B, 0xBA, 0xA6, 0x92, 0x58, 0xFB, 0xDE, 0xFC, 0x2A, 0xA9, 0xF6, - 0x6F, 0x80, 0xA3, 0x30, 0x36, 0xF2, 0x3E, 0x18, 0x0F, 0xB2, 0x4D, 0x30, - 0xE7, 0x2B, 0xFF, 0x42, 0xAA, 0xBD, 0x72, 0xEC, 0x70, 0x83, 0x22, 0x33, - 0x6F, 0xBD, 0xF2, 0x0C, 0x7E, 0x37, 0x1A, 0x0F, 0xE1, 0x17, 0xE5, 0xB8, - 0x64, 0x55, 0xC4, 0xE7, 0xD6, 0x15, 0x63, 0x5E, 0xAD, 0x67, 0xD4, 0x73, - 0x56, 0xF0, 0x2F, 0xC0, 0x9C, 0xF1, 0x7C, 0x65, 0x35, 0x8F, 0x2A, 0xCC, - 0xF9, 0x5C, 0xAC, 0x85, 0x79, 0xFD, 0x0E, 0x58, 0x62, 0xCD, 0x8B, 0x57, - 0x21, 0xD5, 0x24, 0x7C, 0x6F, 0x7C, 0xD6, 0x35, 0x99, 0x0D, 0xCF, 0x42, - 0xA0, 0xA5, 0x92, 0xDD, 0xDF, 0xE4, 0xF8, 0xD9, 0x3A, 0xD6, 0xA8, 0x3A, - 0xDF, 0x4B, 0xF7, 0xA8, 0x1D, 0x75, 0xE2, 0x42, 0xAD, 0xEF, 0xB5, 0x9A, - 0x5D, 0x5F, 0xFB, 0x0E, 0xBD, 0x31, 0x66, 0xD4, 0xC7, 0x91, 0x6A, 0x65, - 0x0E, 0x6D, 0xA8, 0x03, 0x10, 0x88, 0x21, 0x91, 0xD9, 0x59, 0x9A, 0x48, - 0x14, 0x12, 0x79, 0xD8, 0x6A, 0x22, 0x8F, 0x35, 0x11, 0x04, 0xC7, 0x3A, - 0xCD, 0xA3, 0xD5, 0x31, 0x44, 0x48, 0xB5, 0x49, 0xB6, 0xA9, 0xC8, 0xE2, - 0xF4, 0xAC, 0x7F, 0xBF, 0x9D, 0xF5, 0x45, 0x8E, 0x4E, 0xC3, 0xCD, 0x76, - 0xE7, 0x36, 0xCC, 0x0A, 0x5C, 0x30, 0xF6, 0xF7, 0x99, 0x65, 0x7A, 0x87, - 0xC1, 0x34, 0x7A, 0x0A, 0x0D, 0xC2, 0xF7, 0xA6, 0x65, 0xAD, 0xA9, 0x9F, - 0x04, 0x20, 0x90, 0xD7, 0x05, 0x0C, 0xB1, 0xA6, 0xBA, 0x31, 0xC0, 0x86, - 0xC0, 0x18, 0x21, 0xD5, 0x81, 0x59, 0x3B, 0xE9, 0x49, 0x98, 0xC3, 0xE1, - 0x5E, 0x02, 0x6E, 0xD1, 0xBE, 0xB7, 0x96, 0x65, 0xA7, 0x5A, 0xAA, 0xAA, - 0x2E, 0x42, 0xA4, 0x97, 0x9E, 0x33, 0x54, 0xD7, 0x87, 0x61, 0x50, 0xDF, - 0xAB, 0xC3, 0xD8, 0xD6, 0xC7, 0xC2, 0x1A, 0xEF, 0xAA, 0xA3, 0x30, 0x07, - 0xCB, 0x9D, 0xED, 0xBB, 0x45, 0x25, 0x90, 0x37, 0x81, 0x1F, 0x03, 0x7B, - 0x5B, 0x22, 0x5A, 0x6B, 0x87, 0xAD, 0xCD, 0x1C, 0xC7, 0x9A, 0xCE, 0x71, - 0xA5, 0xA0, 0xC1, 0x10, 0x6A, 0x46, 0x6B, 0xB8, 0x10, 0xDF, 0x7B, 0x72, - 0x0D, 0x21, 0x4B, 0xD5, 0x55, 0x1B, 0x8F, 0xAE, 0xE6, 0x30, 0x1B, 0xE3, - 0x94, 0x50, 0x65, 0xC7, 0xA2, 0x3B, 0xF0, 0x18, 0x52, 0xED, 0x97, 0x75, - 0xA6, 0x32, 0x09, 0x73, 0x60, 0xBE, 0x21, 0x30, 0x16, 0xDF, 0x9B, 0x94, - 0x93, 0xC8, 0x7C, 0x2F, 0xCC, 0xB4, 0xB2, 0xD0, 0x68, 0x6E, 0xAF, 0xD8, - 0x3F, 0x99, 0xB5, 0xB8, 0x27, 0xC6, 0x39, 0xA2, 0x27, 0xC6, 0x5D, 0xB8, - 0x83, 0x12, 0x48, 0xF1, 0x48, 0x44, 0xE3, 0x7B, 0x8F, 0xDB, 0xC1, 0x1D, - 0x6C, 0xC9, 0xA3, 0x53, 0x84, 0xDE, 0xFC, 0x17, 0x13, 0x61, 0xEE, 0xC8, - 0x23, 0x3C, 0x7A, 0x6A, 0x98, 0x88, 0x54, 0xE7, 0x75, 0x11, 0x4C, 0x5E, - 0xA5, 0xE9, 0xA1, 0xCD, 0x19, 0x84, 0xBF, 0x46, 0x08, 0x65, 0xC5, 0x22, - 0xA4, 0x8D, 0x4B, 0xEF, 0xFD, 0x98, 0x8F, 0xF7, 0xB8, 0x26, 0x9A, 0x49, - 0xB9, 0x90, 0xBD, 0xA3, 0xDD, 0x0A, 0x1B, 0x00, 0x2B, 0xA4, 0xDA, 0x40, - 0xC3, 0x9F, 0x5A, 0xBA, 0x59, 0xC0, 0x91, 0xDA, 0xF7, 0x16, 0x01, 0xCF, - 0xD8, 0xB3, 0x9F, 0x6B, 0x80, 0x3D, 0xB4, 0x39, 0xE3, 0x39, 0x39, 0xEB, - 0xFD, 0x8E, 0x07, 0x36, 0xD1, 0x46, 0x33, 0x18, 0x56, 0x03, 0x6F, 0x54, - 0x09, 0xF4, 0x4A, 0xCD, 0x5E, 0xF5, 0x30, 0x62, 0xA9, 0xD6, 0xAF, 0x63, - 0x3C, 0x84, 0x82, 0xC1, 0x1C, 0xB6, 0x5E, 0x01, 0xDC, 0x13, 0x83, 0xD7, - 0xD3, 0xBE, 0x57, 0xB7, 0xEC, 0x9C, 0xA1, 0xDA, 0xBA, 0x58, 0xE7, 0xD2, - 0xFE, 0xC2, 0xE2, 0x7E, 0x8C, 0x2B, 0x74, 0x27, 0xE0, 0x59, 0xA4, 0xF2, - 0xAB, 0xE1, 0xC5, 0x06, 0xE8, 0xA2, 0x4D, 0x4C, 0xCA, 0x70, 0x7B, 0xDD, - 0xB4, 0x18, 0x4C, 0x48, 0x57, 0xDE, 0xDA, 0x6C, 0x3A, 0xAF, 0xC6, 0x44, - 0x64, 0x62, 0x67, 0x6E, 0xA4, 0xE5, 0x74, 0x2D, 0x67, 0xAF, 0xD1, 0xDE, - 0xA4, 0xFA, 0x25, 0x26, 0xAE, 0xA5, 0x07, 0x30, 0xD6, 0x6E, 0x0E, 0x96, - 0x0B, 0x78, 0x58, 0x1B, 0x6F, 0xB6, 0x0D, 0x31, 0xE7, 0x15, 0xC3, 0x62, - 0x30, 0xBE, 0x8B, 0x10, 0xAB, 0x97, 0x6B, 0xBD, 0x0D, 0xF0, 0x87, 0x06, - 0xD8, 0xB4, 0x46, 0xAA, 0xA1, 0x75, 0x39, 0xCC, 0x80, 0x39, 0xD7, 0x93, - 0x71, 0x2A, 0x79, 0x02, 0xA8, 0x15, 0xF0, 0x42, 0xD6, 0x26, 0x64, 0x59, - 0x91, 0xE6, 0x35, 0xAF, 0xBA, 0xD5, 0xB6, 0x60, 0x0E, 0x94, 0xFA, 0x93, - 0xDB, 0x96, 0x1D, 0x84, 0x44, 0x1E, 0x41, 0xAA, 0x53, 0x2D, 0x79, 0x8C, - 0x21, 0xBF, 0x4F, 0x7B, 0x4B, 0xE4, 0xD1, 0x1F, 0xDF, 0x9B, 0xE5, 0xB8, - 0x20, 0x12, 0x56, 0x60, 0xDC, 0x65, 0xDF, 0x59, 0xA9, 0x59, 0xA2, 0x8D, - 0x57, 0xC9, 0x55, 0x46, 0xAE, 0x32, 0x17, 0xE3, 0x7A, 0xD8, 0x54, 0xE0, - 0x3E, 0x68, 0xEF, 0x8B, 0xD9, 0x3F, 0x1F, 0x75, 0x15, 0xE2, 0xA5, 0x32, - 0xBE, 0xC3, 0xB3, 0x59, 0x26, 0x98, 0x09, 0x48, 0x35, 0x01, 0xA9, 0x46, - 0xEB, 0x4C, 0x00, 0x69, 0xCB, 0x04, 0xB2, 0x46, 0x60, 0x77, 0x36, 0x04, - 0x90, 0x31, 0x51, 0x9D, 0x64, 0x83, 0xE3, 0xB0, 0x31, 0x24, 0x57, 0x5B, - 0xF3, 0xC9, 0xDE, 0xC0, 0x6B, 0x75, 0xB0, 0x74, 0xA5, 0x66, 0x09, 0xC6, - 0x33, 0xEC, 0x70, 0xE0, 0x0A, 0xA4, 0xDA, 0x3A, 0x44, 0xBF, 0xFF, 0x68, - 0x49, 0xE9, 0xA5, 0x34, 0x2C, 0x42, 0xAA, 0x99, 0x36, 0x06, 0xE3, 0x05, - 0x3B, 0xFE, 0x5F, 0x8B, 0xDC, 0x9E, 0x3F, 0x41, 0xBF, 0xCF, 0xCF, 0x30, - 0xE7, 0x53, 0x69, 0xAB, 0x3D, 0xBD, 0x50, 0x0F, 0xCB, 0xB4, 0x19, 0xAB, - 0xFB, 0x30, 0xF1, 0x37, 0xF3, 0x81, 0x93, 0xD2, 0x46, 0x13, 0xAA, 0x28, - 0x74, 0x32, 0x0E, 0x0F, 0x99, 0xF3, 0xA6, 0xE1, 0x48, 0xF5, 0x32, 0x52, - 0x3D, 0x59, 0x67, 0x34, 0xB2, 0x8B, 0x03, 0x98, 0xC2, 0x96, 0x67, 0x8D, - 0xC5, 0xB3, 0x34, 0x6E, 0x26, 0xF6, 0xD2, 0x66, 0x0D, 0xA3, 0x7D, 0xEF, - 0x7B, 0x4C, 0xB0, 0xEA, 0x2A, 0xCC, 0x39, 0xC8, 0xE8, 0x34, 0x2C, 0x59, - 0xAE, 0xF5, 0x22, 0x6B, 0x0E, 0x3D, 0x0F, 0x38, 0xB6, 0x0E, 0x7E, 0x16, - 0xB4, 0xDF, 0x1A, 0x7E, 0x8D, 0xB9, 0x7E, 0x8C, 0x86, 0x05, 0x48, 0xF5, - 0x5F, 0xA4, 0xFA, 0xAF, 0xDD, 0xD8, 0xF4, 0xB4, 0xCF, 0x2A, 0x7A, 0x49, - 0xF0, 0x58, 0x9B, 0x14, 0x3F, 0x8D, 0x24, 0x12, 0x45, 0x80, 0x1B, 0x12, - 0x31, 0xE4, 0x11, 0x25, 0xF1, 0xD9, 0x6C, 0x47, 0x1E, 0x05, 0x63, 0xB8, - 0x15, 0x26, 0xAB, 0x30, 0xF1, 0x0A, 0x02, 0x63, 0xC3, 0x36, 0xBE, 0xF1, - 0xBE, 0xF7, 0xE9, 0x3A, 0x1F, 0x88, 0xEF, 0xCD, 0xB1, 0xDA, 0x62, 0x06, - 0x77, 0x2F, 0x2B, 0x67, 0x39, 0x60, 0x53, 0x49, 0xF2, 0x08, 0xCC, 0x39, - 0xC6, 0x7A, 0xD6, 0xEC, 0x73, 0xBC, 0x15, 0x20, 0xE7, 0x86, 0x69, 0x6A, - 0x95, 0xEF, 0xA5, 0x85, 0xD9, 0x9D, 0x67, 0x9C, 0x0B, 0x6E, 0xB6, 0x11, - 0xC5, 0xE0, 0x7B, 0x37, 0x60, 0xBC, 0x96, 0x26, 0x59, 0x22, 0xE9, 0x62, - 0xC7, 0x2C, 0x8D, 0x71, 0x7B, 0x1E, 0x10, 0x32, 0xDB, 0xC1, 0x3D, 0x18, - 0x1B, 0xFF, 0x4A, 0xDB, 0xEF, 0xED, 0x30, 0x1E, 0x64, 0xAB, 0xED, 0xF8, - 0xF7, 0x6B, 0x6A, 0x2A, 0x8B, 0x30, 0x36, 0xB5, 0xD6, 0x8C, 0xF3, 0xA6, - 0xED, 0x73, 0x67, 0xFB, 0xDD, 0xAD, 0xC6, 0x1C, 0xF4, 0xFE, 0xA8, 0x52, - 0x33, 0xD6, 0x5A, 0x6F, 0xA5, 0x21, 0x96, 0x50, 0xC1, 0x9C, 0x0B, 0x9D, - 0x64, 0x85, 0xF0, 0x25, 0x18, 0xCF, 0xA9, 0xC0, 0x58, 0x5F, 0x88, 0xEB, - 0xAC, 0x16, 0x02, 0x70, 0x0A, 0x52, 0x0D, 0xB3, 0x63, 0x34, 0x1E, 0x73, - 0xAE, 0xF7, 0x1C, 0xC6, 0xD5, 0xB7, 0x06, 0x63, 0x9A, 0xD5, 0x98, 0x33, - 0xAF, 0x93, 0x9B, 0x9C, 0x9B, 0xB4, 0x84, 0xB1, 0x98, 0xB8, 0x95, 0x05, - 0xD6, 0xB2, 0xB4, 0x35, 0x8D, 0xC1, 0xA7, 0x29, 0xBB, 0x4E, 0xDE, 0x2D, - 0xF6, 0x78, 0x55, 0xB7, 0x59, 0x11, 0xB4, 0xB6, 0x39, 0x6B, 0xBB, 0x08, - 0x24, 0x12, 0x25, 0x73, 0xA8, 0x23, 0x8F, 0x02, 0x20, 0x1A, 0x17, 0xF4, - 0x32, 0xED, 0x7B, 0xCB, 0x85, 0x54, 0x7F, 0xB0, 0x79, 0x95, 0x10, 0x30, - 0x53, 0xFB, 0xDE, 0xE2, 0x16, 0x9A, 0xC8, 0x9C, 0x99, 0xCC, 0x13, 0x30, - 0x52, 0x87, 0x7B, 0xF6, 0x81, 0x19, 0x21, 0xA6, 0xD7, 0xFD, 0x08, 0xA6, - 0x35, 0x98, 0x20, 0x34, 0x80, 0x25, 0x7A, 0xDD, 0x7B, 0xEF, 0xC7, 0x78, - 0x4D, 0xD1, 0x09, 0xE6, 0xAD, 0x6A, 0x5C, 0x83, 0x6F, 0x02, 0x7D, 0xAC, - 0x06, 0xB0, 0x29, 0xB0, 0xA0, 0x06, 0x66, 0xC5, 0x40, 0xAF, 0x36, 0xF1, - 0x02, 0x08, 0x58, 0x9C, 0x6E, 0x6C, 0xE7, 0x0E, 0xAC, 0x1B, 0xF2, 0x46, - 0x42, 0xAC, 0x9E, 0xB7, 0x36, 0x41, 0x2E, 0x16, 0x52, 0x65, 0x47, 0x60, - 0xAF, 0xD2, 0x8D, 0xCF, 0x19, 0x0F, 0x8C, 0xB7, 0x91, 0xCD, 0xBD, 0x6D, - 0x5B, 0xB3, 0x73, 0x09, 0x7A, 0x61, 0xDA, 0x7F, 0x12, 0xA0, 0xB3, 0x60, - 0xC1, 0xCA, 0x75, 0xBF, 0x9B, 0x17, 0x80, 0x17, 0x6C, 0x24, 0xFD, 0xD6, - 0x18, 0xC7, 0x85, 0x65, 0x02, 0xBE, 0xC8, 0xC4, 0xD8, 0x04, 0xF8, 0x78, - 0xDE, 0x4B, 0xDB, 0xF1, 0xCA, 0x7E, 0xBF, 0x26, 0xCF, 0x79, 0x11, 0x38, - 0x50, 0x48, 0xD5, 0xCB, 0xBA, 0xC2, 0xD6, 0x09, 0x98, 0x91, 0xEF, 0x19, - 0x02, 0xF6, 0xB0, 0x1B, 0xDA, 0x55, 0x21, 0xE7, 0xF5, 0x57, 0x19, 0xCD, - 0x40, 0xE7, 0x20, 0x03, 0x21, 0xD5, 0x76, 0x76, 0x83, 0xB2, 0x52, 0xAF, - 0xBB, 0x7B, 0x4E, 0xE9, 0xC6, 0xF7, 0x58, 0x94, 0x6E, 0xEC, 0xFB, 0x97, - 0xC0, 0x20, 0xA4, 0xFA, 0x81, 0x15, 0xC2, 0x2B, 0x62, 0x30, 0x3D, 0xED, - 0x7B, 0xAB, 0x84, 0x54, 0x8F, 0xDB, 0x7E, 0x2E, 0xD7, 0x8D, 0xED, 0xBC, - 0x98, 0x69, 0x27, 0x06, 0x0B, 0xB3, 0x6D, 0x4E, 0xCB, 0xCF, 0x19, 0xAA, - 0x63, 0x52, 0x9D, 0xA4, 0x8D, 0xB9, 0x0A, 0xA0, 0x21, 0x6B, 0x5E, 0xDF, - 0xC7, 0x9C, 0x07, 0x76, 0xD3, 0x46, 0x86, 0x75, 0x02, 0xBE, 0xCD, 0xA4, - 0x3A, 0x69, 0x32, 0xE6, 0x6F, 0xA5, 0x73, 0xF5, 0xB5, 0xB1, 0xAD, 0x8F, - 0x80, 0xD3, 0xAA, 0xA5, 0xAA, 0xAA, 0x37, 0xA6, 0xB7, 0x5E, 0x76, 0xCC, - 0x67, 0x37, 0x89, 0x53, 0x2A, 0xF6, 0x37, 0xDD, 0xC6, 0x21, 0xD5, 0x36, - 0x11, 0x49, 0x24, 0x2A, 0x79, 0xCC, 0xC4, 0xA1, 0xB5, 0xE6, 0xF6, 0x38, - 0x2B, 0xDC, 0x5E, 0xD1, 0x10, 0xB7, 0x66, 0x9E, 0xF5, 0x81, 0x1B, 0xF1, - 0xBD, 0x2B, 0xDD, 0x00, 0x39, 0x38, 0x94, 0x7D, 0x53, 0xD8, 0x2E, 0x04, - 0xCD, 0xB6, 0x96, 0x44, 0xB6, 0x2D, 0xD1, 0x13, 0xBE, 0xB4, 0xE4, 0x31, - 0xC3, 0x2D, 0x99, 0x56, 0x5A, 0x98, 0x52, 0xAD, 0xAF, 0x61, 0x2A, 0x66, - 0x37, 0x55, 0x9F, 0xA5, 0x2D, 0xCF, 0x14, 0xB0, 0x4F, 0x00, 0x6D, 0xC5, - 0xC1, 0xC1, 0xA1, 0xC4, 0x88, 0xB5, 0x8B, 0xB7, 0x30, 0x9E, 0x50, 0xFD, - 0x09, 0x9E, 0x37, 0xC7, 0x91, 0x47, 0xE5, 0xA3, 0x1A, 0x93, 0x5E, 0x63, - 0x99, 0xFD, 0x77, 0x03, 0xC6, 0x2E, 0x3D, 0xD0, 0x91, 0x87, 0x83, 0x83, - 0xD3, 0x40, 0xDA, 0x82, 0x26, 0xE2, 0xC8, 0xA3, 0xCC, 0xB0, 0x69, 0xC1, - 0x7B, 0x09, 0x58, 0x14, 0xD4, 0x46, 0xEF, 0xE0, 0xE0, 0xE0, 0x08, 0x24, - 0x2A, 0x89, 0x6C, 0x67, 0x49, 0x64, 0x9B, 0x22, 0x90, 0xC7, 0x00, 0x57, - 0x24, 0xC7, 0xC1, 0xC1, 0xC1, 0xA1, 0xA3, 0x10, 0x48, 0x71, 0x48, 0xE4, - 0x2B, 0xAB, 0x79, 0x38, 0xF2, 0x70, 0x70, 0x70, 0x70, 0xE8, 0x50, 0x04, - 0x52, 0x18, 0x89, 0x38, 0xF2, 0x70, 0x70, 0x70, 0x70, 0xE8, 0xD0, 0x04, - 0x12, 0x8D, 0x44, 0x1C, 0x79, 0x38, 0x38, 0x38, 0x38, 0x38, 0x02, 0x59, - 0x43, 0x22, 0xDB, 0x5B, 0x12, 0x69, 0x29, 0xDD, 0xC3, 0xD7, 0x96, 0x3C, - 0xA6, 0xB9, 0x65, 0xE1, 0xE0, 0xE0, 0xE0, 0xE0, 0x08, 0x24, 0x28, 0x89, - 0x38, 0xF2, 0x70, 0x70, 0x70, 0x70, 0x70, 0x04, 0x12, 0x9A, 0x44, 0x1C, - 0x79, 0x38, 0x38, 0x38, 0x38, 0x44, 0x40, 0xAC, 0xC3, 0xBC, 0xA9, 0x49, - 0x41, 0x32, 0x00, 0xE3, 0x9E, 0x9B, 0x4D, 0x1E, 0x03, 0x1C, 0x79, 0x38, - 0x38, 0x38, 0x38, 0x38, 0x0D, 0x24, 0x88, 0x26, 0xB2, 0x83, 0xD5, 0x44, - 0x62, 0x96, 0x3C, 0x3E, 0x77, 0xCB, 0xC0, 0xC1, 0xC1, 0xC1, 0xC1, 0x21, - 0x28, 0x89, 0xEC, 0x88, 0x54, 0x3B, 0xBB, 0x81, 0x70, 0x70, 0x70, 0x70, - 0x88, 0x8E, 0xFF, 0x07, 0x02, 0x8E, 0xE9, 0xCF, 0xD4, 0x3B, 0x89, 0x6F, - 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82 -}; - -const unsigned int logo_web_png_len = 11136; diff --git a/SW/esplan_v2_renke_weather_station/logo_web.h b/SW/esplan_v2_renke_weather_station/logo_web.h deleted file mode 100755 index 891f5cf..0000000 --- a/SW/esplan_v2_renke_weather_station/logo_web.h +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once -#include -#include -#include - -extern const uint8_t logo_web_png[]; -extern const size_t logo_web_png_len;