/** * TODO file description */ #include #include #include "app_gui.h" #include "app_heater.h" #include "app_buzzer.h" #include "app_temp.h" #include "FreeRTOS.h" #include "task.h" #include "queue.h" #include "ufb/framebuffer.h" #include "ufb/fb_text.h" #include "ufb/fb_7seg.h" #define MAX_TEMP 400 /** * Screen callback type. The event is either INIT, PAINT, or one of the input events. */ typedef void (*screen_t)(GuiEvent event); /** * Choice callback for the generic menu screen */ typedef void (*menu_callback_t)(int choice); /** * Generic menu screen (must be called from a screen function with the standard signature) * * @param event - currently handled event * @param options - options for the menu (items to show) * @param cb - choice callback */ static void screen_menu(GuiEvent event, const char **options, menu_callback_t cb); static struct State { float oven_temp; //float soc_temp; // manual mode controls int set_temp; bool heater_enabled; bool down_prescaller; bool up_prescaller; bool pushed; bool paint_needed; uint32_t last_tick_event; uint32_t push_tick; uint32_t last_read_temp_tick; // true if the button is still held since init (i.e. the push action should not work as "enter") bool initial_pushed; screen_t screen; union { struct menu_state { int pos; int len; uint32_t change_time; uint32_t slide_end_time; uint16_t text_slide; uint8_t tick_counter; } menu; } page; } s_app = {}; /** Get push time (while held) */ static uint32_t push_time() { return s_app.pushed ? (xTaskGetTickCount() - s_app.push_tick) : 0; } /** Schedule paint (the screen func will be called with the PAINT event argument */ static void request_paint() { s_app.paint_needed = true; } /** Home screen */ static void screen_home(GuiEvent event); /** Manual temperature setting screen */ static void screen_manual(GuiEvent event); /** Menu in the manual temperature setting screen */ static void screen_manual_menu(GuiEvent event); /** Draw the common overlay / HUD (with temperatures and heater status) */ static void draw_common_overlay(); /** Input beep (push or knob turn) */ static void input_sound_effect(); /** Switch to a different screen. Handles initial push state handling (so release * does not cause a "click" event). * * @param pScreen - screen to switch to * @param init - call the INIT event immediately after */ static void switch_screen(screen_t pScreen, bool init); static char tmp[100]; /** Main loop */ void app_task_gui(void *argument) { // Wait until inited ulTaskNotifyTake(pdTRUE, portMAX_DELAY); PUTS("GUI task starts\r\n"); s_app.last_tick_event = xTaskGetTickCount(); switch_screen(screen_home, true); while (1) { uint32_t message = GUI_EVENT_NONE; int32_t ticks_remain = (int32_t) pdMS_TO_TICKS(10) - (int32_t)(xTaskGetTickCount() - s_app.last_tick_event); if (ticks_remain < 0) { ticks_remain = 0; } osMessageQueueGet(guiEventQueHandle, &message, NULL, ticks_remain); if (message == GUI_EVENT_KNOB_PLUS) { if (s_app.up_prescaller) { s_app.up_prescaller = 0; // let this through } else { // consume this s_app.down_prescaller = 0; s_app.up_prescaller = 1; message = GUI_EVENT_NONE; } } else if (message == GUI_EVENT_KNOB_MINUS) { if (s_app.down_prescaller) { s_app.down_prescaller = 0; // let this through } else { // consume this s_app.up_prescaller = 0; s_app.down_prescaller = 1; message = GUI_EVENT_NONE; } } uint32_t tickNow = xTaskGetTickCount(); // 10ms tick event if (tickNow - s_app.last_tick_event > pdMS_TO_TICKS(10)) { s_app.screen(GUI_EVENT_SCREEN_TICK); s_app.last_tick_event = tickNow; } if (tickNow - s_app.last_read_temp_tick > pdMS_TO_TICKS(250)) { s_app.oven_temp = app_temp_read_oven(); //s_app.soc_temp = app_temp_read_soc(); request_paint(); s_app.last_read_temp_tick = tickNow; } switch (message) { case GUI_EVENT_KNOB_PRESS: s_app.pushed = true; s_app.push_tick = tickNow; break; case GUI_EVENT_KNOB_RELEASE: s_app.pushed = false; if (s_app.initial_pushed) { s_app.initial_pushed = false; message = GUI_EVENT_NONE; } break; } if (message != GUI_EVENT_NONE) { s_app.screen(message); } if (s_app.paint_needed) { s_app.paint_needed = false; fb_clear(); draw_common_overlay(); s_app.screen(GUI_EVENT_PAINT); fb_blit(); } } } /** Switch to a different screen handler. * If "init" is true, immediately call it with the init event. */ static void switch_screen(screen_t pScreen, bool init) { s_app.initial_pushed = s_app.pushed; s_app.screen = pScreen; // clear the union field memset(&s_app.page, 0, sizeof(s_app.page)); request_paint(); if (init) { pScreen(GUI_EVENT_SCREEN_INIT); } } /** Draw GUI common to all screens */ static void draw_common_overlay() { SPRINTF(tmp, "%3.1f°C →%3d°C", s_app.oven_temp, s_app.set_temp); fb_text(3, 3, tmp, FONT_3X5, 1); if (s_app.heater_enabled) { fb_frame(0, 0, FBW, 11, 2, 1); } } /** Play input sound effect if this is an input event */ static void input_sound_effect() { app_buzzer_beep(); } // ------------- home screen ---------------- static const char* main_menu_opts[] = { "Manual mode", "Calibration", NULL }; static void main_menu_cb(int opt) { switch (opt) { case 0: switch_screen(screen_manual, true); break; case 1: // TODO break; } } static void screen_home(GuiEvent event) { if (event == GUI_EVENT_SCREEN_INIT) { app_heater_enable(false); } screen_menu(event, main_menu_opts, main_menu_cb); } // --------- manual mode screen --------------- static void screen_manual(GuiEvent event) { bool temp_changed = false; if (event == GUI_EVENT_SCREEN_INIT) { return; } // menu is activated by long push if (push_time() >= pdMS_TO_TICKS(500)) { switch_screen(screen_manual_menu, true); return; } switch (event) { case GUI_EVENT_PAINT: fb_7seg_number(4, 40, 12, 20, 2, 2, 1, s_app.set_temp, 3, 0 ); break; case GUI_EVENT_KNOB_RELEASE: input_sound_effect(); s_app.heater_enabled ^= 1; app_heater_enable(s_app.heater_enabled); request_paint(); break; case GUI_EVENT_KNOB_PLUS: if (s_app.set_temp <= MAX_TEMP - 5) { s_app.set_temp += 5; temp_changed = true; } break; case GUI_EVENT_KNOB_MINUS: if (s_app.set_temp >= 5) { s_app.set_temp -= 5; temp_changed = true; } break; } if (temp_changed) { input_sound_effect(); app_heater_set_target((float) s_app.set_temp); request_paint(); } } // --------------------------- static const char* manual_menu_opts[] = { "Close menu", "Exit manual", NULL }; static void manual_menu_cb(int opt) { switch (opt) { case 0: // Close menu switch_screen(screen_manual, false); break; case 1: s_app.heater_enabled = false; app_heater_enable(false); switch_screen(screen_home, true); break; } } static void screen_manual_menu(GuiEvent event) { screen_menu(event, manual_menu_opts, manual_menu_cb); } // ------------------------ static void screen_menu(GuiEvent event, const char **options, menu_callback_t cb) { bool menu_changed = false; const uint32_t tickNow = xTaskGetTickCount(); struct menu_state *menu = &s_app.page.menu; switch (event) { case GUI_EVENT_SCREEN_INIT: menu->pos = 0; menu->len = 0; menu->change_time = tickNow; menu->text_slide = 0; const char **opt = options; while (*opt) { menu->len++; opt++; } break; case GUI_EVENT_SCREEN_TICK: // long text sliding animation if (tickNow - menu->change_time >= pdMS_TO_TICKS(500)) { const uint32_t textlen = strlen(options[menu->pos]) * 6; if (textlen >= FBW - 2) { if (textlen - menu->text_slide > FBW - 2) { menu->text_slide += 1; if (textlen - menu->text_slide > FBW - 2) { menu->slide_end_time = tickNow; } } else if (tickNow - menu->slide_end_time >= pdMS_TO_TICKS(500)) { menu->change_time = tickNow; menu->slide_end_time = 0; menu->text_slide = 0; } request_paint(); } } break; case GUI_EVENT_PAINT: for (int i = 0; i < menu->len; i++) { // is the row currently rendered the selected row? const bool is_selected = menu->pos == i; const fbcolor_t color = !is_selected; // text color - black if selected, because it's inverted const fbpos_t y = 27 + i * 10; fb_rect(0, y, FBW, 10, !color); fb_text(1 - (is_selected ? menu->text_slide : 0), y + 1, options[i], FONT_5X7, color); // ensure the text doesn't touch the edge (looks ugly) fb_vline(FBW - 1, y, 10, !color); fb_vline(0, y, 10, !color); } break; // the button is held! release is what activates the button case GUI_EVENT_KNOB_RELEASE: input_sound_effect(); cb(menu->pos); break; case GUI_EVENT_KNOB_PLUS: if (menu->pos < menu->len - 1) { menu->pos++; menu_changed = true; } break; case GUI_EVENT_KNOB_MINUS: if (menu->pos > 0) { menu->pos--; menu_changed = true; } break; } if (menu_changed) { menu->change_time = tickNow; menu->text_slide = 0; menu->slide_end_time = 0; input_sound_effect(); request_paint(); } }