|
|
|
/**
|
|
|
|
* TODO file description
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include <stdbool.h>
|
|
|
|
#include <string.h>
|
|
|
|
#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"
|
|
|
|
#include "app_safety.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);
|
|
|
|
}
|
|
|
|
|
|
|
|
app_safety_pass_display_updating();
|
|
|
|
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 - 1) {
|
|
|
|
menu->text_slide += 1;
|
|
|
|
if (textlen - menu->text_slide >= FBW - 1) {
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
}
|