From 132acaf75842c10ccddc1f6caa810049b1dbf839 Mon Sep 17 00:00:00 2001 From: Proddy Date: Thu, 5 Jan 2023 15:11:15 +0100 Subject: [PATCH] updated console based on latest uuid::console --- src/console.cpp | 919 +++++++++++++++++++++++------------------------- src/console.h | 73 ++-- 2 files changed, 467 insertions(+), 525 deletions(-) diff --git a/src/console.cpp b/src/console.cpp index a9486facf..95c724add 100644 --- a/src/console.cpp +++ b/src/console.cpp @@ -17,6 +17,7 @@ */ #include "console.h" +#include "console_stream.h" #include "emsesp.h" #include "version.h" @@ -24,116 +25,331 @@ #include "test/test.h" #endif +using ::uuid::console::Commands; +using ::uuid::console::Shell; +using LogLevel = ::uuid::log::Level; +using LogFacility = ::uuid::log::Facility; + namespace emsesp { -std::shared_ptr EMSESPShell::commands = [] { - std::shared_ptr commands = std::make_shared(); - return commands; -}(); +static constexpr unsigned long INVALID_PASSWORD_DELAY_MS = 3000; -std::shared_ptr shell; -std::vector EMSESPStreamConsole::ptys_; - -#ifndef EMSESP_STANDALONE -uuid::telnet::TelnetService telnet_([](Stream & stream, const IPAddress & addr, uint16_t port) -> std::shared_ptr { - return std::make_shared(stream, addr, port); -}); -#endif - -EMSESPShell::EMSESPShell() - : Shell() { +static inline EMSESPShell & to_shell(Shell & shell) { + return static_cast(shell); } -void EMSESPShell::started() { - logger().log(LogLevel::DEBUG, LogFacility::CONSOLE, ("User session opened on console %s"), console_name().c_str()); +static inline EMSESP & to_app(Shell & shell) { + return to_shell(shell).emsesp_; } -void EMSESPShell::stopped() { - if (has_flags(CommandFlags::ADMIN)) { - logger().log(LogLevel::DEBUG, LogFacility::AUTH, ("su session closed on console %s"), console_name().c_str()); +#define NO_ARGUMENTS \ + std::vector {} + +// add static functions here.... + +static void console_log_level(Shell & shell, const std::vector & arguments) { + if (!arguments.empty()) { + uuid::log::Level level; + + if (uuid::log::parse_level_lowercase(arguments[0], level)) { + shell.log_level(level); + } else { + shell.printfln(F_(invalid_log_level)); + return; + } } - logger().log(LogLevel::DEBUG, LogFacility::CONSOLE, ("User session closed on console %s"), console_name().c_str()); + shell.printfln(F_(log_level_fmt), uuid::log::format_level_uppercase(shell.log_level())); } -// show welcome banner -// this is one of the first functions called when the shell is started -void EMSESPShell::display_banner() { - println(); - printfln("┌────────────────────────────────────────────┐"); - printfln("│ %sEMS-ESP version %-10s%s │", COLOR_BOLD_ON, EMSESP_APP_VERSION, COLOR_BOLD_OFF); - printfln("│ %s%shttps://github.com/emsesp/EMS-ESP32%s │", COLOR_BRIGHT_GREEN, COLOR_UNDERLINE, COLOR_RESET); - printfln("│ │"); - printfln("│ type %shelp%s to show available commands │", COLOR_UNDERLINE, COLOR_RESET); - printfln("└────────────────────────────────────────────┘"); - println(); +static std::vector log_level_autocomplete(Shell & shell, const std::vector & current_arguments, const std::string & next_argument) { + return uuid::log::levels_lowercase(); +} - // set console name - EMSESP::esp8266React.getNetworkSettingsService()->read([&](NetworkSettings & networkSettings) { console_hostname_ = networkSettings.hostname.c_str(); }); +static void setup_commands(std::shared_ptr & commands) { + // add for all contexts + // console, exit, help, logout + for (unsigned int context = ShellContext::MAIN; context < ShellContext::END; context++) { + commands->add_command(context, CommandFlags::USER, {F_(console), F_(log)}, {F_(log_level_optional)}, console_log_level, log_level_autocomplete); - if (console_hostname_.empty()) { - console_hostname_ = "ems-esp"; + commands->add_command(context, + CommandFlags::USER, + {F_(exit)}, + context == ShellContext::MAIN ? EMSESPShell::main_exit_function : EMSESPShell::generic_exit_context_function); + + commands->add_command(context, CommandFlags::USER, {F_(help)}, EMSESPShell::main_help_function); + + commands->add_command(context, CommandFlags::USER, {F_(logout)}, EMSESPShell::main_logout_function); } - // load the list of commands - add_console_commands(); + /* example of going into a new context + commands->add_command(ShellContext::MAIN, CommandFlags::ADMIN, flash_string_vector{F_(fs)}, + [] (Shell &shell, const std::vector &arguments) { + shell.enter_context(ShellContext::FILESYSTEM); + }); + */ - // turn off watch, unless is test mode - EMSESP::watch_id(WATCH_ID_NONE); -#if defined(EMSESP_STANDALONE) - EMSESP::watch(EMSESP::WATCH_ON); -#else - EMSESP::watch(EMSESP::WATCH_OFF); -#endif -} - -// pre-loads all the console commands into the MAIN context -// This is only done after a connection is established, to save on Heap memory -void EMSESPShell::add_console_commands() { - // if we already have these commands loaded, stop adding duplicates - // for example when opening multiple serial/telnet sessions - if (console_commands_loaded_) { - return; - } - console_commands_loaded_ = true; - // just in case, remove everything - commands->remove_all_commands(); + // + // Show commands + // commands->add_command(ShellContext::MAIN, CommandFlags::USER, - string_vector{F_(show)}, - [](Shell & shell, const std::vector & arguments __attribute__((unused))) { + string_vector{F_(show), F_(system)}, + [=](Shell & shell, const std::vector & arguments) { + shell.println(); shell.printfln("%s%sEMS-ESP version %s%s", COLOR_BRIGHT_GREEN, COLOR_BOLD_ON, EMSESP_APP_VERSION, COLOR_RESET); shell.println(); - EMSESP::show_device_values(shell); - EMSESP::show_sensor_values(shell); + to_app(shell).system_.show_system(shell); + shell.println(); }); + commands->add_command(ShellContext::MAIN, + CommandFlags::ADMIN, + string_vector{F_(show), F_(users)}, + [](Shell & shell, const std::vector & arguments) { to_app(shell).system_.show_users(shell); }); + commands->add_command(ShellContext::MAIN, CommandFlags::USER, string_vector{F_(show), F_(devices)}, - [](Shell & shell, const std::vector & arguments __attribute__((unused))) { EMSESP::show_devices(shell); }); + [](Shell & shell, const std::vector & arguments) { to_app(shell).show_devices(shell); }); - commands->add_command(ShellContext::MAIN, - CommandFlags::USER, - string_vector{F_(show), F_(ems)}, - [](Shell & shell, const std::vector & arguments __attribute__((unused))) { EMSESP::show_ems(shell); }); + commands->add_command(ShellContext::MAIN, CommandFlags::USER, string_vector{F_(show), F_(ems)}, [](Shell & shell, const std::vector & arguments) { + to_app(shell).show_ems(shell); + }); - commands->add_command(ShellContext::MAIN, - CommandFlags::USER, - string_vector{F_(show), F_(values)}, - [](Shell & shell, const std::vector & arguments __attribute__((unused))) { EMSESP::show_device_values(shell); }); + commands->add_command(ShellContext::MAIN, CommandFlags::USER, string_vector{F_(show), F_(values)}, [](Shell & shell, const std::vector & arguments) { + to_app(shell).show_device_values(shell); + to_app(shell).show_sensor_values(shell); + }); commands->add_command(ShellContext::MAIN, CommandFlags::USER, string_vector{F_(show), F_(mqtt)}, - [](Shell & shell, const std::vector & arguments __attribute__((unused))) { Mqtt::show_mqtt(shell); }); + [](Shell & shell, const std::vector & arguments) { Mqtt::show_mqtt(shell); }); commands->add_command(ShellContext::MAIN, CommandFlags::USER, string_vector{F_(show), F_(commands)}, - [](Shell & shell, const std::vector & arguments __attribute__((unused))) { Command::show_all(shell); }); + [](Shell & shell, const std::vector & arguments) { Command::show_all(shell); }); + + + // + // System commands + // + +#if defined(EMSESP_DEBUG) + // create commands test and t + commands->add_command(ShellContext::MAIN, + CommandFlags::USER, + string_vector{"test"}, + string_vector{F_(name_optional), F_(data_optional)}, + [=](Shell & shell, const std::vector & arguments) { + if (arguments.empty()) { + Test::run_test(shell, "default"); + } else if (arguments.size() == 1) { + Test::run_test(shell, arguments.front()); + } else { + Test::run_test(shell, arguments[0].c_str(), arguments[1].c_str()); + } + }); + + commands->add_command(ShellContext::MAIN, CommandFlags::USER, string_vector{("t")}, [](Shell & shell, const std::vector & arguments) { + Test::run_test(shell, "default"); + }); + + commands->add_command(ShellContext::MAIN, + CommandFlags::USER, + string_vector{F_(debug)}, + string_vector{F_(name_optional)}, + [](Shell & shell, const std::vector & arguments) { + if (arguments.empty()) { + Test::debug(shell, "default"); + } else { + Test::debug(shell, arguments.front()); + } + }); +#endif + + commands->add_command(ShellContext::MAIN, CommandFlags::USER, string_vector{F_(su)}, [=](Shell & shell, const std::vector & arguments) { + auto become_admin = [](Shell & shell) { + shell.logger().log(LogLevel::NOTICE, LogFacility::AUTH, F("Admin session opened on console %s"), to_shell(shell).console_name().c_str()); + shell.add_flags(CommandFlags::ADMIN); + }; + + if (shell.has_flags(CommandFlags::ADMIN)) { + return; + } else if (shell.has_flags(CommandFlags::LOCAL)) { + become_admin(shell); + } else { + shell.enter_password(F_(password_prompt), [=](Shell & shell, bool completed, const std::string & password) { + if (completed) { + uint64_t now = uuid::get_uptime_ms(); + + to_app(shell).esp8266React.getSecuritySettingsService()->read([&](SecuritySettings & securitySettings) { + if (!password.empty() && (securitySettings.jwtSecret.equals(password.c_str()))) { + become_admin(shell); + } else { + shell.delay_until(now + INVALID_PASSWORD_DELAY_MS, [](Shell & shell) { + shell.logger().log(LogLevel::NOTICE, + LogFacility::AUTH, + F("Invalid admin password on console %s"), + to_shell(shell).console_name().c_str()); + shell.println(F("su: incorrect password")); + }); + } + }); + } + }); + } + }); + + commands->add_command(ShellContext::MAIN, CommandFlags::ADMIN, string_vector{F_(passwd)}, [](Shell & shell, const std::vector & arguments) { + shell.enter_password(F_(new_password_prompt1), [](Shell & shell, bool completed, const std::string & password1) { + if (completed) { + shell.enter_password(F_(new_password_prompt2), [password1](Shell & shell, bool completed, const std::string & password2) { + if (completed) { + if (password1 == password2) { + to_app(shell).esp8266React.getSecuritySettingsService()->update( + [&](SecuritySettings & securitySettings) { + securitySettings.jwtSecret = password2.c_str(); + return StateUpdateResult::CHANGED; + }, + "local"); + shell.println("Admin password updated"); + } else { + shell.println("Passwords do not match"); + } + } + }); + } + }); + }); + + commands->add_command(ShellContext::MAIN, CommandFlags::ADMIN, string_vector{F_(restart)}, [](Shell & shell, const std::vector & arguments) { + to_app(shell).system_.system_restart(); + }); + + commands->add_command(ShellContext::MAIN, + CommandFlags::ADMIN, + string_vector{F_(wifi), F_(reconnect)}, + [](Shell & shell, const std::vector & arguments) { to_app(shell).system_.wifi_reconnect(); }); + + commands->add_command(ShellContext::MAIN, CommandFlags::ADMIN, string_vector{F_(format)}, [](Shell & shell, const std::vector & arguments) { + shell.enter_password(F_(password_prompt), [=](Shell & shell, bool completed, const std::string & password) { + if (completed) { + to_app(shell).esp8266React.getSecuritySettingsService()->read([&](SecuritySettings & securitySettings) { + if (securitySettings.jwtSecret.equals(password.c_str())) { + to_app(shell).system_.format(shell); + } else { + shell.println("incorrect password"); + } + }); + } + }); + }); + + // + // SET commands + // + + /* +#ifndef EMSESP_STANDALONE + commands->add_command(ShellContext::MAIN, + CommandFlags::USER, + string_vector{F_(set), F_(timeout)}, + string_vector{F_(n_mandatory)}, + [](Shell & shell, const std::vector & arguments) { + uint16_t value = Helpers::atoint(arguments.front().c_str()); + telnet_.initial_idle_timeout(value * 60); + shell.printfln("Console timeout set to %d minutes", value); + }); +#endif +*/ + + commands->add_command(ShellContext::MAIN, + CommandFlags::ADMIN, + string_vector{F_(set), F_(wifi), F_(password)}, + [](Shell & shell, const std::vector & arguments) { + shell.enter_password(F_(new_password_prompt1), [](Shell & shell, bool completed, const std::string & password1) { + if (completed) { + shell.enter_password(F_(new_password_prompt2), [password1](Shell & shell, bool completed, const std::string & password2) { + if (completed) { + if (password1 == password2) { + to_app(shell).esp8266React.getNetworkSettingsService()->updateWithoutPropagation( + [&](NetworkSettings & networkSettings) { + networkSettings.password = password2.c_str(); + return StateUpdateResult::CHANGED; + }); + shell.println("Use `wifi reconnect` to save and apply the new settings"); + } else { + shell.println("Passwords do not match"); + } + } + }); + } + }); + }); + + commands->add_command(ShellContext::MAIN, + CommandFlags::ADMIN, + string_vector{F_(set), F_(hostname)}, + string_vector{F_(name_mandatory)}, + [](Shell & shell, const std::vector & arguments) { + shell.println("The network connection will be reset..."); + Shell::loop_all(); + delay(1000); // wait a second + to_app(shell).esp8266React.getNetworkSettingsService()->update( + [&](NetworkSettings & networkSettings) { + networkSettings.hostname = arguments.front().c_str(); + return StateUpdateResult::CHANGED; + }, + "local"); + }); + + commands->add_command(ShellContext::MAIN, + CommandFlags::ADMIN, + string_vector{F_(set), F_(wifi), F_(ssid)}, + string_vector{F_(name_mandatory)}, + [](Shell & shell, const std::vector & arguments) { + to_app(shell).esp8266React.getNetworkSettingsService()->updateWithoutPropagation([&](NetworkSettings & networkSettings) { + networkSettings.ssid = arguments.front().c_str(); + return StateUpdateResult::CHANGED; + }); + shell.println("Use `wifi reconnect` to apply the new settings"); + }); + + + commands->add_command(ShellContext::MAIN, + CommandFlags::ADMIN, + string_vector{F_(set), F_(board_profile)}, + string_vector{F_(name_mandatory)}, + [](Shell & shell, const std::vector & arguments) { + std::vector data; // led, dallas, rx, tx, button, phy_type, eth_power, eth_phy_addr, eth_clock_mode + std::string board_profile = Helpers::toUpper(arguments.front()); + if (!to_app(shell).system_.load_board_profile(data, board_profile)) { + shell.println("Invalid board profile (S32, E32, MH-ET, NODEMCU, OLIMEX, OLIMEXPOE, C3MINI, S2MINI, CUSTOM)"); + return; + } + to_app(shell).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]; + settings.phy_type = data[5]; + settings.eth_power = data[6]; // can be -1 + settings.eth_phy_addr = data[7]; + settings.eth_clock_mode = data[8]; + return StateUpdateResult::CHANGED; + }, + "local"); + shell.printfln("Loaded board profile %s", board_profile.c_str()); + to_app(shell).system_.network_init(true); + }); commands->add_command( ShellContext::MAIN, @@ -143,7 +359,7 @@ void EMSESPShell::add_console_commands() { [](Shell & shell, const std::vector & arguments) { uint8_t device_id = Helpers::hextoint(arguments.front().c_str()); if ((device_id == 0x0B) || (device_id == 0x0D) || (device_id == 0x0A) || (device_id == 0x0F) || (device_id == 0x12)) { - EMSESP::webSettingsService.update( + to_app(shell).webSettingsService.update( [&](WebSettings & settings) { settings.ems_bus_id = device_id; shell.printfln(F_(bus_id_fmt), settings.ems_bus_id); @@ -154,7 +370,7 @@ void EMSESPShell::add_console_commands() { shell.println("Must be 0B, 0D, 0A, 0E, 0F, or 48 - 4D"); } }, - [](Shell & shell __attribute__((unused)), const std::vector & arguments __attribute__((unused))) -> const std::vector { + [](Shell & shell, const std::vector & current_arguments, const std::string & next_argument) -> std::vector { return std::vector{"0B", "0D", "0A", "0E", "0F", "48", "49", "4A", "4B", "4C", "4D"}; }); @@ -165,7 +381,7 @@ void EMSESPShell::add_console_commands() { [](Shell & shell, const std::vector & arguments) { uint8_t tx_mode = std::strtol(arguments[0].c_str(), nullptr, 10); // save the tx_mode - EMSESP::webSettingsService.update( + to_app(shell).webSettingsService.update( [&](WebSettings & settings) { settings.tx_mode = tx_mode; shell.printfln(F_(tx_mode_fmt), settings.tx_mode); @@ -174,16 +390,29 @@ void EMSESPShell::add_console_commands() { "local"); }); + commands->add_command(ShellContext::MAIN, CommandFlags::USER, string_vector{F_(set)}, [](Shell & shell, const std::vector & arguments) { + to_app(shell).webSettingsService.read([&](WebSettings & settings) { + shell.printfln("Language: %s", settings.locale.c_str()); + shell.printfln(F_(tx_mode_fmt), settings.tx_mode); + shell.printfln(F_(bus_id_fmt), settings.ems_bus_id); + shell.printfln(F_(board_profile_fmt), settings.board_profile.c_str()); + }); + }); + + // + // EMS device commands + // + commands->add_command(ShellContext::MAIN, CommandFlags::ADMIN, string_vector{F_(scan), F_(devices)}, string_vector{F_(deep_optional)}, [](Shell & shell, const std::vector & arguments) { if (arguments.size() == 0) { - EMSESP::scan_devices(); + to_app(shell).scan_devices(); } else { shell.printfln("Performing a deep scan..."); - EMSESP::clear_all_devices(); + to_app(shell).clear_all_devices(); std::vector Device_Ids; Device_Ids.push_back(0x08); // Boilers - 0x08 @@ -208,31 +437,19 @@ void EMSESPShell::add_console_commands() { // send the read command with Version command for (const uint8_t device_id : Device_Ids) { - EMSESP::send_read_request(EMSdevice::EMS_TYPE_VERSION, device_id); + to_app(shell).send_read_request(EMSdevice::EMS_TYPE_VERSION, device_id); } } }); - commands->add_command(ShellContext::MAIN, - CommandFlags::USER, - string_vector{F_(set)}, - [](Shell & shell, const std::vector & arguments __attribute__((unused))) { - EMSESP::webSettingsService.read([&](WebSettings & settings) { - shell.printfln("Language: %s", settings.locale.c_str()); - shell.printfln(F_(tx_mode_fmt), settings.tx_mode); - shell.printfln(F_(bus_id_fmt), settings.ems_bus_id); - shell.printfln(F_(board_profile_fmt), settings.board_profile.c_str()); - }); - }); - commands->add_command(ShellContext::MAIN, CommandFlags::USER, string_vector{F_(read)}, string_vector{F_(deviceid_mandatory), F_(typeid_mandatory), F_(offset_optional), F_(length_optional)}, - [=](Shell & shell __attribute__((unused)), const std::vector & arguments) { + [=](Shell & shell, const std::vector & arguments) { uint8_t device_id = Helpers::hextoint(arguments.front().c_str()); - if (!EMSESP::valid_device(device_id)) { + if (!to_app(shell).valid_device(device_id)) { shell.printfln("Invalid deviceID"); return; } @@ -241,28 +458,16 @@ void EMSESPShell::add_console_commands() { if (arguments.size() == 4) { uint16_t offset = Helpers::hextoint(arguments[2].c_str()); uint8_t length = Helpers::hextoint(arguments.back().c_str()); - EMSESP::send_read_request(type_id, device_id, offset, length); + to_app(shell).send_read_request(type_id, device_id, offset, length); } else if (arguments.size() == 3) { uint16_t offset = Helpers::hextoint(arguments.back().c_str()); - EMSESP::send_read_request(type_id, device_id, offset, EMS_MAX_TELEGRAM_LENGTH); + to_app(shell).send_read_request(type_id, device_id, offset, EMS_MAX_TELEGRAM_LENGTH); } else { // send with length to send immediately and trigger publish read_id - EMSESP::send_read_request(type_id, device_id, 0, EMS_MAX_TELEGRAM_LENGTH); + to_app(shell).send_read_request(type_id, device_id, 0, EMS_MAX_TELEGRAM_LENGTH); } }); -#ifndef EMSESP_STANDALONE - commands->add_command(ShellContext::MAIN, - CommandFlags::USER, - string_vector{F_(set), F_(timeout)}, - string_vector{F_(n_mandatory)}, - [](Shell & shell, const std::vector & arguments) { - uint16_t value = Helpers::atoint(arguments.front().c_str()); - telnet_.initial_idle_timeout(value * 60); - shell.printfln("Telnet timeout set to %d minutes", value); - }); -#endif - commands->add_command(ShellContext::MAIN, CommandFlags::USER, string_vector{F_(watch)}, @@ -274,20 +479,21 @@ void EMSESPShell::add_console_commands() { if (!arguments.empty()) { // get raw/pretty if (arguments[0] == (F_(raw))) { - EMSESP::watch(EMSESP::WATCH_RAW); // raw + to_app(shell).watch(to_app(shell).WATCH_RAW); // raw } else if (arguments[0] == (FL_(on)[0])) { - EMSESP::watch(EMSESP::WATCH_ON); // on + to_app(shell).watch(to_app(shell).WATCH_ON); // on } else if (arguments[0] == (FL_(off)[0])) { - EMSESP::watch(EMSESP::WATCH_OFF); // off + to_app(shell).watch(to_app(shell).WATCH_OFF); // off } else if (arguments[0] == (FL_(unknown)[0])) { - EMSESP::watch(EMSESP::WATCH_UNKNOWN); // unknown + to_app(shell).watch(to_app(shell).WATCH_UNKNOWN); // unknown watch_id = WATCH_ID_NONE; } else { watch_id = Helpers::hextoint(arguments[0].c_str()); - if (watch_id > 0 && ((EMSESP::watch() == EMSESP::WATCH_OFF) || (EMSESP::watch() == EMSESP::WATCH_UNKNOWN))) { - EMSESP::watch(EMSESP::WATCH_ON); // on + if (watch_id > 0 + && ((to_app(shell).watch() == to_app(shell).WATCH_OFF) || (to_app(shell).watch() == to_app(shell).WATCH_UNKNOWN))) { + to_app(shell).watch(to_app(shell).WATCH_ON); // on } else if (watch_id == 0) { - EMSESP::watch(EMSESP::WATCH_OFF); // off + to_app(shell).watch(to_app(shell).WATCH_OFF); // off return; } } @@ -297,14 +503,14 @@ void EMSESPShell::add_console_commands() { watch_id = Helpers::hextoint(arguments[1].c_str()); } - EMSESP::watch_id(watch_id); + to_app(shell).watch_id(watch_id); } else { shell.printfln("Invalid: use watch raw|on|off|unknown|id [id]"); return; } - uint8_t watch = EMSESP::watch(); - if (watch == EMSESP::WATCH_OFF) { + uint8_t watch = to_app(shell).watch(); + if (watch == to_app(shell).WATCH_OFF) { shell.printfln("Watching telegrams is off"); return; } @@ -315,15 +521,15 @@ void EMSESPShell::add_console_commands() { shell.printfln("Setting log level to Notice"); } - if (watch == EMSESP::WATCH_ON) { + if (watch == to_app(shell).WATCH_ON) { shell.printfln("Watching incoming telegrams, displayed in decoded format"); - } else if (watch == EMSESP::WATCH_RAW) { + } else if (watch == to_app(shell).WATCH_RAW) { shell.printfln("Watching incoming telegrams, displayed as raw bytes"); // WATCH_RAW } else { shell.printfln("Watching unknown telegrams"); // WATCH_UNKNOWN } - watch_id = EMSESP::watch_id(); + watch_id = to_app(shell).watch_id(); if (watch_id > 0x80) { shell.printfln("Filtering only telegrams that match a telegram type of 0x%02X", watch_id); } else if (watch_id != WATCH_ID_NONE) { @@ -350,7 +556,6 @@ void EMSESPShell::add_console_commands() { return; } - // validate that a command is present if (arguments.size() < 2) { shell.print("Missing command. Available commands are: "); @@ -358,7 +563,7 @@ void EMSESPShell::add_console_commands() { return; } - DynamicJsonDocument doc(EMSESP_JSON_SIZE_XXLARGE_DYN); + DynamicJsonDocument doc(EMSESP_JSON_SIZE_XXXLARGE); int8_t id = -1; const char * cmd = Command::parse_command_string(arguments[1].c_str(), id); uint8_t return_code = CommandRet::OK; @@ -400,8 +605,8 @@ void EMSESPShell::add_console_commands() { shell.printfln("Bad syntax (error code %d)", return_code); } }, - [&](Shell & shell __attribute__((unused)), const std::vector & arguments) -> std::vector { - if (arguments.size() == 0) { + [](Shell & shell, const std::vector & current_arguments, const std::string & next_argument) -> std::vector { + if (current_arguments.size() == 0) { std::vector devices_list; devices_list.emplace_back(EMSdevice::device_type_2_device_name(EMSdevice::DeviceType::SYSTEM)); devices_list.emplace_back(EMSdevice::device_type_2_device_name(EMSdevice::DeviceType::DALLASSENSOR)); @@ -412,9 +617,9 @@ void EMSESPShell::add_console_commands() { } } return devices_list; - } else if (arguments.size() == 1) { + } else if (current_arguments.size() == 1) { std::vector command_list; - uint8_t device_type = EMSdevice::device_name_2_device_type(arguments[0].c_str()); + uint8_t device_type = EMSdevice::device_name_2_device_type(current_arguments[0].c_str()); if (Command::device_has_commands(device_type)) { for (const auto & cf : Command::commands()) { if (cf.device_type_ == device_type) { @@ -427,291 +632,63 @@ void EMSESPShell::add_console_commands() { return {}; }); +} - Console::load_standard_commands(ShellContext::MAIN); - Console::load_system_commands(ShellContext::MAIN); +std::shared_ptr EMSESPShell::commands_ = [] { + std::shared_ptr commands = std::make_shared(); + setup_commands(commands); + return commands; +}(); + +EMSESPShell::EMSESPShell(EMSESP & emsesp, Stream & stream, unsigned int context, unsigned int flags) + : Shell(stream, commands_, context, flags) + , emsesp_(emsesp) { +} + +void EMSESPShell::started() { + logger().log(LogLevel::INFO, LogFacility::CONSOLE, F("User session opened on console %s"), console_name().c_str()); +} + +void EMSESPShell::stopped() { + if (has_flags(CommandFlags::ADMIN)) { + logger().log(LogLevel::INFO, LogFacility::AUTH, F("Admin session closed on console %s"), console_name().c_str()); + } + logger().log(LogLevel::INFO, LogFacility::CONSOLE, F("User session closed on console %s"), console_name().c_str()); +} + +// show welcome banner +void EMSESPShell::display_banner() { + println(); + printfln("┌──────────────────────────────────────┐"); + printfln("│ %sEMS-ESP version %-12s%s │", COLOR_BOLD_ON, EMSESP_APP_VERSION, COLOR_BOLD_OFF); + printfln("│ %s%shttps://github.com/emsesp/EMS-ESP32%s │", COLOR_BRIGHT_GREEN, COLOR_UNDERLINE, COLOR_RESET); + printfln("│ │"); + printfln("│ type %shelp%s to show available commands │", COLOR_UNDERLINE, COLOR_RESET); + printfln("└──────────────────────────────────────┘"); + println(); + + // set console name + EMSESP::esp8266React.getNetworkSettingsService()->read([&](NetworkSettings & networkSettings) { console_hostname_ = networkSettings.hostname.c_str(); }); + if (console_hostname_.empty()) { + console_hostname_ = "ems-esp"; + } } std::string EMSESPShell::hostname_text() { return console_hostname_; } -// each custom context has the common commands like log, help, exit, su etc -void Console::load_standard_commands(unsigned int context) { -#if defined(EMSESP_DEBUG) - // create commands test and t - EMSESPShell::commands->add_command(context, - CommandFlags::USER, - string_vector{"test"}, - string_vector{F_(name_optional), F_(data_optional)}, - [](Shell & shell, const std::vector & arguments) { - if (arguments.empty()) { - Test::run_test(shell, "default"); - } else if (arguments.size() == 1) { - Test::run_test(shell, arguments.front()); - } else { - Test::run_test(shell, arguments[0].c_str(), arguments[1].c_str()); - } - }); +std::string EMSESPShell::context_text() { + auto shell_context = static_cast(context()); - EMSESPShell::commands->add_command(context, CommandFlags::USER, string_vector{("t")}, [](Shell & shell, const std::vector & arguments) { - Test::run_test(shell, "default"); - }); -#endif - -#if defined(EMSESP_DEBUG) - EMSESPShell::commands->add_command(context, - CommandFlags::USER, - string_vector{F_(debug)}, - string_vector{F_(name_optional)}, - [](Shell & shell, const std::vector & arguments) { - if (arguments.empty()) { - Test::debug(shell, "default"); - } else { - Test::debug(shell, arguments.front()); - } - }); -#endif - - EMSESPShell::commands->add_command( - context, - CommandFlags::USER, - string_vector{F_(log)}, - string_vector{F_(log_level_optional)}, - [](Shell & shell, const std::vector & arguments) { - if (!arguments.empty()) { - uuid::log::Level level; - if (uuid::log::parse_level_lowercase(arguments[0], level)) { - shell.log_level(level); - } else { - shell.printfln(F_(invalid_log_level)); - return; - } - } else { - shell.print("levels: "); - std::vector v = uuid::log::levels_lowercase(); - size_t i = v.size(); - while (i--) { - shell.printf(v[i].c_str()); - shell.print(' '); - } - shell.println(); - } - shell.printfln(F_(log_level_fmt), uuid::log::format_level_lowercase(shell.log_level())); - }, - [](Shell & shell __attribute__((unused)), const std::vector & arguments __attribute__((unused))) -> std::vector { - return uuid::log::levels_lowercase(); - }); - - EMSESPShell::commands->add_command(context, - CommandFlags::USER, - string_vector{F_(help)}, - [](Shell & shell, const std::vector & arguments __attribute__((unused))) { - shell.print_all_available_commands(); - }); - - EMSESPShell::commands->add_command(context, - CommandFlags::USER, - string_vector{F_(exit)}, - [=](Shell & shell, const std::vector & arguments __attribute__((unused))) { shell.stop(); }); - - EMSESPShell::commands->add_command(context, - CommandFlags::USER, - string_vector{F_(su)}, - [=](Shell & shell, const std::vector & arguments __attribute__((unused))) { - auto become_admin = [](Shell & shell) { - shell.logger().log(LogLevel::NOTICE, LogFacility::AUTH, ("su session opened on console")); - shell.add_flags(CommandFlags::ADMIN); - }; - - if (shell.has_flags(CommandFlags::LOCAL)) { - become_admin(shell); - } else { - shell.enter_password(F_(password_prompt), [=](Shell & shell, bool completed, const std::string & password) { - if (completed) { - uint64_t now = uuid::get_uptime_ms(); - - EMSESP::esp8266React.getSecuritySettingsService()->read([&](SecuritySettings & securitySettings) { - if (securitySettings.jwtSecret.equals(password.c_str())) { - become_admin(shell); - } else { - shell.delay_until(now + INVALID_PASSWORD_DELAY_MS, [](Shell & shell) { - shell.logger().log(LogLevel::NOTICE, LogFacility::AUTH, "Invalid su password on console"); - shell.println("su: incorrect password"); - }); - } - }); - } - }); - } - }); -} - -// console commands to add -void Console::load_system_commands(unsigned int context) { - EMSESPShell::commands->add_command(context, - CommandFlags::ADMIN, - string_vector{F_(restart)}, - [](Shell & shell __attribute__((unused)), const std::vector & arguments __attribute__((unused))) { - EMSESP::system_.system_restart(); - }); - - EMSESPShell::commands->add_command(context, - CommandFlags::ADMIN, - string_vector{F_(wifi), F_(reconnect)}, - [](Shell & shell __attribute__((unused)), const std::vector & arguments __attribute__((unused))) { - EMSESP::system_.wifi_reconnect(); - }); - - EMSESPShell::commands->add_command(ShellContext::MAIN, - CommandFlags::ADMIN, - string_vector{F_(format)}, - [](Shell & shell, const std::vector & arguments __attribute__((unused))) { - shell.enter_password(F_(password_prompt), [=](Shell & shell, bool completed, const std::string & password) { - if (completed) { - EMSESP::esp8266React.getSecuritySettingsService()->read([&](SecuritySettings & securitySettings) { - if (securitySettings.jwtSecret.equals(password.c_str())) { - EMSESP::system_.format(shell); - } else { - shell.println("incorrect password"); - } - }); - } - }); - }); - - EMSESPShell::commands->add_command(context, - CommandFlags::ADMIN, - string_vector{F_(passwd)}, - [](Shell & shell, const std::vector & arguments __attribute__((unused))) { - shell.enter_password(F_(new_password_prompt1), [](Shell & shell, bool completed, const std::string & password1) { - if (completed) { - shell.enter_password(F_(new_password_prompt2), - [password1](Shell & shell, bool completed, const std::string & password2) { - if (completed) { - if (password1 == password2) { - EMSESP::esp8266React.getSecuritySettingsService()->update( - [&](SecuritySettings & securitySettings) { - securitySettings.jwtSecret = password2.c_str(); - return StateUpdateResult::CHANGED; - }, - "local"); - shell.println("su password updated"); - } else { - shell.println("Passwords do not match"); - } - } - }); - } - }); - }); - - EMSESPShell::commands->add_command(context, - CommandFlags::USER, - string_vector{F_(show), F_(system)}, - [=](Shell & shell, const std::vector & arguments __attribute__((unused))) { - EMSESP::system_.show_system(shell); - shell.println(); - }); - - EMSESPShell::commands->add_command(context, - CommandFlags::ADMIN, - string_vector{F_(set), F_(hostname)}, - string_vector{F_(name_mandatory)}, - [](Shell & shell, const std::vector & arguments) { - shell.println("The network connection will be reset..."); - Shell::loop_all(); - delay(1000); // wait a second - EMSESP::esp8266React.getNetworkSettingsService()->update( - [&](NetworkSettings & networkSettings) { - networkSettings.hostname = arguments.front().c_str(); - return StateUpdateResult::CHANGED; - }, - "local"); - }); - - EMSESPShell::commands->add_command(context, - CommandFlags::ADMIN, - string_vector{F_(set), F_(wifi), F_(ssid)}, - string_vector{F_(name_mandatory)}, - [](Shell & shell, const std::vector & arguments) { - EMSESP::esp8266React.getNetworkSettingsService()->updateWithoutPropagation([&](NetworkSettings & networkSettings) { - networkSettings.ssid = arguments.front().c_str(); - return StateUpdateResult::CHANGED; - }); - shell.println("Use `wifi reconnect` to apply the new settings"); - }); - - // added by mvdp - EMSESPShell::commands->add_command(context, - CommandFlags::ADMIN, - string_vector{("mqtt"), ("subscribe")}, - string_vector{("")}, - [](Shell & shell, const std::vector & arguments) { - Mqtt::subscribe(arguments.front()); - shell.println("subscribing"); - }); - - EMSESPShell::commands->add_command(context, - CommandFlags::ADMIN, - string_vector{F_(set), F_(wifi), F_(password)}, - [](Shell & shell, const std::vector & arguments __attribute__((unused))) { - shell.enter_password(F_(new_password_prompt1), [](Shell & shell, bool completed, const std::string & password1) { - if (completed) { - shell.enter_password(F_(new_password_prompt2), - [password1](Shell & shell, bool completed, const std::string & password2) { - if (completed) { - if (password1 == password2) { - EMSESP::esp8266React.getNetworkSettingsService()->updateWithoutPropagation( - [&](NetworkSettings & networkSettings) { - networkSettings.password = password2.c_str(); - return StateUpdateResult::CHANGED; - }); - shell.println("Use `wifi reconnect` to save and apply the new settings"); - } else { - shell.println("Passwords do not match"); - } - } - }); - } - }); - }); - - EMSESPShell::commands->add_command(context, - CommandFlags::ADMIN, - string_vector{F_(set), F_(board_profile)}, - string_vector{F_(name_mandatory)}, - [](Shell & shell, const std::vector & arguments) { - std::vector data; // led, dallas, rx, tx, button, phy_type, eth_power, eth_phy_addr, eth_clock_mode - std::string board_profile = Helpers::toUpper(arguments.front()); - if (!EMSESP::system_.load_board_profile(data, board_profile)) { - shell.println("Invalid board profile (S32, E32, MH-ET, NODEMCU, OLIMEX, OLIMEXPOE, C3MINI, S2MINI, CUSTOM)"); - return; - } - 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]; - settings.phy_type = data[5]; - settings.eth_power = data[6]; // can be -1 - settings.eth_phy_addr = data[7]; - settings.eth_clock_mode = data[8]; - return StateUpdateResult::CHANGED; - }, - "local"); - shell.printfln("Loaded board profile %s", board_profile.c_str()); - EMSESP::system_.network_init(true); - }); - EMSESPShell::commands->add_command(context, - CommandFlags::ADMIN, - string_vector{F_(show), F_(users)}, - [](Shell & shell, const std::vector & arguments __attribute__((unused))) { - EMSESP::system_.show_users(shell); - }); + if (shell_context == ShellContext::MAIN) { + return std::string{}; + // return std::string{'/'}; + // } else if (shell_context == ShellContext::FILESYSTEM) { + // return "/fs"); + } else { + return std::string{}; + } } // when in su (admin) show # as the prompt suffix @@ -724,26 +701,68 @@ std::string EMSESPShell::prompt_suffix() { } void EMSESPShell::end_of_transmission() { - invoke_command(F_(exit)); + if (context() != ShellContext::MAIN || has_flags(CommandFlags::ADMIN)) { + invoke_command(F_(exit)); + } else { + invoke_command(F_(logout)); + } } -EMSESPStreamConsole::EMSESPStreamConsole(Stream & stream, bool local) - : uuid::console::Shell(commands, ShellContext::MAIN, local ? (CommandFlags::USER | CommandFlags::LOCAL) : CommandFlags::USER) - , uuid::console::StreamConsole(stream) - , EMSESPShell() - , name_("Serial") - , pty_(SIZE_MAX) +void EMSESPShell::generic_exit_context_function(Shell & shell, const std::vector & arguments) { + shell.exit_context(); +}; + +void EMSESPShell::main_help_function(Shell & shell, const std::vector & arguments) { + shell.print_all_available_commands(); +} + +void EMSESPShell::main_exit_function(Shell & shell, const std::vector & arguments) { + if (shell.has_flags(CommandFlags::ADMIN)) { + EMSESPShell::main_exit_admin_function(shell, NO_ARGUMENTS); + } else { + EMSESPShell::main_exit_user_function(shell, NO_ARGUMENTS); + } +} + +void EMSESPShell::main_logout_function(Shell & shell, const std::vector & arguments) { + if (shell.has_flags(CommandFlags::ADMIN)) { + EMSESPShell::main_exit_admin_function(shell, NO_ARGUMENTS); + } + EMSESPShell::main_exit_user_function(shell, NO_ARGUMENTS); +}; + +void EMSESPShell::main_exit_user_function(Shell & shell, const std::vector & arguments) { + shell.stop(); +}; + +void EMSESPShell::main_exit_admin_function(Shell & shell, const std::vector & arguments) { + shell.logger().log(LogLevel::INFO, LogFacility::AUTH, "Admin session closed on console %s", to_shell(shell).console_name().c_str()); + shell.remove_flags(CommandFlags::ADMIN); +}; + + // **** EMSESPConsole ***** + +#ifndef EMSESP_STANDALONE +std::vector EMSESPConsole::ptys_; +#endif + +EMSESPConsole::EMSESPConsole(EMSESP & emsesp, Stream & stream, bool local) + : APP_SHELL_TYPE(emsesp, stream, ShellContext::MAIN, local ? (CommandFlags::USER | CommandFlags::LOCAL) : CommandFlags::USER) + , name_("ttyS0") +#ifndef EMSESP_STANDALONE + , pty_(std::numeric_limits::max()) , addr_() - , port_(0) { + , port_(0) +#endif +{ } -EMSESPStreamConsole::EMSESPStreamConsole(Stream & stream, const IPAddress & addr, uint16_t port) - : uuid::console::Shell(commands, ShellContext::MAIN, CommandFlags::USER) - , uuid::console::StreamConsole(stream) - , EMSESPShell() +#ifndef EMSESP_STANDALONE +EMSESPConsole::EMSESPConsole(EMSESP & emsesp, Stream & stream, const IPAddress & addr, uint16_t port) + : APP_SHELL_TYPE(emsesp, stream, ShellContext::MAIN, CommandFlags::USER) , addr_(addr) , port_(port) { - std::vector text(16); + std::array text; pty_ = 0; while (pty_ < ptys_.size() && ptys_[pty_]) @@ -754,70 +773,26 @@ EMSESPStreamConsole::EMSESPStreamConsole(Stream & stream, const IPAddress & addr ptys_[pty_] = true; } - snprintf(text.data(), text.size(), "pty%u", (uint16_t)pty_); + snprintf(text.data(), text.size(), "pty%u", pty_); name_ = text.data(); -#ifndef EMSESP_STANDALONE - logger().info("Allocated console %s for connection from [%s]:%u", name_.c_str(), uuid::printable_to_string(addr_).c_str(), port_); -#endif -} -EMSESPStreamConsole::~EMSESPStreamConsole() { - if (pty_ != SIZE_MAX) { -#ifndef EMSESP_STANDALONE - logger().info("Shutdown console %s for connection from [%s]:%u", name_.c_str(), uuid::printable_to_string(addr_).c_str(), port_); + logger().info("Allocated console %s for connection from [%s]:%u", name_.c_str(), uuid::printable_to_string(addr_).c_str(), port_); +} #endif + +EMSESPConsole::~EMSESPConsole() { +#ifndef EMSESP_STANDALONE + if (pty_ != SIZE_MAX) { + logger().info("Shutdown console %s for connection from [%s]:%u", name_.c_str(), uuid::printable_to_string(addr_).c_str(), port_); + ptys_[pty_] = false; ptys_.shrink_to_fit(); } +#endif } -std::string EMSESPStreamConsole::console_name() { +std::string EMSESPConsole::console_name() { return name_; } -// Start serial console -void Console::start_serial() { - Serial.begin(115200); - - // Serial Console - is always active - shell = std::make_shared(Serial, true); - shell->maximum_log_messages(100); - shell->start(); - -#if defined(EMSESP_DEBUG) - shell->log_level(uuid::log::Level::DEBUG); -#endif - -#if defined(EMSESP_STANDALONE) - shell->add_flags(CommandFlags::ADMIN); // always start in su/admin mode when running tests -#endif -} - -// Start up telnet -void Console::start_telnet() { - telnet_enabled_ = true; // telnet is enabled when calling this function - - // start the telnet service - // default idle is 10 minutes, default write timeout is 0 (automatic) - // note, this must be started after the network/wifi for ESP32 otherwise it'll crash -#ifndef EMSESP_STANDALONE - telnet_.start(); - telnet_.initial_idle_timeout(3600); // in sec, one hour idle timeout - telnet_.default_write_timeout(1000); // in ms, socket timeout 1 second -#endif -} - -// handles telnet sync and logging to console -void Console::loop() { - uuid::loop(); - -#ifndef EMSESP_STANDALONE - if (telnet_enabled_) { - telnet_.loop(); - } -#endif - - Shell::loop_all(); -} - } // namespace emsesp diff --git a/src/console.h b/src/console.h index 3f5d649be..d6118eb18 100644 --- a/src/console.h +++ b/src/console.h @@ -26,85 +26,52 @@ #include "system.h" #include "mqtt.h" -using uuid::console::Commands; -using uuid::console::Shell; - -static constexpr uint32_t INVALID_PASSWORD_DELAY_MS = 2000; - -namespace emsesp { - -using LogLevel = ::uuid::log::Level; -using LogFacility = ::uuid::log::Facility; - #ifdef LOCAL #undef LOCAL #endif + +namespace emsesp { + enum CommandFlags : uint8_t { USER = 0, ADMIN = (1 << 0), LOCAL = (1 << 1) }; -enum ShellContext : uint8_t { - MAIN = 0, - SYSTEM, -}; +enum ShellContext : uint8_t { MAIN = 0, SYSTEM, END }; -class EMSESPShell : virtual public uuid::console::Shell { +class EMSESP; + +class EMSESPShell : public uuid::console::Shell { public: ~EMSESPShell() override = default; virtual std::string console_name() = 0; - static std::shared_ptr commands; - static std::shared_ptr shell; + static void generic_exit_context_function(Shell & shell, const std::vector & arguments); + static void main_help_function(Shell & shell, const std::vector & arguments); + static void main_exit_function(Shell & shell, const std::vector & arguments); + static void main_logout_function(Shell & shell, const std::vector & arguments); + + EMSESP & emsesp_; protected: - EMSESPShell(); + EMSESPShell(EMSESP & emsesp, Stream & stream, unsigned int context, unsigned int flags); + + static std::shared_ptr commands_; // our custom functions for Shell void started() override; void stopped() override; void display_banner() override; std::string hostname_text() override; + std::string context_text() override; std::string prompt_suffix() override; void end_of_transmission() override; - private: - void add_console_commands(); - bool console_commands_loaded_ = false; // set to true when the initial commands are loaded + static void main_exit_user_function(Shell & shell, const std::vector & arguments); + static void main_exit_admin_function(Shell & shell, const std::vector & arguments); + std::string console_hostname_; }; -class EMSESPStreamConsole : public uuid::console::StreamConsole, public EMSESPShell { - public: - EMSESPStreamConsole(Stream & stream, bool local); - EMSESPStreamConsole(Stream & stream, const IPAddress & addr, uint16_t port); - ~EMSESPStreamConsole() override; - - std::string console_name() override; - - private: - static std::vector ptys_; - - std::string name_; - size_t pty_; - IPAddress addr_; - uint16_t port_; -}; - -class Console { - public: - void loop(); - void start_serial(); - void start_telnet(); - - uuid::log::Level log_level(); - - static void load_standard_commands(unsigned int context); - static void load_system_commands(unsigned int context); - - private: - bool telnet_enabled_ = false; // telnet is default off -}; - } // namespace emsesp #endif \ No newline at end of file