reflow-oven-control-sw/stm-firmware/ui/gui.c

724 lines
19 KiB
C
Raw Normal View History

/* Reflow Oven Controller
*
* Copyright (C) 2020 Mario Hüttel <mario.huettel@gmx.net>
*
* This file is part of the Reflow Oven Controller Project.
*
* The reflow oven controller is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* The Reflow Oven Control Firmware is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with the reflow oven controller project.
* If not, see <http://www.gnu.org/licenses/>.
*/
#include <reflow-controller/ui/gui.h>
2020-12-07 00:19:42 +01:00
#include <reflow-controller/ui/gui-config.h>
#include <reflow-controller/ui/menu.h>
#include <reflow-controller/ui/lcd.h>
#include <reflow-controller/rotary-encoder.h>
#include <reflow-controller/systick.h>
#include <reflow-controller/adc-meas.h>
2020-07-09 22:31:42 +02:00
#include <reflow-controller/safety/safety-controller.h>
#include <reflow-controller/settings/settings.h>
#include <reflow-controller/temp-converter.h>
#include <helper-macros/helper-macros.h>
#include <stm-periph/unique-id.h>
#include <stddef.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <inttypes.h>
#include <reflow-controller/oven-driver.h>
#include <fatfs/ff.h>
#include <reflow-controller/temp-profile-executer.h>
static char IN_SECTION(.ccm.bss) display_buffer[4][21] = {0};
static struct lcd_menu IN_SECTION(.ccm.bss) reflow_menu;
2020-08-22 13:30:59 +02:00
#define reflow_menu_ptr (&reflow_menu)
static void update_display_buffer(uint8_t row, const char *data)
{
int i;
if (row > 4)
return;
if (!data)
return;
for (i = 0; data[i] && i < LCD_CHAR_WIDTH; i++) {
display_buffer[row][i] = data[i];
}
display_buffer[row][i] = 0;
}
static void gui_menu_monitor(struct lcd_menu *menu, enum menu_entry_func_entry entry_type, void *parent)
{
static void *my_parent;
static uint64_t my_timestamp = 0;
char line[17];
float tmp;
int res;
const char *prefix;
if (entry_type == MENU_ENTRY_FIRST_ENTER) {
my_parent = parent;
menu_display_clear(menu);
}
2020-12-07 00:19:42 +01:00
if (systick_ticks_have_passed(my_timestamp, GUI_MONITORING_INTERVAL_MS)) {
my_timestamp = systick_get_global_tick();
adc_pt1000_get_current_resistance(&tmp);
2020-06-16 20:05:32 +02:00
snprintf(line, sizeof(line), "Res: %.1f " LCD_OHM_SYMBOL_STRING, tmp);
menu->update_display(0, line);
res = temp_converter_convert_resistance_to_temp(tmp, &tmp);
switch (res) {
case -1:
prefix = "<";
break;
case 1:
prefix = ">";
break;
default:
prefix = "";
break;
}
snprintf(line, sizeof(line), "Temp: %s%.1f " LCD_DEGREE_SYMBOL_STRING "C", prefix, tmp);
menu->update_display(1, line);
2020-07-09 22:31:42 +02:00
(void)safety_controller_get_analog_mon_value(ERR_AMON_UC_TEMP, &tmp);
snprintf(line, sizeof(line), "Tj: %.1f " LCD_DEGREE_SYMBOL_STRING "C", tmp);
menu->update_display(2, line);
2020-07-09 22:31:42 +02:00
(void)safety_controller_get_analog_mon_value(ERR_AMON_VREF, &tmp);
snprintf(line, sizeof(line), "Vref: %.1f mV", tmp);
menu->update_display(3, line);
}
if (menu->inputs.push_button == BUTTON_SHORT_RELEASED || menu->inputs.push_button == BUTTON_LONG) {
menu_entry_dropback(menu, my_parent);
}
}
static void gui_menu_about(struct lcd_menu *menu, enum menu_entry_func_entry entry_type, void *parent)
{
static void *my_parent;
static int page = 0;
static int last_page = -1;
static uint32_t uptime_secs;
uint32_t new_uptime_secs;
uint32_t uptime_mins;
uint32_t uptime_hours;
uint32_t uptime_days;
int16_t rot_delta;
uint32_t ser1, ser2, ser3;
enum button_state push_button;
bool button_ready;
if (entry_type == MENU_ENTRY_FIRST_ENTER) {
uptime_secs = 0ULL;
page = 0;
last_page = -1;
my_parent = parent;
menu_display_clear(menu);
menu_ack_rotary_delta(menu);
}
rot_delta = menu_get_rotary_delta(menu);
if (rot_delta >= 4) {
menu_ack_rotary_delta(menu);
if (page < 4) {
page++;
menu_display_clear(menu);
}
} else if (rot_delta <= -4) {
menu_ack_rotary_delta(menu);
if (page > 0) {
page--;
menu_display_clear(menu);
}
}
switch (page) {
case 0:
if (last_page == 0)
break;
last_page = 0;
menu_lcd_output(menu, 0, LCD_SHIMATTA_STRING " Shimatta");
menu_lcd_output(menu, 1, "Oven Controller");
menu_lcd_output(menu, 2, "(c) Mario H\xF5ttel");
menu_lcd_output(menu, 3, "Page 1/5");
break;
case 1:
if (last_page == 1)
break;
last_page = 1;
menu_lcd_output(menu, 0, "Version Number:");
menu_lcd_outputf(menu, 1, "%.*s", LCD_CHAR_WIDTH, xstr(GIT_VER));
if (strlen(xstr(GIT_VER)) > LCD_CHAR_WIDTH) {
menu_lcd_outputf(menu, 2, "%s", &xstr(GIT_VER)[LCD_CHAR_WIDTH]);
}
#ifdef DEBUGBUILD
menu_lcd_output(menu, 3, "Page 2/5 [DEBUG]");
#else
menu_lcd_output(menu, 3, "Page 2/5");
#endif
break;
case 2:
if (last_page == 2)
break;
last_page = 2;
menu_lcd_output(menu, 0, "Compile Info");
menu_lcd_output(menu, 1, __DATE__);
menu_lcd_output(menu, 2, __TIME__);
menu_lcd_output(menu, 3, "Page 3/5");
break;
case 3:
if (last_page == 3)
break;
last_page = 3;
unique_id_get(&ser1, &ser2, &ser3);
menu_lcd_outputf(menu, 0, "Serial: %08X", ser1);
menu_lcd_outputf(menu, 1, " %08X", ser2);
menu_lcd_outputf(menu, 2, " %08X", ser3);
menu_lcd_output(menu, 3, "Page 4/5");
break;
case 4:
last_page = 4;
systick_get_uptime_from_tick(&uptime_days, &uptime_hours, &uptime_mins, &new_uptime_secs);
if (new_uptime_secs != uptime_secs) {
uptime_secs = new_uptime_secs;
menu_lcd_output(menu, 0, "Uptime:");
menu_lcd_outputf(menu, 1, "%lu day%s %02lu:%02lu:%02lu",
uptime_days, (uptime_days == 1 ? "" : "s"), uptime_hours, uptime_mins, uptime_secs);
menu_lcd_output(menu, 3, "Page 5/5");
}
break;
default:
page = 0;
last_page = -1;
break;
}
push_button = menu_get_button_state(menu);
button_ready = menu_get_button_ready_state(menu);
if (push_button == BUTTON_IDLE)
button_ready = true;
if (button_ready &&
(push_button == BUTTON_SHORT_RELEASED || push_button == BUTTON_LONG)) {
menu_entry_dropback(menu, my_parent);
}
}
static void gui_menu_err_flags(struct lcd_menu *menu, enum menu_entry_func_entry entry_type, void *parent)
{
static void *my_parent = NULL;
static uint8_t offset;
static uint64_t timestamp;
static bool end_of_list_reached = true;
bool state;
enum button_state push_button;
int16_t rot;
uint32_t i;
char name[64];
int32_t line_counter;
bool skip_err_flag_prefix;
bool try_ack = false;
enum safety_flag flag;
bool button_ready;
const char *err_flag_prefix = "ERR_FLAG_";
push_button = menu_get_button_state(menu);
rot = menu_get_rotary_delta(menu);
if (entry_type != MENU_ENTRY_CONTINUE) {
if (entry_type == MENU_ENTRY_FIRST_ENTER) {
my_parent = parent;
offset = 0;
end_of_list_reached = true;
}
} else {
if (push_button == BUTTON_IDLE && rot == 0) {
if (!systick_ticks_have_passed(timestamp, 150))
return;
}
}
button_ready = menu_get_button_ready_state(menu);
if (push_button == BUTTON_SHORT_RELEASED && button_ready) {
menu_entry_dropback(menu, my_parent);
} if (push_button == BUTTON_LONG && button_ready) {
try_ack = true;
}
if (rot >= 4 || rot <= -4) {
menu_ack_rotary_delta(menu);
if (rot > 0) {
if (!end_of_list_reached)
offset++;
} else {
if (offset > 0)
offset--;
}
}
menu_display_clear(menu);
line_counter = -offset;
for (i = 0; i < safety_controller_get_flag_count(); i++) {
(void)safety_controller_get_flag_by_index(i, &state, &flag);
if (try_ack)
safety_controller_ack_flag(flag);
if (state) {
if (line_counter >= 0 && line_counter < 4) {
safety_controller_get_flag_name_by_index(i, name, sizeof(name));
if (!strncmp(name, err_flag_prefix, 9)) {
skip_err_flag_prefix = true;
} else {
skip_err_flag_prefix = false;
}
menu_lcd_outputf(menu, line_counter, "%s",
(skip_err_flag_prefix ? &name[9] : name));
}
line_counter++;
}
}
end_of_list_reached = (line_counter > 4 ? false : true);
timestamp = systick_get_global_tick();
}
static void gui_menu_constant_temperature_driver(struct lcd_menu *menu, enum menu_entry_func_entry entry_type, void *parent)
{
static void IN_SECTION(.ccm.bss) *my_parent;
static int16_t IN_SECTION(.ccm.bss) temperature;
static bool IN_SECTION(.ccm.bss) fine;
2020-12-07 00:19:42 +01:00
static uint64_t IN_SECTION(.ccm.bss) last_temp_refresh;
static float IN_SECTION(.ccm.bss) last_temp;
enum button_state button;
2020-12-07 00:19:42 +01:00
float current_temp;
int status;
int16_t rot;
int16_t temp_old;
if (entry_type == MENU_ENTRY_FIRST_ENTER) {
my_parent = parent;
2020-12-07 00:19:42 +01:00
last_temp = -2000.0f;
temperature = 30;
menu_display_clear(menu);
menu_lcd_outputf(menu, 0, "Temp Controller");
temp_old = 0;
} else {
temp_old = temperature;
}
if (menu_get_button_ready_state(menu)) {
button = menu_get_button_state(menu);
rot = menu_get_rotary_delta(menu);
if (rot >= 4 || rot <= -4) {
menu_ack_rotary_delta(menu);
if (rot > 0)
temperature += (fine ? 1 : 10);
else
temperature -= (fine ? 1 : 10);
if (temperature > 300)
temperature = 300;
else if (temperature < 0)
temperature = 0;
}
switch (button) {
case BUTTON_SHORT_RELEASED:
fine = !fine;
break;
case BUTTON_LONG:
oven_pid_stop();
menu_entry_dropback(menu, my_parent);
break;
default:
break;
}
}
if (oven_pid_get_status() != OVEN_PID_RUNNING) {
menu_lcd_output(menu, 1, "PID stopped!");
menu_lcd_output(menu, 2, "Check Flags!");
} else {
if (temperature != temp_old) {
oven_pid_set_target_temperature((float)temperature);
2020-12-07 00:19:42 +01:00
menu_lcd_outputf(menu, 1, "Target: %d " LCD_DEGREE_SYMBOL_STRING "C", (int)temperature);
}
if (entry_type == MENU_ENTRY_FIRST_ENTER || systick_ticks_have_passed(last_temp_refresh, GUI_TEMP_DRIVER_REFRESH_MS)) {
(void)adc_pt1000_get_current_resistance(&current_temp);
status = temp_converter_convert_resistance_to_temp(current_temp, &current_temp);
if (current_temp != last_temp) {
last_temp = current_temp;
menu_lcd_outputf(menu, 2, "Current: %s%.1f", current_temp, (status < 0 ? "<" : status > 0 ? ">" : ""));
}
last_temp_refresh = systick_get_global_tick();
}
}
}
/**
* @brief A safe strncpy which ensures thtat the copied string ends in a null character
* @param dest Destiantion ptr
* @param src Source pointer
* @param max_size maximum size of the destination (including null terminator)
* @return 0 if successful, positive, if string was to short to hold all data, negative if parameter error
*/
static int safe_strncpy(char *dest, const char *src, size_t max_size) {
size_t idx;
bool terminated = false;
if (!dest || !src || !max_size)
return -1000;
for (idx = 0; idx < max_size; idx++) {
dest[idx] = src[idx];
if (src[idx] == '\0') {
terminated = true;
break;
}
}
if (!terminated) {
dest[max_size - 1] = 0;
return 1;
}
return 0;
}
/**
* @brief load_temperature_file_list_from_sdcard
* @return -1 File error, 0 successful
*/
static int load_temperature_file_list_from_sdcard(char (*list)[17], uint32_t len)
{
uint32_t i, j;
DIR directory;
FILINFO finfo;
FRESULT fres;
if (!list)
return -1001;
/* Zero out the list */
for (i = 0; i < len; i++) {
for (j = 0; j < sizeof(*list); j++) {
list[i][j] = 0;
}
}
/* find the frist file */
fres = f_findfirst(&directory, &finfo, "/", "*.tpr");
i = 0;
while (fres == FR_OK && finfo.fname[0]) {
safe_strncpy(&list[i][0], finfo.fname, sizeof(*list));
fres = f_findnext(&directory, &finfo);
i++;
if (i >= len)
break;
}
if (fres != FR_OK) {
return -1;
}
return (int)i;
}
static void gui_menu_temp_profile_execute(struct lcd_menu *menu, enum menu_entry_func_entry entry_type, void* parent)
{
static void *my_parent;
const struct tpe_current_state *state;
static uint64_t last_tick;
float temperature;
float resistance;
int res;
if (entry_type == MENU_ENTRY_FIRST_ENTER) {
my_parent = parent;
menu_display_clear(menu);
last_tick = 0ULL;
}
if (systick_ticks_have_passed(last_tick, 250)) {
state = temp_profile_executer_status();
if (state->status == TPE_RUNNING) {
menu_lcd_outputf(menu, 0, "Executing...");
menu_lcd_outputf(menu, 1, "Step %u/%u", state->step, state->profile_steps);
(void)adc_pt1000_get_current_resistance(&resistance);
res = temp_converter_convert_resistance_to_temp(resistance, &temperature);
menu_lcd_outputf(menu, 2, "Temp: %s%.1f " LCD_DEGREE_SYMBOL_STRING "C",
(res < 0 ? "<" : (res > 0 ? ">" : "")), temperature);
if (oven_pid_get_status() == OVEN_PID_RUNNING) {
menu_lcd_outputf(menu, 3, "Target: %.0f " LCD_DEGREE_SYMBOL_STRING "C", state->setpoint);
} else {
menu_lcd_outputf(menu, 3, "Temp Off");
}
} else if (state->status == TPE_OFF) {
menu_lcd_outputf(menu, 0, "Finished!");
menu_lcd_outputf(menu, 1, "Press button");
menu_lcd_outputf(menu, 2, "to return.");
(void)adc_pt1000_get_current_resistance(&resistance);
res = temp_converter_convert_resistance_to_temp(resistance, &temperature);
menu_lcd_outputf(menu, 3, "Temp: %.1f ", LCD_DEGREE_SYMBOL_STRING "C", temperature);
} else {
menu_lcd_outputf(menu, 0, "Profile aborted!");
menu_lcd_outputf(menu, 1, "Check flags!");
menu_lcd_outputf(menu, 2, "");
menu_lcd_outputf(menu, 3, "Press button");
}
last_tick = systick_get_global_tick();
}
if (menu_get_button_ready_state(menu)) {
if (menu_get_button_state(menu) != BUTTON_IDLE) {
temp_profile_executer_stop();
menu_entry_dropback(menu, my_parent);
}
}
}
static void gui_menu_temp_profile_select(struct lcd_menu *menu, enum menu_entry_func_entry entry_type, void *parent)
{
static void *my_parent;
static char profile_list[10][17];
static bool file_error = false;
static enum pl_ret_val profile_ret_val = PL_RET_SUCCESS;
static uint8_t currently_selected = 0U;
static uint8_t loaded;
int16_t delta;
enum button_state button;
int res;
if (entry_type == MENU_ENTRY_FIRST_ENTER) {
menu_display_clear(menu);
my_parent = parent;
res = load_temperature_file_list_from_sdcard(profile_list, 10);
file_error = false;
loaded = 0;
if (res < 0) {
file_error = true;
}
currently_selected = 0u;
profile_ret_val = PL_RET_SUCCESS;
loaded = (uint32_t)res;
menu_lcd_outputf(menu, 0, "Select:");
} else if (entry_type == MENU_ENTRY_DROPBACK) {
menu_entry_dropback(menu, my_parent);
return;
}
if (menu_get_button_ready_state(menu)) {
delta = menu_get_rotary_delta(menu);
button = menu_get_button_state(menu);
if (button == BUTTON_LONG) {
menu_entry_dropback(menu, my_parent);
}
if (file_error) {
menu_lcd_outputf(menu, 0, "Disk Error");
menu_lcd_outputf(menu, 1, "SD inserted?");
if (button == BUTTON_SHORT_RELEASED)
menu_entry_dropback(menu, my_parent);
return;
} else if (loaded == 0) {
menu_lcd_outputf(menu, 0, "No profiles");
menu_lcd_outputf(menu, 1, "found");
if (button == BUTTON_SHORT_RELEASED)
menu_entry_dropback(menu, my_parent);
return;
} else if (profile_ret_val != PL_RET_SUCCESS) {
menu_lcd_outputf(menu, 0, "ERROR");
switch (profile_ret_val) {
case PL_RET_SCRIPT_ERR:
menu_lcd_outputf(menu, 1, "Syntax Error");
break;
case PL_RET_DISK_ERR:
menu_lcd_outputf(menu, 1, "Disk Error");
break;
case PL_RET_LIST_FULL:
menu_lcd_output(menu, 1, "Too many com-");
menu_lcd_output(menu, 2, "mands in file");
break;
default:
menu_lcd_output(menu, 1, "Unknown error");
break;
}
if (button == BUTTON_SHORT_RELEASED)
menu_entry_dropback(menu, my_parent);
return;
} else if (currently_selected < loaded) {
/* Show currently selected profile */
menu_lcd_outputf(menu, 1, "%s", &profile_list[currently_selected][0]);
if (button == BUTTON_SHORT_RELEASED) {
/* Execute selected profile */
profile_ret_val = temp_profile_executer_start(&profile_list[currently_selected][0]);
if (profile_ret_val == PL_RET_SUCCESS) {
menu_entry_enter(menu, gui_menu_temp_profile_execute, true);
return;
}
}
if (delta >= 4) {
menu_ack_rotary_delta(menu);
if (currently_selected < (loaded - 1))
currently_selected++;
} else if (delta <= -4) {
menu_ack_rotary_delta(menu);
if (currently_selected > 0)
currently_selected--;
}
}
}
}
static void gui_menu_constant_temperature_driver_setup(struct lcd_menu *menu, enum menu_entry_func_entry entry_type, void *parent)
{
static void IN_SECTION(.ccm.bss) *my_parent;
struct oven_pid_settings pid_settings;
enum button_state button;
struct pid_controller pid_controller;
if (entry_type == MENU_ENTRY_FIRST_ENTER) {
my_parent = parent;
/* Try loading PID parameters */
if (settings_load_pid_oven_parameters(&pid_settings)) {
menu_display_clear(menu);
menu_lcd_output(menu, 0, "Could not load");
menu_lcd_output(menu, 1, "PID parameters");
} else {
pid_init(&pid_controller, pid_settings.kd, pid_settings.ki, pid_settings.kp, 0, 100,
2021-01-24 19:56:00 +01:00
pid_settings.max_integral, pid_settings.kd_tau, pid_settings.t_sample);
oven_pid_init(&pid_controller);
menu_entry_enter(menu, gui_menu_constant_temperature_driver, true);
}
} else if (entry_type == MENU_ENTRY_DROPBACK) {
menu_entry_dropback(menu, my_parent);
}
if (menu_get_button_ready_state(menu)) {
button = menu_get_button_state(menu);
if (button == BUTTON_SHORT_RELEASED || button == BUTTON_LONG) {
menu_entry_dropback(menu, my_parent);
}
}
}
static void gui_menu_root_entry(struct lcd_menu *menu, enum menu_entry_func_entry entry_type, void *parent)
{
(void)parent;
static struct menu_list list;
bool menu_changed = false;
static const char * const root_entry_names[] = {
"Constant Temp",
"Temp Profile",
"Monitoring",
"Error Flags",
"About",
NULL
};
static const menu_func_t root_entry_funcs[] = {
gui_menu_constant_temperature_driver_setup,
gui_menu_temp_profile_select,
gui_menu_monitor,
gui_menu_err_flags,
gui_menu_about,
};
enum button_state push_button;
int16_t rot_delta;
if (entry_type != MENU_ENTRY_CONTINUE) {
2020-06-25 23:52:58 +02:00
menu_changed = true;
menu_display_clear(menu);
update_display_buffer(0, "Main Menu");
2020-06-14 01:04:21 +02:00
menu_ack_rotary_delta(menu);
if (entry_type == MENU_ENTRY_FIRST_ENTER) {
list.entry_names = root_entry_names;
list.submenu_list = root_entry_funcs;
list.update_display = menu->update_display;
list.currently_selected = 0;
menu_list_compute_count(&list);
}
}
push_button = menu_get_button_state(menu);
rot_delta = menu_get_rotary_delta(menu);
if (menu_get_button_ready_state(menu)) {
if (menu_get_button_ready_state(menu) && push_button == BUTTON_SHORT_RELEASED) {
/* Enter currently selected menu_entry */
menu_list_enter_selected_entry(&list, menu);
}
if (rot_delta >= 4) {
menu_list_scroll_down(&list);
menu_ack_rotary_delta(menu);
menu_changed = true;
} else if (rot_delta <= -4) {
menu_list_scroll_up(&list);
menu_ack_rotary_delta(menu);
menu_changed = true;
}
}
2020-06-25 23:52:58 +02:00
if (menu_changed)
menu_list_display(&list, 1, 3);
}
int gui_handle()
{
int32_t rot_delta;
enum button_state button;
static enum lcd_fsm_ret lcd_ret = LCD_FSM_NOP;
rot_delta = rotary_encoder_get_change_val();
button = button_read_event();
menu_handle(reflow_menu_ptr, (int16_t)rot_delta, button);
2020-06-14 17:52:27 +02:00
if (lcd_ret == LCD_FSM_CALL_AGAIN || lcd_tick_100us >= 5) {
lcd_ret = lcd_fsm_write_buffer(display_buffer);
lcd_tick_100us = 0UL;
}
if (lcd_ret == LCD_FSM_CALL_AGAIN)
return 0;
else
return 1;
}
void gui_init()
{
rotary_encoder_setup();
button_init();
lcd_init();
menu_init(reflow_menu_ptr, gui_menu_root_entry, update_display_buffer);
}