mirror of https://github.com/LaskaKit/ESPlan.git
update
parent
e9077a979a
commit
0763f5b446
|
|
@ -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 <Arduino.h>
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include <ETH.h>
|
||||||
|
#include <Preferences.h>
|
||||||
|
|
||||||
|
#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);
|
||||||
|
}
|
||||||
|
|
@ -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 <Arduino.h>
|
||||||
|
#include <Preferences.h>
|
||||||
|
|
||||||
|
// ─── 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
|
||||||
|
|
@ -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 <Arduino.h>
|
||||||
|
|
||||||
|
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
|
||||||
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 <Arduino.h>
|
||||||
|
#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
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,661 @@
|
||||||
|
/*
|
||||||
|
* ws_server.cpp – HTTP webserver pro RS485 rele modul (8 / 16 kanalu)
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "ws_server.h"
|
||||||
|
#include <ETH.h>
|
||||||
|
|
||||||
|
// ─── 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("<!DOCTYPE html><html lang='");
|
||||||
|
out += cz ? "cs" : "en";
|
||||||
|
out += F("'><head><meta charset='UTF-8'>"
|
||||||
|
"<meta name='viewport' content='width=device-width,initial-scale=1'>"
|
||||||
|
"<title>");
|
||||||
|
out += s.title;
|
||||||
|
out += F("</title><style>");
|
||||||
|
out += FPSTR(CSS);
|
||||||
|
out += F("</style></head><body>"
|
||||||
|
"<nav class='topbar'>"
|
||||||
|
"<div class='logo'>"
|
||||||
|
"<div class='logo-icon'>R</div>");
|
||||||
|
out += s.title;
|
||||||
|
out += F("</div><div class='nav-links'>");
|
||||||
|
out += F("<a href='/' class='nav-link");
|
||||||
|
if (strcmp(page,"status")==0) out += F(" active");
|
||||||
|
out += F("'>");
|
||||||
|
out += s.navStatus;
|
||||||
|
out += F("</a><a href='/config' class='nav-link");
|
||||||
|
if (strcmp(page,"config")==0) out += F(" active");
|
||||||
|
out += F("'>");
|
||||||
|
out += s.navConfig;
|
||||||
|
out += F("</a></div><a href='");
|
||||||
|
out += langUrl;
|
||||||
|
out += F("' class='lang-btn'>");
|
||||||
|
out += s.langSwitch;
|
||||||
|
out += F("</a></nav><div class='main'>");
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String htmlFoot() {
|
||||||
|
return F("</div></body></html>");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 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("<div class='ip-bar'>"
|
||||||
|
"<span><span class='ip-lbl'>IP: </span><b>");
|
||||||
|
html += ETH.localIP().toString();
|
||||||
|
html += F("</b></span><span><span class='ip-lbl'>MAC: </span>");
|
||||||
|
html += ETH.macAddress();
|
||||||
|
html += F("</span><span><span class='ip-lbl'>Modbus: </span>");
|
||||||
|
html += gCfg->modbusAddr;
|
||||||
|
html += F(" @ ");
|
||||||
|
html += gCfg->baudRate;
|
||||||
|
html += F(" bps</span><span><span class='ip-lbl'>CH: </span>");
|
||||||
|
html += gCfg->channelCount;
|
||||||
|
html += F("</span></div>");
|
||||||
|
|
||||||
|
if (gState->commError) {
|
||||||
|
html += F("<div class='err-banner'>⚠ ");
|
||||||
|
html += s.commError;
|
||||||
|
html += F(": ");
|
||||||
|
html += gState->errorMsg;
|
||||||
|
html += F("</div>");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tabulka kanalu
|
||||||
|
html += F("<h2>");
|
||||||
|
html += s.sectionRelays;
|
||||||
|
html += F("</h2><div class='card'><table>"
|
||||||
|
"<thead><tr><th class='ch-num'>");
|
||||||
|
html += s.colChannel;
|
||||||
|
html += F("</th><th>");
|
||||||
|
html += s.colName;
|
||||||
|
html += F("</th><th>");
|
||||||
|
html += s.colState;
|
||||||
|
html += F("</th><th>");
|
||||||
|
html += s.colCtrl;
|
||||||
|
html += F("</th></tr></thead><tbody>");
|
||||||
|
|
||||||
|
String langInp = cz ? "cs" : "en";
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < gCfg->channelCount; i++) {
|
||||||
|
bool on = gState->channels[i];
|
||||||
|
html += F("<tr><td class='ch-num'>");
|
||||||
|
html += (i + 1);
|
||||||
|
html += F("</td><td>");
|
||||||
|
html += gCfg->names[i];
|
||||||
|
html += F("</td><td id='st_");
|
||||||
|
html += i;
|
||||||
|
html += F("'>");
|
||||||
|
if (on) {
|
||||||
|
html += F("<span class='state-on'><span class='dot dot-g'></span>");
|
||||||
|
html += s.stateOn;
|
||||||
|
html += F("</span>");
|
||||||
|
} else {
|
||||||
|
html += F("<span class='state-off'><span class='dot dot-x'></span>");
|
||||||
|
html += s.stateOff;
|
||||||
|
html += F("</span>");
|
||||||
|
}
|
||||||
|
html += F("</td><td><div class='actions-row'>");
|
||||||
|
|
||||||
|
auto addBtn = [&](const char* act, const char* cls, const char* label) {
|
||||||
|
html += F("<form method='POST' action='/relay' style='display:inline'>"
|
||||||
|
"<input type='hidden' name='ch' value='");
|
||||||
|
html += (i + 1);
|
||||||
|
html += F("'><input type='hidden' name='act' value='");
|
||||||
|
html += act;
|
||||||
|
html += F("'><input type='hidden' name='lang' value='");
|
||||||
|
html += langInp;
|
||||||
|
html += F("'><button type='submit' class='btn ");
|
||||||
|
html += cls;
|
||||||
|
html += F(" btn-sm'>");
|
||||||
|
html += label;
|
||||||
|
html += F("</button></form>");
|
||||||
|
};
|
||||||
|
|
||||||
|
addBtn("on", "btn-g", s.btnOn);
|
||||||
|
addBtn("off", "btn-r", s.btnOff);
|
||||||
|
addBtn("toggle", "btn-d", s.btnToggle);
|
||||||
|
|
||||||
|
html += F("</div></td></tr>");
|
||||||
|
}
|
||||||
|
html += F("</tbody></table></div>");
|
||||||
|
|
||||||
|
// Specialni rezimy (Latch / Momentary / Delay) ako kompaktni panel
|
||||||
|
html += F("<h2>");
|
||||||
|
html += s.sectionAdvanced;
|
||||||
|
html += F("</h2><div class='card'>");
|
||||||
|
html += F("<div class='ht' style='margin-bottom:10px'>");
|
||||||
|
html += s.labelLatchHelp;
|
||||||
|
html += F(" — ");
|
||||||
|
html += s.labelMomentaryHelp;
|
||||||
|
html += F(" — ");
|
||||||
|
html += s.labelDelayHelp;
|
||||||
|
html += F("</div>");
|
||||||
|
|
||||||
|
// Latch / Momentary / Delay – pro kazdy kanal radek
|
||||||
|
html += F("<table><thead><tr><th class='ch-num'>");
|
||||||
|
html += s.colChannel;
|
||||||
|
html += F("</th><th>");
|
||||||
|
html += s.colName;
|
||||||
|
html += F("</th><th>");
|
||||||
|
html += s.colCtrl;
|
||||||
|
html += F("</th></tr></thead><tbody>");
|
||||||
|
for (uint8_t i = 0; i < gCfg->channelCount; i++) {
|
||||||
|
html += F("<tr><td class='ch-num'>");
|
||||||
|
html += (i + 1);
|
||||||
|
html += F("</td><td>");
|
||||||
|
html += gCfg->names[i];
|
||||||
|
html += F("</td><td><div class='actions-row'>");
|
||||||
|
|
||||||
|
// Latch
|
||||||
|
html += F("<form method='POST' action='/relay' style='display:inline'>"
|
||||||
|
"<input type='hidden' name='ch' value='");
|
||||||
|
html += (i + 1);
|
||||||
|
html += F("'><input type='hidden' name='act' value='latch'>"
|
||||||
|
"<input type='hidden' name='lang' value='");
|
||||||
|
html += langInp;
|
||||||
|
html += F("'><button type='submit' class='btn btn-w btn-sm'>");
|
||||||
|
html += s.btnLatch;
|
||||||
|
html += F("</button></form>");
|
||||||
|
|
||||||
|
// Momentary (Pulse)
|
||||||
|
html += F("<form method='POST' action='/relay' style='display:inline'>"
|
||||||
|
"<input type='hidden' name='ch' value='");
|
||||||
|
html += (i + 1);
|
||||||
|
html += F("'><input type='hidden' name='act' value='momentary'>"
|
||||||
|
"<input type='hidden' name='lang' value='");
|
||||||
|
html += langInp;
|
||||||
|
html += F("'><button type='submit' class='btn btn-pp btn-sm'>");
|
||||||
|
html += s.btnMomentary;
|
||||||
|
html += F("</button></form>");
|
||||||
|
|
||||||
|
// Delay s inputem sekund
|
||||||
|
html += F("<form method='POST' action='/relay' style='display:inline'>"
|
||||||
|
"<input type='hidden' name='ch' value='");
|
||||||
|
html += (i + 1);
|
||||||
|
html += F("'><input type='hidden' name='act' value='delay'>"
|
||||||
|
"<input type='hidden' name='lang' value='");
|
||||||
|
html += langInp;
|
||||||
|
html += F("'><span class='adv-inline'>"
|
||||||
|
"<input type='number' name='sec' min='0' max='255' value='");
|
||||||
|
html += gCfg->defaultDelay;
|
||||||
|
html += F("'><button type='submit' class='btn btn-w btn-sm'>");
|
||||||
|
html += s.btnDelay;
|
||||||
|
html += F(" ");
|
||||||
|
html += s.labelDelaySec;
|
||||||
|
html += F("</button></span></form>");
|
||||||
|
|
||||||
|
html += F("</div></td></tr>");
|
||||||
|
}
|
||||||
|
html += F("</tbody></table>");
|
||||||
|
html += F("</div>");
|
||||||
|
|
||||||
|
// Hromadne ovladani
|
||||||
|
html += F("<h2>");
|
||||||
|
html += s.sectionActions;
|
||||||
|
html += F("</h2><div class='card'><div class='bulk-row'>"
|
||||||
|
"<form method='POST' action='/relay'>"
|
||||||
|
"<input type='hidden' name='act' value='all_on'>"
|
||||||
|
"<input type='hidden' name='lang' value='");
|
||||||
|
html += langInp;
|
||||||
|
html += F("'><button type='submit' class='btn btn-g'>");
|
||||||
|
html += s.btnAllOn;
|
||||||
|
html += F("</button></form>"
|
||||||
|
"<form method='POST' action='/relay'>"
|
||||||
|
"<input type='hidden' name='act' value='all_off'>"
|
||||||
|
"<input type='hidden' name='lang' value='");
|
||||||
|
html += langInp;
|
||||||
|
html += F("'><button type='submit' class='btn btn-r'>");
|
||||||
|
html += s.btnAllOff;
|
||||||
|
html += F("</button></form>"
|
||||||
|
"</div>");
|
||||||
|
if (gState->lastAction.length() > 0) {
|
||||||
|
html += F("<div class='ht' style='margin-top:10px'>");
|
||||||
|
html += s.lastAction;
|
||||||
|
html += F(": <span class='smeta'>");
|
||||||
|
html += gState->lastAction;
|
||||||
|
html += F("</span></div>");
|
||||||
|
}
|
||||||
|
html += F("</div>");
|
||||||
|
|
||||||
|
// Raw RS485 panel
|
||||||
|
html += F("<h2>");
|
||||||
|
html += s.sectionRaw;
|
||||||
|
html += F("</h2><div class='raw-panel'>"
|
||||||
|
"<div><span class='raw-label'>TX:</span> <span class='raw-tx' id='rawTx'>...</span>"
|
||||||
|
" <span class='raw-status' id='rawSt'></span></div>"
|
||||||
|
"<div><span class='raw-label'>RX:</span> <span class='raw-rx' id='rawRx'>...</span></div>"
|
||||||
|
"</div>");
|
||||||
|
|
||||||
|
// JS: refresh stavu + raw panelu
|
||||||
|
html += F("<script>"
|
||||||
|
"var SON='");
|
||||||
|
html += s.stateOn;
|
||||||
|
html += F("',SOFF='");
|
||||||
|
html += s.stateOff;
|
||||||
|
html += F("';"
|
||||||
|
"function setSt(i,on){"
|
||||||
|
"var e=document.getElementById('st_'+i);"
|
||||||
|
"if(!e)return;"
|
||||||
|
"e.innerHTML=on?"
|
||||||
|
"(\"<span class='state-on'><span class='dot dot-g'></span>\"+SON+\"</span>\"):"
|
||||||
|
"(\"<span class='state-off'><span class='dot dot-x'></span>\"+SOFF+\"</span>\");"
|
||||||
|
"}"
|
||||||
|
"function upd(){"
|
||||||
|
"fetch('/api/state').then(function(r){return r.json()}).then(function(d){"
|
||||||
|
"if(d.ch){for(var i=0;i<d.ch.length;i++)setSt(i,d.ch[i]);}"
|
||||||
|
"}).catch(function(){});"
|
||||||
|
"fetch('/api/raw').then(function(r){return r.json()}).then(function(d){"
|
||||||
|
"var tx=document.getElementById('rawTx');"
|
||||||
|
"var rx=document.getElementById('rawRx');"
|
||||||
|
"var st=document.getElementById('rawSt');"
|
||||||
|
"if(tx)tx.textContent=d.tx||'-';"
|
||||||
|
"if(rx){rx.textContent=d.rx||'-';"
|
||||||
|
"rx.className=(d.s==='OK')?'raw-rx':'raw-err';}"
|
||||||
|
"if(st){st.textContent=d.s;"
|
||||||
|
"st.className='raw-status '+(d.s==='OK'?'ok':'err');}"
|
||||||
|
"}).catch(function(){});"
|
||||||
|
"}"
|
||||||
|
"upd();setInterval(upd,3000);"
|
||||||
|
"</script>");
|
||||||
|
|
||||||
|
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("<h1>");
|
||||||
|
html += s.pageConfigTitle;
|
||||||
|
html += F("</h1>");
|
||||||
|
|
||||||
|
// Info banner o DIP
|
||||||
|
html += F("<div class='info-banner'>");
|
||||||
|
html += s.infoDip;
|
||||||
|
html += F("</div>");
|
||||||
|
|
||||||
|
html += F("<form method='POST' action='/config'>");
|
||||||
|
html += F("<input type='hidden' name='lang' value='");
|
||||||
|
html += cz ? "cs" : "en";
|
||||||
|
html += F("'>");
|
||||||
|
|
||||||
|
// Komunikace
|
||||||
|
html += F("<h2>");
|
||||||
|
html += s.sectionComm;
|
||||||
|
html += F("</h2><div class='card'><div class='frow'>");
|
||||||
|
|
||||||
|
html += F("<div class='fg'><label>");
|
||||||
|
html += s.cfgAddr;
|
||||||
|
html += F("</label><input type='number' name='addr' min='0' max='47' value='");
|
||||||
|
html += gCfg->modbusAddr;
|
||||||
|
html += F("'><div class='ht'>");
|
||||||
|
html += s.cfgAddrHelp;
|
||||||
|
html += F("</div></div>");
|
||||||
|
|
||||||
|
html += F("<div class='fg'><label>");
|
||||||
|
html += s.cfgBaud;
|
||||||
|
html += F("</label><select name='baud'>");
|
||||||
|
const uint32_t bauds[] = {2400, 4800, 9600, 19200};
|
||||||
|
for (auto b : bauds) {
|
||||||
|
html += F("<option value='");
|
||||||
|
html += b;
|
||||||
|
html += F("'");
|
||||||
|
if (b == gCfg->baudRate) html += F(" selected");
|
||||||
|
html += F(">");
|
||||||
|
html += b;
|
||||||
|
html += F(" bps</option>");
|
||||||
|
}
|
||||||
|
html += F("</select><div class='ht'>");
|
||||||
|
html += s.cfgBaudHelp;
|
||||||
|
html += F("</div></div>");
|
||||||
|
|
||||||
|
html += F("<div class='fg'><label>");
|
||||||
|
html += s.cfgChannels;
|
||||||
|
html += F("</label><select name='chCount'>"
|
||||||
|
"<option value='8'");
|
||||||
|
if (gCfg->channelCount == 8) html += F(" selected");
|
||||||
|
html += F(">8</option><option value='16'");
|
||||||
|
if (gCfg->channelCount == 16) html += F(" selected");
|
||||||
|
html += F(">16</option></select><div class='ht'>");
|
||||||
|
html += s.cfgChannelsHelp;
|
||||||
|
html += F("</div></div>");
|
||||||
|
|
||||||
|
html += F("<div class='fg'><label>");
|
||||||
|
html += s.cfgDefDelay;
|
||||||
|
html += F("</label><input type='number' name='defDelay' min='0' max='255' value='");
|
||||||
|
html += gCfg->defaultDelay;
|
||||||
|
html += F("'><div class='ht'>");
|
||||||
|
html += s.cfgDefDelayHelp;
|
||||||
|
html += F("</div></div>");
|
||||||
|
|
||||||
|
html += F("</div></div>");
|
||||||
|
|
||||||
|
// Nazvy kanalu
|
||||||
|
html += F("<h2>");
|
||||||
|
html += s.sectionNames;
|
||||||
|
html += F("</h2><div class='card'><div class='ht' style='margin-bottom:10px'>");
|
||||||
|
html += s.cfgNameHelp;
|
||||||
|
html += F("</div><div class='frow'>");
|
||||||
|
for (uint8_t i = 0; i < gCfg->channelCount; i++) {
|
||||||
|
html += F("<div class='fg' style='min-width:170px'><label>#");
|
||||||
|
html += (i + 1);
|
||||||
|
html += F("</label><input class='name-input' type='text' name='n");
|
||||||
|
html += i;
|
||||||
|
html += F("' maxlength='15' value='");
|
||||||
|
html += gCfg->names[i];
|
||||||
|
html += F("'></div>");
|
||||||
|
}
|
||||||
|
html += F("</div></div>");
|
||||||
|
|
||||||
|
html += F("<div style='margin-top:8px'>"
|
||||||
|
"<button type='submit' class='btn btn-p'>");
|
||||||
|
html += s.btnSave;
|
||||||
|
html += F("</button></div></form>");
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
@ -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 <WebServer.h>
|
||||||
|
#include <Preferences.h>
|
||||||
|
#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
|
||||||
|
|
@ -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 <Arduino.h>
|
||||||
|
#include <WiFi.h> // nutné pro WiFiEvent_t a WiFi.onEvent
|
||||||
|
#include <ETH.h>
|
||||||
|
#include <Preferences.h>
|
||||||
|
|
||||||
|
#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);
|
||||||
|
}
|
||||||
|
|
@ -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 <Arduino.h>
|
||||||
|
#include <Preferences.h>
|
||||||
|
|
||||||
|
// ─── 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
|
||||||
|
|
@ -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 <Arduino.h>
|
||||||
|
|
||||||
|
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
|
||||||
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 <Arduino.h>
|
||||||
|
#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
|
||||||
|
|
@ -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] +--------------------------+"));
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,710 @@
|
||||||
|
/*
|
||||||
|
* ws_server.cpp – HTTP webserver implementace
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "ws_server.h"
|
||||||
|
#include <ETH.h>
|
||||||
|
|
||||||
|
// ─── 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("<!DOCTYPE html><html lang='");
|
||||||
|
out += cz ? "cs" : "en";
|
||||||
|
out += F("'><head><meta charset='UTF-8'>"
|
||||||
|
"<meta name='viewport' content='width=device-width,initial-scale=1'>"
|
||||||
|
"<title>");
|
||||||
|
out += s.title;
|
||||||
|
out += F("</title><style>");
|
||||||
|
out += FPSTR(CSS);
|
||||||
|
out += F("</style></head><body>"
|
||||||
|
"<nav class='topbar'>"
|
||||||
|
"<div class='logo'>"
|
||||||
|
"<div class='logo-icon'>"
|
||||||
|
"<svg width='18' height='18' viewBox='0 0 18 18' fill='none'>"
|
||||||
|
"<circle cx='9' cy='9' r='3' stroke='#58a6ff' stroke-width='1.5'/>"
|
||||||
|
"<line x1='9' y1='2' x2='9' y2='5' stroke='#58a6ff' stroke-width='1.5' stroke-linecap='round'/>"
|
||||||
|
"<line x1='9' y1='13' x2='9' y2='16' stroke='#58a6ff' stroke-width='1.5' stroke-linecap='round'/>"
|
||||||
|
"<line x1='2' y1='9' x2='5' y2='9' stroke='#58a6ff' stroke-width='1.5' stroke-linecap='round'/>"
|
||||||
|
"<line x1='13' y1='9' x2='16' y2='9' stroke='#58a6ff' stroke-width='1.5' stroke-linecap='round'/>"
|
||||||
|
"</svg></div>");
|
||||||
|
out += s.title;
|
||||||
|
out += F("</div><div class='nav-links'>");
|
||||||
|
out += F("<a href='/' class='nav-link");
|
||||||
|
if (strcmp(page,"status")==0) out += F(" active");
|
||||||
|
out += F("'>");
|
||||||
|
out += s.navStatus;
|
||||||
|
out += F("</a><a href='/config' class='nav-link");
|
||||||
|
if (strcmp(page,"config")==0) out += F(" active");
|
||||||
|
out += F("'>");
|
||||||
|
out += s.navConfig;
|
||||||
|
out += F("</a></div><a href='");
|
||||||
|
out += langUrl;
|
||||||
|
out += F("' class='lang-btn'>");
|
||||||
|
out += s.langSwitch;
|
||||||
|
out += F("</a></nav><div class='main'>");
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String htmlFoot() {
|
||||||
|
return F("</div></body></html>");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 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("<div class='ip-bar'>"
|
||||||
|
"<span><span class='ip-lbl'>IP: </span><b>");
|
||||||
|
html += ETH.localIP().toString();
|
||||||
|
html += F("</b></span><span><span class='ip-lbl'>MAC: </span>");
|
||||||
|
html += ETH.macAddress();
|
||||||
|
html += F("</span><span><span class='ip-lbl'>ETH: </span>");
|
||||||
|
html += ETH.linkSpeed();
|
||||||
|
html += F(" Mbit/s</span></div>");
|
||||||
|
|
||||||
|
if (gData->commError) {
|
||||||
|
html += F("<div class='err-banner'>⚠ ");
|
||||||
|
html += s.commError;
|
||||||
|
html += F(": ");
|
||||||
|
html += gData->errorMsg;
|
||||||
|
html += F("</div>");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toolbar
|
||||||
|
html += F("<div class='toolbar'>"
|
||||||
|
"<form method='POST' action='/detect' style='display:inline'>"
|
||||||
|
"<button type='submit' class='btn btn-d btn-sm'>");
|
||||||
|
html += s.btnDetect;
|
||||||
|
html += F("</button></form>"
|
||||||
|
"<span class='live-dot'></span>"
|
||||||
|
"<span id='updInfo' class='smeta'></span>");
|
||||||
|
html += F("</div>");
|
||||||
|
|
||||||
|
// Tabulka detekce
|
||||||
|
html += F("<h2>");
|
||||||
|
html += s.sectionSensors;
|
||||||
|
html += F("</h2><div class='card'><table>"
|
||||||
|
"<thead><tr><th>");
|
||||||
|
html += s.colSensor;
|
||||||
|
html += F("</th><th>");
|
||||||
|
html += s.colPresent;
|
||||||
|
html += F("</th></tr></thead><tbody>");
|
||||||
|
|
||||||
|
auto row = [&](const char* name, bool present) {
|
||||||
|
html += F("<tr><td>");
|
||||||
|
html += name;
|
||||||
|
html += F("</td><td>");
|
||||||
|
if (present) {
|
||||||
|
html += F("<span class='badge-yes'><span class='dot dot-g'></span>");
|
||||||
|
html += s.yes;
|
||||||
|
} else {
|
||||||
|
html += F("<span class='badge-no'><span class='dot dot-x'></span>");
|
||||||
|
html += s.no;
|
||||||
|
}
|
||||||
|
html += F("</span></td></tr>");
|
||||||
|
};
|
||||||
|
|
||||||
|
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("</tbody></table></div>");
|
||||||
|
|
||||||
|
// Tabulka hodnot
|
||||||
|
html += F("<h2>");
|
||||||
|
html += s.sectionValues;
|
||||||
|
html += F("</h2><div class='card'><table><thead><tr><th>");
|
||||||
|
html += s.colSensor;
|
||||||
|
html += F("</th><th>");
|
||||||
|
html += s.colValue;
|
||||||
|
html += F("</th><th>");
|
||||||
|
html += s.colUnit;
|
||||||
|
html += F("</th></tr></thead><tbody>");
|
||||||
|
|
||||||
|
auto valRow = [&](const char* id, const char* name, bool present,
|
||||||
|
const String& val, const char* unit) {
|
||||||
|
html += F("<tr><td>");
|
||||||
|
html += name;
|
||||||
|
html += F("</td><td class='val' id='v_");
|
||||||
|
html += id;
|
||||||
|
html += F("'>");
|
||||||
|
html += present ? val : s.noData;
|
||||||
|
html += F("</td><td class='unit' id='u_");
|
||||||
|
html += id;
|
||||||
|
html += F("'>");
|
||||||
|
html += present ? unit : "";
|
||||||
|
html += F("</td></tr>");
|
||||||
|
};
|
||||||
|
|
||||||
|
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("</tbody></table></div>");
|
||||||
|
|
||||||
|
// ── Raw RS485 panel ────────────────────────────────────────────────────
|
||||||
|
html += F("<h2>");
|
||||||
|
html += s.sectionRaw;
|
||||||
|
html += F("</h2><div class='raw-panel'>"
|
||||||
|
"<div><span class='raw-label'>TX:</span> <span class='raw-tx' id='rawTx'>...</span>"
|
||||||
|
" <span class='raw-status' id='rawSt'></span></div>"
|
||||||
|
"<div><span class='raw-label'>RX:</span> <span class='raw-rx' id='rawRx'>...</span></div>"
|
||||||
|
"</div>");
|
||||||
|
|
||||||
|
// Inline JavaScript – AJAX auto-refresh
|
||||||
|
html += F("<script>"
|
||||||
|
"var dn=['N','NE','E','SE','S','SW','W','NW'];"
|
||||||
|
"function u(id,v){"
|
||||||
|
"var e=document.getElementById(id);"
|
||||||
|
"if(!e)return;"
|
||||||
|
"var s=String(v);"
|
||||||
|
"if(e.textContent!==s){"
|
||||||
|
"e.textContent=s;"
|
||||||
|
"e.classList.remove('flash');"
|
||||||
|
"void e.offsetWidth;"
|
||||||
|
"e.classList.add('flash');"
|
||||||
|
"}"
|
||||||
|
"}"
|
||||||
|
"function updRaw(){"
|
||||||
|
"fetch('/api/raw').then(function(r){return r.json()}).then(function(d){"
|
||||||
|
"var tx=document.getElementById('rawTx');"
|
||||||
|
"var rx=document.getElementById('rawRx');"
|
||||||
|
"var st=document.getElementById('rawSt');"
|
||||||
|
"if(tx)tx.textContent=d.tx||'—';"
|
||||||
|
"if(rx){rx.textContent=d.rx||'—';"
|
||||||
|
"rx.className=(d.s==='OK')?'raw-rx':'raw-err';}"
|
||||||
|
"if(st){st.textContent=d.s;"
|
||||||
|
"st.className='raw-status '+(d.s==='OK'?'ok':'err');}"
|
||||||
|
"}).catch(function(){});"
|
||||||
|
"}"
|
||||||
|
"function upd(){"
|
||||||
|
"fetch('/api/data').then(function(r){return r.json()}).then(function(d){"
|
||||||
|
"if(d.err){"
|
||||||
|
"document.getElementById('updInfo').textContent='\\u26A0 error';"
|
||||||
|
"return;"
|
||||||
|
"}"
|
||||||
|
"if(d.windSpeed!==undefined){"
|
||||||
|
"u('v_ws',d.windSpeed.toFixed(1));"
|
||||||
|
"u('v_wf',d.windForce);"
|
||||||
|
"u('v_wd',d.windDir8+' ('+(dn[d.windDir8]||'?')+')');"
|
||||||
|
"u('v_w3',d.windDir360);"
|
||||||
|
"}"
|
||||||
|
"if(d.temp!==undefined) u('v_te',d.temp.toFixed(1));"
|
||||||
|
"if(d.hum!==undefined) u('v_hu',d.hum.toFixed(1));"
|
||||||
|
"if(d.pres!==undefined) u('v_pr',d.pres.toFixed(2));"
|
||||||
|
"if(d.noise!==undefined) u('v_no',d.noise.toFixed(1));"
|
||||||
|
"if(d.pm25!==undefined) u('v_p2',d.pm25);"
|
||||||
|
"if(d.pm10!==undefined) u('v_p1',d.pm10);"
|
||||||
|
"if(d.co2!==undefined) u('v_co',d.co2);"
|
||||||
|
"if(d.lux!==undefined) u('v_lx',d.lux);"
|
||||||
|
"if(d.rain!==undefined) u('v_rn',d.rain.toFixed(1));"
|
||||||
|
"if(d.compass!==undefined) u('v_cp',d.compass.toFixed(1));"
|
||||||
|
"if(d.solar!==undefined) u('v_sl',d.solar);"
|
||||||
|
"document.getElementById('updInfo').textContent="
|
||||||
|
"new Date().toLocaleTimeString();"
|
||||||
|
"}).catch(function(){"
|
||||||
|
"document.getElementById('updInfo').textContent='\\u26A0 offline';"
|
||||||
|
"});"
|
||||||
|
"}"
|
||||||
|
"function tick(){upd();updRaw();}"
|
||||||
|
"tick();setInterval(tick,5000);"
|
||||||
|
"</script>");
|
||||||
|
|
||||||
|
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("<h1>");
|
||||||
|
html += s.pageConfigTitle;
|
||||||
|
html += F("</h1><form method='POST' action='/config'>");
|
||||||
|
html += F("<input type='hidden' name='lang' value='");
|
||||||
|
html += cz ? "cs" : "en";
|
||||||
|
html += F("'>");
|
||||||
|
|
||||||
|
// Komunikace
|
||||||
|
html += F("<h2>");
|
||||||
|
html += s.sectionComm;
|
||||||
|
html += F("</h2><div class='card'><div class='frow'>");
|
||||||
|
|
||||||
|
html += F("<div class='fg'><label>");
|
||||||
|
html += s.cfgAddr;
|
||||||
|
html += F("</label><input type='number' name='addr' min='1' max='247' value='");
|
||||||
|
html += gCfg->modbusAddr;
|
||||||
|
html += F("'><div class='ht'>");
|
||||||
|
html += s.cfgAddrHelp;
|
||||||
|
html += F("</div></div>");
|
||||||
|
|
||||||
|
html += F("<div class='fg'><label>");
|
||||||
|
html += s.cfgBaud;
|
||||||
|
html += F("</label><select name='baud'>");
|
||||||
|
const uint32_t bauds[] = {1200,2400,4800,9600,19200,38400,57600,115200};
|
||||||
|
for (auto b : bauds) {
|
||||||
|
html += F("<option value='");
|
||||||
|
html += b;
|
||||||
|
html += F("'");
|
||||||
|
if (b == gCfg->baudRate) html += F(" selected");
|
||||||
|
html += F(">");
|
||||||
|
html += b;
|
||||||
|
html += F(" bps</option>");
|
||||||
|
}
|
||||||
|
html += F("</select><div class='ht'>");
|
||||||
|
html += s.cfgBaudHelp;
|
||||||
|
html += F("</div></div>");
|
||||||
|
|
||||||
|
html += F("<div class='fg'><label>");
|
||||||
|
html += s.cfgPoll;
|
||||||
|
html += F("</label><input type='number' name='poll' min='1' max='60' value='");
|
||||||
|
html += gCfg->pollInterval;
|
||||||
|
html += F("'><div class='ht'>");
|
||||||
|
html += s.cfgPollHelp;
|
||||||
|
html += F("</div></div></div></div>");
|
||||||
|
|
||||||
|
// Kalibrace
|
||||||
|
html += F("<h2>");
|
||||||
|
html += s.sectionCalib;
|
||||||
|
html += F("</h2><div class='card'><div class='frow'>");
|
||||||
|
|
||||||
|
html += F("<div class='fg'><label>");
|
||||||
|
html += s.cfgWindDirOffset;
|
||||||
|
html += F("</label><select name='wdOffset'>"
|
||||||
|
"<option value='0'");
|
||||||
|
if (gCfg->windDirOffset == 0) html += F(" selected");
|
||||||
|
html += F(">");
|
||||||
|
html += s.cfgWindDirOff0;
|
||||||
|
html += F("</option><option value='1'");
|
||||||
|
if (gCfg->windDirOffset == 1) html += F(" selected");
|
||||||
|
html += F(">");
|
||||||
|
html += s.cfgWindDirOff1;
|
||||||
|
html += F("</option></select><div class='ht'>");
|
||||||
|
html += s.cfgWindDirOffsetHelp;
|
||||||
|
html += F("</div></div>");
|
||||||
|
|
||||||
|
char rainHex[8];
|
||||||
|
snprintf(rainHex, sizeof(rainHex), "0x%02X", gCfg->rainSensitivity);
|
||||||
|
html += F("<div class='fg'><label>");
|
||||||
|
html += s.cfgRainSens;
|
||||||
|
html += F("</label><input type='text' name='rainSens' value='");
|
||||||
|
html += rainHex;
|
||||||
|
html += F("' maxlength='4'><div class='ht'>");
|
||||||
|
html += s.cfgRainSensHelp;
|
||||||
|
html += F("</div></div>");
|
||||||
|
|
||||||
|
html += F("<div class='fg'><label>");
|
||||||
|
html += s.cfgSlot507;
|
||||||
|
html += F("</label><select name='slot507'>"
|
||||||
|
"<option value='0'");
|
||||||
|
if (gCfg->slot507Mode==0) html += F(" selected");
|
||||||
|
html += F(">");
|
||||||
|
html += s.cfgSlotAuto;
|
||||||
|
html += F("</option><option value='1'");
|
||||||
|
if (gCfg->slot507Mode==1) html += F(" selected");
|
||||||
|
html += F(">");
|
||||||
|
html += s.cfgSlotPM;
|
||||||
|
html += F("</option><option value='2'");
|
||||||
|
if (gCfg->slot507Mode==2) html += F(" selected");
|
||||||
|
html += F(">");
|
||||||
|
html += s.cfgSlotCO2;
|
||||||
|
html += F("</option></select><div class='ht'>");
|
||||||
|
html += s.cfgSlot507Help;
|
||||||
|
html += F("</div></div></div></div>");
|
||||||
|
|
||||||
|
html += F("<div style='margin-top:8px'>"
|
||||||
|
"<button type='submit' class='btn btn-p'>");
|
||||||
|
html += s.btnSave;
|
||||||
|
html += F("</button></div></form>");
|
||||||
|
|
||||||
|
// Akce
|
||||||
|
html += F("<h2>");
|
||||||
|
html += s.sectionActions;
|
||||||
|
html += F("</h2><div class='card'><div class='act-row'>");
|
||||||
|
|
||||||
|
html += F("<div class='act-item'>"
|
||||||
|
"<form method='POST' action='/action'>"
|
||||||
|
"<input type='hidden' name='act' value='wind_zero'>"
|
||||||
|
"<input type='hidden' name='lang' value='");
|
||||||
|
html += cz ? "cs" : "en";
|
||||||
|
html += F("'><button type='submit' class='btn btn-w'>");
|
||||||
|
html += s.btnWindZero;
|
||||||
|
html += F("</button></form><div class='ht' style='margin-top:6px'>");
|
||||||
|
html += s.btnWindZeroHelp;
|
||||||
|
html += F("</div></div>");
|
||||||
|
|
||||||
|
html += F("<div class='act-item'>"
|
||||||
|
"<form method='POST' action='/action'>"
|
||||||
|
"<input type='hidden' name='act' value='rain_zero'>"
|
||||||
|
"<input type='hidden' name='lang' value='");
|
||||||
|
html += cz ? "cs" : "en";
|
||||||
|
html += F("'><button type='submit' class='btn btn-w'>");
|
||||||
|
html += s.btnRainZero;
|
||||||
|
html += F("</button></form><div class='ht' style='margin-top:6px'>");
|
||||||
|
html += s.btnRainZeroHelp;
|
||||||
|
html += F("</div></div></div></div>");
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
@ -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 <WebServer.h>
|
||||||
|
#include <Preferences.h>
|
||||||
|
#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
|
||||||
|
|
@ -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 <Arduino.h>
|
||||||
|
#include <WiFi.h> // nutne pro WiFiEvent_t a WiFi.onEvent
|
||||||
|
#include <ETH.h>
|
||||||
|
#include <Preferences.h>
|
||||||
|
|
||||||
|
#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);
|
||||||
|
}
|
||||||
|
|
@ -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 <Arduino.h>
|
||||||
|
#include <Preferences.h>
|
||||||
|
|
||||||
|
// ─── 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
|
||||||
|
|
@ -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 <Arduino.h>
|
||||||
|
|
||||||
|
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
|
||||||
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 <Arduino.h>
|
||||||
|
#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
|
||||||
|
|
@ -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] +--------------------------+"));
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,592 @@
|
||||||
|
/*
|
||||||
|
* ws_server.cpp – HTTP webserver implementace
|
||||||
|
* ZTS-3000-FSJT-N01
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "ws_server.h"
|
||||||
|
#include <ETH.h>
|
||||||
|
|
||||||
|
// ─── 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("<!DOCTYPE html><html lang='");
|
||||||
|
out += cz ? "cs" : "en";
|
||||||
|
out += F("'><head><meta charset='UTF-8'>"
|
||||||
|
"<meta name='viewport' content='width=device-width,initial-scale=1'>"
|
||||||
|
"<title>");
|
||||||
|
out += s.title;
|
||||||
|
out += F("</title><style>");
|
||||||
|
out += FPSTR(CSS);
|
||||||
|
out += F("</style></head><body>"
|
||||||
|
"<nav class='topbar'>"
|
||||||
|
"<div class='logo'>"
|
||||||
|
"<div class='logo-icon'>"
|
||||||
|
"<svg width='18' height='18' viewBox='0 0 18 18' fill='none'>"
|
||||||
|
"<circle cx='9' cy='9' r='3' stroke='#58a6ff' stroke-width='1.5'/>"
|
||||||
|
"<line x1='9' y1='2' x2='9' y2='5' stroke='#58a6ff' stroke-width='1.5' stroke-linecap='round'/>"
|
||||||
|
"<line x1='9' y1='13' x2='9' y2='16' stroke='#58a6ff' stroke-width='1.5' stroke-linecap='round'/>"
|
||||||
|
"<line x1='2' y1='9' x2='5' y2='9' stroke='#58a6ff' stroke-width='1.5' stroke-linecap='round'/>"
|
||||||
|
"<line x1='13' y1='9' x2='16' y2='9' stroke='#58a6ff' stroke-width='1.5' stroke-linecap='round'/>"
|
||||||
|
"</svg></div>");
|
||||||
|
out += s.title;
|
||||||
|
out += F("</div><div class='nav-links'>");
|
||||||
|
out += F("<a href='/' class='nav-link");
|
||||||
|
if (strcmp(page,"status")==0) out += F(" active");
|
||||||
|
out += F("'>");
|
||||||
|
out += s.navStatus;
|
||||||
|
out += F("</a><a href='/config' class='nav-link");
|
||||||
|
if (strcmp(page,"config")==0) out += F(" active");
|
||||||
|
out += F("'>");
|
||||||
|
out += s.navConfig;
|
||||||
|
out += F("</a></div><a href='");
|
||||||
|
out += langUrl;
|
||||||
|
out += F("' class='lang-btn'>");
|
||||||
|
out += s.langSwitch;
|
||||||
|
out += F("</a></nav><div class='main'>");
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String htmlFoot() {
|
||||||
|
return F("</div></body></html>");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 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("<div class='ip-bar'>"
|
||||||
|
"<span><span class='ip-lbl'>IP: </span><b>");
|
||||||
|
html += ETH.localIP().toString();
|
||||||
|
html += F("</b></span><span><span class='ip-lbl'>MAC: </span>");
|
||||||
|
html += ETH.macAddress();
|
||||||
|
html += F("</span><span><span class='ip-lbl'>ETH: </span>");
|
||||||
|
html += ETH.linkSpeed();
|
||||||
|
html += F(" Mbit/s</span></div>");
|
||||||
|
|
||||||
|
if (gData->commError) {
|
||||||
|
html += F("<div class='err-banner'>⚠ ");
|
||||||
|
html += s.commError;
|
||||||
|
html += F(": ");
|
||||||
|
html += gData->errorMsg;
|
||||||
|
html += F("</div>");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toolbar
|
||||||
|
html += F("<div class='toolbar'>"
|
||||||
|
"<form method='POST' action='/detect' style='display:inline'>"
|
||||||
|
"<button type='submit' class='btn btn-d btn-sm'>");
|
||||||
|
html += s.btnDetect;
|
||||||
|
html += F("</button></form>"
|
||||||
|
"<span class='live-dot'></span>"
|
||||||
|
"<span id='updInfo' class='smeta'></span>");
|
||||||
|
html += F("</div>");
|
||||||
|
|
||||||
|
// Tabulka detekce
|
||||||
|
html += F("<h2>");
|
||||||
|
html += s.sectionSensors;
|
||||||
|
html += F("</h2><div class='card'><table>"
|
||||||
|
"<thead><tr><th>");
|
||||||
|
html += s.colSensor;
|
||||||
|
html += F("</th><th>");
|
||||||
|
html += s.colPresent;
|
||||||
|
html += F("</th></tr></thead><tbody>");
|
||||||
|
|
||||||
|
const SensorPresence& p = gData->present;
|
||||||
|
html += F("<tr><td>");
|
||||||
|
html += s.snsWind;
|
||||||
|
html += F("</td><td>");
|
||||||
|
if (p.wind) {
|
||||||
|
html += F("<span class='badge-yes'><span class='dot dot-g'></span>");
|
||||||
|
html += s.yes;
|
||||||
|
} else {
|
||||||
|
html += F("<span class='badge-no'><span class='dot dot-x'></span>");
|
||||||
|
html += s.no;
|
||||||
|
}
|
||||||
|
html += F("</span></td></tr>");
|
||||||
|
html += F("</tbody></table></div>");
|
||||||
|
|
||||||
|
// Tabulka hodnot
|
||||||
|
html += F("<h2>");
|
||||||
|
html += s.sectionValues;
|
||||||
|
html += F("</h2><div class='card'><table><thead><tr><th>");
|
||||||
|
html += s.colSensor;
|
||||||
|
html += F("</th><th>");
|
||||||
|
html += s.colValue;
|
||||||
|
html += F("</th><th>");
|
||||||
|
html += s.colUnit;
|
||||||
|
html += F("</th></tr></thead><tbody>");
|
||||||
|
|
||||||
|
const SensorData& d = *gData;
|
||||||
|
const BeaufortInfo& bi = beaufortInfo(d.windLevel);
|
||||||
|
const char* bName = cz ? bi.nameCz : bi.nameEn;
|
||||||
|
|
||||||
|
// Rychlost vetru
|
||||||
|
html += F("<tr><td>");
|
||||||
|
html += s.snsWindSpeed;
|
||||||
|
html += F("</td><td class='val' id='v_ws'>");
|
||||||
|
html += p.wind ? fmtFloat(d.windSpeed) : String(s.noData);
|
||||||
|
html += F("</td><td class='unit'>");
|
||||||
|
html += p.wind ? s.unitMs : "";
|
||||||
|
html += F("</td></tr>");
|
||||||
|
|
||||||
|
// Sila vetru
|
||||||
|
html += F("<tr><td>");
|
||||||
|
html += s.snsWindLevel;
|
||||||
|
html += F("</td><td class='val' id='v_wl'>");
|
||||||
|
html += p.wind ? String(d.windLevel) : String(s.noData);
|
||||||
|
html += F("</td><td class='unit'>");
|
||||||
|
html += p.wind ? s.unitBft : "";
|
||||||
|
html += F("</td></tr>");
|
||||||
|
|
||||||
|
// Slovni popis
|
||||||
|
html += F("<tr><td>");
|
||||||
|
html += s.snsWindLevelName;
|
||||||
|
html += F("</td><td class='val' id='v_wn'>");
|
||||||
|
html += p.wind ? bName : s.noData;
|
||||||
|
html += F("</td><td class='unit'></td></tr>");
|
||||||
|
|
||||||
|
html += F("</tbody></table></div>");
|
||||||
|
|
||||||
|
// ── Raw RS485 panel ────────────────────────────────────────────────────
|
||||||
|
html += F("<h2>");
|
||||||
|
html += s.sectionRaw;
|
||||||
|
html += F("</h2><div class='raw-panel'>"
|
||||||
|
"<div><span class='raw-label'>TX:</span> <span class='raw-tx' id='rawTx'>...</span>"
|
||||||
|
" <span class='raw-status' id='rawSt'></span></div>"
|
||||||
|
"<div><span class='raw-label'>RX:</span> <span class='raw-rx' id='rawRx'>...</span></div>"
|
||||||
|
"</div>");
|
||||||
|
|
||||||
|
// Inline JavaScript – AJAX auto-refresh
|
||||||
|
html += F("<script>"
|
||||||
|
"var BCZ=['Bezvetri','Vanek','Slaby vitr','Mirny vitr','Dosti silny vitr',"
|
||||||
|
"'Cerstvy vitr','Silny vitr','Prudky vitr','Bourlivy vitr','Vichrice',"
|
||||||
|
"'Silna vichrice','Mohutna vichrice','Orkan','Orkan','Orkan','Orkan','Orkan','Orkan'];"
|
||||||
|
"var BEN=['Calm','Light air','Light breeze','Gentle breeze','Moderate breeze',"
|
||||||
|
"'Fresh breeze','Strong breeze','Near gale','Gale','Strong gale','Storm',"
|
||||||
|
"'Violent storm','Hurricane','Hurricane','Hurricane','Hurricane','Hurricane','Hurricane'];"
|
||||||
|
"var LANG=document.documentElement.lang||'cs';"
|
||||||
|
"function bName(l){var t=(LANG==='en'?BEN:BCZ);return t[l<0?0:(l>17?17:l)];}"
|
||||||
|
"function u(id,v){"
|
||||||
|
"var e=document.getElementById(id);"
|
||||||
|
"if(!e)return;"
|
||||||
|
"var s=String(v);"
|
||||||
|
"if(e.textContent!==s){"
|
||||||
|
"e.textContent=s;"
|
||||||
|
"e.classList.remove('flash');"
|
||||||
|
"void e.offsetWidth;"
|
||||||
|
"e.classList.add('flash');"
|
||||||
|
"}"
|
||||||
|
"}"
|
||||||
|
"function updRaw(){"
|
||||||
|
"fetch('/api/raw').then(function(r){return r.json()}).then(function(d){"
|
||||||
|
"var tx=document.getElementById('rawTx');"
|
||||||
|
"var rx=document.getElementById('rawRx');"
|
||||||
|
"var st=document.getElementById('rawSt');"
|
||||||
|
"if(tx)tx.textContent=d.tx||'-';"
|
||||||
|
"if(rx){rx.textContent=d.rx||'-';"
|
||||||
|
"rx.className=(d.s==='OK')?'raw-rx':'raw-err';}"
|
||||||
|
"if(st){st.textContent=d.s;"
|
||||||
|
"st.className='raw-status '+(d.s==='OK'?'ok':'err');}"
|
||||||
|
"}).catch(function(){});"
|
||||||
|
"}"
|
||||||
|
"function upd(){"
|
||||||
|
"fetch('/api/data').then(function(r){return r.json()}).then(function(d){"
|
||||||
|
"if(d.err){"
|
||||||
|
"document.getElementById('updInfo').textContent='\\u26A0 error';"
|
||||||
|
"return;"
|
||||||
|
"}"
|
||||||
|
"if(d.windSpeed!==undefined){"
|
||||||
|
"u('v_ws',d.windSpeed.toFixed(1));"
|
||||||
|
"u('v_wl',d.windLevel);"
|
||||||
|
"u('v_wn',bName(d.windLevel));"
|
||||||
|
"}"
|
||||||
|
"document.getElementById('updInfo').textContent="
|
||||||
|
"new Date().toLocaleTimeString();"
|
||||||
|
"}).catch(function(){"
|
||||||
|
"document.getElementById('updInfo').textContent='\\u26A0 offline';"
|
||||||
|
"});"
|
||||||
|
"}"
|
||||||
|
"function tick(){upd();updRaw();}"
|
||||||
|
"tick();setInterval(tick,2000);"
|
||||||
|
"</script>");
|
||||||
|
|
||||||
|
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("<h1>");
|
||||||
|
html += s.pageConfigTitle;
|
||||||
|
html += F("</h1><form method='POST' action='/config'>");
|
||||||
|
html += F("<input type='hidden' name='lang' value='");
|
||||||
|
html += cz ? "cs" : "en";
|
||||||
|
html += F("'>");
|
||||||
|
|
||||||
|
// Komunikace
|
||||||
|
html += F("<h2>");
|
||||||
|
html += s.sectionComm;
|
||||||
|
html += F("</h2><div class='card'><div class='frow'>");
|
||||||
|
|
||||||
|
html += F("<div class='fg'><label>");
|
||||||
|
html += s.cfgAddr;
|
||||||
|
html += F("</label><input type='number' name='addr' min='1' max='247' value='");
|
||||||
|
html += gCfg->modbusAddr;
|
||||||
|
html += F("'><div class='ht'>");
|
||||||
|
html += s.cfgAddrHelp;
|
||||||
|
html += F("</div></div>");
|
||||||
|
|
||||||
|
html += F("<div class='fg'><label>");
|
||||||
|
html += s.cfgBaud;
|
||||||
|
html += F("</label><select name='baud'>");
|
||||||
|
const uint32_t bauds[] = {1200,2400,4800,9600,19200,38400,57600,115200};
|
||||||
|
for (auto b : bauds) {
|
||||||
|
html += F("<option value='");
|
||||||
|
html += b;
|
||||||
|
html += F("'");
|
||||||
|
if (b == gCfg->baudRate) html += F(" selected");
|
||||||
|
html += F(">");
|
||||||
|
html += b;
|
||||||
|
html += F(" bps</option>");
|
||||||
|
}
|
||||||
|
html += F("</select><div class='ht'>");
|
||||||
|
html += s.cfgBaudHelp;
|
||||||
|
html += F("</div></div>");
|
||||||
|
|
||||||
|
html += F("<div class='fg'><label>");
|
||||||
|
html += s.cfgPoll;
|
||||||
|
html += F("</label><input type='number' name='poll' min='1' max='60' value='");
|
||||||
|
html += gCfg->pollInterval;
|
||||||
|
html += F("'><div class='ht'>");
|
||||||
|
html += s.cfgPollHelp;
|
||||||
|
html += F("</div></div></div></div>");
|
||||||
|
|
||||||
|
html += F("<div style='margin-top:8px'>"
|
||||||
|
"<button type='submit' class='btn btn-p'>");
|
||||||
|
html += s.btnSave;
|
||||||
|
html += F("</button></div></form>");
|
||||||
|
|
||||||
|
// Akce – zmena adresy snimace
|
||||||
|
html += F("<h2>");
|
||||||
|
html += s.sectionActions;
|
||||||
|
html += F("</h2><div class='card'>"
|
||||||
|
"<form method='POST' action='/action'>"
|
||||||
|
"<input type='hidden' name='act' value='change_addr'>"
|
||||||
|
"<input type='hidden' name='lang' value='");
|
||||||
|
html += cz ? "cs" : "en";
|
||||||
|
html += F("'><div class='fg'><label>");
|
||||||
|
html += s.cfgNewAddr;
|
||||||
|
html += F("</label><input type='number' name='newAddr' min='1' max='247' value='");
|
||||||
|
html += gCfg->modbusAddr;
|
||||||
|
html += F("'></div>"
|
||||||
|
"<button type='submit' class='btn btn-w'>");
|
||||||
|
html += s.btnChangeAddr;
|
||||||
|
html += F("</button><div class='ht' style='margin-top:6px'>");
|
||||||
|
html += s.btnChangeAddrHelp;
|
||||||
|
html += F("</div></form></div>");
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
@ -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 <WebServer.h>
|
||||||
|
#include <Preferences.h>
|
||||||
|
#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
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <Arduino.h>
|
|
||||||
#include <ETH.h>
|
|
||||||
|
|
||||||
#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
|
|
||||||
};
|
|
||||||
|
|
@ -1,262 +0,0 @@
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <Arduino.h>
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,531 +0,0 @@
|
||||||
#include "WeatherStation.h"
|
|
||||||
#include <string.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
|
|
||||||
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<uint8_t>(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<uint16_t>(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<int16_t>(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<uint8_t>(_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<uint16_t>(frame[frameLen - 2]) | (static_cast<uint16_t>(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<uint8_t>(crc & 0xFF);
|
|
||||||
frame[frameLen++] = static_cast<uint8_t>((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<uint8_t>((startReg >> 8) & 0xFF);
|
|
||||||
req[3] = static_cast<uint8_t>(startReg & 0xFF);
|
|
||||||
req[4] = static_cast<uint8_t>((count >> 8) & 0xFF);
|
|
||||||
req[5] = static_cast<uint8_t>(count & 0xFF);
|
|
||||||
const uint16_t crc = crc16(req, 6);
|
|
||||||
req[6] = static_cast<uint8_t>(crc & 0xFF);
|
|
||||||
req[7] = static_cast<uint8_t>((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<uint16_t>(resp[respLen - 2]) | (static_cast<uint16_t>(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<uint8_t>(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<size_t>(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<uint16_t>(resp[3 + i * 2]) << 8) | static_cast<uint16_t>(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<uint8_t>((reg >> 8) & 0xFF);
|
|
||||||
req[3] = static_cast<uint8_t>(reg & 0xFF);
|
|
||||||
req[4] = static_cast<uint8_t>((value >> 8) & 0xFF);
|
|
||||||
req[5] = static_cast<uint8_t>(value & 0xFF);
|
|
||||||
const uint16_t crc = crc16(req, 6);
|
|
||||||
req[6] = static_cast<uint8_t>(crc & 0xFF);
|
|
||||||
req[7] = static_cast<uint8_t>((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<uint16_t>(resp[respLen - 2]) | (static_cast<uint16_t>(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<uint8_t>(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<int>(_values.raw[1]);
|
|
||||||
_values.windDir8 = static_cast<int>(_values.raw[2]);
|
|
||||||
_values.windDirDeg = static_cast<int>(_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<int>(_values.raw[7]);
|
|
||||||
_values.air2 = static_cast<int>(_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<int>(_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<uint8_t>(crc & 0xFF);
|
|
||||||
req[reqLen++] = static_cast<uint8_t>((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");
|
|
||||||
}
|
|
||||||
|
|
@ -1,112 +0,0 @@
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <Arduino.h>
|
|
||||||
#include <HardwareSerial.h>
|
|
||||||
#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);
|
|
||||||
};
|
|
||||||
|
|
@ -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<double>(v), static_cast<unsigned int>(decimals));
|
|
||||||
}
|
|
||||||
|
|
||||||
static String jsonFloat(float v, uint8_t decimals = 1) {
|
|
||||||
if (isnan(v) || !isfinite(v)) return F("null");
|
|
||||||
return String(static_cast<double>(v), static_cast<unsigned int>(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("<span class='");
|
|
||||||
s += present ? F("ok'>") : F("bad'>");
|
|
||||||
s += present ? t.present : t.absent;
|
|
||||||
s += F("</span>");
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
String buildStyle(const Texts&, UiLang) {
|
|
||||||
return F(
|
|
||||||
"<style>"
|
|
||||||
"body{font-family:Arial,sans-serif;background:#f4f7fb;color:#222;margin:0;padding:20px;}"
|
|
||||||
".wrap{max-width:1800px;margin:0 auto;}"
|
|
||||||
".grid{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;align-items:stretch;}"
|
|
||||||
".card{background:#fff;border-radius:14px;padding:16px;box-shadow:0 2px 12px rgba(0,0,0,.08);min-width:0;height:100%;box-sizing:border-box;display:flex;flex-direction:column;}"
|
|
||||||
".card-wide{grid-column:1 / -1;}"
|
|
||||||
".top{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:20px;}"
|
|
||||||
".title{display:flex;align-items:center;gap:18px;flex-wrap:wrap}"
|
|
||||||
".brand-logo{display:block;max-width:180px;width:100%;height:auto}"
|
|
||||||
".nav{display:flex;gap:10px;flex-wrap:wrap;margin:10px 0 18px 0}"
|
|
||||||
".nav a{display:inline-block;padding:10px 14px;border-radius:10px;background:#e8eef8;color:#1f6feb;text-decoration:none;font-weight:bold}"
|
|
||||||
".nav a.active{background:#1f6feb;color:#fff}"
|
|
||||||
"table{width:100%;border-collapse:collapse;table-layout:fixed}"
|
|
||||||
"th,td{padding:8px;border-bottom:1px solid #ddd;text-align:left;font-size:14px;vertical-align:top;word-break:break-word}"
|
|
||||||
".ok{color:#0a7a2f;font-weight:bold}.bad{color:#b00020;font-weight:bold}"
|
|
||||||
".sub{color:#637186;font-size:13px}"
|
|
||||||
".mono{font-family:monospace;white-space:pre-wrap;background:#eef2f7;padding:10px;border-radius:10px;overflow:auto}"
|
|
||||||
".mono-frames{max-height:140px;font-size:12px;line-height:1.3}"
|
|
||||||
".mono-raw{max-height:420px;font-size:12px;line-height:1.3}"
|
|
||||||
".cfg-table td:first-child{width:22%;font-weight:bold}"
|
|
||||||
".cfg-table td:nth-child(2){width:28%}"
|
|
||||||
".cfg-table td:nth-child(3){width:50%}"
|
|
||||||
".cfg-inline-form{margin:0}"
|
|
||||||
".cfg-inline-form input,.cfg-inline-form select,.cfg-inline-form textarea{margin:0}"
|
|
||||||
"label{display:block;margin:8px 0 4px;font-weight:bold}"
|
|
||||||
"input[type=text],input[type=number],select,textarea{width:100%;padding:10px;border:1px solid #ccc;border-radius:8px;box-sizing:border-box}"
|
|
||||||
"textarea{resize:vertical;min-height:88px;font-family:monospace}"
|
|
||||||
"button{padding:10px 14px;border:0;border-radius:10px;background:#1f6feb;color:#fff;cursor:pointer;margin-top:8px}"
|
|
||||||
"button:hover{opacity:.92}"
|
|
||||||
".row2{display:grid;grid-template-columns:1fr 1fr;gap:12px}"
|
|
||||||
".msg{padding:10px 12px;border-radius:10px;margin-bottom:12px}"
|
|
||||||
".note{font-size:13px;color:#5e6a7a}"
|
|
||||||
"@media (max-width:1400px){.grid{grid-template-columns:repeat(2,1fr);}}"
|
|
||||||
"@media (max-width:800px){.grid{grid-template-columns:1fr}.row2{grid-template-columns:1fr}.brand-logo{max-width:240px;}}"
|
|
||||||
"</style>");
|
|
||||||
}
|
|
||||||
|
|
||||||
static void appendTop(String& html, UiLang lang, const Texts& t, const char* active) {
|
|
||||||
html += F("<div class='top'><div class='title'><img src='/logo.png' alt='logo' class='brand-logo'><div><h1>");
|
|
||||||
html += t.appTitle;
|
|
||||||
html += F("</h1></div></div>");
|
|
||||||
|
|
||||||
html += F("<form method='GET' action='");
|
|
||||||
html += (strcmp(active, "main") == 0) ? F("/") : F("/config");
|
|
||||||
html += F("' style='display:flex;gap:8px;align-items:center;margin:0;'>");
|
|
||||||
html += F("<label style='margin:0;font-weight:bold;'>");
|
|
||||||
html += t.language;
|
|
||||||
html += F("</label><select name='lang' onchange='this.form.submit()'>");
|
|
||||||
html += F("<option value='cz'");
|
|
||||||
if (lang == UiLang::CZ) html += F(" selected");
|
|
||||||
html += F(">CZ</option><option value='en'");
|
|
||||||
if (lang == UiLang::EN) html += F(" selected");
|
|
||||||
html += F(">ENG</option></select></form></div>");
|
|
||||||
|
|
||||||
html += F("<div class='nav'>");
|
|
||||||
html += F("<a href='/?lang="); html += langCode(lang); html += F("' class='");
|
|
||||||
html += (strcmp(active, "main") == 0) ? F("active") : F("");
|
|
||||||
html += F("'>"); html += t.tabMain; html += F("</a>");
|
|
||||||
html += F("<a href='/config?lang="); html += langCode(lang); html += F("' class='");
|
|
||||||
html += (strcmp(active, "config") == 0) ? F("active") : F("");
|
|
||||||
html += F("'>"); html += t.tabConfig; html += F("</a>");
|
|
||||||
html += F("</div>");
|
|
||||||
}
|
|
||||||
|
|
||||||
static void appendRow(String& html, const String& id, const String& label, const String& value) {
|
|
||||||
html += F("<tr><td>");
|
|
||||||
html += label;
|
|
||||||
html += F("</td><td id='");
|
|
||||||
html += id;
|
|
||||||
html += F("'>");
|
|
||||||
html += value;
|
|
||||||
html += F("</td></tr>");
|
|
||||||
}
|
|
||||||
|
|
||||||
static String buildConfigHistoryBox(const Texts& t, const WeatherStation& ws) {
|
|
||||||
String html;
|
|
||||||
html.reserve(1800);
|
|
||||||
|
|
||||||
html += F("<div class='card card-wide' style='margin-bottom:16px;'><h2>");
|
|
||||||
html += t.modbusLog;
|
|
||||||
html += F("</h2>");
|
|
||||||
|
|
||||||
if (ws.configLogCount() == 0) {
|
|
||||||
html += F("<div class='note'>");
|
|
||||||
html += t.noResult;
|
|
||||||
html += F("</div></div>");
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CommLog& c = ws.configLogAt(0);
|
|
||||||
|
|
||||||
html += F("<table>");
|
|
||||||
appendRow(html, F("cfg-log-state"), t.modbusLastStatus, c.lastOk ? String(F("<span class='ok'>")) + t.statusOkShort + F("</span>") : String(F("<span class='bad'>")) + t.statusErrorShort + F("</span>"));
|
|
||||||
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("</table>");
|
|
||||||
|
|
||||||
html += F("<div class='sub' style='margin-top:10px'>");
|
|
||||||
html += t.modbusTx;
|
|
||||||
html += F("</div><div class='mono'>");
|
|
||||||
html += htmlEscape(c.lastRequestHex);
|
|
||||||
html += F("</div>");
|
|
||||||
|
|
||||||
html += F("<div class='sub' style='margin-top:10px'>");
|
|
||||||
html += t.modbusRx;
|
|
||||||
html += F("</div><div class='mono'>");
|
|
||||||
html += htmlEscape(c.lastResponseHex);
|
|
||||||
html += F("</div>");
|
|
||||||
|
|
||||||
html += F("</div>");
|
|
||||||
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("<div class='grid'>");
|
|
||||||
|
|
||||||
html += F("<div class='card'><h2>");
|
|
||||||
html += t.sensorStatus;
|
|
||||||
html += F("</h2><table>");
|
|
||||||
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("</table></div>");
|
|
||||||
|
|
||||||
html += F("<div class='card'><h2>");
|
|
||||||
html += t.values;
|
|
||||||
html += F("</h2><table>");
|
|
||||||
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("</table></div>");
|
|
||||||
|
|
||||||
html += F("<div class='card'><h2>");
|
|
||||||
html += t.modbusLog;
|
|
||||||
html += F("</h2><table>");
|
|
||||||
appendRow(html, F("mb-state"), t.modbusLastStatus, c.lastOk ? String(F("<span class='ok'>")) + t.statusOkShort + F("</span>") : String(F("<span class='bad'>")) + t.statusErrorShort + F("</span>"));
|
|
||||||
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("</table></div>");
|
|
||||||
|
|
||||||
html += F("<div class='card'><h2>");
|
|
||||||
html += t.modbusFrames;
|
|
||||||
html += F("</h2>");
|
|
||||||
html += F("<div class='sub'>");
|
|
||||||
html += t.modbusTx;
|
|
||||||
html += F("</div><div class='mono mono-frames' id='mb-tx'>");
|
|
||||||
html += htmlEscape(c.lastRequestHex);
|
|
||||||
html += F("</div>");
|
|
||||||
html += F("<div class='sub' style='margin-top:10px'>");
|
|
||||||
html += t.modbusRx;
|
|
||||||
html += F("</div><div class='mono mono-frames' id='mb-rx'>");
|
|
||||||
html += htmlEscape(c.lastResponseHex);
|
|
||||||
html += F("</div>");
|
|
||||||
|
|
||||||
html += F("<div class='sub' style='margin-top:12px'>");
|
|
||||||
html += t.rawRegisters;
|
|
||||||
html += F("</div><div class='mono mono-raw' id='raw-regs'>");
|
|
||||||
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("</div></div>");
|
|
||||||
|
|
||||||
html += F("</div>");
|
|
||||||
}
|
|
||||||
|
|
||||||
static String buildMainScript(UiLang lang, const Texts& t) {
|
|
||||||
String s;
|
|
||||||
s.reserve(9000);
|
|
||||||
|
|
||||||
s += F("<script>");
|
|
||||||
s += F("const L={present:\""); s += t.present;
|
|
||||||
s += F("\",absent:\""); s += t.absent;
|
|
||||||
s += F("\",seconds:\""); s += t.seconds;
|
|
||||||
s += F("\",ok:\""); s += t.statusOkShort;
|
|
||||||
s += F("\",err:\""); s += t.statusErrorShort;
|
|
||||||
s += F("\"};");
|
|
||||||
|
|
||||||
s += F("function badge(v){return \"<span class='\"+(v?\"ok\":\"bad\")+\"'>\"+(v?L.present:L.absent)+\"</span>\";}");
|
|
||||||
s += F("function setHtml(id,v){const e=document.getElementById(id);if(e)e.innerHTML=v;}");
|
|
||||||
s += F("function setText(id,v){const e=document.getElementById(id);if(e)e.textContent=v;}");
|
|
||||||
s += F("function num(v,d){if(v===null||v===undefined||v==='-'||Number.isNaN(Number(v)))return '-';return Number(v).toFixed(d);}");
|
|
||||||
|
|
||||||
s += F("async function tick(){");
|
|
||||||
s += F("try{");
|
|
||||||
s += F("const url='/data.json?lang="); s += langCode(lang); s += F("&_=' + Date.now();");
|
|
||||||
s += F("const r=await fetch(url,{cache:'no-store'});");
|
|
||||||
s += F("if(!r.ok)return;");
|
|
||||||
s += F("const d=await r.json();");
|
|
||||||
|
|
||||||
s += F("setHtml('st-wind',badge(d.pwind));");
|
|
||||||
s += F("setHtml('st-th',badge(d.pth));");
|
|
||||||
s += F("setHtml('st-noise',badge(d.pnoise));");
|
|
||||||
s += F("setHtml('st-air',badge(d.pair));");
|
|
||||||
s += F("setHtml('st-pressure',badge(d.ppress));");
|
|
||||||
s += F("setHtml('st-light',badge(d.plight));");
|
|
||||||
s += F("setHtml('st-rain',badge(d.prain));");
|
|
||||||
s += F("setHtml('st-compass',badge(d.pcomp));");
|
|
||||||
s += F("setHtml('st-solar',badge(d.psolar));");
|
|
||||||
|
|
||||||
s += F("setText('v-wind-short',num(d.wind,1));");
|
|
||||||
s += F("setText('v-beaufort-short',(d.beaufort<0)?'-':String(d.beaufort));");
|
|
||||||
s += F("setText('v-dir8-short',(d.dir8<0)?'-':String(d.dir8));");
|
|
||||||
s += F("setText('v-dirdeg-short',(d.dirdeg<0)?'-':String(d.dirdeg));");
|
|
||||||
s += F("setText('v-hum-short',num(d.hum,1));");
|
|
||||||
s += F("setText('v-temp-short',num(d.temp,1));");
|
|
||||||
s += F("setText('v-noise-short',num(d.noise,1));");
|
|
||||||
s += F("setText('v-air1-short',(d.air1<0?'-':String(d.air1)));");
|
|
||||||
s += F("setText('v-air2-short',(d.air2<0?'-':String(d.air2)));");
|
|
||||||
s += F("setText('v-pressure-short',num(d.press,1));");
|
|
||||||
s += F("setText('v-luxh-short',String(d.lightHigh));");
|
|
||||||
s += F("setText('v-luxl-short',String(d.lightLow));");
|
|
||||||
s += F("setText('v-lux-short',String(d.light));");
|
|
||||||
s += F("setText('v-rain-short',num(d.rain,1));");
|
|
||||||
s += F("setText('v-compass-short',num(d.compass,2));");
|
|
||||||
s += F("setText('v-solar-short',(d.solar<0)?'-':String(d.solar));");
|
|
||||||
s += F("setText('data-age',String(d.age)+' '+L.seconds);");
|
|
||||||
|
|
||||||
s += F("setHtml('mb-state',d.mbOk?\"<span class='ok'>\"+L.ok+\"</span>\":\"<span class='bad'>\"+L.err+\"</span>\");");
|
|
||||||
s += F("setText('mb-action',d.mbAction||'-');");
|
|
||||||
s += F("setText('mb-dur',String(d.mbDuration)+' ms');");
|
|
||||||
s += F("setText('mb-stats',String(d.mbOkCount)+' / '+String(d.mbErrCount));");
|
|
||||||
s += F("setText('mb-err',d.mbError||'-');");
|
|
||||||
s += F("setText('mb-tx',d.mbTx||'');");
|
|
||||||
s += F("setText('mb-rx',d.mbRx||'');");
|
|
||||||
s += F("if(Array.isArray(d.raw)){setText('raw-regs',d.raw.map((v,i)=>'R'+(500+i)+' = '+v).join('\\n'));}");
|
|
||||||
|
|
||||||
s += F("}catch(e){}");
|
|
||||||
s += F("}");
|
|
||||||
s += F("window.addEventListener('load',function(){tick();setInterval(tick,2000);});");
|
|
||||||
s += F("</script>");
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
static String buildManualCommandScript() {
|
|
||||||
String s;
|
|
||||||
s.reserve(2400);
|
|
||||||
s += F("<script>");
|
|
||||||
s += F("function wsCrc16(bytes){let crc=0xFFFF;for(let i=0;i<bytes.length;i++){crc^=bytes[i];for(let j=0;j<8;j++){if(crc&1){crc=(crc>>1)^0xA001;}else{crc>>=1;}}}return crc&0xFFFF;}");
|
|
||||||
s += F("function wsHexByte(v){return Number(v).toString(16).toUpperCase().padStart(2,'0');}");
|
|
||||||
s += F("function wsHexWordFromInput(id,defv){let s=(document.getElementById(id).value||defv).trim();s=s.replace(/^0x/i,'');let v=parseInt(s,16);if(Number.isNaN(v))v=parseInt(defv,16);if(Number.isNaN(v))v=0;return Math.max(0,Math.min(65535,v));}");
|
|
||||||
s += F("function wsUpdateManualPreview(){");
|
|
||||||
s += F("const a=Math.max(1,Math.min(247,parseInt(document.getElementById('manual-addr').value||'1',10)));");
|
|
||||||
s += F("const r=wsHexWordFromInput('manual-reg','6000');");
|
|
||||||
s += F("const v=wsHexWordFromInput('manual-val','0000');");
|
|
||||||
s += F("const frame=[a,0x06,(r>>8)&0xFF,r&0xFF,(v>>8)&0xFF,v&0xFF];");
|
|
||||||
s += F("const crc=wsCrc16(frame);frame.push(crc&0xFF);frame.push((crc>>8)&0xFF);");
|
|
||||||
s += F("const el=document.getElementById('manual-preview');if(el){el.textContent=frame.map(wsHexByte).join(' ');}}");
|
|
||||||
s += F("window.addEventListener('load',function(){['manual-addr','manual-reg','manual-val'].forEach(function(id){const e=document.getElementById(id);if(e){e.addEventListener('input',wsUpdateManualPreview);e.addEventListener('change',wsUpdateManualPreview);}});wsUpdateManualPreview();});");
|
|
||||||
s += F("</script>");
|
|
||||||
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("<!DOCTYPE html><html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'><title>");
|
|
||||||
html += t.appTitle;
|
|
||||||
html += F("</title>");
|
|
||||||
html += buildStyle(t, lang);
|
|
||||||
html += F("</head><body><div class='wrap'>");
|
|
||||||
appendTop(html, lang, t, "main");
|
|
||||||
appendMainCards(html, t, ws, ethUp, ip, mac);
|
|
||||||
html += buildMainScript(lang, t);
|
|
||||||
html += F("</div></body></html>");
|
|
||||||
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("<!DOCTYPE html><html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'><title>");
|
|
||||||
html += t.tabConfig;
|
|
||||||
html += F("</title>");
|
|
||||||
html += buildStyle(t, lang);
|
|
||||||
html += F("</head><body><div class='wrap'>");
|
|
||||||
appendTop(html, lang, t, "config");
|
|
||||||
|
|
||||||
html += F("<div class='grid'>");
|
|
||||||
html += buildConfigHistoryBox(t, ws);
|
|
||||||
|
|
||||||
html += F("<div class='card card-wide'><h2>");
|
|
||||||
html += t.manualCommand;
|
|
||||||
html += F("</h2><div class='note'>");
|
|
||||||
html += t.manualNote;
|
|
||||||
html += F("</div>");
|
|
||||||
|
|
||||||
html += F("<form method='POST' action='/cmd-manual?lang=");
|
|
||||||
html += langCode(lang);
|
|
||||||
html += F("'>");
|
|
||||||
|
|
||||||
html += F("<div class='row2'>");
|
|
||||||
|
|
||||||
html += F("<div><label>");
|
|
||||||
html += t.manualAddress;
|
|
||||||
html += F("</label><input type='number' id='manual-addr' name='address' min='1' max='247' value='");
|
|
||||||
html += String(ws.config().address);
|
|
||||||
html += F("'></div>");
|
|
||||||
|
|
||||||
html += F("<div><label>");
|
|
||||||
html += t.manualRegisterHex;
|
|
||||||
html += F("</label><input type='text' id='manual-reg' name='reg' value='6000' placeholder='6000'></div>");
|
|
||||||
|
|
||||||
html += F("<div><label>");
|
|
||||||
html += t.manualValueHex;
|
|
||||||
html += F("</label><input type='text' id='manual-val' name='value' value='0000' placeholder='0000'></div>");
|
|
||||||
|
|
||||||
html += F("<div><label>");
|
|
||||||
html += t.manualPreview;
|
|
||||||
html += F("</label><div class='mono' id='manual-preview'>-</div></div>");
|
|
||||||
|
|
||||||
html += F("</div><button type='submit'>");
|
|
||||||
html += t.manualSend;
|
|
||||||
html += F("</button></form>");
|
|
||||||
|
|
||||||
html += F("<hr style='margin:16px 0;border:none;border-top:1px solid #ddd'>");
|
|
||||||
|
|
||||||
html += F("<h2>");
|
|
||||||
html += t.rawHexTitle;
|
|
||||||
html += F("</h2><div class='note'>");
|
|
||||||
html += t.rawHexNote;
|
|
||||||
html += F("</div>");
|
|
||||||
|
|
||||||
html += F("<form method='POST' action='/cmd-rawhex?lang=");
|
|
||||||
html += langCode(lang);
|
|
||||||
html += F("'>");
|
|
||||||
html += F("<label>");
|
|
||||||
html += t.rawHexInput;
|
|
||||||
html += F("</label><textarea name='rawhex' placeholder='010660000001560A'></textarea>");
|
|
||||||
html += F("<button type='submit'>");
|
|
||||||
html += t.rawHexSend;
|
|
||||||
html += F("</button></form></div>");
|
|
||||||
|
|
||||||
html += F("<div class='card card-wide'><h2>");
|
|
||||||
html += t.configTitle;
|
|
||||||
html += F("</h2>");
|
|
||||||
|
|
||||||
html += F("<table class='cfg-table'>");
|
|
||||||
html += F("<tr><th>");
|
|
||||||
html += t.tableItem;
|
|
||||||
html += F("</th><th>");
|
|
||||||
html += t.tableSetting;
|
|
||||||
html += F("</th><th>");
|
|
||||||
html += t.tableNote;
|
|
||||||
html += F("</th></tr>");
|
|
||||||
|
|
||||||
html += F("<tr><td>");
|
|
||||||
html += t.address;
|
|
||||||
html += F("</td><td>");
|
|
||||||
html += F("<form class='cfg-inline-form' method='POST' action='/config-save?lang=");
|
|
||||||
html += langCode(lang);
|
|
||||||
html += F("'>");
|
|
||||||
html += F("<input type='hidden' name='baudrate' value='");
|
|
||||||
html += String(ws.config().baudrate);
|
|
||||||
html += F("'>");
|
|
||||||
html += F("<input type='hidden' name='windOffset' value='");
|
|
||||||
html += hex16(ws.config().windOffset);
|
|
||||||
html += F("'>");
|
|
||||||
html += F("<input type='hidden' name='rainSensitivity' value='");
|
|
||||||
html += hex16(ws.config().rainSensitivity);
|
|
||||||
html += F("'>");
|
|
||||||
html += F("<input type='number' name='address' min='1' max='247' value='");
|
|
||||||
html += String(ws.config().address);
|
|
||||||
html += F("'> <button type='submit'>");
|
|
||||||
html += t.save;
|
|
||||||
html += F("</button></form></td><td>");
|
|
||||||
html += t.noteAddress;
|
|
||||||
html += F("</td></tr>");
|
|
||||||
|
|
||||||
html += F("<tr><td>");
|
|
||||||
html += t.baudrate;
|
|
||||||
html += F("</td><td>");
|
|
||||||
html += F("<form class='cfg-inline-form' method='POST' action='/config-save?lang=");
|
|
||||||
html += langCode(lang);
|
|
||||||
html += F("'>");
|
|
||||||
html += F("<input type='hidden' name='address' value='");
|
|
||||||
html += String(ws.config().address);
|
|
||||||
html += F("'>");
|
|
||||||
html += F("<input type='hidden' name='windOffset' value='");
|
|
||||||
html += hex16(ws.config().windOffset);
|
|
||||||
html += F("'>");
|
|
||||||
html += F("<input type='hidden' name='rainSensitivity' value='");
|
|
||||||
html += hex16(ws.config().rainSensitivity);
|
|
||||||
html += F("'>");
|
|
||||||
html += F("<select name='baudrate'>");
|
|
||||||
const uint32_t baudrates[] = {1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200};
|
|
||||||
for (uint8_t i = 0; i < sizeof(baudrates) / sizeof(baudrates[0]); i++) {
|
|
||||||
html += F("<option value='");
|
|
||||||
html += String(baudrates[i]);
|
|
||||||
html += F("'");
|
|
||||||
if (baudrates[i] == ws.config().baudrate) html += F(" selected");
|
|
||||||
html += F(">");
|
|
||||||
html += String(baudrates[i]);
|
|
||||||
html += F("</option>");
|
|
||||||
}
|
|
||||||
html += F("</select> <button type='submit'>");
|
|
||||||
html += t.save;
|
|
||||||
html += F("</button></form></td><td>");
|
|
||||||
html += t.noteBaudrate;
|
|
||||||
html += F("</td></tr>");
|
|
||||||
|
|
||||||
html += F("<tr><td>");
|
|
||||||
html += t.windOffset;
|
|
||||||
html += F("</td><td>");
|
|
||||||
html += F("<form class='cfg-inline-form' method='POST' action='/config-save?lang=");
|
|
||||||
html += langCode(lang);
|
|
||||||
html += F("'>");
|
|
||||||
html += F("<input type='hidden' name='address' value='");
|
|
||||||
html += String(ws.config().address);
|
|
||||||
html += F("'>");
|
|
||||||
html += F("<input type='hidden' name='baudrate' value='");
|
|
||||||
html += String(ws.config().baudrate);
|
|
||||||
html += F("'>");
|
|
||||||
html += F("<input type='hidden' name='rainSensitivity' value='");
|
|
||||||
html += hex16(ws.config().rainSensitivity);
|
|
||||||
html += F("'>");
|
|
||||||
html += F("<input type='text' name='windOffset' value='");
|
|
||||||
html += hex16(ws.config().windOffset);
|
|
||||||
html += F("' placeholder='0000'> <button type='submit'>");
|
|
||||||
html += t.save;
|
|
||||||
html += F("</button></form></td><td>");
|
|
||||||
html += t.noteWindOffset;
|
|
||||||
html += F("</td></tr>");
|
|
||||||
|
|
||||||
html += F("<tr><td>");
|
|
||||||
html += t.rainSensitivity;
|
|
||||||
html += F("</td><td>");
|
|
||||||
html += F("<form class='cfg-inline-form' method='POST' action='/config-save?lang=");
|
|
||||||
html += langCode(lang);
|
|
||||||
html += F("'>");
|
|
||||||
html += F("<input type='hidden' name='address' value='");
|
|
||||||
html += String(ws.config().address);
|
|
||||||
html += F("'>");
|
|
||||||
html += F("<input type='hidden' name='baudrate' value='");
|
|
||||||
html += String(ws.config().baudrate);
|
|
||||||
html += F("'>");
|
|
||||||
html += F("<input type='hidden' name='windOffset' value='");
|
|
||||||
html += hex16(ws.config().windOffset);
|
|
||||||
html += F("'>");
|
|
||||||
html += F("<input type='text' name='rainSensitivity' value='");
|
|
||||||
html += hex16(ws.config().rainSensitivity);
|
|
||||||
html += F("' placeholder='0011'> <button type='submit'>");
|
|
||||||
html += t.save;
|
|
||||||
html += F("</button></form></td><td>");
|
|
||||||
html += t.noteRainSensitivity;
|
|
||||||
html += F("</td></tr>");
|
|
||||||
|
|
||||||
html += F("<tr><td>");
|
|
||||||
html += t.commandWindZero;
|
|
||||||
html += F("</td><td><form class='cfg-inline-form' method='POST' action='/cmd-wind-zero?lang=");
|
|
||||||
html += langCode(lang);
|
|
||||||
html += F("'><button type='submit'>");
|
|
||||||
html += t.execute;
|
|
||||||
html += F("</button></form></td><td>");
|
|
||||||
html += t.noteWindZero;
|
|
||||||
html += F("</td></tr>");
|
|
||||||
|
|
||||||
html += F("<tr><td>");
|
|
||||||
html += t.commandRainZero;
|
|
||||||
html += F("</td><td><form class='cfg-inline-form' method='POST' action='/cmd-rain-zero?lang=");
|
|
||||||
html += langCode(lang);
|
|
||||||
html += F("'><button type='submit'>");
|
|
||||||
html += t.execute;
|
|
||||||
html += F("</button></form></td><td>");
|
|
||||||
html += t.noteRainZero;
|
|
||||||
html += F("</td></tr>");
|
|
||||||
|
|
||||||
html += F("</table></div>");
|
|
||||||
|
|
||||||
html += F("</div>");
|
|
||||||
html += buildManualCommandScript();
|
|
||||||
html += F("</div></body></html>");
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <Arduino.h>
|
|
||||||
#include <WebServer.h>
|
|
||||||
#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);
|
|
||||||
|
|
@ -1,455 +0,0 @@
|
||||||
#include <Arduino.h>
|
|
||||||
#include <WiFi.h>
|
|
||||||
#include <ETH.h>
|
|
||||||
#include <Preferences.h>
|
|
||||||
#include <WebServer.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#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<AirQualityMode>(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<uint8_t>(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("<div><strong>");
|
|
||||||
msg += htmlEscape(title);
|
|
||||||
msg += F(":</strong> ");
|
|
||||||
msg += ok ? String(F("<span class='ok'>")) + t.statusOkShort + F("</span>") : String(F("<span class='bad'>")) + t.statusErrorShort + F("</span>");
|
|
||||||
if (detail.length()) {
|
|
||||||
msg += F("<br>");
|
|
||||||
msg += htmlEscape(detail);
|
|
||||||
}
|
|
||||||
msg += F("</div>");
|
|
||||||
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("<div><strong>");
|
|
||||||
msg += htmlEscape(title);
|
|
||||||
msg += F(":</strong> ");
|
|
||||||
msg += c.lastOk ? String(F("<span class='ok'>")) + t.statusOkShort + F("</span>") : String(F("<span class='bad'>")) + t.statusErrorShort + F("</span>");
|
|
||||||
msg += F("<br>");
|
|
||||||
msg += htmlEscape(t.modbusAction);
|
|
||||||
msg += F(": ");
|
|
||||||
msg += htmlEscape(c.lastAction.length() ? c.lastAction : String("-"));
|
|
||||||
msg += F("<br>");
|
|
||||||
msg += htmlEscape(t.modbusDuration);
|
|
||||||
msg += F(": ");
|
|
||||||
msg += String(c.lastDurationMs);
|
|
||||||
msg += F(" ms");
|
|
||||||
msg += F("<br>");
|
|
||||||
msg += htmlEscape(t.modbusError);
|
|
||||||
msg += F(": ");
|
|
||||||
msg += (c.lastError.length() ? htmlEscape(c.lastError) : F("-"));
|
|
||||||
msg += F("</div>");
|
|
||||||
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<uint16_t>(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<uint32_t>(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<uint8_t>(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<PGM_P>(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<uint8_t>(addr)) {
|
|
||||||
station.config().address = static_cast<uint8_t>(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("<div style='margin-top:8px'>");
|
|
||||||
msg += htmlEscape(firstValidationError);
|
|
||||||
msg += F("</div>");
|
|
||||||
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<uint8_t>(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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,936 +0,0 @@
|
||||||
#include "logo_web.h"
|
|
||||||
#include <pgmspace.h>
|
|
||||||
#include <stddef.h>
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
#pragma once
|
|
||||||
#include <Arduino.h>
|
|
||||||
#include <pgmspace.h>
|
|
||||||
#include <stddef.h>
|
|
||||||
|
|
||||||
extern const uint8_t logo_web_png[];
|
|
||||||
extern const size_t logo_web_png_len;
|
|
||||||
Loading…
Reference in New Issue