From 27b2cfb80bd4429f660dcb49dd9176188c759de5 Mon Sep 17 00:00:00 2001 From: GRMrGecko Date: Mon, 1 Jul 2024 19:28:28 -0500 Subject: [PATCH] First commit --- LICENSE.txt | 19 ++ README.md | 11 + cmd.cpp | 377 +++++++++++++++++++++++++++++++++ cmd.h | 67 ++++++ examples/standard/standard.ino | 95 +++++++++ library.json | 19 ++ library.properties | 9 + 7 files changed, 597 insertions(+) create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 cmd.cpp create mode 100644 cmd.h create mode 100644 examples/standard/standard.ino create mode 100644 library.json create mode 100644 library.properties diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..ade9c16 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (c) 2024 Mr. Gecko's Media (James Coleman). http://mrgeckosmedia.com/ + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1f36780 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# cmd + +This is a simple Arduino Library I wrote because the existing Libraries did not do what I was looking for. I wanted a simple command line interface that worked similar to Linux and Cisco clis where you can edit the buffer and ask for help. Its fairly simple to use: + +## Example usage +See the standard example .ino file. + +You can load the tty with: +```bash +screen -L /dev/ttyUSB0 9600 +``` diff --git a/cmd.cpp b/cmd.cpp new file mode 100644 index 0000000..2c1573a --- /dev/null +++ b/cmd.cpp @@ -0,0 +1,377 @@ +#include "cmd.h" + +// Initiator. +// Size is the number of commands, default callback is called if a command is +// not found. +Cmd::Cmd(size_t size, CmdFunction defaultCallback) { + m_size = size; + m_commands = (const char **)malloc(sizeof(const char *) * m_size); + m_functions = (CmdFunction *)malloc(sizeof(CmdFunction) * m_size); + m_defaultFunction = defaultCallback; +} + +// Return the set size of the command array. +size_t Cmd::GetSize() { return m_size; } + +// Add a command to the function list. If list is too full, false is returned. +bool Cmd::AddCmd(const char *cmd, CmdFunction function) { + if (m_nextCmd >= m_size) { + return false; + } + + m_commands[m_nextCmd] = cmd; + m_functions[m_nextCmd] = function; + + m_nextCmd++; + return true; +} + +// Get array of all commands. +const char **Cmd::GetCmds() { return m_commands; } + +// Rather or not we echo back to serial characters received. +bool Cmd::GetEcho() { return m_echo; } +void Cmd::SetEcho(bool echo) { m_echo = echo; } + +// The separator for command parsing. +const char *Cmd::GetSeparator() { return m_separator; } +void Cmd::SetSeparator(const char *separator) { m_separator = separator; } + +// Indicates the command line. +const char *Cmd::GetLineIndicator() { return m_line_indicator; } +void Cmd::SetLineIndicator(const char *line_indicator) { + m_line_indicator = line_indicator; +} + +// Buffer size configuration. +size_t Cmd::GetBufferSize() { return m_buffer_size; } +void Cmd::SetBufferSize(size_t bufferSize) { m_buffer_size = bufferSize; } + +// Return current buffer. +const char *Cmd::GetBuffer() { return m_buffer; } + +// Resets the buffer and prints +void Cmd::StartNewBuffer() { + free(m_buffer); + m_buffer = NULL; + Serial.print(m_line_indicator); +} + +// Print the current buffer. +void Cmd::PrintBuffer() { + // If we're processing a command, do not print. + if (m_processing) { + return; + } + // Set the last character in the buffer to null to terminate. + m_buffer[m_buffer_read] = '\0'; + // Print indicator and buffer. + Serial.print(m_line_indicator); + Serial.print(m_buffer); + // Move cursor to correct location. + for (unsigned int i = m_buffer_cursor; i < m_buffer_read; i++) { + Serial.write('\x08'); + } +} + +// Parse next token. +char *Cmd::Parse() { return strtok(NULL, m_separator); } + +// Print help for the current command. +void Cmd::PrintHelp() { + // Copy buffer to token buffer. + m_bufferTok = (char *)malloc(m_buffer_size); + strcpy(m_bufferTok, m_buffer); + + // Tokenize buffer based on separator and get first token being the command. + char *cmd = strtok(m_bufferTok, m_separator); + + // Printing help means the command line is currently the line we're on. + Serial.println(); + + // To prevent buffer printing during a command execution. + m_processing = true; + // Look for the command that matches. + bool foundCmd = false; + // Only scan for command if specified. + if (cmd != NULL) { + for (unsigned int i = 0; i < m_size && m_commands[i] != NULL; i++) { + if (strcasecmp_P(cmd, m_commands[i]) == 0) { + // If command matches, call its function and tell it we're asking for + // help. + m_functions[i](this, cmd, true); + foundCmd = true; + break; + } + } + } + + // If command wasn't found, call the default callback and tell it we're asking + // for help. + if (!foundCmd) { + Serial.println("Calling default function"); + m_defaultFunction(this, cmd, true); + } + m_processing = false; + + // Print the buffer now that help was provided. + Serial.println(); + PrintBuffer(); + + // Free memory used by the token buffer. + free(m_bufferTok); + m_bufferTok = NULL; +} + +// Parse buffer for command. +void Cmd::ParseBuffer() { + // Copy buffer to token buffer. + m_bufferTok = (char *)malloc(m_buffer_size); + strcpy(m_bufferTok, m_buffer); + + // Tokenize buffer based on separator and get first token being the command. + char *cmd = strtok(m_bufferTok, m_separator); + + // To prevent buffer printing during a command execution. + m_processing = true; + // Look for the command that matches. + bool foundCmd = false; + // Only scan for command if specified. + if (cmd != NULL) { + for (unsigned int i = 0; i < m_size && m_commands[i] != NULL; i++) { + if (strcasecmp_P(cmd, m_commands[i]) == 0) { + // If command matches, call its function. + m_functions[i](this, cmd, false); + foundCmd = true; + break; + } + } + } + + // If command wasn't found, call the default callback. + if (!foundCmd) { + m_defaultFunction(this, cmd, false); + } + m_processing = false; + + // Free memory used by the token buffer. + free(m_bufferTok); + m_bufferTok = NULL; +} + +// Main command loop, call in the main loop of your program. +void Cmd::Loop() { + // If no serial data is available, we do not have anything to process. + if (!Serial.available()) { + return; + } + + // If buffer is not allocated, let's reset. + if (m_buffer == NULL) { + m_buffer = (char *)malloc(m_buffer_size); + m_buffer_read = 0; + m_buffer_cursor = 0; + } + + // Start read and read all available data in serial RX buffer. + bool receivedEndLine = false; + size_t availableData = Serial.available(); + for (size_t i = 1; i <= availableData; i++) { + char byteRead = Serial.read(); + + // If we're currently reading an escape character, verify state. + if (m_buffer_reading_esc == 1) { + // If we get the bracket, we need to move on to the next step in reading + // an escape character. Otherwise, we are not reading an escape character. + if (byteRead == '[') { + m_buffer_reading_esc = 2; + continue; + } else { + m_buffer_reading_esc = 0; + } + } else if (m_buffer_reading_esc == 2) { + // Process escape character read. + switch (byteRead) { + case 'D': // Move cursor left + m_buffer_reading_esc = 0; + if (m_buffer_cursor <= 0) { + continue; + } + m_buffer_cursor--; + Serial.print("\x1b[D"); + break; + case 'C': // Move cursor right + m_buffer_reading_esc = 0; + if (m_buffer_cursor >= m_buffer_read) { + continue; + } + m_buffer_cursor++; + Serial.print("\x1b[C"); + break; + + default: + m_buffer_reading_esc = 0; + break; + } + continue; + } + + // Check if ascii byte. + bool is_ascii = byteRead >= 32 && byteRead <= 126; + + // If we're to echo and its an ascii byte, send the byte we read back to the + // serial console. + if (m_echo && is_ascii) { + Serial.write(byteRead); + } + + // If escape character received, move into escape reading mode. + if (byteRead == '\x1b') { + m_buffer_reading_esc = 1; + continue; + } + + // If backspace or delete key. + if (byteRead == 8 || byteRead == 127) { + // Only perform character delete if buffer exists and cursor isn't at + // start. + if (m_buffer_read != 0 && m_buffer_cursor != 0) { + // If cursor isn't at start, we need to re-print the line minus the + // character deleted. + if (m_buffer_cursor != m_buffer_read) { + // Create new buffer for re-writing current buffer. + char *buf = (char *)malloc(m_buffer_size); + // Copy current buffer. + strcpy(buf, m_buffer); + // Clear the line from the curosr. + Serial.print("\x08\x1b[1P"); + // Print and re-write the buffer from the cursor location minus + // character deleted. + for (unsigned int i = m_buffer_cursor; i < m_buffer_read; i++) { + m_buffer[i - 1] = buf[i]; + Serial.write(buf[i]); + } + // Now that the buffer has been re-written, we can free the buffer. + free(buf); + // Move the cursor back to where it should be. + for (unsigned int i = m_buffer_cursor; i < m_buffer_read; i++) { + Serial.write('\x08'); + } + } else { + // As we're not deleting from cursor location, we can just clear the + // last character via this escape. + Serial.print("\x08\x1b[K"); + } + // Wipe the last byte in the buffer in both cases. + m_buffer_read--; + m_buffer_cursor--; + m_buffer[m_buffer_read] = '\0'; + } + } + + // If begining of line requested. + if (byteRead == 1) { + // Move cursor to beginning of line. + while (m_buffer_cursor != 0) { + m_buffer_cursor--; + Serial.print("\x1b[D"); + } + } + + // If end of line requested. + if (byteRead == 5) { + // Move cursor to end of line. + while (m_buffer_cursor < m_buffer_read) { + m_buffer_cursor++; + Serial.print("\x1b[C"); + } + } + + // If cancel or exit received. + if (byteRead == 3 || byteRead == 4) { + Serial.println(); + // Clear buffer, start new line, + StartNewBuffer(); + continue; + } + + // Clear screen. + if (byteRead == 12) { + m_buffer[m_buffer_read] = '\0'; + Serial.print("\x1b[H\x1b[J"); + PrintBuffer(); + } + + // If end of line. + if (byteRead == '\r') { + receivedEndLine = true; + break; + } + + // If help requested, print it. + if (byteRead == '?') { + m_buffer[m_buffer_read] = '\0'; + PrintHelp(); + continue; + } + + // If not ascii, we only allow ascii on the cli. + if (!is_ascii) { + continue; + } + + // If cursor is not at end, we need to write new byte where cursor is. + if (m_buffer_cursor != m_buffer_read) { + // Copy current buffer for re-write. + char *buf = (char *)malloc(m_buffer_size); + strcpy(buf, m_buffer); + // Set current cursor location byte to newly read byte. + m_buffer[m_buffer_cursor] = byteRead; + // From cursor location, re-write the buffer with old buffer data and + // print to the console. + for (unsigned int i = m_buffer_cursor; i < m_buffer_read; i++) { + m_buffer[i + 1] = buf[i]; + Serial.write(buf[i]); + } + // We're done with the buffer copy. + free(buf); + // Move cursor back to where it was. + for (unsigned int i = m_buffer_cursor; i < m_buffer_read; i++) { + Serial.write('\x08'); + } + } else { + // Otherwise new byte can go to end of buffer. + m_buffer[m_buffer_read] = byteRead; + } + // We read a new byte, increment the cursor location and read index. + m_buffer_read++; + m_buffer_cursor++; + + // If we're going to overflow next read, we need to stop that. + if (m_buffer_read >= (m_buffer_size - 1)) { + Serial.println("Data too large."); + + // Clear the buffer, and start new line. + StartNewBuffer(); + + // Clear the serial buffer. + while (Serial.available()) { + Serial.read(); + } + break; + } + } + + // If we received end of line in read, we need to parse the buffer. + if (receivedEndLine) { + // Print new line and null terminate the buffer. + Serial.println(); + m_buffer[m_buffer_read] = '\0'; + + // Parse the buffer. + ParseBuffer(); + + // Start new buffer. + StartNewBuffer(); + } +} \ No newline at end of file diff --git a/cmd.h b/cmd.h new file mode 100644 index 0000000..4774c3e --- /dev/null +++ b/cmd.h @@ -0,0 +1,67 @@ +#ifndef _CMD_H_ +#define _CMD_H_ + +#include +#include +#include +#include + +class Cmd; + +typedef void (*CmdFunction)(Cmd *thisCmd, char *command, bool printHelp); + +class Cmd { + protected: + const char **m_commands = NULL; + CmdFunction *m_functions = NULL; + + CmdFunction m_defaultFunction; + + size_t m_size = 0; + size_t m_nextCmd = 0; + + bool m_echo = true; + bool m_processing = false; + const char *m_separator = " "; + const char *m_line_indicator = "$ "; + size_t m_buffer_size = 50; + + char *m_buffer = NULL; + char *m_bufferTok = NULL; + size_t m_buffer_read = 0; + uint8_t m_buffer_reading_esc = 0; + size_t m_buffer_cursor = 0; + + void PrintHelp(); + void ParseBuffer(); + void StartNewBuffer(); + + public: + Cmd(size_t size, CmdFunction defaultCallback); + + size_t GetSize(); + bool AddCmd(const char *cmd, CmdFunction function); + const char **GetCmds(); + + bool GetEcho(); + void SetEcho(bool echo); + + const char *GetSeparator(); + void SetSeparator(const char *separator); + + const char *GetLineIndicator(); + void SetLineIndicator(const char *line_indicator); + + size_t GetBufferSize(); + void SetBufferSize(size_t bufferSize); + + const char *GetBuffer(); + void PrintBuffer(); + char *Parse(); + + void SendESC(const char *code); + + void Loop(); +}; + +#endif // _CMD_H_ \ No newline at end of file diff --git a/examples/standard/standard.ino b/examples/standard/standard.ino new file mode 100644 index 0000000..995c68c --- /dev/null +++ b/examples/standard/standard.ino @@ -0,0 +1,95 @@ +#include + +// Global command variable. +Cmd *cmd; + +// Command entered was invalid, or help is being requested. +void cmd_unrecognized(Cmd *thisCmd, char *command, bool printHelp) { + // If help is being requested, print the available commands. + if (printHelp) { + // Get the command parameters. + size_t size = cmd->GetSize(); + PGM_P *commands = cmd->GetCmds(); + + // Print each command. + Serial.println("Available commands:\n"); + for (int i = 0; i < size && commands[i] != NULL; i++) { + char buf[100]; + sprintf_P(buf, commands[i]); + Serial.println(buf); + } + // Stop here. + return; + } + // No help was requested, so the command provided likely doesn't exist. + Serial.print("Unrecognized command ["); + Serial.print(command); + Serial.println("]"); +} + +// Simple echo ping command. +void cmd_pi(Cmd *thisCmd, char *command, bool printHelp) { + // If help was requested, print the help for this command. + if (printHelp) { + Serial.print(command); + Serial.println(" *"); + return; + } + // Echo back the buffer. + Serial.println(cmd->GetBuffer()); +} + +// Demo send command. +void cmd_send(Cmd *thisCmd, char *command, bool printHelp) { + // If help was requested, print the help. + if (printHelp) { + Serial.print(command); + Serial.println(" address code"); + return; + } + + // Parse the next available argument. + char *parsed = cmd->Parse(); + if (parsed == NULL) { + Serial.println("Invalid address"); + return; + } + // Parse integer. + int address = atoi(parsed); + + // Get the next argument. + parsed = cmd->Parse(); + if (parsed == NULL) { + Serial.println("Invalid code"); + return; + } + // Parse char. + unsigned char code = atoi(parsed); + + // Print parsed arguments. + Serial.print("Sending code: "); + Serial.print(code); + Serial.print(" to <"); + Serial.print(address); + Serial.println(">"); +} + +void setup() { + // Setup serial interface. + Serial.begin(9600); + + // Initialize the command line with 2 commands. + cmd = new Cmd(2, cmd_unrecognized); + + // Add commands. + cmd->AddCmd(PSTR("pi"), cmd_pi); + cmd->AddCmd(PSTR("send"), cmd_send); + + // Print a line indicator to inform the user the cli is ready. + Serial.print(cmd->GetLineIndicator()); +} + +void loop() { + // Run the command line loop. + cmd->Loop(); +} diff --git a/library.json b/library.json new file mode 100644 index 0000000..e50fb27 --- /dev/null +++ b/library.json @@ -0,0 +1,19 @@ +{ + "name": "cmd", + "description": "A serial command line interface with buffer editing.", + "keywords": "serial, cmd, communication, processing, command", + "authors": + { + "name": "James Coleman", + "email": "grmrgecko@gmail.com", + "url": "https://github.com/GRMrGecko" + }, + "repository": + { + "type": "git", + "url": "https://github.com/GRMrGecko/cmd.git" + }, + "version": "1.0.0", + "frameworks": "arduino", + "platforms": "*" + } \ No newline at end of file diff --git a/library.properties b/library.properties new file mode 100644 index 0000000..463f074 --- /dev/null +++ b/library.properties @@ -0,0 +1,9 @@ +name=cmd +version=1.0.0 +author=James Coleman +maintainer=James Coleman +sentence=A serial command line interface with buffer editing. +paragraph=An easy to use command line interface, simply add commands and their callbacks. +category=Data Processing +url=https://github.com/GRMrGecko/cmd +architectures=* \ No newline at end of file