diff options
32 files changed, 1460 insertions, 430 deletions
@@ -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 \ @@ -229,3 +230,9 @@ endif ifeq ($(BOARD), OSO-FEAL-A1-00) CFLAGS += -DCRYSTALLESS endif + +# Build options to customize movement and faces + +ifdef CLOCK_FACE_24H_ONLY +CFLAGS += -DCLOCK_FACE_24H_ONLY +endif diff --git a/movement/filesystem.c b/movement/filesystem.c index 97e35455..9df0a8d2 100644 --- a/movement/filesystem.c +++ b/movement/filesystem.c @@ -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; +} + diff --git a/movement/filesystem.h b/movement/filesystem.h index 3cd3d092..fa3d9d1a 100644 --- a/movement/filesystem.h +++ b/movement/filesystem.h @@ -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_ diff --git a/movement/make/Makefile b/movement/make/Makefile index 42dfc644..5d53ff7b 100644 --- a/movement/make/Makefile +++ b/movement/make/Makefile @@ -49,7 +49,9 @@ SRCS += \ ../../littlefs/lfs_util.c \ ../movement.c \ ../filesystem.c \ - ../watch_faces/clock/simple_clock_face.c \ + ../shell.c \ + ../shell_cmd_list.c \ + ../watch_faces/clock/clock_face.c \ ../watch_faces/clock/world_clock_face.c \ ../watch_faces/clock/beats_face.c \ ../watch_faces/clock/weeknumber_clock_face.c \ diff --git a/movement/movement.c b/movement/movement.c index d780a2f3..816624d4 100644 --- a/movement/movement.c +++ b/movement/movement.c @@ -33,6 +33,7 @@ #include "watch.h" #include "filesystem.h" #include "movement.h" +#include "shell.h" #ifndef MOVEMENT_FIRMWARE #include "movement_config.h" @@ -69,6 +70,31 @@ #define MOVEMENT_DEFAULT_GREEN_COLOR 0xF #endif +// Default to 12h mode +#ifndef MOVEMENT_DEFAULT_24H_MODE +#define MOVEMENT_DEFAULT_24H_MODE false +#endif + +// Default to mode button sounding on press +#ifndef MOVEMENT_DEFAULT_BUTTON_SOUND +#define MOVEMENT_DEFAULT_BUTTON_SOUND true +#endif + +// Default to switch back to main watch face after 60 seconds +#ifndef MOVEMENT_DEFAULT_TIMEOUT_INTERVAL +#define MOVEMENT_DEFAULT_TIMEOUT_INTERVAL 0 +#endif + +// Default to switch to low energy mode after 2 hours +#ifndef MOVEMENT_DEFAULT_LOW_ENERGY_INTERVAL +#define MOVEMENT_DEFAULT_LOW_ENERGY_INTERVAL 2 +#endif + +// Default to 1 second led duration +#ifndef MOVEMENT_DEFAULT_LED_DURATION +#define MOVEMENT_DEFAULT_LED_DURATION 1 +#endif + #if __EMSCRIPTEN__ #include <emscripten.h> #endif @@ -351,11 +377,13 @@ void app_init(void) { memset(&movement_state, 0, sizeof(movement_state)); + movement_state.settings.bit.clock_mode_24h = MOVEMENT_DEFAULT_24H_MODE; movement_state.settings.bit.led_red_color = MOVEMENT_DEFAULT_RED_COLOR; movement_state.settings.bit.led_green_color = MOVEMENT_DEFAULT_GREEN_COLOR; - movement_state.settings.bit.button_should_sound = true; - movement_state.settings.bit.le_interval = 2; - movement_state.settings.bit.led_duration = 1; + movement_state.settings.bit.button_should_sound = MOVEMENT_DEFAULT_BUTTON_SOUND; + movement_state.settings.bit.to_interval = MOVEMENT_DEFAULT_TIMEOUT_INTERVAL; + movement_state.settings.bit.le_interval = MOVEMENT_DEFAULT_LOW_ENERGY_INTERVAL; + movement_state.settings.bit.led_duration = MOVEMENT_DEFAULT_LED_DURATION; movement_state.light_ticks = -1; movement_state.alarm_ticks = -1; movement_state.next_available_backup_register = 4; @@ -561,30 +589,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; diff --git a/movement/movement_config.h b/movement/movement_config.h index 067ca44b..72203ebd 100644 --- a/movement/movement_config.h +++ b/movement/movement_config.h @@ -28,7 +28,7 @@ #include "movement_faces.h" const watch_face_t watch_faces[] = { - simple_clock_face, + clock_face, world_clock_face, sunrise_sunset_face, moon_phase_face, @@ -52,4 +52,47 @@ const watch_face_t watch_faces[] = { /* Custom hourly chime tune. Check movement_custom_signal_tunes.h for options. */ #define SIGNAL_TUNE_DEFAULT +/* Determines the intensity of the led colors + * Set a hex value 0-15 with 0x0 being off and 0xF being max intensity + */ +#define MOVEMENT_DEFAULT_GREEN_COLOR 0xF +#define MOVEMENT_DEFAULT_RED_COLOR 0x0 + +/* Set to true for 24h mode or false for 12h mode */ +#define MOVEMENT_DEFAULT_24H_MODE false + +/* Enable or disable the sound on mode button press */ +#define MOVEMENT_DEFAULT_BUTTON_SOUND true + +/* Set the timeout before switching back to the main watch face + * Valid values are: + * 0: 60 seconds + * 1: 2 minutes + * 2: 5 minutes + * 3: 30 minutes + */ +#define MOVEMENT_DEFAULT_TIMEOUT_INTERVAL 0 + +/* Set the timeout before switching to low energy mode + * Valid values are: + * 0: Never + * 1: 1 hour + * 2: 2 hours + * 3: 6 hours + * 4: 12 hours + * 5: 1 day + * 6: 2 days + * 7: 7 days + */ +#define MOVEMENT_DEFAULT_LOW_ENERGY_INTERVAL 1 + +/* Set the led duration + * Valid values are: + * 0: No LED + * 1: 1 second + * 2: 3 seconds + * 3: 5 seconds + */ +#define MOVEMENT_DEFAULT_LED_DURATION 1 + #endif // MOVEMENT_CONFIG_H_ diff --git a/movement/movement_faces.h b/movement/movement_faces.h index 7feb0f40..949a071a 100644 --- a/movement/movement_faces.h +++ b/movement/movement_faces.h @@ -25,7 +25,7 @@ #ifndef MOVEMENT_FACES_H_ #define MOVEMENT_FACES_H_ -#include "simple_clock_face.h" +#include "clock_face.h" #include "world_clock_face.h" #include "preferences_face.h" #include "set_time_face.h" diff --git a/movement/shell.c b/movement/shell.c new file mode 100644 index 00000000..8f146e59 --- /dev/null +++ b/movement/shell.c @@ -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 index 00000000..27dbf672 --- /dev/null +++ b/movement/shell.h @@ -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 index 00000000..0ea08a56 --- /dev/null +++ b/movement/shell_cmd_list.c @@ -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 index 00000000..89031a54 --- /dev/null +++ b/movement/shell_cmd_list.h @@ -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 diff --git a/movement/watch_faces/clock/clock_face.c b/movement/watch_faces/clock/clock_face.c new file mode 100644 index 00000000..eab5cd8d --- /dev/null +++ b/movement/watch_faces/clock/clock_face.c @@ -0,0 +1,291 @@ +/* SPDX-License-Identifier: MIT */ + +/* + * MIT License + * + * Copyright © 2021-2023 Joey Castillo <joeycastillo@utexas.edu> <jose.castillo@gmail.com> + * Copyright © 2022 David Keck <davidskeck@users.noreply.github.com> + * Copyright © 2022 TheOnePerson <a.nebinger@web.de> + * Copyright © 2023 Jeremy O'Brien <neutral@fastmail.com> + * Copyright © 2023 Mikhail Svarichevsky <3@14.by> + * Copyright © 2023 Wesley Aptekar-Cassels <me@wesleyac.com> + * Copyright © 2024 Matheus Afonso Martins Moreira <matheus.a.m.moreira@gmail.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. + */ + +#include <stdlib.h> +#include "clock_face.h" +#include "watch.h" +#include "watch_utility.h" +#include "watch_private_display.h" + +// 2.2 volts will happen when the battery has maybe 5-10% remaining? +// we can refine this later. +#ifndef CLOCK_FACE_LOW_BATTERY_VOLTAGE_THRESHOLD +#define CLOCK_FACE_LOW_BATTERY_VOLTAGE_THRESHOLD 2200 +#endif + +#ifndef CLOCK_FACE_24H_ONLY +#define CLOCK_FACE_24H_ONLY 0 +#endif + +typedef struct { + struct { + watch_date_time previous; + } date_time; + uint8_t last_battery_check; + uint8_t watch_face_index; + bool time_signal_enabled; + bool battery_low; +} clock_state_t; + +static bool clock_is_in_24h_mode(movement_settings_t *settings) { + if (CLOCK_FACE_24H_ONLY) { return true; } + return settings->bit.clock_mode_24h; +} + +static void clock_indicate(WatchIndicatorSegment indicator, bool on) { + if (on) { + watch_set_indicator(indicator); + } else { + watch_clear_indicator(indicator); + } +} + +static void clock_indicate_alarm(movement_settings_t *settings) { + clock_indicate(WATCH_INDICATOR_BELL, settings->bit.alarm_enabled); +} + +static void clock_indicate_time_signal(clock_state_t *clock) { + clock_indicate(WATCH_INDICATOR_SIGNAL, clock->time_signal_enabled); +} + +static void clock_indicate_24h(movement_settings_t *settings) { + clock_indicate(WATCH_INDICATOR_24H, clock_is_in_24h_mode(settings)); +} + +static bool clock_is_pm(watch_date_time date_time) { + return date_time.unit.hour >= 12; +} + +static void clock_indicate_pm(movement_settings_t *settings, watch_date_time date_time) { + if (settings->bit.clock_mode_24h) { return; } + clock_indicate(WATCH_INDICATOR_PM, clock_is_pm(date_time)); +} + +static void clock_indicate_low_available_power(clock_state_t *clock) { + // Set the LAP indicator if battery power is low + clock_indicate(WATCH_INDICATOR_LAP, clock->battery_low); +} + +static watch_date_time clock_24h_to_12h(watch_date_time date_time) { + date_time.unit.hour %= 12; + + if (date_time.unit.hour == 0) { + date_time.unit.hour = 12; + } + + return date_time; +} + +static void clock_check_battery_periodically(clock_state_t *clock, watch_date_time date_time) { + // check the battery voltage once a day + if (date_time.unit.day == clock->last_battery_check) { return; } + + clock->last_battery_check = date_time.unit.day; + + watch_enable_adc(); + uint16_t voltage = watch_get_vcc_voltage(); + watch_disable_adc(); + + clock->battery_low = voltage < CLOCK_FACE_LOW_BATTERY_VOLTAGE_THRESHOLD; + + clock_indicate_low_available_power(clock); +} + +static void clock_toggle_time_signal(clock_state_t *clock) { + clock->time_signal_enabled = !clock->time_signal_enabled; + clock_indicate_time_signal(clock); +} + +static void clock_display_all(watch_date_time date_time) { + char buf[10 + 1]; + + snprintf( + buf, + sizeof(buf), + "%s%2d%2d%02d%02d", + watch_utility_get_weekday(date_time), + date_time.unit.day, + date_time.unit.hour, + date_time.unit.minute, + date_time.unit.second + ); + + watch_display_string(buf, 0); +} + +static bool clock_display_some(watch_date_time current, watch_date_time previous) { + if ((current.reg >> 6) == (previous.reg >> 6)) { + // everything before seconds is the same, don't waste cycles setting those segments. + + watch_display_character_lp_seconds('0' + current.unit.second / 10, 8); + watch_display_character_lp_seconds('0' + current.unit.second % 10, 9); + + return true; + + } else if ((current.reg >> 12) == (previous.reg >> 12)) { + // everything before minutes is the same. + + char buf[4 + 1]; + + snprintf( + buf, + sizeof(buf), + "%02d%02d", + current.unit.minute, + current.unit.second + ); + + watch_display_string(buf, 6); + + return true; + + } else { + // other stuff changed; let's do it all. + return false; + } +} + +static void clock_display_clock(movement_settings_t *settings, clock_state_t *clock, watch_date_time current) { + if (!clock_display_some(current, clock->date_time.previous)) { + if (!clock_is_in_24h_mode(settings)) { + // if we are in 12 hour mode, do some cleanup. + clock_indicate_pm(settings, current); + current = clock_24h_to_12h(current); + } + clock_display_all(current); + } +} + +static void clock_display_low_energy(watch_date_time date_time) { + char buf[10 + 1]; + + snprintf( + buf, + sizeof(buf), + "%s%2d%2d%02d ", + watch_utility_get_weekday(date_time), + date_time.unit.day, + date_time.unit.hour, + date_time.unit.minute + ); + + watch_display_string(buf, 0); +} + +static void clock_start_tick_tock_animation(void) { + if (!watch_tick_animation_is_running()) { + watch_start_tick_animation(500); + } +} + +static void clock_stop_tick_tock_animation(void) { + if (watch_tick_animation_is_running()) { + watch_stop_tick_animation(); + } +} + +void clock_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) { + (void) settings; + (void) watch_face_index; + + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(clock_state_t)); + clock_state_t *state = (clock_state_t *) *context_ptr; + state->time_signal_enabled = false; + state->watch_face_index = watch_face_index; + } +} + +void clock_face_activate(movement_settings_t *settings, void *context) { + clock_state_t *clock = (clock_state_t *) context; + + clock_stop_tick_tock_animation(); + + clock_indicate_time_signal(clock); + clock_indicate_alarm(settings); + clock_indicate_24h(settings); + + watch_set_colon(); + + // this ensures that none of the timestamp fields will match, so we can re-render them all. + clock->date_time.previous.reg = 0xFFFFFFFF; +} + +bool clock_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { + clock_state_t *state = (clock_state_t *) context; + watch_date_time current; + + switch (event.event_type) { + case EVENT_LOW_ENERGY_UPDATE: + clock_start_tick_tock_animation(); + clock_display_low_energy(watch_rtc_get_date_time()); + break; + case EVENT_TICK: + case EVENT_ACTIVATE: + current = watch_rtc_get_date_time(); + + clock_display_clock(settings, state, current); + + clock_check_battery_periodically(state, current); + + state->date_time.previous = current; + + break; + case EVENT_ALARM_LONG_PRESS: + clock_toggle_time_signal(state); + break; + case EVENT_BACKGROUND_TASK: + // uncomment this line to snap back to the clock face when the hour signal sounds: + // movement_move_to_face(state->watch_face_index); + movement_play_signal(); + break; + default: + return movement_default_loop_handler(event, settings); + } + + return true; +} + +void clock_face_resign(movement_settings_t *settings, void *context) { + (void) settings; + (void) context; +} + +bool clock_face_wants_background_task(movement_settings_t *settings, void *context) { + (void) settings; + clock_state_t *state = (clock_state_t *) context; + if (!state->time_signal_enabled) return false; + + watch_date_time date_time = watch_rtc_get_date_time(); + + return date_time.unit.minute == 0; +} diff --git a/movement/watch_faces/clock/simple_clock_face.h b/movement/watch_faces/clock/clock_face.h index e74a6e86..c4209e3b 100644 --- a/movement/watch_faces/clock/simple_clock_face.h +++ b/movement/watch_faces/clock/clock_face.h @@ -1,7 +1,13 @@ +/* SPDX-License-Identifier: MIT */ + /* * MIT License * - * Copyright (c) 2022 Joey Castillo + * Copyright © 2021-2022 Joey Castillo <joeycastillo@utexas.edu> <jose.castillo@gmail.com> + * Copyright © 2022 Alexsander Akers <me@a2.io> + * Copyright © 2022 TheOnePerson <a.nebinger@web.de> + * Copyright © 2023 Alex Utter <ooterness@gmail.com> + * Copyright © 2024 Matheus Afonso Martins Moreira <matheus.a.m.moreira@gmail.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 @@ -22,41 +28,33 @@ * SOFTWARE. */ -#ifndef SIMPLE_CLOCK_FACE_H_ -#define SIMPLE_CLOCK_FACE_H_ +#ifndef CLOCK_FACE_H_ +#define CLOCK_FACE_H_ /* - * SIMPLE CLOCK FACE + * CLOCK FACE * - * Displays the current time, matching the original operation of the watch. + * Displays the current local time, just like the original watch. * This is the default display mode in most watch configurations. * * Long-press ALARM to toggle the hourly chime. + * */ #include "movement.h" -typedef struct { - uint32_t previous_date_time; - uint8_t last_battery_check; - uint8_t watch_face_index; - bool signal_enabled; - bool battery_low; - bool alarm_enabled; -} simple_clock_state_t; - -void simple_clock_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); -void simple_clock_face_activate(movement_settings_t *settings, void *context); -bool simple_clock_face_loop(movement_event_t event, movement_settings_t *settings, void *context); -void simple_clock_face_resign(movement_settings_t *settings, void *context); -bool simple_clock_face_wants_background_task(movement_settings_t *settings, void *context); +void clock_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); +void clock_face_activate(movement_settings_t *settings, void *context); +bool clock_face_loop(movement_event_t event, movement_settings_t *settings, void *context); +void clock_face_resign(movement_settings_t *settings, void *context); +bool clock_face_wants_background_task(movement_settings_t *settings, void *context); -#define simple_clock_face ((const watch_face_t){ \ - simple_clock_face_setup, \ - simple_clock_face_activate, \ - simple_clock_face_loop, \ - simple_clock_face_resign, \ - simple_clock_face_wants_background_task, \ +#define clock_face ((const watch_face_t) { \ + clock_face_setup, \ + clock_face_activate, \ + clock_face_loop, \ + clock_face_resign, \ + clock_face_wants_background_task, \ }) -#endif // SIMPLE_CLOCK_FACE_H_ +#endif // CLOCK_FACE_H_ diff --git a/movement/watch_faces/clock/simple_clock_face.c b/movement/watch_faces/clock/simple_clock_face.c deleted file mode 100644 index fbc2c4b3..00000000 --- a/movement/watch_faces/clock/simple_clock_face.c +++ /dev/null @@ -1,161 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2022 Joey Castillo - * - * 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 <stdlib.h> -#include "simple_clock_face.h" -#include "watch.h" -#include "watch_utility.h" -#include "watch_private_display.h" - -static void _update_alarm_indicator(bool settings_alarm_enabled, simple_clock_state_t *state) { - state->alarm_enabled = settings_alarm_enabled; - if (state->alarm_enabled) watch_set_indicator(WATCH_INDICATOR_SIGNAL); - else watch_clear_indicator(WATCH_INDICATOR_SIGNAL); -} - -void simple_clock_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) { - (void) settings; - (void) watch_face_index; - - if (*context_ptr == NULL) { - *context_ptr = malloc(sizeof(simple_clock_state_t)); - simple_clock_state_t *state = (simple_clock_state_t *)*context_ptr; - state->signal_enabled = false; - state->watch_face_index = watch_face_index; - } -} - -void simple_clock_face_activate(movement_settings_t *settings, void *context) { - simple_clock_state_t *state = (simple_clock_state_t *)context; - - if (watch_tick_animation_is_running()) watch_stop_tick_animation(); - - if (settings->bit.clock_mode_24h) watch_set_indicator(WATCH_INDICATOR_24H); - - // handle chime indicator - if (state->signal_enabled) watch_set_indicator(WATCH_INDICATOR_BELL); - else watch_clear_indicator(WATCH_INDICATOR_BELL); - - // show alarm indicator if there is an active alarm - _update_alarm_indicator(settings->bit.alarm_enabled, state); - - watch_set_colon(); - - // this ensures that none of the timestamp fields will match, so we can re-render them all. - state->previous_date_time = 0xFFFFFFFF; -} - -bool simple_clock_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { - simple_clock_state_t *state = (simple_clock_state_t *)context; - char buf[11]; - uint8_t pos; - - watch_date_time date_time; - uint32_t previous_date_time; - switch (event.event_type) { - case EVENT_ACTIVATE: - case EVENT_TICK: - case EVENT_LOW_ENERGY_UPDATE: - date_time = watch_rtc_get_date_time(); - previous_date_time = state->previous_date_time; - state->previous_date_time = date_time.reg; - - // check the battery voltage once a day... - if (date_time.unit.day != state->last_battery_check) { - state->last_battery_check = date_time.unit.day; - watch_enable_adc(); - uint16_t voltage = watch_get_vcc_voltage(); - watch_disable_adc(); - // 2.2 volts will happen when the battery has maybe 5-10% remaining? - // we can refine this later. - state->battery_low = (voltage < 2200); - } - - // ...and set the LAP indicator if low. - if (state->battery_low) watch_set_indicator(WATCH_INDICATOR_LAP); - - if ((date_time.reg >> 6) == (previous_date_time >> 6) && event.event_type != EVENT_LOW_ENERGY_UPDATE) { - // everything before seconds is the same, don't waste cycles setting those segments. - watch_display_character_lp_seconds('0' + date_time.unit.second / 10, 8); - watch_display_character_lp_seconds('0' + date_time.unit.second % 10, 9); - break; - } else if ((date_time.reg >> 12) == (previous_date_time >> 12) && event.event_type != EVENT_LOW_ENERGY_UPDATE) { - // everything before minutes is the same. - pos = 6; - sprintf(buf, "%02d%02d", date_time.unit.minute, date_time.unit.second); - } else { - // other stuff changed; let's do it all. - if (!settings->bit.clock_mode_24h) { - // if we are in 12 hour mode, do some cleanup. - if (date_time.unit.hour < 12) { - watch_clear_indicator(WATCH_INDICATOR_PM); - } else { - watch_set_indicator(WATCH_INDICATOR_PM); - } - date_time.unit.hour %= 12; - if (date_time.unit.hour == 0) date_time.unit.hour = 12; - } - pos = 0; - if (event.event_type == EVENT_LOW_ENERGY_UPDATE) { - if (!watch_tick_animation_is_running()) watch_start_tick_animation(500); - sprintf(buf, "%s%2d%2d%02d ", watch_utility_get_weekday(date_time), date_time.unit.day, date_time.unit.hour, date_time.unit.minute); - } else { - sprintf(buf, "%s%2d%2d%02d%02d", watch_utility_get_weekday(date_time), date_time.unit.day, date_time.unit.hour, date_time.unit.minute, date_time.unit.second); - } - } - watch_display_string(buf, pos); - // handle alarm indicator - if (state->alarm_enabled != settings->bit.alarm_enabled) _update_alarm_indicator(settings->bit.alarm_enabled, state); - break; - case EVENT_ALARM_LONG_PRESS: - state->signal_enabled = !state->signal_enabled; - if (state->signal_enabled) watch_set_indicator(WATCH_INDICATOR_BELL); - else watch_clear_indicator(WATCH_INDICATOR_BELL); - break; - case EVENT_BACKGROUND_TASK: - // uncomment this line to snap back to the clock face when the hour signal sounds: - // movement_move_to_face(state->watch_face_index); - movement_play_signal(); - break; - default: - return movement_default_loop_handler(event, settings); - } - - return true; -} - -void simple_clock_face_resign(movement_settings_t *settings, void *context) { - (void) settings; - (void) context; -} - -bool simple_clock_face_wants_background_task(movement_settings_t *settings, void *context) { - (void) settings; - simple_clock_state_t *state = (simple_clock_state_t *)context; - if (!state->signal_enabled) return false; - - watch_date_time date_time = watch_rtc_get_date_time(); - - return date_time.unit.minute == 0; -} diff --git a/movement/watch_faces/complication/moon_phase_face.c b/movement/watch_faces/complication/moon_phase_face.c index 9aac374a..f74de647 100644 --- a/movement/watch_faces/complication/moon_phase_face.c +++ b/movement/watch_faces/complication/moon_phase_face.c @@ -161,6 +161,10 @@ bool moon_phase_face_loop(movement_event_t event, movement_settings_t *settings, state->offset += 86400; _update(settings, state, state->offset); break; + case EVENT_ALARM_LONG_PRESS: + state->offset = 0; + _update(settings, state, state->offset); + break; case EVENT_TIMEOUT: // QUESTION: Should timeout reset offset to 0? break; diff --git a/movement/watch_faces/complication/pulsometer_face.c b/movement/watch_faces/complication/pulsometer_face.c index 2247421c..3c04aa1a 100644 --- a/movement/watch_faces/complication/pulsometer_face.c +++ b/movement/watch_faces/complication/pulsometer_face.c @@ -1,7 +1,11 @@ +/* SPDX-License-Identifier: MIT */ + /* * MIT License * - * Copyright (c) 2022 Joey Castillo + * Copyright © 2021-2022 Joey Castillo <joeycastillo@utexas.edu> <jose.castillo@gmail.com> + * Copyright © 2023 Jeremy O'Brien <neutral@fastmail.com> + * Copyright © 2024 Matheus Afonso Martins Moreira <matheus.a.m.moreira@gmail.com> (https://www.matheusmoreira.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 @@ -24,73 +28,162 @@ #include <stdlib.h> #include <string.h> + #include "pulsometer_face.h" #include "watch.h" -#define PULSOMETER_FACE_FREQUENCY_FACTOR (4ul) // refresh rate will be 2 to this power Hz (0 for 1 Hz, 2 for 4 Hz, etc.) +#ifndef PULSOMETER_FACE_TITLE +#define PULSOMETER_FACE_TITLE "PL" +#endif + +#ifndef PULSOMETER_FACE_CALIBRATION_DEFAULT +#define PULSOMETER_FACE_CALIBRATION_DEFAULT (30) +#endif + +#ifndef PULSOMETER_FACE_CALIBRATION_INCREMENT +#define PULSOMETER_FACE_CALIBRATION_INCREMENT (10) +#endif + +// tick frequency will be 2 to this power Hz (0 for 1 Hz, 2 for 4 Hz, etc.) +#ifndef PULSOMETER_FACE_FREQUENCY_FACTOR +#define PULSOMETER_FACE_FREQUENCY_FACTOR (4ul) +#endif + #define PULSOMETER_FACE_FREQUENCY (1 << PULSOMETER_FACE_FREQUENCY_FACTOR) +typedef struct { + bool measuring; + int16_t pulses; + int16_t ticks; + int8_t calibration; +} pulsometer_state_t; + +static void pulsometer_display_title(pulsometer_state_t *pulsometer) { + watch_display_string(PULSOMETER_FACE_TITLE, 0); +} + +static void pulsometer_display_calibration(pulsometer_state_t *pulsometer) { + char buf[3]; + snprintf(buf, sizeof(buf), "%2hhd", pulsometer->calibration); + watch_display_string(buf, 2); +} + +static void pulsometer_display_measurement(pulsometer_state_t *pulsometer) { + char buf[7]; + snprintf(buf, sizeof(buf), "%-6hd", pulsometer->pulses); + watch_display_string(buf, 4); +} + +static void pulsometer_indicate(pulsometer_state_t *pulsometer) { + if (pulsometer->measuring) { + watch_set_indicator(WATCH_INDICATOR_LAP); + } else { + watch_clear_indicator(WATCH_INDICATOR_LAP); + } +} + +static void pulsometer_start_measurement(pulsometer_state_t *pulsometer) { + pulsometer->measuring = true; + pulsometer->pulses = INT16_MAX; + pulsometer->ticks = 0; + + pulsometer_indicate(pulsometer); + + movement_request_tick_frequency(PULSOMETER_FACE_FREQUENCY); +} + +static void pulsometer_measure(pulsometer_state_t *pulsometer) { + if (!pulsometer->measuring) { return; } + + pulsometer->ticks++; + + float ticks_per_minute = 60 << PULSOMETER_FACE_FREQUENCY_FACTOR; + float pulses_while_button_held = ticks_per_minute / pulsometer->ticks; + float calibrated_pulses = pulses_while_button_held * pulsometer->calibration; + calibrated_pulses += 0.5f; + + pulsometer->pulses = (int16_t) calibrated_pulses; + + pulsometer_display_measurement(pulsometer); +} + +static void pulsometer_stop_measurement(pulsometer_state_t *pulsometer) { + movement_request_tick_frequency(1); + + pulsometer->measuring = false; + + pulsometer_display_measurement(pulsometer); + pulsometer_indicate(pulsometer); +} + +static void pulsometer_cycle_calibration(pulsometer_state_t *pulsometer, int8_t increment) { + if (pulsometer->measuring) { return; } + + if (pulsometer->calibration <= 0) { + pulsometer->calibration = 1; + } + + int8_t last = pulsometer->calibration; + pulsometer->calibration += increment; + + if (pulsometer->calibration > 39) { + pulsometer->calibration = last == 39? 1 : 39; + } + + pulsometer_display_calibration(pulsometer); +} + void pulsometer_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) { (void) settings; (void) watch_face_index; - if (*context_ptr == NULL) *context_ptr = malloc(sizeof(pulsometer_state_t)); + + if (*context_ptr == NULL) { + pulsometer_state_t *pulsometer = malloc(sizeof(pulsometer_state_t)); + + pulsometer->calibration = PULSOMETER_FACE_CALIBRATION_DEFAULT; + pulsometer->pulses = 0; + pulsometer->ticks = 0; + + *context_ptr = pulsometer; + } } void pulsometer_face_activate(movement_settings_t *settings, void *context) { (void) settings; - memset(context, 0, sizeof(pulsometer_state_t)); + + pulsometer_state_t *pulsometer = context; + + pulsometer->measuring = false; + + pulsometer_display_title(pulsometer); + pulsometer_display_calibration(pulsometer); + pulsometer_display_measurement(pulsometer); } bool pulsometer_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { (void) settings; - pulsometer_state_t *pulsometer_state = (pulsometer_state_t *)context; - char buf[14]; + + pulsometer_state_t *pulsometer = (pulsometer_state_t *) context; + switch (event.event_type) { case EVENT_ALARM_BUTTON_DOWN: - pulsometer_state->measuring = true; - pulsometer_state->pulse = 0xFFFF; - pulsometer_state->ticks = 0; - movement_request_tick_frequency(PULSOMETER_FACE_FREQUENCY); + pulsometer_start_measurement(pulsometer); break; case EVENT_ALARM_BUTTON_UP: case EVENT_ALARM_LONG_UP: - pulsometer_state->measuring = false; - movement_request_tick_frequency(1); + pulsometer_stop_measurement(pulsometer); break; case EVENT_TICK: - if (pulsometer_state->pulse == 0 && !pulsometer_state->measuring) { - switch (pulsometer_state->ticks % 5) { - case 0: - watch_display_string(" Hold ", 2); - break; - case 1: - watch_display_string(" Alarn", 4); - break; - case 2: - watch_display_string("* Count ", 0); - break; - case 3: - watch_display_string(" 30Beats ", 0); - break; - case 4: - watch_clear_display(); - break; - } - pulsometer_state->ticks = (pulsometer_state->ticks + 1) % 5; - } else { - if (pulsometer_state->measuring && pulsometer_state->ticks) { - pulsometer_state->pulse = (int16_t)((30.0 * ((float)(60 << PULSOMETER_FACE_FREQUENCY_FACTOR) / (float)pulsometer_state->ticks)) + 0.5); - } - if (pulsometer_state->pulse > 240) { - watch_display_string(" Hi", 0); - } else if (pulsometer_state->pulse < 40) { - watch_display_string(" Lo", 0); - } else { - sprintf(buf, " %-3dbpn", pulsometer_state->pulse); - watch_display_string(buf, 0); - } - if (pulsometer_state->measuring) pulsometer_state->ticks++; - } + pulsometer_measure(pulsometer); + break; + case EVENT_LIGHT_BUTTON_UP: + pulsometer_cycle_calibration(pulsometer, 1); + break; + case EVENT_LIGHT_LONG_UP: + pulsometer_cycle_calibration(pulsometer, PULSOMETER_FACE_CALIBRATION_INCREMENT); + break; + case EVENT_LIGHT_BUTTON_DOWN: + // Inhibit the LED break; case EVENT_TIMEOUT: movement_move_to_face(0); diff --git a/movement/watch_faces/complication/pulsometer_face.h b/movement/watch_faces/complication/pulsometer_face.h index 288b62c4..5c1dae91 100644 --- a/movement/watch_faces/complication/pulsometer_face.h +++ b/movement/watch_faces/complication/pulsometer_face.h @@ -1,7 +1,12 @@ +/* SPDX-License-Identifier: MIT */ + /* * MIT License * - * Copyright (c) 2022 Joey Castillo + * Copyright © 2021-2022 Joey Castillo <joeycastillo@utexas.edu> <jose.castillo@gmail.com> + * Copyright © 2022 Alexsander Akers <me@a2.io> + * Copyright © 2023 Alex Utter <ooterness@gmail.com> + * Copyright © 2024 Matheus Afonso Martins Moreira <matheus.a.m.moreira@gmail.com> (https://www.matheusmoreira.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 @@ -28,38 +33,44 @@ /* * PULSOMETER face * - * The Pulsometer is an implementation of a sort of a classic mechanical - * watch complication. A classic pulsometer complication involves a - * chronograph with a scale calibrated for counting a certain number of - * heartbeats (often 30). You start it and begin counting heartbeats, and - * stop it after counting the specified number of beats. Once stopped, - * the needle will point to your heart rate. - * - * The pulsometer on Sensor Watch flashes its instructions at launch: - * “Hold Alarm + count 30 beats.” Using the hand on the side where you wear - * your watch, touch your carotid artery (in your neck) and feel for your - * pulse. Once you find it, use your other hand to press and hold the Alarm - * button, and count your heartbeats. When you reach 30 beats, release the - * Alarm button. The display will show a number such as “60 bpm”; this is - * your heart rate in beats per minute. - * - * Two notes: - * o For the first few seconds of a measurement, the display will read “Hi”. - * This indicates that it’s too early for the measured value to be a valid - * heart rate. Once the measurement is below 240 bpm, the display will update. - * o If you hold the button down for more than 45 seconds, the display will - * read “Lo”. If it took this long for you to count 30 heartbeats, this - * indicates that your heart rate is below 40 beats per minute. + * The pulsometer implements a classic mechanical watch complication. + * A mechanical pulsometer involves a chronograph with a scale that + * allows the user to compute the number of heart beats per minute + * in less time. The scale is calibrated, or graduated, for a fixed + * number of heart beats, most often 30. The user starts the chronograph + * and simultaneously begins counting the heart beats. The movement of + * the chronograph's seconds hand over time automatically performs the + * computations required. When the calibrated number of heart beats + * is reached, the chronograph is stopped and the seconds hand shows + * the heart rate. + * + * The Sensor Watch pulsometer improves this design with user calibration: + * it can be graduated to any value between 1 and 39 pulsations per minute. + * The default is still 30, mirroring the classic pulsometer calibration. + * This feature allows the user to reconfigure the pulsometer to count + * many other types of periodic minutely events, making it more versatile. + * For example, it can be set to 5 respirations per minute to turn it into + * an asthmometer, a nearly identical mechanical watch complication + * that doctors might use to quickly measure respiratory rate. + * + * To use the pulsometer, hold the ALARM button and count the pulses. + * When the calibrated number of pulses is reached, release the button. + * The display will show the number of pulses per minute. + * + * In order to measure heart rate, feel for a pulse using the hand with + * the watch while holding the button down with the other. + * The pulse can be easily felt on the carotid artery of the neck. + * + * In order to measure breathing rate, simply hold the ALARM button + * and count the number of breaths. + * + * To calibrate the pulsometer, press LIGHT + * to cycle to the next integer calibration. + * Long press LIGHT to cycle it by 10. */ #include "movement.h" -typedef struct { - bool measuring; - int16_t pulse; - int16_t ticks; -} pulsometer_state_t; - void pulsometer_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); void pulsometer_face_activate(movement_settings_t *settings, void *context); bool pulsometer_face_loop(movement_event_t event, movement_settings_t *settings, void *context); diff --git a/movement/watch_faces/complication/randonaut_face.c b/movement/watch_faces/complication/randonaut_face.c index bca334fb..3bbc147f 100644 --- a/movement/watch_faces/complication/randonaut_face.c +++ b/movement/watch_faces/complication/randonaut_face.c @@ -357,7 +357,7 @@ static uint32_t _get_true_entropy(void) { while (!hri_trng_get_INTFLAG_reg(TRNG, TRNG_INTFLAG_DATARDY)); // Wait for TRNG data to be ready - hri_trng_clear_CTRLA_ENABLE_bit(TRNG); + watch_disable_TRNG(); hri_mclk_clear_APBCMASK_TRNG_bit(MCLK); return hri_trng_read_DATA_reg(TRNG); // Read a single 32-bit word from TRNG and return it #endif diff --git a/movement/watch_faces/complication/sunrise_sunset_face.c b/movement/watch_faces/complication/sunrise_sunset_face.c index 82de9c6e..7330c42c 100644 --- a/movement/watch_faces/complication/sunrise_sunset_face.c +++ b/movement/watch_faces/complication/sunrise_sunset_face.c @@ -197,6 +197,8 @@ static void _sunrise_sunset_face_update_settings_display(movement_event_t event, char buf[12]; switch (state->page) { + case 0: + return; case 1: sprintf(buf, "LA %c %04d", state->working_latitude.sign ? '-' : '+', abs(_sunrise_sunset_face_latlon_from_struct(state->working_latitude))); break; diff --git a/movement/watch_faces/complication/toss_up_face.c b/movement/watch_faces/complication/toss_up_face.c index 08dd0052..cf6ca680 100644 --- a/movement/watch_faces/complication/toss_up_face.c +++ b/movement/watch_faces/complication/toss_up_face.c @@ -255,7 +255,8 @@ uint32_t get_true_entropy(void) { while (!hri_trng_get_INTFLAG_reg(TRNG, TRNG_INTFLAG_DATARDY)); // Wait for TRNG data to be ready - hri_trng_clear_CTRLA_ENABLE_bit(TRNG); + watch_disable_TRNG(); + hri_mclk_clear_APBCMASK_TRNG_bit(MCLK); return hri_trng_read_DATA_reg(TRNG); // Read a single 32-bit word from TRNG and return it #endif diff --git a/watch-library/hardware/hal/include/hpl_sleep.h b/watch-library/hardware/hal/include/hpl_sleep.h index 6731ec30..4106fb73 100644 --- a/watch-library/hardware/hal/include/hpl_sleep.h +++ b/watch-library/hardware/hal/include/hpl_sleep.h @@ -71,6 +71,16 @@ extern "C" { int32_t _set_sleep_mode(const uint8_t mode); /** + * \brief Get the sleep mode for the device + * + * This function gets the sleep mode for the device. + * + * \return the current value of the sleep mode configuration bits + */ +int32_t _get_sleep_mode(void); + + +/** * \brief Reset MCU */ void _reset_mcu(void); diff --git a/watch-library/hardware/hal/src/hal_sleep.c b/watch-library/hardware/hal/src/hal_sleep.c index 89472f15..2fac64d5 100644 --- a/watch-library/hardware/hal/src/hal_sleep.c +++ b/watch-library/hardware/hal/src/hal_sleep.c @@ -57,6 +57,15 @@ int sleep(const uint8_t mode) if (ERR_NONE != _set_sleep_mode(mode)) return ERR_INVALID_ARG; + // wait for the mode set to actually take, per note in Microchip data + // sheet DS60001465, section 19.8.2: + // + // A small latency happens between the store instruction and actual + // writing of the SLEEPCFG register due to bridges. Software has to make + // sure the SLEEPCFG register reads the wanted value before issuing WFI + // instruction. + while(_get_sleep_mode() != mode); + _go_to_sleep(); return ERR_NONE; diff --git a/watch-library/hardware/hpl/pm/hpl_pm.c b/watch-library/hardware/hpl/pm/hpl_pm.c index d6439f1d..2e9e37b5 100644 --- a/watch-library/hardware/hpl/pm/hpl_pm.c +++ b/watch-library/hardware/hpl/pm/hpl_pm.c @@ -64,6 +64,14 @@ int32_t _set_sleep_mode(const uint8_t mode) } /** + * \brief Get the sleep mode for the device + */ +int32_t _get_sleep_mode() +{ + return hri_pm_read_SLEEPCFG_SLEEPMODE_bf(PM); +} + +/** * \brief Set performance level */ void _set_performance_level(const uint8_t level) diff --git a/watch-library/hardware/main.c b/watch-library/hardware/main.c index 325610f6..8ac4fca6 100755 --- a/watch-library/hardware/main.c +++ b/watch-library/hardware/main.c @@ -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); diff --git a/watch-library/hardware/startup_saml22.c b/watch-library/hardware/startup_saml22.c index f4982564..2d2027f0 100755 --- a/watch-library/hardware/startup_saml22.c +++ b/watch-library/hardware/startup_saml22.c @@ -220,6 +220,5 @@ void Reset_Handler(void) */ void Dummy_Handler(void) { - while (1) { - } + NVIC_SystemReset(); } diff --git a/watch-library/hardware/watch/watch_deepsleep.c b/watch-library/hardware/watch/watch_deepsleep.c index ae2ad31d..efdad6dd 100644 --- a/watch-library/hardware/watch/watch_deepsleep.c +++ b/watch-library/hardware/watch/watch_deepsleep.c @@ -22,6 +22,8 @@ * SOFTWARE. */ +#include "hpl_systick_config.h" + #include "watch_extint.h" // this warning only appears when you `make BOARD=OSO-SWAT-A1-02`. it's annoying, @@ -158,14 +160,20 @@ void watch_enter_sleep_mode(void) { // disable brownout detector interrupt, which could inadvertently wake us up. SUPC->INTENCLR.bit.BOD33DET = 1; + // per Microchip datasheet clarification DS80000782, + // work around silicon erratum 1.8.4 by disabling the SysTick interrupt, which is + // enabled as part of driver init, before going to sleep. + SysTick->CTRL = SysTick->CTRL & ~(CONF_SYSTICK_TICKINT << SysTick_CTRL_TICKINT_Pos); + // disable all pins _watch_disable_all_pins_except_rtc(); // enter standby (4); we basically hang out here until an interrupt wakes us. sleep(4); - // and we awake! re-enable the brownout detector + // and we awake! re-enable the brownout detector and SysTick interrupt SUPC->INTENSET.bit.BOD33DET = 1; + SysTick->CTRL = SysTick->CTRL | (CONF_SYSTICK_TICKINT << SysTick_CTRL_TICKINT_Pos); // call app_setup so the app can re-enable everything we disabled. app_setup(); diff --git a/watch-library/hardware/watch/watch_private.c b/watch-library/hardware/watch/watch_private.c index cd607b8e..4de97131 100644 --- a/watch-library/hardware/watch/watch_private.c +++ b/watch-library/hardware/watch/watch_private.c @@ -23,6 +23,7 @@ */ #include "watch_private.h" +#include "watch_private_cdc.h" #include "watch_utility.h" #include "tusb.h" @@ -35,6 +36,12 @@ void _watch_init(void) { // Use switching regulator for lower power consumption. SUPC->VREG.bit.SEL = 1; + + // per Microchip datasheet clarification DS80000782, + // work around silicon erratum 1.7.2, which causes the microcontroller to lock up on leaving standby: + // request that the voltage regulator run in standby, and also that it switch to PL0. + SUPC->VREG.bit.RUNSTDBY = 1; + SUPC->VREG.bit.STDBYPL0 = 1; while(!SUPC->STATUS.bit.VREGRDY); // wait for voltage regulator to become ready // check the battery voltage... @@ -106,12 +113,21 @@ int getentropy(void *buf, size_t buflen) { } } - hri_trng_clear_CTRLA_ENABLE_bit(TRNG); + watch_disable_TRNG(); hri_mclk_clear_APBCMASK_TRNG_bit(MCLK); return 0; } +void watch_disable_TRNG() { + // per Microchip datasheet clarification DS80000782, + // silicon erratum 1.16.1 indicates that the TRNG may leave internal components powered after being disabled. + // the workaround is to disable the TRNG by clearing the control register, twice. + hri_trng_write_CTRLA_reg(TRNG, 0); + hri_trng_write_CTRLA_reg(TRNG, 0); +} + + void _watch_enable_tcc(void) { // clock TCC0 with the main clock (8 MHz) and enable the peripheral clock. hri_gclk_write_PCHCTRL_reg(GCLK, TCC0_GCLK_ID, GCLK_PCHCTRL_GEN_GCLK0_Val | GCLK_PCHCTRL_CHEN); @@ -170,6 +186,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 +313,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 index 00000000..a961b5ed --- /dev/null +++ b/watch-library/hardware/watch/watch_private_cdc.c @@ -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 index 00000000..b7fa9585 --- /dev/null +++ b/watch-library/hardware/watch/watch_private_cdc.h @@ -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 diff --git a/watch-library/shared/watch/watch.h b/watch-library/shared/watch/watch.h index 790f9a16..62e57a59 100644 --- a/watch-library/shared/watch/watch.h +++ b/watch-library/shared/watch/watch.h @@ -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,8 @@ void watch_reset_to_bootloader(void); */ int read(int file, char *ptr, int len); -#endif /* WATCH_H_ */
\ No newline at end of file +/** @brief Disables the TRNG twice in order to work around silicon erratum 1.16.1. + */ +void watch_disable_TRNG(); + +#endif /* WATCH_H_ */ diff --git a/watch-library/shared/watch/watch_private.h b/watch-library/shared/watch/watch_private.h index 9d55bc21..8fcc5755 100644 --- a/watch-library/shared/watch/watch_private.h +++ b/watch-library/shared/watch/watch_private.h @@ -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 diff --git a/watch-library/simulator/watch/watch_private.c b/watch-library/simulator/watch/watch_private.c index 3425341a..03e1f08b 100644 --- a/watch-library/simulator/watch/watch_private.c +++ b/watch-library/simulator/watch/watch_private.c @@ -57,6 +57,8 @@ void _watch_disable_tcc(void) {} void _watch_enable_usb(void) {} +void watch_disable_TRNG() {} + // this function ends up getting called by printf to log stuff to the USB console. int _write(int file, char *ptr, int len) { // TODO: (a2) hook to UI |