/* TERMINAL MODULE Copyright (C) 2016-2019 by Xose Pérez Copyright (C) 2020 by Maxim Prokhorov */ #include "espurna.h" #if TERMINAL_SUPPORT #include "api.h" #include "settings.h" #include "system.h" #include "telnet.h" #include "utils.h" #include "mqtt.h" #include "wifi.h" #include "ws.h" #include "libs/URL.h" #include "libs/StreamAdapter.h" #include "libs/PrintString.h" #include "web_asyncwebprint_impl.h" #include #include #include #include #include // not yet CONNECTING or LISTENING extern struct tcp_pcb *tcp_bound_pcbs; // accepting or sending data extern struct tcp_pcb *tcp_active_pcbs; // // TIME-WAIT status extern struct tcp_pcb *tcp_tw_pcbs; namespace { // Based on libs/StreamInjector.h by Xose Pérez (see git-log for more info) // Instead of custom write(uint8_t) callback, we provide writer implementation in-place struct TerminalIO final : public Stream { TerminalIO(size_t capacity = 128) : _buffer(new char[capacity]), _capacity(capacity), _write(0), _read(0) {} ~TerminalIO() { delete[] _buffer; } // --------------------------------------------------------------------- // Injects data into the internal buffer so we can read() it // --------------------------------------------------------------------- size_t capacity() { return _capacity; } size_t inject(char ch) { _buffer[_write] = ch; _write = (_write + 1) % _capacity; return 1; } size_t inject(char *data, size_t len) { for (size_t index = 0; index < len; ++index) { inject(data[index]); } return len; } // --------------------------------------------------------------------- // XXX: We are only supporting part of the Print & Stream interfaces // But, we need to be have all pure virtual methods implemented // --------------------------------------------------------------------- // Return data from the internal buffer int available() override { unsigned int bytes = 0; if (_read > _write) { bytes += (_write - _read + _capacity); } else if (_read < _write) { bytes += (_write - _read); } return bytes; } int peek() override { int ch = -1; if (_read != _write) { ch = _buffer[_read]; } return ch; } int read() override { int ch = -1; if (_read != _write) { ch = _buffer[_read]; _read = (_read + 1) % _capacity; } return ch; } // {Stream,Print}::flush(), see: // - https://github.com/esp8266/Arduino/blob/master/cores/esp8266/Print.h // - https://github.com/espressif/arduino-esp32/blob/master/cores/esp32/Print.h // - https://github.com/arduino/ArduinoCore-API/issues/102 // Old 2.3.0 expects flush() on Stream, latest puts in in Print // We may have to cheat the system and implement everything as Stream to have it available. void flush() override { // Here, reset reader position so that we return -1 until we have new data // writer flushing is implemented below, we don't need it here atm _read = _write; } size_t write(const uint8_t* buffer, size_t size) override { // Buffer data until we encounter line break, then flush via Raw debug method // (which is supposed to 1-to-1 copy the data, without adding the timestamp) #if DEBUG_SUPPORT if (!size) return 0; if (buffer[size-1] == '\0') return 0; if (_output.capacity() < (size + 2)) { _output.reserve(_output.size() + size + 2); } _output.insert(_output.end(), reinterpret_cast(buffer), reinterpret_cast(buffer) + size ); if (_output.end() != std::find(_output.begin(), _output.end(), '\n')) { _output.push_back('\0'); debugSendRaw(_output.data()); _output.clear(); } #endif return size; } size_t write(uint8_t ch) override { uint8_t buffer[1] {ch}; return write(buffer, 1); } private: #if DEBUG_SUPPORT std::vector _output; #endif char * _buffer; unsigned char _capacity; unsigned char _write; unsigned char _read; }; auto _io = TerminalIO(TERMINAL_SHARED_BUFFER_SIZE); terminal::Terminal _terminal(_io, _io.capacity()); // TODO: re-evaluate how and why this is used #if SERIAL_RX_ENABLED constexpr size_t SerialRxBufferSize { 128u }; char _serial_rx_buffer[SerialRxBufferSize]; static unsigned char _serial_rx_pointer = 0; #endif // SERIAL_RX_ENABLED // ----------------------------------------------------------------------------- // Commands // ----------------------------------------------------------------------------- void _terminalHelpCommand(const terminal::CommandContext& ctx) { auto names = _terminal.names(); // XXX: Core's ..._P funcs only allow 2nd pointer to be in PROGMEM, // explicitly load the 1st one std::sort(names.begin(), names.end(), [](const __FlashStringHelper* lhs, const __FlashStringHelper* rhs) { const String lhs_as_string(lhs); return strncasecmp_P(lhs_as_string.c_str(), reinterpret_cast(rhs), lhs_as_string.length()) < 0; }); ctx.output.print(F("Available commands:\n")); for (auto* name : names) { ctx.output.printf("> %s\n", reinterpret_cast(name)); } terminalOK(ctx.output); } namespace dns { using Callback = std::function; namespace internal { struct Task { Task() = delete; explicit Task(String&& hostname, Callback&& callback) : _hostname(std::move(hostname)), _callback(std::move(callback)) {} ip_addr_t* addr() { return &_addr; } const char* hostname() const { return _hostname.c_str(); } void callback(const char* name, const ip_addr_t* addr, void* arg) { _callback(name, addr, arg); } void callback() { _callback(hostname(), addr(), nullptr); } private: String _hostname; Callback _callback; ip_addr_t _addr { IPADDR_NONE }; }; using TaskPtr = std::unique_ptr; TaskPtr task; void callback(const char* name, const ip_addr_t* addr, void* arg) { if (task) { task->callback(name, addr, arg); } task.reset(); } } // namespace internal bool started() { return static_cast(internal::task); } void start(String&& hostname, Callback&& callback) { auto task = std::make_unique(std::move(hostname), std::move(callback)); switch (dns_gethostbyname(task->hostname(), task->addr(), internal::callback, nullptr)) { case ERR_OK: task->callback(); break; case ERR_INPROGRESS: internal::task = std::move(task); break; default: break; } } } // namespace dns void _terminalInitCommands() { terminalRegisterCommand(F("COMMANDS"), _terminalHelpCommand); terminalRegisterCommand(F("HELP"), _terminalHelpCommand); terminalRegisterCommand(F("ERASE.CONFIG"), [](const terminal::CommandContext&) { terminalOK(); customResetReason(CustomResetReason::Terminal); eraseSDKConfig(); *((int*) 0) = 0; // see https://github.com/esp8266/Arduino/issues/1494 }); terminalRegisterCommand(F("ADC"), [](const terminal::CommandContext& ctx) { const int pin = (ctx.argc == 2) ? ctx.argv[1].toInt() : A0; ctx.output.println(analogRead(pin)); terminalOK(ctx); }); terminalRegisterCommand(F("GPIO"), [](const terminal::CommandContext& ctx) { const int pin = (ctx.argc >= 2) ? ctx.argv[1].toInt() : -1; if ((pin >= 0) && !gpioValid(pin)) { terminalError(ctx, F("Invalid pin number")); return; } int start = 0; int end = gpioPins(); switch (ctx.argc) { case 3: pinMode(pin, OUTPUT); digitalWrite(pin, (1 == ctx.argv[2].toInt())); break; case 2: start = pin; end = pin + 1; // fallthrough into print case 1: for (auto current = start; current < end; ++current) { if (gpioValid(current)) { ctx.output.printf_P(PSTR("%c %s @ GPIO%02d (%s)\n"), gpioLocked(current) ? '*' : ' ', GPEP(current) ? "OUTPUT" : "INPUT ", current, (HIGH == digitalRead(current)) ? "HIGH" : "LOW" ); } } break; } terminalOK(ctx); }); terminalRegisterCommand(F("HEAP"), [](const terminal::CommandContext& ctx) { static auto initial = systemInitialFreeHeap(); auto stats = systemHeapStats(); ctx.output.printf_P(PSTR("initial: %u, available: %u, fragmentation: %hhu%%\n"), initial, stats.available, stats.frag_pct); terminalOK(ctx); }); terminalRegisterCommand(F("STACK"), [](const terminal::CommandContext& ctx) { ctx.output.printf_P(PSTR("continuation stack initial: %d, free: %u\n"), CONT_STACKSIZE, systemFreeStack()); terminalOK(ctx); }); terminalRegisterCommand(F("INFO"), [](const terminal::CommandContext&) { info(); terminalOK(); }); terminalRegisterCommand(F("RESET"), [](const terminal::CommandContext& ctx) { auto count = 1; if (ctx.argc == 2) { count = ctx.argv[1].toInt(); if (count < SYSTEM_CHECK_MAX) { systemStabilityCounter(count); } } terminalOK(ctx); deferredReset(100, CustomResetReason::Terminal); }); terminalRegisterCommand(F("UPTIME"), [](const terminal::CommandContext& ctx) { ctx.output.println(getUptime()); terminalOK(ctx); }); #if SECURE_CLIENT == SECURE_CLIENT_BEARSSL terminalRegisterCommand(F("MFLN.PROBE"), [](const terminal::CommandContext& ctx) { if (ctx.argc != 3) { terminalError(F("[url] [value]")); return; } URL _url(ctx.argv[1]); uint16_t requested_mfln = atol(ctx.argv[2].c_str()); auto client = std::make_unique(); client->setInsecure(); if (client->probeMaxFragmentLength(_url.host.c_str(), _url.port, requested_mfln)) { terminalOK(); } else { terminalError(F("Buffer size not supported")); } }); #endif terminalRegisterCommand(F("HOST"), [](const terminal::CommandContext& ctx) { if (ctx.argc != 2) { terminalError(ctx, F("HOST ")); return; } dns::start(String(ctx.argv[1]), [&](const char* name, const ip_addr_t* addr, void*) { if (!addr) { ctx.output.printf_P(PSTR("%s not found\n"), name); return; } ctx.output.printf_P(PSTR("%s has address %s\n"), name, IPAddress(addr).toString().c_str()); }); while (dns::started()) { delay(100); } }); terminalRegisterCommand(F("NETSTAT"), [](const terminal::CommandContext& ctx) { auto print = [](Print& out, tcp_pcb* list) { for (tcp_pcb* pcb = list; pcb != nullptr; pcb = pcb->next) { out.printf_P(PSTR("state %s local %s:%hu remote %s:%hu\n"), tcp_debug_state_str(pcb->state), IPAddress(pcb->local_ip).toString().c_str(), pcb->local_port, IPAddress(pcb->remote_ip).toString().c_str(), pcb->remote_port); } }; print(ctx.output, tcp_active_pcbs); print(ctx.output, tcp_tw_pcbs); print(ctx.output, tcp_bound_pcbs); }); } void _terminalLoop() { #if DEBUG_SERIAL_SUPPORT while (DEBUG_PORT.available()) { _io.inject(DEBUG_PORT.read()); } #endif _terminal.process([](terminal::Terminal::Result result) { bool out = false; switch (result) { case terminal::Terminal::Result::CommandNotFound: terminalError(terminalDefaultStream(), F("Command not found")); out = true; break; case terminal::Terminal::Result::BufferOverflow: terminalError(terminalDefaultStream(), F("Command line buffer overflow")); out = true; break; case terminal::Terminal::Result::Command: out = true; break; case terminal::Terminal::Result::Pending: out = false; break; case terminal::Terminal::Result::Error: terminalError(terminalDefaultStream(), F("Unexpected error when parsing command line")); out = false; break; case terminal::Terminal::Result::NoInput: out = false; break; } return out; }); #if SERIAL_RX_ENABLED while (SERIAL_RX_PORT.available() > 0) { char rc = SERIAL_RX_PORT.read(); _serial_rx_buffer[_serial_rx_pointer++] = rc; if ((_serial_rx_pointer == SerialRxBufferSize) || (rc == 10)) { terminalInject(_serial_rx_buffer, (size_t) _serial_rx_pointer); _serial_rx_pointer = 0; } } #endif // SERIAL_RX_ENABLED } #if MQTT_SUPPORT && TERMINAL_MQTT_SUPPORT void _terminalMqttSetup() { mqttRegister([](unsigned int type, const char * topic, const char * payload) { if (type == MQTT_CONNECT_EVENT) { mqttSubscribe(MQTT_TOPIC_CMD); return; } if (type == MQTT_MESSAGE_EVENT) { String t = mqttMagnitude((char *) topic); if (!t.startsWith(MQTT_TOPIC_CMD)) return; if (!strlen(payload)) return; String cmd(payload); if (!cmd.endsWith("\r\n") && !cmd.endsWith("\n")) { cmd += '\n'; } // TODO: unlike http handler, we have only one output stream // and **must** have a fixed-size output buffer // (wishlist: MQTT client does some magic and we don't buffer twice) schedule_function([cmd]() { PrintString buffer(TCP_MSS); StreamAdapter stream(buffer, cmd.c_str(), cmd.c_str() + cmd.length() + 1); String out; terminal::Terminal handler(stream); switch (handler.processLine()) { case terminal::Terminal::Result::CommandNotFound: out += F("Command not found"); break; case terminal::Terminal::Result::Command: out = std::move(buffer); default: break; } if (out.length()) { mqttSendRaw(mqttTopic(MQTT_TOPIC_CMD, false).c_str(), out.c_str(), false); } }); return; } }); } #endif // MQTT_SUPPORT && TERMINAL_MQTT_SUPPORT } // namespace // ----------------------------------------------------------------------------- // Pubic API // ----------------------------------------------------------------------------- #if TERMINAL_WEB_API_SUPPORT // XXX: new `apiRegister()` depends that `webServer()` is available, meaning we can't call this setup func // before the `webSetup()` is called. ATM, just make sure it is in order. void terminalWebApiSetup() { #if API_SUPPORT apiRegister(getSetting("termWebApiPath", TERMINAL_WEB_API_PATH), [](ApiRequest& api) { api.handle([](AsyncWebServerRequest* request) { AsyncResponseStream *response = request->beginResponseStream("text/plain"); for (auto* name : _terminal.names()) { response->print(name); response->print("\r\n"); } request->send(response); }); return true; }, [](ApiRequest& api) { // TODO: since HTTP spec allows query string to contain repeating keys, allow iteration // over every received 'line' to provide a way to call multiple commands at once auto cmd = api.param(F("line")); if (!cmd.length()) { return false; } if (!cmd.endsWith("\r\n") && !cmd.endsWith("\n")) { cmd += '\n'; } api.handle([&](AsyncWebServerRequest* request) { AsyncWebPrint::scheduleFromRequest(request, [cmd](Print& print) { StreamAdapter stream(print, cmd.c_str(), cmd.c_str() + cmd.length() + 1); terminal::Terminal handler(stream); handler.processLine(); }); }); return true; } ); #else webRequestRegister([](AsyncWebServerRequest* request) { String path(F(API_BASE_PATH)); path += getSetting("termWebApiPath", TERMINAL_WEB_API_PATH); if (path != request->url()) { return false; } if (!apiAuthenticate(request)) { request->send(403); return true; } auto* cmd_param = request->getParam("line", (request->method() == HTTP_PUT)); if (!cmd_param) { request->send(500); return true; } auto cmd = cmd_param->value(); if (!cmd.length()) { request->send(500); return true; } if (!cmd.endsWith("\r\n") && !cmd.endsWith("\n")) { cmd += '\n'; } // TODO: batch requests? processLine() -> process(...) AsyncWebPrint::scheduleFromRequest(request, [cmd](Print& print) { StreamAdapter stream(print, cmd.c_str(), cmd.c_str() + cmd.length() + 1); terminal::Terminal handler(stream); handler.processLine(); }); return true; }); #endif // API_SUPPORT } #endif // TERMINAL_WEB_API_SUPPORT Stream & terminalDefaultStream() { return (Stream &) _io; } size_t terminalCapacity() { return _io.capacity(); } void terminalInject(void *data, size_t len) { _io.inject((char *) data, len); } void terminalInject(char ch) { _io.inject(ch); } void terminalRegisterCommand(const __FlashStringHelper* name, terminal::Terminal::CommandFunc func) { terminal::Terminal::addCommand(name, func); }; void terminalOK(Print& print) { print.print(F("+OK\n")); } void terminalError(Print& print, const String& error) { print.printf("-ERROR: %s\n", error.c_str()); } void terminalOK(const terminal::CommandContext& ctx) { terminalOK(ctx.output); } void terminalError(const terminal::CommandContext& ctx, const String& error) { terminalError(ctx.output, error); } void terminalOK() { terminalOK(_io); } void terminalError(const String& error) { terminalError(_io, error); } void terminalSetup() { // Show DEBUG panel with input #if WEB_SUPPORT wsRegister() .onVisible([](JsonObject& root) { root["cmdVisible"] = 1; }); #endif // Similar to the above, but we allow only very small and in-place outputs. #if MQTT_SUPPORT && TERMINAL_MQTT_SUPPORT _terminalMqttSetup(); #endif // Initialize default commands _terminalInitCommands(); #if SERIAL_RX_ENABLED SERIAL_RX_PORT.begin(SERIAL_RX_BAUDRATE); #endif // SERIAL_RX_ENABLED // Register loop espurnaRegisterLoop(_terminalLoop); } #endif // TERMINAL_SUPPORT