diff options
-rw-r--r-- | .vscode/settings.json | 3 | ||||
-rw-r--r-- | CMakeLists.txt | 2 | ||||
-rw-r--r-- | api.cpp | 8 | ||||
-rw-r--r-- | api.hpp | 3 | ||||
-rw-r--r-- | apps/home_menu.cpp | 4 | ||||
-rw-r--r-- | apps/settings/main.cpp | 212 | ||||
-rw-r--r-- | apps/settings/main.hpp | 10 | ||||
-rw-r--r-- | buttons.hpp | 14 | ||||
-rw-r--r-- | pico-watch.cpp | 22 |
9 files changed, 261 insertions, 17 deletions
diff --git a/.vscode/settings.json b/.vscode/settings.json index 37cfe28..a837a13 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -33,6 +33,7 @@ "typeinfo": "cpp", "pico.h": "c", "config.h": "c", - "config_autogen.h": "c" + "config_autogen.h": "c", + "ctime": "cpp" } }
\ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 8ec8115..0b0cc8a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,6 +50,8 @@ add_executable(pico-watch apps/home_menu.hpp apps/main_clock.cpp apps/main_clock.hpp + apps/settings/main.cpp + apps/settings/main.hpp ) pico_set_program_name(pico-watch "pico-watch") @@ -18,7 +18,7 @@ void Api::init_display() { sleep_ms(500); // Wait for the OLED to settle oledInit(&m_oled, OLED_128x64, 0x3d, 0, 0, 1, SDA_PIN, SCL_PIN, RESET_PIN, 1000000L); oledFill(&m_oled, 0,1); - oledSetContrast(&m_oled, OLED_DEFAULT_CONTRAST); + oledSetContrast(&m_oled, g_user.oled_contrast); oledSetBackBuffer(&m_oled, m_ucBuffer); // Seems to be required to draw lines, rectangles… //oledSetTextWrap(&oled, true); } @@ -27,6 +27,10 @@ void Api::display_power(bool mode) { oledPower(&m_oled, mode); } +void Api::display_set_contrast(unsigned char contrast) { + oledSetContrast(&m_oled, contrast); +} + int Api::display_write_string(int iScrollX, int x, int y, const char *szMsg, int iSize, int bInvert, int bRender) { oledWriteString(&m_oled, iScrollX, x, y, szMsg, iSize, bInvert, 0); m_writebb_needed = true; @@ -41,7 +45,7 @@ void Api::display_draw_line(int x1, int y1, int x2, int y2, int bRender) { m_writebb_needed = true; } -void Api::display_draw_rectange(int x1, int y1, int x2, int y2, uint8_t ucColor, uint8_t bFilled) { +void Api::display_draw_rectange(int x1, int y1, int x2, int y2, uint8_t ucColor, uint8_t bFilled) { // FIXME: Fix typo oledRectangle(&m_oled, x1, y1, x2, y2, ucColor, bFilled); m_writebb_needed = true; // Write the back buffer, after experimentation, seems to be required when drawing this shape } @@ -39,6 +39,9 @@ class Api { void init(); // Control the display's power (on or off) void display_power(bool mode); + // Set the display's contrast. + // \param contrast Between 0 and 255 + void display_set_contrast(unsigned char contrast); int display_write_string(int iScrollX, int x, int y, const char *szMsg, int iSize, int bInvert, int bRender); void display_fill(unsigned char ucData, int bRender); void display_draw_line(int x1, int y1, int x2, int y2, int bRender); diff --git a/apps/home_menu.cpp b/apps/home_menu.cpp index 531a53c..239ffda 100644 --- a/apps/home_menu.cpp +++ b/apps/home_menu.cpp @@ -8,11 +8,11 @@ extern void app_switch(int old_appid, int new_appid); extern bool rtc_get_datetime(datetime_t *t); -#define NUMBER_OF_APPS 2 +#define NUMBER_OF_APPS 3 #define SIZE_APP_NAME 12 namespace app_home_menu { - const char *APPS_NAME[NUMBER_OF_APPS] = {"Home", "Clock"}; + const char *APPS_NAME[NUMBER_OF_APPS] = {"Home", "Clock", "Settings"}; int selected_app = 0; char display_app_name[SIZE_APP_NAME]; diff --git a/apps/settings/main.cpp b/apps/settings/main.cpp new file mode 100644 index 0000000..5472adf --- /dev/null +++ b/apps/settings/main.cpp @@ -0,0 +1,212 @@ +#include <stdio.h> +#include "pico/stdlib.h" + +#include "main.hpp" +#include "../../api.hpp" + +extern void app_switch(int old_appid, int new_appid); +extern bool rtc_get_datetime(datetime_t *t); + +#define MAIN_SET_NUM 2 +#define MAIN_SET_NUM_STR "2" +#define SIZE_SETTING_NAME 12 +#define STR_SET_APPLY "Apply and close" +#define STR_SET_CANCEL "Quit without saving" +#define SET0_NAME "Date & Time" +#define SET1_NAME "Display" + +#define SET0_DESC "Set date/time. Choose unit to change:" +#define SET0_0_DESC "Adjust selected unit. Use good values!" +#define SET0_1_DESC "Set the current month or day of week." + +#define SET1_DESC "Adjust settings related to OLED display." +#define SET1_0_DESC "Adjust display brightness." +#define SET1_1_DESC "Time before turning off OLED and entering low power." +#define SET1_2_DESC "Set display time format.\nCurrent:\nY: 24h\nN: AM/PM" +#define SET1_2_DESC_INDEX_CURRENT 33 // Don't forget me! +#define SET1_1_MIN 5000 +// According to https://stackoverflow.com/questions/589575/what-does-the-c-standard-state-the-size-of-int-long-type-to-be : +#define SET1_1_MAX 65500 +#define SET1_1_STEP 500 + +namespace app_settings { + const char *MAIN_SET_NAMES[MAIN_SET_NUM] = {SET0_NAME, SET1_NAME}; + int selected_setting = 0; + char display_setting_name[SIZE_SETTING_NAME]; + bool selected = false; + + void show_title(Api *app_api) { + std::string title_str {"Settings (/" MAIN_SET_NUM_STR ")"}; + title_str.insert(10, std::to_string(selected_setting+1)); + app_api->gui_header_text(title_str); + } + + // Time and date settings + void set0_menu(Api *app_api) { + #define NUM_CHOICES 9 + static const char *choices[NUM_CHOICES] = {"Hour", "Minute", "Second", "Year", "Month", "Day", "Day of week", STR_SET_APPLY, STR_SET_CANCEL}; + uint max_value; + uint min_value; + uint default_value; + datetime_t datetime; + app_api->datetime_get(&datetime); + + int choice = 0; + while (true) { + choice = app_api->gui_popup_strchoice(SET0_NAME, SET0_DESC, choices, NUM_CHOICES, 0, -1, choice); + + min_value = 0; + switch (choice) { + case 0: // Hour + max_value = 23; + default_value = datetime.hour; + break; + case 1: // Minute + max_value = 59; + default_value = datetime.min; + break; + case 2: // Second + max_value = 59; + default_value = datetime.sec; + break; + case 3: // Year + max_value = 4095; + default_value = datetime.year; + break; + case 5: // Day + max_value = 31; // FIXME: Depends on month! + min_value = 1; + default_value = datetime.day; + break; + case 7: // Apply and exit + app_api->datetime_set(&datetime); + case 8: // Quit without saving + return; + } + + // Display popup for editing value + int new_value; + if (choice == 4) { // Month + // From newlib (which already includes this array of char arrays in the complied program). This should not take more space. + // See newlib/libc/locale/timelocal.c (mirror @ https://github.com/bminor/newlib/blob/80cda9bbda04a1e9e3bee5eadf99061ed69ca5fb/newlib/libc/locale/timelocal.c#L40) + static const char *month_choice[12] = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}; + + datetime.month = app_api->gui_popup_strchoice(choices[choice], SET0_1_DESC, month_choice, 12, 0, -1, datetime.month - 1) + 1; + + } else if (choice == 6) { // Day of week + static const char *dotw_choice[7] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}; + + datetime.dotw = app_api->gui_popup_strchoice(choices[choice], SET0_1_DESC, dotw_choice, 7, 0, -1, datetime.dotw); + + } else { + new_value = app_api->gui_popup_intchoice(choices[choice], SET0_0_DESC, min_value, max_value, default_value); + } + + // Store the modification in the struct + switch (choice) { + case 0: datetime.hour = new_value; break; + case 1: datetime.min = new_value; break; + case 2: datetime.sec = new_value; break; + case 3: datetime.year = new_value; break; + // Month & date of week is stored directly after their special popup. + case 5: datetime.day = new_value; break; + } + } + #undef NUM_CHOICES + } + + // Display settings + void set1_menu(Api *app_api) { + #define NUM_CHOICES 4 + static const char *choices[NUM_CHOICES] = {"Display brightness", "Sleep timeout", "Time format", STR_SET_APPLY}; + + // There is no space for the "AM/PM" as this would push the last "M" on a new line, to make it nicer, a space could be afforded before the "24h" text. + static const char *time_format[2] = {"AM/PM", " 24h"}; + + int choice = 0; + while (true) { + choice = app_api->gui_popup_strchoice(SET1_NAME, SET1_DESC, choices, NUM_CHOICES, 0,-1,choice); + + switch(choice) { + case 0: // Display brightness + g_user.oled_contrast = app_api->gui_popup_intchoice(SET1_NAME, SET1_0_DESC, 1, 255, g_user.oled_contrast, 5); + app_api->display_set_contrast(g_user.oled_contrast); + break; + case 1: // Sleep timeout + g_user.sleep_delay = app_api->gui_popup_intchoice(SET1_NAME, SET1_1_DESC, SET1_1_MIN, SET1_1_MAX, g_user.sleep_delay, SET1_1_STEP); + break; + case 2: // Time format + // TODO: Rewrite this, one day + g_user.time_format = app_api->gui_popup_booleanchoice(SET1_NAME, ((std::string)SET1_2_DESC).insert(SET1_2_DESC_INDEX_CURRENT, time_format[(int)g_user.time_format])); + break; + case 3: + return; + } + } + #undef NUM_CHOICES + } + + // Rendering of app + int render(Api *app_api) { + show_title(app_api); + app_api->display_write_string(0,0,3, display_setting_name, FONT_12x16, 0, 1); + + if (selected) { + selected = false; + switch (selected_setting) { + case 0: + set0_menu(app_api); + break; + case 1: + set1_menu(app_api); + break; + } + } + + return 0; + } + + // Example of how button inputs could be interpreted. + // Drawing on screen should be done in the render function. + int btnpressed(Api *app_api, uint gpio, unsigned long delta) { + switch (gpio) { + case BUTTON_SELECT: + selected = true; + break; + case BUTTON_DOWN: + selected_setting--; + break; + case BUTTON_UP: + selected_setting++; + break; + } + if (selected_setting > MAIN_SET_NUM-1) { + selected_setting = MAIN_SET_NUM-1; + } else if (selected_setting < 0) { + selected_setting = 0; + } + // Add spaces to avoid "ghost" characters from app names displayed before + snprintf(display_setting_name, SIZE_SETTING_NAME, "%s ", MAIN_SET_NAMES[selected_setting]); + return 0; + } + + // Initlisation of the app. + int init(Api *app_api) { + app_api->performance_set(Api::perf_modes::LOW_POWER); + app_api->performance_render_interval_set(100); + selected_setting = 0; + selected = false; + snprintf(display_setting_name, SIZE_SETTING_NAME, "%s", MAIN_SET_NAMES[0]); + return Api::app_init_return_status::OK; + } + + // Processor intensive operations and functions related to drawing to the screen should only be done when the app is in_foreground(=1). This function is only called when the app is init. + int bgrefresh(Api *app_api, bool in_foreground) { + return 1; + } + + // Destruction of app, deinitlisation should be done here. This is only called if the app's APPS_DESTROY_ON_EXIT is set to 1. When it is not a "service" app. + int destroy(Api *app_api) { + return 0; + } +} diff --git a/apps/settings/main.hpp b/apps/settings/main.hpp new file mode 100644 index 0000000..927caca --- /dev/null +++ b/apps/settings/main.hpp @@ -0,0 +1,10 @@ +#pragma once +#include "../../api.hpp" + +namespace app_settings { + int init(Api *app_api); + int render(Api *app_api); + int btnpressed(Api *app_api, uint gpio, unsigned long delta); + int bgrefresh(Api *app_api, bool in_foreground); + int destroy(Api *app_api); +} diff --git a/buttons.hpp b/buttons.hpp index e5bf4e2..c9a8b2d 100644 --- a/buttons.hpp +++ b/buttons.hpp @@ -1,6 +1,6 @@ #ifndef __BUTTONS_H__ #define __BUTTONS_H__ -#include <stdio.h> +#include "init.hpp" // Init buttons used in conjuction with interrupts // All buttons are connected to ground. @@ -23,10 +23,20 @@ struct global_status { // See https://www.raspberrypi.org/forums/viewtopic.php?f=145&t=301522#p1812063 // Time is currently shared between all buttons. unsigned long button_last_pressed_time; - const int button_delay_time = 50; + const int button_delay_time = 125; +}; + +struct user_settings { + unsigned char oled_contrast = OLED_DEFAULT_CONTRAST; + // In milliseconds + unsigned int sleep_delay = ENTER_SLEEP_DELAY; + // true: 24h, false: AM/PM + // TODO: Use an enum, but this would make programming the UI more complex. + bool time_format = true; }; extern global_status g_s; +extern user_settings g_user; void init_buttons(); void gpio_interrupt_cb(uint gpio, uint32_t events); diff --git a/pico-watch.cpp b/pico-watch.cpp index bbaa679..f75317e 100644 --- a/pico-watch.cpp +++ b/pico-watch.cpp @@ -10,19 +10,21 @@ #include "buttons.hpp" #include "apps/main_clock.hpp" #include "apps/home_menu.hpp" +#include "apps/settings/main.hpp" global_status g_s; +user_settings g_user; Api app_api; -#define NUMBER_OF_APPS 2 +#define NUMBER_OF_APPS 3 #define APP_DATA_BUFFER_LEN 256 -int (*APPS_FUNC_INIT[NUMBER_OF_APPS])(Api *app_api) = {app_home_menu::init, app_main_clock::init}; -int (*APPS_FUNC_RENDER[NUMBER_OF_APPS])(Api *app_api) = {app_home_menu::render, app_main_clock::render}; -int (*APPS_FUNC_BTNPRESS[NUMBER_OF_APPS])(Api *app_api, uint gpio, unsigned long delta) = {app_home_menu::btnpressed, app_main_clock::btnpressed}; -int (*APPS_FUNC_BGREFRESH[NUMBER_OF_APPS])(Api *app_api, bool in_foreground) = {app_home_menu::bgrefresh, app_main_clock::bgrefresh}; -int (*APPS_FUNC_DESTROY[NUMBER_OF_APPS])(Api *app_api) = {app_home_menu::destroy, app_main_clock::destroy}; -int APPS_DESTROY_ON_EXIT[NUMBER_OF_APPS] = {0, 1}; -int APPS_IS_INIT[NUMBER_OF_APPS] = {0, 0}; // Only run in background if init +int (*APPS_FUNC_INIT[NUMBER_OF_APPS])(Api *app_api) = {app_home_menu::init, app_main_clock::init, app_settings::init}; +int (*APPS_FUNC_RENDER[NUMBER_OF_APPS])(Api *app_api) = {app_home_menu::render, app_main_clock::render, app_settings::render}; +int (*APPS_FUNC_BTNPRESS[NUMBER_OF_APPS])(Api *app_api, uint gpio, unsigned long delta) = {app_home_menu::btnpressed, app_main_clock::btnpressed, app_settings::btnpressed}; +int (*APPS_FUNC_BGREFRESH[NUMBER_OF_APPS])(Api *app_api, bool in_foreground) = {app_home_menu::bgrefresh, app_main_clock::bgrefresh, app_settings::bgrefresh}; +int (*APPS_FUNC_DESTROY[NUMBER_OF_APPS])(Api *app_api) = {app_home_menu::destroy, app_main_clock::destroy, app_settings::destroy}; +int APPS_DESTROY_ON_EXIT[NUMBER_OF_APPS] = {0, 1, 1}; +int APPS_IS_INIT[NUMBER_OF_APPS] = {0}; // Only run in background if init int app_init(int app_id) { app_api.display_fill(0,1); // Clear OLED @@ -83,11 +85,11 @@ int app_bgrefresh(int app_id) { bool repeating_callback(struct repeating_timer *t) { // Enter shallow sleep mode when needed auto time_since_last_press = time_since_button_press(); - if (!g_s.is_sleeping && time_since_last_press > ENTER_SLEEP_DELAY) { + if (!g_s.is_sleeping && time_since_last_press > g_user.sleep_delay) { g_s.is_sleeping = true; app_api.performance_set(Api::perf_modes::ENTER_SHALLOW_SLEEP); app_api.display_power(false); - } else if (g_s.is_sleeping && time_since_last_press < ENTER_SLEEP_DELAY) { + } else if (g_s.is_sleeping && time_since_last_press < g_user.sleep_delay) { g_s.is_sleeping = false; app_api.performance_set(Api::perf_modes::EXIT_SHALLOW_SLEEP); app_api.display_power(true); |