A serial command line interface with buffer editing.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

376 lines
10 KiB

3 months ago
  1. #include "cmd.h"
  2. // Initiator.
  3. // Size is the number of commands, default callback is called if a command is
  4. // not found.
  5. Cmd::Cmd(size_t size, CmdFunction defaultCallback) {
  6. m_size = size;
  7. m_commands = (const char **)malloc(sizeof(const char *) * m_size);
  8. m_functions = (CmdFunction *)malloc(sizeof(CmdFunction) * m_size);
  9. m_defaultFunction = defaultCallback;
  10. }
  11. // Return the set size of the command array.
  12. size_t Cmd::GetSize() { return m_size; }
  13. // Add a command to the function list. If list is too full, false is returned.
  14. bool Cmd::AddCmd(const char *cmd, CmdFunction function) {
  15. if (m_nextCmd >= m_size) {
  16. return false;
  17. }
  18. m_commands[m_nextCmd] = cmd;
  19. m_functions[m_nextCmd] = function;
  20. m_nextCmd++;
  21. return true;
  22. }
  23. // Get array of all commands.
  24. const char **Cmd::GetCmds() { return m_commands; }
  25. // Rather or not we echo back to serial characters received.
  26. bool Cmd::GetEcho() { return m_echo; }
  27. void Cmd::SetEcho(bool echo) { m_echo = echo; }
  28. // The separator for command parsing.
  29. const char *Cmd::GetSeparator() { return m_separator; }
  30. void Cmd::SetSeparator(const char *separator) { m_separator = separator; }
  31. // Indicates the command line.
  32. const char *Cmd::GetLineIndicator() { return m_line_indicator; }
  33. void Cmd::SetLineIndicator(const char *line_indicator) {
  34. m_line_indicator = line_indicator;
  35. }
  36. // Buffer size configuration.
  37. size_t Cmd::GetBufferSize() { return m_buffer_size; }
  38. void Cmd::SetBufferSize(size_t bufferSize) { m_buffer_size = bufferSize; }
  39. // Return current buffer.
  40. const char *Cmd::GetBuffer() { return m_buffer; }
  41. // Resets the buffer and prints
  42. void Cmd::StartNewBuffer() {
  43. free(m_buffer);
  44. m_buffer = NULL;
  45. Serial.print(m_line_indicator);
  46. }
  47. // Print the current buffer.
  48. void Cmd::PrintBuffer() {
  49. // If we're processing a command, do not print.
  50. if (m_processing) {
  51. return;
  52. }
  53. // Set the last character in the buffer to null to terminate.
  54. m_buffer[m_buffer_read] = '\0';
  55. // Print indicator and buffer.
  56. Serial.print(m_line_indicator);
  57. Serial.print(m_buffer);
  58. // Move cursor to correct location.
  59. for (unsigned int i = m_buffer_cursor; i < m_buffer_read; i++) {
  60. Serial.write('\x08');
  61. }
  62. }
  63. // Parse next token.
  64. char *Cmd::Parse() { return strtok(NULL, m_separator); }
  65. // Print help for the current command.
  66. void Cmd::PrintHelp() {
  67. // Copy buffer to token buffer.
  68. m_bufferTok = (char *)malloc(m_buffer_size);
  69. strcpy(m_bufferTok, m_buffer);
  70. // Tokenize buffer based on separator and get first token being the command.
  71. char *cmd = strtok(m_bufferTok, m_separator);
  72. // Printing help means the command line is currently the line we're on.
  73. Serial.println();
  74. // To prevent buffer printing during a command execution.
  75. m_processing = true;
  76. // Look for the command that matches.
  77. bool foundCmd = false;
  78. // Only scan for command if specified.
  79. if (cmd != NULL) {
  80. for (unsigned int i = 0; i < m_size && m_commands[i] != NULL; i++) {
  81. if (strcasecmp_P(cmd, m_commands[i]) == 0) {
  82. // If command matches, call its function and tell it we're asking for
  83. // help.
  84. m_functions[i](this, cmd, true);
  85. foundCmd = true;
  86. break;
  87. }
  88. }
  89. }
  90. // If command wasn't found, call the default callback and tell it we're asking
  91. // for help.
  92. if (!foundCmd) {
  93. Serial.println("Calling default function");
  94. m_defaultFunction(this, cmd, true);
  95. }
  96. m_processing = false;
  97. // Print the buffer now that help was provided.
  98. Serial.println();
  99. PrintBuffer();
  100. // Free memory used by the token buffer.
  101. free(m_bufferTok);
  102. m_bufferTok = NULL;
  103. }
  104. // Parse buffer for command.
  105. void Cmd::ParseBuffer() {
  106. // Copy buffer to token buffer.
  107. m_bufferTok = (char *)malloc(m_buffer_size);
  108. strcpy(m_bufferTok, m_buffer);
  109. // Tokenize buffer based on separator and get first token being the command.
  110. char *cmd = strtok(m_bufferTok, m_separator);
  111. // To prevent buffer printing during a command execution.
  112. m_processing = true;
  113. // Look for the command that matches.
  114. bool foundCmd = false;
  115. // Only scan for command if specified.
  116. if (cmd != NULL) {
  117. for (unsigned int i = 0; i < m_size && m_commands[i] != NULL; i++) {
  118. if (strcasecmp_P(cmd, m_commands[i]) == 0) {
  119. // If command matches, call its function.
  120. m_functions[i](this, cmd, false);
  121. foundCmd = true;
  122. break;
  123. }
  124. }
  125. }
  126. // If command wasn't found, call the default callback.
  127. if (!foundCmd) {
  128. m_defaultFunction(this, cmd, false);
  129. }
  130. m_processing = false;
  131. // Free memory used by the token buffer.
  132. free(m_bufferTok);
  133. m_bufferTok = NULL;
  134. }
  135. // Main command loop, call in the main loop of your program.
  136. void Cmd::Loop() {
  137. // If no serial data is available, we do not have anything to process.
  138. if (!Serial.available()) {
  139. return;
  140. }
  141. // If buffer is not allocated, let's reset.
  142. if (m_buffer == NULL) {
  143. m_buffer = (char *)malloc(m_buffer_size);
  144. m_buffer_read = 0;
  145. m_buffer_cursor = 0;
  146. }
  147. // Start read and read all available data in serial RX buffer.
  148. bool receivedEndLine = false;
  149. size_t availableData = Serial.available();
  150. for (size_t i = 1; i <= availableData; i++) {
  151. char byteRead = Serial.read();
  152. // If we're currently reading an escape character, verify state.
  153. if (m_buffer_reading_esc == 1) {
  154. // If we get the bracket, we need to move on to the next step in reading
  155. // an escape character. Otherwise, we are not reading an escape character.
  156. if (byteRead == '[') {
  157. m_buffer_reading_esc = 2;
  158. continue;
  159. } else {
  160. m_buffer_reading_esc = 0;
  161. }
  162. } else if (m_buffer_reading_esc == 2) {
  163. // Process escape character read.
  164. switch (byteRead) {
  165. case 'D': // Move cursor left
  166. m_buffer_reading_esc = 0;
  167. if (m_buffer_cursor <= 0) {
  168. continue;
  169. }
  170. m_buffer_cursor--;
  171. Serial.print("\x1b[D");
  172. break;
  173. case 'C': // Move cursor right
  174. m_buffer_reading_esc = 0;
  175. if (m_buffer_cursor >= m_buffer_read) {
  176. continue;
  177. }
  178. m_buffer_cursor++;
  179. Serial.print("\x1b[C");
  180. break;
  181. default:
  182. m_buffer_reading_esc = 0;
  183. break;
  184. }
  185. continue;
  186. }
  187. // Check if ascii byte.
  188. bool is_ascii = byteRead >= 32 && byteRead <= 126;
  189. // If we're to echo and its an ascii byte, send the byte we read back to the
  190. // serial console.
  191. if (m_echo && is_ascii) {
  192. Serial.write(byteRead);
  193. }
  194. // If escape character received, move into escape reading mode.
  195. if (byteRead == '\x1b') {
  196. m_buffer_reading_esc = 1;
  197. continue;
  198. }
  199. // If backspace or delete key.
  200. if (byteRead == 8 || byteRead == 127) {
  201. // Only perform character delete if buffer exists and cursor isn't at
  202. // start.
  203. if (m_buffer_read != 0 && m_buffer_cursor != 0) {
  204. // If cursor isn't at start, we need to re-print the line minus the
  205. // character deleted.
  206. if (m_buffer_cursor != m_buffer_read) {
  207. // Create new buffer for re-writing current buffer.
  208. char *buf = (char *)malloc(m_buffer_size);
  209. // Copy current buffer.
  210. strcpy(buf, m_buffer);
  211. // Clear the line from the curosr.
  212. Serial.print("\x08\x1b[1P");
  213. // Print and re-write the buffer from the cursor location minus
  214. // character deleted.
  215. for (unsigned int i = m_buffer_cursor; i < m_buffer_read; i++) {
  216. m_buffer[i - 1] = buf[i];
  217. Serial.write(buf[i]);
  218. }
  219. // Now that the buffer has been re-written, we can free the buffer.
  220. free(buf);
  221. // Move the cursor back to where it should be.
  222. for (unsigned int i = m_buffer_cursor; i < m_buffer_read; i++) {
  223. Serial.write('\x08');
  224. }
  225. } else {
  226. // As we're not deleting from cursor location, we can just clear the
  227. // last character via this escape.
  228. Serial.print("\x08\x1b[K");
  229. }
  230. // Wipe the last byte in the buffer in both cases.
  231. m_buffer_read--;
  232. m_buffer_cursor--;
  233. m_buffer[m_buffer_read] = '\0';
  234. }
  235. }
  236. // If begining of line requested.
  237. if (byteRead == 1) {
  238. // Move cursor to beginning of line.
  239. while (m_buffer_cursor != 0) {
  240. m_buffer_cursor--;
  241. Serial.print("\x1b[D");
  242. }
  243. }
  244. // If end of line requested.
  245. if (byteRead == 5) {
  246. // Move cursor to end of line.
  247. while (m_buffer_cursor < m_buffer_read) {
  248. m_buffer_cursor++;
  249. Serial.print("\x1b[C");
  250. }
  251. }
  252. // If cancel or exit received.
  253. if (byteRead == 3 || byteRead == 4) {
  254. Serial.println();
  255. // Clear buffer, start new line,
  256. StartNewBuffer();
  257. continue;
  258. }
  259. // Clear screen.
  260. if (byteRead == 12) {
  261. m_buffer[m_buffer_read] = '\0';
  262. Serial.print("\x1b[H\x1b[J");
  263. PrintBuffer();
  264. }
  265. // If end of line.
  266. if (byteRead == '\r') {
  267. receivedEndLine = true;
  268. break;
  269. }
  270. // If help requested, print it.
  271. if (byteRead == '?') {
  272. m_buffer[m_buffer_read] = '\0';
  273. PrintHelp();
  274. continue;
  275. }
  276. // If not ascii, we only allow ascii on the cli.
  277. if (!is_ascii) {
  278. continue;
  279. }
  280. // If cursor is not at end, we need to write new byte where cursor is.
  281. if (m_buffer_cursor != m_buffer_read) {
  282. // Copy current buffer for re-write.
  283. char *buf = (char *)malloc(m_buffer_size);
  284. strcpy(buf, m_buffer);
  285. // Set current cursor location byte to newly read byte.
  286. m_buffer[m_buffer_cursor] = byteRead;
  287. // From cursor location, re-write the buffer with old buffer data and
  288. // print to the console.
  289. for (unsigned int i = m_buffer_cursor; i < m_buffer_read; i++) {
  290. m_buffer[i + 1] = buf[i];
  291. Serial.write(buf[i]);
  292. }
  293. // We're done with the buffer copy.
  294. free(buf);
  295. // Move cursor back to where it was.
  296. for (unsigned int i = m_buffer_cursor; i < m_buffer_read; i++) {
  297. Serial.write('\x08');
  298. }
  299. } else {
  300. // Otherwise new byte can go to end of buffer.
  301. m_buffer[m_buffer_read] = byteRead;
  302. }
  303. // We read a new byte, increment the cursor location and read index.
  304. m_buffer_read++;
  305. m_buffer_cursor++;
  306. // If we're going to overflow next read, we need to stop that.
  307. if (m_buffer_read >= (m_buffer_size - 1)) {
  308. Serial.println("Data too large.");
  309. // Clear the buffer, and start new line.
  310. StartNewBuffer();
  311. // Clear the serial buffer.
  312. while (Serial.available()) {
  313. Serial.read();
  314. }
  315. break;
  316. }
  317. }
  318. // If we received end of line in read, we need to parse the buffer.
  319. if (receivedEndLine) {
  320. // Print new line and null terminate the buffer.
  321. Serial.println();
  322. m_buffer[m_buffer_read] = '\0';
  323. // Parse the buffer.
  324. ParseBuffer();
  325. // Start new buffer.
  326. StartNewBuffer();
  327. }
  328. }