/* Reflow Oven Controller * * Copyright (C) 2020 Mario Hüttel * * 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 . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static char IN_SECTION(.ccm.bss) display_buffer[4][21] = {0}; static struct lcd_menu IN_SECTION(.ccm.bss) reflow_menu; #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); } if (systick_ticks_have_passed(my_timestamp, GUI_MONITORING_INTERVAL_MS)) { my_timestamp = systick_get_global_tick(); adc_pt1000_get_current_resistance(&tmp); 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); (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); (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; static uint64_t IN_SECTION(.ccm.bss) last_temp_refresh; static float IN_SECTION(.ccm.bss) last_temp; enum button_state button; float current_temp; int status; int16_t rot; int16_t temp_old; if (entry_type == MENU_ENTRY_FIRST_ENTER) { my_parent = parent; 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); 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(¤t_temp); status = temp_converter_convert_resistance_to_temp(current_temp, ¤t_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(); } } } static void delete_file_list_entry(void *obj) { if (obj) free(obj); } static SlList *load_file_list_from_sdcard(int *error, const char *file_pattern) { DIR directory; FILINFO finfo; FRESULT fres; SlList *list = NULL; char *name; /* find the frist file */ fres = f_findfirst(&directory, &finfo, "/", file_pattern); while (fres == FR_OK && finfo.fname[0]) { name = malloc(strlen(finfo.fname) + 1); strcpy(name, finfo.fname); list = sl_list_append(list, name); fres = f_findnext(&directory, &finfo); } if (fres != FR_OK) { sl_list_free_full(list, delete_file_list_entry); list = NULL; if (error) *error = -1; } return list; } 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 SlList *file_list = NULL; static int file_error = 0; 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; if (entry_type == MENU_ENTRY_FIRST_ENTER) { menu_display_clear(menu); my_parent = parent; file_error = 0; loaded = 0; if (file_list) { sl_list_free_full(file_list, delete_file_list_entry); } file_list = load_file_list_from_sdcard(&file_error, "*.tpr"); currently_selected = 0u; profile_ret_val = PL_RET_SUCCESS; loaded = sl_list_length(file_list); 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) { sl_list_free_full(file_list, delete_file_list_entry); file_list = NULL; 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) { sl_list_free_full(file_list, delete_file_list_entry); file_list = NULL; 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) { sl_list_free_full(file_list, delete_file_list_entry); file_list = NULL; menu_entry_dropback(menu, my_parent); } return; } else if (currently_selected < loaded) { /* Show currently selected profile */ menu_lcd_outputf(menu, 1, "%s", sl_list_nth(file_list, currently_selected)->data); if (button == BUTTON_SHORT_RELEASED) { /* Execute selected profile */ profile_ret_val = temp_profile_executer_start(sl_list_nth(file_list, currently_selected)->data); if (profile_ret_val == PL_RET_SUCCESS) { sl_list_free_full(file_list, delete_file_list_entry); file_list = NULL; 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, 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_update_firmware(struct lcd_menu *menu, enum menu_entry_func_entry entry_type, void *parent) { static void *my_parent; static SlList *file_list = NULL; static int error; enum button_state button; static uint32_t currently_selected_file; uint32_t previously_selected_file; static uint32_t list_length; const char *fname; int16_t rotary; if (entry_type == MENU_ENTRY_FIRST_ENTER) { my_parent = parent; error = 0; list_length = 0; if (file_list) sl_list_free_full(file_list, delete_file_list_entry); file_list = load_file_list_from_sdcard(&error, "*.hex"); if (error) { if (file_list) sl_list_free_full(file_list, delete_file_list_entry); file_list = NULL; } else { list_length = sl_list_length(file_list); } currently_selected_file = 0; menu_display_clear(menu); } if (menu_get_button_ready_state(menu)) { button = menu_get_button_state(menu); rotary = menu_get_rotary_delta(menu); if (error) { menu_lcd_output(menu, 0, "Error reading"); menu_lcd_output(menu, 1, "file list"); if (button == BUTTON_LONG || button == BUTTON_SHORT_RELEASED) menu_entry_dropback(menu, my_parent); } else { previously_selected_file = currently_selected_file; /* Display the list */ if (rotary <= -4) { menu_ack_rotary_delta(menu); if (currently_selected_file > 0) { currently_selected_file--; } } else if (rotary >= 4) { menu_ack_rotary_delta(menu); if (currently_selected_file < (list_length - 1)) { currently_selected_file++; } } if (entry_type == MENU_ENTRY_FIRST_ENTER || previously_selected_file != currently_selected_file) { fname = sl_list_nth(file_list, currently_selected_file)->data; menu_lcd_output(menu, 0, "Select File:"); if (fname) menu_lcd_output(menu, 1, fname); } if (button == BUTTON_SHORT_RELEASED) { fname = sl_list_nth(file_list, currently_selected_file)->data; menu_display_clear(menu); file_list = NULL; updater_update_from_file(fname); /* This code is here for completeness. It will never be reached! */ sl_list_free_full(file_list, delete_file_list_entry); } else if (button == BUTTON_LONG) { sl_list_free_full(file_list, delete_file_list_entry); file_list = NULL; menu_entry_dropback(menu, my_parent); } } } } static char *overlay_heading = NULL; static char *overlay_text = NULL; static void gui_menu_overlay_entry(struct lcd_menu *menu, enum menu_entry_func_entry entry_type, void *parent) { static void *my_parent; enum button_state button; if (entry_type == MENU_ENTRY_FIRST_ENTER) { my_parent = parent; menu_display_clear(menu); if (overlay_heading) menu_lcd_output(menu, 0, overlay_heading); if (overlay_text) { menu_lcd_output(menu, 2, overlay_text); if (strlen(overlay_text) > 16) { menu_lcd_output(menu, 3, &overlay_text[16]); } } } if (menu_get_button_ready_state(menu)) { button = menu_get_button_state(menu); menu_ack_rotary_delta(menu); if (button != BUTTON_IDLE) { if (overlay_heading) free(overlay_heading); if (overlay_text) free(overlay_text); overlay_heading = NULL; overlay_text = NULL; 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", "Update", 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, gui_update_firmware, }; enum button_state push_button; int16_t rot_delta; if (entry_type != MENU_ENTRY_CONTINUE) { menu_changed = true; menu_display_clear(menu); update_display_buffer(0, "Main Menu"); 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; } } /* Display the message overlay in case it is set */ if (overlay_heading || overlay_text) { menu_entry_enter(menu, gui_menu_overlay_entry, true); return; } else 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); 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(); if (overlay_heading) free(overlay_heading); if (overlay_text) free(overlay_text); overlay_heading = NULL; overlay_text = NULL; menu_init(reflow_menu_ptr, gui_menu_root_entry, update_display_buffer); } void gui_root_menu_message_set(const char *heading, const char *text) { if (heading) { overlay_heading = (char *)malloc(strlen(heading) + 1); strcpy(overlay_heading, heading); } if (text) { overlay_text = (char *)malloc(strlen(text) + 1); strcpy(overlay_text, text); } } void gui_lcd_write_direct_blocking(uint8_t line, const char *text) { if (!text) return; if (line > 3) return; lcd_setcursor(0, line); lcd_string(text); }