]> git.earman.xyz Git - sensor-watch.git/commitdiff
USB Improvements
authorEdward Shin <contact@edwardsh.in>
Sun, 15 Oct 2023 04:36:49 +0000 (00:36 -0400)
committerEdward Shin <contact@edwardsh.in>
Sun, 7 Jan 2024 05:20:20 +0000 (00:20 -0500)
* Introduce shell module for basic serial shell with argument parsing
* Introduce shell_cmd_list module for basic compile-time command
  registration
* Harden USB handling to hang less and drop fewer inputs
  - Service tud_task() with periodic TC0 timer interrupt
  - Service cdc_task() with periodic TC1 timer interrupt
  - Handle shell servicing in main app loop
  - Add a circular buffering layer for reads/writes
* Change newline prints to also send carriage return
* Refactor filesystem commands for shell subsystem
* Introduce new shell commands:
  - 'help' command
  - 'flash' command to reset into bootloader
  - 'stress' command to stress CDC writes

Testing:
* Shell validated on Sensor Watch Blue w/ Linux host
* Shell validated in emscripten emulator
* Tuned by spamming inputs during `stress` cmd until stack didn't crash

15 files changed:
make.mk
movement/filesystem.c
movement/filesystem.h
movement/make/Makefile
movement/movement.c
movement/shell.c [new file with mode: 0644]
movement/shell.h [new file with mode: 0644]
movement/shell_cmd_list.c [new file with mode: 0644]
movement/shell_cmd_list.h [new file with mode: 0644]
watch-library/hardware/main.c
watch-library/hardware/watch/watch_private.c
watch-library/hardware/watch/watch_private_cdc.c [new file with mode: 0644]
watch-library/hardware/watch/watch_private_cdc.h [new file with mode: 0644]
watch-library/shared/watch/watch.h
watch-library/shared/watch/watch_private.h

diff --git a/make.mk b/make.mk
index 955ea3102004b2e2c6207452b6ca378517082dcd..891f9363ec7b91367a79ded88f3eff8ba1482296 100644 (file)
--- a/make.mk
+++ b/make.mk
@@ -121,6 +121,7 @@ SRCS += \
   $(TOP)/watch-library/hardware/watch/watch_storage.c \
   $(TOP)/watch-library/hardware/watch/watch_deepsleep.c \
   $(TOP)/watch-library/hardware/watch/watch_private.c \
+  $(TOP)/watch-library/hardware/watch/watch_private_cdc.c \
   $(TOP)/watch-library/hardware/watch/watch.c \
   $(TOP)/watch-library/hardware/hal/src/hal_atomic.c \
   $(TOP)/watch-library/hardware/hal/src/hal_delay.c \
index 97e35455d71e53bdbe2f297e97fa945a08b666ae..9df0a8d29077b2193139cbb011a2f35ec4e15c9b 100644 (file)
@@ -100,7 +100,7 @@ static int filesystem_ls(lfs_t *lfs, const char *path) {
 
         printf("%4ld bytes ", info.size);
 
-        printf("%s\n", info.name);
+        printf("%s\r\n", info.name);
     }
 
     err = lfs_dir_close(lfs, &dir);
@@ -117,11 +117,11 @@ bool filesystem_init(void) {
     // reformat if we can't mount the filesystem
     // this should only happen on the first boot
     if (err < 0) {
-        printf("Ignore that error! Formatting filesystem...\n");
+        printf("Ignore that error! Formatting filesystem...\r\n");
         err = lfs_format(&lfs, &cfg);
         if (err < 0) return false;
         err = lfs_mount(&lfs, &cfg) == LFS_ERR_OK;
-        printf("Filesystem mounted with %ld bytes free.\n", filesystem_get_free_space());
+        printf("Filesystem mounted with %ld bytes free.\r\n", filesystem_get_free_space());
     }
 
     return err == LFS_ERR_OK;
@@ -139,7 +139,7 @@ bool filesystem_rm(char *filename) {
     if (filesystem_file_exists(filename)) {
         return lfs_remove(&lfs, filename) == LFS_ERR_OK;
     } else {
-        printf("rm: %s: No such file\n", filename);
+        printf("rm: %s: No such file\r\n", filename);
         return false;
     }
 }
@@ -197,13 +197,13 @@ static void filesystem_cat(char *filename) {
             char *buf = malloc(info.size + 1);
             filesystem_read_file(filename, buf, info.size);
             buf[info.size] = '\0';
-            printf("%s\n", buf);
+            printf("%s\r\n", buf);
             free(buf);
         } else {
-            printf("\n");
+            printf("\r\n");
         }
     } else {
-        printf("cat: %s: No such file\n", filename);
+        printf("cat: %s: No such file\r\n", filename);
     }
 }
 
@@ -223,59 +223,60 @@ bool filesystem_append_file(char *filename, char *text, int32_t length) {
     return lfs_file_close(&lfs, &file) == LFS_ERR_OK;
 }
 
-void filesystem_process_command(char *line) {
-    printf("$ %s", line);
-    char *command = strtok(line, " \n");
-    
-    if (strcmp(command, "ls") == 0) {
-        char *directory = strtok(NULL, " \n");
-        if (directory == NULL) {
-            filesystem_ls(&lfs, "/");
-        } else {
-            printf("usage: ls\n");
-        }
-    } else if (strcmp(command, "cat") == 0) {
-        char *filename = strtok(NULL, " \n");
-        if (filename == NULL) {
-            printf("usage: cat file\n");
-        } else {
-            filesystem_cat(filename);
-        }
-    } else if (strcmp(command, "df") == 0) {
-        printf("free space: %ld bytes\n", filesystem_get_free_space());
-    } else if (strcmp(command, "rm") == 0) {
-        char *filename = strtok(NULL, " \n");
-        if (filename == NULL) {
-            printf("usage: rm file\n");
-        } else {
-            filesystem_rm(filename);
-        } 
-    } else if (strcmp(command, "echo") == 0) {
-        char *text = malloc(248);
-        memset(text, 0, 248);
-        size_t pos = 0;
-        char *word = strtok(NULL, " \n");
-        while (strcmp(word, ">") && strcmp(word, ">>")) {
-            sprintf(text + pos, "%s ", word);
-            pos += strlen(word) + 1;
-            word = strtok(NULL, " \n");
-            if (word == NULL) break;
-        }
-        text[strlen(text) - 1] = 0;
-        char *filename = strtok(NULL, " \n");
-        if (filename == NULL) {
-            printf("usage: echo text > file\n");
-        } else if (strchr(filename, '/') || strchr(filename, '\\')) {
-            printf("subdirectories are not supported\n");
-        } else if (!strcmp(word, ">")) {
-            filesystem_write_file(filename, text, strlen(text));
-            filesystem_append_file(filename, "\n", 1);
-        } else if (!strcmp(word, ">>")) {
-            filesystem_append_file(filename, text, strlen(text));
-            filesystem_append_file(filename, "\n", 1);
-        }
-        free(text);
+int filesystem_cmd_ls(int argc, char *argv[]) {
+    if (argc >= 2) {
+        filesystem_ls(&lfs, argv[1]);
     } else {
-        printf("%s: command not found\n", command);
+        filesystem_ls(&lfs, "/");
     }
+    return 0;
 }
+
+int filesystem_cmd_cat(int argc, char *argv[]) {
+    (void) argc;
+    filesystem_cat(argv[1]);
+    return 0;
+}
+
+int filesystem_cmd_df(int argc, char *argv[]) {
+    (void) argc;
+    (void) argv;
+    printf("free space: %ld bytes\r\n", filesystem_get_free_space());
+    return 0;
+}
+
+int filesystem_cmd_rm(int argc, char *argv[]) {
+    (void) argc;
+    filesystem_rm(argv[1]);
+    return 0;
+}
+
+int filesystem_cmd_echo(int argc, char *argv[]) {
+    (void) argc;
+
+    char *line = argv[1];
+    size_t line_len = strlen(line);
+    if (line[0] == '"' || line[0] == '\'') {
+        line++;
+        line_len -= 2;
+        line[line_len] = '\0';
+    }
+
+    if (strchr(argv[3], '/')) {
+        printf("subdirectories are not supported\r\n");
+        return -2;
+    }
+
+    if (!strcmp(argv[2], ">")) {
+        filesystem_write_file(argv[3], line, line_len);
+        filesystem_append_file(argv[3], "\n", 1);
+    } else if (!strcmp(argv[2], ">>")) {
+        filesystem_append_file(argv[3], line, line_len);
+        filesystem_append_file(argv[3], "\n", 1);
+    } else {
+        return -2;
+    }
+
+    return 0;
+}
+
index 3cd3d0921d09d0d611c1e4d79e1b5a25d7fd7bec..fa3d9d1ad19e52acf8b18bc97524993683b933a0 100644 (file)
@@ -96,9 +96,10 @@ bool filesystem_write_file(char *filename, char *text, int32_t length);
   */
 bool filesystem_append_file(char *filename, char *text, int32_t length);
 
-/** @brief Handles the interactive file browser when Movement is plugged in to USB.
-  * @param line The command that the user typed into the serial console.
-  */
-void filesystem_process_command(char *line);
+int filesystem_cmd_ls(int argc, char *argv[]);
+int filesystem_cmd_cat(int argc, char *argv[]);
+int filesystem_cmd_df(int argc, char *argv[]);
+int filesystem_cmd_rm(int argc, char *argv[]);
+int filesystem_cmd_echo(int argc, char *argv[]);
 
 #endif // FILESYSTEM_H_
index 512f2ea803a68770bbaf4090a7c02c82b058de7a..cc6a32de569d1ad9b607df3d712a25803380563c 100644 (file)
@@ -49,6 +49,8 @@ SRCS += \
   ../../littlefs/lfs_util.c \
   ../movement.c \
   ../filesystem.c \
+  ../shell.c \
+  ../shell_cmd_list.c \
   ../watch_faces/clock/simple_clock_face.c \
   ../watch_faces/clock/world_clock_face.c \
   ../watch_faces/clock/beats_face.c \
index f0868416eb0b2f9cee5271a732692f8c0e5aea91..b6c119786bcd6fc909bd2c20206f598f1416d685 100644 (file)
@@ -33,6 +33,7 @@
 #include "watch.h"
 #include "filesystem.h"
 #include "movement.h"
+#include "shell.h"
 
 #ifndef MOVEMENT_FIRMWARE
 #include "movement_config.h"
@@ -538,30 +539,9 @@ bool app_loop(void) {
         }
     }
 
-    // if we are plugged into USB, handle the file browser tasks
+    // if we are plugged into USB, handle the serial shell
     if (watch_is_usb_enabled()) {
-        char line[256] = {0};
-#if __EMSCRIPTEN__
-        // This is a terrible hack; ideally this should be handled deeper in the watch library.
-        // Alas, emscripten treats read() as something that should pop up an input box, so I
-        // wasn't able to implement this over there. I sense that this relates to read() being
-        // the wrong way to read data from USB (like we should be using fgets or something), but
-        // until I untangle that, this will have to do.
-        char *received_data = (char*)EM_ASM_INT({
-            var len = lengthBytesUTF8(tx) + 1;
-            var s = _malloc(len);
-            stringToUTF8(tx, s, len);
-            return s;
-        });
-        memcpy(line, received_data, min(255, strlen(received_data)));
-        free(received_data);
-        EM_ASM({
-            tx = "";
-        });
-#else
-        read(0, line, 256);
-#endif
-        if (strlen(line)) filesystem_process_command(line);
+        shell_task();
     }
 
     event.subsecond = 0;
@@ -633,13 +613,13 @@ void cb_fast_tick(void) {
     // Notice: is it possible that two or more buttons have an identical timestamp? In this case
     // only one of these buttons would receive the long press event. Don't bother for now...
     if (movement_state.light_down_timestamp > 0)
-        if (movement_state.fast_ticks - movement_state.light_down_timestamp == MOVEMENT_LONG_PRESS_TICKS + 1) 
+        if (movement_state.fast_ticks - movement_state.light_down_timestamp == MOVEMENT_LONG_PRESS_TICKS + 1)
             event.event_type = EVENT_LIGHT_LONG_PRESS;
     if (movement_state.mode_down_timestamp > 0)
-        if (movement_state.fast_ticks - movement_state.mode_down_timestamp == MOVEMENT_LONG_PRESS_TICKS + 1) 
+        if (movement_state.fast_ticks - movement_state.mode_down_timestamp == MOVEMENT_LONG_PRESS_TICKS + 1)
             event.event_type = EVENT_MODE_LONG_PRESS;
     if (movement_state.alarm_down_timestamp > 0)
-        if (movement_state.fast_ticks - movement_state.alarm_down_timestamp == MOVEMENT_LONG_PRESS_TICKS + 1) 
+        if (movement_state.fast_ticks - movement_state.alarm_down_timestamp == MOVEMENT_LONG_PRESS_TICKS + 1)
             event.event_type = EVENT_ALARM_LONG_PRESS;
     // this is just a fail-safe; fast tick should be disabled as soon as the button is up, the LED times out, and/or the alarm finishes.
     // but if for whatever reason it isn't, this forces the fast tick off after 20 seconds.
diff --git a/movement/shell.c b/movement/shell.c
new file mode 100644 (file)
index 0000000..8f146e5
--- /dev/null
@@ -0,0 +1,220 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2023 Edward Shin
+ *
+ * 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.
+ */
+
+#include "shell.h"
+
+#include <ctype.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+#if __EMSCRIPTEN__
+#include <emscripten.h>
+#endif
+
+#include "watch.h"
+#include "shell_cmd_list.h"
+
+extern shell_command_t g_shell_commands[];
+extern const size_t g_num_shell_commands;
+
+#define NEWLINE  "\r\n"
+
+#define SHELL_BUF_SZ  (256)
+#define SHELL_MAX_ARGS  (16)
+#define SHELL_PROMPT  "swsh> "
+
+static char s_buf[SHELL_BUF_SZ] = {0};
+static size_t s_buf_len = 0;
+// Pointer to the first invalid byte after the end of input.
+static char *const s_buf_end = s_buf + SHELL_BUF_SZ;
+
+static char *prv_skip_whitespace(char *c) {
+    while (c >= s_buf && c < s_buf_end) {
+        if (*c == 0) {
+            return NULL;
+        }
+        if (!isspace((int) *c) != 0) {
+            return c;
+        }
+        c++;
+    }
+    return NULL;
+}
+
+static char *prv_skip_non_whitespace(char *c) {
+    bool in_quote = false;
+    char quote_char;
+    while (c >= s_buf && c < s_buf_end) {
+        if (*c == 0) {
+            return NULL;
+        }
+        // Basic handling of quoted arguments.
+        // Can't handle recursive quotes. :(
+        if (in_quote || *c == '"' || *c == '\'') {
+            if (!in_quote) {
+                quote_char = *c;
+                in_quote = true;
+            } else if (*c == quote_char) {
+                in_quote = false;
+            }
+        } else {
+            if (isspace((int) *c) != 0) {
+                return c;
+            }
+        }
+        c++;
+    }
+    return NULL;
+}
+
+static int prv_handle_command() {
+    char *argv[SHELL_MAX_ARGS] = {0};
+    int argc = 0;
+
+    char *c = &s_buf[0];
+    s_buf[SHELL_BUF_SZ - 1] = '\0';
+
+    while (argc < SHELL_MAX_ARGS) {
+        // Skip contiguous whitespace
+        c = prv_skip_whitespace(c);
+        if (c == NULL) {
+            // Reached end of buffer
+            break;
+        }
+
+        // We hit non-whitespace, set argv and argc for this upcoming argument
+        argv[argc++] = c;
+
+        // Skip contiguous non-whitespace
+        c = prv_skip_non_whitespace(c);
+        if (c == NULL) {
+            // Reached end of buffer
+            break;
+        }
+
+        // NULL-terminate this arg string and then increment.
+        *(c++) = '\0';
+    }
+
+    if (argc == 0) {
+        return -1;
+    }
+
+    // Match against the command list
+    for (size_t i = 0; i < g_num_shell_commands; i++) {
+        if (!strcasecmp(g_shell_commands[i].name, argv[0])) {
+            // If argc isn't valid for this command, display its help instead.
+            if (((argc - 1) < g_shell_commands[i].min_args) ||
+                ((argc - 1) > g_shell_commands[i].max_args)) {
+                if (g_shell_commands[i].help != NULL) {
+                    printf(NEWLINE "%s" NEWLINE, g_shell_commands[i].help);
+                }
+                return -2;
+            }
+            // Call the command's callback
+            if (g_shell_commands[i].cb != NULL) {
+                printf(NEWLINE);
+                int ret = g_shell_commands[i].cb(argc, argv);
+                if (ret == -2) {
+                    printf(NEWLINE "%s" NEWLINE, g_shell_commands[i].help);
+                }
+                return ret;
+            }
+        }
+    }
+
+    return -1;
+}
+
+void shell_task(void) {
+#if __EMSCRIPTEN__
+    // This is a terrible hack; ideally this should be handled deeper in the watch library.
+    // Alas, emscripten treats read() as something that should pop up an input box, so I
+    // wasn't able to implement this over there. I sense that this relates to read() being
+    // the wrong way to read data from USB (like we should be using fgets or something), but
+    // until I untangle that, this will have to do.
+    char *received_data = (char*)EM_ASM_INT({
+        var len = lengthBytesUTF8(tx) + 1;
+        var s = _malloc(len);
+        stringToUTF8(tx, s, len);
+        return s;
+    });
+    s_buf_len = min((SHELL_BUF_SZ - 2), strlen(received_data));
+    memcpy(s_buf, received_data, s_buf_len);
+    free(received_data);
+    s_buf[s_buf_len++] = '\n';
+    s_buf[s_buf_len++] = '\0';
+    prv_handle_command();
+    EM_ASM({
+        tx = "";
+    });
+#else
+    // Read one character at a time until we run out.
+    while (true) {
+        if (s_buf_len >= (SHELL_BUF_SZ - 1)) {
+            printf(NEWLINE "Command too long, clearing.");
+            printf(NEWLINE SHELL_PROMPT);
+            s_buf_len = 0;
+            break;
+        }
+
+        int c = getchar();
+
+        if (c < 0) {
+            // Nothing left to read, we're done.
+            break;
+        }
+
+        if (c == '\b') {
+            // Handle backspace character.
+            // We need to emit a backspace, overwrite the character on the
+            // screen with a space, and then backspace again to move the cursor.
+            if (s_buf_len > 0) {
+                printf("\b \b");
+                s_buf_len--;
+            }
+            continue;
+        } else if (c != '\n' && c != '\r') {
+            // Print regular characters to the screen.
+            putchar(c);
+        }
+
+        s_buf[s_buf_len] = c;
+
+        if (c == '\n' || c == '\r') {
+            // Newline! Handle the command.
+            s_buf[s_buf_len+1] = '\0';
+            (void) prv_handle_command();
+            s_buf_len = 0;
+            printf(NEWLINE SHELL_PROMPT);
+            break;
+        } else {
+            s_buf_len++;
+        }
+    }
+#endif
+}
diff --git a/movement/shell.h b/movement/shell.h
new file mode 100644 (file)
index 0000000..27dbf67
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2023 Edward Shin
+ *
+ * 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.
+ */
+
+#ifndef SHELL_H_
+#define SHELL_H_
+
+/** @brief Called periodically from the app loop to handle shell commands.
+ *         When a full command is complete, parses and executes its matching
+ *         callback.
+ */
+void shell_task(void);
+
+#endif
diff --git a/movement/shell_cmd_list.c b/movement/shell_cmd_list.c
new file mode 100644 (file)
index 0000000..0ea08a5
--- /dev/null
@@ -0,0 +1,159 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2023 Edward Shin
+ *
+ * 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.
+ */
+
+#include "shell_cmd_list.h"
+
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "filesystem.h"
+#include "watch.h"
+
+static int help_cmd(int argc, char *argv[]);
+static int flash_cmd(int argc, char *argv[]);
+static int stress_cmd(int argc, char *argv[]);
+
+shell_command_t g_shell_commands[] = {
+    {
+        .name = "?",
+        .help = "print command list",
+        .min_args = 0,
+        .max_args = 0,
+        .cb = help_cmd,
+    },
+    {
+        .name = "help",
+        .help = "print command list",
+        .min_args = 0,
+        .max_args = 0,
+        .cb = help_cmd,
+    },
+    {
+        .name = "flash",
+        .help = "reboot to UF2 bootloader",
+        .min_args = 0,
+        .max_args = 0,
+        .cb = flash_cmd,
+    },
+    {
+        .name = "ls",
+        .help = "usage: ls [PATH]",
+        .min_args = 0,
+        .max_args = 1,
+        .cb = filesystem_cmd_ls,
+    },
+    {
+        .name = "cat",
+        .help = "usage: cat <PATH>",
+        .min_args = 1,
+        .max_args = 1,
+        .cb = filesystem_cmd_cat,
+    },
+    {
+        .name = "df",
+        .help = "print filesystem free space",
+        .min_args = 0,
+        .max_args = 0,
+        .cb = filesystem_cmd_df,
+    },
+    {
+        .name = "rm",
+        .help = "usage: rm [PATH]",
+        .min_args = 1,
+        .max_args = 1,
+        .cb = filesystem_cmd_rm,
+    },
+    {
+        .name = "echo",
+        .help = "usage: echo TEXT {>,>>} FILE",
+        .min_args = 3,
+        .max_args = 3,
+        .cb = filesystem_cmd_echo,
+    },
+    {
+        .name = "stress",
+        .help = "test CDC write; usage: stress [LEN] [DELAY_MS]",
+        .min_args = 0,
+        .max_args = 2,
+        .cb = stress_cmd,
+    },
+};
+
+const size_t g_num_shell_commands = sizeof(g_shell_commands) / sizeof(shell_command_t);
+
+static int help_cmd(int argc, char *argv[]) {
+    (void) argc;
+    (void) argv;
+
+    printf("Command List:\r\n");
+    for (size_t i = 0; i < g_num_shell_commands; i++) {
+        printf(" %s\t%s\r\n", 
+                g_shell_commands[i].name,
+                (g_shell_commands[i].help) ? g_shell_commands[i].help : ""
+        );
+    }
+
+    return 0;
+}
+
+static int flash_cmd(int argc, char *argv[]) {
+    (void) argc;
+    (void) argv;
+
+    watch_reset_to_bootloader();
+    return 0;
+}
+
+#define STRESS_CMD_MAX_LEN  (512)
+static int stress_cmd(int argc, char *argv[]) {
+    char test_str[STRESS_CMD_MAX_LEN+1] = {0};
+
+    int max_len = 512;
+    int delay = 0;
+
+    if (argc >= 2) {
+        if ((max_len = atoi(argv[1])) == 0) {
+            return -1;
+        }
+        if (max_len > 512) {
+            return -1;
+        }
+    }
+
+    if (argc >= 3) {
+        delay = atoi(argv[2]);
+    }
+
+    for (int i = 0; i < max_len; i++) {
+        snprintf(&test_str[i], 2, "%u", (i+1)%10);
+        printf("%u:\t%s\r\n", (i+1), test_str);
+        if (delay > 0) {
+            delay_ms(delay);
+        }
+    }
+
+    return 0;
+}
+
diff --git a/movement/shell_cmd_list.h b/movement/shell_cmd_list.h
new file mode 100644 (file)
index 0000000..89031a5
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2023 Edward Shin
+ *
+ * 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.
+ */
+
+#ifndef SHELL_CMD_LIST_H_
+#define SHELL_CMD_LIST_H_
+
+#include <stdint.h>
+
+typedef struct {
+    const char *name; // Name used to invoke the command
+    const char *help; // Help string
+    int8_t min_args;  // Minimum number of arguments (_excluding_ the command name)
+    int8_t max_args;  // Maximum number of arguments (_excluding_ the command name)
+    int (*cb)(int argc, char *argv[]); // Callback for the command
+} shell_command_t;
+
+#endif
index 325610f6ead20bc919863429c448407de9ff8f4b..8ac4fca66c8c4d2dc85719854a62ec3c193db5ee 100755 (executable)
@@ -79,7 +79,6 @@ int main(void) {
     while (1) {
         bool usb_enabled = hri_usbdevice_get_CTRLA_ENABLE_bit(USB);
         bool can_sleep = app_loop();
-
         if (can_sleep && !usb_enabled) {
             app_prepare_for_standby();
             sleep(4);
index cd607b8e8529d82028471147d9ac8c4f7ab38a5b..e6db33e7e79996b62a44f3f3912dd6e1fc2e3e45 100644 (file)
@@ -23,6 +23,7 @@
  */
 
 #include "watch_private.h"
+#include "watch_private_cdc.h"
 #include "watch_utility.h"
 #include "tusb.h"
 
@@ -170,6 +171,87 @@ void _watch_disable_tcc(void) {
     // disable the TCC
     hri_tcc_clear_CTRLA_ENABLE_bit(TCC0);
     hri_mclk_clear_APBCMASK_TCC0_bit(MCLK);
+}    
+
+void _watch_enable_tc0(void) {
+    // before we init TinyUSB, we are going to need a periodic callback to handle TinyUSB tasks.
+    // TC2 and TC3 are reserved for devices on the 9-pin connector, so let's use TC0.
+    // clock TC0 with the 8 MHz clock on GCLK0.
+    hri_gclk_write_PCHCTRL_reg(GCLK, TC0_GCLK_ID, GCLK_PCHCTRL_GEN_GCLK0_Val | GCLK_PCHCTRL_CHEN);
+    // and enable the peripheral clock.
+    hri_mclk_set_APBCMASK_TC0_bit(MCLK);
+    // disable and reset TC0.
+    hri_tc_clear_CTRLA_ENABLE_bit(TC0);
+    hri_tc_wait_for_sync(TC0, TC_SYNCBUSY_ENABLE);
+    hri_tc_write_CTRLA_reg(TC0, TC_CTRLA_SWRST);
+    hri_tc_wait_for_sync(TC0, TC_SYNCBUSY_SWRST);
+    hri_tc_write_CTRLA_reg(TC0, TC_CTRLA_PRESCALER_DIV1024 | // divide the 8 MHz clock by 1024 to count at 7812.5 Hz
+                                TC_CTRLA_MODE_COUNT8 |       // count in 8-bit mode
+                                TC_CTRLA_RUNSTDBY);          // run in standby, just in case we figure that out
+    hri_tccount8_write_PER_reg(TC0, 10);                     // 7812.5 Hz / 10 = 781.125 Hz
+    // set an interrupt on overflow; this will call TC0_Handler below.
+    hri_tc_set_INTEN_OVF_bit(TC0);
+
+    // set priority higher than TC1
+    NVIC_SetPriority(TC0_IRQn, 5);
+    NVIC_ClearPendingIRQ(TC0_IRQn);
+    NVIC_EnableIRQ(TC0_IRQn);
+
+    // Start the timer
+    hri_tc_set_CTRLA_ENABLE_bit(TC0);
+}
+
+void _watch_disable_tc0(void) {
+    NVIC_DisableIRQ(TC0_IRQn);
+    NVIC_ClearPendingIRQ(TC0_IRQn);
+    hri_tc_clear_CTRLA_ENABLE_bit(TC0);
+    hri_tc_wait_for_sync(TC0, TC_SYNCBUSY_ENABLE);
+    hri_tc_write_CTRLA_reg(TC0, TC_CTRLA_SWRST);
+    hri_tc_wait_for_sync(TC0, TC_SYNCBUSY_SWRST);
+}
+
+void _watch_enable_tc1(void) {
+    hri_gclk_write_PCHCTRL_reg(GCLK, TC1_GCLK_ID, GCLK_PCHCTRL_GEN_GCLK0_Val | GCLK_PCHCTRL_CHEN);
+    // and enable the peripheral clock.
+    hri_mclk_set_APBCMASK_TC1_bit(MCLK);
+    // disable and reset TC1.
+    hri_tc_clear_CTRLA_ENABLE_bit(TC1);
+    hri_tc_wait_for_sync(TC1, TC_SYNCBUSY_ENABLE);
+    hri_tc_write_CTRLA_reg(TC1, TC_CTRLA_SWRST);
+    hri_tc_wait_for_sync(TC1, TC_SYNCBUSY_SWRST);
+    hri_tc_write_CTRLA_reg(TC1, TC_CTRLA_PRESCALER_DIV1024 | // divide the 8 MHz clock by 1024 to count at 7812.5 Hz
+                                TC_CTRLA_MODE_COUNT8 |       // count in 8-bit mode
+                                TC_CTRLA_RUNSTDBY);          // run in standby, just in case we figure that out
+    hri_tccount8_write_PER_reg(TC1, 20);                     // 7812.5 Hz / 50 = 156.25 Hz
+    // set an interrupt on overflow; this will call TC1_Handler below.
+    hri_tc_set_INTEN_OVF_bit(TC1);
+
+    // set priority lower than TC0
+    NVIC_SetPriority(TC1_IRQn, 6);
+    NVIC_ClearPendingIRQ(TC1_IRQn);
+    NVIC_EnableIRQ(TC1_IRQn);
+
+    // Start the timer
+    hri_tc_set_CTRLA_ENABLE_bit(TC1);
+}
+
+void _watch_disable_tc1(void) {
+    NVIC_DisableIRQ(TC1_IRQn);
+    NVIC_ClearPendingIRQ(TC1_IRQn);
+    hri_tc_clear_CTRLA_ENABLE_bit(TC1);
+    hri_tc_wait_for_sync(TC1, TC_SYNCBUSY_ENABLE);
+    hri_tc_write_CTRLA_reg(TC1, TC_CTRLA_SWRST);
+    hri_tc_wait_for_sync(TC1, TC_SYNCBUSY_SWRST);
+}
+
+void TC0_Handler(void) {
+    tud_task();
+    TC0->COUNT8.INTFLAG.reg |= TC_INTFLAG_OVF;
+}
+
+void TC1_Handler(void) {
+    cdc_task();
+    TC1->COUNT8.INTFLAG.reg |= TC_INTFLAG_OVF;
 }
 
 void _watch_enable_usb(void) {
@@ -216,76 +298,17 @@ void _watch_enable_usb(void) {
     gpio_set_pin_function(PIN_PA24, PINMUX_PA24G_USB_DM);
     gpio_set_pin_function(PIN_PA25, PINMUX_PA25G_USB_DP);
 
-    // before we init TinyUSB, we are going to need a periodic callback to handle TinyUSB tasks.
-    // TC2 and TC3 are reserved for devices on the 9-pin connector, so let's use TC0.
-    // clock TC0 with the 8 MHz clock on GCLK0.
-    hri_gclk_write_PCHCTRL_reg(GCLK, TC0_GCLK_ID, GCLK_PCHCTRL_GEN_GCLK0_Val | GCLK_PCHCTRL_CHEN);
-    // and enable the peripheral clock.
-    hri_mclk_set_APBCMASK_TC0_bit(MCLK);
-    // disable and reset TC0.
-    hri_tc_clear_CTRLA_ENABLE_bit(TC0);
-    hri_tc_wait_for_sync(TC0, TC_SYNCBUSY_ENABLE);
-    hri_tc_write_CTRLA_reg(TC0, TC_CTRLA_SWRST);
-    hri_tc_wait_for_sync(TC0, TC_SYNCBUSY_SWRST);
-    // configure the TC to overflow 1,000 times per second
-    hri_tc_write_CTRLA_reg(TC0, TC_CTRLA_PRESCALER_DIV64 |  // divide the 8 MHz clock by 64 to count at 125 KHz
-                                TC_CTRLA_MODE_COUNT8 |      // count in 8-bit mode
-                                TC_CTRLA_RUNSTDBY);         // run in standby, just in case we figure that out
-    hri_tccount8_write_PER_reg(TC0, 125);                   // 125000 Hz / 125 = 1,000 Hz
-    // set an interrupt on overflow; this will call TC0_Handler below.
-    hri_tc_set_INTEN_OVF_bit(TC0);
-    NVIC_ClearPendingIRQ(TC0_IRQn);
-    NVIC_EnableIRQ (TC0_IRQn);
+    _watch_enable_tc0();
 
-    // now we can init TinyUSB
     tusb_init();
-    // and start the timer that handles USB device tasks.
-    hri_tc_set_CTRLA_ENABLE_bit(TC0);
-}
 
-// this function ends up getting called by printf to log stuff to the USB console.
-int _write(int file, char *ptr, int len) {
-    (void)file;
-    if (hri_usbdevice_get_CTRLA_ENABLE_bit(USB)) {
-        tud_cdc_n_write(0, (void const*)ptr, len);
-        tud_cdc_n_write_flush(0);
-        return len;
-    }
-
-    return 0;
-}
-
-static char buf[256] = {0};
-
-int _read(int file, char *ptr, int len) {
-    (void)file;
-    int actual_length = strlen(buf);
-    if (actual_length) {
-        memcpy(ptr, buf, min(len, actual_length));
-        return actual_length;
-    }
-    return 0;
+    _watch_enable_tc1();
 }
 
 void USB_Handler(void) {
     tud_int_handler(0);
 }
 
-static void cdc_task(void) {
-    if (tud_cdc_n_available(0)) {
-        tud_cdc_n_read(0, buf, sizeof(buf));
-    } else {
-        memset(buf, 0, 256);
-    }
-}
-
-void TC0_Handler(void) {
-    tud_task();
-    cdc_task();
-    TC0->COUNT8.INTFLAG.reg |= TC_INTFLAG_OVF;
-}
-
-
 // USB Descriptors and tinyUSB callbacks follow.
 
 /*
diff --git a/watch-library/hardware/watch/watch_private_cdc.c b/watch-library/hardware/watch/watch_private_cdc.c
new file mode 100644 (file)
index 0000000..a961b5e
--- /dev/null
@@ -0,0 +1,160 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 Joey Castillo
+ * Copyright (c) 2023 Edward Shin
+ *
+ * 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.
+ */
+
+#include "watch_private_cdc.h"
+
+#include <stddef.h>
+
+#include "watch_utility.h"
+#include "tusb.h"
+
+/*
+ * Implement a circular buffer for the USB CDC Serial read buffer.
+ * The size of the buffer must be a power of two for this circular buffer
+ * implementation to work.
+ */
+
+// Size of the circular buffer. Must be a power of two.
+#define CDC_WRITE_BUF_SZ  (1024)
+// Macro function to perform modular arithmetic on an index.
+// eg. (63 + 2) & (64 - 1) -> 1
+#define CDC_WRITE_BUF_IDX(x)  ((x) & (CDC_WRITE_BUF_SZ - 1))
+static char s_write_buf[CDC_WRITE_BUF_SZ] = {0};
+static size_t s_write_buf_pos = 0;
+static size_t s_write_buf_len = 0;
+
+#define CDC_READ_BUF_SZ  (256)
+#define CDC_READ_BUF_IDX(x)  ((x) & (CDC_READ_BUF_SZ - 1))
+static char s_read_buf[CDC_READ_BUF_SZ] = {0};
+static size_t s_read_buf_pos = 0;
+static size_t s_read_buf_len = 0;
+
+// Mask TC1 interrupts, preventing calls to cdc_task()
+static inline void prv_critical_section_enter(void) {
+    NVIC_DisableIRQ(TC1_IRQn);
+}
+
+// Unmask TC1 interrupts, allowing calls to cdc_task()
+static inline void prv_critical_section_exit(void) {
+    NVIC_EnableIRQ(TC1_IRQn);
+}
+
+int _write(int file, char *ptr, int len) {
+    (void) file;
+
+    if (ptr == NULL || len <= 0) {
+        return -1;
+    }
+
+    int bytes_written = 0;
+
+    prv_critical_section_enter();
+
+    for (int i = 0; i < len; i++) {
+        s_write_buf[s_write_buf_pos] = ptr[i];
+        s_write_buf_pos = CDC_WRITE_BUF_IDX(s_write_buf_pos + 1);
+        if (s_write_buf_len < CDC_WRITE_BUF_SZ) {
+            s_write_buf_len++;
+        }
+        bytes_written++;
+    }
+
+    prv_critical_section_exit();
+
+    return bytes_written;
+}
+
+int _read(int file, char *ptr, int len) {
+    (void) file;
+
+    prv_critical_section_enter();
+
+    if (ptr == NULL || len <= 0 || s_read_buf_len == 0) {
+        prv_critical_section_exit();
+        return -1;
+    }
+
+    // Clamp to the length of the read buffer
+    if ((size_t) len > s_read_buf_len) {
+        len = s_read_buf_len;
+    }
+
+    // Calculate the start of the circular buffer, and iterate from there
+    const size_t start_pos = CDC_READ_BUF_IDX(s_read_buf_pos - len);
+    for (size_t i = 0; i < (size_t) len; i++) {
+        const size_t idx = CDC_READ_BUF_IDX(start_pos + i);
+        ptr[i] = s_read_buf[idx];
+        s_read_buf[idx] = 0;
+    }
+
+    // Update circular buffer position and length
+    s_read_buf_len -= len;
+    s_read_buf_pos = CDC_READ_BUF_IDX(s_read_buf_pos - len);
+
+    prv_critical_section_exit();
+
+    return len;
+}
+
+static void prv_handle_reads(void) {
+    while (tud_cdc_available()) {
+        int c = tud_cdc_read_char();
+        if (c < 0) {
+            continue;
+        }
+        s_read_buf[s_read_buf_pos] = c;
+        s_read_buf_pos = CDC_READ_BUF_IDX(s_read_buf_pos + 1);
+        if (s_read_buf_len < CDC_READ_BUF_SZ) {
+            s_read_buf_len++;
+        }
+    }
+}
+
+static void prv_handle_writes(void) {
+    if (s_write_buf_len > 0) {
+        const size_t start_pos =
+            CDC_WRITE_BUF_IDX(s_write_buf_pos - s_write_buf_len);
+        for (size_t i = 0; i < (size_t) s_write_buf_len; i++) {
+            const size_t idx = CDC_WRITE_BUF_IDX(start_pos + i);
+            if (tud_cdc_available() > 0) {
+                // If we receive data while doing a large write, we need to
+                // fully service it before continuing to write, or the
+                // stack will crash.
+                prv_handle_reads();
+            }
+            if (tud_cdc_write_available()) {
+                tud_cdc_write(&s_write_buf[idx], 1);
+            }
+            s_write_buf[idx] = 0;
+            s_write_buf_len--;
+        }
+        tud_cdc_write_flush();
+    }
+}
+
+void cdc_task(void) {
+    prv_handle_reads();
+    prv_handle_writes();
+}
diff --git a/watch-library/hardware/watch/watch_private_cdc.h b/watch-library/hardware/watch/watch_private_cdc.h
new file mode 100644 (file)
index 0000000..b7fa958
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 Joey Castillo
+ * Copyright (c) 2023 Edward Shin
+ *
+ * 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.
+ */
+
+#ifndef _WATCH_PRIVATE_CDC_H_INCLUDED
+#define _WATCH_PRIVATE_CDC_H_INCLUDED
+
+int _write(int file, char *ptr, int len);
+int _read(int file, char *ptr, int len);
+void cdc_task(void);
+
+#endif
index 790f9a163d1c70fc0f1df3bec6076df335bd3c00..2f697f0aa76b0c615238cd4ec37bc370c3c6fdcd 100644 (file)
@@ -88,6 +88,10 @@ bool watch_is_usb_enabled(void);
   */
 void watch_reset_to_bootloader(void);
 
+/** @brief Call periodically from app main loop to service CDC RX/TX.
+  */
+void cdc_task(void);
+
 /** @brief Reads up to len bytes from the USB serial.
   * @param file ignored, you can pass in 0
   * @param ptr pointer to a buffer of at least len bytes
@@ -96,4 +100,4 @@ void watch_reset_to_bootloader(void);
   */
 int read(int file, char *ptr, int len);
 
-#endif /* WATCH_H_ */
\ No newline at end of file
+#endif /* WATCH_H_ */
index 9d55bc212372f706da5866c4592616b155a46189..8fcc575544f99cdc88195d1649aafef61b0609cb 100644 (file)
@@ -38,14 +38,19 @@ void _watch_enable_tcc(void);
 /// Called by buzzer and LED teardown functions. You should not call this from your app.
 void _watch_disable_tcc(void);
 
-/// Called by main.c if plugged in to USB. You should not call this from your app.
-void _watch_enable_usb(void);
+/// Enable USB task timer. Called by USB enable routine in main(). You should not call this from your app.
+void _watch_enable_tc0(void);
+
+/// Disable USB task timer. You should not call this from your app.
+void _watch_disable_tc0(void);
 
-// this function ends up getting called by printf to log stuff to the USB console.
-int _write(int file, char *ptr, int len);
+/// Enable CDC task timer. Called by USB enable routine in main(). You should not call this from your app.
+void _watch_enable_tc1(void);
 
-// i thought this would be called by gets but it doesn't? anyway it does get called by read()
-// so that's our mechanism for reading data from the USB serial console.
-int _read(int file, char *ptr, int len);
+/// Disable CDC task timer. You should not call this from your app.
+void _watch_disable_tc1(void);
+
+/// Called by main.c if plugged in to USB. You should not call this from your app.
+void _watch_enable_usb(void);
 
 #endif