From fdbc1f05c4c3c785c62280140add6a4ae1faf00e Mon Sep 17 00:00:00 2001 From: Mike Date: Fri, 22 Mar 2024 07:48:48 +0000 Subject: [PATCH] Provide consistent serial console interface for samples (#2739) This PR provides a consistent mechanism for a basic serial console interface using the `LineBuffer` class. **Refactor LineBuffer** Implement LineBufferBase so we take non-trivial methods out of header. **Add `bool`, `String` operators, `printTo` method&** Simplify usage **Add `process`, `processKey` methods and update samples** Use LineBuffer for LiveDebug, Basic_Ota and Basic_FlashIP samples. These new methods are used to simplify application code. Note that the `CommandProcessing::Handler` class hasn't been updated here, mainly because it requires customisation of EOL handling (setCommandEOL, getCommandEOL). Presumably this is because of the variation in EOL sequences (CR, LF, CRLF or LFCR combinations). The LineBuffer `processKey` method doesn't care, it handles all of these. --- Sming/Core/Data/Buffer/LineBuffer.cpp | 107 +++++++++++ Sming/Core/Data/Buffer/LineBuffer.h | 104 +++++++---- .../src/CommandProcessing/Handler.cpp | 3 +- Sming/Libraries/FlashIP | 2 +- samples/Basic_Ota/app/application.cpp | 170 +++++++++++------- samples/LiveDebug/app/application.cpp | 44 ++--- 6 files changed, 299 insertions(+), 131 deletions(-) create mode 100644 Sming/Core/Data/Buffer/LineBuffer.cpp diff --git a/Sming/Core/Data/Buffer/LineBuffer.cpp b/Sming/Core/Data/Buffer/LineBuffer.cpp new file mode 100644 index 0000000000..2f4d14ab75 --- /dev/null +++ b/Sming/Core/Data/Buffer/LineBuffer.cpp @@ -0,0 +1,107 @@ +/**** + * Sming Framework Project - Open Source framework for high efficiency native ESP8266 development. + * Created 2015 by Skurydin Alexey + * http://github.com/SmingHub/Sming + * All files of the Sming Core are provided under the LGPL v3 license. + * + * LineBuffer.h - support for buffering/editing a line of text + * + * author mikee47 Feb 2019 + * + ****/ + +#include "LineBuffer.h" + +LineBufferBase::Action LineBufferBase::process(Stream& input, ReadWriteStream& output) +{ + int c; + while((c = input.read()) >= 0) { + auto action = processKey(c, &output); + if(action == Action::clear || action == Action::submit) { + return action; + } + } + return Action::none; +} + +LineBufferBase::Action LineBufferBase::processKey(char key, ReadWriteStream* output) +{ + auto prevKey = previousKey; + previousKey = key; + + switch(key) { + case '\x1b': // ESC -> delete current commandLine + clear(); + if(output) { + output->println(); + } + return Action::clear; + + case '\b': // delete (backspace) + case '\x7f': // xterm ctrl-? + if(!backspace()) { + return Action::none; + } + if(output) { + output->print("\b \b"); + } + return Action::backspace; + + case '\r': + case '\n': + // For "\r\n" or "\n\r" sequence ignore second key + if(prevKey != key && (prevKey == '\r' || prevKey == '\n')) { + previousKey = '\0'; + return Action::none; + } + if(output) { + output->println(); + } + return Action::submit; + + default: + if(!addChar(key)) { + return Action::none; + } + if(output) { + output->print(key); + } + return Action::echo; + } +} + +char LineBufferBase::addChar(char c) +{ + if(c == '\n' || c == '\r') { + return '\n'; + } + + if(c >= 0x20 && c < 0x7f && length < (size - 1)) { + buffer[length++] = c; + buffer[length] = '\0'; + return c; + } + + return '\0'; +} + +bool LineBufferBase::backspace() +{ + if(length == 0) { + return false; + } + --length; + buffer[length] = '\0'; + return true; +} + +bool LineBufferBase::startsWith(const char* text) const +{ + auto len = strlen(text); + return memcmp(buffer, text, len) == 0; +} + +bool LineBufferBase::contains(const char* text) const +{ + return strstr(buffer, text) != nullptr; +} diff --git a/Sming/Core/Data/Buffer/LineBuffer.h b/Sming/Core/Data/Buffer/LineBuffer.h index e23186b483..0295edc41f 100644 --- a/Sming/Core/Data/Buffer/LineBuffer.h +++ b/Sming/Core/Data/Buffer/LineBuffer.h @@ -14,14 +14,47 @@ #include #include +#include +#include /** * @brief Class to enable buffering of a single line of text, with simple editing * @note We define this as a template class for simplicity, no need for separate buffer memory management */ -template class LineBuffer +class LineBufferBase { public: + LineBufferBase(char* buffer, uint16_t size) : buffer(buffer), size(size) + { + } + + /** + * @brief Returned from `processKey` method directing caller + */ + enum class Action { + none, ///< Do nothing, ignore the key + clear, ///< Line is cleared: typically perform a carriage return + echo, ///< Key should be echoed + backspace, ///< Perform backspace edit, e.g. output "\b \b" + submit, ///< User hit return, process line and clear it + }; + + /** + * @brief Process all available data from `input` + * @param input Source of keystrokes + * @param output The output stream (e.g. Serial) for echoing + * @retval Action: none, clear or submit + */ + Action process(Stream& input, ReadWriteStream& output); + + /** + * @brief Process a keypress in a consistent manner for console editing + * @param key The keypress value + * @param output The output stream (e.g. Serial) for echoing, if required + * @retval Action + */ + Action processKey(char key, ReadWriteStream* output = nullptr); + /** * @brief Add a character to the buffer * @retval char Character added to buffer, '\0' if ignored, '\n' if line is complete @@ -36,6 +69,20 @@ template class LineBuffer length = 0; } + explicit operator bool() const + { + return length != 0; + } + + /** + * @brief Copy buffer contents into a String + * @retval String + */ + explicit operator String() const + { + return length ? String(buffer, length) : nullptr; + } + /** * @brief Get the text, nul-terminated */ @@ -72,44 +119,29 @@ template class LineBuffer */ bool backspace(); -private: - char buffer[BUFSIZE] = {'\0'}; ///< The text buffer - uint16_t length = 0; ///< Number of characters stored -}; - -template char LineBuffer::addChar(char c) -{ - if(c == '\n' || c == '\r') { - return '\n'; - } - - if(c >= 0x20 && c < 0x7f && length < (BUFSIZE - 1)) { - buffer[length++] = c; - buffer[length] = '\0'; - return c; + size_t printTo(Print& p) const + { + return p.write(buffer, length); } - return '\0'; -} +private: + char* buffer; + uint16_t size; + uint16_t length{0}; ///< Number of characters stored + char previousKey{'\0'}; ///< For processing CR/LF +}; -template bool LineBuffer::backspace() +/** + * @brief Class to enable buffering of a single line of text, with simple editing + * @note We define this as a template class for simplicity, no need for separate buffer memory management + */ +template class LineBuffer : public LineBufferBase { - if(length == 0) { - return false; - } else { - --length; - buffer[length] = '\0'; - return true; +public: + LineBuffer() : LineBufferBase(buffer, BUFSIZE) + { } -} -template bool LineBuffer::startsWith(const char* text) const -{ - auto len = strlen(text); - return memcmp(buffer, text, len) == 0; -} - -template bool LineBuffer::contains(const char* text) const -{ - return strstr(buffer, text) != nullptr; -} +private: + char buffer[BUFSIZE]{}; +}; diff --git a/Sming/Libraries/CommandProcessing/src/CommandProcessing/Handler.cpp b/Sming/Libraries/CommandProcessing/src/CommandProcessing/Handler.cpp index 03bec19a4e..60ec754bfd 100644 --- a/Sming/Libraries/CommandProcessing/src/CommandProcessing/Handler.cpp +++ b/Sming/Libraries/CommandProcessing/src/CommandProcessing/Handler.cpp @@ -29,9 +29,8 @@ size_t Handler::process(char recvChar) output.print(getCommandPrompt()); } } else if(recvChar == getCommandEOL()) { - String command(commandBuf.getBuffer(), commandBuf.getLength()); + processCommandLine(String(commandBuf)); commandBuf.clear(); - processCommandLine(command); } else if(recvChar == '\b' || recvChar == 0x7f) { if(commandBuf.backspace()) { output.print(_F("\b \b")); diff --git a/Sming/Libraries/FlashIP b/Sming/Libraries/FlashIP index 5f6d552d7d..820d08aab8 160000 --- a/Sming/Libraries/FlashIP +++ b/Sming/Libraries/FlashIP @@ -1 +1 @@ -Subproject commit 5f6d552d7d18f80bbbce7cd891fdce9026802953 +Subproject commit 820d08aab817dde262d5d5f9fb453dca88b374e9 diff --git a/samples/Basic_Ota/app/application.cpp b/samples/Basic_Ota/app/application.cpp index f9aa5f7153..51770d421c 100644 --- a/samples/Basic_Ota/app/application.cpp +++ b/samples/Basic_Ota/app/application.cpp @@ -3,6 +3,7 @@ #include #include #include +#include // If you want, you can define WiFi settings globally in Eclipse Environment Variables #ifndef WIFI_SSID @@ -10,9 +11,12 @@ #define WIFI_PWD "PleaseEnterPass" #endif +namespace +{ std::unique_ptr otaUpdater; Storage::Partition spiffsPartition; OtaUpgrader ota; +LineBuffer<16> commandBuffer; Storage::Partition findSpiffsPartition(Storage::Partition appPart) { @@ -122,76 +126,114 @@ void showInfo() << " @ 0x" << String(after.address(), HEX) << endl; } -void serialCallBack(Stream& stream, char arrivedChar, unsigned short availableCharsCount) +void showPrompt() +{ + Serial << _F("OTA> ") << commandBuffer; +} + +void handleCommand(const String& str) { - int pos = stream.indexOf('\n'); - if(pos > -1) { - char str[pos + 1]; - for(int i = 0; i < pos + 1; i++) { - str[i] = stream.read(); - if(str[i] == '\r' || str[i] == '\n') { - str[i] = '\0'; + if(F("connect") == str) { + Serial << _F("Connecting to '") << WIFI_SSID << "'..." << endl; + WifiStation.config(WIFI_SSID, WIFI_PWD); + WifiStation.enable(true); + WifiStation.connect(); + return; + } + + if(F("ip") == str) { + Serial << "ip: " << WifiStation.getIP() << ", mac: " << WifiStation.getMacAddress() << endl; + return; + } + + if(F("ota") == str) { + doUpgrade(); + return; + } + + if(F("switch") == str) { + doSwitch(); + return; + } + + if(F("restart") == str) { + System.restart(); + return; + } + + if(F("ls") == str) { + Directory dir; + if(dir.open()) { + while(dir.next()) { + Serial << " " << dir.stat().name << endl; } } + Serial << _F("filecount ") << dir.count() << endl; + return; + } - if(F("connect") == str) { - // connect to wifi - WifiStation.config(WIFI_SSID, WIFI_PWD); - WifiStation.enable(true); - WifiStation.connect(); - } else if(F("ip") == str) { - Serial << "ip: " << WifiStation.getIP() << ", mac: " << WifiStation.getMacAddress() << endl; - } else if(F("ota") == str) { - doUpgrade(); - } else if(F("switch") == str) { - doSwitch(); - } else if(F("restart") == str) { - System.restart(); - } else if(F("ls") == str) { - Directory dir; - if(dir.open()) { - while(dir.next()) { - Serial << " " << dir.stat().name << endl; - } - } - Serial << _F("filecount ") << dir.count() << endl; - } else if(F("cat") == str) { - Directory dir; - if(dir.open() && dir.next()) { - auto filename = dir.stat().name.c_str(); - Serial << "dumping file " << filename << ": " << endl; - // We don't know how big the is, so streaming it is safest - FileStream fs; - fs.open(filename); - Serial.copyFrom(&fs); - Serial.println(); - } else { - Serial.println(F("Empty spiffs!")); - } - } else if(F("info") == str) { - showInfo(); - } else if(F("help") == str) { - Serial.print(_F("\r\n" - "available commands:\r\n" - " help - display this message\r\n" - " ip - show current ip address\r\n" - " connect - connect to wifi\r\n" - " restart - restart the device\r\n" - " switch - switch to the other rom and reboot\r\n" - " ota - perform ota update, switch rom and reboot\r\n" - " info - show device info\r\n")); - - if(spiffsPartition) { - Serial.print(_F(" ls - list files in spiffs\r\n" - " cat - show first file in spiffs\r\n")); - } + if(F("cat") == str) { + Directory dir; + if(dir.open() && dir.next()) { + auto filename = dir.stat().name.c_str(); + Serial << "dumping file " << filename << ": " << endl; + // We don't know how big the is, so streaming it is safest + FileStream fs; + fs.open(filename); + Serial.copyFrom(&fs); Serial.println(); } else { - Serial.println("unknown command"); + Serial.println(F("Empty spiffs!")); + } + return; + } + + if(F("info") == str) { + showInfo(); + return; + } + + if(F("help") == str) { + Serial.print(_F("\r\n" + "available commands:\r\n" + " help - display this message\r\n" + " ip - show current ip address\r\n" + " connect - connect to wifi\r\n" + " restart - restart the device\r\n" + " switch - switch to the other rom and reboot\r\n" + " ota - perform ota update, switch rom and reboot\r\n" + " info - show device info\r\n")); + + if(spiffsPartition) { + Serial.print(_F(" ls - list files in spiffs\r\n" + " cat - show first file in spiffs\r\n")); } + Serial.println(); + return; } + + Serial << _F("unknown command: ") << str << endl; } +void serialCallBack(Stream& stream, char arrivedChar, unsigned short availableCharsCount) +{ + switch(commandBuffer.process(stream, Serial)) { + case commandBuffer.Action::submit: + if(commandBuffer) { + handleCommand(String(commandBuffer)); + commandBuffer.clear(); + } + showPrompt(); + break; + case commandBuffer.Action::clear: + showPrompt(); + break; + default:; + } +} + +} // namespace + void init() { Serial.begin(SERIAL_BAUD_RATE); // 115200 by default @@ -207,10 +249,14 @@ void init() } WifiAccessPoint.enable(false); + WifiEvents.onStationGotIP([](IpAddress ip, IpAddress netmask, IpAddress gateway) { showPrompt(); }); - Serial << _F("\r\nCurrently running ") << partition.name() << " @ 0x" << String(partition.address(), HEX) << '.' + Serial << endl + << _F("Currently running ") << partition.name() << " @ 0x" << String(partition.address(), HEX) << '.' << endl + << _F("Type 'help' and press enter for instructions.") << endl << endl; - Serial << _F("Type 'help' and press enter for instructions.") << endl << endl; + + showPrompt(); Serial.onDataReceived(serialCallBack); } diff --git a/samples/LiveDebug/app/application.cpp b/samples/LiveDebug/app/application.cpp index 6b7a5bd780..0adca0af28 100644 --- a/samples/LiveDebug/app/application.cpp +++ b/samples/LiveDebug/app/application.cpp @@ -96,9 +96,7 @@ void showPrompt() void onDataReceived(Stream& source, char arrivedChar, unsigned short availableCharsCount) { - static unsigned commandLength; - const unsigned MAX_COMMAND_LENGTH = 16; - static char commandBuffer[MAX_COMMAND_LENGTH + 1]; + static LineBuffer commandBuffer; // Error detection unsigned status = Serial.getStatus(); @@ -118,38 +116,24 @@ void onDataReceived(Stream& source, char arrivedChar, unsigned short availableCh } // Discard what is likely to be garbage Serial.clear(SERIAL_RX_ONLY); - commandLength = 0; + commandBuffer.clear(); showPrompt(); return; } - int c; - while((c = Serial.read()) >= 0) { - switch(c) { - case '\b': // delete (backspace) - case 0x7f: // xterm ctrl-? - if(commandLength > 0) { - --commandLength; - Serial.print(_F("\b \b")); - } - break; - case '\r': - case '\n': - if(commandLength > 0) { - Serial.println(); - String cmd(commandBuffer, commandLength); - commandLength = 0; - Serial.clear(SERIAL_RX_ONLY); - handleCommand(cmd); - } - showPrompt(); - break; - default: - if(c >= 0x20 && c <= 0x7f && commandLength < MAX_COMMAND_LENGTH) { - commandBuffer[commandLength++] = c; - Serial.print(char(c)); - } + switch(commandBuffer.process(source, Serial)) { + case commandBuffer.Action::clear: + showPrompt(); + break; + case commandBuffer.Action::submit: { + if(commandBuffer) { + handleCommand(String(commandBuffer)); + commandBuffer.clear(); } + showPrompt(); + } + default: + break; } }