├── .gitattributes ├── doc ├── AT commands spec.docx ├── T-REC-V.250-200307.pdf └── Arduino-library-indexer-log.url ├── .gitignore ├── library.properties ├── src ├── NuSerial.cpp ├── NuShellCommands.cpp ├── NuShellCommands.hpp ├── NuATCommands.cpp ├── NuATCommandsLegacy2.cpp ├── NuPacket.cpp ├── NuSerial.hpp ├── NuATCommands.hpp ├── NuATCommandsLegacy2.hpp ├── NuPacket.hpp ├── NuStream.hpp ├── cyan_semaphore.h ├── NuStream.cpp ├── NuCLIParser.hpp ├── NuCLIParser.cpp ├── NuS.hpp ├── NuS.cpp ├── NuATCommandParserLegacy2.hpp ├── NuATParser.hpp └── NuATCommandParserLegacy2.cpp ├── extras ├── test │ ├── Issue14 │ │ └── Issue14.ino │ ├── StartStopTest │ │ └── StartStopTest.ino │ ├── HandshakeTest │ │ └── HandshakeTest.ino │ ├── Issue8 │ │ └── Issue8.ino │ ├── SimpleCommandTester │ │ └── SimpleCommandTester.ino │ ├── ATCommandsTesterLegacy2 │ │ └── ATCommandsTesterLegacy2.ino │ └── ATCommandsTester │ │ └── ATCommandsTester.ino └── BatchCompile.ps1 ├── keywords.txt ├── .github └── workflows │ └── APIdoc.yaml ├── examples ├── NuSerialDump │ └── NuSerialDump.ino ├── ReadBytesDemo │ └── ReadBytesDemo.ino ├── NuSEcho │ └── NuSEcho.ino ├── UartBleAdapter │ └── UartBleAdapter.ino ├── README.md ├── ShellCommandDemo │ └── ShellCommandDemo.ino ├── ATCommandDemo │ └── ATCommandDemo.ino ├── CustomCommandProcessor │ └── CustomCommandProcessor.ino └── ATCommandDemoLegacy2 │ └── ATCommandDemoLegacy2.ino └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /doc/AT commands spec.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afpineda/NuS-NimBLE-Serial/HEAD/doc/AT commands spec.docx -------------------------------------------------------------------------------- /doc/T-REC-V.250-200307.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afpineda/NuS-NimBLE-Serial/HEAD/doc/T-REC-V.250-200307.pdf -------------------------------------------------------------------------------- /doc/Arduino-library-indexer-log.url: -------------------------------------------------------------------------------- 1 | [InternetShortcut] 2 | URL=https://downloads.arduino.cc/libraries/logs/github.com/afpineda/NuS-NimBLE-Serial/ 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | 34 | #Arduino 35 | *.svd 36 | debug.cfg 37 | debug_custom.json 38 | .development 39 | 40 | #other 41 | .vscode 42 | .~* 43 | 44 | # Doxygen 45 | doc/html/ -------------------------------------------------------------------------------- /library.properties: -------------------------------------------------------------------------------- 1 | name=NuS-NimBLE-Serial 2 | version=4.2.1 3 | author=afpineda 4 | maintainer=afpineda <74291754+afpineda@users.noreply.github.com> 5 | sentence=Nordic UART Service (NuS) and BLE serial communications 6 | paragraph=Serial read and write, both with blocking and non-blocking semantics, through BLE (not to be confused with Bluetooth classic). Customizable AT/shell command processors available. Support for custom protocols. Can coexist with other services. 7 | url=https://github.com/afpineda/Nus-NimBLE-Serial 8 | category=Communication 9 | architectures=esp32,arm-ble 10 | depends=NimBLE-Arduino (>=2.1.0 && <3.0.0) -------------------------------------------------------------------------------- /src/NuSerial.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file NuSerial.cpp 3 | * @author Ángel Fernández Pineda. Madrid. Spain. 4 | * @date 2023-12-24 5 | * @brief Communications stream based on the Nordic UART Service 6 | * with non-blocking Arduino semantics 7 | * 8 | * @copyright Creative Commons Attribution 4.0 International (CC BY 4.0) 9 | * 10 | */ 11 | 12 | #include "NuSerial.hpp" 13 | 14 | //----------------------------------------------------------------------------- 15 | // Globals 16 | //----------------------------------------------------------------------------- 17 | 18 | NordicUARTSerial &NuSerial = NordicUARTSerial::getInstance(); -------------------------------------------------------------------------------- /src/NuShellCommands.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file NuShellCommands.cpp 3 | * @author Ángel Fernández Pineda. Madrid. Spain. 4 | * @date 2023-12-27 5 | * @brief Shell command processor using the Nordic UART Service 6 | * 7 | * @copyright Creative Commons Attribution 4.0 International (CC BY 4.0) 8 | * 9 | */ 10 | 11 | #include "NuShellCommands.hpp" 12 | 13 | //----------------------------------------------------------------------------- 14 | // Globals 15 | //----------------------------------------------------------------------------- 16 | 17 | NuShellCommandProcessor &NuShellCommands = NuShellCommandProcessor::getInstance(); 18 | 19 | //----------------------------------------------------------------------------- 20 | // NordicUARTService implementation 21 | //----------------------------------------------------------------------------- 22 | 23 | void NuShellCommandProcessor::onWrite( 24 | NimBLECharacteristic *pCharacteristic, 25 | NimBLEConnInfo &connInfo) 26 | { 27 | // Incoming data 28 | NimBLEAttValue incomingPacket = pCharacteristic->getValue(); 29 | 30 | // Parse and execute 31 | execute((const uint8_t *)incomingPacket.data(), incomingPacket.size()); 32 | } 33 | -------------------------------------------------------------------------------- /src/NuShellCommands.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file NuShellCommands.hpp 3 | * @author Ángel Fernández Pineda. Madrid. Spain. 4 | * @date 2023-12-27 5 | * @brief Shell command processor using the Nordic UART Service 6 | * 7 | * @copyright Creative Commons Attribution 4.0 International (CC BY 4.0) 8 | * 9 | */ 10 | #ifndef __NU_SHELL_COMMANDS_HPP__ 11 | #define __NU_SHELL_COMMANDS_HPP__ 12 | 13 | #include "NuS.hpp" 14 | #include "NuCLIParser.hpp" 15 | 16 | /** 17 | * @brief Execute shell commands received thanks to the Nordic UART Service 18 | * 19 | */ 20 | class NuShellCommandProcessor : public NordicUARTService, public NuCLIParser 21 | { 22 | public: 23 | // Singleton pattern 24 | 25 | NuShellCommandProcessor(const NuShellCommandProcessor &) = delete; 26 | void operator=(NuShellCommandProcessor const &) = delete; 27 | 28 | /** 29 | * @brief Get the instance of the NuShellCommandProcessor 30 | * 31 | * @note No need to use. Use `NuShellCommands` instead. 32 | * 33 | * @return NuShellCommandProcessor& 34 | */ 35 | static NuShellCommandProcessor &getInstance() 36 | { 37 | static NuShellCommandProcessor instance; 38 | return instance; 39 | }; 40 | 41 | protected: 42 | // Overriden Methods 43 | virtual void onWrite( 44 | NimBLECharacteristic *pCharacteristic, 45 | NimBLEConnInfo &connInfo) override; 46 | 47 | private: 48 | NuShellCommandProcessor(){}; 49 | }; 50 | 51 | /** 52 | * @brief Singleton instance of the NuShellCommandProcessor class 53 | * 54 | */ 55 | extern NuShellCommandProcessor &NuShellCommands; 56 | 57 | #endif -------------------------------------------------------------------------------- /extras/test/Issue14/Issue14.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Issue14.ino 3 | * @author Ángel Fernández Pineda. Madrid. Spain. 4 | * @author gary7530 (https://github.com/gary7530). 5 | * 6 | * @date 2025-09-23 7 | * 8 | * @brief Regression test for issue #14 9 | * 10 | * @see https://github.com/afpineda/NuS-NimBLE-Serial/issues/14 11 | * 12 | * @copyright Creative Commons Attribution 4.0 International (CC BY 4.0) 13 | * 14 | */ 15 | 16 | #include 17 | #include 18 | 19 | #define DEVICE_NAME "NusIssue14" 20 | 21 | char nmeabuff[4096] = { 22 | "$GNGSA,A,1,05,07,13,14,15,21,22,30,,,,,1.4,0.7,1.2,1*33\n" 23 | "$GNGSA,A,1,04,10,11,12,19,21,27,29,,,,,1.4,0.7,1.2,3*33\n" 24 | "$GNGSA,A,1,14,21,24,26,,,,,,,,,1.4,0.7,1.2,4*33\n" 25 | "$GPGSV,3,1,12,05,68,268,47,07,37,057,45,08,01,050,35,09,04,097,26,0*6D\n" 26 | "$GPGSV,3,2,12,11,08,206,39,13,53,285,47,14,33,140,43,15,21,288,44,0*6B\n" 27 | "$GPGSV,3,3,12,18,12,326,38,21,61,176,46,22,21,153,41,30,68,071,47,0*6F\n" 28 | "$GAGSV,3,1,11,04,27,277,41,06,03,273,35,10,36,274,44,11,28,306,40,0*7B\n" 29 | "$GAGSV,3,2,11,12,33,245,40,19,80,343,41,21,30,133,41,27,33,070,42,0*70\n" 30 | "$GAGSV,3,3,11,29,42,083,43,30,07,026,31,33,07,196,38,,,,,0*45\n" 31 | "$GBGSV,2,1,07,09,14,049,34,10,04,080,30,14,49,303,46,21,46,119,46,0*7B\n" 32 | "$GBGSV,2,2,07,24,19,198,40,26,59,159,47,28,07,306,36,,,,,0*41\n"}; 33 | 34 | void setup() 35 | { 36 | NimBLEDevice::init(DEVICE_NAME); 37 | NimBLEDevice::getAdvertising()->setName(DEVICE_NAME); 38 | NuSerial.begin(115200); 39 | } 40 | 41 | void loop() 42 | { 43 | NuSerial.printf("%s", nmeabuff); 44 | delay(5000); 45 | } -------------------------------------------------------------------------------- /extras/test/StartStopTest/StartStopTest.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * @file StartStopTest.ino 3 | * @author Ángel Fernández Pineda. Madrid. Spain. 4 | * @date 2025-05-27 5 | * 6 | * @brief Test multiple service start / stop events 7 | * 8 | * @copyright Creative Commons Attribution 4.0 International (CC BY 4.0) 9 | * 10 | */ 11 | 12 | #include 13 | #include "NuPacket.hpp" 14 | #include "NimBLEDevice.h" 15 | 16 | #define DEVICE_NAME "Start Stop Test" 17 | 18 | void setup() 19 | { 20 | // Initialize serial monitor 21 | Serial.begin(115200); 22 | Serial.println("*****************************"); 23 | Serial.println(" Start / Stop test "); 24 | Serial.println("*****************************"); 25 | Serial.println("--Ready--"); 26 | } 27 | 28 | void loop() 29 | { 30 | // Print service state 31 | Serial.println(); 32 | if (NuPacket.isStarted()) 33 | Serial.println("NuS is started"); 34 | else 35 | Serial.println("NuS is stopped"); 36 | Serial.println("Enter any character to change the service state..."); 37 | 38 | // Wait for serial input 39 | while (!Serial.available()) 40 | ; 41 | // Remove serial input 42 | while (Serial.available()) 43 | Serial.read(); 44 | 45 | // Change service state 46 | if (NuPacket.isStarted()) 47 | { 48 | Serial.println("Stoping NuS..."); 49 | NuPacket.stop(); 50 | Serial.println("Deinitializing BLE..."); 51 | NimBLEDevice::deinit(true); 52 | } else 53 | { 54 | Serial.println("Initializing BLE..."); 55 | NimBLEDevice::init(DEVICE_NAME); 56 | NimBLEDevice::getAdvertising()->setName(DEVICE_NAME); 57 | Serial.println("Starting Nus..."); 58 | NuPacket.start(); 59 | } 60 | } -------------------------------------------------------------------------------- /extras/test/HandshakeTest/HandshakeTest.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * @file HandshakeTest.ino 3 | * @author Ángel Fernández Pineda. Madrid. Spain. 4 | * 5 | * @date 2024-11-27 6 | * 7 | * @brief Proof of concept. Send a handshake message on connection. 8 | * 9 | * @see https://github.com/afpineda/NuS-NimBLE-Serial/issues/8 10 | * 11 | * @copyright Creative Commons Attribution 4.0 International (CC BY 4.0) 12 | * 13 | */ 14 | 15 | #include "NuSerial.hpp" 16 | #include 17 | 18 | //----------------------------------------------------------------------------- 19 | // Entry point 20 | //----------------------------------------------------------------------------- 21 | 22 | void setup() 23 | { 24 | Serial.begin(115200); 25 | Serial.println("--READY--"); 26 | 27 | NimBLEDevice::init("Handshake"); 28 | NuSerial.begin(115200); // Note: Parameter is ignored. 29 | 30 | Serial.println("--GO--"); 31 | } 32 | 33 | // unsigned long int count = 0L; 34 | bool lastConnectionStatus = false; 35 | 36 | void loop() 37 | { 38 | if (!lastConnectionStatus && NuSerial.isConnected()) 39 | { 40 | Serial.println("Device connected!"); 41 | lastConnectionStatus = true; 42 | NuSerial.printf("Hello \n"); 43 | } 44 | else if (lastConnectionStatus && !NuSerial.isConnected()) 45 | { 46 | Serial.println("Device disconnected!"); 47 | lastConnectionStatus = false; 48 | } 49 | // if (lastConnectionStatus) 50 | // { 51 | // // Do stuff 52 | // Serial.println("I'm busy"); 53 | // NuSerial.printf("This is message number %ld.\n", count++); 54 | // delay(5000); 55 | // } 56 | // else 57 | // { 58 | // // Do background stuff 59 | // Serial.println("I'm bored"); 60 | // delay(5000); 61 | // } 62 | } 63 | -------------------------------------------------------------------------------- /extras/test/Issue8/Issue8.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Issue8.ino 3 | * @author Ángel Fernández Pineda. Madrid. Spain. 4 | * @author cmumme (https://github.com/cmumme). 5 | * 6 | * @date 2024-11-27 7 | * 8 | * @brief Regression test for issue #8 9 | * 10 | * @see https://github.com/afpineda/NuS-NimBLE-Serial/issues/8 11 | * 12 | * @copyright Creative Commons Attribution 4.0 International (CC BY 4.0) 13 | * 14 | */ 15 | 16 | #include "NuSerial.hpp" 17 | #include 18 | 19 | //----------------------------------------------------------------------------- 20 | // MOCK 21 | //----------------------------------------------------------------------------- 22 | 23 | class ServerCallbacks : public NimBLEServerCallbacks 24 | { 25 | void onConnect(NimBLEServer *pServer, ble_gap_conn_desc *connection) 26 | { 27 | Serial.println("Connected!!"); 28 | NuSerial.send("Hello\n"); 29 | NuSerial.println("Ready to receive"); // <- stack overflow occurs 30 | NuSerial.print("Hello, world!"); // <- stack overflow occurs 31 | NuSerial.write(0x4A); // J <- stack overflow occurs 32 | NuSerial.write(0x0A); // \n <- stack overflow occurs 33 | Serial.println("Handshake sent!"); 34 | }; 35 | void onDisconnect(NimBLEServer *pServer) 36 | { 37 | NuSerial.end(); 38 | }; 39 | }; 40 | 41 | //----------------------------------------------------------------------------- 42 | // Arduino entry points 43 | //----------------------------------------------------------------------------- 44 | 45 | void setup() 46 | { 47 | Serial.begin(115200); 48 | Serial.println("--READY--"); 49 | NimBLEDevice::init("Issue8"); 50 | 51 | NuSerial.start(); 52 | NimBLEDevice::getServer()->setCallbacks(new ServerCallbacks()); 53 | Serial.println("--GO--"); 54 | } 55 | 56 | void loop() 57 | { 58 | delay(30000); 59 | Serial.println("--Heartbeat--"); 60 | NuSerial.println("--I am alive--"); 61 | } -------------------------------------------------------------------------------- /keywords.txt: -------------------------------------------------------------------------------- 1 | ############################################ 2 | # Syntax Coloring Map For NuS-NimBLE-Serial 3 | ############################################ 4 | 5 | ############################################ 6 | # Data types (KEYWORD1) 7 | ############################################ 8 | 9 | NordicUARTPacket KEYWORD1 10 | NordicUARTSerial KEYWORD1 11 | NordicUARTService KEYWORD1 12 | NuATCommandCallback_t KEYWORD1 13 | NuATCommandCallbacks KEYWORD1 14 | NuATCommandParameters_t KEYWORD1 15 | NuATCommandParser KEYWORD1 16 | NuATCommandProcessor KEYWORD1 17 | NuATCommandResult_t KEYWORD1 18 | NuATErrorCallback_t KEYWORD1 19 | NuATNotACommandLineCallback_t KEYWORD1 20 | NuATParser KEYWORD1 21 | NuATParsingResult_t KEYWORD1 22 | NuATSyntaxError_t KEYWORD1 23 | NuCLIParser KEYWORD1 24 | NuCLIParsingResult_t KEYWORD1 25 | NuCommandLine_t KEYWORD1 26 | NuShellCommandProcessor KEYWORD1 27 | 28 | ############################################ 29 | # Methods and Functions (KEYWORD2) 30 | ############################################ 31 | 32 | allowLowerCase KEYWORD2 33 | available KEYWORD2 34 | begin KEYWORD2 35 | connect KEYWORD2 36 | disconnect KEYWORD2 37 | end KEYWORD2 38 | execute KEYWORD2 39 | forceUpperCaseCommandName KEYWORD2 40 | isConnected KEYWORD2 41 | maxCommandLineLength KEYWORD2 42 | on KEYWORD2 43 | onError KEYWORD2 44 | onExecute KEYWORD2 45 | onNotACommandLine KEYWORD2 46 | onParseError KEYWORD2 47 | onQuery KEYWORD2 48 | onSet KEYWORD2 49 | onTest KEYWORD2 50 | onUnknown KEYWORD2 51 | peek KEYWORD2 52 | print KEYWORD2 53 | printATResponse KEYWORD2 54 | printf KEYWORD2 55 | read KEYWORD2 56 | readBytes KEYWORD2 57 | send KEYWORD2 58 | setATCallbacks KEYWORD2 59 | setBufferSize KEYWORD2 60 | setCallbacks KEYWORD2 61 | setShellCommandCallbacks KEYWORD2 62 | start KEYWORD2 63 | stopOnFirstFailure KEYWORD2 64 | write KEYWORD2 65 | 66 | ############################################ 67 | # Constants (LITERAL1) 68 | ############################################ 69 | 70 | NuSerial LITERAL1 71 | NuPacket LITERAL1 72 | NuATCommands LITERAL1 73 | NuShellCommands LITERAL1 74 | -------------------------------------------------------------------------------- /src/NuATCommands.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file NuATCommands.cpp 3 | * @author Ángel Fernández Pineda. Madrid. Spain. 4 | * @date 2024-08-21 5 | * @brief AT command processor using the Nordic UART Service 6 | * 7 | * @copyright Creative Commons Attribution 4.0 International (CC BY 4.0) 8 | * 9 | */ 10 | 11 | #include "NuATCommands.hpp" 12 | 13 | //----------------------------------------------------------------------------- 14 | // Globals 15 | //----------------------------------------------------------------------------- 16 | 17 | NuATCommandProcessor &NuATCommands = NuATCommandProcessor::getInstance(); 18 | 19 | //----------------------------------------------------------------------------- 20 | // NordicUARTService implementation 21 | //----------------------------------------------------------------------------- 22 | 23 | void NuATCommandProcessor::onWrite( 24 | NimBLECharacteristic *pCharacteristic, 25 | NimBLEConnInfo &connInfo) 26 | { 27 | // Incoming data 28 | NimBLEAttValue incomingPacket = pCharacteristic->getValue(); 29 | const char *in = incomingPacket.c_str(); 30 | if ((uMaxCommandLineLength > 0) && 31 | (incomingPacket.size() > uMaxCommandLineLength)) 32 | { 33 | printResultResponse(NuATCommandResult_t::AT_RESULT_ERROR); 34 | notifyError("", NuATSyntaxError_t::AT_ERR_TOO_LONG); 35 | } 36 | else 37 | execute((const uint8_t *)in, incomingPacket.size()); 38 | } 39 | 40 | //----------------------------------------------------------------------------- 41 | // Printing 42 | //----------------------------------------------------------------------------- 43 | 44 | void NuATCommandProcessor::printATResponse(::std::string message) 45 | { 46 | print("\r\n"); 47 | print(message); 48 | print("\r\n"); 49 | } 50 | 51 | //----------------------------------------------------------------------------- 52 | // Other 53 | //----------------------------------------------------------------------------- 54 | 55 | uint32_t NuATCommandProcessor::maxCommandLineLength(uint32_t value) 56 | { 57 | uint32_t result = uMaxCommandLineLength; 58 | uMaxCommandLineLength = value; 59 | return result; 60 | } -------------------------------------------------------------------------------- /src/NuATCommandsLegacy2.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file NuATCommandsLegacy2.cpp 3 | * @author Ángel Fernández Pineda. Madrid. Spain. 4 | * @date 2023-12-18 5 | * @brief AT command processor using the Nordic UART Service 6 | * 7 | * @copyright Creative Commons Attribution 4.0 International (CC BY 4.0) 8 | * 9 | */ 10 | 11 | #include 12 | #include 13 | #include "NuATCommandsLegacy2.hpp" 14 | 15 | //----------------------------------------------------------------------------- 16 | // Globals 17 | //----------------------------------------------------------------------------- 18 | 19 | NuSLegacy2::NuATCommandProcessor &NuSLegacy2::NuATCommands = 20 | NuSLegacy2::NuATCommandProcessor::getInstance(); 21 | 22 | //----------------------------------------------------------------------------- 23 | // NordicUARTService implementation 24 | //----------------------------------------------------------------------------- 25 | 26 | void NuSLegacy2::NuATCommandProcessor::onWrite( 27 | NimBLECharacteristic *pCharacteristic, 28 | NimBLEConnInfo &connInfo) 29 | { 30 | // Incoming data 31 | NimBLEAttValue incomingPacket = pCharacteristic->getValue(); 32 | // const char *in = pCharacteristic->getValue().c_str(); 33 | const char *in = incomingPacket.c_str(); 34 | // Serial.printf("onWrite(): %s\n"); 35 | 36 | // Parse 37 | parseCommandLine(in); 38 | } 39 | 40 | //----------------------------------------------------------------------------- 41 | // Callbacks 42 | //----------------------------------------------------------------------------- 43 | 44 | void NuSLegacy2::NuATCommandProcessor::setATCallbacks(NuATCommandCallbacks *pCallbacks) 45 | { 46 | if (!isConnected()) 47 | NuATCommandParser::setATCallbacks(pCallbacks); 48 | else 49 | throw ::std::runtime_error("Unable to set AT command callbacks while connected"); 50 | } 51 | 52 | //----------------------------------------------------------------------------- 53 | // Printing 54 | //----------------------------------------------------------------------------- 55 | 56 | void NuSLegacy2::NuATCommandProcessor::printATResponse(const char message[]) 57 | { 58 | send("\r\n"); 59 | send(message); 60 | send("\r\n"); 61 | } 62 | -------------------------------------------------------------------------------- /src/NuPacket.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file NuPacket.cpp 3 | * @author Ángel Fernández Pineda. Madrid. Spain. 4 | * @date 2023-12-18 5 | * @brief Communications stream based on the Nordic UART Service 6 | * with blocking semantics 7 | * 8 | * @copyright Creative Commons Attribution 4.0 International (CC BY 4.0) 9 | * 10 | */ 11 | 12 | #include 13 | #include 14 | #include "NuPacket.hpp" 15 | 16 | //----------------------------------------------------------------------------- 17 | // Globals 18 | //----------------------------------------------------------------------------- 19 | 20 | NordicUARTPacket &NuPacket = NordicUARTPacket::getInstance(); 21 | 22 | //----------------------------------------------------------------------------- 23 | // Event callback 24 | //----------------------------------------------------------------------------- 25 | 26 | void NordicUARTPacket::onUnsubscribe(size_t subscriberCount) 27 | { 28 | if (subscriberCount == 0) 29 | { 30 | // Awake task at read() 31 | availableByteCount = 0; 32 | incomingBuffer = nullptr; 33 | dataAvailable.release(); 34 | } 35 | }; 36 | 37 | //----------------------------------------------------------------------------- 38 | // NordicUARTService implementation 39 | //----------------------------------------------------------------------------- 40 | 41 | void NordicUARTPacket::onWrite( 42 | NimBLECharacteristic *pCharacteristic, 43 | NimBLEConnInfo &connInfo) 44 | { 45 | // Wait for previous data to get consumed 46 | dataConsumed.acquire(); 47 | 48 | // Hold data until next read 49 | incomingPacket = pCharacteristic->getValue(); 50 | incomingBuffer = incomingPacket.data(); 51 | availableByteCount = incomingPacket.size(); 52 | 53 | // signal available data 54 | dataAvailable.release(); 55 | } 56 | 57 | //----------------------------------------------------------------------------- 58 | // Reading 59 | //----------------------------------------------------------------------------- 60 | 61 | const uint8_t *NordicUARTPacket::read(size_t &size) const noexcept 62 | { 63 | dataConsumed.release(); 64 | dataAvailable.acquire(); 65 | size = availableByteCount; 66 | return incomingBuffer; 67 | } 68 | -------------------------------------------------------------------------------- /.github/workflows/APIdoc.yaml: -------------------------------------------------------------------------------- 1 | name: Publish API documentation 2 | 3 | # This workflow is based on D'oxygen for static site generation. 4 | # However, there are many reusable GitHub Actions in the marketplace 5 | # for this purpose. I choose a random one. 6 | # Other workflow steps follow the official guide at the time of writing. 7 | 8 | on: 9 | # IMPORTANT NOTE: (Please, read) 10 | # API documentation is deployed to an automatically-generated environment 11 | # called "github-pages" (see the "deploy" job below). 12 | # This environment is protected by default in a way that only the 'main' 13 | # branch is able to be deployed. 14 | # Any trigger not matching the 'main' branch will work, but the workflow 15 | # will always fail for security reasons. 16 | # For example, the following trigger will not success... 17 | # 18 | # push: 19 | # tags: 20 | # - '[0-9]+.[0-9]+.[0-9]+' 21 | # 22 | push: 23 | branches: 24 | - 'main' 25 | paths: 26 | - 'src/*.hpp' 27 | workflow_dispatch: 28 | 29 | jobs: 30 | build: 31 | name: Generate static HTML files 32 | runs-on: 'ubuntu-latest' 33 | steps: 34 | 35 | - name: Checkout repository contents 36 | uses: actions/checkout@v4 37 | 38 | - name: Run D'oxygen 39 | uses: mattnotmitt/doxygen-action@v1.9.4 40 | with: 41 | working-directory: 'extras' 42 | doxyfile-path: 'Doxyfile' 43 | # NOTE: output directory is configured in Doxyfile. 44 | # Should point to: 45 | # '${{ github.workspace }}/doc' 46 | 47 | - name: Upload static files as artifact 48 | id: deployment 49 | uses: actions/upload-pages-artifact@v3 50 | with: 51 | path: '${{ github.workspace }}/doc/html/' 52 | 53 | deploy: 54 | name: Deploy to GitHub Pages 55 | needs: build 56 | permissions: 57 | pages: write # to deploy to Pages 58 | id-token: write # to verify the deployment originates from an appropriate source 59 | environment: 60 | name: github-pages 61 | url: ${{ steps.deployment.outputs.page_url }} 62 | runs-on: ubuntu-latest 63 | steps: 64 | - name: Deploy to GitHub Pages 65 | id: deployment 66 | uses: actions/deploy-pages@v4 -------------------------------------------------------------------------------- /src/NuSerial.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file NuSerial.hpp 3 | * @author Ángel Fernández Pineda. Madrid. Spain. 4 | * @date 2023-12-18 5 | * @brief Communications stream based on the Nordic UART Service 6 | * with non-blocking Arduino semantics 7 | * 8 | * @copyright Creative Commons Attribution 4.0 International (CC BY 4.0) 9 | * 10 | */ 11 | 12 | #ifndef __NUSERIAL_HPP__ 13 | #define __NUSERIAL_HPP__ 14 | 15 | #include 16 | #include "NuStream.hpp" 17 | 18 | /** 19 | * @brief Non-blocking serial communications through BLE and Nordic UART Service 20 | * 21 | */ 22 | class NordicUARTSerial : public NordicUARTStream 23 | { 24 | public: 25 | // Singleton pattern and Rule of Five 26 | 27 | NordicUARTSerial(const NordicUARTSerial &) = delete; 28 | NordicUARTSerial(NordicUARTSerial &&) = delete; 29 | NordicUARTSerial &operator=(const NordicUARTSerial &) = delete; 30 | NordicUARTSerial &operator=(NordicUARTSerial &&) = delete; 31 | 32 | /** 33 | * @brief Get the instance of the BLE stream 34 | * 35 | * @note No need to use. Use `NuSerial` instead. 36 | * 37 | * @return NordicUARTSerial& 38 | */ 39 | static NordicUARTSerial &getInstance() 40 | { 41 | static NordicUARTSerial instance; 42 | return instance; 43 | }; 44 | 45 | public: 46 | // Methods not strictly needed. Provided to mimic `Serial` 47 | 48 | /** 49 | * @brief Start the Nordic UART Service 50 | * 51 | * @param baud Ignored parameter 52 | * @param ... Ignored parameters 53 | */ 54 | void begin(unsigned long baud, ...) 55 | { 56 | start(); 57 | }; 58 | 59 | /** 60 | * @brief Start the Nordic UART Service 61 | * 62 | */ 63 | void begin() 64 | { 65 | start(); 66 | }; 67 | 68 | /** 69 | * @brief 70 | * 71 | * @param dummy 72 | */ 73 | void end(bool dummy = true) 74 | { 75 | disconnect(); 76 | }; 77 | 78 | private: 79 | // Singleton pattern 80 | NordicUARTSerial() : NordicUARTStream() {}; 81 | ~NordicUARTSerial() {}; 82 | }; 83 | 84 | /** 85 | * @brief Singleton instance of the NordicUARTSerial class 86 | * 87 | * @note Use this object as you do with Arduino's `Serial` 88 | */ 89 | extern NordicUARTSerial &NuSerial; 90 | 91 | #endif -------------------------------------------------------------------------------- /examples/NuSerialDump/NuSerialDump.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * @file NuSerialDump.ino 3 | * @author Ángel Fernández Pineda. Madrid. Spain. 4 | * @date 2023-12-18 5 | * 6 | * @brief Example of a blocking communications stream 7 | * based on the Nordic UART Service 8 | * 9 | * @note See examples/README.md for a description 10 | * 11 | * @copyright Creative Commons Attribution 4.0 International (CC BY 4.0) 12 | * 13 | */ 14 | 15 | #include 16 | #include 17 | #include "NuPacket.hpp" 18 | 19 | #define DEVICE_NAME "NuPacket demo" 20 | 21 | void setup() 22 | { 23 | // Initialize serial monitor 24 | Serial.begin(115200); 25 | Serial.println("*****************************"); 26 | Serial.println(" BLE serial dump demo"); 27 | Serial.println("*****************************"); 28 | Serial.println("--Initializing--"); 29 | 30 | // Initialize BLE stack and Nordic UART service 31 | NimBLEDevice::init(DEVICE_NAME); 32 | NimBLEDevice::getAdvertising()->setName(DEVICE_NAME); 33 | NuPacket.start(); 34 | 35 | // Initialization complete 36 | Serial.println("--Ready--"); 37 | } 38 | 39 | void loop() 40 | { 41 | Serial.println("--Waiting for connection--"); 42 | // Block current task until a connection is established. 43 | // This is not active waiting, so the CPU is free for other tasks. 44 | if (NuPacket.connect()) 45 | { 46 | Serial.println("--Connected--"); 47 | // "data" is a pointer to the incoming bytes 48 | // "size" is the count of bytes pointed by "data" 49 | size_t size; 50 | 51 | // Receive first packet: 52 | // current task is blocked until data is received or connection is lost. 53 | // This is not active waiting. 54 | const uint8_t *data = NuPacket.read(size); 55 | while (data) 56 | { 57 | // Dump incoming data to the serial monitor 58 | Serial.printf("--data packet of %d bytes follows--\n", size); 59 | Serial.write(data, size); 60 | Serial.println(""); 61 | Serial.println("--end of packet--"); 62 | 63 | // Acknowledge data reception 64 | NuPacket.send("Data received. Ready for more.\n"); 65 | 66 | // Receive next packet 67 | data = NuPacket.read(size); 68 | } 69 | // data==nullptr here, which means that the 70 | // the connection is lost 71 | Serial.println("--Disconnected--"); 72 | } 73 | } -------------------------------------------------------------------------------- /examples/ReadBytesDemo/ReadBytesDemo.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * @file ReadBytesDemo.ino 3 | * @author Ángel Fernández Pineda. Madrid. Spain. 4 | * @date 2023-12-24 5 | * @brief Example of a readBytes() with no active wait 6 | * 7 | * @note See examples/README.md for a description 8 | * 9 | * @copyright Creative Commons Attribution 4.0 International (CC BY 4.0) 10 | * 11 | */ 12 | 13 | #include 14 | #include 15 | #include "NuSerial.hpp" 16 | 17 | #define DEVICE_NAME "ReadBytes demo" 18 | 19 | void setup() 20 | { 21 | // Initialize serial monitor 22 | Serial.begin(115200); 23 | Serial.println("*********************"); 24 | Serial.println(" BLE readBytes()demo "); 25 | Serial.println("*********************"); 26 | Serial.println("--Initializing--"); 27 | 28 | // Initialize BLE stack and Nordic UART service 29 | NimBLEDevice::init(DEVICE_NAME); 30 | NimBLEDevice::getAdvertising()->setName(DEVICE_NAME); 31 | NuSerial.setTimeout(ULONG_MAX); // no timeout at readBytes() 32 | NuSerial.start(); 33 | 34 | // Initialization complete 35 | Serial.println("--Ready--"); 36 | } 37 | 38 | void loop() 39 | { 40 | uint8_t buffer[4]; 41 | size_t readBytes; 42 | 43 | Serial.println("--Waiting for connection--"); 44 | // Block current task until a connection is established. 45 | // This is not active waiting, so the CPU is free for other tasks. 46 | if (NuSerial.connect()) 47 | { 48 | Serial.println("--Connected--"); 49 | 50 | // Receive data in chunks of 4 bytes. 51 | // Current task is blocked until data is received or connection is lost. 52 | // This is not active waiting. 53 | readBytes = NuSerial.readBytes(buffer, 4); 54 | while (readBytes == 4) 55 | { 56 | // Dump incoming data to the serial monitor 57 | Serial.printf("%c%c%c%c\n", buffer[0], buffer[1], buffer[2], buffer[3]); 58 | 59 | // Receive next chunk 60 | readBytes = NuSerial.readBytes(buffer, 4); 61 | } 62 | // At this point the connection is lost, but some data may be still unread 63 | if (readBytes > 0) 64 | { 65 | // Dump remaining data 66 | int i = 0; 67 | while (readBytes > 0) 68 | { 69 | Serial.printf("%c", buffer[i++]); 70 | readBytes--; 71 | } 72 | Serial.println(""); 73 | } 74 | Serial.println("--Disconnected--"); 75 | } 76 | } -------------------------------------------------------------------------------- /src/NuATCommands.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file NuATCommands.hpp 3 | * @author Ángel Fernández Pineda. Madrid. Spain. 4 | * @date 2024-08-21 5 | * @brief AT command processor using the Nordic UART Service 6 | * 7 | * @copyright Creative Commons Attribution 4.0 International (CC BY 4.0) 8 | * 9 | */ 10 | 11 | #ifndef __NU_AT_COMMANDS_HPP__ 12 | #define __NU_AT_COMMANDS_HPP__ 13 | #ifndef __NU_AT_COMMANDS_LEGACY2_HPP__ 14 | 15 | #include "NuS.hpp" 16 | #include "NuATParser.hpp" 17 | 18 | /** 19 | * @brief Execute AT commands received thanks to the Nordic UART Service 20 | * 21 | */ 22 | class NuATCommandProcessor : public NordicUARTService, public NuATParser 23 | { 24 | public: 25 | // Singleton pattern 26 | 27 | NuATCommandProcessor(const NuATCommandProcessor &) = delete; 28 | NuATCommandProcessor(NuATCommandProcessor &&) = delete; 29 | NuATCommandProcessor &operator=(const NuATCommandProcessor &) = delete; 30 | NuATCommandProcessor &operator=(NuATCommandProcessor &&) = delete; 31 | 32 | /** 33 | * @brief Get the instance of the NuATCommandProcessor 34 | * 35 | * @note No need to use. Use `NuATCommands` instead. 36 | * 37 | * @return NuATCommandProcessor& Single instance 38 | */ 39 | static NuATCommandProcessor &getInstance() 40 | { 41 | static NuATCommandProcessor instance; 42 | return instance; 43 | }; 44 | 45 | public: 46 | // Overriden Methods 47 | virtual void onWrite( 48 | NimBLECharacteristic *pCharacteristic, 49 | NimBLEConnInfo &connInfo) override; 50 | virtual void printATResponse(::std::string message) override; 51 | 52 | // New methods 53 | 54 | /** 55 | * @brief Set a maximum command line length to prevent overflow 56 | * 57 | * @note If a command line exceeds this limit, it will be ignored 58 | * and an error response will be sent. 59 | * 60 | * @note A 256 bytes limit is recommended. 61 | * 62 | * @param value Zero to disable this feature. 63 | * Otherwise, a maximum line length in bytes. 64 | * @return uint32_t previous limit or zero if disabled. 65 | */ 66 | uint32_t maxCommandLineLength(uint32_t value = 0); 67 | 68 | private: 69 | uint32_t uMaxCommandLineLength = 256; 70 | 71 | // Singleton pattern 72 | NuATCommandProcessor() {}; 73 | }; 74 | 75 | /** 76 | * @brief Singleton instance of the NuATCommandProcessor class 77 | * 78 | */ 79 | extern NuATCommandProcessor &NuATCommands; 80 | 81 | #else 82 | #error NuATCommands.hpp is incompatible with NuATCommandsLegacy2.hpp 83 | #endif 84 | 85 | #endif 86 | -------------------------------------------------------------------------------- /src/NuATCommandsLegacy2.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file NuATCommandsLegacy2.hpp 3 | * @author Ángel Fernández Pineda. Madrid. Spain. 4 | * @date 2023-12-18 5 | * @brief AT command processor using the Nordic UART Service 6 | * 7 | * @copyright Creative Commons Attribution 4.0 International (CC BY 4.0) 8 | * 9 | */ 10 | #ifndef __NU_AT_COMMANDS_LEGACY2_HPP__ 11 | #define __NU_AT_COMMANDS_LEGACY2_HPP__ 12 | 13 | #ifndef __NU_AT_COMMANDS_HPP__ 14 | 15 | #include "NuS.hpp" 16 | #include "NuATCommandParserLegacy2.hpp" 17 | 18 | namespace NuSLegacy2 19 | { 20 | /** 21 | * @brief Execute AT commands received thanks to the Nordic UART Service 22 | * 23 | */ 24 | class NuATCommandProcessor : public NordicUARTService, public NuATCommandParser 25 | { 26 | public: 27 | // Singleton pattern 28 | 29 | NuATCommandProcessor(const NuATCommandProcessor &) = delete; 30 | NuATCommandProcessor(NuATCommandProcessor &&) = delete; 31 | NuATCommandProcessor &operator=(const NuATCommandProcessor &) = delete; 32 | NuATCommandProcessor &operator=( NuATCommandProcessor &&) = delete; 33 | 34 | /** 35 | * @brief Get the instance of the NuATCommandProcessor 36 | * 37 | * @note No need to use. Use `NuATCommands` instead. 38 | * 39 | * @return NuATCommandProcessor& 40 | */ 41 | static NuATCommandProcessor &getInstance() 42 | { 43 | static NuATCommandProcessor instance; 44 | return instance; 45 | }; 46 | 47 | public: 48 | // Overriden Methods 49 | virtual void onWrite( 50 | NimBLECharacteristic *pCharacteristic, 51 | NimBLEConnInfo &connInfo) override; 52 | virtual void printATResponse(const char message[]) override; 53 | 54 | /** 55 | * @brief Set custom AT command processing callbacks 56 | * 57 | * @note This method should be called before start(). Any way, 58 | * you are not allowed to set a new callbacks object while 59 | * a peer is connected. 60 | * 61 | * @param pCallbacks A pointer to your own callbacks. Must 62 | * remain valid forever (do not destroy). 63 | * 64 | * @throws ::std::runtime_error If called while a peer is connected. 65 | */ 66 | void setATCallbacks(NuATCommandCallbacks *pCallbacks); 67 | 68 | private: 69 | // Singleton pattern 70 | NuATCommandProcessor() {}; 71 | }; 72 | 73 | /** 74 | * @brief Singleton instance of the NuATCommandProcessor class 75 | * 76 | */ 77 | extern NuATCommandProcessor &NuATCommands; 78 | } 79 | 80 | #else 81 | #error NuATCommands.hpp is incompatible with NuATCommandsLegacy2.hpp 82 | #endif 83 | 84 | #endif -------------------------------------------------------------------------------- /src/NuPacket.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file NuPacket.hpp 3 | * @author Ángel Fernández Pineda. Madrid. Spain. 4 | * @date 2023-12-18 5 | * @brief Communications stream based on the Nordic UART Service 6 | * with blocking semantics 7 | * 8 | * @copyright Creative Commons Attribution 4.0 International (CC BY 4.0) 9 | * 10 | */ 11 | 12 | #ifndef __NUPACKET_HPP__ 13 | #define __NUPACKET_HPP__ 14 | 15 | #include "NuS.hpp" 16 | 17 | /** 18 | * @brief Blocking serial communications through BLE and Nordic UART Service 19 | * 20 | * @note Unlike `Serial`, the semantics 21 | * are those of blocking communications. This is more efficient in 22 | * terms of CPU usage, since no active waiting is used, and a performance 23 | * boost, since incoming bytes are processed in packets, not one bye one. 24 | * However, a multi-tasking app design must be adopted. 25 | * 26 | */ 27 | class NordicUARTPacket : public NordicUARTService 28 | { 29 | public: 30 | // Singleton pattern and Rule of Five 31 | 32 | NordicUARTPacket(const NordicUARTPacket &) = delete; 33 | NordicUARTPacket(NordicUARTPacket &&) = delete; 34 | NordicUARTPacket &operator=(const NordicUARTPacket &) = delete; 35 | NordicUARTPacket &operator=(NordicUARTPacket &&) = delete; 36 | 37 | /** 38 | * @brief Get the instance of the BLE stream 39 | * 40 | * @note No need to use. Use `NuPacket` instead. 41 | * 42 | * @return NordicUARTPacket& 43 | */ 44 | static NordicUARTPacket &getInstance() 45 | { 46 | static NordicUARTPacket instance; 47 | return instance; 48 | }; 49 | 50 | protected: 51 | // Overriden Methods 52 | virtual void onUnsubscribe(size_t subscriberCount) override; 53 | void onWrite( 54 | NimBLECharacteristic *pCharacteristic, 55 | NimBLEConnInfo &connInfo) override; 56 | 57 | public: 58 | /** 59 | * @brief Wait for and get incoming data in packets (blocking) 60 | * 61 | * @note The calling task will get blocked until incoming data is 62 | * available or the connection is lost. Just one task 63 | * can go beyond read() if more than one exists. 64 | * 65 | * @note You should not perform any time-consuming task between calls. 66 | * Use buffers/queues/etc for that. Follow this advice to increase 67 | * app responsiveness. 68 | * 69 | * @param[out] size Count of incoming bytes, 70 | * or zero if the connection was lost. This is the size of 71 | * the data packet. 72 | * @return uint8_t* Pointer to incoming data, or `nullptr` if the connection 73 | * was lost. 74 | * Do not access more bytes than available as given in 75 | * @p size. Otherwise, a segmentation fault may occur. 76 | */ 77 | const uint8_t *read(size_t &size) const noexcept; 78 | 79 | private: 80 | mutable nus_semaphore dataConsumed{1}; 81 | mutable nus_semaphore dataAvailable{0}; 82 | NimBLEAttValue incomingPacket; 83 | size_t availableByteCount = 0; 84 | const uint8_t *incomingBuffer = nullptr; 85 | 86 | // Singleton pattern 87 | NordicUARTPacket() {}; 88 | ~NordicUARTPacket() {}; 89 | }; 90 | 91 | /** 92 | * @brief Singleton instance of the NordicUARTPacket class 93 | * 94 | */ 95 | extern NordicUARTPacket &NuPacket; 96 | 97 | #endif -------------------------------------------------------------------------------- /extras/BatchCompile.ps1: -------------------------------------------------------------------------------- 1 | <############################################################################ 2 | 3 | .SYNOPSYS 4 | Compile all sketches in batch 5 | 6 | .AUTHOR 7 | Ángel Fernández Pineda. Madrid. Spain. 2024. 8 | 9 | #############################################################################> 10 | 11 | #setup 12 | $ErrorActionPreference = 'Stop' 13 | 14 | $thisPath = Split-Path $($MyInvocation.MyCommand.Path) -parent 15 | Set-Location "$thispath/.." 16 | 17 | # global constants 18 | $_compiler = "arduino-cli" 19 | 20 | <############################################################################# 21 | # Auxiliary functions 22 | #############################################################################> 23 | function Test-ArduinoCLI { 24 | &$_compiler version | Out-Null 25 | if ($LASTEXITCODE -ne 0) { 26 | throw "Arduino-cli not found. Check your PATH." 27 | } 28 | } 29 | 30 | function Invoke-ArduinoCLI { 31 | param( 32 | [string]$Filename, 33 | [string]$BuildPath 34 | ) 35 | Write-Host "--------------------------------------------------------------------------------" 36 | Write-Host "Sketch: $Filename" 37 | Write-Host "================================================================================" 38 | $ErrorActionPreference = 'Continue' 39 | &$_compiler compile $Filename -b esp32:esp32:esp32 --no-color --warnings all --build-path $BuildPath # 2>&1 40 | $ErrorActionPreference = 'Stop' 41 | } 42 | 43 | function New-TemporaryFolder { 44 | $File = New-TemporaryFile 45 | Remove-Item $File -Force | Out-Null 46 | $tempFolderName = Join-Path $ENV:Temp $File.Name 47 | $Folder = New-Item -Itemtype Directory -Path $tempFolderName 48 | return $Folder 49 | } 50 | 51 | <############################################################################# 52 | # MAIN 53 | #############################################################################> 54 | 55 | # Initialization 56 | $VerbosePreference = "continue" 57 | $InformationPreference = "continue" 58 | Test-ArduinoCLI 59 | 60 | # Create a temporary folder (will speed up compilation) 61 | $tempFolder = New-TemporaryFolder 62 | 63 | try { 64 | Clear-Host 65 | Write-Host "************" 66 | Write-Host "* EXAMPLES *" 67 | Write-Host "************" 68 | Invoke-ArduinoCLI -Filename "examples/ATCommandDemo/ATCommandDemo.ino" -BuildPath $tempFolder 69 | Invoke-ArduinoCLI -Filename "examples/ATCommandDemoLegacy2/ATCommandDemoLegacy2.ino" -BuildPath $tempFolder 70 | Invoke-ArduinoCLI -Filename "examples/CustomCommandProcessor/CustomCommandProcessor.ino" -BuildPath $tempFolder 71 | Invoke-ArduinoCLI -Filename "examples/NuSEcho/NuSEcho.ino" -BuildPath $tempFolder 72 | Invoke-ArduinoCLI -Filename "examples/NuSerialDump/NuSerialDump.ino" -BuildPath $tempFolder 73 | Invoke-ArduinoCLI -Filename "examples/ShellCommandDemo/ShellCommandDemo.ino" -BuildPath $tempFolder 74 | Write-Host "*********" 75 | Write-Host "* TESTS *" 76 | Write-Host "*********" 77 | Invoke-ArduinoCLI -Filename "extras/test/ATCommandsTester/ATCommandsTester.ino" -BuildPath $tempFolder 78 | Invoke-ArduinoCLI -Filename "extras/test/ATCommandsTesterLegacy2/ATCommandsTesterLegacy2.ino" -BuildPath $tempFolder 79 | Invoke-ArduinoCLI -Filename "extras/test/HandshakeTest/HandshakeTest.ino" -BuildPath $tempFolder 80 | Invoke-ArduinoCLI -Filename "extras/test/Issue8/Issue8.ino" -BuildPath $tempFolder 81 | Invoke-ArduinoCLI -Filename "extras/test/SimpleCommandTester/SimpleCommandTester.ino" -BuildPath $tempFolder 82 | } 83 | finally { 84 | # Remove temporary folder 85 | Remove-Item -Recurse -LiteralPath $tempFolder.FullName -Force | Out-Null 86 | } -------------------------------------------------------------------------------- /examples/NuSEcho/NuSEcho.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * @file NusEcho.ino 3 | * @author Ángel Fernández Pineda. Madrid. Spain. 4 | * @date 2023-12-18 5 | * 6 | * @brief Example of a non-blocking communications stream 7 | * based on the Nordic UART Service 8 | * 9 | * @note See examples/README.md for a description 10 | * 11 | * @copyright Creative Commons Attribution 4.0 International (CC BY 4.0) 12 | * 13 | */ 14 | 15 | #include 16 | #include "NuSerial.hpp" 17 | #include "NimBLEDevice.h" 18 | 19 | #define DEVICE_NAME "NuSerial Echo" 20 | 21 | void setup() 22 | { 23 | // Initialize serial monitor 24 | Serial.begin(115200); 25 | Serial.println("*****************************"); 26 | Serial.println(" BLE echo server demo "); 27 | Serial.println("*****************************"); 28 | Serial.println("--Initializing--"); 29 | 30 | // Initialize BLE stack and Nordic UART service 31 | NimBLEDevice::init(DEVICE_NAME); 32 | NimBLEDevice::getAdvertising()->setName(DEVICE_NAME); 33 | NuSerial.begin(115200); 34 | 35 | // Initialization complete 36 | Serial.println("--Ready--"); 37 | } 38 | 39 | void loop() 40 | { 41 | NimBLEServer *pServer = NimBLEDevice::getServer(); 42 | if (NuSerial.isConnected()) 43 | { 44 | int serialMonitorChar = Serial.read(); 45 | if ((serialMonitorChar == 'E') || (serialMonitorChar == 'e')) 46 | { 47 | // Open the serial monitor in Arduino IDE 48 | // Type "E" or "e" and press ENTER to drop the BLE connection 49 | Serial.println("--Terminating connection from server side--"); 50 | NuSerial.end(); 51 | } 52 | else 53 | { 54 | int processedCount = 0; 55 | int availableCount = NuSerial.available(); 56 | if (availableCount) 57 | Serial.printf("--Available %d bytes for processing--\n", availableCount); 58 | while (NuSerial.available()) 59 | { 60 | int bleChar = NuSerial.read(); 61 | if (bleChar < 0) 62 | Serial.println("ERROR: NuSerial.read()<0, but NuSerial.available()>0. Should not happen."); 63 | else 64 | { 65 | // Echo 66 | if (NuSerial.write(bleChar) < 1) 67 | Serial.println("ERROR: NuSerial.write() failed"); 68 | 69 | // Note: the following delay is required because we are sending data in a byte-by-byte basis. 70 | // If we send bytes quicker than they are consumed by the peer, 71 | // the internal buffer of NimBLE will overflow, thus losing some bytes. 72 | // BLE is designed to transmit a larger chunk of bytes slowly rather than a single byte quickly. 73 | // That's another reason to use NuPacket instead of NuSerial. 74 | delay(30); 75 | 76 | // log ASCII/ANSI codes 77 | Serial.printf("%d.", bleChar); 78 | processedCount++; 79 | } 80 | } 81 | 82 | if (processedCount != availableCount) 83 | Serial.printf("\nERROR: %d bytes were available, but %d bytes were processed.\n", availableCount, processedCount); 84 | else if (processedCount) 85 | { 86 | Serial.printf("\n--Stream of %d bytes processed--\n", processedCount); 87 | } 88 | } 89 | } 90 | else 91 | { 92 | Serial.println("--Waiting for connection and subscription--"); 93 | // Wait for subscription to the Nordic UART TX characteristic 94 | while (!NuSerial.isConnected()) 95 | { 96 | delay(500); 97 | } 98 | // A client is connected and subscribed 99 | Serial.println("--Connected and subscribed--"); 100 | } 101 | } -------------------------------------------------------------------------------- /src/NuStream.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file NuStream.hpp 3 | * @author Ángel Fernández Pineda. Madrid. Spain. 4 | * @date 2023-12-24 5 | * @brief Communications stream based on the Nordic UART Service 6 | * 7 | * @copyright Creative Commons Attribution 4.0 International (CC BY 4.0) 8 | * 9 | */ 10 | 11 | #ifndef __NUSTREAM_HPP__ 12 | #define __NUSTREAM_HPP__ 13 | 14 | #include // For ULONG_MAX 15 | #include 16 | #include "NuS.hpp" 17 | 18 | /** 19 | * @brief Communication stream via BLE and Nordic UART service 20 | * 21 | */ 22 | class NordicUARTStream : public NordicUARTService, public Stream 23 | { 24 | public: 25 | using NordicUARTService::print; 26 | using NordicUARTService::printf; 27 | 28 | protected: 29 | // Overriden Methods 30 | virtual void onUnsubscribe(size_t subscriberCount) override; 31 | void onWrite( 32 | NimBLECharacteristic *pCharacteristic, 33 | NimBLEConnInfo &connInfo) override; 34 | 35 | public: 36 | NordicUARTStream() : NordicUARTService(), Stream() {}; 37 | NordicUARTStream(const NordicUARTStream &) = delete; 38 | NordicUARTStream(NordicUARTStream &&) = delete; 39 | NordicUARTStream &operator=(const NordicUARTStream &) = delete; 40 | NordicUARTStream &operator=(NordicUARTStream &&) = delete; 41 | virtual ~NordicUARTStream() {}; 42 | 43 | public: 44 | /** 45 | * @brief Gets the number of bytes available in the stream 46 | * 47 | * @return int The number of bytes available to read 48 | */ 49 | virtual int available() override; 50 | 51 | /** 52 | * @brief Read a byte from the stream without advancing to the next one 53 | * 54 | * @note Successive calls to peek() will return the same value, 55 | * as will the next call to read(). 56 | * 57 | * @return int The next byte or -1 if none is available. 58 | */ 59 | virtual int peek() override; 60 | 61 | /** 62 | * @brief Reads a single character from an incoming stream 63 | * 64 | * @return int The first byte of incoming data available (or -1 if no data is available). 65 | */ 66 | virtual int read() override; 67 | 68 | /** 69 | * @brief Read characters from a stream into a buffer. 70 | * 71 | * @note Terminates if the determined length has been read, it times out, 72 | * or peer is disconnected. Unlike other read methods, no active waiting is used here. 73 | * 74 | * @note Call `setTimeout(ULONG_MAX)` to disable time outs. 75 | * 76 | * @param[out] buffer To store the bytes in 77 | * @param[in] size the Number of bytes to read 78 | * @return size_t Number of bytes placed in the buffer. This number is in the 79 | * range from 0 to @p size. When this number is less than @p size, 80 | * a timeout or peer disconnection happened. Check isConnected() 81 | * to know the case. 82 | */ 83 | virtual size_t readBytes(uint8_t *buffer, size_t size) override; 84 | virtual size_t readBytes(char *buffer, size_t length) override 85 | { 86 | return NordicUARTStream::readBytes((uint8_t *)buffer, length); 87 | }; 88 | 89 | public: 90 | /** 91 | * @brief Write a single byte to the stream 92 | * 93 | * @param[in] byte Byte to write 94 | * @return size_t The number of bytes written 95 | */ 96 | virtual size_t write(uint8_t byte) override 97 | { 98 | return NordicUARTService::write(&byte, 1); 99 | }; 100 | 101 | /** 102 | * @brief Write bytes to the stream 103 | * 104 | * @param[in] buffer Pointer to first byte to write 105 | * @param[in] size Count of bytes to write 106 | * @return size_t Actual count of bytes that were written 107 | */ 108 | virtual size_t write(const uint8_t *buffer, size_t size) override 109 | { 110 | return NordicUARTService::write(buffer, size); 111 | }; 112 | 113 | private: 114 | nus_semaphore dataConsumed{1}; 115 | nus_semaphore dataAvailable{0}; 116 | NimBLEAttValue incomingPacket; 117 | bool disconnected = false; 118 | size_t unreadByteCount = 0; 119 | }; 120 | 121 | #endif -------------------------------------------------------------------------------- /src/cyan_semaphore.h: -------------------------------------------------------------------------------- 1 | // MIT License 2 | 3 | // Copyright (c) 2021 CyanHill 4 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | #ifndef __CYAN_SEMAPHORE_H__ 24 | #define __CYAN_SEMAPHORE_H__ 25 | 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | 33 | namespace cyan 34 | { 35 | 36 | template <::std::ptrdiff_t least_max_value = ::std::numeric_limits<::std::ptrdiff_t>::max()> 37 | class counting_semaphore 38 | { 39 | public: 40 | static constexpr ::std::ptrdiff_t max() noexcept 41 | { 42 | static_assert(least_max_value >= 0, "least_max_value shall be non-negative"); 43 | return least_max_value; 44 | } 45 | 46 | explicit counting_semaphore(::std::ptrdiff_t desired) : counter_(desired) { assert(desired >= 0 && desired <= max()); } 47 | 48 | ~counting_semaphore() = default; 49 | 50 | counting_semaphore(const counting_semaphore &) = delete; 51 | counting_semaphore &operator=(const counting_semaphore &) = delete; 52 | 53 | void release(::std::ptrdiff_t update = 1) 54 | { 55 | if (update <= 0) 56 | return; 57 | { 58 | ::std::lock_guard lock{mutex_}; 59 | ::std::ptrdiff_t newCounter = counter_ + update; 60 | if (newCounter > max()) 61 | newCounter = max(); 62 | counter_ = newCounter; 63 | } // avoid hurry up and wait 64 | cv_.notify_all(); 65 | } 66 | 67 | void acquire() 68 | { 69 | ::std::unique_lock lock{mutex_}; 70 | cv_.wait(lock, [&]() 71 | { return (counter_ > 0); }); 72 | --counter_; 73 | } 74 | 75 | // bool try_acquire() noexcept { 76 | // ::std::unique_lock lock{mutex_}; 77 | // if (counter_ <= 0) { 78 | // return false; 79 | // } 80 | // --counter_; 81 | // return true; 82 | // } 83 | 84 | template 85 | bool try_acquire_for(const ::std::chrono::duration &rel_time) 86 | { 87 | const auto timeout_time = ::std::chrono::steady_clock::now() + rel_time; 88 | return do_try_acquire_wait(timeout_time); 89 | } 90 | 91 | // template 92 | // bool try_acquire_until(const ::std::chrono::time_point& abs_time) { 93 | // return do_try_acquire_wait(abs_time); 94 | // } 95 | 96 | private: 97 | template 98 | bool do_try_acquire_wait(const ::std::chrono::time_point &timeout_time) 99 | { 100 | ::std::unique_lock lock{mutex_}; 101 | if (!cv_.wait_until(lock, timeout_time, [&]() 102 | { return counter_ > 0; })) 103 | { 104 | return false; 105 | } 106 | --counter_; 107 | return true; 108 | } 109 | 110 | private: 111 | ::std::ptrdiff_t counter_{0}; 112 | ::std::condition_variable cv_; 113 | ::std::mutex mutex_; 114 | }; 115 | 116 | using binary_semaphore = counting_semaphore<1>; 117 | 118 | } // namespace cyan 119 | 120 | #endif -------------------------------------------------------------------------------- /src/NuStream.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file NuStream.cpp 3 | * @author Ángel Fernández Pineda. Madrid. Spain. 4 | * @date 2023-12-24 5 | * @brief Communications stream based on the Nordic UART Service 6 | * 7 | * @copyright Creative Commons Attribution 4.0 International (CC BY 4.0) 8 | * 9 | */ 10 | 11 | #include "NuStream.hpp" 12 | #include 13 | 14 | //----------------------------------------------------------------------------- 15 | // GATT server events 16 | //----------------------------------------------------------------------------- 17 | 18 | void NordicUARTStream::onUnsubscribe(size_t subscriberCount) 19 | { 20 | if (subscriberCount == 0) 21 | { 22 | // Awake task at readBytes() 23 | disconnected = true; 24 | dataAvailable.release(); 25 | } 26 | }; 27 | 28 | //----------------------------------------------------------------------------- 29 | // NordicUARTService implementation 30 | //----------------------------------------------------------------------------- 31 | 32 | void NordicUARTStream::onWrite( 33 | NimBLECharacteristic *pCharacteristic, 34 | NimBLEConnInfo &connInfo) 35 | { 36 | // Wait for previous data to get consumed 37 | dataConsumed.acquire(); 38 | 39 | // Hold data until next read 40 | incomingPacket = pCharacteristic->getValue(); 41 | unreadByteCount = incomingPacket.size(); 42 | disconnected = false; 43 | 44 | // signal available data 45 | dataAvailable.release(); 46 | } 47 | 48 | //----------------------------------------------------------------------------- 49 | // Reading with no active wait 50 | //----------------------------------------------------------------------------- 51 | 52 | size_t NordicUARTStream::readBytes(uint8_t *buffer, size_t size) 53 | { 54 | size_t totalReadCount = 0; 55 | while (size > 0) 56 | { 57 | // copy previously available data, if any 58 | if (unreadByteCount > 0) 59 | { 60 | const uint8_t *incomingData = incomingPacket.data() + incomingPacket.size() - unreadByteCount; 61 | size_t readBytesCount = (unreadByteCount > size) ? size : unreadByteCount; 62 | memcpy(buffer, incomingData, readBytesCount); 63 | buffer = buffer + readBytesCount; 64 | unreadByteCount = unreadByteCount - readBytesCount; 65 | totalReadCount = totalReadCount + readBytesCount; 66 | size = size - readBytesCount; 67 | } // note: at this point (unreadByteCount == 0) || (size == 0) 68 | if (unreadByteCount == 0) 69 | { 70 | dataConsumed.release(); 71 | // xSemaphoreGive(dataConsumed); 72 | } 73 | if (size > 0) 74 | { 75 | // wait for more data or timeout or disconnection 76 | bool waitResult = true; 77 | if (_timeout == ULONG_MAX) 78 | dataAvailable.acquire(); 79 | else 80 | waitResult = dataAvailable.try_acquire_for(::std::chrono::milliseconds(_timeout)); 81 | if (!waitResult || disconnected) 82 | size = 0; // break; 83 | // Note: at this point, readBuffer and unreadByteCount were updated thanks to onWrite() 84 | } 85 | } 86 | return totalReadCount; 87 | } 88 | 89 | //----------------------------------------------------------------------------- 90 | // Stream implementation 91 | //----------------------------------------------------------------------------- 92 | 93 | int NordicUARTStream::available() 94 | { 95 | return unreadByteCount; 96 | } 97 | 98 | int NordicUARTStream::peek() 99 | { 100 | if (unreadByteCount > 0) 101 | { 102 | const uint8_t *readBuffer = incomingPacket.data(); 103 | size_t index = incomingPacket.size() - unreadByteCount; 104 | return readBuffer[index]; 105 | } 106 | return -1; 107 | } 108 | 109 | int NordicUARTStream::read() 110 | { 111 | if (unreadByteCount > 0) 112 | { 113 | const uint8_t *readBuffer = incomingPacket.data(); 114 | size_t index = incomingPacket.size() - unreadByteCount; 115 | int result = readBuffer[index]; 116 | unreadByteCount--; 117 | if (unreadByteCount == 0) 118 | dataConsumed.release(); 119 | return result; 120 | } 121 | return -1; 122 | } -------------------------------------------------------------------------------- /examples/UartBleAdapter/UartBleAdapter.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * @file UartBleAdapter.ino 3 | * @author Sergii Pylypenko. 4 | * 5 | * @date 2025-12-10 6 | * 7 | * @brief UART to BLE adapter - transfers data between 8 | * the configured ESP32's UARTS and the Nordic UART Service, 9 | * including hardware UARTS and the USB CDC UART (if available). 10 | * 11 | * @note See examples/README.md for a description 12 | * 13 | * @copyright Creative Commons Attribution 4.0 International (CC BY 4.0) 14 | * 15 | */ 16 | 17 | #include 18 | #include "NuSerial.hpp" 19 | #include "NimBLEDevice.h" 20 | 21 | // Expected hardware UART baud rate for UART0 22 | // (using the default RX/TX pins) and 23 | // UART1 (configured to pins 4 and 5 below). 24 | // **Set to ZERO to ignore such an UART**. 25 | // All ESP32 chips are configured by default to 115200 bauds. 26 | // Make sure the device connected to the other endpoint 27 | // matches this baud rate. 28 | #define UART0_BAUD_RATE 115200 29 | #define UART1_BAUD_RATE 115200 30 | 31 | // NOTE: 32 | // To enable or disable the USB CDC UART (if available) 33 | // go to the "Board Configuration" option in Arduino IDE 34 | // and choose a value for "USB CDC on boot" 35 | 36 | // Buffer size (set as you wish) 37 | #define BUFFER_SIZE 2048 38 | // Read/write buffer to hold data in transit 39 | uint8_t data_buffer[BUFFER_SIZE]; 40 | 41 | void setup() 42 | { 43 | #if UART0_BAUD_RATE > 0 44 | // Initialize the 1st hardware UART 45 | Serial0.setRxBufferSize(BUFFER_SIZE); 46 | Serial0.begin(UART0_BAUD_RATE); 47 | Serial0.setTimeout(20); 48 | #endif 49 | #if UART1_BAUD_RATE > 0 50 | // Initialize the 2nd hardware UART 51 | Serial1.setRxBufferSize(BUFFER_SIZE); 52 | // Configured to pins 4 and 5. Feel free to change. 53 | Serial1.begin(UART1_BAUD_RATE, SERIAL_8N1, 4, 5); 54 | Serial1.setTimeout(20); 55 | #endif 56 | #if ARDUINO_USB_CDC_ON_BOOT && ARDUINO_USB_MODE 57 | // Initialize the USB CDC UART (if available) 58 | HWCDCSerial.setRxBufferSize(BUFFER_SIZE); 59 | HWCDCSerial.begin(); // Note: USB CDC ignores the baud parameter 60 | HWCDCSerial.setTimeout(20); 61 | #endif 62 | #if ARDUINO_USB_CDC_ON_BOOT && !ARDUINO_USB_MODE 63 | USBSerial.setRxBufferSize(BUFFER_SIZE); 64 | USBSerial.begin(); // Note: USB CDC ignores the baud parameter 65 | USBSerial.setTimeout(20); 66 | #endif 67 | 68 | char name[17]; 69 | snprintf(name, sizeof(name), "BLE-Serial-%04X", (uint16_t)ESP.getEfuseMac()); 70 | // Initialize the BLE stack and the Nordic UART service 71 | NimBLEDevice::init(name); 72 | NimBLEDevice::getAdvertising()->setName(name); 73 | NuSerial.begin(); // Note: NuS ignores the baud parameter 74 | } 75 | 76 | // Some general notes: 77 | // - We don't care about the connection state (there is no need to). 78 | // - We don't handle errors as there is no 79 | // place to report them. 80 | 81 | void loop() 82 | { 83 | // First, we read data from the configured UARTS 84 | // and send it to NuSerial 85 | // --------------------------------------------- 86 | size_t available_size; 87 | 88 | #if UART0_BAUD_RATE > 0 89 | available_size = Serial0.readBytes(data_buffer, Serial0.available()); 90 | NuSerial.write(data_buffer, available_size); 91 | #endif 92 | #if UART1_BAUD_RATE > 0 93 | available_size = Serial1.readBytes(data_buffer, Serial1.available()); 94 | NuSerial.write(data_buffer, available_size); 95 | #endif 96 | #if ARDUINO_USB_CDC_ON_BOOT && ARDUINO_USB_MODE 97 | available_size = HWCDCSerial.readBytes(data_buffer, HWCDCSerial.available()); 98 | NuSerial.write(data_buffer, available_size); 99 | #endif 100 | #if ARDUINO_USB_CDC_ON_BOOT && !ARDUINO_USB_MODE 101 | available_size = USBSerial.readBytes(data_buffer, USBSerial.available()); 102 | NuSerial.write(data_buffer, available_size); 103 | #endif 104 | 105 | // Next, we read data from NuSerial 106 | // and send it to the configured UARTS 107 | // --------------------------------------------- 108 | available_size = NuSerial.readBytes(data_buffer, NuSerial.available()); 109 | #if UART0_BAUD_RATE > 0 110 | Serial0.write(data_buffer, available_size); 111 | #endif 112 | #if UART1_BAUD_RATE > 0 113 | Serial1.write(data_buffer, available_size); 114 | #endif 115 | #if ARDUINO_USB_CDC_ON_BOOT && ARDUINO_USB_MODE 116 | HWCDCSerial.write(data_buffer, available_size); 117 | #endif 118 | #if ARDUINO_USB_CDC_ON_BOOT && !ARDUINO_USB_MODE 119 | USBSerial.write(data_buffer, available_size); 120 | #endif 121 | } 122 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Nordic UART Service: Arduino demos 2 | 3 | ## Summary 4 | 5 | - [NuSEcho.ino](./NuSEcho/NuSEcho.ino) 6 | 7 | Runs a service that takes incoming data from a BLE peer 8 | and dumps it back to the same peer. 9 | This is called an "echo" service. 10 | The serial monitor is also feed with log messages. 11 | The device is advertised as "NuSerial Echo". 12 | Demonstrates the usage of non-blocking communications 13 | by the means of `NuSerial`. 14 | 15 | You may type `E` or `e` at the serial monitor to 16 | forcedly terminate current peer connection and wait for another. 17 | 18 | - [NuSerialDump.ino](./NuSerialDump/NuSerialDump.ino) 19 | 20 | Runs a service that takes incoming data from a BLE peer 21 | and dumps it to the serial monitor. 22 | The device is advertised as "NuPacket demo". 23 | Demonstrates the usage of blocking communications by the means of `NuPacket`. 24 | 25 | - [ReadBytesDemo.ino](./ReadBytesDemo/ReadBytesDemo.ino) 26 | 27 | As the previous one, runs a service that takes incoming data 28 | from a BLE peer and dumps it into the serial monitor. 29 | However, this example uses a fixed size buffer 30 | and does not care about packet size. 31 | The device is advertised as "ReadBytes demo". 32 | Demonstrates the usage of blocking communications 33 | by the means of `NuSerial.readBytes()` with no active waiting. 34 | 35 | Since incoming data is buffered, you have to send 36 | at least 4 characters or disconnect to see any output at the serial monitor. 37 | Note that the terminating LF character (`\n`) also counts. 38 | 39 | - [CustomCommandProcessor.ino](./CustomCommandProcessor/CustomCommandProcessor.ino) 40 | 41 | Runs a service that parses incoming commands from a BLE peer 42 | and executes them. 43 | The serial monitor is also feed with log messages. 44 | The device is advertised as "Custom commands demo". 45 | Demonstrates how to write a custom protocol based on NuS. 46 | 47 | Commands and their syntax: 48 | 49 | - `exit`: forces the service to disconnect. 50 | - `sum `: retrieve the sum of two integer numbers. 51 | Substitute `` with those numbers. 52 | 53 | All commands are lower-case. Arguments must be separated by blank spaces. 54 | 55 | - [ATCommandDemo.ino](./ATCommandDemo/ATCommandDemo.ino) 56 | 57 | Runs a service that parses incoming AT commands 58 | from a BLE peer and executes them. 59 | The serial monitor is also feed with log messages. 60 | The device is advertised as "AT commands demo". 61 | Demonstrates how to serve custom AT commands on NuS. 62 | The service works as a simple calculator. 63 | 64 | Supported commands (always follow AT command syntax): 65 | 66 | - `+A=`. Set the value of the first operand. 67 | - `+A?`. Get the value of the first operand. 68 | - `+B=`. Set the value of the second operand. 69 | - `+B?`. Get the value of the second operand. 70 | - `+OP=,`. Set the value of both operands. 71 | - `+OP?`. Get the value of both operands, A then B. 72 | - `+SUM` or `+SUM?`. Get the sum A+B. 73 | - `+SUB` or `+SUB?`. Get the subtraction A-B. 74 | - `+MULT` or `+MULT?`. Get the multiplication A*B. 75 | - `+DIV` or `+DIV?`. Get the division A/B. 76 | - `&V`. Get the version number. 77 | 78 | For example: `AT+OP=14,2;+DIV?` 79 | 80 | - [ATCommandDemoLegacy2.ino](./ATCommandDemoLegacy2/ATCommandDemoLegacy2.ino) 81 | 82 | Works just the same as the previous demo. 83 | 84 | Uses the legacy API (up to version 2.0.5). 85 | You should not base your code in this example anymore. 86 | 87 | - [ShellCommandDemo.ino](./ShellCommandDemo/ShellCommandDemo.ino) 88 | 89 | Runs a service that parses shell-like commands from a BLE peer 90 | and executes them. 91 | The serial monitor is also feed with log messages. 92 | The device is advertised as "Shell commands demo". 93 | Demonstrates how to serve shell commands on NuS. 94 | The service works as a simple calculator. 95 | 96 | Supported commands (one per line): 97 | 98 | - `sum ` 99 | - `sub ` 100 | - `mult ` 101 | - `div ` 102 | 103 | Replace `` with an integer number. 104 | 105 | - [UartBleAdapter.ino](./UartBleAdapter/UartBleAdapter.ino) 106 | 107 | Turns your ESP32 into a **Bluetooth-to-UART bridge**. 108 | Transfers data between the configured UARTs (TX/RX pins) 109 | and the Nordic UART Service back and forth. 110 | 111 | This is quite handy to debug code in other devices having a hardware UART, 112 | but no USB-to-UART bridge. 113 | 114 | Without modification, it bridges UART0, UART1 (configured to pins 4 and 5) 115 | and the USB CDC UART (if available), 116 | so make sure the involved pins are not floating. 117 | Otherwise, the sketch will read random data 118 | and send it to the Bluetooth side (⚠️). 119 | You may edit the sketch to disable the unneeded UARTs. 120 | 121 | > **Note**: 122 | > if your device gets data from two or more UARTs, 123 | > you don't know which one of them is sending it 124 | > to the Bluetooth side. 125 | 126 | If your devkit is connected to the PC serial monitor, 127 | this sketch behaves quite similar to the *NuSerialDump.ino* example above. 128 | In such a case, make sure to configure 129 | the same line ending character sequence in both sides. 130 | 131 | ## Testing 132 | 133 | In order to test those sketches, 134 | you need a serial terminal app compatible with NuS in your smartphone or PC. 135 | During development, this one was used (Android): 136 | [Serial Bluetooth terminal](https://play.google.com/store/apps/details?id=de.kai_morich.serial_bluetooth_terminal). 137 | 138 | Configure LF (line feed, aka `\n`) as the line-terminating character. 139 | -------------------------------------------------------------------------------- /examples/ShellCommandDemo/ShellCommandDemo.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * @file ShellCommandDemo.ino 3 | * @author Ángel Fernández Pineda. Madrid. Spain. 4 | * @date 2023-12-27 5 | * @brief Example of a shell command processor based on 6 | * the Nordic UART Service 7 | * 8 | * @note See examples/README.md for a description 9 | * 10 | * @copyright Creative Commons Attribution 4.0 International (CC BY 4.0) 11 | * 12 | */ 13 | 14 | #include 15 | #include "NuShellCommands.hpp" 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | #define DEVICE_NAME "Shell commands demo" 22 | 23 | //------------------------------------------------------ 24 | // Auxiliary functions 25 | //------------------------------------------------------ 26 | 27 | /** 28 | * @brief Parse a string into an integer number 29 | * 30 | * @param[in] text String containing an integer 31 | * @param[out] number The number contained in @p text 32 | * @return true On success 33 | * @return false If @p text does not contain a valid integer 34 | */ 35 | bool strToIntMax(std::string text, intmax_t &number) 36 | { 37 | // "errno" is used to detect non-integer data 38 | // errno==0 means no error 39 | errno = 0; 40 | intmax_t r = strtoimax(text.c_str(), NULL, 10); 41 | if (errno == 0) 42 | { 43 | number = r; 44 | return true; 45 | } 46 | else 47 | return false; 48 | } 49 | 50 | /** 51 | * @brief Parse two strings into integer numbers and react to errors 52 | * 53 | */ 54 | bool strArgsToInt(std::string arg1, std::string arg2, intmax_t &num1, intmax_t &num2) 55 | { 56 | if (strToIntMax(arg1, num1)) 57 | { 58 | if (strToIntMax(arg2, num2)) 59 | return true; 60 | NuShellCommands.send("ERROR: 2nd argument is not a valid integer\n"); 61 | } 62 | else 63 | NuShellCommands.send("ERROR: 1st argument is not a valid integer\n"); 64 | return false; 65 | } 66 | 67 | void printArgError() 68 | { 69 | NuShellCommands.send("ERROR: expected a command and two arguments\n"); 70 | } 71 | 72 | //------------------------------------------------------ 73 | // Shell Commands implementation for a simple calculator 74 | //------------------------------------------------------ 75 | 76 | void onAdd(NuCommandLine_t &commandLine) 77 | { 78 | if (commandLine.size() != 3) 79 | { 80 | printArgError(); 81 | return; 82 | } 83 | intmax_t n1, n2; 84 | if (strArgsToInt(commandLine[1], commandLine[2], n1, n2)) 85 | NuShellCommands.printf("%lld\n", (n1 + n2)); 86 | } 87 | 88 | void onSubtract(NuCommandLine_t &commandLine) 89 | { 90 | if (commandLine.size() != 3) 91 | { 92 | printArgError(); 93 | return; 94 | } 95 | intmax_t n1, n2; 96 | if (strArgsToInt(commandLine[1], commandLine[2], n1, n2)) 97 | NuShellCommands.printf("%lld\n", (n1 - n2)); 98 | } 99 | 100 | void onMultiply(NuCommandLine_t &commandLine) 101 | { 102 | if (commandLine.size() != 3) 103 | { 104 | printArgError(); 105 | return; 106 | } 107 | intmax_t n1, n2; 108 | if (strArgsToInt(commandLine[1], commandLine[2], n1, n2)) 109 | NuShellCommands.printf("%lld\n", (n1 * n2)); 110 | } 111 | 112 | void onDivide(NuCommandLine_t &commandLine) 113 | { 114 | if (commandLine.size() != 3) 115 | { 116 | printArgError(); 117 | return; 118 | } 119 | intmax_t n1, n2; 120 | if (strArgsToInt(commandLine[1], commandLine[2], n1, n2)) 121 | { 122 | if (n2 != 0) 123 | NuShellCommands.printf("%lld\n", (n1 / n2)); 124 | else 125 | NuShellCommands.send("ERROR: divide by zero\n"); 126 | } 127 | } 128 | 129 | void onParseError(NuCLIParsingResult_t parsingResult, size_t index) 130 | { 131 | if (parsingResult == CLI_PR_ILL_FORMED_STRING) 132 | NuShellCommands.printf("SYNTAX ERROR at char index %d. Code %d.\n", index, parsingResult); 133 | else if (parsingResult == CLI_PR_NO_COMMAND) 134 | { 135 | NuShellCommands.send("Available commands: sum sub div mult\n"); 136 | } 137 | } 138 | 139 | //------------------------------------------------------ 140 | // Arduino entry points 141 | //------------------------------------------------------ 142 | 143 | void setup() 144 | { 145 | // Initialize serial monitor 146 | Serial.begin(115200); 147 | Serial.println("**********************************"); 148 | Serial.println(" BLE shell command processor demo "); 149 | Serial.println("**********************************"); 150 | Serial.println("--Initializing--"); 151 | 152 | // Set callbacks 153 | NuShellCommands 154 | .on("add", onAdd) 155 | .on("sum", onAdd) 156 | .on("sub", onSubtract) 157 | .on("subtract", onSubtract) 158 | .on("mult", onMultiply) 159 | .on("multiply", onMultiply) 160 | .on("div", onDivide) 161 | .on("divide", onDivide) 162 | .onUnknown([](NuCommandLine_t &commandLine) 163 | { NuShellCommands.printf("ERROR: Unknown command \"%s\"\n", commandLine[0].c_str()); }) 164 | .onParseError(onParseError); 165 | 166 | // Initialize BLE and Nordic UART service 167 | NimBLEDevice::init(DEVICE_NAME); 168 | NimBLEDevice::getAdvertising()->setName(DEVICE_NAME); 169 | NuShellCommands.caseSensitive(false); 170 | NuShellCommands.start(); 171 | 172 | // Initialization complete 173 | Serial.println("--Ready--"); 174 | } 175 | 176 | void loop() 177 | { 178 | // Incoming data is processed in another task created by the BLE stack, 179 | // so there is nothing to do here (in this demo) 180 | Serial.println("--Running (heart beat each 30 seconds)--"); 181 | delay(30000); 182 | } -------------------------------------------------------------------------------- /src/NuCLIParser.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file NuCLIParser.hpp 3 | * @author Ángel Fernández Pineda. Madrid. Spain. 4 | * @date 2023-12-26 5 | * @brief Simple command line parser 6 | * 7 | * @copyright Creative Commons Attribution 4.0 International (CC BY 4.0) 8 | * 9 | */ 10 | 11 | #ifndef __NU_CLI_PARSER_HPP__ 12 | #define __NU_CLI_PARSER_HPP__ 13 | 14 | #include 15 | #include 16 | #include // Needed for strlen() 17 | #include 18 | 19 | /** 20 | * @brief Parsing state of a received command 21 | * 22 | * @note Additional information about parsing for debug or logging purposes 23 | */ 24 | typedef enum 25 | { 26 | /** No Parsing error */ 27 | CLI_PR_OK = 0, 28 | /** Callbacks not set */ 29 | CLI_PR_NO_CALLBACKS, 30 | /** Command line is empty */ 31 | CLI_PR_NO_COMMAND, 32 | /** A string parameter is not properly enclosed between double quotes */ 33 | CLI_PR_ILL_FORMED_STRING 34 | 35 | } NuCLIParsingResult_t; 36 | 37 | /** 38 | * @brief Parsed strings in a command line, from left to right 39 | * 40 | * @note First item is always a command name, so this vector 41 | * is never empty. However, it may contain empty strings 42 | * typed as "". 43 | */ 44 | typedef ::std::vector<::std::string> NuCommandLine_t; 45 | 46 | /** 47 | * @brief Callback to execute for a parsed command line 48 | * 49 | * @param[in] commandLine Parsed command line. 50 | */ 51 | typedef ::std::function NuCLICommandCallback_t; 52 | 53 | /** 54 | * @brief Callback to execute in case of parsing errors 55 | * 56 | * @note This callback is never executed with parsing result CLI_PR_OK. 57 | * 58 | * @param[in] result Parsing result 59 | * @param[in] index Byte index where the parsing error was found (0-based). 60 | */ 61 | typedef void (*NuCLIParseErrorCallback_t)(NuCLIParsingResult_t, size_t); 62 | 63 | /** 64 | * @brief Parse and execute simple commands 65 | * 66 | */ 67 | class NuCLIParser 68 | { 69 | public: 70 | /** 71 | * @brief Enable or disable case-sensitive command names 72 | * 73 | * @param[in] yesOrNo True for case-sensitive. False, otherwise. 74 | * @return true Previously, case-sensitive. 75 | * @return false Previously, case-insensitive. 76 | */ 77 | bool caseSensitive(bool yesOrNo) noexcept; 78 | 79 | /** 80 | * @brief Set a callback for a command name 81 | * 82 | * @note If you set two or more callbacks for the same command name, 83 | * just the first one will be executed, so don't do that. 84 | * 85 | * @note Example: 86 | * @code {.cpp} 87 | * NuShellCommands 88 | * .on("mycmd", [](NuCommandLine_t &commandLine) 89 | * { ...do something...}) 90 | * .onUnknown([](NuCommandLine_t &commandLine) 91 | * { ...do something else...}); 92 | * 93 | * @endcode 94 | * 95 | * @param[in] commandName Command name 96 | * @param[in] callback Function to execute if @p commandName is found 97 | * 98 | * @return NuCLIParser& This instance. Used to chain calls. 99 | */ 100 | NuCLIParser &on(const ::std::string commandName, NuCLICommandCallback_t callback) noexcept; 101 | 102 | /** 103 | * @brief Set a callback for unknown commands 104 | * 105 | * @param[in] callback Function to execute if the parsed command line contains 106 | * an unknown command name. 107 | * 108 | * @return NuCLIParser& This instance. Used to chain calls. 109 | */ 110 | NuCLIParser &onUnknown(NuCLICommandCallback_t callback) noexcept 111 | { 112 | cbUnknown = callback; 113 | return *this; 114 | }; 115 | 116 | /** 117 | * @brief Set a callback for parsing errors 118 | * 119 | * @param[in] callback Function to execute if some parsing error is found. 120 | * @return NuCLIParser& This instance. Used to chain calls. 121 | */ 122 | NuCLIParser &onParseError(NuCLIParseErrorCallback_t callback) noexcept 123 | { 124 | cbParseError = callback; 125 | return *this; 126 | }; 127 | 128 | /** 129 | * @brief Execute the given command line 130 | * 131 | * @param commandLine Pointer to a buffer containing a command line 132 | * @param size Size in bytes of @p commandLine 133 | */ 134 | void execute(const uint8_t *commandLine, size_t size); 135 | 136 | /** 137 | * @brief Execute the given command line 138 | * 139 | * @param commandLine String containing command line 140 | */ 141 | void execute(::std::string commandLine) 142 | { 143 | execute((const uint8_t *)commandLine.data(), commandLine.length()); 144 | }; 145 | 146 | /** 147 | * @brief Execute the given command line 148 | * 149 | * @param commandLine Null-terminated string containing a command line 150 | */ 151 | void execute(const char *commandLine) 152 | { 153 | if (commandLine) 154 | execute((const uint8_t *)commandLine, strlen(commandLine)); 155 | }; 156 | 157 | protected: 158 | static NuCLIParsingResult_t parse(const uint8_t *in, size_t size, size_t &index, NuCommandLine_t &parsedCommandLine); 159 | static NuCLIParsingResult_t parseNext(const uint8_t *in, size_t size, size_t &index, NuCommandLine_t &parsedCommandLine); 160 | static void ignoreSeparator(const uint8_t *in, size_t size, size_t &index); 161 | static bool isSeparator(const uint8_t *in, size_t size, size_t index); 162 | 163 | /** 164 | * @brief Notify successfully parsed command line 165 | * 166 | * @note Current implementation executes the appropiate callback. 167 | * Override for custom command processing if you don't like callbacks. 168 | * 169 | * @param[in] commandLine Parsed command line 170 | */ 171 | virtual void onParsingSuccess(NuCommandLine_t &commandLine) noexcept; 172 | 173 | /** 174 | * @brief Notify parsing error 175 | * 176 | * @note Current implementation executes onParseError() callback. 177 | * Override for custom error processing. 178 | * 179 | * @param[in] result Parsing result 180 | * @param[in] index Byte index where the parsing error was found (0-based). 181 | */ 182 | virtual void onParsingFailure(NuCLIParsingResult_t result, size_t index) noexcept; 183 | 184 | private: 185 | bool bCaseSensitive = false; 186 | NuCLIParseErrorCallback_t cbParseError = nullptr; 187 | NuCLICommandCallback_t cbUnknown = nullptr; 188 | ::std::vector<::std::string> vsCommandName; 189 | ::std::vector vcbCommand; 190 | }; 191 | 192 | #endif -------------------------------------------------------------------------------- /src/NuCLIParser.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file NuCLIParser.cpp 3 | * @author Ángel Fernández Pineda. Madrid. Spain. 4 | * @date 2023-12-26 5 | * @brief Simple command parser 6 | * 7 | * @copyright Creative Commons Attribution 4.0 International (CC BY 4.0) 8 | * 9 | */ 10 | 11 | #include 12 | #include 13 | #include 14 | // #include 15 | #include "NuCLIParser.hpp" 16 | 17 | //----------------------------------------------------------------------------- 18 | // Set callbacks 19 | //----------------------------------------------------------------------------- 20 | 21 | NuCLIParser &NuCLIParser::on(const ::std::string commandName, NuCLICommandCallback_t callback) noexcept 22 | { 23 | if (callback && (commandName.length() > 0)) 24 | { 25 | vsCommandName.push_back(commandName); 26 | vcbCommand.push_back(callback); 27 | } 28 | return *this; 29 | } 30 | 31 | //----------------------------------------------------------------------------- 32 | // Auxiliary. Taken from an example at O'Really book 33 | //----------------------------------------------------------------------------- 34 | 35 | inline bool caseInsCharCompareN(char a, char b) 36 | { 37 | return (toupper(a) == toupper(b)); 38 | } 39 | 40 | // Future work ? 41 | // inline bool caseInsCharCompareW(wchar_t a, wchar_t b) 42 | // { 43 | // return (towupper(a) == towupper(b)); 44 | // } 45 | 46 | bool caseInsCompare(const ::std::string &s1, const ::std::string &s2) 47 | { 48 | return ((s1.size() == s2.size()) && 49 | equal(s1.begin(), s1.end(), s2.begin(), caseInsCharCompareN)); 50 | } 51 | 52 | // Future work ? 53 | // bool caseInsCompare(const wstring &s1, const wstring &s2) 54 | // { 55 | // return ((s1.size() == s2.size()) && 56 | // equal(s1.begin(), s1.end(), s2.begin(), caseInsCharCompareW)); 57 | // } 58 | 59 | //----------------------------------------------------------------------------- 60 | // Execute 61 | //----------------------------------------------------------------------------- 62 | 63 | void NuCLIParser::execute(const uint8_t *commandLine, size_t size) 64 | { 65 | if ((vcbCommand.size() == 0) && (!cbUnknown)) 66 | { 67 | onParsingFailure(CLI_PR_NO_CALLBACKS, 0); 68 | return; 69 | } 70 | 71 | NuCommandLine_t parsedCommandLine; 72 | size_t index = 0; 73 | NuCLIParsingResult_t parsingResult = parse(commandLine, size, index, parsedCommandLine); 74 | if (parsingResult == CLI_PR_OK) 75 | { 76 | if (parsedCommandLine.size() == 0) 77 | onParsingFailure(CLI_PR_NO_COMMAND, 0); 78 | else 79 | onParsingSuccess(parsedCommandLine); 80 | } 81 | else 82 | onParsingFailure(parsingResult, index); 83 | } 84 | 85 | //----------------------------------------------------------------------------- 86 | 87 | void NuCLIParser::onParsingSuccess(NuCommandLine_t &commandLine) noexcept 88 | { 89 | ::std::string &givenCommandName = commandLine[0]; 90 | for (size_t index = 0; index < vsCommandName.size(); index++) 91 | { 92 | ::std::string &candidate = vsCommandName.at(index); 93 | bool test; 94 | if (bCaseSensitive) 95 | test = (candidate.compare(givenCommandName) == 0); 96 | else 97 | test = caseInsCompare(givenCommandName, candidate); 98 | if (test) 99 | { 100 | NuCLICommandCallback_t cb = vcbCommand.at(index); 101 | cb(commandLine); 102 | return; 103 | } 104 | } 105 | if (cbUnknown) 106 | cbUnknown(commandLine); 107 | } 108 | 109 | //----------------------------------------------------------------------------- 110 | 111 | void NuCLIParser::onParsingFailure(NuCLIParsingResult_t result, size_t index) noexcept 112 | { 113 | if (cbParseError) 114 | cbParseError(result, index); 115 | } 116 | 117 | //----------------------------------------------------------------------------- 118 | // Parse 119 | //----------------------------------------------------------------------------- 120 | 121 | NuCLIParsingResult_t NuCLIParser::parse(const uint8_t *in, size_t size, size_t &index, NuCommandLine_t &parsedCommandLine) 122 | { 123 | NuCLIParsingResult_t result = CLI_PR_OK; 124 | while ((index < size) && (result == CLI_PR_OK)) 125 | { 126 | ignoreSeparator(in, size, index); 127 | result = parseNext(in, size, index, parsedCommandLine); 128 | } 129 | return result; 130 | } 131 | 132 | NuCLIParsingResult_t NuCLIParser::parseNext(const uint8_t *in, size_t size, size_t &index, NuCommandLine_t &parsedCommandLine) 133 | { 134 | if (index < size) 135 | { 136 | ::std::string current = ""; 137 | if (in[index] == '\"') 138 | { 139 | // Quoted string 140 | index++; 141 | bool openString = true; 142 | while ((index < size) && openString) 143 | { 144 | if (in[index] == '\"') 145 | { 146 | index++; 147 | if ((index < size) && (in[index] == '\"')) 148 | { 149 | // Escaped double quotes 150 | current.push_back('\"'); 151 | index++; 152 | } 153 | else 154 | // Closing double quotes 155 | openString = false; 156 | } 157 | else 158 | current.push_back(in[index++]); 159 | } 160 | if (openString || !isSeparator(in, size, index)) 161 | { 162 | // No closing double quotes or text after closing double quotes 163 | return CLI_PR_ILL_FORMED_STRING; 164 | } 165 | } 166 | else 167 | { 168 | // Unquoted string 169 | while (!isSeparator(in, size, index)) 170 | { 171 | current.push_back(in[index++]); 172 | } 173 | } 174 | parsedCommandLine.push_back(current); 175 | } 176 | return CLI_PR_OK; 177 | } 178 | 179 | bool NuCLIParser::isSeparator(const uint8_t *in, size_t size, size_t index) 180 | { 181 | if (index < size) 182 | return ((in[index] == ' ') || (in[index] == '\r') || (in[index] == '\n')); 183 | else 184 | return true; 185 | } 186 | 187 | void NuCLIParser::ignoreSeparator(const uint8_t *in, size_t size, size_t &index) 188 | { 189 | while ((index < size) && ((in[index] == ' ') || (in[index] == '\r') || (in[index] == '\n'))) 190 | index++; 191 | } 192 | 193 | //----------------------------------------------------------------------------- 194 | // Other 195 | //----------------------------------------------------------------------------- 196 | 197 | bool NuCLIParser::caseSensitive(bool yesOrNo) noexcept 198 | { 199 | bool result = bCaseSensitive; 200 | bCaseSensitive = yesOrNo; 201 | return result; 202 | }; -------------------------------------------------------------------------------- /examples/ATCommandDemo/ATCommandDemo.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * @file ATCommandDemo.ino 3 | * @author Ángel Fernández Pineda. Madrid. Spain. 4 | * @date 2024-08-21 5 | * @brief Example of an AT command processor based on 6 | * the Nordic UART Service 7 | * 8 | * @note See examples/README.md for a description 9 | * 10 | * @copyright Creative Commons Attribution 4.0 International (CC BY 4.0) 11 | * 12 | */ 13 | 14 | #include 15 | #include "NuATCommands.hpp" 16 | #include 17 | #include 18 | #include 19 | 20 | #define DEVICE_NAME "AT commands demo" 21 | 22 | //------------------------------------------------------ 23 | // Auxiliary 24 | //------------------------------------------------------ 25 | 26 | void printNumberATResponse(intmax_t number) 27 | { 28 | NuATCommands.printATResponse(std::to_string(number)); 29 | } 30 | 31 | bool strToIntMax(const std::string text, intmax_t &number) 32 | { 33 | try 34 | { 35 | number = std::stoll(text); 36 | return true; 37 | } 38 | catch (std::exception &e) 39 | { 40 | return false; 41 | } 42 | } 43 | 44 | //------------------------------------------------------ 45 | // AT Commands implementation for a simple calculator 46 | //------------------------------------------------------ 47 | 48 | intmax_t op1 = 0LL; 49 | intmax_t op2 = 0LL; 50 | 51 | NuATCommandResult_t onVersion(NuATCommandParameters_t ¶meters) 52 | { 53 | NuATCommands.printATResponse("Version 2.0 (fictional)"); 54 | return NuATCommandResult_t::AT_RESULT_OK; 55 | } 56 | 57 | NuATCommandResult_t onAdd(NuATCommandParameters_t ¶meters) 58 | { 59 | printNumberATResponse(op1 + op2); 60 | return NuATCommandResult_t::AT_RESULT_OK; 61 | } 62 | 63 | NuATCommandResult_t onSub(NuATCommandParameters_t ¶meters) 64 | { 65 | printNumberATResponse(op1 - op2); 66 | return NuATCommandResult_t::AT_RESULT_OK; 67 | } 68 | 69 | NuATCommandResult_t onMult(NuATCommandParameters_t ¶meters) 70 | { 71 | printNumberATResponse(op1 * op2); 72 | return NuATCommandResult_t::AT_RESULT_OK; 73 | } 74 | 75 | NuATCommandResult_t onDiv(NuATCommandParameters_t ¶meters) 76 | { 77 | if (op2 != 0LL) 78 | { 79 | printNumberATResponse(op1 / op2); 80 | return AT_RESULT_OK; 81 | } 82 | return NuATCommandResult_t::AT_RESULT_ERROR; 83 | } 84 | 85 | NuATCommandResult_t onSetOp1(NuATCommandParameters_t ¶meters) 86 | { 87 | if ((parameters.size() == 1) && strToIntMax(parameters.at(0), op1)) 88 | return AT_RESULT_OK; 89 | else 90 | return AT_RESULT_INVALID_PARAM; 91 | } 92 | 93 | NuATCommandResult_t onSetOp2(NuATCommandParameters_t ¶meters) 94 | { 95 | if ((parameters.size() == 1) && strToIntMax(parameters.at(0), op2)) 96 | return AT_RESULT_OK; 97 | else 98 | return AT_RESULT_INVALID_PARAM; 99 | } 100 | 101 | NuATCommandResult_t onSetOperands(NuATCommandParameters_t ¶meters) 102 | { 103 | if ((parameters.size() == 2) && 104 | strToIntMax(parameters.at(0), op1) && 105 | strToIntMax(parameters.at(1), op2)) 106 | return AT_RESULT_OK; 107 | else 108 | return AT_RESULT_INVALID_PARAM; 109 | } 110 | 111 | NuATCommandResult_t onQueryOp1(NuATCommandParameters_t ¶meters) 112 | { 113 | printNumberATResponse(op1); 114 | return AT_RESULT_OK; 115 | } 116 | 117 | NuATCommandResult_t onQueryOp2(NuATCommandParameters_t ¶meters) 118 | { 119 | printNumberATResponse(op1); 120 | return AT_RESULT_OK; 121 | } 122 | 123 | NuATCommandResult_t onQueryOperands(NuATCommandParameters_t ¶meters) 124 | { 125 | printNumberATResponse(op1); 126 | printNumberATResponse(op2); 127 | return AT_RESULT_OK; 128 | } 129 | 130 | NuATCommandResult_t onTestOp1(NuATCommandParameters_t ¶meters) 131 | { 132 | NuATCommands.printATResponse("+A: (integer)"); 133 | return AT_RESULT_OK; 134 | } 135 | 136 | NuATCommandResult_t onTestOp2(NuATCommandParameters_t ¶meters) 137 | { 138 | NuATCommands.printATResponse("+B: (integer)"); 139 | return AT_RESULT_OK; 140 | } 141 | 142 | NuATCommandResult_t onTestOperands(NuATCommandParameters_t ¶meters) 143 | { 144 | NuATCommands.printATResponse("+OP: (integer),(integer)"); 145 | return AT_RESULT_OK; 146 | } 147 | 148 | //------------------------------------------------------ 149 | // logging 150 | //------------------------------------------------------ 151 | 152 | void logError(const std::string text, NuATSyntaxError_t errorCode) 153 | { 154 | Serial.printf("ERROR LOG. Code %d at \"%s\"\n", errorCode, text.c_str()); 155 | } 156 | 157 | void logMessage(const uint8_t *text, size_t size) 158 | { 159 | std::string msg; 160 | msg.assign((const char *)text, size); 161 | Serial.printf("NON-AT message: %s\n", msg.c_str()); 162 | } 163 | 164 | //------------------------------------------------------ 165 | // Arduino entry points 166 | //------------------------------------------------------ 167 | 168 | void setup() 169 | { 170 | // Initialize serial monitor 171 | Serial.begin(115200); 172 | Serial.println("*******************************"); 173 | Serial.println(" BLE AT command processor demo "); 174 | Serial.println("*******************************"); 175 | Serial.println("--Initializing--"); 176 | 177 | // Initialize BLE and Nordic UART service 178 | NimBLEDevice::init(DEVICE_NAME); 179 | NimBLEDevice::getAdvertising()->setName(DEVICE_NAME); 180 | NuATCommands.maxCommandLineLength(64); 181 | NuATCommands.allowLowerCase(true); 182 | NuATCommands.stopOnFirstFailure(true); 183 | NuATCommands 184 | .onSet("a", onSetOp1) 185 | .onSet("b", onSetOp2) 186 | .onSet("op", onSetOperands); 187 | NuATCommands 188 | .onExecute("v", onVersion) 189 | .onExecute("add", onAdd) 190 | .onExecute("sum", onAdd) 191 | .onExecute("sub", onSub) 192 | .onExecute("subtract", onSub) 193 | .onExecute("mult", onMult) 194 | .onExecute("div", onDiv); 195 | NuATCommands 196 | .onQuery("v", onVersion) 197 | .onQuery("add", onAdd) 198 | .onQuery("sum", onAdd) 199 | .onQuery("sub", onSub) 200 | .onQuery("subtract", onSub) 201 | .onQuery("mult", onMult) 202 | .onQuery("div", onDiv) 203 | .onQuery("a", onQueryOp1) 204 | .onQuery("b", onQueryOp2) 205 | .onQuery("op", onQueryOperands); 206 | NuATCommands 207 | .onTest("a", onTestOp1) 208 | .onTest("b", onTestOp1) 209 | .onTest("op", onTestOperands); 210 | 211 | NuATCommands 212 | .onError(logError) 213 | .onNotACommandLine(logMessage); 214 | 215 | NuATCommands.start(); 216 | 217 | // Initialization complete 218 | Serial.println("--Ready--"); 219 | } 220 | 221 | void loop() 222 | { 223 | // Incoming data is processed in another task created by the BLE stack, 224 | // so there is nothing to do here (in this demo) 225 | Serial.println("--Running (heart beat each 30 seconds)--"); 226 | delay(30000); 227 | } -------------------------------------------------------------------------------- /extras/test/SimpleCommandTester/SimpleCommandTester.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Ángel Fernández Pineda. Madrid. Spain. 3 | * @date 2023-12-27 4 | * @brief Automated test 5 | * 6 | * @copyright Creative Commons Attribution 4.0 International (CC BY 4.0) 7 | * 8 | */ 9 | 10 | #include 11 | #include 12 | #include "NuCLIParser.hpp" 13 | 14 | //----------------------------------------------------------------------------- 15 | // C++20 compatibility 16 | // See: https://stackoverflow.com/questions/56833000/c20-with-u8-char8-t-and-stdstring 17 | //----------------------------------------------------------------------------- 18 | 19 | std::string from_u8string(const std::string &s) 20 | { 21 | return s; 22 | } 23 | 24 | std::string from_u8string(std::string &&s) 25 | { 26 | return std::move(s); 27 | } 28 | 29 | #if defined(__cpp_lib_char8_t) 30 | std::string from_u8string(const std::u8string &s) 31 | { 32 | return std::string(s.begin(), s.end()); 33 | } 34 | #endif 35 | 36 | //----------------------------------------------------------------------------- 37 | // MOCK 38 | //----------------------------------------------------------------------------- 39 | 40 | NuCLIParser tester; 41 | NuCLIParsingResult_t lastParsingResult; 42 | bool testExecution = false; 43 | bool testParseCallback = false; 44 | NuCommandLine_t expectedCmdLine; 45 | bool testCallbackExecuted = false; 46 | 47 | #define TEST_COMMAND "testCmd" 48 | #define TEST_COMMAND_IGNORE_CASE "TESTcMD" 49 | 50 | void initializeTester() 51 | { 52 | tester 53 | .on(TEST_COMMAND, [](NuCommandLine_t &commandLine) 54 | { testCallbackExecuted = true; }) 55 | .onUnknown([](NuCommandLine_t &commandLine) 56 | { 57 | lastParsingResult = CLI_PR_OK; 58 | if (testExecution) 59 | { 60 | if (commandLine.size() == expectedCmdLine.size()) 61 | { 62 | for (int index = 0; index < commandLine.size(); index++) 63 | { 64 | std::string expected = expectedCmdLine[index]; 65 | std::string found = commandLine[index]; 66 | bool test = (expected.compare(found) != 0); 67 | if (test) 68 | { 69 | Serial.printf(" --Failure at string index #%d. Expected: [%s] Found: [%s]\n", index, expected.c_str(), found.c_str()); 70 | } 71 | } 72 | } 73 | else 74 | { 75 | Serial.printf(" --Failure at strings count. Expected: %d Found: %d\n", expectedCmdLine.size(), commandLine.size()); 76 | } 77 | } }) 78 | .onParseError([](NuCLIParsingResult_t result, size_t byteIndex) 79 | { 80 | lastParsingResult = result; 81 | if (testParseCallback) 82 | { 83 | Serial.printf("onParseError(%d)", result); 84 | } }); 85 | } 86 | 87 | void reset() 88 | { 89 | testExecution = false; 90 | testParseCallback = false; 91 | testCallbackExecuted = false; 92 | } 93 | 94 | //----------------------------------------------------------------------------- 95 | // Test macros 96 | //----------------------------------------------------------------------------- 97 | 98 | void Test_parsingResult(std::string line, NuCLIParsingResult_t parsingResult) 99 | { 100 | reset(); 101 | tester.execute(line); 102 | if (lastParsingResult != parsingResult) 103 | { 104 | Serial.printf("Parsing failure at [%s]. Expected code: %d. Found code: %d\n", line.c_str(), parsingResult, lastParsingResult); 105 | } 106 | } 107 | 108 | void Test_execution(std::string line) 109 | { 110 | reset(); 111 | testExecution = true; 112 | Serial.printf("--Executing: [%s]\n", line.c_str()); 113 | tester.execute(line); 114 | if (lastParsingResult != CLI_PR_OK) 115 | { 116 | Serial.printf("Failure. Unexpected parsing result code: %d\n", lastParsingResult); 117 | } 118 | } 119 | 120 | void Test_callback(std::string line, bool expected) 121 | { 122 | reset(); 123 | tester.execute(line); 124 | if (expected != testCallbackExecuted) 125 | { 126 | Serial.printf("Callback execution failure at [%s]. Expected: %d. Found: %d\n", line.c_str(), expected, testCallbackExecuted); 127 | } 128 | } 129 | 130 | //----------------------------------------------------------------------------- 131 | // Arduino entry points 132 | //----------------------------------------------------------------------------- 133 | 134 | void setup() 135 | { 136 | // Initialize serial monitor 137 | Serial.begin(115200); 138 | Serial.println("**************************************************"); 139 | Serial.println(" Automated test for simple command processor "); 140 | Serial.println("**************************************************"); 141 | initializeTester(); 142 | 143 | Test_parsingResult("", CLI_PR_NO_COMMAND); 144 | Test_parsingResult(" \n", CLI_PR_NO_COMMAND); 145 | Test_parsingResult(" abc de", CLI_PR_OK); 146 | Test_parsingResult("abc de \n", CLI_PR_OK); 147 | Test_parsingResult(" abc de \n", CLI_PR_OK); 148 | Test_parsingResult(" \"abc\" ", CLI_PR_OK); 149 | Test_parsingResult(" abc\"abc ", CLI_PR_OK); 150 | Test_parsingResult("\"abc\"", CLI_PR_OK); 151 | Test_parsingResult("\"abc\"\n", CLI_PR_OK); 152 | Test_parsingResult(" \"abc \"\"def\"\" abc \" ", CLI_PR_OK); 153 | Test_parsingResult("\"unterminated string\n", CLI_PR_ILL_FORMED_STRING); 154 | Test_parsingResult("\"unterminated \"\" string\n", CLI_PR_ILL_FORMED_STRING); 155 | Test_parsingResult("\"text\"after quotes\n", CLI_PR_ILL_FORMED_STRING); 156 | Test_parsingResult("\"text\"\"\"after quotes\n", CLI_PR_ILL_FORMED_STRING); 157 | Test_parsingResult("\"text \"\"___\"\" \"after quotes\n", CLI_PR_ILL_FORMED_STRING); 158 | 159 | expectedCmdLine.clear(); 160 | expectedCmdLine.push_back(from_u8string(u8"áéí")); 161 | Test_execution(from_u8string(u8"áéí")); 162 | expectedCmdLine.push_back(from_u8string(u8"áéí")); 163 | Test_execution(from_u8string(u8"áéí\náéí\n")); 164 | 165 | expectedCmdLine.clear(); 166 | expectedCmdLine.push_back("abc"); 167 | 168 | Test_execution("abc"); 169 | Test_execution("abc\n"); 170 | Test_execution(" abc\n"); 171 | Test_execution(" abc "); 172 | 173 | expectedCmdLine.push_back("cde"); 174 | 175 | Test_execution("abc cde"); 176 | Test_execution("abc cde \n"); 177 | Test_execution("abc \"cde\" "); 178 | Test_execution(" \"abc\" \"cde\" "); 179 | Test_execution("\"abc\" cde"); 180 | Test_execution("abc\ncde"); 181 | 182 | expectedCmdLine.push_back("123 456"); 183 | 184 | Test_execution("abc cde \"123 456\" \n"); 185 | 186 | expectedCmdLine.push_back(".\"xyz\"."); 187 | 188 | Test_execution("abc cde \"123 456\" \".\"\"xyz\"\".\""); 189 | 190 | tester.caseSensitive(true); 191 | Test_callback(TEST_COMMAND, true); 192 | Test_callback(TEST_COMMAND_IGNORE_CASE, false); 193 | tester.caseSensitive(false); 194 | Test_callback(TEST_COMMAND, true); 195 | Test_callback(TEST_COMMAND_IGNORE_CASE, true); 196 | 197 | Serial.println("**************************************************"); 198 | Serial.println("END"); 199 | Serial.println("**************************************************"); 200 | } 201 | 202 | void loop() 203 | { 204 | delay(30000); 205 | } -------------------------------------------------------------------------------- /examples/CustomCommandProcessor/CustomCommandProcessor.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Ángel Fernández Pineda. Madrid. Spain. 3 | * @date 2023-12-18 4 | * @brief Example of a custom command processor 5 | * based on the Nordic UART Service 6 | * 7 | * @note See examples/README.md for a description 8 | * 9 | * @copyright Creative Commons Attribution 4.0 International (CC BY 4.0) 10 | * 11 | */ 12 | 13 | #include 14 | #include "NuS.hpp" 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | #define DEVICE_NAME "Custom commands demo" 21 | /** 22 | * @brief Custom command processor class 23 | * 24 | */ 25 | class CustomCommandProcessor : public NordicUARTService 26 | { 27 | protected: 28 | // Do not get confused by this method's name. 29 | // Data is received here. 30 | void onWrite( 31 | NimBLECharacteristic *pCharacteristic, 32 | NimBLEConnInfo &connInfo) override; 33 | 34 | private: 35 | // Methods that execute received commands 36 | void onExitCommand(); 37 | void onSumCommand(char *param1, char *param2); 38 | } server; 39 | 40 | //------------------------------------------------------ 41 | // Globals 42 | //------------------------------------------------------ 43 | 44 | // Internal buffer size required for command parsing 45 | #define BUFFER_SIZE 64 46 | 47 | // Messages for the BLE peer 48 | #define MSG_UNKNOWN_CMD "ERROR: Unknown command. Valid commands are (case sensitive) \"sum \" and \"exit\"\n" 49 | #define MSG_TOO_LONG "ERROR: command line too long\n" 50 | #define MSG_PARAM_ERROR "ERROR: a parameter is not a valid integer\n" 51 | #define MSG_UNEXPECTED_ERROR "ERROR: unexpected failure. Sorry.\n" 52 | #define MSG_GOODBYE "See you later\n" 53 | 54 | // Command names 55 | #define CMD_SUM "sum" 56 | #define CMD_EXIT "exit" 57 | 58 | //------------------------------------------------------ 59 | // CustomCommandProcessor implementation 60 | //------------------------------------------------------ 61 | 62 | /** 63 | * @brief Split a string into two parts separated by blank spaces 64 | * 65 | * @param[in/out] string On entry, the string to split. 66 | * On exit, the substring at the left side 67 | * @return char* The substring at the right side. May be empty. 68 | */ 69 | char *split(char *string) 70 | { 71 | char *rightSide = string; 72 | // find first blank space or null 73 | while ((rightSide[0] != ' ') && (rightSide[0] != '\0')) 74 | rightSide++; 75 | if (rightSide[0] != '\0') 76 | { 77 | rightSide[0] = '\0'; 78 | rightSide++; 79 | // ignore continuous blank spaces 80 | while (rightSide[0] == ' ') 81 | rightSide++; 82 | } 83 | return rightSide; 84 | } 85 | 86 | /** 87 | * @brief Parse incoming data 88 | * 89 | */ 90 | void CustomCommandProcessor::onWrite( 91 | NimBLECharacteristic *pCharacteristic, 92 | NimBLEConnInfo &connInfo) 93 | { 94 | // convert data to a null-terminated string 95 | const char *data = pCharacteristic->getValue().c_str(); 96 | Serial.printf("--Incoming text line:\n%s\n", data); 97 | 98 | // Preliminary check to discard wrong commands early 99 | // and simplify further processing 100 | auto dataLen = strlen(data); 101 | if (dataLen > BUFFER_SIZE) 102 | { 103 | send(MSG_TOO_LONG); 104 | return; 105 | } 106 | else if ((dataLen < 4) || (data[0] < 'a') || (data[0] > 'z')) 107 | { 108 | send(MSG_UNKNOWN_CMD); 109 | return; 110 | } 111 | 112 | // Since data is "const", we need a modifiable buffer 113 | // to parse each parameter (commandLine). 114 | // A null terminating character will be inserted after the 115 | // command name and after each parameter. 116 | 117 | // Copy string from "data" to "commandLine" 118 | char commandLine[BUFFER_SIZE + 1]; 119 | strncpy(commandLine, data, BUFFER_SIZE); 120 | commandLine[BUFFER_SIZE] = '\0'; 121 | 122 | // Substitute unwanted characters with blank spaces 123 | // (unwanted characters are, mostly, line feeds and carriage returns) 124 | for (int i = 0; (i < BUFFER_SIZE) && (commandLine[i] != '\0'); i++) 125 | if ((commandLine[i] < ' ') || (commandLine[i] > 'z')) 126 | commandLine[i] = ' '; 127 | 128 | // Separate command name from first parameter (if any) 129 | char *firstParam = split(commandLine); 130 | 131 | // Decode command 132 | if (strcmp(commandLine, CMD_EXIT) == 0) 133 | { 134 | Serial.printf("--Processing \"%s\"\n", CMD_EXIT); 135 | onExitCommand(); 136 | return; 137 | } 138 | else if (strcmp(commandLine, CMD_SUM) == 0) 139 | { 140 | char *secondParam = split(firstParam); 141 | Serial.printf("--Processing \"%s %s %s\"\n", CMD_SUM, firstParam, secondParam); 142 | onSumCommand(firstParam, secondParam); 143 | return; 144 | } 145 | else 146 | { 147 | Serial.printf("--Command %s NOT processed\n", commandLine); 148 | send(MSG_UNKNOWN_CMD); 149 | } 150 | } 151 | 152 | //------------------------------------------------------ 153 | // Command execution 154 | //------------------------------------------------------ 155 | 156 | void CustomCommandProcessor::onExitCommand() 157 | { 158 | send(MSG_GOODBYE); 159 | disconnect(); 160 | } 161 | 162 | void CustomCommandProcessor::onSumCommand(char *param1, char *param2) 163 | { 164 | // Convert string parameters into integer values 165 | 166 | // "errno" is used to detect non-integer data 167 | // errno==0 means no error 168 | errno = 0; 169 | intmax_t n1 = 0; 170 | intmax_t n2 = 0; 171 | n1 = strtoimax(param1, NULL, 10); // convert first parameter 172 | if (!errno) 173 | n2 = strtoimax(param2, NULL, 10); // convert second parameter 174 | if (errno) 175 | { 176 | send(MSG_PARAM_ERROR); 177 | return; 178 | } 179 | else 180 | { 181 | // Execute command and send result to the BLE peer 182 | 183 | auto sum = n1 + n2; // command result 184 | Serial.printf("(sum is %lld)\n", sum); 185 | // convert command result to string in a private buffer 186 | char output[BUFFER_SIZE]; 187 | int t = snprintf(output, BUFFER_SIZE, "Sum is %lld\n", sum); 188 | if ((t >= 0) && (t < BUFFER_SIZE)) 189 | { 190 | // Transmit result 191 | send(output); 192 | } 193 | else 194 | { 195 | // Buffer is too small (t>=BUFFER_SIZE) or encoding error (t<0) 196 | send(MSG_UNEXPECTED_ERROR); 197 | Serial.printf("ERROR at onSumCommand()-->snprintf(): return code %d. Increase buffer size >%d.\n", t, BUFFER_SIZE); 198 | } 199 | } 200 | } 201 | 202 | //------------------------------------------------------ 203 | // Arduino entry points 204 | //------------------------------------------------------ 205 | 206 | void setup() 207 | { 208 | // Initialize serial monitor 209 | Serial.begin(115200); 210 | Serial.println("***********************************"); 211 | Serial.println(" BLE custom command processor demo "); 212 | Serial.println("***********************************"); 213 | Serial.println("--Initializing--"); 214 | 215 | // Initialize BLE and Nordic UART service 216 | NimBLEDevice::init(DEVICE_NAME); 217 | NimBLEDevice::getAdvertising()->setName(DEVICE_NAME); 218 | server.start(); 219 | 220 | // Initialization complete 221 | Serial.println("--Ready--"); 222 | } 223 | 224 | void loop() 225 | { 226 | // Incoming data is processed in another task created by the BLE stack, 227 | // so there is nothing to do here (in this demo) 228 | Serial.println("--Running (heart beat each 30 seconds)--"); 229 | delay(30000); 230 | } -------------------------------------------------------------------------------- /src/NuS.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file NuS.hpp 3 | * @author Ángel Fernández Pineda. Madrid. Spain. 4 | * @date 2023-12-18 5 | * @brief Nordic UART Service implementation on NimBLE stack 6 | * 7 | * @note NimBLE-Arduino library is required. 8 | * https://github.com/h2zero/NimBLE-Arduino 9 | * 10 | * @copyright Creative Commons Attribution 4.0 International (CC BY 4.0) 11 | * 12 | */ 13 | #ifndef __NUS_NIMBLE_HPP__ 14 | #define __NUS_NIMBLE_HPP__ 15 | 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | #if __cplusplus < 202002L 22 | // NOTE: ::std::binary_semaphore is not available in c++17. 23 | // This is a workaround 24 | #include "cyan_semaphore.h" 25 | typedef ::cyan::binary_semaphore nus_semaphore; 26 | #else 27 | #include 28 | typedef ::std::binary_semaphore nus_semaphore; 29 | #endif 30 | 31 | /** 32 | * @brief UUID for the Nordic UART Service 33 | * 34 | * @note You may need this to handle advertising on your own 35 | */ 36 | #define NORDIC_UART_SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" 37 | 38 | /** 39 | * @brief Nordic UART Service (NuS) implementation using the NimBLE stack 40 | * 41 | * @note This is an abstract class. 42 | * Override NimBLECharacteristicCallbacks::onWrite() 43 | * to process incoming data. A singleton pattern is suggested. 44 | */ 45 | class NordicUARTService : protected NimBLECharacteristicCallbacks 46 | { 47 | public: 48 | /** 49 | * @brief When true, allow multiple instances of the Nordic UART Service 50 | * 51 | * @note False by default. 52 | * When false, your application can not start two or more instances 53 | * of the Nordic UART service. 54 | * This is what most client applications expect. 55 | * 56 | */ 57 | static bool allowMultipleInstances; 58 | 59 | /** 60 | * @brief Check if a peer is connected and subscribed to this service 61 | * 62 | * @return true When a connection is established and 63 | * the peer is subscribed to the Nordic UART TX characteristic 64 | * @return false When no peer is connected or a peer is connected but not subscribed yet. 65 | * Call `NimBLEServer::getConnectedCount()` to know the case. 66 | */ 67 | bool isConnected(); 68 | 69 | /** 70 | * @brief Get the count of clients subscribed 71 | * 72 | * @return uint8_t Number of clients subscribed to the service 73 | */ 74 | size_t subscriberCount() { return _subscriberCount; }; 75 | 76 | /** 77 | * @brief Wait for a peer connection or a timeout if set (blocking) 78 | * 79 | * @param[in] timeoutMillis Maximum time to wait (in milliseconds) or 80 | * zero to disable timeouts and wait forever 81 | * 82 | * @note It is not mandatory to call this method in order to read or write. 83 | * 84 | * @note Just one task can go beyond connect(), except in case of timeout, 85 | * if more than one exists. 86 | * 87 | * @return true on peer connection and service subscription 88 | * @return false on timeout 89 | */ 90 | bool connect(const unsigned int timeoutMillis = 0); 91 | 92 | /** 93 | * @brief Terminate all peer connections (if any), 94 | * subscribed or not. 95 | * 96 | */ 97 | void disconnect(void); 98 | 99 | /** 100 | * @brief Send bytes 101 | * 102 | * @param[in] data Pointer to bytes to be sent. 103 | * @param[in] size Count of bytes to be sent. 104 | * @return size_t @p size . 105 | */ 106 | size_t write(const uint8_t *data, size_t size); 107 | 108 | /** 109 | * @brief Send a null-terminated string (ANSI encoded) 110 | * 111 | * @param[in] str Pointer to null-terminated string to be sent. 112 | * @param[in] includeNullTerminatingChar When true, the null terminating character is sent too. 113 | * When false, such a character is not sent, so @p str should end with another 114 | * termination token, like CR (Unix), LF (old MacOS) or CR+LF (Windows). 115 | * 116 | * @return size_t Count of bytes sent. 117 | */ 118 | size_t send(const char *str, bool includeNullTerminatingChar = false); 119 | 120 | /** 121 | * @brief Send a string (any encoding) 122 | * 123 | * @param str String to send 124 | * @return size_t Count of bytes sent. 125 | */ 126 | size_t print(::std::string str) 127 | { 128 | return write((const uint8_t *)str.data(), str.length()); 129 | }; 130 | 131 | /** 132 | * @brief Send a formatted string (ANSI encoded) 133 | * 134 | * @note The null terminating character is sent too. 135 | * 136 | * @param[in] format String that follows the same specifications as format in printf() 137 | * @param[in] ... Depending on the format string, a sequence of additional arguments, 138 | * each containing a value to replace a format specifier in the format string. 139 | * 140 | * @return size_t Count of bytes sent. 141 | */ 142 | size_t printf(const char *format, ...); 143 | 144 | /** 145 | * @brief Start the Nordic UART Service 146 | * 147 | * @note NimBLEDevice::init() **must** be called in advance. 148 | * @note The service is unavailable if start() is not called. 149 | * Do not call start() before BLE initialization is complete in your application. 150 | * 151 | * @param autoAdvertising True to automatically handle BLE advertising. 152 | * When false, you have to handle advertising on your own. 153 | * 154 | * @throws ::std::runtime_error if the UART service is already created or can not be created 155 | */ 156 | void start(bool autoAdvertising = true); 157 | 158 | /** 159 | * @brief Stop and remove the Nordic UART Service 160 | * 161 | * @note Advertising will be temporarily disabled. 162 | * If you handle advertising on your own, 163 | * you should stop it first and then restart it with your own parameters. 164 | * Otherwise, this function will restart advertising with default parameters. 165 | * 166 | * @warning This will terminate all open connections, 167 | * including peers not using the Nordic UART Service. 168 | * This function is discouraged. 169 | * Design your application so that it is not necessary to stop the service. 170 | * 171 | * @warning This method is not thread-safe 172 | */ 173 | void stop(); 174 | 175 | /** 176 | * @brief Check if the Nordic UART Service is started 177 | * 178 | * @return true If started 179 | * @return false If not started 180 | */ 181 | bool isStarted(); 182 | 183 | protected: 184 | virtual void onSubscribe( 185 | NimBLECharacteristic *pCharacteristic, 186 | NimBLEConnInfo &connInfo, 187 | uint16_t subValue) override; 188 | 189 | protected: 190 | /** 191 | * @brief Event callback for client subscription to the TX characteristic 192 | * 193 | * @note Called before the semaphore is released. 194 | * 195 | * @param subscriberCount Number of subscribed clients 196 | */ 197 | virtual void onSubscribe(size_t subscriberCount) {}; 198 | 199 | /** 200 | * @brief Event callback for client unsubscription to the TX characteristic 201 | * 202 | * @param subscriberCount Number of subscribed clients 203 | */ 204 | virtual void onUnsubscribe(size_t subscriberCount) {}; 205 | 206 | protected: 207 | NordicUARTService() {}; 208 | NordicUARTService(const NordicUARTService &) = delete; 209 | NordicUARTService(NordicUARTService &&) = delete; 210 | NordicUARTService &operator=(const NordicUARTService &) = delete; 211 | NordicUARTService &operator=(NordicUARTService &&) = delete; 212 | virtual ~NordicUARTService() {}; 213 | 214 | private: 215 | NimBLEService *pNus = nullptr; 216 | NimBLECharacteristic *pTxCharacteristic = nullptr; 217 | mutable nus_semaphore peerConnected{0}; 218 | uint32_t _subscriberCount = 0; 219 | 220 | /** 221 | * @brief Create the NuS service in a new or existing GATT server 222 | * 223 | * @param advertise True to add the NuS UUID to the advertised data, false otherwise. 224 | * 225 | * @return NimBLEService internal instance of the NuS 226 | * @throws ::std::runtime_error if the UART service can not be created 227 | */ 228 | 229 | void init(bool advertise); 230 | 231 | /** 232 | * @brief Remove the NuS service 233 | * 234 | * @note Advertising will be temporarily disabled 235 | * 236 | * @warning The service will not be removed until all open connections are closed. 237 | * In the meantime the service will have it's visibility disabled. 238 | * 239 | * @warning This method is not thread-safe 240 | */ 241 | void deinit(); 242 | }; 243 | 244 | #endif -------------------------------------------------------------------------------- /src/NuS.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file NuS.cpp 3 | * @author Ángel Fernández Pineda. Madrid. Spain. 4 | * @date 2023-12-18 5 | * @brief Nordic UART Service implementation on NimBLE stack 6 | * 7 | * @note NimBLE-Arduino library is required. 8 | * https://github.com/h2zero/NimBLE-Arduino 9 | * 10 | * @copyright Creative Commons Attribution 4.0 International (CC BY 4.0) 11 | * 12 | */ 13 | 14 | #include 15 | #include // For runtime_error 16 | #include // For runtime_error 17 | #include 18 | #include // For strlen() 19 | #include // For formatted output 20 | #include // for variadric arguments 21 | #include 22 | #include "NuS.hpp" 23 | 24 | //----------------------------------------------------------------------------- 25 | // Globals 26 | //----------------------------------------------------------------------------- 27 | 28 | #define RX_CHARACTERISTIC_UUID "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" 29 | #define TX_CHARACTERISTIC_UUID "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" 30 | 31 | //----------------------------------------------------------------------------- 32 | // Initialization / Deinitialization 33 | //----------------------------------------------------------------------------- 34 | 35 | bool NordicUARTService::allowMultipleInstances = false; 36 | 37 | void NordicUARTService::init(bool advertise) 38 | { 39 | // Get the server instance or create one 40 | NimBLEServer *pServer = NimBLEDevice::getServer(); 41 | if (pServer == nullptr) 42 | pServer = NimBLEDevice::createServer(); 43 | if (pServer) 44 | { 45 | // Add the service UUID to the advertised data 46 | if (advertise) 47 | pServer->getAdvertising()->addServiceUUID(NORDIC_UART_SERVICE_UUID); 48 | 49 | // Check if there is another service instance 50 | if ((!pServer->getServiceByUUID(NORDIC_UART_SERVICE_UUID) || allowMultipleInstances)) 51 | { 52 | // Create an instance of the service. 53 | // Note that a server can have many instances of the same service 54 | // if `allowMultipleInstances` is true. 55 | pNus = pServer->createService(NORDIC_UART_SERVICE_UUID); 56 | if (pNus) 57 | { 58 | // Create the transmission characteristic 59 | pTxCharacteristic = pNus->createCharacteristic(TX_CHARACTERISTIC_UUID, NIMBLE_PROPERTY::NOTIFY); 60 | if (pTxCharacteristic) 61 | { 62 | pTxCharacteristic->setCallbacks(this); // uses onSubscribe 63 | 64 | // Create the receive characteristic 65 | NimBLECharacteristic *pRxCharacteristic = 66 | pNus->createCharacteristic(RX_CHARACTERISTIC_UUID, NIMBLE_PROPERTY::WRITE); 67 | if (pRxCharacteristic) 68 | { 69 | pRxCharacteristic->setCallbacks(this); // uses onWrite 70 | return; 71 | } 72 | } 73 | } 74 | } 75 | } 76 | // Unable to initialize the service 77 | throw ::std::runtime_error("Unable to create BLE server and/or Nordic UART Service"); 78 | } 79 | 80 | void NordicUARTService::deinit() 81 | { 82 | NimBLEServer *pServer = pNus->getServer(); 83 | bool wasAdvertising = pServer->getAdvertising()->isAdvertising(); 84 | pServer->removeService(pNus, true); 85 | // At this point, the pNus pointer is invalid 86 | pNus = nullptr; 87 | pTxCharacteristic = nullptr; 88 | _subscriberCount = 0; 89 | if (wasAdvertising) 90 | pServer->startAdvertising(); 91 | } 92 | 93 | //----------------------------------------------------------------------------- 94 | // Start/Stop service 95 | //----------------------------------------------------------------------------- 96 | 97 | void NordicUARTService::start(bool autoAdvertising) 98 | { 99 | if (!pNus) 100 | { 101 | init(autoAdvertising); 102 | pNus->start(); 103 | if (autoAdvertising) 104 | { 105 | pNus->getServer()->advertiseOnDisconnect(true); 106 | pNus->getServer()->startAdvertising(); 107 | } 108 | } 109 | } 110 | 111 | void NordicUARTService::stop() 112 | { 113 | if (pNus) 114 | { 115 | disconnect(); 116 | deinit(); 117 | } 118 | } 119 | 120 | bool NordicUARTService::isStarted() 121 | { 122 | return (pNus != nullptr); 123 | } 124 | 125 | //----------------------------------------------------------------------------- 126 | // Connection 127 | //----------------------------------------------------------------------------- 128 | 129 | bool NordicUARTService::isConnected() 130 | { 131 | return (_subscriberCount > 0); 132 | } 133 | 134 | bool NordicUARTService::connect(const unsigned int timeoutMillis) 135 | { 136 | if (timeoutMillis == 0) 137 | { 138 | peerConnected.acquire(); 139 | return true; 140 | } 141 | else 142 | { 143 | return peerConnected.try_acquire_for(::std::chrono::milliseconds(timeoutMillis)); 144 | } 145 | } 146 | 147 | void NordicUARTService::disconnect(void) 148 | { 149 | NimBLEServer *pServer = NimBLEDevice::getServer(); 150 | if (pServer) 151 | { 152 | ::std::vector devices = pServer->getPeerDevices(); 153 | for (uint16_t id : devices) 154 | pServer->disconnect(id); 155 | } 156 | } 157 | 158 | //----------------------------------------------------------------------------- 159 | // TX events 160 | //----------------------------------------------------------------------------- 161 | 162 | void NordicUARTService::onSubscribe( 163 | NimBLECharacteristic *pCharacteristic, 164 | NimBLEConnInfo &connInfo, 165 | uint16_t subValue) 166 | { 167 | // Note: for robustness, we assume this callback could be called 168 | // even if no subscription event exists. 169 | 170 | if (subValue == 0) 171 | { 172 | // unsubscribe 173 | if (_subscriberCount > 0) 174 | { 175 | _subscriberCount--; 176 | onUnsubscribe(_subscriberCount); 177 | } 178 | } 179 | else if (subValue < 4) 180 | { 181 | // subscribe 182 | _subscriberCount++; 183 | onSubscribe(_subscriberCount); 184 | peerConnected.release(); 185 | } 186 | // else: Invalid subscription value, ignore 187 | } 188 | 189 | //----------------------------------------------------------------------------- 190 | // Data transmission 191 | //----------------------------------------------------------------------------- 192 | 193 | size_t NordicUARTService::write(const uint8_t *data, size_t size) 194 | { 195 | if (pTxCharacteristic) 196 | { 197 | // Data is sent in chunks of MTU size to avoid data loss 198 | // as each chunk is notified separately 199 | size_t chunkSize = NimBLEDevice::getMTU(); 200 | size_t remainingByteCount = size; 201 | size_t totalSent = 0; 202 | 203 | while (remainingByteCount >= chunkSize) 204 | { 205 | if (!pTxCharacteristic->notify(data, chunkSize)) 206 | { 207 | // Notify failed - return how much we've sent so far 208 | return totalSent; 209 | } 210 | data += chunkSize; 211 | remainingByteCount -= chunkSize; 212 | totalSent += chunkSize; 213 | } 214 | // Note: remainingByteCount < chunkSize at this point 215 | if ((remainingByteCount > 0) && pTxCharacteristic->notify(data, remainingByteCount)) 216 | totalSent += remainingByteCount; 217 | 218 | return totalSent; 219 | } 220 | else 221 | return 0; 222 | } 223 | 224 | size_t NordicUARTService::send(const char *str, bool includeNullTerminatingChar) 225 | { 226 | if (pTxCharacteristic) 227 | { 228 | size_t size = includeNullTerminatingChar ? strlen(str) + 1 : strlen(str); 229 | return write((uint8_t *)str, size); 230 | } 231 | else 232 | return 0; 233 | } 234 | 235 | size_t NordicUARTService::printf(const char *format, ...) 236 | { 237 | char dummy; 238 | va_list args; 239 | va_start(args, format); 240 | int requiredSize = vsnprintf(&dummy, 1, format, args); 241 | va_end(args); 242 | if (requiredSize == 0) 243 | { 244 | // Write the terminating null character as we have an empty string 245 | return write((uint8_t *)&dummy, 1); 246 | } 247 | else if (requiredSize > 0) 248 | { 249 | char *buffer = (char *)malloc(requiredSize + 1); 250 | if (buffer) 251 | { 252 | va_start(args, format); 253 | int result = vsnprintf(buffer, requiredSize + 1, format, args); 254 | va_end(args); 255 | if ((result >= 0) && (result <= requiredSize)) 256 | { 257 | size_t writtenBytesCount = write((uint8_t *)buffer, result + 1); 258 | free(buffer); 259 | return writtenBytesCount; 260 | } 261 | free(buffer); 262 | } 263 | } 264 | return 0; 265 | } -------------------------------------------------------------------------------- /extras/test/ATCommandsTesterLegacy2/ATCommandsTesterLegacy2.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Ángel Fernández Pineda. Madrid. Spain. 3 | * @date 2023-12-19 4 | * @brief Automated test 5 | * 6 | * @copyright Creative Commons Attribution 4.0 International (CC BY 4.0) 7 | * 8 | */ 9 | 10 | #include "NuATCommandsLegacy2.hpp" 11 | #include 12 | 13 | using namespace NuSLegacy2; 14 | 15 | //----------------------------------------------------------------------------- 16 | // MOCK 17 | //----------------------------------------------------------------------------- 18 | 19 | class NuATCommandTester : public NuATCommandParser, NuATCommandCallbacks 20 | { 21 | public: 22 | NuATCommandResult_t lastResponse; 23 | int id = 0; 24 | bool bExecute = false; 25 | bool bWrite = false; 26 | bool bRead = false; 27 | bool bTest = false; 28 | bool bPrintParams = false; 29 | bool bPrintCmd = false; 30 | 31 | public: 32 | virtual void printATResponse(const char message[]) override 33 | { 34 | Serial.print("\r\n"); 35 | Serial.print(message); 36 | Serial.print("\r\n"); 37 | }; 38 | 39 | virtual void printResultResponse(const NuATCommandResult_t response) override 40 | { 41 | lastResponse = response; 42 | }; 43 | 44 | virtual int getATCommandId(const char commandName[]) override 45 | { 46 | if (bPrintCmd) 47 | Serial.printf("Command: %s\n", commandName); 48 | return id; 49 | }; 50 | 51 | virtual NuATCommandResult_t onExecute(int commandId) override 52 | { 53 | bExecute = true; 54 | return AT_RESULT_OK; 55 | }; 56 | 57 | virtual NuATCommandResult_t onSet(int commandId, NuATCommandParameters_t ¶meters) override 58 | { 59 | if (bPrintParams) 60 | { 61 | int count = 1; 62 | for (const char *param : parameters) 63 | Serial.printf("Parameter %d: %s\n", count++, param); 64 | } 65 | bWrite = true; 66 | return AT_RESULT_OK; 67 | }; 68 | 69 | virtual NuATCommandResult_t onQuery(int commandId) override 70 | { 71 | bRead = true; 72 | return AT_RESULT_OK; 73 | }; 74 | 75 | virtual void onTest(int commandId) override 76 | { 77 | bTest = true; 78 | }; 79 | 80 | public: 81 | void reset() 82 | { 83 | lastResponse = AT_RESULT_OK; 84 | id = 0; 85 | bExecute = false; 86 | bWrite = false; 87 | bRead = false; 88 | bTest = false; 89 | bPrintParams = false; 90 | bPrintCmd = false; 91 | }; 92 | 93 | NuATCommandResult_t test(const char commandLine[]) 94 | { 95 | parseCommandLine(commandLine); 96 | return lastResponse; 97 | }; 98 | 99 | NuATCommandTester() 100 | { 101 | setATCallbacks(this); 102 | }; 103 | } tester; 104 | 105 | //----------------------------------------------------------------------------- 106 | // ASSERTION utilities 107 | //----------------------------------------------------------------------------- 108 | 109 | template 110 | void assert_eq(T expected, T actual, int testID, bool eqOrNot = true) 111 | { 112 | bool test = (expected == actual); 113 | if (test != eqOrNot) 114 | { 115 | Serial.printf(" --Test #%d failure: expected %d, found %d\n", testID, expected, actual); 116 | Serial.printf(" --Last parsing result: %d\n", tester.lastParsingResult); 117 | } 118 | } 119 | 120 | // void assert_strEq(const char *expected, const char *actual, int testID, bool eqOrNot = true) 121 | // { 122 | // bool test = (strcmp(expected, actual) == 0); 123 | // if (test != eqOrNot) 124 | // { 125 | // Serial.printf("--Test #%d failure: expected %d, found %d\n", testID, expected, actual); 126 | // } 127 | // } 128 | 129 | //----------------------------------------------------------------------------- 130 | // Test macros 131 | //----------------------------------------------------------------------------- 132 | 133 | int testNumber = 1; 134 | 135 | void Test_parsing(char commandLine[], NuATCommandResult_t expectedResult) 136 | { 137 | tester.reset(); 138 | Serial.printf("--Test #%d. Parsing command line: %s\n", testNumber, commandLine); 139 | NuATCommandResult_t actualResult = tester.test(commandLine); 140 | assert_eq(expectedResult, actualResult, testNumber); 141 | testNumber++; 142 | } 143 | 144 | void Test_actionFlags(char commandLine[], bool mustExecute, bool mustRead, bool mustWrite, bool mustTest) 145 | { 146 | Serial.printf("--Test #%d. Callbacks for %s\n", testNumber, commandLine); 147 | tester.reset(); 148 | tester.test(commandLine); 149 | bool test = (tester.bExecute == mustExecute) && (tester.bRead == mustRead) && (tester.bWrite == mustWrite) && (tester.bTest == mustTest); 150 | if (!test) 151 | { 152 | Serial.printf(" --Test failed (execute,read,write,test). Expected (%d,%d,%d,%d). Found (%d,%d,%d,%d)\n", 153 | mustExecute, mustRead, mustWrite, mustTest, 154 | tester.bExecute, tester.bRead, tester.bWrite, tester.bTest); 155 | Serial.printf(" --Last parsing result: %d\n", tester.lastParsingResult); 156 | } 157 | testNumber++; 158 | } 159 | 160 | void Test_setActionParameters(char commandLine[]) 161 | { 162 | tester.reset(); 163 | tester.bPrintParams = true; 164 | Serial.printf("-- Test #%d. Check parameters for %s\n", testNumber, commandLine); 165 | tester.test(commandLine); 166 | testNumber++; 167 | } 168 | 169 | void Test_commandName(char commandLine[]) 170 | { 171 | tester.reset(); 172 | tester.bPrintCmd = true; 173 | Serial.printf("-- Test #%d. Check command names for %s\n", testNumber, commandLine); 174 | tester.test(commandLine); 175 | testNumber++; 176 | } 177 | 178 | //----------------------------------------------------------------------------- 179 | // Arduino entry points 180 | //----------------------------------------------------------------------------- 181 | 182 | void setup() 183 | { 184 | // Initialize serial monitor 185 | Serial.begin(115200); 186 | Serial.println("*****************************************"); 187 | Serial.println(" Automated test for AT command processor "); 188 | Serial.println("*****************************************"); 189 | 190 | // Test #1 191 | Test_parsing("AT\n", AT_RESULT_OK); 192 | Test_parsing("ATn\n", AT_RESULT_ERROR); 193 | Test_parsing("AT+\n", AT_RESULT_ERROR); 194 | Test_parsing("AT&\n", AT_RESULT_ERROR); 195 | Test_parsing("AT$n\n", AT_RESULT_ERROR); 196 | 197 | Test_parsing("AT&F\n", AT_RESULT_OK); 198 | Test_parsing("AT&FF\n", AT_RESULT_ERROR); 199 | Test_parsing("AT+F\n", AT_RESULT_OK); 200 | Test_parsing("AT+FFFF\n", AT_RESULT_OK); 201 | 202 | // Test #10 203 | Test_parsing("AT&+F\n", AT_RESULT_ERROR); 204 | Test_parsing("AT&F=\n", AT_RESULT_OK); 205 | Test_parsing("AT+FFFF=\"value\"\n", AT_RESULT_OK); 206 | Test_parsing("AT+FFFF=\"value\",1\n", AT_RESULT_OK); 207 | Test_parsing("AT+FFFF=\"value\",\n", AT_RESULT_OK); 208 | 209 | Test_parsing("AT+FFFF=,1\n", AT_RESULT_OK); 210 | Test_parsing("AT+FFFF=,,,\n", AT_RESULT_OK); 211 | Test_parsing("AT+F?\n", AT_RESULT_OK); 212 | Test_parsing("AT+F=?\n", AT_RESULT_OK); 213 | Test_parsing("AT+F/\n", AT_RESULT_ERROR); 214 | 215 | // Test #20 216 | Test_parsing("AT+F1F\n", AT_RESULT_ERROR); 217 | Test_parsing("AT&F;AT&F\n", AT_RESULT_ERROR); 218 | Test_parsing("AT&F;&G=1;&H?\n", AT_RESULT_OK); 219 | Test_parsing("AT&F;&G=1;&H?;\n", AT_RESULT_ERROR); 220 | Test_parsing("AT&F;;&H?\n", AT_RESULT_ERROR); 221 | 222 | Test_parsing("AT&F=\"\"\n", AT_RESULT_OK); 223 | Test_parsing("AT&F=error\"string\"\n", AT_RESULT_ERROR); 224 | Test_parsing("AT&F=\"string\"error\n", AT_RESULT_ERROR); 225 | Test_parsing("AT&F=\"error\n", AT_RESULT_ERROR); 226 | Test_parsing("AT&F=error\"\n", AT_RESULT_ERROR); 227 | 228 | // Test #30 229 | Test_parsing("AT&F=\"a \\\\ b\"\n", AT_RESULT_OK); 230 | Test_parsing("AT&F=\"a \\, b\"\n", AT_RESULT_OK); 231 | Test_parsing("AT&F=\"a \\; b\"\n", AT_RESULT_OK); 232 | Test_parsing("AT&F=\"a \\\" b\"\n", AT_RESULT_OK); 233 | Test_parsing("AT&F=\"too long too long too long too long too long too long\"\n", AT_RESULT_ERROR); 234 | 235 | Test_actionFlags("AT&FFF\n", false, false, false, false); 236 | Test_actionFlags("AT+F/\n", false, false, false, false); 237 | Test_actionFlags("AT&F\n", true, false, false, false); 238 | Test_actionFlags("AT&F?\n", false, true, false, false); 239 | Test_actionFlags("AT&F=99\n", false, false, true, false); 240 | 241 | // Test #40 242 | Test_actionFlags("AT&F=?\n", false, false, false, true); 243 | Test_actionFlags("AT&+F/;&G\n", false, false, false, false); 244 | Test_actionFlags("AT&G=?;&F\n", true, false, false, true); 245 | Test_actionFlags("AT&G;&F?\n", true, true, false, false); 246 | Test_actionFlags("AT&F;&G=99\n", true, false, true, false); 247 | 248 | Test_actionFlags("AT&F=1;&G=?\n", false, false, true, true); 249 | 250 | Serial.println("*****************************************"); 251 | Serial.println(" Non-Automated test (check visually) "); 252 | Serial.println("*****************************************"); 253 | 254 | // Test #46 255 | 256 | Test_setActionParameters("AT&F=\"value\"\n"); 257 | Test_setActionParameters("AT&F=1,2,3,4,5\n"); 258 | Test_setActionParameters("AT&F=\"a \\\\ b\"\n"); 259 | Test_setActionParameters("AT&F=\"a \\, b\"\n"); 260 | 261 | // Test #50 262 | Test_setActionParameters("AT&F=\"a \\; b\"\n"); 263 | Test_setActionParameters("AT&F=\"a \\\" b\"\n"); 264 | Test_setActionParameters("AT&F=\"a \\\n b\"\n"); 265 | Test_commandName("AT&F;+AB;+ABC;+ABCD\n"); 266 | 267 | Serial.println("*****************************************"); 268 | Serial.println("END"); 269 | Serial.println("*****************************************"); 270 | } 271 | 272 | void loop() 273 | { 274 | delay(30000); 275 | } -------------------------------------------------------------------------------- /src/NuATCommandParserLegacy2.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file NuATCommandParserLegacy2.hpp 3 | * @author Ángel Fernández Pineda. Madrid. Spain. 4 | * @date 2023-12-24 5 | * @brief AT command parser 6 | * 7 | * @copyright Creative Commons Attribution 4.0 International (CC BY 4.0) 8 | * 9 | */ 10 | 11 | #ifndef __NU_AT_COMMAND_PARSER_LEGACY2_HPP__ 12 | #define __NU_AT_COMMAND_PARSER_LEGACY2_HPP__ 13 | 14 | #include 15 | 16 | /** 17 | * @brief Pseudo-standardized result of AT command execution 18 | * 19 | * @note Negative means error and non-negative means success. 20 | * In case of doubt, use either AT_RESULT_OK or AT_RESULT_ERROR. 21 | */ 22 | typedef enum 23 | { 24 | /** Failure to send a command to a protocol stack */ 25 | AT_RESULT_SEND_FAIL = -3, 26 | /** Command not executed due to invalid or missing parameter(s) */ 27 | AT_RESULT_INVALID_PARAM = -2, 28 | /** Command executed with no success */ 29 | AT_RESULT_ERROR = -1, 30 | /** Command executed with success */ 31 | AT_RESULT_OK = 0, 32 | /** Command send successfully to a protocol stack but execution pending */ 33 | AT_RESULT_SEND_OK = 1 34 | } NuATCommandResult_t; 35 | 36 | /** 37 | * @brief Parsing state of a received command 38 | * 39 | * @note Additional information about parsing for debug or logging purposes 40 | */ 41 | typedef enum 42 | { 43 | /** No Parsing error */ 44 | AT_PR_OK = 0, 45 | /** Callbacks not set */ 46 | AT_PR_NO_CALLBACKS, 47 | /** Not an AT command line */ 48 | AT_PR_NO_PREAMBLE, 49 | /** AT preamble found but no commands */ 50 | AT_PR_NO_COMMANDS, 51 | /** Prefix token was not found */ 52 | AT_PR_INVALID_PREFIX, 53 | /** No command name, buffer overflow or command name has "&" prefix but more than one letter */ 54 | AT_PR_INVALID_CMD1, 55 | /** Command name contains non alphabetic characters */ 56 | AT_PR_INVALID_CMD2, 57 | /** Command name valid, but not supported by this app */ 58 | AT_PR_UNSUPPORTED_CMD, 59 | /** Command - end token was expected but not found */ 60 | AT_PR_END_TOKEN_EXPECTED, 61 | /** Buffer overflow in a SET command(parameters too long) */ 62 | AT_PR_SET_OVERFLOW, 63 | /** A string parameter is not properly enclosed between double quotes */ 64 | AT_PR_ILL_FORMED_STRING, 65 | /** Unable to allocate buffer memory */ 66 | AT_PR_NO_HEAP 67 | } NuATParsingResult_t; 68 | 69 | typedef ::std::vector NuATCommandParameters_t; 70 | 71 | /** 72 | * @brief Custom AT command processing for your application 73 | * 74 | * @note Derive a new class to implement your own AT commands 75 | */ 76 | class NuATCommandCallbacks 77 | { 78 | public: 79 | /** 80 | * @brief Custom processing of non-AT data 81 | * 82 | * @note Optional 83 | * 84 | * @param text Null-terminated incoming string, 85 | * not matching an AT command line. 86 | */ 87 | virtual void onNonATCommand(const char text[]){}; 88 | 89 | /** 90 | * @brief Identify supported command names 91 | * 92 | * @note Override this method to inform which commands are 93 | * supported or not. This is mandatory. 94 | * 95 | * @note AT commands should comprise uppercase characters, but this is up 96 | * to you. You may return the same ID for lowercase 97 | * command names. You may also return the same ID for aliases. 98 | * 99 | * @param commandName A null-terminated string containing a valid command name. 100 | * This string does not contain any prefix (`&` or `+`), just the 101 | * name. Length of command names is limited by buffer size. 102 | * Will comprise alphabetic characters only, as required by the AT 103 | * standard, so don't expect something like "PARAM1". 104 | * 105 | * @return int Any negative value if @p commandName is not a supported 106 | * AT command. Any positive number as an **unique** 107 | * identification (ID) of a supported command name. 108 | */ 109 | virtual int getATCommandId(const char commandName[]) = 0; 110 | 111 | /** 112 | * @brief Execute a supported AT command (with no suffix) 113 | * 114 | * @param commandId Unique (non-negative) identification number as returned by getATCommandId() 115 | * @return NuATCommandResult_t Proper result of command execution 116 | */ 117 | virtual NuATCommandResult_t onExecute(int commandId) { return AT_RESULT_ERROR; }; 118 | 119 | /** 120 | * @brief Execute or set the value given in a supported AT command (with '=' suffix) 121 | * 122 | * @param commandId Unique (non-negative) identification number as returned by getATCommandId() 123 | * 124 | * @param parameters A sorted list of null-terminated strings, one for each parameter, 125 | * from left to right. Total length of all parameters 126 | * is limited by buffer size. There is at least one parameter, but any parameter 127 | * may be an empty string. AT string parameters are properly parsed before calling. 128 | * 129 | * @return NuATCommandResult_t Proper result of command execution 130 | */ 131 | virtual NuATCommandResult_t onSet(int commandId, NuATCommandParameters_t ¶meters) { return AT_RESULT_ERROR; }; 132 | 133 | /** 134 | * @brief Print the value requested in a supported AT command (with '?' suffix) 135 | * 136 | * @note Use NuATCommands.printATResponse() to print the requested value. 137 | * 138 | * @param commandId Unique (non-negative) identification number as returned by getATCommandId() 139 | * 140 | * @return NuATCommandResult_t Proper result of command execution 141 | */ 142 | virtual NuATCommandResult_t onQuery(int commandId) { return AT_RESULT_ERROR; }; 143 | 144 | /** 145 | * @brief Print the syntax and parameters of a supported command (with '=?' suffix) 146 | * 147 | * @note Optional. Use NuATCommands.printATResponse() to print. 148 | * 149 | * @param commandId Unique (non-negative) identification number as returned by getATCommandId() 150 | */ 151 | virtual void onTest(int commandId){}; 152 | 153 | /** 154 | * @brief Get informed of the parsing result of each received command 155 | * 156 | * @note Optional. Use this method to store the details about the result of the last command, so you can 157 | * implement another AT command to send that information. Always called after the command 158 | * is parsed and, if no parsing errors were found, executed. Note that if a parsing error is found 159 | * in a command line, the following commands in that command line are not parsed nor executed. 160 | * 161 | * @param index 0-based index of the command being informed as it was written in the command line, 162 | * from left to right. For example, for the command line "AT&F;&G;&H", index 163 | * 1 refers to "&G". 164 | * 165 | * @param parsingResult Detailed result of command parsing 166 | */ 167 | virtual void onFinished(int index, NuATParsingResult_t parsingResult){}; 168 | }; 169 | 170 | /** 171 | * @brief Parse and execute AT commands 172 | * 173 | */ 174 | class NuATCommandParser 175 | { 176 | public: 177 | /** 178 | * @brief Print a message properly formatted as an AT response 179 | * 180 | * @note Error and success messages are already managed by this class. 181 | * Do not print those messages to avoid misunderstandings. 182 | * 183 | * @note An AT response is just a text starting with CR+LF and 184 | * ending with CR+LF. 185 | * You may use `NuATCommands.printf("\r\n%s\r\n",...)` instead. 186 | * 187 | * @param message Text to print. 188 | * Must not contain the CR+LF sequence of characters. 189 | */ 190 | virtual void printATResponse(const char message[]) = 0; 191 | 192 | /** 193 | * @brief Set custom AT command processing callbacks 194 | * 195 | * @note Not thread-safe. 196 | * 197 | * @param pCallbacks A pointer to your own callbacks. Must 198 | * remain valid forever (do not destroy). 199 | * 200 | */ 201 | void setATCallbacks(NuATCommandCallbacks *pCallbacks) 202 | { 203 | pCmdCallbacks = pCallbacks; 204 | }; 205 | 206 | /** 207 | * @brief Size of the parsing buffer 208 | * 209 | * @note An error response will be printed if command names or 210 | * command parameters exceed this size. Buffer is allocated 211 | * in the heap. 212 | * 213 | * @note Default size is 42 bytes 214 | * 215 | * @param size Size in bytes 216 | */ 217 | void setBufferSize(size_t size); 218 | 219 | /** 220 | * @brief Allow or disallow a lower case "AT" preamble 221 | * 222 | * @note By default, the "at" preamble (in lower case) is not allowed. 223 | * Should be called before start(). 224 | * 225 | * @param allowOrNot When true, "at" is a valid preamble for a command line. 226 | * When false, just "AT" is allowed as preamble. 227 | */ 228 | void lowerCasePreamble(bool allowOrNot = true) 229 | { 230 | bLowerCasePreamble = allowOrNot; 231 | }; 232 | 233 | public: 234 | /** 235 | * @brief Check this attribute to know why parsing failed (or not) 236 | * on the last received command 237 | * 238 | * @note Exposed for testing, mainly. Do not write. 239 | */ 240 | NuATParsingResult_t lastParsingResult = AT_PR_OK; 241 | 242 | private: 243 | NuATCommandCallbacks *pCmdCallbacks = nullptr; 244 | size_t bufferSize = 42; 245 | bool bLowerCasePreamble = false; 246 | 247 | const char *parseSingleCommand(const char *in); 248 | const char *parseAction(const char *in, int commandId); 249 | const char *parseWriteParameters(const char *in, int commandId); 250 | 251 | protected: 252 | virtual void printResultResponse(const NuATCommandResult_t response); 253 | void parseCommandLine(const char *in); 254 | }; 255 | 256 | #endif -------------------------------------------------------------------------------- /examples/ATCommandDemoLegacy2/ATCommandDemoLegacy2.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * @file ATCommandDemoLegacy2.ino 3 | * @author Ángel Fernández Pineda. Madrid. Spain. 4 | * @date 2023-12-24 5 | * @brief Example of an AT command processor based on 6 | * the Nordic UART Service 7 | * 8 | * @note See examples/README.md for a description 9 | * 10 | * @copyright Creative Commons Attribution 4.0 International (CC BY 4.0) 11 | * 12 | */ 13 | 14 | #include 15 | #include "NuATCommandsLegacy2.hpp" 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | // Use legacy API 22 | using namespace NuSLegacy2; 23 | 24 | #define DEVICE_NAME "AT commands demo (legacy)" 25 | 26 | //------------------------------------------------------ 27 | // AT Commands implementation for a simple calculator 28 | //------------------------------------------------------ 29 | 30 | class MyATCommandCallbacks : public NuATCommandCallbacks 31 | { 32 | public: 33 | // commands are implemented by overriding the following Methods 34 | 35 | virtual int getATCommandId(const char commandName[]) override; 36 | virtual NuATCommandResult_t onExecute(int commandId) override; 37 | virtual NuATCommandResult_t onSet(int commandId, NuATCommandParameters_t ¶meters) override; 38 | virtual NuATCommandResult_t onQuery(int commandId) override; 39 | virtual void onTest(int commandId) override; 40 | 41 | // The following methods are overriden for logging purposes 42 | 43 | virtual void onNonATCommand(const char text[]) override; 44 | virtual void onFinished(int index, NuATParsingResult_t parsingResult) override; 45 | 46 | private: 47 | // operands for add, sub, mult and div operators 48 | 49 | intmax_t op1 = 0LL; 50 | intmax_t op2 = 0LL; 51 | 52 | // macro to print a number into the BLE serial port in AT format 53 | static void printNumberATResponse(intmax_t number); 54 | 55 | // macro to parse a string into an integer 56 | static bool strToIntMax(const char text[], intmax_t &number); 57 | 58 | } myATCallbacks; // just one global instance is needed 59 | 60 | // Identifiers for supported commands (arbitrary non-negative integers) 61 | 62 | #define CMD_VERSION 0 63 | #define CMD_ADD 1 64 | #define CMD_SUB 2 65 | #define CMD_MULT 3 66 | #define CMD_DIV 4 67 | #define CMD_OPERAND1 10 68 | #define CMD_OPERAND2 20 69 | #define CMD_OPERANDS 30 70 | 71 | void MyATCommandCallbacks::printNumberATResponse(intmax_t number) 72 | { 73 | char buffer[64]; // should be enough for a single integer number 74 | snprintf(buffer, 64, "%lld", number); 75 | buffer[63] = '\0'; 76 | NuATCommands.printATResponse(buffer); 77 | } 78 | 79 | bool MyATCommandCallbacks::strToIntMax(const char text[], intmax_t &number) 80 | { 81 | // "errno" is used to detect non-integer data 82 | // errno==0 means no error 83 | errno = 0; 84 | intmax_t r = strtoimax(text, NULL, 10); 85 | if (errno == 0) 86 | { 87 | number = r; 88 | return true; 89 | } 90 | else 91 | return false; 92 | } 93 | 94 | void MyATCommandCallbacks::onNonATCommand(const char text[]) 95 | { 96 | Serial.println("--Non-AT text received--"); 97 | Serial.println(text); 98 | } 99 | 100 | int MyATCommandCallbacks::getATCommandId(const char commandName[]) 101 | { 102 | Serial.println("--Command identification request--"); 103 | Serial.println(commandName); 104 | 105 | // Must return a non-negative integer for supported commands 106 | // Command aliases returns the same integer 107 | if ((strcmp(commandName, "V") == 0) || (strcmp(commandName, "v") == 0)) 108 | return CMD_VERSION; 109 | if ((strcmp(commandName, "A") == 0) || (strcmp(commandName, "a") == 0)) 110 | return CMD_OPERAND1; 111 | if ((strcmp(commandName, "B") == 0) || (strcmp(commandName, "b") == 0)) 112 | return CMD_OPERAND2; 113 | if ((strcmp(commandName, "OP") == 0) || (strcmp(commandName, "op") == 0)) 114 | return CMD_OPERANDS; 115 | if ((strcmp(commandName, "ADD") == 0) || (strcmp(commandName, "add") == 0)) 116 | return CMD_ADD; 117 | if ((strcmp(commandName, "SUM") == 0) || (strcmp(commandName, "sum") == 0)) 118 | return CMD_ADD; 119 | if ((strcmp(commandName, "SUBTRACT") == 0) || (strcmp(commandName, "subtract") == 0)) 120 | return CMD_SUB; 121 | if ((strcmp(commandName, "SUB") == 0) || (strcmp(commandName, "sub") == 0)) 122 | return CMD_SUB; 123 | if ((strcmp(commandName, "MULT") == 0) || (strcmp(commandName, "mult") == 0)) 124 | return CMD_MULT; 125 | if ((strcmp(commandName, "DIVIDE") == 0) || (strcmp(commandName, "divide") == 0)) 126 | return CMD_DIV; 127 | if ((strcmp(commandName, "DIV") == 0) || (strcmp(commandName, "div") == 0)) 128 | return CMD_DIV; 129 | Serial.println("-- command not supported. Supported commands are: V A B OP ADD SUB MULT DIV --"); 130 | 131 | // Must return a negative integer for unsupported commands 132 | return -1; 133 | } 134 | 135 | NuATCommandResult_t MyATCommandCallbacks::onExecute(int commandId) 136 | { 137 | Serial.printf("--Command execution (no parameters). ID %d--\n", commandId); 138 | switch (commandId) 139 | { 140 | case CMD_VERSION: 141 | NuATCommands.printATResponse("Version 1.0 (fictional)"); 142 | return AT_RESULT_OK; 143 | case CMD_ADD: 144 | printNumberATResponse(op1 + op2); 145 | return AT_RESULT_OK; 146 | case CMD_SUB: 147 | printNumberATResponse(op1 - op2); 148 | return AT_RESULT_OK; 149 | case CMD_MULT: 150 | printNumberATResponse(op1 * op2); 151 | return AT_RESULT_OK; 152 | case CMD_DIV: 153 | if (op2 != 0LL) 154 | { 155 | printNumberATResponse(op1 / op2); 156 | return AT_RESULT_OK; 157 | } 158 | break; 159 | } 160 | return AT_RESULT_ERROR; 161 | } 162 | 163 | NuATCommandResult_t MyATCommandCallbacks::onSet(int commandId, NuATCommandParameters_t ¶meters) 164 | { 165 | Serial.printf("--Command execution (with parameters). ID %d--\n", commandId); 166 | int c = 1; 167 | for (const char *param : parameters) 168 | Serial.printf("Parameter %d: %s\n", c++, param); 169 | 170 | switch (commandId) 171 | { 172 | case CMD_OPERAND1: 173 | if ((parameters.size() == 1) && strToIntMax(parameters.at(0), op1)) 174 | return AT_RESULT_OK; 175 | else 176 | return AT_RESULT_INVALID_PARAM; 177 | break; 178 | case CMD_OPERAND2: 179 | if ((parameters.size() == 1) && strToIntMax(parameters.at(0), op1)) 180 | return AT_RESULT_OK; 181 | else 182 | return AT_RESULT_INVALID_PARAM; 183 | break; 184 | case CMD_OPERANDS: 185 | if ((parameters.size() == 2) && strToIntMax(parameters.at(0), op1) && strToIntMax(parameters.at(1), op2)) 186 | return AT_RESULT_OK; 187 | else 188 | return AT_RESULT_INVALID_PARAM; 189 | break; 190 | } 191 | return AT_RESULT_ERROR; 192 | } 193 | 194 | NuATCommandResult_t MyATCommandCallbacks::onQuery(int commandId) 195 | { 196 | Serial.printf("--Data request. ID %d--\n", commandId); 197 | switch (commandId) 198 | { 199 | case CMD_OPERAND1: 200 | printNumberATResponse(op1); 201 | return AT_RESULT_OK; 202 | case CMD_OPERAND2: 203 | printNumberATResponse(op1); 204 | return AT_RESULT_OK; 205 | case CMD_OPERANDS: 206 | printNumberATResponse(op1); 207 | printNumberATResponse(op2); 208 | return AT_RESULT_OK; 209 | } 210 | Serial.println("--Routing as an \"execute\" command--"); 211 | return onExecute(commandId); 212 | } 213 | 214 | void MyATCommandCallbacks::onTest(int commandId) 215 | { 216 | Serial.printf("--Command Syntax request. ID %d--\n", commandId); 217 | switch (commandId) 218 | { 219 | case CMD_OPERAND1: 220 | NuATCommands.printATResponse("+A: (integer)"); 221 | return; 222 | case CMD_OPERAND2: 223 | NuATCommands.printATResponse("+B: (integer)"); 224 | return; 225 | case CMD_OPERANDS: 226 | NuATCommands.printATResponse("+OP: (integer),(integer)"); 227 | return; 228 | } 229 | } 230 | 231 | void MyATCommandCallbacks::onFinished(int index, NuATParsingResult_t parsingResult) 232 | { 233 | Serial.printf("--Command at index %d was parsed with result code %d--\n", index, parsingResult); 234 | } 235 | 236 | void connectionStatusChanged(const bool status) 237 | { 238 | if (status) 239 | { 240 | Serial.println("-- Client connected"); 241 | } 242 | else 243 | { 244 | Serial.println("-- Client disconnected"); 245 | } 246 | } 247 | 248 | class MyServerCallbacks : public NimBLEServerCallbacks 249 | { 250 | public: 251 | virtual void onConnect( 252 | NimBLEServer *pServer, 253 | NimBLEConnInfo &connInfo) override 254 | { 255 | Serial.println("-- Client connected"); 256 | }; 257 | 258 | virtual void onDisconnect( 259 | NimBLEServer *pServer, 260 | NimBLEConnInfo &connInfo, 261 | int reason) override 262 | { 263 | Serial.println("-- Client disconnected"); 264 | }; 265 | 266 | } myServerCallbacks; 267 | 268 | //------------------------------------------------------ 269 | // Arduino entry points 270 | //------------------------------------------------------ 271 | 272 | void setup() 273 | { 274 | // Initialize serial monitor 275 | Serial.begin(115200); 276 | Serial.println("*******************************"); 277 | Serial.println(" BLE AT command processor demo "); 278 | Serial.println("*******************************"); 279 | Serial.println("--Initializing--"); 280 | 281 | // Initialize BLE and Nordic UART service 282 | NimBLEDevice::init(DEVICE_NAME); 283 | NimBLEDevice::getAdvertising()->setName(DEVICE_NAME); 284 | NuATCommands.setBufferSize(64); 285 | NuATCommands.lowerCasePreamble(true); 286 | NuATCommands.setATCallbacks(&myATCallbacks); 287 | NuATCommands.start(); 288 | NimBLEServer *pServer = NimBLEDevice::getServer(); 289 | if (pServer) 290 | pServer->setCallbacks(&myServerCallbacks); 291 | else 292 | Serial.println("ERROR: unable to set server callbacks"); 293 | 294 | // Initialization complete 295 | Serial.println("--Ready--"); 296 | } 297 | 298 | void loop() 299 | { 300 | // Incoming data is processed in another task created by the BLE stack, 301 | // so there is nothing to do here (in this demo) 302 | Serial.println("--Running (heart beat each 30 seconds)--"); 303 | delay(30000); 304 | } -------------------------------------------------------------------------------- /src/NuATParser.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file NuATParser.hpp 3 | * @author Ángel Fernández Pineda. Madrid. Spain. 4 | * @date 2024-08-20 5 | * @brief Simple command line parser 6 | * 7 | * @copyright Creative Commons Attribution 4.0 International (CC BY 4.0) 8 | * 9 | */ 10 | 11 | #ifndef __NU_AT_PARSER_HPP__ 12 | #define __NU_AT_PARSER_HPP__ 13 | 14 | #include 15 | #include 16 | #include 17 | #include // Needed for strlen() 18 | 19 | /** 20 | * @brief Pseudo-standardized result of AT command execution 21 | * 22 | * @note Negative means error and non-negative means success. 23 | * In case of doubt, use either AT_RESULT_OK or AT_RESULT_ERROR. 24 | */ 25 | typedef enum 26 | { 27 | /** Failure to send a command to a protocol stack */ 28 | AT_RESULT_SEND_FAIL = -3, 29 | /** Command not executed due to invalid or missing parameter(s) */ 30 | AT_RESULT_INVALID_PARAM = -2, 31 | /** Command executed with no success */ 32 | AT_RESULT_ERROR = -1, 33 | /** Command executed with success */ 34 | AT_RESULT_OK = 0, 35 | /** Command send successfully to a protocol stack but execution pending */ 36 | AT_RESULT_SEND_OK = 1 37 | } NuATCommandResult_t; 38 | 39 | /** 40 | * @brief Parsing/execution errors in a single command 41 | * 42 | * @note Additional information for debug or logging purposes 43 | */ 44 | typedef enum 45 | { 46 | /** No command */ 47 | AT_ERR_EMPTY_COMMAND = 0, 48 | /** Prefix token was not found */ 49 | AT_ERR_INVALID_PREFIX, 50 | /** No command name, or command name has "&" prefix but more than one letter, 51 | * or contains non alphabetic characters, or not upper case */ 52 | AT_ERR_INVALID_NAME, 53 | /** Text found after a "?" or "=?" suffix */ 54 | AT_ERR_PARAMETER_NOT_ALLOWED, 55 | /** No syntax error, but the given command is not supported by this app */ 56 | AT_ERR_NO_CALLBACK, 57 | /** A string parameter is not properly enclosed between double quotes */ 58 | AT_ERR_ILL_FORMED_STRING, 59 | /** A numeric parameter (without double quotes) is not properly 60 | * formatted as binary, decimal or hexadecimal */ 61 | AT_ERR_ILL_FORMED_NUMBER, 62 | /** Command line is too long (not used by class NuATParser) */ 63 | AT_ERR_TOO_LONG, 64 | /** Unspecified error to be used by descendant classes 65 | * (not used by class NuATParser) */ 66 | AT_ERR_UNSPECIFIED 67 | } NuATSyntaxError_t; 68 | 69 | /** 70 | * @brief AT command parameters 71 | * 72 | */ 73 | typedef ::std::vector<::std::string> NuATCommandParameters_t; 74 | 75 | /** 76 | * @brief Callback to execute for AT commands 77 | * 78 | * @param[in] params Parameters as a string vector. 79 | * Empty if there are no parameters or parameters are not allowed. 80 | */ 81 | typedef ::std::function NuATCommandCallback_t; 82 | 83 | /** 84 | * @brief Callback to execute for parsing/execution errors 85 | * 86 | * @param[in] text The text causing an error 87 | * @param[in] errorCode Code of error 88 | */ 89 | typedef ::std::function NuATErrorCallback_t; 90 | 91 | /** 92 | * @brief Callback to execute for non-AT commands 93 | * 94 | * @param[in] text Pointer to buffer containing text 95 | * @param[in] errorCode Size of the buffer 96 | */ 97 | typedef ::std::function NuATNotACommandLineCallback_t; 98 | 99 | /** 100 | * @brief Parse and execute AT commands 101 | * 102 | */ 103 | class NuATParser 104 | { 105 | public: 106 | /** 107 | * @brief Allow or not AT preamble/command names in lower case 108 | * 109 | * @note The AT standard requires upper-case 110 | * 111 | * @param[in] yesOrNo True to allow, false to disallow. 112 | * @return true Previously, allowed. 113 | * @return false Previously, disallowed. 114 | */ 115 | bool allowLowerCase(bool yesOrNo) noexcept; 116 | 117 | /** 118 | * @brief Stop execution on failure of a single command, or not 119 | * 120 | * @note Applies to a single call to execute() 121 | * 122 | * @param yesOrNo True to stop parsing if a single command fails, 123 | * false to continue the execution of the following commands. 124 | * @return true Previously, stop 125 | * @return false Previously, continue 126 | */ 127 | bool stopOnFirstFailure(bool yesOrNo) noexcept; 128 | 129 | /** 130 | * @brief Set a callback for a command with no suffix 131 | * 132 | * @note If you set two or more callbacks for the same command name, 133 | * just the first one will be executed, so don't do that. 134 | * 135 | * @param[in] commandName Command name 136 | * @param[in] callback Function to execute if @p commandName is found 137 | * with no suffix 138 | * 139 | * @return NuATParser& This instance. Used to chain calls. 140 | */ 141 | NuATParser &onExecute( 142 | const ::std::string commandName, 143 | NuATCommandCallback_t callback) noexcept; 144 | 145 | /** 146 | * @brief Set a callback for a command with "=" suffix 147 | * 148 | * @note If you set two or more callbacks for the same command name, 149 | * just the first one will be executed, so don't do that. 150 | * 151 | * @param[in] commandName Command name 152 | * @param[in] callback Function to execute if @p commandName is found 153 | * with "=" suffix 154 | * 155 | * @return NuATParser& This instance. Used to chain calls. 156 | */ 157 | NuATParser &onSet( 158 | const ::std::string commandName, 159 | NuATCommandCallback_t callback) noexcept; 160 | 161 | /** 162 | * @brief Set a callback for a command with "?" suffix 163 | * 164 | * @note If you set two or more callbacks for the same command name, 165 | * just the first one will be executed, so don't do that. 166 | * 167 | * @param[in] commandName Command name 168 | * @param[in] callback Function to execute if @p commandName is found 169 | * with "?" suffix 170 | * 171 | * @return NuATParser& This instance. Used to chain calls. 172 | */ 173 | NuATParser &onQuery( 174 | const ::std::string commandName, 175 | NuATCommandCallback_t callback) noexcept; 176 | 177 | /** 178 | * @brief Set a callback for a command with "=?" suffix 179 | * 180 | * @note If you set two or more callbacks for the same command name, 181 | * just the first one will be executed, so don't do that. 182 | * 183 | * @param[in] commandName Command name 184 | * @param[in] callback Function to execute if @p commandName is found 185 | * with "=?" suffix 186 | * 187 | * @return NuATParser& This instance. Used to chain calls. 188 | */ 189 | NuATParser &onTest( 190 | const ::std::string commandName, 191 | NuATCommandCallback_t callback) noexcept; 192 | 193 | /** 194 | * @brief Set a callback for command errors 195 | * 196 | * @param[in] callback Function to execute on command errors 197 | * 198 | * @return NuATParser& This instance. Used to chain calls. 199 | */ 200 | NuATParser &onError(NuATErrorCallback_t callback) noexcept; 201 | 202 | /** 203 | * @brief Set a callback for non-AT commands 204 | * 205 | * @param[in] callback Function to execute 206 | * 207 | * @return NuATParser& This instance. Used to chain calls 208 | */ 209 | NuATParser &onNotACommandLine( 210 | NuATNotACommandLineCallback_t callback) noexcept; 211 | 212 | /** 213 | * @brief Print a message properly formatted as an AT response 214 | * 215 | * @note Error and success messages are already managed by this class. 216 | * Do not print those messages to avoid misunderstandings. 217 | * 218 | * @note An AT response is just a text starting with CR+LF and 219 | * ending with CR+LF. 220 | * You may use `NuATCommands.printf("\r\n%s\r\n",...)` instead. 221 | * 222 | * @param message Text to print. 223 | * Must not contain the CR+LF sequence of characters. 224 | */ 225 | virtual void printATResponse(::std::string message) = 0; 226 | 227 | /** 228 | * @brief Execute the given AT command line 229 | * 230 | * @param commandLine Pointer to a buffer containing a command line 231 | * @param size Size in bytes of @p commandLine 232 | */ 233 | void execute(const uint8_t *commandLine, size_t size); 234 | 235 | /** 236 | * @brief Execute the given AT command line 237 | * 238 | * @param commandLine String containing command line 239 | */ 240 | void execute(::std::string commandLine) 241 | { 242 | execute((const uint8_t *)commandLine.data(), commandLine.length()); 243 | }; 244 | 245 | /** 246 | * @brief Execute the given AT command line 247 | * 248 | * @param commandLine Null-terminated string containing a command line 249 | */ 250 | void execute(const char *commandLine) 251 | { 252 | if (commandLine) 253 | execute((const uint8_t *)commandLine, strlen(commandLine)); 254 | }; 255 | 256 | protected: 257 | virtual void notifyError( 258 | ::std::string command, 259 | NuATSyntaxError_t errorCode); 260 | 261 | virtual void doExecute(const uint8_t *in, size_t size); 262 | 263 | virtual void doQuery(const uint8_t *in, size_t size); 264 | 265 | virtual void doTest(const uint8_t *in, size_t size); 266 | 267 | virtual void doSet(::std::string command, NuATCommandParameters_t ¶ms); 268 | 269 | virtual void doNotACommandLine(const uint8_t *in, size_t size); 270 | 271 | void printResultResponse(const NuATCommandResult_t response); 272 | 273 | private: 274 | bool bAllowLowerCase = false; 275 | bool bStopOnFirstFailure = false; 276 | ::std::vector<::std::string> vsOnExecuteCN; 277 | ::std::vector<::std::string> vsOnSetCN; 278 | ::std::vector<::std::string> vsOnQueryCN; 279 | ::std::vector<::std::string> vsOnTestCN; 280 | ::std::vector vcbOnExecuteCallback; 281 | ::std::vector vcbOnSetCallback; 282 | ::std::vector vcbOnQueryCallback; 283 | ::std::vector vcbOnTestCallback; 284 | NuATErrorCallback_t cbErrorCallback = nullptr; 285 | NuATNotACommandLineCallback_t cbNoCommandsCallback = nullptr; 286 | 287 | bool findCallback( 288 | ::std::string name, 289 | ::std::vector<::std::string> nameList, 290 | ::std::vector callbackList, 291 | NuATCommandCallback_t &callback); 292 | 293 | void notifyError( 294 | const uint8_t *command, 295 | size_t size, 296 | NuATSyntaxError_t errorCode); 297 | 298 | bool executeSingleCommand(const uint8_t *in, size_t size); 299 | 300 | bool parseParameter(const uint8_t *in, size_t size, ::std::string &text); 301 | }; 302 | 303 | #endif -------------------------------------------------------------------------------- /extras/test/ATCommandsTester/ATCommandsTester.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * @file ATCommandsTester.ino 3 | * @author Ángel Fernández Pineda. Madrid. Spain. 4 | * @date 2024-08-20 5 | * @brief Automated test 6 | * 7 | * @copyright Creative Commons Attribution 4.0 International (CC BY 4.0) 8 | * 9 | */ 10 | 11 | #include "NuATParser.hpp" 12 | #include 13 | 14 | //----------------------------------------------------------------------------- 15 | // MOCK 16 | //----------------------------------------------------------------------------- 17 | 18 | class NuATCommandTester : public NuATParser 19 | { 20 | public: 21 | std::string lastCommand; 22 | NuATSyntaxError_t lastSyntaxError; 23 | bool bSyntaxError = false; 24 | bool bExecute = false; 25 | bool bWrite = false; 26 | bool bRead = false; 27 | bool bTest = false; 28 | bool bNoCommands = false; 29 | bool bPrintParams = false; 30 | bool bPrintCmd = false; 31 | 32 | protected: 33 | virtual void notifyError( 34 | std::string command, 35 | NuATSyntaxError_t errorCode) override 36 | { 37 | bSyntaxError = true; 38 | lastSyntaxError = errorCode; 39 | }; 40 | 41 | virtual void doExecute(const uint8_t *in, size_t size) override 42 | { 43 | bExecute = true; 44 | bSyntaxError = false; 45 | lastCommand.assign((const char *)in, size); 46 | }; 47 | 48 | virtual void doQuery(const uint8_t *in, size_t size) override 49 | { 50 | bRead = true; 51 | bSyntaxError = false; 52 | lastCommand.assign((const char *)in, size); 53 | }; 54 | 55 | virtual void doTest(const uint8_t *in, size_t size) override 56 | { 57 | bTest = true; 58 | bSyntaxError = false; 59 | lastCommand.assign((const char *)in, size); 60 | }; 61 | 62 | virtual void doSet(std::string command, NuATCommandParameters_t ¶ms) override 63 | { 64 | if (bPrintParams) 65 | { 66 | int count = 1; 67 | // Serial.printf("Count: %d\n",params.size()); 68 | for (std::string param : params) 69 | { 70 | param.push_back(0); 71 | Serial.printf(" Parameter %d: %s\n", count++, param.c_str()); 72 | } 73 | } 74 | bWrite = true; 75 | bSyntaxError = false; 76 | lastCommand.assign(command); 77 | }; 78 | 79 | virtual void doNotACommandLine(const uint8_t *in, size_t size) override 80 | { 81 | bNoCommands = true; 82 | bSyntaxError = false; 83 | lastCommand.assign((const char *)in, size); 84 | }; 85 | 86 | public: 87 | virtual void printATResponse(std::string message) override {}; 88 | 89 | void reset() 90 | { 91 | bExecute = false; 92 | bWrite = false; 93 | bRead = false; 94 | bTest = false; 95 | bNoCommands = false; 96 | bSyntaxError = false; 97 | bPrintParams = false; 98 | bPrintCmd = false; 99 | lastCommand.clear(); 100 | }; 101 | 102 | } tester; 103 | 104 | class NuATCommandTester2 : public NuATParser 105 | { 106 | public: 107 | virtual void printATResponse(std::string message) override {}; 108 | } tester2; 109 | 110 | NuATCommandResult_t testOkCallback(NuATCommandParameters_t ¶ms) 111 | { 112 | Serial.printf(" AT command callback\n"); 113 | return NuATCommandResult_t::AT_RESULT_OK; 114 | } 115 | 116 | void testErrorCallback(const std::string text, NuATSyntaxError_t errorCode) 117 | { 118 | if (errorCode == NuATSyntaxError_t::AT_ERR_NO_CALLBACK) 119 | Serial.printf(" No callback for %s\n", text.c_str()); 120 | else 121 | Serial.printf(" Syntax error in %s\n", text.c_str()); 122 | } 123 | 124 | void testNoCommandLineCallback(const uint8_t *text, size_t size) 125 | { 126 | Serial.printf(" Not an AT command line\n"); 127 | } 128 | 129 | //----------------------------------------------------------------------------- 130 | // ASSERTION utilities 131 | //----------------------------------------------------------------------------- 132 | 133 | template 134 | void assert_eq(T expected, T actual, int testID, bool eqOrNot = true) 135 | { 136 | bool test = (expected == actual); 137 | if (test != eqOrNot) 138 | { 139 | Serial.printf(" --Test #%d failure: expected %d, found %d\n", testID, expected, actual); 140 | } 141 | } 142 | 143 | //----------------------------------------------------------------------------- 144 | // Test macros 145 | //----------------------------------------------------------------------------- 146 | 147 | int testNumber = 1; 148 | 149 | void Test_parsing(const char commandLine[], NuATSyntaxError_t expectedError) 150 | { 151 | tester.reset(); 152 | Serial.printf("--Test #%d. Parsing command line: %s\n", testNumber, commandLine); 153 | tester.execute(commandLine); 154 | if (tester.bNoCommands) 155 | Serial.printf(" --Failure. Parsed as non-AT text.\n"); 156 | if (!tester.bSyntaxError) 157 | Serial.printf(" --Failure. Expected syntax error not found.\n"); 158 | else 159 | assert_eq(expectedError, tester.lastSyntaxError, testNumber); 160 | testNumber++; 161 | } 162 | 163 | void Test_parsing(const char commandLine[]) 164 | { 165 | tester.reset(); 166 | Serial.printf("--Test #%d. Parsing command line: %s\n", testNumber, commandLine); 167 | tester.execute(commandLine); 168 | if (tester.bNoCommands) 169 | Serial.printf(" --Failure. Parsed as non-AT text.\n"); 170 | if (tester.bSyntaxError) 171 | Serial.printf(" --Failure. Unexpected syntax error %d.\n", tester.lastSyntaxError); 172 | testNumber++; 173 | } 174 | 175 | void Test_noCommand(const char commandLine[]) 176 | { 177 | tester.reset(); 178 | Serial.printf("--Test #%d. Parsing command line: %s\n", testNumber, commandLine); 179 | tester.execute(commandLine); 180 | if (!tester.bNoCommands) 181 | Serial.printf(" --Failure. Expected a non-AT command line.\n"); 182 | testNumber++; 183 | } 184 | 185 | void Test_actionFlags(const char commandLine[], bool mustExecute, bool mustRead, bool mustWrite, bool mustTest) 186 | { 187 | Serial.printf("--Test #%d. Callbacks for %s\n", testNumber, commandLine); 188 | tester.reset(); 189 | tester.execute(commandLine); 190 | if (tester.bNoCommands) 191 | Serial.printf(" --Failure. Parsed as non-AT text.\n"); 192 | else 193 | { 194 | bool test = (tester.bExecute == mustExecute) && (tester.bRead == mustRead) && (tester.bWrite == mustWrite) && (tester.bTest == mustTest); 195 | if (!test) 196 | { 197 | Serial.printf(" --Failure (execute,read,write,test). Expected (%d,%d,%d,%d). Found (%d,%d,%d,%d)\n", 198 | mustExecute, mustRead, mustWrite, mustTest, 199 | tester.bExecute, tester.bRead, tester.bWrite, tester.bTest); 200 | if (tester.bSyntaxError) 201 | Serial.printf(" --There is a syntax error %d.\n", tester.lastSyntaxError); 202 | } 203 | } 204 | testNumber++; 205 | } 206 | 207 | void Test_setActionParameters(const char commandLine[]) 208 | { 209 | tester.reset(); 210 | tester.bPrintParams = true; 211 | Serial.printf("-- Test #%d. Check parameters for %s\n", testNumber, commandLine); 212 | tester.execute(commandLine); 213 | testNumber++; 214 | } 215 | 216 | void Test_callbacks(const char commandLine[]) 217 | { 218 | Serial.printf("-- Test #%d. Check execution for %s\n", testNumber, commandLine); 219 | tester2.execute(commandLine); 220 | testNumber++; 221 | } 222 | 223 | //----------------------------------------------------------------------------- 224 | // Arduino entry points 225 | //----------------------------------------------------------------------------- 226 | 227 | void setup() 228 | { 229 | // Initialize serial monitor 230 | Serial.begin(115200); 231 | Serial.println("*****************************************"); 232 | Serial.println(" Automated test for AT command processor "); 233 | Serial.println("*****************************************"); 234 | 235 | tester.stopOnFirstFailure(false); 236 | 237 | // Test #1 238 | tester.allowLowerCase(false); 239 | Test_parsing("AT\n"); 240 | Test_noCommand("at\n"); 241 | tester.allowLowerCase(true); 242 | Test_parsing("at\n"); 243 | tester.allowLowerCase(false); 244 | Test_noCommand("at"); 245 | 246 | Test_noCommand("\n"); 247 | Test_parsing("ATn\n", NuATSyntaxError_t::AT_ERR_INVALID_PREFIX); 248 | Test_parsing("AT+\n", NuATSyntaxError_t::AT_ERR_INVALID_NAME); 249 | Test_parsing("AT&\n", NuATSyntaxError_t::AT_ERR_INVALID_NAME); 250 | Test_parsing("AT+F;;", NuATSyntaxError_t::AT_ERR_EMPTY_COMMAND); 251 | 252 | // Test #10 253 | Test_parsing("AT&F\n"); 254 | Test_parsing("AT&FF\n", NuATSyntaxError_t::AT_ERR_INVALID_NAME); 255 | Test_parsing("AT+F\n"); 256 | Test_parsing("AT+FFFF\n"); 257 | Test_parsing("AT&+F\n", NuATSyntaxError_t::AT_ERR_INVALID_NAME); 258 | 259 | Test_parsing("AT&F=\n"); 260 | Test_parsing("AT+FFFF=\"value\"\n"); 261 | Test_parsing("AT+FFFF=\"value\",1\n"); 262 | Test_parsing("AT+FFFF=\"value\",\n"); 263 | Test_parsing("AT+FFFF=,1\n"); 264 | 265 | // Test #20 266 | Test_parsing("AT+FFFF=,,,\n"); 267 | Test_parsing("AT+F?\n"); 268 | Test_parsing("AT+F=?\n"); 269 | Test_parsing("AT+F/\n", NuATSyntaxError_t::AT_ERR_INVALID_NAME); 270 | Test_parsing("AT+F1F\n", NuATSyntaxError_t::AT_ERR_INVALID_NAME); 271 | 272 | Test_parsing("AT&F;AT&F\n", NuATSyntaxError_t::AT_ERR_INVALID_PREFIX); 273 | Test_parsing("AT&F;&G=1;&H?\n"); 274 | Test_parsing("AT&F;&G=1;&H?;\n", NuATSyntaxError_t::AT_ERR_EMPTY_COMMAND); 275 | Test_parsing("AT&F;;&H?\n"); 276 | Test_parsing("AT&F=\"\"\n"); 277 | 278 | // Test #30 279 | Test_parsing("AT&F=error\"string\"\n", NuATSyntaxError_t::AT_ERR_ILL_FORMED_NUMBER); 280 | Test_parsing("AT&F=\"string\"error\n", NuATSyntaxError_t::AT_ERR_ILL_FORMED_STRING); 281 | Test_parsing("AT&F=\"error\n", NuATSyntaxError_t::AT_ERR_ILL_FORMED_STRING); 282 | Test_parsing("AT&F=error\"\n", NuATSyntaxError_t::AT_ERR_ILL_FORMED_NUMBER); 283 | Test_parsing("AT&F=\"a \\\\ b\"\n"); 284 | 285 | Test_parsing("AT&F=\"a \\, b\"\n"); 286 | Test_parsing("AT&F=\"a \\; b\"\n"); 287 | Test_parsing("AT&F=\"a \\\" b\"\n"); 288 | Test_parsing("AT&F=1234567890ABCDEF"); 289 | Test_parsing("AT&F=1234567890abcdef"); 290 | 291 | // Test 40 292 | Test_parsing("AT&F=1234567890 abcdef", NuATSyntaxError_t::AT_ERR_ILL_FORMED_NUMBER); 293 | Test_parsing("AT&F=\"\\41\\41\""); // Equal to "AA" 294 | Test_parsing("AT&F=\" \\\" \""); 295 | Test_parsing("AT&F=\"param\nATnotACommand\""); 296 | Test_parsing("AT\nAT&F"); 297 | 298 | Test_parsing("AT&F;&G\nAT&F;&G"); 299 | tester.allowLowerCase(true); 300 | Test_parsing("AT+F?;+f?\n"); 301 | 302 | tester.stopOnFirstFailure(true); 303 | Test_actionFlags("AT&FFF\n", false, false, false, false); 304 | Test_actionFlags("AT+F/\n", false, false, false, false); 305 | Test_actionFlags("AT&F\n", true, false, false, false); 306 | 307 | // Test #50 308 | Test_actionFlags("AT&F?\n", false, true, false, false); 309 | Test_actionFlags("AT&F=99\n", false, false, true, false); 310 | Test_actionFlags("AT&F=?\n", false, false, false, true); 311 | Test_actionFlags("AT&+F/;&G\n", false, false, false, false); 312 | Test_actionFlags("AT&G=?;&F\n", true, false, false, true); 313 | 314 | Test_actionFlags("AT&G;&F?\n", true, true, false, false); 315 | Test_actionFlags("AT&F;&G=99\n", true, false, true, false); 316 | Test_actionFlags("AT&F=1;&G=?\n", false, false, true, true); 317 | 318 | Serial.println("*****************************************"); 319 | Serial.println(" Non-Automated test (check visually) "); 320 | Serial.println("*****************************************"); 321 | 322 | // Test #58 323 | 324 | Test_setActionParameters("AT&F=\"value\"\n"); 325 | Test_setActionParameters("AT&F=1,2,3,4,5\n"); 326 | 327 | // Test #60 328 | Test_setActionParameters("AT&F=\"a \\\\ b\"\n"); 329 | Test_setActionParameters("AT&F=\"a \\, b\"\n"); 330 | Test_setActionParameters("AT&F=\"a \\; b\"\n"); 331 | Test_setActionParameters("AT&F=\"a \\\" b\"\n"); 332 | Test_setActionParameters("AT&F=\"a \\\n b\"\n"); 333 | 334 | Test_setActionParameters("AT&F=\"\\41\\42\"\n"); // Equals to "AB" 335 | Test_setActionParameters("AT&F=\"\\4\\5\"\n"); // Equals to "45" 336 | 337 | tester2.stopOnFirstFailure(false); 338 | tester2.allowLowerCase(false); 339 | 340 | // Serial.printf("(Installing callbacks)\n"); 341 | tester2.onError(testErrorCallback) 342 | .onExecute("a", testOkCallback) 343 | .onSet("a", testOkCallback) 344 | .onQuery("a", testOkCallback) 345 | .onTest("a", testOkCallback) 346 | .onNotACommandLine(testNoCommandLineCallback); 347 | // Serial.printf("(Callbacks installed)\n"); 348 | 349 | // Test #67 350 | Test_callbacks("AT&A;&A=?\nAT&A=1234;&A?"); 351 | Test_callbacks("AT&A;&B"); 352 | Test_callbacks("AT&A;ERROR;&A?"); 353 | 354 | // Test #70 355 | Test_callbacks("ATERROR;&A"); 356 | Test_callbacks("not an at command line"); 357 | Test_callbacks("AT&A\nnot an at command line"); 358 | tester2.allowLowerCase(true); 359 | Test_callbacks("AT&a"); 360 | 361 | // Test #74 362 | 363 | Serial.println("*****************************************"); 364 | Serial.println("END"); 365 | Serial.println("*****************************************"); 366 | } 367 | 368 | void loop() 369 | { 370 | delay(30000); 371 | } -------------------------------------------------------------------------------- /src/NuATCommandParserLegacy2.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file NuATCommandParserLegacy2.cpp 3 | * @author Ángel Fernández Pineda. Madrid. Spain. 4 | * @date 2023-12-24 5 | * @brief AT command parser 6 | * 7 | * @copyright Creative Commons Attribution 4.0 International (CC BY 4.0) 8 | * 9 | */ 10 | 11 | #include 12 | #include "NuATCommandParserLegacy2.hpp" 13 | 14 | //----------------------------------------------------------------------------- 15 | // Parsing macros 16 | //----------------------------------------------------------------------------- 17 | 18 | inline bool isATPreamble(const char *in, bool allowLowerCase) 19 | { 20 | return ((in[0] == 'A') && (in[1] == 'T')) || (allowLowerCase && (in[0] == 'a') && (in[1] == 't')); 21 | } 22 | 23 | inline bool isCommandEndToken(const char c) 24 | { 25 | return (c == '\n') || (c == '\0') || (c == ';'); 26 | } 27 | 28 | bool isAlphaString(const char *in) 29 | { 30 | while (((in[0] >= 'A') && (in[0] <= 'Z')) || ((in[0] >= 'a') && (in[0] <= 'z'))) 31 | in++; 32 | return (in[0] == '\0'); 33 | } 34 | 35 | const char *followingCommand(const char *in, NuATCommandResult_t conditional = AT_RESULT_OK) 36 | { 37 | if ((conditional < 0) || (in[0] == '\0') || (in[0] == '\n')) 38 | return nullptr; 39 | else if (in[0] == ';') 40 | return in + 1; 41 | else 42 | // should not enter here 43 | return nullptr; 44 | } 45 | 46 | const char *findSuffix(const char *in) 47 | { 48 | while ((in[0] != '\0') && (in[0] != '\n') && (in[0] != ';') && (in[0] != '?') && (in[0] != '=')) 49 | in++; 50 | return in; 51 | } 52 | 53 | //----------------------------------------------------------------------------- 54 | // Parsing machinery 55 | //----------------------------------------------------------------------------- 56 | 57 | void NuATCommandParser::parseCommandLine(const char *in) 58 | { 59 | lastParsingResult = AT_PR_NO_CALLBACKS; 60 | if (!pCmdCallbacks) 61 | // no callbacks: nothing to do here 62 | return; 63 | 64 | // Detect AT preamble 65 | if (isATPreamble(in, bLowerCasePreamble)) 66 | { 67 | // skip preamble 68 | in = in + 2; 69 | if ((in[0] == '\n') || (in[0] == '\0')) 70 | { 71 | // This is a preamble with no commands at all. 72 | // Response is OK to signal that AT commands are accepted. 73 | printResultResponse(AT_RESULT_OK); 74 | lastParsingResult = AT_PR_NO_COMMANDS; 75 | return; 76 | } 77 | } 78 | else 79 | { 80 | // Not an AT command line 81 | lastParsingResult = AT_PR_NO_PREAMBLE; 82 | try 83 | { 84 | pCmdCallbacks->onNonATCommand(in); 85 | } 86 | catch (...) 87 | { 88 | }; 89 | return; 90 | } 91 | 92 | // Parse all commands contained in incoming data 93 | int commandIndex = 0; 94 | do 95 | { 96 | // Serial.printf("parseCommandLine(): %s\n", in); 97 | lastParsingResult = AT_PR_OK; // may be changed later 98 | in = parseSingleCommand(in); 99 | try 100 | { 101 | pCmdCallbacks->onFinished(commandIndex++, lastParsingResult); 102 | } 103 | catch (...) 104 | { 105 | }; 106 | } while (in); 107 | } 108 | 109 | const char *NuATCommandParser::parseSingleCommand(const char *in) 110 | { 111 | // Detect prefix. 112 | // Note: if prefix is '&', just a single letter is allowed as command name 113 | // Serial.printf("parseSingleCommand(): %s\n", in); 114 | if ((in[0] == '&') || (in[0] == '+')) 115 | { 116 | // Prefix is valid, now detect suffix. 117 | // Text between a prefix and a suffix is a command name. 118 | // Text between a prefix and ";", "\n" or "\0" is also a command name. 119 | const char *suffix = findSuffix(in + 1); 120 | size_t cmdNameLength = suffix - (in + 1); 121 | if ((cmdNameLength > 0) && (cmdNameLength < bufferSize) && ((in[0] == '+') || (cmdNameLength == 1))) 122 | { 123 | // Serial.printf("parseSingleCommand(1): %s. Suffix: %s. Length: %d\n", in + 1, suffix, cmdNameLength); 124 | // store command name in "cmdName" as a null-terminated string 125 | // char cmdName[bufferSize]; 126 | char *cmdName = (char *)malloc(cmdNameLength + 1); 127 | if (!cmdName) 128 | { 129 | lastParsingResult = AT_PR_NO_HEAP; 130 | printResultResponse(AT_RESULT_ERROR); 131 | return nullptr; 132 | } 133 | // memcpy(cmdName, in + 1, cmdNameLength); 134 | for (int i = 0; i < cmdNameLength; i++) 135 | cmdName[i] = *(in + 1 + i); 136 | cmdName[cmdNameLength] = '\0'; 137 | // Serial.printf("parseSingleCommand(2): %s. Suffix: %s. Name: %s. Length: %d. pName %d. pIn+1:%d\n", in + 1, suffix, cmdName, cmdNameLength, cmdName, in + 1); 138 | 139 | if (isAlphaString(cmdName)) 140 | { 141 | // check if command is supported 142 | int commandId; 143 | try 144 | { 145 | commandId = pCmdCallbacks->getATCommandId(cmdName); 146 | } 147 | catch (...) 148 | { 149 | commandId = -1; 150 | } 151 | if (commandId >= 0) 152 | { 153 | // continue parsing 154 | free(cmdName); 155 | return parseAction(suffix, commandId); 156 | } 157 | else // this command is not supported 158 | lastParsingResult = AT_PR_UNSUPPORTED_CMD; 159 | } 160 | else // command name contains non-alphabetic characters 161 | lastParsingResult = AT_PR_INVALID_CMD2; 162 | 163 | free(cmdName); 164 | } 165 | else // error: no command name, buffer overflow or command name has "&" prefix but more than one letter 166 | lastParsingResult = AT_PR_INVALID_CMD1; 167 | 168 | } // invalid prefix 169 | else 170 | { 171 | lastParsingResult = AT_PR_INVALID_PREFIX; 172 | // Serial.printf("Invalid prefix\n"); 173 | } 174 | printResultResponse(AT_RESULT_ERROR); 175 | return nullptr; 176 | } 177 | 178 | const char *NuATCommandParser::parseAction(const char *in, int commandId) 179 | { 180 | // Serial.printf("parseAction(): %s\n", in); 181 | // Note: "in" points to a suffix or an end-of-command token 182 | if ((in[0] == '=') && (in[1] == '?')) 183 | { 184 | // This is a TEST command 185 | if (isCommandEndToken(in[2])) 186 | { 187 | NuATCommandResult_t result = AT_RESULT_OK; 188 | try 189 | { 190 | pCmdCallbacks->onTest(commandId); 191 | } 192 | catch (...) 193 | { 194 | result = AT_RESULT_ERROR; 195 | } 196 | printResultResponse(result); 197 | return followingCommand(in + 2, result); 198 | } // else syntax error 199 | } 200 | else if (in[0] == '?') 201 | { 202 | // This is a READ/QUERY command 203 | if (isCommandEndToken(in[1])) 204 | { 205 | NuATCommandResult_t response; 206 | try 207 | { 208 | response = pCmdCallbacks->onQuery(commandId); 209 | } 210 | catch (...) 211 | { 212 | response = AT_RESULT_ERROR; 213 | } 214 | printResultResponse(response); 215 | return followingCommand(in + 1, response); 216 | } // else syntax Error 217 | } 218 | else if (in[0] == '=') 219 | { 220 | // This is a SET/WRITE command 221 | return parseWriteParameters(in + 1, commandId); 222 | } 223 | else if (isCommandEndToken(in[0])) 224 | { 225 | // This is an EXECUTE Command 226 | NuATCommandResult_t response; 227 | try 228 | { 229 | response = pCmdCallbacks->onExecute(commandId); 230 | } 231 | catch (...) 232 | { 233 | response = AT_RESULT_ERROR; 234 | } 235 | printResultResponse(response); 236 | return followingCommand(in, response); 237 | } // else syntax error 238 | lastParsingResult = AT_PR_END_TOKEN_EXPECTED; 239 | printResultResponse(AT_RESULT_ERROR); 240 | return nullptr; 241 | } 242 | 243 | const char *NuATCommandParser::parseWriteParameters(const char *in, int commandId) 244 | { 245 | // See https://docs.espressif.com/projects/esp-at/en/release-v2.2.0.0_esp8266/AT_Command_Set/index.html 246 | // about parameters' syntax. 247 | 248 | NuATCommandParameters_t paramList; 249 | // char buffer[bufferSize]; 250 | char *buffer = (char *)malloc(bufferSize); 251 | if (!buffer) 252 | { 253 | lastParsingResult = AT_PR_NO_HEAP; 254 | printResultResponse(AT_RESULT_ERROR); 255 | return nullptr; 256 | } 257 | size_t l = 0; 258 | bool doubleQuotes = false; 259 | bool syntaxError = false; 260 | char *currentParam = buffer; 261 | 262 | // Parse, tokenize and copy parameters to buffer 263 | while (!isCommandEndToken(in[0]) && (l < bufferSize)) 264 | { 265 | if (doubleQuotes) 266 | { 267 | if ((in[0] == '\"') && ((in[1] == ',') || isCommandEndToken(in[1]))) 268 | { 269 | // Closing double quotes 270 | doubleQuotes = false; 271 | in++; 272 | continue; 273 | } 274 | else if (in[0] == '\"') 275 | { 276 | // there is more text after the closing double quotes 277 | syntaxError = true; 278 | break; 279 | } 280 | else if ((in[0] == '\\') && (in[1] != '\0')) 281 | { 282 | // Escaped character 283 | in++; 284 | buffer[l++] = in[0]; 285 | in++; 286 | continue; 287 | } 288 | } 289 | else 290 | { 291 | if ((in[0] == '\"') && (currentParam == (buffer + l))) 292 | { 293 | // Opening double quotes 294 | doubleQuotes = true; 295 | in++; 296 | continue; 297 | } 298 | else if (in[0] == '\"') 299 | { 300 | // There is some text before the opening double quotes 301 | syntaxError = true; 302 | break; 303 | } 304 | } 305 | 306 | // copy char to buffer and tokenize 307 | if (in[0] == ',') 308 | { 309 | // Serial.println("param token"); 310 | if (doubleQuotes) 311 | { 312 | // Missing closing double quotes 313 | syntaxError = true; 314 | break; 315 | } 316 | else 317 | { 318 | // End of this parameter 319 | buffer[l++] = '\0'; 320 | paramList.push_back(currentParam); 321 | // Serial.printf("Prev param: %s\n", currentParam); 322 | currentParam = (buffer + l); 323 | in++; 324 | } 325 | } 326 | else 327 | { 328 | buffer[l++] = in[0]; 329 | in++; 330 | } 331 | } // end-while 332 | 333 | // check for syntax errors or missing double quotes in last parameter 334 | if (syntaxError || doubleQuotes) 335 | { 336 | free(buffer); 337 | lastParsingResult = AT_PR_ILL_FORMED_STRING; 338 | printResultResponse(AT_RESULT_ERROR); 339 | return nullptr; 340 | } 341 | 342 | // check for buffer overflow 343 | if (l >= bufferSize) 344 | { 345 | free(buffer); 346 | lastParsingResult = AT_PR_SET_OVERFLOW; 347 | printResultResponse(AT_RESULT_ERROR); 348 | return nullptr; 349 | } 350 | 351 | // Add the last parameter 352 | buffer[l] = '\0'; 353 | paramList.push_back(currentParam); 354 | // Serial.printf("Last param: %s\n", currentParam); 355 | 356 | // Invoke callback 357 | NuATCommandResult_t response; 358 | try 359 | { 360 | response = pCmdCallbacks->onSet(commandId, paramList); 361 | } 362 | catch (...) 363 | { 364 | response = AT_RESULT_ERROR; 365 | } 366 | free(buffer); 367 | printResultResponse(response); 368 | return followingCommand(in, response); 369 | } 370 | 371 | //----------------------------------------------------------------------------- 372 | // Buffer size 373 | //----------------------------------------------------------------------------- 374 | 375 | void NuATCommandParser::setBufferSize(size_t size) 376 | { 377 | if (size > 5) 378 | { 379 | bufferSize = size; 380 | } 381 | else 382 | // absolute minimum 383 | bufferSize = 5; 384 | } 385 | 386 | //----------------------------------------------------------------------------- 387 | // Printing 388 | //----------------------------------------------------------------------------- 389 | 390 | void NuATCommandParser::printResultResponse(const NuATCommandResult_t response) 391 | { 392 | switch (response) 393 | { 394 | case AT_RESULT_INVALID_PARAM: 395 | printATResponse("INVALID INPUT PARAMETERS"); 396 | break; 397 | case AT_RESULT_ERROR: 398 | printATResponse("ERROR"); 399 | break; 400 | case AT_RESULT_OK: 401 | printATResponse("OK"); 402 | break; 403 | case AT_RESULT_SEND_OK: 404 | printATResponse("SEND OK"); 405 | break; 406 | case AT_RESULT_SEND_FAIL: 407 | printATResponse("SEND FAIL"); 408 | break; 409 | } 410 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nordic UART Service (NuS) and BLE serial communications (NimBLE stack) 2 | 3 | Library for serial communications through 4 | Bluetooth Low Energy on ESP32-Arduino boards 5 | 6 | In summary, this library provides: 7 | 8 | - A BLE serial communications object that can be used as Arduino's 9 | [Serial](https://www.arduino.cc/reference/en/language/functions/communication/serial/). 10 | - A BLE serial communications object that can handle incoming data in packets, 11 | eluding active waiting thanks to blocking semantics. 12 | - A customizable and easy to use 13 | [AT command](https://www.twilio.com/docs/iot/supersim/introduction-to-modem-at-commands) 14 | processor based on NuS. 15 | - A customizable [shell](https://en.wikipedia.org/wiki/Shell_(computing)) 16 | command processor based on NuS. 17 | - A generic class to implement custom protocols 18 | for serial communications through BLE. 19 | 20 | ## Supported DevKit boards 21 | 22 | Any DevKit supported by 23 | [NimBLE-Arduino](https://github.com/h2zero/NimBLE-Arduino). 24 | 25 | > [!NOTE] 26 | > Since version 3.3.0, *FreeRTOS* is no longer required. 27 | 28 | ## Installing and upgrading to a newer version 29 | 30 | The Arduino IDE should list this library in all available versions, 31 | but sometimes the *library indexer* **fails to catch updates**. 32 | In this case, download the ZIP file from the 33 | [releases section](https://github.com/afpineda/NuS-NimBLE-Serial/releases) 34 | or the `CODE` drop-down button found on this GitHub page (see above). 35 | Then, import the ZIP file into the Arduino IDE or install manually. 36 | For instructions, see the 37 | [official guide](https://docs.arduino.cc/software/ide-v1/tutorials/installing-libraries/). 38 | 39 | ## Introduction 40 | 41 | Serial communications are already available through the old 42 | [Bluetooth classic](https://www.argenox.com/library/bluetooth-classic/introduction-to-bluetooth-classic/) 43 | specification (see 44 | [this tutorial](https://circuitdigest.com/microcontroller-projects/using-classic-bluetooth-in-esp32-and-toogle-an-led)), 45 | [Serial Port Profile (SPP)](https://www.bluetooth.com/specifications/specs/serial-port-profile-1-2/). 46 | However, this is not the case with the 47 | [Bluetooth Low Energy (BLE) specification](https://en.wikipedia.org/wiki/Bluetooth_Low_Energy). 48 | **No standard** protocol was defined for serial communications in BLE 49 | (see [this article](https://punchthrough.com/serial-over-ble/) for further information). 50 | 51 | As Bluetooth Classic is being dropped in favor of BLE, an alternative is needed. 52 | [Nordic UART Service (NuS)](https://docs.nordicsemi.com/bundle/ncs-latest/page/nrf/libraries/bluetooth_services/services/nus.html) 53 | is a popular alternative, if not the *de facto* standard. 54 | This library implements the Nordic UART service on the *NimBLE-Arduino* stack. 55 | 56 | ## Client-side application 57 | 58 | You may need a generic terminal (PC or smartphone) application 59 | in order to communicate with your Arduino application through BLE. 60 | Such a generic application must support the Nordic UART Service. 61 | There are several free alternatives (known to me): 62 | 63 | - Android: 64 | - [nRF connect for mobile](https://play.google.com/store/apps/details?id=no.nordicsemi.android.mcp) 65 | - [Serial Bluetooth terminal](https://play.google.com/store/apps/details?id=de.kai_morich.serial_bluetooth_terminal) 66 | - iOS: 67 | - [nRF connect for mobile](https://apps.apple.com/es/app/nrf-connect-for-mobile/id1054362403) 68 | - Multi-platform: 69 | - [NeutralNUS](https://github.com/KevinJohnMulligan/neutral-nus-terminal/releases) 70 | 71 | > [!NOTE] 72 | > In Android, you have to enable both Bluetooth and geolocation, 73 | > otherwise, your device will not be discovered. 74 | 75 | ## How to use this library 76 | 77 | Summary: 78 | 79 | - The `NuSerial` object provides non-blocking serial communications through BLE, 80 | *Arduino's style*. 81 | - The `NuPacket` object provides blocking serial communications through BLE. 82 | - The `NuATCommands` object provides custom processing 83 | of AT commands through BLE. 84 | - The `NuShellCommands` object provides custom processing 85 | of shell commands through BLE. 86 | - Create your own object to provide a custom protocol 87 | based on serial communications through BLE, 88 | by deriving a new class from `NordicUARTService`. 89 | 90 | The **basic rules** are: 91 | 92 | - You must initialize the *NimBLE stack* **before** using this library. 93 | See [NimBLEDevice::init()](https://h2zero.github.io/NimBLE-Arduino/class_nim_b_l_e_device.html). 94 | 95 | > [!TIP] 96 | > Due to changes in *NimBLE-Arduino* version 2.1.0+ 97 | > you may need to manually add the device name to the advertised data: 98 | > 99 | > `NimBLEDevice::getAdvertising()->setName(DEVICE_NAME);` 100 | 101 | - You must also call `.start()` 102 | **after** all BLE initialization is complete. 103 | - By default, just one object can use the Nordic UART Service. 104 | For example, this code **fails** at run time: 105 | 106 | ```c++ 107 | void setup() { 108 | ... 109 | NuSerial.start(); 110 | NuPacket.start(); // raises an exception (runtime_error) 111 | } 112 | ``` 113 | 114 | Most client applications expect a single Nordic UART service in your device. 115 | However, at your own risk, you can start multiple objects by setting 116 | the static field `NordicUARTService::allowMultipleInstances` to `true` before 117 | calling `.start()`. 118 | 119 | - The Nordic UART Service can coexist 120 | with other GATT services in your application. 121 | This library does not require specific code for this. 122 | Just ignore the fact that *NuS-NimBLE-Serial* is there and 123 | register other services with *NimBLE-Arduino*. 124 | 125 | - Since version 3.1.0, `.isConnected()` and `.connect()` 126 | refer to devices connected **and subscribed** 127 | to the NuS transmission characteristic. 128 | If you have other services, 129 | a client may be connected but not using the Nordic UART Service. 130 | In this case, `.isConnected()` will return `false` 131 | but [NimBLEServer::getConnectedCount()](https://h2zero.github.io/NimBLE-Arduino/class_nim_b_l_e_server.html#a98ea12f57c10c0477b0c1c5efab23ee5) 132 | will return `1`. 133 | 134 | - By default, this library will automatically advertise 135 | existing GATT services when no peer is connected. 136 | This includes the Nordic UART Service and other 137 | services you configured for advertising (if any). 138 | To change this behavior, call `.start(false)` 139 | instead of `.start()` and handle advertising on your own. 140 | To disable automatic advertising once NuS is started, 141 | call `NimBLEDevice::getServer()->advertiseOnDisconnect(false)` and 142 | remove the service UUID (constant `NORDIC_UART_SERVICE_UUID`) 143 | from the advertised data (if required). 144 | 145 | - You can stop the service by calling `.stop()`. 146 | However, **this is discouraged** as there are **side effects**: 147 | all peer connections will be closed, 148 | advertising needs to be restarted and 149 | there is no thread safety. 150 | Design your application in a way that *NuS* does not need to be stopped. 151 | 152 | You may learn from the provided [examples](./examples/README.md). 153 | Read the [API documentation](https://afpineda.github.io/NuS-NimBLE-Serial/) 154 | for more information. 155 | 156 | ### Non-blocking serial communications 157 | 158 | ```c++ 159 | #include "NuSerial.hpp" 160 | ``` 161 | 162 | In short, 163 | use the `NuSerial` object as you do with the Arduino's `Serial` object. 164 | For example: 165 | 166 | ```c++ 167 | void setup() 168 | { 169 | ... 170 | NimBLEDevice::init("My device"); 171 | ... 172 | NuSerial.begin(115200); // Note: parameter is ignored 173 | } 174 | 175 | void loop() 176 | { 177 | if (NuSerial.available()) 178 | { 179 | // read incoming data and do something 180 | ... 181 | } else { 182 | // other background processing 183 | ... 184 | } 185 | } 186 | ``` 187 | 188 | Take into account: 189 | 190 | - `NuSerial` inherits from Arduino's `Stream`, 191 | so you can use it with other libraries. 192 | - As you should know, `read()` will immediately return 193 | if there is no data available. 194 | But, this is also the case when no peer device is connected. 195 | Use `NuSerial.isConnected()` to know the case (if you need to). 196 | - `NuSerial.begin()` or `NuSerial.start()` 197 | must be called at least once before reading. 198 | Calling more than once have no effect. 199 | - `NuSerial.end()` (as well as `NuSerial.disconnect()`) 200 | will terminate any peer connection. 201 | If you pretend to read again, 202 | it's not mandatory to call `NuSerial.begin()` (nor `NuSerial.start()`) again, 203 | but you can. 204 | - As a bonus, `NuSerial.readBytes()` does not perform active waiting, 205 | unlike `Serial.readBytes()`. 206 | - As you should know, `Stream` read methods are not thread-safe. 207 | Do not read from two different OS tasks. 208 | 209 | ### Blocking serial communications 210 | 211 | ```c++ 212 | #include "NuPacket.hpp" 213 | ``` 214 | 215 | Use the `NuPacket` object, based on blocking semantics. The advantages are: 216 | 217 | - Efficiency in terms of CPU usage, since no active waiting is used. 218 | - Performance, since incoming bytes are processed in packets, not one by one. 219 | - Simplicity. Only two methods are strictly needed: `read()` and `write()`. 220 | You don't need to worry about data being available or not. 221 | However, you have to handle packet size. 222 | 223 | For example: 224 | 225 | ```c++ 226 | void setup() 227 | { 228 | ... 229 | NimBLEDevice::init("My device"); 230 | ... // other initialization 231 | NuPacket.start(); // don't forget this!! 232 | } 233 | 234 | void loop() 235 | { 236 | size_t size; 237 | const uint8_t *data = NuPacket.read(size); // "size" is an output parameter 238 | while (data) 239 | { 240 | // do something with data and size 241 | ... 242 | data = NuPacket.read(size); 243 | } 244 | // No peer connection at this point 245 | } 246 | ``` 247 | 248 | Take into account: 249 | 250 | - **Just one** OS task can work with `NuPacket` (others will get blocked). 251 | - Data should be processed as soon as possible. 252 | Use other tasks and buffers/queues for time-consuming computation. 253 | While data is being processed, 254 | the peer will stay blocked, unable to send another packet. 255 | - If you just pretend to read a known-sized burst of bytes, 256 | `NuSerial.readBytes()` do the job with the same benefits as `NuPacket` 257 | and there is no need to manage packet sizes. 258 | Call `NuSerial.setTimeout(ULONG_MAX)` previously 259 | to get the blocking semantics. 260 | 261 | ### Custom AT commands 262 | 263 | ```c++ 264 | #include "NuATCommands.hpp" 265 | ``` 266 | 267 | **This API is new to version 3.x**. 268 | To keep **old** code working, use the following header instead: 269 | 270 | ```c++ 271 | #include "NuATCommandsLegacy2.hpp" 272 | using namespace NuSLegacy2; 273 | ``` 274 | 275 | - Call `NuATCommands.allowLowerCase()` 276 | and/or `NuATCommands.stopOnFirstFailure()` to your convenience. 277 | - Call `NuATCommands.on*()` to provide a command name 278 | and the callback to be executed if such a command is found. 279 | - `onExecute()`: commands with no suffix. 280 | - `onSet()`: commands with "=" suffix. 281 | - `onQuery()`: commands with "?" suffix. 282 | - `onTest()`: commands with "=?" suffix. 283 | - Call `NuATCommands.onNotACommandLine()` to provide a callback to be executed 284 | if non-AT text is received. 285 | - You may chain calls to "`on*()`" methods. 286 | - Call `NuATCommands.start()` 287 | 288 | Implementation is based in these sources: 289 | 290 | - [Espressif's AT command set](https://docs.espressif.com/projects/esp-at/en/release-v2.2.0.0_esp8266/AT_Command_Set/index.html) 291 | - [An Introduction to AT Commands](https://www.twilio.com/docs/iot/supersim/introduction-to-modem-at-commands) 292 | - [GSM AT Commands Tutorial](https://microcontrollerslab.com/at-commands-tutorial/#Response_of_AT_commands) 293 | - [General Syntax of Extended AT Commands](https://www.developershome.com/sms/atCommandsIntro2.asp) 294 | - [ITU-T recommendation V.250](./doc/T-REC-V.250-200307.pdf) 295 | - [AT command set for User Equipment (UE)](./doc/AT%20commands%20spec.docx) 296 | 297 | The following implementation details may be relevant to you: 298 | 299 | - ASCII, ANSI, and UTF8 character encodings are accepted, 300 | but note that AT commands are supposed to work in ASCII. 301 | - Only "extended syntax" is allowed 302 | (all commands must have a prefix, either "+" or "&"). 303 | This is non-standard behavior. 304 | - In string parameters (between double quotes), the following rules apply: 305 | - Write `\\` to insert a single backslash character (`\`). 306 | This is standard behavior. 307 | - Write `\"` to insert a single double quotes character (`"`). 308 | This is standard behavior. 309 | - Write `\` to insert a non-printable character in the ASCII table, 310 | where `` is a **two-digit** hexadecimal number. 311 | This is standard behavior. 312 | - The escape character (`\`) is ignored in all other cases. 313 | For example, `\a` is the same as `a`. 314 | This is non-standard behavior. 315 | - Any non-printable character is allowed without escaping. 316 | This is non-standard behavior. 317 | - In non-string parameters (without double quotes), 318 | a number is expected either in binary, decimal or hexadecimal format. 319 | No prefixes or suffixes are allowed to denote format. 320 | This is standard behavior. 321 | - Text after the line terminator (carriage return), if any, 322 | will be parsed as another command line. 323 | This is non-standard behavior. 324 | - Any text bigger than 256 bytes will be disregarded and handled as a 325 | syntax error in order to prevent denial of service attacks. 326 | However, you may disable or adjust this limit to your needs by calling 327 | `NuATCommands.maxCommandLineLength()`. 328 | 329 | As a bonus, you may use class `NuATParser` to implement an AT command processor 330 | that takes data from other sources. 331 | 332 | ### Custom shell commands 333 | 334 | ```c++ 335 | #include "NuShellCommands.hpp" 336 | 337 | void setup() 338 | { 339 | NuShellCommands 340 | .on("cmd1", [](NuCommandLine_t &commandLine) 341 | { 342 | // Note: commandLine[0] == "cmd1" 343 | // commandLine[1] is the first argument an so on 344 | ... 345 | } 346 | ) 347 | .on("cmd2", [](NuCommandLine_t &commandLine) 348 | { 349 | ... 350 | } 351 | ) 352 | .onUnknown([](NuCommandLine_t &commandLine) 353 | { 354 | Serial.printf("ERROR: unknown command \"%s\"\n",commandLine[0].c_str()); 355 | } 356 | ) 357 | .onParseError([](NuCLIParsingResult_t result, size_t index) 358 | { 359 | if (result == CLI_PR_ILL_FORMED_STRING) 360 | Serial.printf("Syntax error at character index %d\n",index); 361 | } 362 | ) 363 | .start(); 364 | } 365 | ``` 366 | 367 | - Call `NuShellCommands.caseSensitive()` to your convenience. 368 | By default, command names are not case-sensitive. 369 | - Call `on()` to provide a command name and the callback 370 | to be executed if such a command is found. 371 | - Call `onUnknown()` to provide a callback 372 | to be executed if the command line does not contain any command name. 373 | - Call `onParseError()` to provide a callback to be executed in case of error. 374 | - You can chain calls to "`on*`" methods. 375 | - Call `NuShellCommands.start()`. 376 | - Note that all callbacks will be executed at the NimBLE OS task, 377 | so make them thread-safe. 378 | 379 | Command line syntax: 380 | 381 | - Blank spaces, LF and CR characters are separators. 382 | - Command arguments are separated by one or more consecutive separators. 383 | For example, the command line `cmd arg1 arg2 arg3\n` 384 | is parsed as the command "cmd" with three arguments: 385 | "arg1", "arg2" and "arg3", being `\n` the LF character. 386 | `cmd arg1\narg2\n\narg3` would be parsed just the same. 387 | Usually, LF and CR characters are command line terminators, 388 | so don't worry about them. 389 | - Unquoted arguments can not contain a separator, 390 | but can contain double quotes. 391 | For example: `this"is"valid`. 392 | - Quoted arguments can contain a separator, 393 | but double quotes have to be escaped with another double quote. 394 | For example: 395 | `"this ""is"" valid"` is parsed to `this "is" valid` as a single argument. 396 | - ASCII, ANSI and UTF-8 character encodings are supported. 397 | Client software must use the same character encoding as your application. 398 | 399 | As a bonus, you may use class `NuCLIParser` 400 | to implement a shell that takes data from other sources. 401 | 402 | ### Custom serial communications protocol 403 | 404 | ```c++ 405 | #include "NuS.hpp" 406 | 407 | class MyCustomSerialProtocol: public NordicUARTService { 408 | public: 409 | void onWrite( 410 | NimBLECharacteristic *pCharacteristic, 411 | NimBLEConnInfo &connInfo) override; 412 | ... 413 | } 414 | ``` 415 | 416 | Derive a new class and override 417 | [onWrite()](https://h2zero.github.io/NimBLE-Arduino/class_nim_b_l_e_characteristic_callbacks.html). 418 | Then, use `pCharacteristic` to read incoming data. For example: 419 | 420 | ```c++ 421 | void MyCustomSerialProtocol::onWrite( 422 | NimBLECharacteristic *pCharacteristic, 423 | NimBLEConnInfo &connInfo) 424 | { 425 | // Retrieve a pointer to received data and its size 426 | NimBLEAttValue val = pCharacteristic->getValue(); 427 | const uint8_t *receivedData = val.data(); 428 | size_t receivedDataSize = val.size(); 429 | 430 | // Custom processing here 431 | ... 432 | } 433 | ``` 434 | 435 | In the previous example, 436 | the data pointed by `*receivedData` will **not remain valid** 437 | after `onWrite()` has finished to execute. 438 | If you need that data for later use, you must make a copy of the data itself, 439 | not just the pointer. 440 | For that purpose, 441 | you may store a non-local copy of the `pCharacteristic->getValue()` object. 442 | 443 | Since just one object can use the Nordic UART Service, 444 | you should also implement a 445 | [singleton pattern](https://www.geeksforgeeks.org/implementation-of-singleton-class-in-cpp/) 446 | (not mandatory). 447 | 448 | ## Licensed work 449 | 450 | [cyanhill/semaphore](https://github.com/cyanhill/semaphore) under MIT License. 451 | --------------------------------------------------------------------------------