First commit

This commit is contained in:
GRMrGecko 2024-07-01 19:28:28 -05:00
commit 27b2cfb80b
7 changed files with 597 additions and 0 deletions

19
LICENSE.txt Normal file
View File

@ -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.

11
README.md Normal file
View File

@ -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
```

377
cmd.cpp Normal file
View File

@ -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();
}
}

67
cmd.h Normal file
View File

@ -0,0 +1,67 @@
#ifndef _CMD_H_
#define _CMD_H_
#include <Arduino.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
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_

View File

@ -0,0 +1,95 @@
#include <cmd.h>
// 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();
}

19
library.json Normal file
View File

@ -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": "*"
}

9
library.properties Normal file
View File

@ -0,0 +1,9 @@
name=cmd
version=1.0.0
author=James Coleman <grmrgecko@gmail.com>
maintainer=James Coleman <grmrgecko@gmail.com>
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=*