From da7b0e95970e808f79c7e47544c812f98bf97eff Mon Sep 17 00:00:00 2001 From: proddy Date: Fri, 26 Mar 2021 17:29:00 +0100 Subject: [PATCH] feat: board profiles (#11) --- CHANGELOG_LATEST.md | 2 +- interface/src/project/EMSESPBoardProfiles.tsx | 23 ++++ interface/src/project/EMSESPSettingsForm.tsx | 107 +++++++++++++----- interface/src/project/EMSESPtypes.ts | 2 +- lib_standalone/ESP8266React.h | 2 +- src/WebSettingsService.cpp | 40 ++++++- src/WebSettingsService.h | 12 +- src/locale_EN.h | 17 +-- src/system.cpp | 96 ++++++++++------ src/system.h | 6 +- src/version.h | 2 +- 11 files changed, 231 insertions(+), 78 deletions(-) create mode 100644 interface/src/project/EMSESPBoardProfiles.tsx diff --git a/CHANGELOG_LATEST.md b/CHANGELOG_LATEST.md index bed071666..926f611c0 100644 --- a/CHANGELOG_LATEST.md +++ b/CHANGELOG_LATEST.md @@ -46,6 +46,6 @@ - new ESP32 partition side to allow for smoother OTA and fallback - network Gateway IP is optional (#682)emsesp/EMS-ESP - moved to a new GitHub repo https://github.com/emsesp/EMS-ESP32 - +- Invert LED changed to Hide LED ### Removed - Shower Alert (disabled for now) diff --git a/interface/src/project/EMSESPBoardProfiles.tsx b/interface/src/project/EMSESPBoardProfiles.tsx new file mode 100644 index 000000000..ce1cc6bb8 --- /dev/null +++ b/interface/src/project/EMSESPBoardProfiles.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import MenuItem from '@material-ui/core/MenuItem'; + +type BoardProfiles = { + [name: string]: string +}; + +export const BOARD_PROFILES: BoardProfiles = { + "S32": "Gateway S32", + "NODEMCU": "NodeMCU 32S", + "MT-ET": "MT-ET Live D1 Mini", + "LOLIN": "Lolin D32", + "WEMOS": "Wemos Mini D1-32", + "E32": "Gateway E32", + "OLIMEX": "Olimex ESP32-EVB-EA", + "TLK110": "Ethernet (TLK110)" +} + +export function boardProfileSelectItems() { + return Object.keys(BOARD_PROFILES).map(code => ( + {BOARD_PROFILES[code]} + )); +} diff --git a/interface/src/project/EMSESPSettingsForm.tsx b/interface/src/project/EMSESPSettingsForm.tsx index 5bce4f5ae..7add38aa5 100644 --- a/interface/src/project/EMSESPSettingsForm.tsx +++ b/interface/src/project/EMSESPSettingsForm.tsx @@ -7,27 +7,88 @@ import MenuItem from '@material-ui/core/MenuItem'; import Grid from '@material-ui/core/Grid'; +import { redirectingAuthorizedFetch, withAuthenticatedContext, AuthenticatedContextProps } from "../authentication"; + import { RestFormProps, FormActions, FormButton, BlockFormControlLabel } from '../components'; import { isIP, optional } from '../validators'; import { EMSESPSettings } from './EMSESPtypes'; -type EMSESPSettingsFormProps = RestFormProps & WithWidthProps; +import { boardProfileSelectItems } from './EMSESPBoardProfiles'; + +import { ENDPOINT_ROOT } from "../api"; +export const BOARD_PROFILE_ENDPOINT = ENDPOINT_ROOT + "boardProfile"; + +type EMSESPSettingsFormProps = RestFormProps & AuthenticatedContextProps & WithWidthProps; + +interface EMSESPSettingsFormState { + processing: boolean; +} class EMSESPSettingsForm extends React.Component { + state: EMSESPSettingsFormState = { + processing: false + } + componentDidMount() { ValidatorForm.addValidationRule('isOptionalIP', optional(isIP)); } + changeBoardProfile = (event: React.ChangeEvent) => { + const { data, setData } = this.props; + setData({ + ...data, + board_profile: event.target.value + }); + + if (event.target.value === "CUSTOM") + return; + + this.setState({ processing: true }); + redirectingAuthorizedFetch(BOARD_PROFILE_ENDPOINT, { + method: "POST", + body: JSON.stringify({ code: event.target.value }), + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => { + if (response.status === 200) { + return response.json(); + } + throw Error("Unexpected response code: " + response.status); + }) + .then((json) => { + this.props.enqueueSnackbar("Profile loaded", { variant: 'success' }); + setData({ + ...data, + led_gpio: json.led_gpio, + dallas_gpio: json.dallas_gpio, + rx_gpio: json.rx_gpio, + tx_gpio: json.tx_gpio, + pbutton_gpio: json.pbutton_gpio, + board_profile: event.target.value + }); + this.setState({ processing: false }); + }) + .catch((error) => { + this.props.enqueueSnackbar( + error.message || "Problem fetching board profile", + { variant: "warning" } + ); + this.setState({ processing: false }); + }); + }; + render() { const { data, saveData, handleValueChange } = this.props; return ( - Modify any of the EMS-ESP settings here. For help visit the {'wiki'}. + Modify any of the EMS-ESP settings here. For help refer to the {'online documentation'}. @@ -37,7 +98,6 @@ class EMSESPSettingsForm extends React.Component { - { - Choose from a pre-configured board layout to automatically set the GPIO pins + Select a pre-configured board layout to automatically set the GPIO pins, or set your own custom configuration @@ -100,25 +160,18 @@ class EMSESPSettingsForm extends React.Component { value={data.board_profile} fullWidth variant="outlined" - onChange={handleValueChange('board_profile')} + onChange={this.changeBoardProfile} margin="normal"> - Gateway S32 - NodeMCU 32S - Lolin D32 - Wemos Mini D1-32 - Gateway E32 (LAN8720) - Olimex ESP32-EVB-EA (LAN8720) - Ethernet (TLK110) - Custom... + {boardProfileSelectItems()} + Custom... - { (data.board_profile === 0) && - + { (data.board_profile === "CUSTOM") && { { { { { value="hide_led" /> } - label="Invert LED" + label = "Hide LED" /> } @@ -361,4 +414,4 @@ class EMSESPSettingsForm extends React.Component { } -export default withWidth()(EMSESPSettingsForm); +export default withAuthenticatedContext(withWidth()(EMSESPSettingsForm)); diff --git a/interface/src/project/EMSESPtypes.ts b/interface/src/project/EMSESPtypes.ts index 1d8d69785..a2a8f0ce7 100644 --- a/interface/src/project/EMSESPtypes.ts +++ b/interface/src/project/EMSESPtypes.ts @@ -20,7 +20,7 @@ export interface EMSESPSettings { analog_enabled: boolean; pbutton_gpio: number; trace_raw: boolean; - board_profile: number; + board_profile: string; } export enum busConnectionStatus { diff --git a/lib_standalone/ESP8266React.h b/lib_standalone/ESP8266React.h index 62aaddb82..e598051f3 100644 --- a/lib_standalone/ESP8266React.h +++ b/lib_standalone/ESP8266React.h @@ -49,7 +49,7 @@ class DummySettings { String staticIPConfig = ""; String dnsIP1 = ""; String dnsIP2 = ""; - uint8_t board_profile = 0; + String board_profile = "CUSTOM"; uint16_t publish_time_boiler = 10; uint16_t publish_time_thermostat = 10; uint16_t publish_time_solar = 10; diff --git a/src/WebSettingsService.cpp b/src/WebSettingsService.cpp index b299546b5..b5bbf1db6 100644 --- a/src/WebSettingsService.cpp +++ b/src/WebSettingsService.cpp @@ -22,9 +22,16 @@ namespace emsesp { uint8_t WebSettings::flags_; +using namespace std::placeholders; // for `_1` etc + WebSettingsService::WebSettingsService(AsyncWebServer * server, FS * fs, SecurityManager * securityManager) : _httpEndpoint(WebSettings::read, WebSettings::update, this, server, EMSESP_SETTINGS_SERVICE_PATH, securityManager) - , _fsPersistence(WebSettings::read, WebSettings::update, this, fs, EMSESP_SETTINGS_FILE) { + , _fsPersistence(WebSettings::read, WebSettings::update, this, fs, EMSESP_SETTINGS_FILE) + , _boardProfileHandler(EMSESP_BOARD_PROFILE_SERVICE_PATH, securityManager->wrapCallback(std::bind(&WebSettingsService::board_profile, this, _1, _2), AuthenticationPredicates::IS_ADMIN)) { + _boardProfileHandler.setMethod(HTTP_POST); + _boardProfileHandler.setMaxContentLength(256); + server->addHandler(&_boardProfileHandler); + addUpdateHandler([&](const String & originId) { onUpdate(); }, false); } @@ -189,4 +196,35 @@ void WebSettingsService::save() { _fsPersistence.writeToFS(); } +// build the json profile to send back +void WebSettingsService::board_profile(AsyncWebServerRequest * request, JsonVariant & json) { + if (json.is()) { + AsyncJsonResponse * response = new AsyncJsonResponse(false, EMSESP_JSON_SIZE_MEDIUM); + JsonObject root = response->getRoot(); + if (json.containsKey("code")) { + String board_profile = json["code"]; + std::vector data; // led, dallas, rx, tx, button + // check for valid board + if (System::load_board_profile(data, board_profile.c_str())) { + root["led_gpio"] = data[0]; + root["dallas_gpio"] = data[1]; + root["rx_gpio"] = data[2]; + root["tx_gpio"] = data[3]; + root["pbutton_gpio"] = data[4]; + } else { + AsyncWebServerResponse * response = request->beginResponse(200); + request->send(response); + return; + } + + response->setLength(); + request->send(response); + return; + } + } + + AsyncWebServerResponse * response = request->beginResponse(200); + request->send(response); +} + } // namespace emsesp \ No newline at end of file diff --git a/src/WebSettingsService.h b/src/WebSettingsService.h index c03f488bc..fc812cfc3 100644 --- a/src/WebSettingsService.h +++ b/src/WebSettingsService.h @@ -24,6 +24,7 @@ #define EMSESP_SETTINGS_FILE "/config/emsespSettings.json" #define EMSESP_SETTINGS_SERVICE_PATH "/rest/emsespSettings" +#define EMSESP_BOARD_PROFILE_SERVICE_PATH "/rest/boardProfile" #define EMSESP_DEFAULT_TX_MODE 1 // EMS1.0 #define EMSESP_DEFAULT_TX_DELAY 0 // no delay @@ -42,7 +43,7 @@ #define EMSESP_DEFAULT_API_ENABLED false // turn off, because its insecure #define EMSESP_DEFAULT_BOOL_FORMAT 1 // on/off #define EMSESP_DEFAULT_ANALOG_ENABLED false -#define EMSESP_DEFAULT_BOARD_PROFILE 0 // default ESP32 +#define EMSESP_DEFAULT_BOARD_PROFILE "S32" // Default GPIO PIN definitions #if defined(ESP32) @@ -85,7 +86,7 @@ class WebSettings { bool api_enabled; bool analog_enabled; uint8_t pbutton_gpio; - uint8_t board_profile; + String board_profile; static void read(WebSettings & settings, JsonObject & root); static StateUpdateResult update(JsonObject & root, WebSettings & settings); @@ -137,8 +138,11 @@ class WebSettingsService : public StatefulService { void save(); private: - HttpEndpoint _httpEndpoint; - FSPersistence _fsPersistence; + HttpEndpoint _httpEndpoint; + FSPersistence _fsPersistence; + AsyncCallbackJsonWebHandler _boardProfileHandler; + + void board_profile(AsyncWebServerRequest * request, JsonVariant & json); void onUpdate(); }; diff --git a/src/locale_EN.h b/src/locale_EN.h index 3c192fabd..121678fd7 100644 --- a/src/locale_EN.h +++ b/src/locale_EN.h @@ -68,7 +68,7 @@ MAKE_PSTR_WORD(master) MAKE_PSTR_WORD(pin) MAKE_PSTR_WORD(publish) MAKE_PSTR_WORD(timeout) -MAKE_PSTR_WORD(ethernet) +MAKE_PSTR_WORD(board_profile) // for commands MAKE_PSTR_WORD(call) @@ -101,6 +101,7 @@ MAKE_PSTR(master_thermostat_fmt, "Master Thermostat Device ID = %s") MAKE_PSTR(host_fmt, "Host = %s") MAKE_PSTR(port_fmt, "Port = %d") MAKE_PSTR(hostname_fmt, "Hostname = %s") +MAKE_PSTR(board_profile_fmt, "Board Profile = %s") MAKE_PSTR(mark_interval_fmt, "Mark interval = %lus") MAKE_PSTR(wifi_ssid_fmt, "WiFi SSID = %s") MAKE_PSTR(wifi_password_fmt, "WiFi Password = %S") @@ -187,13 +188,6 @@ MAKE_PSTR_WORD(French) MAKE_PSTR_WORD(Italian) MAKE_PSTR_WORD(high) MAKE_PSTR_WORD(low) -MAKE_PSTR(internal_temperature, "internal temperature") -MAKE_PSTR(internal_setpoint, "internal setpoint") -MAKE_PSTR(external_temperature, "external temperature") -MAKE_PSTR(burner_temperature, "burner temperature") -MAKE_PSTR(WW_temperature, "WW temperature") -MAKE_PSTR(functioning_mode, "functioning mode") -MAKE_PSTR(smoke_temperature, "smoke temperature") MAKE_PSTR_WORD(radiator) MAKE_PSTR_WORD(convector) MAKE_PSTR_WORD(floor) @@ -212,6 +206,13 @@ MAKE_PSTR_WORD(night) MAKE_PSTR_WORD(day) MAKE_PSTR_WORD(holiday) MAKE_PSTR_WORD(reduce) +MAKE_PSTR(internal_temperature, "internal temperature") +MAKE_PSTR(internal_setpoint, "internal setpoint") +MAKE_PSTR(external_temperature, "external temperature") +MAKE_PSTR(burner_temperature, "burner temperature") +MAKE_PSTR(WW_temperature, "WW temperature") +MAKE_PSTR(functioning_mode, "functioning mode") +MAKE_PSTR(smoke_temperature, "smoke temperature") // thermostat lists MAKE_PSTR_LIST(enum_ibaMainDisplay, F_(internal_temperature), F_(internal_setpoint), F_(external_temperature), F_(burner_temperature), F_(WW_temperature), F_(functioning_mode), F_(time), F_(date), F_(smoke_temperature)) diff --git a/src/system.cpp b/src/system.cpp index ac29ba8ee..e282cb1d2 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -203,12 +203,12 @@ void System::wifi_tweak() { } // check for valid ESP32 pins. This is very dependent on which ESP32 board is being used. -// Typically you can't use 1, 6-11 (SPI flash), 12, 14 & 15, 20, 24 and 28-31 +// Typically you can't use 1, 6-11, 12, 14, 15, 20, 24, 28-31 and 40+ // we allow 0 as it has a special function on the NodeMCU apparently // See https://diyprojects.io/esp32-how-to-use-gpio-digital-io-arduino-code/#.YFpVEq9KhjG // and https://nodemcu.readthedocs.io/en/dev-esp32/modules/gpio/ bool System::is_valid_gpio(uint8_t pin) { - if ((pin == 1) || (pin >= 6 && pin <= 12) || (pin >= 14 && pin <= 15) || (pin == 20) || (pin == 24) || (pin >= 28 && pin <= 31)) { + if ((pin == 1) || (pin >= 6 && pin <= 12) || (pin >= 14 && pin <= 15) || (pin == 20) || (pin == 24) || (pin >= 28 && pin <= 31) || (pin > 40)) { return false; // bad pin } return true; @@ -233,6 +233,8 @@ void System::start(uint32_t heap_start) { LOG_INFO(F("System %s booted (EMS-ESP version %s)"), networkSettings.hostname.c_str(), EMSESP_APP_VERSION); // print boot message }); + LOG_INFO("Loaded Board profile: %s", board_profile_.c_str()); + commands_init(); // console & api commands led_init(false); // init LED adc_init(false); // analog ADC @@ -321,7 +323,7 @@ void System::led_init(bool refresh) { if ((led_gpio_ != 0) && is_valid_gpio(led_gpio_)) { pinMode(led_gpio_, OUTPUT); // 0 means disabled - digitalWrite(led_gpio_, hide_led_ ? !LED_ON : LED_ON); // LED on, forever + digitalWrite(led_gpio_, hide_led_ ? !LED_ON : LED_ON); } } @@ -470,6 +472,8 @@ void System::network_init(bool refresh) { last_system_check_ = 0; // force the LED to go from fast flash to pulse send_heartbeat(); + /* + // check board profile for those which use ethernet (id > 10) // ethernet uses lots of additional memory so we only start it when it's explicitly set in the config if (board_profile_ < 10) { @@ -531,6 +535,7 @@ void System::network_init(bool refresh) { "local"); #endif } + */ } // check health of system, done every few seconds @@ -559,7 +564,7 @@ void System::system_check() { system_healthy_ = true; send_heartbeat(); if (led_gpio_) { - digitalWrite(led_gpio_, hide_led_ ? !LED_ON : LED_ON); // LED on, for ever + digitalWrite(led_gpio_, hide_led_ ? !LED_ON : LED_ON); } } } @@ -568,7 +573,7 @@ void System::system_check() { // commands - takes static function pointers // these commands respond to the topic "system" and take a payload like {cmd:"", data:"", id:""} -// no individual subsribe for pin command because id is needed +// no individual subscribe for pin command because id is needed void System::commands_init() { Command::add(EMSdevice::DeviceType::SYSTEM, F_(pin), System::command_pin, MqttSubFlag::FLAG_NOSUB); Command::add(EMSdevice::DeviceType::SYSTEM, F_(send), System::command_send); @@ -779,7 +784,7 @@ void System::console_commands(Shell & shell, unsigned int context) { CommandFlags::ADMIN, flash_string_vector{F_(set), F_(hostname)}, flash_string_vector{F_(name_mandatory)}, - [](Shell & shell __attribute__((unused)), const std::vector & arguments) { + [](Shell & shell, const std::vector & arguments) { shell.println("The network connection will be reset..."); Shell::loop_all(); delay(1000); // wait a second @@ -823,39 +828,41 @@ void System::console_commands(Shell & shell, unsigned int context) { }); }); - EMSESPShell::commands->add_command( - ShellContext::SYSTEM, - CommandFlags::ADMIN, - flash_string_vector{F_(set), F_(ethernet)}, - flash_string_vector{F_(n_mandatory)}, - [](Shell & shell, const std::vector & arguments) { - uint8_t n = Helpers::hextoint(arguments.front().c_str()); - if (n <= 2) { - EMSESP::webSettingsService.update( - [&](WebSettings & settings) { - settings.board_profile = n; - shell.printfln(F_(ethernet_option_fmt), n); - return StateUpdateResult::CHANGED; - }, - "local"); - EMSESP::system_.network_init(true); - } else { - shell.println(F("Must be 0, 1 or 2")); - } - }, - [](Shell & shell __attribute__((unused)), const std::vector & arguments __attribute__((unused))) -> const std::vector { - return std::vector{read_flash_string(F("0")), read_flash_string(F("1")), read_flash_string(F("2"))}; - }); + EMSESPShell::commands->add_command(ShellContext::SYSTEM, + CommandFlags::ADMIN, + flash_string_vector{F_(set), F_(board_profile)}, + flash_string_vector{F_(name_mandatory)}, + [](Shell & shell, const std::vector & arguments) { + // check for valid profile + std::vector data; // led, dallas, rx, tx, button + std::string board_profile = Helpers::toUpper(arguments.front()); + if (!load_board_profile(data, board_profile)) { + shell.println(F("Invalid board profile")); + return; + } + shell.printfln("Loaded board profile %s with %d,%d,%d,%d,%d", board_profile.c_str(), data[0], data[1], data[2], data[3], data[4]); + EMSESP::webSettingsService.update( + [&](WebSettings & settings) { + settings.board_profile = board_profile.c_str(); + settings.led_gpio = data[0]; + settings.dallas_gpio = data[1]; + settings.rx_gpio = data[2]; + settings.tx_gpio = data[3]; + settings.pbutton_gpio = data[4]; + return StateUpdateResult::CHANGED; + }, + "local"); + shell.printfln("Loaded board profile %s with %d,%d,%d,%d,%d", board_profile.c_str(), data[0], data[1], data[2], data[3], data[4]); + EMSESP::system_.network_init(true); + }); EMSESPShell::commands->add_command(ShellContext::SYSTEM, CommandFlags::USER, flash_string_vector{F_(set)}, [](Shell & shell, const std::vector & arguments __attribute__((unused))) { EMSESP::esp8266React.getNetworkSettingsService()->read([&](NetworkSettings & networkSettings) { - shell.print(F(" ")); shell.printfln(F_(hostname_fmt), networkSettings.hostname.isEmpty() ? uuid::read_flash_string(F_(unset)).c_str() : networkSettings.hostname.c_str()); - shell.print(F(" ")); shell.printfln(F_(wifi_ssid_fmt), networkSettings.ssid.isEmpty() ? uuid::read_flash_string(F_(unset)).c_str() : networkSettings.ssid.c_str()); - shell.print(F(" ")); shell.printfln(F_(wifi_password_fmt), networkSettings.ssid.isEmpty() ? F_(unset) : F_(asterisks)); }); + EMSESP::webSettingsService.read([&](WebSettings & settings) { shell.printfln(F_(board_profile_fmt), settings.board_profile.c_str()); }); }); EMSESPShell::commands->add_command(ShellContext::SYSTEM, CommandFlags::ADMIN, flash_string_vector{F_(show), F_(users)}, [](Shell & shell, const std::vector & arguments __attribute__((unused))) { @@ -961,6 +968,7 @@ bool System::command_settings(const char * value, const int8_t id, JsonObject & node["api_enabled"] = settings.api_enabled; node["analog_enabled"] = settings.analog_enabled; node["pbutton_gpio"] = settings.pbutton_gpio; + node["board_profile"] = settings.board_profile; }); return true; @@ -1030,5 +1038,29 @@ bool System::command_test(const char * value, const int8_t id) { } #endif +// takes a board profile and populates a data array with GPIO configurations +bool System::load_board_profile(std::vector & data, const std::string & board_profile) { + if (board_profile == "S32") { + data = {2, 3, 23, 5, 0}; // Gateway S32 + } else if (board_profile == "E32") { + data = {2, 4, 5, 17, 33}; // Gateway E32 + } else if (board_profile == "MT-ET") { + data = {2, 4, 23, 5, 0}; // MT-ET Live D1 Mini + } else if (board_profile == "NODEMCU") { + data = {2, 4, 23, 5, 0}; // NodeMCU 32S + } else if (board_profile == "LOLIN") { + data = {2, 14, 17, 16, 0}; // Lolin D32 + } else if (board_profile == "WEMOS") { + data = {2, 14, 17, 16, 0}; // Wemos Mini D1-32 + } else if (board_profile == "OLIMEX") { + data = {32, 4, 5, 17, 34}; // Olimex ESP32-EVB-EA + } else if (board_profile == "TLK110") { + data = {2, 4, 5, 17, 33}; // Ethernet (TLK110) + } else { + return false; + } + + return true; +} } // namespace emsesp diff --git a/src/system.h b/src/system.h index 135a47860..6228358a9 100644 --- a/src/system.h +++ b/src/system.h @@ -76,6 +76,8 @@ class System { static bool is_valid_gpio(uint8_t pin); + static bool load_board_profile(std::vector & data, const std::string & board_profile); + bool check_upgrade(); void send_heartbeat(); @@ -115,7 +117,7 @@ class System { static constexpr uint32_t LED_WARNING_BLINK_FAST = 100; // flash quickly for boot up sequence static constexpr uint32_t SYSTEM_HEARTBEAT_INTERVAL = 60000; // in milliseconds, how often the MQTT heartbeat is sent (1 min) static constexpr uint32_t SYSTEM_MEASURE_ANALOG_INTERVAL = 500; - static constexpr uint8_t LED_ON = LOW; // internal LED + static constexpr uint8_t LED_ON = HIGH; // LED #ifndef EMSESP_STANDALONE static uuid::syslog::SyslogService syslog_; @@ -145,7 +147,7 @@ class System { uint8_t led_gpio_; bool syslog_enabled_; bool analog_enabled_; - uint8_t board_profile_; + String board_profile_; uint8_t pbutton_gpio_; int8_t syslog_level_; uint32_t syslog_mark_interval_; diff --git a/src/version.h b/src/version.h index 963196b11..3962aa887 100644 --- a/src/version.h +++ b/src/version.h @@ -1,2 +1,2 @@ -#define EMSESP_APP_VERSION "3.0.1b3" +#define EMSESP_APP_VERSION "3.0.1b4" #define EMSESP_PLATFORM "ESP32"