From 7c4ed9a4261b3c4ebb6bf5f1047507170f7c7e16 Mon Sep 17 00:00:00 2001 From: ConfuSomu Date: Fri, 20 Aug 2021 15:53:29 +0200 Subject: Tidy up project tree; move api, appMgr and BaseApp --- CMakeLists.txt | 5 +- api.cpp | 329 ----------------------------------------------- api.hpp | 116 ----------------- app/app_manager.cpp | 152 ++++++++++++++++++++++ app/app_manager.hpp | 45 +++++++ app/base_app.hpp | 27 ++++ app_manager.cpp | 152 ---------------------- app_manager.hpp | 45 ------- apps/home_menu/main.cpp | 2 +- apps/home_menu/main.hpp | 2 +- apps/main_clock/main.hpp | 2 +- apps/settings/main.hpp | 2 +- base_app.hpp | 27 ---- buttons.cpp | 4 +- globals.hpp | 2 +- hal/api.cpp | 329 +++++++++++++++++++++++++++++++++++++++++++++++ hal/api.hpp | 116 +++++++++++++++++ pico-watch.cpp | 4 +- 18 files changed, 680 insertions(+), 681 deletions(-) delete mode 100644 api.cpp delete mode 100644 api.hpp create mode 100644 app/app_manager.cpp create mode 100644 app/app_manager.hpp create mode 100644 app/base_app.hpp delete mode 100644 app_manager.cpp delete mode 100644 app_manager.hpp delete mode 100644 base_app.hpp create mode 100644 hal/api.cpp create mode 100644 hal/api.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index fe8dc50..0efeb1f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,13 +40,12 @@ target_link_libraries(Oled pico_stdlib hardware_i2c) # Main code add_executable(pico-watch pico-watch.cpp - app_manager.cpp + app/app_manager.cpp init.cpp init.hpp buttons.cpp buttons.hpp - api.cpp - api.hpp + hal/api.cpp apps/home_menu/main.cpp apps/home_menu/main.hpp apps/main_clock/main.cpp diff --git a/api.cpp b/api.cpp deleted file mode 100644 index 1698346..0000000 --- a/api.cpp +++ /dev/null @@ -1,329 +0,0 @@ -#include -#include "pico/stdlib.h" -extern "C" { -#include "hardware/rtc.h" -} - -#include "api.hpp" -#include "init.hpp" -#include "globals.hpp" - -void Api::init() { - if (!m_init_done) { - init_display(); - m_init_done = 1; - } -} - -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, g_user.oled_contrast); - oledSetBackBuffer(&m_oled, m_ucBuffer); // Seems to be required to draw lines, rectangles… - //oledSetTextWrap(&oled, true); -} - -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; -} - -void Api::display_fill(unsigned char ucData, int bRender) { - oledFill(&m_oled, ucData, bRender); -} - -void Api::display_draw_line(int x1, int y1, int x2, int y2, int bRender) { - oledDrawLine(&m_oled, x1, y1, x2, y2, 0); - m_writebb_needed = true; -} - -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 -} - -void Api::display_draw_ellipse(int iCenterX, int iCenterY, int32_t iRadiusX, int32_t iRadiusY, uint8_t ucColor, uint8_t bFilled) { - oledEllipse(&m_oled, iCenterX, iCenterY, iRadiusX, iRadiusY, ucColor, bFilled); - m_writebb_needed = true; -} - -void Api::display_write_buffer(uint8_t *pBuffer) { - oledDumpBuffer(&m_oled, pBuffer); -} - -void Api::display_write_backbuffer() { - if (m_writebb_needed) - oledDumpBuffer(&m_oled, m_ucBuffer); - m_writebb_needed = false; -} - -int Api::display_write_pixel(int x, int y, unsigned char ucColor, int bRender) { - return oledSetPixel(&m_oled, x, y, ucColor, bRender); -} - -void Api::gui_popup_generic(std::string &title, std::string &body, int max_title_length, int max_body_length) { - oledRectangle(&m_oled, 9,7, 119,63, 0, 1); // Background - oledRectangle(&m_oled, 9,7, 119,63, 1, 0); // Popup border - oledRectangle(&m_oled, 9,7, 119,16, 1, 1); // Title background, FIXME pixel bleeding - m_writebb_needed = true; this->display_write_backbuffer(); // Display rectangle and anything else behind it (drawn before), could be moved after writing strings (but not done for debugging) - - // Truncate longer strings to avoid wasting time in for loop and drawing on OLED - if (max_title_length > 13) max_title_length = 13; - if (max_body_length > 78) max_body_length = 78; - if (title.size() > max_title_length) - title.resize(13); - if (body.size() > max_body_length) - body.resize(78); - - // Make body fit by adding '\n' at a regular interval - #define CHARS_PER_LINE 13 - int since_nl = 0; // Characters since newline - for (std::string::size_type i = 0; i < body.size(); ++i) { - if (body[i] == '\n') - since_nl = 0; - else if (since_nl++ == CHARS_PER_LINE) { - body.insert(i, 1, '\n'); - since_nl = 0; // Probably unneeded - } - } - // See https://stackoverflow.com/questions/1986966/does-s0-point-to-contiguous-characters-in-a-stdstring - oledWriteString(&m_oled, 0, 15,1, title.c_str(), FONT_8x8, 1, 1); // Draw title - oledWriteString(&m_oled, 0, 13,2, body.c_str(), FONT_8x8, 0, 1); // Draw body -} - -bool Api::gui_popup_text(std::string title, std::string body){ - m_button_last_pressed = BUTTON_NONE; - m_interpret_button_press = false; - - gui_popup_generic(title, body); - - while (m_button_last_pressed != BUTTON_SELECT) - sleep_ms(50); // TODO: use _wfi() - // Give back control to running app - oledFill(&m_oled, 0, 1); - m_interpret_button_press = true; - return true; -} - -bool Api::gui_popup_booleanchoice(std::string title, std::string body){ - m_button_last_pressed = BUTTON_NONE; - m_interpret_button_press = false; - - title.insert(0, "Choice|"); // TODO: Could be made nicer with a custom char that uses the whole height, this would give a visible separation, with two "text blocks" composing the title - gui_popup_generic(title, body); - - while (m_button_last_pressed != BUTTON_SELECT and m_button_last_pressed != BUTTON_MODE) - sleep_ms(50); // TODO: use _wfi() - - bool choice; - switch (m_button_last_pressed) { - case BUTTON_SELECT: - choice = true; - break; - case BUTTON_MODE: - choice = false; - break; - default: - __breakpoint(); // Impossible to attain (but you never know…) - } - // Give back control to running app - oledFill(&m_oled, 0, 1); - m_interpret_button_press = true; - return choice; -} - -void Api::gui_popup_intchoice_footer(int current_num, int min_num, int max_num) { - char buf[30]; - snprintf(&buf[0], sizeof(buf), - "Select: %d (%d/%d)", - current_num, - min_num, - max_num); - oledRectangle(&m_oled, 9,55, 119,63, 1, 1); // Footer background, FIXME pixel bleeding - oledWriteString(&m_oled, 0,10,7, buf, FONT_6x8, 1, 0); - m_writebb_needed = true; display_write_backbuffer(); -} - -int Api::gui_popup_intchoice(std::string title, std::string body, int min_num, int max_num, int default_num, int step){ - m_button_last_pressed = BUTTON_NONE; - m_interpret_button_press = false; - - int current_num = default_num; - - title.insert(0, "Number|"); // TODO: Could be made nicer with a custom char instead of pipe char - gui_popup_generic(title, body, 13, 39); // 39: 3 lines of body text, to leave space for number - gui_popup_intchoice_footer(current_num, min_num, max_num); - - do { - m_button_last_pressed = BUTTON_NONE; - sleep_ms(50); // TODO: use _wfi() - switch (m_button_last_pressed) { - case BUTTON_UP: - current_num += step; - if (current_num > max_num) - current_num = max_num; - break; - case BUTTON_DOWN: - current_num -= step; - if (current_num < min_num) - current_num = min_num; - break; - case BUTTON_MODE: - current_num = default_num; - break; - } - if (m_button_last_pressed) - gui_popup_intchoice_footer(current_num, min_num, max_num); - } while (m_button_last_pressed != BUTTON_SELECT); - - // Give back control to running app - oledFill(&m_oled, 0, 1); - m_interpret_button_press = true; - return current_num; -} - -void Api::gui_popup_strchoice_footer(const char selection[]) { - std::string buf{selection}; - buf.insert(0, "Select: "); - - if (buf.size() > 36) // 2 lines of 18 chars - buf.resize(36); - - // Choose most adapted font size - int font; - int chars_per_line; - if (buf.size() > 26) { - font = FONT_6x8; - chars_per_line = 18; - } else { - font = FONT_8x8; - chars_per_line = 13; - } - - // Make selection text fit by adding '\n' at a regular interval - // TODO: Make this a private function - int since_nl = 0; // Characters since newline - for (std::string::size_type i = 0; i < buf.size(); ++i) { - if (buf[i] == '\n') - since_nl = 0; - else if (since_nl++ == chars_per_line) { - buf.insert(i, 1, '\n'); - since_nl = 0; // Probably unneeded - } - } - - oledRectangle(&m_oled, 9,47, 119,63, 1, 1); // Footer background, FIXME pixel bleeding - oledWriteString(&m_oled, 0,10,6, buf.c_str(), font, 1, 0); - m_writebb_needed = true; display_write_backbuffer(); -} - -int Api::gui_popup_strchoice(std::string title, std::string body, const char *choices[27], int choices_size, int min_index, int max_index, int default_index){ - m_button_last_pressed = BUTTON_NONE; - m_interpret_button_press = false; - if (max_index == -1) - max_index = choices_size-1; - - int current_index = default_index; - - title.insert(0, "Choose|"); // TODO: Could be made nicer with a custom char instead of pipe char - gui_popup_generic(title, body, 13, 39); // 39: 3 lines of body text, to leave space for selection string - gui_popup_strchoice_footer(choices[current_index]); - - do { - m_button_last_pressed = BUTTON_NONE; - sleep_ms(50); // TODO: use _wfi() - switch (m_button_last_pressed) { - case BUTTON_UP: - current_index += 1; - if (current_index > max_index) - current_index = max_index; - break; - case BUTTON_DOWN: - current_index -= 1; - if (current_index < min_index) - current_index = min_index; - break; - case BUTTON_MODE: - current_index = default_index; - break; - } - if (m_button_last_pressed) - gui_popup_strchoice_footer(choices[current_index]); - } while (m_button_last_pressed != BUTTON_SELECT); - - // Give back control to running app - oledFill(&m_oled, 0, 1); - m_interpret_button_press = true; - return current_index; -} - -bool Api::gui_footer_text(std::string text, int offset_x, int offset_row, bool invert, bool no_bg) { - // Max chars per line for FONT_8x8 is 16 chars - // Max chars per line for FONT_6x8 is 21 chars - // Truncate longer text - if (text.size() > 21) - text.resize(21); - - // Choose most adapted font size - int font; - if (text.size() > 16) - font = FONT_6x8; - else - font = FONT_8x8; - - if (!no_bg) { - oledRectangle(&m_oled, 0,56-offset_row*8, 127,64-offset_row*8, invert, 1); - m_writebb_needed = true; - } - oledWriteString(&m_oled, 0,offset_x,7-offset_row, text.c_str(), font, invert, 0); -} - -bool Api::gui_header_text(std::string text, int offset_x, int offset_row, bool invert, bool no_bg) { - // Max chars per line for FONT_8x8 is 16 chars - // Max chars per line for FONT_6x8 is 21 chars - // Truncate longer text - if (text.size() > 21) - text.resize(21); - - // Choose most adapted font size - int font; - if (text.size() > 16) - font = FONT_6x8; - else - font = FONT_8x8; - - if (!no_bg) { - oledRectangle(&m_oled, 0,0+offset_row*8, 127,8+offset_row*8, invert, 1); - m_writebb_needed = true; - } - oledWriteString(&m_oled, 0,offset_x,0+offset_row, text.c_str(), font, invert, 0); -} - -bool Api::performance_set(int perf) { - return false; -} - -bool Api::datetime_get(datetime_t *t) { - return rtc_get_datetime(t); -} - -bool Api::datetime_set(datetime_t *t) { - return rtc_set_datetime(t); -} - -uint Api::button_last_get() { - return m_button_last_pressed; -} - -void Api::button_last_set(uint gpio) { - m_button_last_pressed = gpio; -} diff --git a/api.hpp b/api.hpp deleted file mode 100644 index bf0f51b..0000000 --- a/api.hpp +++ /dev/null @@ -1,116 +0,0 @@ -#pragma once - -#include -#include "pico/util/datetime.h" -#include "oled/ss_oled.h" - -#include "buttons.hpp" - -class Api { - private: - SSOLED m_oled; - u_char m_init_done = 0; - bool m_writebb_needed = false; // Do we need to write the internal backbuffer? - uint8_t m_ucBuffer[1024] = {0}; - uint m_button_last_pressed = 0; - int m_app_render_interval = 500; - void init_display(); - // Careful of large heap usage with long strings! Heap seems to be fragmented, so shorter strings work best. More details on findings below: - // When the string cannot be created, this comes from the fact that we, on some runs, allocation can be very close to the SRAM's boundry: 0x20041f88 with boundry at 0x20042000. The heap is nearly full. Even just appending to a string moves the allocation lower. I think that the best course of action would be to have more static variables and pull in less things to save SRAM. - // When char* directly passed as parameter: The memory is possibly fragmented as long strings (>215) push the allocation downwards, into lower (higher address) in the heap. title is at 0x20041f90 and body at 0x200045e8, for length of 215 chars. - void gui_popup_generic(std::string &title, std::string &body, int max_title_length = 13, int max_body_length = 78); - void gui_popup_intchoice_footer(int current_num, int min_num, int max_num); - void gui_popup_strchoice_footer(const char selection[]); - public: - // Allow button press to be registed by app and for app_switch (when HOME). Set to false when displaying Api's internal GUI. - bool m_interpret_button_press = true; - enum perf_modes { - LOW_POWER, - NORMAL_PERF, - HIGH_PERF, - ENTER_SHALLOW_SLEEP, - EXIT_SHALLOW_SLEEP // Restore perf setting - }; - 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); - void display_draw_rectange(int x1, int y1, int x2, int y2, uint8_t ucColor, uint8_t bFilled); - void display_draw_ellipse(int iCenterX, int iCenterY, int32_t iRadiusX, int32_t iRadiusY, uint8_t ucColor, uint8_t bFilled); - void display_write_buffer(uint8_t *pBuffer); - // Write the internal backbuffer to the display. Should be called when after all drawing calls. One call is done to avoid flickering of the display. - void display_write_backbuffer(); - int display_write_pixel(int x, int y, unsigned char ucColor, int bRender); - // Display a popup over the current view and wait for select button to be pressed. - // This is a blocking function and should be used only in the app's render method. - // \param title Popup's title, length should not exceed 13 characters. - // \param body String containing the popup's body. The zone has a size of 13×6 characters, so body should not be longer than 78 characters. Newline allows going to the next line and the text is automatically wrapped. - // \note Strings longer than 13 and 78 respectively will be truncated. - bool gui_popup_text(std::string title, std::string body); - // Display a popup over the current view and wait for select or mode (cancel) button to be pressed. The choice done (yes/no) by the user is returned as a bool. - // This is a blocking function and should be used only in the app's render method. - // \param title Popup's title. The title is prefixed with "Choice|", so the `title` argument cannot exceed 6 characters. - // \param body String containing the popup's body. The zone has a size of 13×6 characters, so body should not be longer than 78 characters. Newline allows going to the next line and the text is automatically wrapped. - // \note Strings longer than 13 and 78 respectively will be truncated. - bool gui_popup_booleanchoice(std::string title, std::string body); - // Display a popup over the current view and wait for user to choose (with left and right) a number between min_num and max_num. The default choice is default_num and the user can reset back to it with mode/cancel button. After confirming with select, the choice is returned. - // This is a blocking function and should be used only in the app's render method. - // \param title Popup's title. The title is prefixed with "Number|", so the `title` argument cannot exceed 6 characters. - // \param body String containing the popup's body. The zone has a size of 13×3 characters, so body should not be longer than 39 characters. Newline allows going to the next line and the text is automatically wrapped. Under the body is displayed the current choosen number with the min and max in parenthesis. - // \param min_num The smallest number that can be choosen. - // \param max_num Biggest number that can be choosen. - // \param default_num This should be between min_num and max_num, else user may be able to return a number out of range - // \param step Value to increment/decrement from when user changes number. This cannot result in an out-of-bounds as the number is clipped to the min/max when this happens. This maybe undesirable behaviour. - // \note Strings longer than 13 and 39 respectively will be truncated. - int gui_popup_intchoice(std::string title, std::string body, int min_num = 0, int max_num = 10, int default_num = 5, int step = 1); - // Display a popup over the current view and wait for user to choose (with left and right) a string (char array). The default choice is default_index and the user can reset back to it with mode/cancel button. After confirming with select, the choice's index is returned. - // This is a blocking function and should be used only in the app's render method. - // \param title Popup's title. The title is prefixed with "Choice|", so the `title` argument cannot exceed 6 characters. - // \param body String containing the popup's body. The zone has a size of 13×3 characters, so body should not be longer than 39 characters. Newline allows going to the next line and the text is automatically wrapped. Under the body is displayed the current choosen number with the min and max in parenthesis. - // \param choices List of const char arrays to choose from, each string cannot be longer than 27 chars because of "Select: " text. Font size between 6×8 and 8×8 is choosen automatically. Set the second dimension size of your array of choices to max 27. For example, define: `const char *choices[27] = {…}` than call this function by passing directly `choices`. - // \param choices_size Size of choices array, used for avoiding selection of element that are out of the array's bounds. This is only used for determining max_index when max_index is unset. **Following parameters can be left at their default value:** - // \param min_index Smallest element index that can be choosen. This allows reusing the same choice list for muliple prompts and allows programmatically changing the choices available to the user. - // \param max_index Biggest element index that can be choosen. This defaults to the size of the list (set by `choices_size`) if left at -1. - // \param default_index Default string displayed. - int gui_popup_strchoice(std::string title, std::string body, const char *choices[27], int choices_size, int min_index = 0, int max_index = -1, int default_index = 0); - // Display text at the bottom of the screen. - // The font size is automatically choosen based on the text lenght. - // \param text Text to display. Text longer than 21 will be truncated. - // \param offset_x Set a horizental offset, to allow, for example, centering the text - // \param offset_row Allow rendering the text higher. For example, one line higher when `offset_row = 1`. - // \param invert Invert text and background color. - // \param no_bg Do not draw background when true. - bool gui_footer_text(std::string text, int offset_x = 0, int offset_row = 0, bool invert = false, bool no_bg = false); - // Display text at the top of the screen. - // The font size is automatically choosen based on the text lenght. - // \param text Text to display. Text longer than 21 will be truncated. - // \param offset_x Set a horizental offset, to allow, for example, centering the text - // \param offset_row Render text lines lower. For example, one text line lower with `offset_row = 1`. - // \param invert Invert text and background color. - // \param no_bg Do not draw background when true. - bool gui_header_text(std::string text, int offset_x = 0, int offset_row = 0, bool invert = false, bool no_bg = false); - // Set performance mode. - // FIXME: function currently does nothing! - // An app should choose the lowest performance that can make it function. Set in init(). Only when required, higher performance should be used. - // \param perf See Api::perf_modes enum for possible values - bool performance_set(int perf_mode); - // Get the current datetime - // \param t Pointer to the datetime structure in which the datetime wil be stored - // \return true if the call to the SDK was successful, else false. - bool datetime_get(datetime_t *t); - // Set the current datetime - // TODO: Not every app should be allowed to set the datetime. Only app_id<2 (home_screen and settings) should be allowed: return false when setting is blocked. - // \param t Pointer to the datetime structure - // \return true if the call to the SDK was successful, else false. - bool datetime_set(datetime_t *t); - // Get last button pressed, see buttons.hpp for values - // \return Last button pressed - uint button_last_get(); - // Set last button pressed, should only be called by button gpio interrupt. - void button_last_set(uint gpio); -}; diff --git a/app/app_manager.cpp b/app/app_manager.cpp new file mode 100644 index 0000000..01bf3fd --- /dev/null +++ b/app/app_manager.cpp @@ -0,0 +1,152 @@ +#include + +#include "app_manager.hpp" +#include "../hal/api.hpp" +#include "../globals.hpp" + +// App classes following: +#include "../apps/main_clock/main.hpp" +#include "../apps/home_menu/main.hpp" +#include "../apps/settings/main.hpp" +#define NUMBER_OF_APPS 3 + +// From pico-watch.c: +extern Api app_api; + +std::vector app_mgr::open_apps; + +BaseApp* app_mgr::app_check_if_init(int app_id) { + for (auto app : open_apps) { + if (app_id == app->app_get_attributes().id) + return app; + } + return nullptr; +} + +void app_mgr::new_foreground_app(BaseApp* app) { + app_api.display_fill(0,1); // Clear OLED + g_s.foreground_app = app; +} + +void app_mgr::app_act_on_return_value(BaseApp* app, BaseApp::AppReturnValues return_value) { + switch (return_value) { + case BaseApp::AppReturnValues::OK: + break; + + case BaseApp::AppReturnValues::QUIT: + app_destroy(app); + // No break + + case BaseApp::AppReturnValues::CLOSE: + new_foreground_app(open_apps.front()); // The app has to be in foreground as the current function is called by app_render and app_btnpress + break; + + default: + printf("Unidentified return value %d for app %d at %x", return_value, app->app_get_attributes().id, app); + } +} + +BaseApp* app_mgr::app_create(int app_id) { + BaseApp* new_app; + + switch (app_id) { + case 0: new_app = new app_home_menu(&app_api); break; + case 1: new_app = new app_main_clock(&app_api); break; + case 2: new_app = new app_settings(&app_api); break; + default: __breakpoint(); return open_apps.front(); // Should be home_menu + } + + if (new_app != nullptr) + open_apps.push_back(new_app); + else { + printf("new failed for app %d. Not enough memory?", app_id); + return open_apps.front(); // Should be home_menu + } + + return open_apps.back(); +} + +BaseApp* app_mgr::app_init(int app_id) { + BaseApp* new_app; + + if (app_id > NUMBER_OF_APPS-1 or app_id < 0) { + printf("Tried to init app %d", app_id); + return app_init(0); + } + + // Check if the app is already running. + // Should this be done instead in app_mgr::app_switch() ? + auto app_ptr = app_check_if_init(app_id); + if (app_ptr) + new_app = app_ptr; + else + new_app = app_create(app_id); + + return new_app; +} + +void app_mgr::app_render(BaseApp* app) { + app_act_on_return_value(app, app->render(&app_api)); +} + +void app_mgr::app_btnpressed(BaseApp* app, uint gpio, unsigned long delta) { + app_act_on_return_value(app, app->btnpressed(&app_api, gpio, delta)); +} + +void app_mgr::app_destroy(BaseApp* to_erase) { + auto erase_it = std::find(open_apps.begin(), open_apps.end(), to_erase); // "it" meaning iterator + if (erase_it != open_apps.end()) { + //assert(to_erase == erase_it); + delete to_erase; + open_apps.erase(erase_it); + } +} + +void app_mgr::app_all_bgrefresh() { + std::vector to_erase; + bool do_erase = false; + + for (auto app : open_apps) { + bool is_foreground = app->app_get_attributes().id == g_s.foreground_app->app_get_attributes().id; + + switch (app->bgrefresh(&app_api, is_foreground)) { + case BaseApp::AppReturnValues::OK: + break; + + case BaseApp::AppReturnValues::QUIT: + do_erase = true; + to_erase.push_back(app); + // No break here! + + case BaseApp::AppReturnValues::CLOSE: + if (is_foreground) + new_foreground_app(open_apps.front()); + break; + + default: + printf("Unidentified return value in bgrefresh for app %d at %x", app->app_get_attributes().id, app); + } + } + + if (do_erase) { + for (auto app : to_erase) { + app_destroy(app); + } + } +} + +void app_mgr::app_switch_request(int to_appid) { + if (!g_s.app_switch_requested) + g_s.app_switch_to_app = to_appid; + g_s.app_switch_requested = true; +} + +void app_mgr::app_switch(BaseApp* app, int new_appid) { + g_s.app_ready = false; + + if (app->app_get_attributes().destroy_on_exit) + app_destroy(app); + + new_foreground_app(app_init(new_appid)); + g_s.app_ready = true; +} diff --git a/app/app_manager.hpp b/app/app_manager.hpp new file mode 100644 index 0000000..b7783ea --- /dev/null +++ b/app/app_manager.hpp @@ -0,0 +1,45 @@ +#pragma once +#include +#include "base_app.hpp" + +// Interface to the app manager. These functions are accessible to other parts of the code, except open_apps, which is managed by the following functions. +namespace app_mgr { + // List of pointers to currently running apps. + extern std::vector open_apps; + + // Init a new app, that is not running. + BaseApp* app_init(int app_id); + + // Allow the running app, referenced by app_id, to invoke its render routine. + void app_render(BaseApp* app); + + // Delta is in ms, from time_since_button_press() + void app_btnpressed(BaseApp* app, uint gpio, unsigned long delta); + + // This should only be called by pico-watch.cpp before app rendering, to chage the current app. + void app_switch(BaseApp* app, int new_appid); + + // Requests the current app to be replaced by an other one. The replacement will be done at the right moment. + void app_switch_request(int to_appid); + + // Refresh each app + void app_all_bgrefresh(); + + // Private functions following. I tried using anonymous namespaces but it was too complicated. I might come back to this later. Just don't use the following internal functions. + + // Check if the specified app (via app_id) is already running. + // \return If app is init, pointer to app, else nullptr (more or less 0). + BaseApp* app_check_if_init(int app_id); + + // Check the return value of the called application method and act on it. + void app_act_on_return_value(BaseApp* app, BaseApp::AppReturnValues return_value); + + // Unconditionally set the new foreground app + void new_foreground_app(BaseApp* app); + + // Called by app_init to create the app object. + BaseApp* app_create(int app_id); + + // Quit the app referenced by the app_id. + void app_destroy(BaseApp* to_erase); +} diff --git a/app/base_app.hpp b/app/base_app.hpp new file mode 100644 index 0000000..c9367de --- /dev/null +++ b/app/base_app.hpp @@ -0,0 +1,27 @@ +#pragma once +#include "../hal/api.hpp" + +// Base app +class BaseApp { + public: + struct AppAttributes { + uint id = 0; + bool destroy_on_exit = true; + uint render_interval = 500; // Interval in ms at which the render method is called. + }; + enum AppReturnValues { + OK = 0, + CLOSE, // Close app from foreground, not respecting AppAttribues::destroy_on_exit. + QUIT // Completely quit app; stop running in background + }; + + // CHECK: Following have to be overwritten by derived classes + virtual const AppAttributes& app_get_attributes() = 0; + // Could be implemented as: + // {return app_attributes;} + // where app_attribues is an instance of AppAttributes + + virtual AppReturnValues render(Api *app_api) = 0; // Has to be implemented + virtual AppReturnValues btnpressed(Api *app_api, uint gpio, unsigned long delta) {return AppReturnValues::OK;}; + virtual AppReturnValues bgrefresh(Api *app_api, bool in_foreground) {return AppReturnValues::OK;}; +}; diff --git a/app_manager.cpp b/app_manager.cpp deleted file mode 100644 index 68aa3b9..0000000 --- a/app_manager.cpp +++ /dev/null @@ -1,152 +0,0 @@ -#include - -#include "app_manager.hpp" -#include "api.hpp" -#include "globals.hpp" - -// App classes following: -#include "apps/main_clock/main.hpp" -#include "apps/home_menu/main.hpp" -#include "apps/settings/main.hpp" -#define NUMBER_OF_APPS 3 - -// From pico-watch.c: -extern Api app_api; - -std::vector app_mgr::open_apps; - -BaseApp* app_mgr::app_check_if_init(int app_id) { - for (auto app : open_apps) { - if (app_id == app->app_get_attributes().id) - return app; - } - return nullptr; -} - -void app_mgr::new_foreground_app(BaseApp* app) { - app_api.display_fill(0,1); // Clear OLED - g_s.foreground_app = app; -} - -void app_mgr::app_act_on_return_value(BaseApp* app, BaseApp::AppReturnValues return_value) { - switch (return_value) { - case BaseApp::AppReturnValues::OK: - break; - - case BaseApp::AppReturnValues::QUIT: - app_destroy(app); - // No break - - case BaseApp::AppReturnValues::CLOSE: - new_foreground_app(open_apps.front()); // The app has to be in foreground as the current function is called by app_render and app_btnpress - break; - - default: - printf("Unidentified return value %d for app %d at %x", return_value, app->app_get_attributes().id, app); - } -} - -BaseApp* app_mgr::app_create(int app_id) { - BaseApp* new_app; - - switch (app_id) { - case 0: new_app = new app_home_menu(&app_api); break; - case 1: new_app = new app_main_clock(&app_api); break; - case 2: new_app = new app_settings(&app_api); break; - default: __breakpoint(); return open_apps.front(); // Should be home_menu - } - - if (new_app != nullptr) - open_apps.push_back(new_app); - else { - printf("new failed for app %d. Not enough memory?", app_id); - return open_apps.front(); // Should be home_menu - } - - return open_apps.back(); -} - -BaseApp* app_mgr::app_init(int app_id) { - BaseApp* new_app; - - if (app_id > NUMBER_OF_APPS-1 or app_id < 0) { - printf("Tried to init app %d", app_id); - return app_init(0); - } - - // Check if the app is already running. - // Should this be done instead in app_mgr::app_switch() ? - auto app_ptr = app_check_if_init(app_id); - if (app_ptr) - new_app = app_ptr; - else - new_app = app_create(app_id); - - return new_app; -} - -void app_mgr::app_render(BaseApp* app) { - app_act_on_return_value(app, app->render(&app_api)); -} - -void app_mgr::app_btnpressed(BaseApp* app, uint gpio, unsigned long delta) { - app_act_on_return_value(app, app->btnpressed(&app_api, gpio, delta)); -} - -void app_mgr::app_destroy(BaseApp* to_erase) { - auto erase_it = std::find(open_apps.begin(), open_apps.end(), to_erase); // "it" meaning iterator - if (erase_it != open_apps.end()) { - //assert(to_erase == erase_it); - delete to_erase; - open_apps.erase(erase_it); - } -} - -void app_mgr::app_all_bgrefresh() { - std::vector to_erase; - bool do_erase = false; - - for (auto app : open_apps) { - bool is_foreground = app->app_get_attributes().id == g_s.foreground_app->app_get_attributes().id; - - switch (app->bgrefresh(&app_api, is_foreground)) { - case BaseApp::AppReturnValues::OK: - break; - - case BaseApp::AppReturnValues::QUIT: - do_erase = true; - to_erase.push_back(app); - // No break here! - - case BaseApp::AppReturnValues::CLOSE: - if (is_foreground) - new_foreground_app(open_apps.front()); - break; - - default: - printf("Unidentified return value in bgrefresh for app %d at %x", app->app_get_attributes().id, app); - } - } - - if (do_erase) { - for (auto app : to_erase) { - app_destroy(app); - } - } -} - -void app_mgr::app_switch_request(int to_appid) { - if (!g_s.app_switch_requested) - g_s.app_switch_to_app = to_appid; - g_s.app_switch_requested = true; -} - -void app_mgr::app_switch(BaseApp* app, int new_appid) { - g_s.app_ready = false; - - if (app->app_get_attributes().destroy_on_exit) - app_destroy(app); - - new_foreground_app(app_init(new_appid)); - g_s.app_ready = true; -} diff --git a/app_manager.hpp b/app_manager.hpp deleted file mode 100644 index b7783ea..0000000 --- a/app_manager.hpp +++ /dev/null @@ -1,45 +0,0 @@ -#pragma once -#include -#include "base_app.hpp" - -// Interface to the app manager. These functions are accessible to other parts of the code, except open_apps, which is managed by the following functions. -namespace app_mgr { - // List of pointers to currently running apps. - extern std::vector open_apps; - - // Init a new app, that is not running. - BaseApp* app_init(int app_id); - - // Allow the running app, referenced by app_id, to invoke its render routine. - void app_render(BaseApp* app); - - // Delta is in ms, from time_since_button_press() - void app_btnpressed(BaseApp* app, uint gpio, unsigned long delta); - - // This should only be called by pico-watch.cpp before app rendering, to chage the current app. - void app_switch(BaseApp* app, int new_appid); - - // Requests the current app to be replaced by an other one. The replacement will be done at the right moment. - void app_switch_request(int to_appid); - - // Refresh each app - void app_all_bgrefresh(); - - // Private functions following. I tried using anonymous namespaces but it was too complicated. I might come back to this later. Just don't use the following internal functions. - - // Check if the specified app (via app_id) is already running. - // \return If app is init, pointer to app, else nullptr (more or less 0). - BaseApp* app_check_if_init(int app_id); - - // Check the return value of the called application method and act on it. - void app_act_on_return_value(BaseApp* app, BaseApp::AppReturnValues return_value); - - // Unconditionally set the new foreground app - void new_foreground_app(BaseApp* app); - - // Called by app_init to create the app object. - BaseApp* app_create(int app_id); - - // Quit the app referenced by the app_id. - void app_destroy(BaseApp* to_erase); -} diff --git a/apps/home_menu/main.cpp b/apps/home_menu/main.cpp index 4508e38..bf47e1e 100644 --- a/apps/home_menu/main.cpp +++ b/apps/home_menu/main.cpp @@ -2,7 +2,7 @@ #include "pico/stdlib.h" #include "../../globals.hpp" -#include "../../app_manager.hpp" +#include "../../app/app_manager.hpp" #include "main.hpp" extern bool rtc_get_datetime(datetime_t *t); diff --git a/apps/home_menu/main.hpp b/apps/home_menu/main.hpp index d82e2a0..0e13c3f 100644 --- a/apps/home_menu/main.hpp +++ b/apps/home_menu/main.hpp @@ -3,7 +3,7 @@ #include "pico/util/datetime.h" // Includes also buttons, API and ss_oled -#include "../../base_app.hpp" +#include "../../app/base_app.hpp" #define NUMBER_OF_APPS 3 #define SIZE_APP_NAME 12 diff --git a/apps/main_clock/main.hpp b/apps/main_clock/main.hpp index 7c07cb3..0919fd7 100644 --- a/apps/main_clock/main.hpp +++ b/apps/main_clock/main.hpp @@ -3,7 +3,7 @@ #include "pico/util/datetime.h" // Includes also buttons, API and ss_oled -#include "../../base_app.hpp" +#include "../../app/base_app.hpp" class app_main_clock : public BaseApp { private: diff --git a/apps/settings/main.hpp b/apps/settings/main.hpp index d53b8a9..c425033 100644 --- a/apps/settings/main.hpp +++ b/apps/settings/main.hpp @@ -1,5 +1,5 @@ #pragma once -#include "../../base_app.hpp" +#include "../../app/base_app.hpp" #include "strings.hpp" class app_settings : public BaseApp { diff --git a/base_app.hpp b/base_app.hpp deleted file mode 100644 index 3615df3..0000000 --- a/base_app.hpp +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once -#include "api.hpp" - -// Base app -class BaseApp { - public: - struct AppAttributes { - uint id = 0; - bool destroy_on_exit = true; - uint render_interval = 500; // Interval in ms at which the render method is called. - }; - enum AppReturnValues { - OK = 0, - CLOSE, // Close app from foreground, not respecting AppAttribues::destroy_on_exit. - QUIT // Completely quit app; stop running in background - }; - - // CHECK: Following have to be overwritten by derived classes - virtual const AppAttributes& app_get_attributes() = 0; - // Could be implemented as: - // {return app_attributes;} - // where app_attribues is an instance of AppAttributes - - virtual AppReturnValues render(Api *app_api) = 0; // Has to be implemented - virtual AppReturnValues btnpressed(Api *app_api, uint gpio, unsigned long delta) {return AppReturnValues::OK;}; - virtual AppReturnValues bgrefresh(Api *app_api, bool in_foreground) {return AppReturnValues::OK;}; -}; diff --git a/buttons.cpp b/buttons.cpp index a4eacab..1b92aa6 100644 --- a/buttons.cpp +++ b/buttons.cpp @@ -3,8 +3,8 @@ #include "buttons.hpp" #include "globals.hpp" -#include "api.hpp" -#include "app_manager.hpp" +#include "hal/api.hpp" +#include "app/app_manager.hpp" // From pico-watch.c: extern Api app_api; diff --git a/globals.hpp b/globals.hpp index 7c6b866..fd24542 100644 --- a/globals.hpp +++ b/globals.hpp @@ -1,5 +1,5 @@ #pragma once -#include "base_app.hpp" +#include "app/base_app.hpp" struct global_status { BaseApp* foreground_app = 0; diff --git a/hal/api.cpp b/hal/api.cpp new file mode 100644 index 0000000..92b3cd1 --- /dev/null +++ b/hal/api.cpp @@ -0,0 +1,329 @@ +#include +#include "pico/stdlib.h" +extern "C" { +#include "hardware/rtc.h" +} + +#include "api.hpp" +#include "../init.hpp" +#include "../globals.hpp" + +void Api::init() { + if (!m_init_done) { + init_display(); + m_init_done = 1; + } +} + +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, g_user.oled_contrast); + oledSetBackBuffer(&m_oled, m_ucBuffer); // Seems to be required to draw lines, rectangles… + //oledSetTextWrap(&oled, true); +} + +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; +} + +void Api::display_fill(unsigned char ucData, int bRender) { + oledFill(&m_oled, ucData, bRender); +} + +void Api::display_draw_line(int x1, int y1, int x2, int y2, int bRender) { + oledDrawLine(&m_oled, x1, y1, x2, y2, 0); + m_writebb_needed = true; +} + +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 +} + +void Api::display_draw_ellipse(int iCenterX, int iCenterY, int32_t iRadiusX, int32_t iRadiusY, uint8_t ucColor, uint8_t bFilled) { + oledEllipse(&m_oled, iCenterX, iCenterY, iRadiusX, iRadiusY, ucColor, bFilled); + m_writebb_needed = true; +} + +void Api::display_write_buffer(uint8_t *pBuffer) { + oledDumpBuffer(&m_oled, pBuffer); +} + +void Api::display_write_backbuffer() { + if (m_writebb_needed) + oledDumpBuffer(&m_oled, m_ucBuffer); + m_writebb_needed = false; +} + +int Api::display_write_pixel(int x, int y, unsigned char ucColor, int bRender) { + return oledSetPixel(&m_oled, x, y, ucColor, bRender); +} + +void Api::gui_popup_generic(std::string &title, std::string &body, int max_title_length, int max_body_length) { + oledRectangle(&m_oled, 9,7, 119,63, 0, 1); // Background + oledRectangle(&m_oled, 9,7, 119,63, 1, 0); // Popup border + oledRectangle(&m_oled, 9,7, 119,16, 1, 1); // Title background, FIXME pixel bleeding + m_writebb_needed = true; this->display_write_backbuffer(); // Display rectangle and anything else behind it (drawn before), could be moved after writing strings (but not done for debugging) + + // Truncate longer strings to avoid wasting time in for loop and drawing on OLED + if (max_title_length > 13) max_title_length = 13; + if (max_body_length > 78) max_body_length = 78; + if (title.size() > max_title_length) + title.resize(13); + if (body.size() > max_body_length) + body.resize(78); + + // Make body fit by adding '\n' at a regular interval + #define CHARS_PER_LINE 13 + int since_nl = 0; // Characters since newline + for (std::string::size_type i = 0; i < body.size(); ++i) { + if (body[i] == '\n') + since_nl = 0; + else if (since_nl++ == CHARS_PER_LINE) { + body.insert(i, 1, '\n'); + since_nl = 0; // Probably unneeded + } + } + // See https://stackoverflow.com/questions/1986966/does-s0-point-to-contiguous-characters-in-a-stdstring + oledWriteString(&m_oled, 0, 15,1, title.c_str(), FONT_8x8, 1, 1); // Draw title + oledWriteString(&m_oled, 0, 13,2, body.c_str(), FONT_8x8, 0, 1); // Draw body +} + +bool Api::gui_popup_text(std::string title, std::string body){ + m_button_last_pressed = BUTTON_NONE; + m_interpret_button_press = false; + + gui_popup_generic(title, body); + + while (m_button_last_pressed != BUTTON_SELECT) + sleep_ms(50); // TODO: use _wfi() + // Give back control to running app + oledFill(&m_oled, 0, 1); + m_interpret_button_press = true; + return true; +} + +bool Api::gui_popup_booleanchoice(std::string title, std::string body){ + m_button_last_pressed = BUTTON_NONE; + m_interpret_button_press = false; + + title.insert(0, "Choice|"); // TODO: Could be made nicer with a custom char that uses the whole height, this would give a visible separation, with two "text blocks" composing the title + gui_popup_generic(title, body); + + while (m_button_last_pressed != BUTTON_SELECT and m_button_last_pressed != BUTTON_MODE) + sleep_ms(50); // TODO: use _wfi() + + bool choice; + switch (m_button_last_pressed) { + case BUTTON_SELECT: + choice = true; + break; + case BUTTON_MODE: + choice = false; + break; + default: + __breakpoint(); // Impossible to attain (but you never know…) + } + // Give back control to running app + oledFill(&m_oled, 0, 1); + m_interpret_button_press = true; + return choice; +} + +void Api::gui_popup_intchoice_footer(int current_num, int min_num, int max_num) { + char buf[30]; + snprintf(&buf[0], sizeof(buf), + "Select: %d (%d/%d)", + current_num, + min_num, + max_num); + oledRectangle(&m_oled, 9,55, 119,63, 1, 1); // Footer background, FIXME pixel bleeding + oledWriteString(&m_oled, 0,10,7, buf, FONT_6x8, 1, 0); + m_writebb_needed = true; display_write_backbuffer(); +} + +int Api::gui_popup_intchoice(std::string title, std::string body, int min_num, int max_num, int default_num, int step){ + m_button_last_pressed = BUTTON_NONE; + m_interpret_button_press = false; + + int current_num = default_num; + + title.insert(0, "Number|"); // TODO: Could be made nicer with a custom char instead of pipe char + gui_popup_generic(title, body, 13, 39); // 39: 3 lines of body text, to leave space for number + gui_popup_intchoice_footer(current_num, min_num, max_num); + + do { + m_button_last_pressed = BUTTON_NONE; + sleep_ms(50); // TODO: use _wfi() + switch (m_button_last_pressed) { + case BUTTON_UP: + current_num += step; + if (current_num > max_num) + current_num = max_num; + break; + case BUTTON_DOWN: + current_num -= step; + if (current_num < min_num) + current_num = min_num; + break; + case BUTTON_MODE: + current_num = default_num; + break; + } + if (m_button_last_pressed) + gui_popup_intchoice_footer(current_num, min_num, max_num); + } while (m_button_last_pressed != BUTTON_SELECT); + + // Give back control to running app + oledFill(&m_oled, 0, 1); + m_interpret_button_press = true; + return current_num; +} + +void Api::gui_popup_strchoice_footer(const char selection[]) { + std::string buf{selection}; + buf.insert(0, "Select: "); + + if (buf.size() > 36) // 2 lines of 18 chars + buf.resize(36); + + // Choose most adapted font size + int font; + int chars_per_line; + if (buf.size() > 26) { + font = FONT_6x8; + chars_per_line = 18; + } else { + font = FONT_8x8; + chars_per_line = 13; + } + + // Make selection text fit by adding '\n' at a regular interval + // TODO: Make this a private function + int since_nl = 0; // Characters since newline + for (std::string::size_type i = 0; i < buf.size(); ++i) { + if (buf[i] == '\n') + since_nl = 0; + else if (since_nl++ == chars_per_line) { + buf.insert(i, 1, '\n'); + since_nl = 0; // Probably unneeded + } + } + + oledRectangle(&m_oled, 9,47, 119,63, 1, 1); // Footer background, FIXME pixel bleeding + oledWriteString(&m_oled, 0,10,6, buf.c_str(), font, 1, 0); + m_writebb_needed = true; display_write_backbuffer(); +} + +int Api::gui_popup_strchoice(std::string title, std::string body, const char *choices[27], int choices_size, int min_index, int max_index, int default_index){ + m_button_last_pressed = BUTTON_NONE; + m_interpret_button_press = false; + if (max_index == -1) + max_index = choices_size-1; + + int current_index = default_index; + + title.insert(0, "Choose|"); // TODO: Could be made nicer with a custom char instead of pipe char + gui_popup_generic(title, body, 13, 39); // 39: 3 lines of body text, to leave space for selection string + gui_popup_strchoice_footer(choices[current_index]); + + do { + m_button_last_pressed = BUTTON_NONE; + sleep_ms(50); // TODO: use _wfi() + switch (m_button_last_pressed) { + case BUTTON_UP: + current_index += 1; + if (current_index > max_index) + current_index = max_index; + break; + case BUTTON_DOWN: + current_index -= 1; + if (current_index < min_index) + current_index = min_index; + break; + case BUTTON_MODE: + current_index = default_index; + break; + } + if (m_button_last_pressed) + gui_popup_strchoice_footer(choices[current_index]); + } while (m_button_last_pressed != BUTTON_SELECT); + + // Give back control to running app + oledFill(&m_oled, 0, 1); + m_interpret_button_press = true; + return current_index; +} + +bool Api::gui_footer_text(std::string text, int offset_x, int offset_row, bool invert, bool no_bg) { + // Max chars per line for FONT_8x8 is 16 chars + // Max chars per line for FONT_6x8 is 21 chars + // Truncate longer text + if (text.size() > 21) + text.resize(21); + + // Choose most adapted font size + int font; + if (text.size() > 16) + font = FONT_6x8; + else + font = FONT_8x8; + + if (!no_bg) { + oledRectangle(&m_oled, 0,56-offset_row*8, 127,64-offset_row*8, invert, 1); + m_writebb_needed = true; + } + oledWriteString(&m_oled, 0,offset_x,7-offset_row, text.c_str(), font, invert, 0); +} + +bool Api::gui_header_text(std::string text, int offset_x, int offset_row, bool invert, bool no_bg) { + // Max chars per line for FONT_8x8 is 16 chars + // Max chars per line for FONT_6x8 is 21 chars + // Truncate longer text + if (text.size() > 21) + text.resize(21); + + // Choose most adapted font size + int font; + if (text.size() > 16) + font = FONT_6x8; + else + font = FONT_8x8; + + if (!no_bg) { + oledRectangle(&m_oled, 0,0+offset_row*8, 127,8+offset_row*8, invert, 1); + m_writebb_needed = true; + } + oledWriteString(&m_oled, 0,offset_x,0+offset_row, text.c_str(), font, invert, 0); +} + +bool Api::performance_set(int perf) { + return false; +} + +bool Api::datetime_get(datetime_t *t) { + return rtc_get_datetime(t); +} + +bool Api::datetime_set(datetime_t *t) { + return rtc_set_datetime(t); +} + +uint Api::button_last_get() { + return m_button_last_pressed; +} + +void Api::button_last_set(uint gpio) { + m_button_last_pressed = gpio; +} diff --git a/hal/api.hpp b/hal/api.hpp new file mode 100644 index 0000000..5d0c33e --- /dev/null +++ b/hal/api.hpp @@ -0,0 +1,116 @@ +#pragma once + +#include +#include "pico/util/datetime.h" +#include "../oled/ss_oled.h" + +#include "../buttons.hpp" + +class Api { + private: + SSOLED m_oled; + u_char m_init_done = 0; + bool m_writebb_needed = false; // Do we need to write the internal backbuffer? + uint8_t m_ucBuffer[1024] = {0}; + uint m_button_last_pressed = 0; + int m_app_render_interval = 500; + void init_display(); + // Careful of large heap usage with long strings! Heap seems to be fragmented, so shorter strings work best. More details on findings below: + // When the string cannot be created, this comes from the fact that we, on some runs, allocation can be very close to the SRAM's boundry: 0x20041f88 with boundry at 0x20042000. The heap is nearly full. Even just appending to a string moves the allocation lower. I think that the best course of action would be to have more static variables and pull in less things to save SRAM. + // When char* directly passed as parameter: The memory is possibly fragmented as long strings (>215) push the allocation downwards, into lower (higher address) in the heap. title is at 0x20041f90 and body at 0x200045e8, for length of 215 chars. + void gui_popup_generic(std::string &title, std::string &body, int max_title_length = 13, int max_body_length = 78); + void gui_popup_intchoice_footer(int current_num, int min_num, int max_num); + void gui_popup_strchoice_footer(const char selection[]); + public: + // Allow button press to be registed by app and for app_switch (when HOME). Set to false when displaying Api's internal GUI. + bool m_interpret_button_press = true; + enum perf_modes { + LOW_POWER, + NORMAL_PERF, + HIGH_PERF, + ENTER_SHALLOW_SLEEP, + EXIT_SHALLOW_SLEEP // Restore perf setting + }; + 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); + void display_draw_rectange(int x1, int y1, int x2, int y2, uint8_t ucColor, uint8_t bFilled); + void display_draw_ellipse(int iCenterX, int iCenterY, int32_t iRadiusX, int32_t iRadiusY, uint8_t ucColor, uint8_t bFilled); + void display_write_buffer(uint8_t *pBuffer); + // Write the internal backbuffer to the display. Should be called when after all drawing calls. One call is done to avoid flickering of the display. + void display_write_backbuffer(); + int display_write_pixel(int x, int y, unsigned char ucColor, int bRender); + // Display a popup over the current view and wait for select button to be pressed. + // This is a blocking function and should be used only in the app's render method. + // \param title Popup's title, length should not exceed 13 characters. + // \param body String containing the popup's body. The zone has a size of 13×6 characters, so body should not be longer than 78 characters. Newline allows going to the next line and the text is automatically wrapped. + // \note Strings longer than 13 and 78 respectively will be truncated. + bool gui_popup_text(std::string title, std::string body); + // Display a popup over the current view and wait for select or mode (cancel) button to be pressed. The choice done (yes/no) by the user is returned as a bool. + // This is a blocking function and should be used only in the app's render method. + // \param title Popup's title. The title is prefixed with "Choice|", so the `title` argument cannot exceed 6 characters. + // \param body String containing the popup's body. The zone has a size of 13×6 characters, so body should not be longer than 78 characters. Newline allows going to the next line and the text is automatically wrapped. + // \note Strings longer than 13 and 78 respectively will be truncated. + bool gui_popup_booleanchoice(std::string title, std::string body); + // Display a popup over the current view and wait for user to choose (with left and right) a number between min_num and max_num. The default choice is default_num and the user can reset back to it with mode/cancel button. After confirming with select, the choice is returned. + // This is a blocking function and should be used only in the app's render method. + // \param title Popup's title. The title is prefixed with "Number|", so the `title` argument cannot exceed 6 characters. + // \param body String containing the popup's body. The zone has a size of 13×3 characters, so body should not be longer than 39 characters. Newline allows going to the next line and the text is automatically wrapped. Under the body is displayed the current choosen number with the min and max in parenthesis. + // \param min_num The smallest number that can be choosen. + // \param max_num Biggest number that can be choosen. + // \param default_num This should be between min_num and max_num, else user may be able to return a number out of range + // \param step Value to increment/decrement from when user changes number. This cannot result in an out-of-bounds as the number is clipped to the min/max when this happens. This maybe undesirable behaviour. + // \note Strings longer than 13 and 39 respectively will be truncated. + int gui_popup_intchoice(std::string title, std::string body, int min_num = 0, int max_num = 10, int default_num = 5, int step = 1); + // Display a popup over the current view and wait for user to choose (with left and right) a string (char array). The default choice is default_index and the user can reset back to it with mode/cancel button. After confirming with select, the choice's index is returned. + // This is a blocking function and should be used only in the app's render method. + // \param title Popup's title. The title is prefixed with "Choice|", so the `title` argument cannot exceed 6 characters. + // \param body String containing the popup's body. The zone has a size of 13×3 characters, so body should not be longer than 39 characters. Newline allows going to the next line and the text is automatically wrapped. Under the body is displayed the current choosen number with the min and max in parenthesis. + // \param choices List of const char arrays to choose from, each string cannot be longer than 27 chars because of "Select: " text. Font size between 6×8 and 8×8 is choosen automatically. Set the second dimension size of your array of choices to max 27. For example, define: `const char *choices[27] = {…}` than call this function by passing directly `choices`. + // \param choices_size Size of choices array, used for avoiding selection of element that are out of the array's bounds. This is only used for determining max_index when max_index is unset. **Following parameters can be left at their default value:** + // \param min_index Smallest element index that can be choosen. This allows reusing the same choice list for muliple prompts and allows programmatically changing the choices available to the user. + // \param max_index Biggest element index that can be choosen. This defaults to the size of the list (set by `choices_size`) if left at -1. + // \param default_index Default string displayed. + int gui_popup_strchoice(std::string title, std::string body, const char *choices[27], int choices_size, int min_index = 0, int max_index = -1, int default_index = 0); + // Display text at the bottom of the screen. + // The font size is automatically choosen based on the text lenght. + // \param text Text to display. Text longer than 21 will be truncated. + // \param offset_x Set a horizental offset, to allow, for example, centering the text + // \param offset_row Allow rendering the text higher. For example, one line higher when `offset_row = 1`. + // \param invert Invert text and background color. + // \param no_bg Do not draw background when true. + bool gui_footer_text(std::string text, int offset_x = 0, int offset_row = 0, bool invert = false, bool no_bg = false); + // Display text at the top of the screen. + // The font size is automatically choosen based on the text lenght. + // \param text Text to display. Text longer than 21 will be truncated. + // \param offset_x Set a horizental offset, to allow, for example, centering the text + // \param offset_row Render text lines lower. For example, one text line lower with `offset_row = 1`. + // \param invert Invert text and background color. + // \param no_bg Do not draw background when true. + bool gui_header_text(std::string text, int offset_x = 0, int offset_row = 0, bool invert = false, bool no_bg = false); + // Set performance mode. + // FIXME: function currently does nothing! + // An app should choose the lowest performance that can make it function. Set in init(). Only when required, higher performance should be used. + // \param perf See Api::perf_modes enum for possible values + bool performance_set(int perf_mode); + // Get the current datetime + // \param t Pointer to the datetime structure in which the datetime wil be stored + // \return true if the call to the SDK was successful, else false. + bool datetime_get(datetime_t *t); + // Set the current datetime + // TODO: Not every app should be allowed to set the datetime. Only app_id<2 (home_screen and settings) should be allowed: return false when setting is blocked. + // \param t Pointer to the datetime structure + // \return true if the call to the SDK was successful, else false. + bool datetime_set(datetime_t *t); + // Get last button pressed, see buttons.hpp for values + // \return Last button pressed + uint button_last_get(); + // Set last button pressed, should only be called by button gpio interrupt. + void button_last_set(uint gpio); +}; diff --git a/pico-watch.cpp b/pico-watch.cpp index b1d0c13..54e78f4 100644 --- a/pico-watch.cpp +++ b/pico-watch.cpp @@ -6,10 +6,10 @@ #include "pico/util/datetime.h" #include "init.hpp" -#include "api.hpp" +#include "hal/api.hpp" #include "buttons.hpp" #include "globals.hpp" -#include "app_manager.hpp" +#include "app/app_manager.hpp" global_status g_s; user_settings g_user; -- cgit v1.2.3-54-g00ecf