#include "display.h" #include "sensor_task.h" #include "stm32l_rtc.h" #include "tempgraph.h" #include "battery.h" #include "powersave.h" #include #include #include #include #include #include #define INVALID_TEMPERATURE -999999 static font_t bigfont; static font_t mediumfont; static int rounding_div(int n, int d) { if ((n < 0) != (d < 0)) return (n - d/2) / d; else return (n + d/2) / d; } struct ephemerals { int prev_temperature; char temperature[8]; char alarm[4]; int alarm_temp; char alarmtime[16]; bool batt_low; int samplecount; int timescale; // Minimum number of seconds between samples bool need_full_update; bool alarm_white_on_black; // Persistence reduction int xrange; // Seconds on X axis int ymin; int ymax; bool have_prediction; int pred_x0; int pred_y0; int pred_uc_per_sec; }; static struct ephemerals g_ephemerals; static struct ephemerals g_prev_ephemerals; struct tempsample { int time; int temperature; }; #define SAMPLECOUNT 100 static struct tempsample g_samples[SAMPLECOUNT]; void display_init() { bigfont = gdispOpenFont("bigfont"); mediumfont = gdispOpenFont("mediumfont"); display_clear(); } void display_clear() { display_update_temperature(); int temp = g_ephemerals.prev_temperature; if (temp == INVALID_TEMPERATURE) { temp = 20000; // Default guess for temperature } g_ephemerals.need_full_update = true; g_ephemerals.samplecount = 0; g_ephemerals.timescale = 5; g_ephemerals.xrange = 300; // 5 minutes initial graph length g_ephemerals.ymin = temp - 5000; g_ephemerals.ymax = temp + 10000; } int display_samplecount() { return g_ephemerals.samplecount; } void display_export_csv(BaseSequentialStream *chp) { int count = g_ephemerals.samplecount; chprintf(chp, "# Time(s), Temperature(mC)\r\n"); for (int i = 0; i < count; i++) { chprintf(chp, "%8d, %8d\r\n", g_samples[i].time, g_samples[i].temperature); } } void display_update_temperature() { int temp, time; bool wireless, have_temp; have_temp = sensor_get_temperature(&temp, &time, &wireless); if (have_temp) { int timedelta = 9999; if (g_ephemerals.samplecount > 0) { timedelta = time - g_samples[g_ephemerals.samplecount - 1].time; } if (abs(temp - g_ephemerals.prev_temperature) > 80 && (timedelta >= 30 || timedelta >= g_ephemerals.timescale)) { snprintf(g_ephemerals.temperature, sizeof(g_ephemerals.temperature), "%4d", rounding_div(temp, 100)); g_ephemerals.temperature[5] = '\0'; g_ephemerals.temperature[4] = g_ephemerals.temperature[3]; g_ephemerals.temperature[3] = ','; if (g_ephemerals.temperature[2] == ' ') g_ephemerals.temperature[2] = '0'; g_ephemerals.prev_temperature = temp; } if (temp > 40000) { powersave_keep_alive(); } if (timedelta >= g_ephemerals.timescale && g_ephemerals.samplecount < SAMPLECOUNT) { g_samples[g_ephemerals.samplecount].temperature = temp; g_samples[g_ephemerals.samplecount].time = time; g_ephemerals.samplecount++; } if ((g_ephemerals.samplecount == SAMPLECOUNT || time >= g_ephemerals.xrange) && time < 12 * 3600) { g_ephemerals.need_full_update = true; if (g_ephemerals.xrange > 3600) g_ephemerals.xrange = time + 900; else if (g_ephemerals.xrange > 1200) g_ephemerals.xrange = time + 600; else g_ephemerals.xrange = time + 300; g_ephemerals.timescale = g_ephemerals.xrange / SAMPLECOUNT; } if (temp < g_ephemerals.ymin) { g_ephemerals.ymin = temp - 10000; g_ephemerals.need_full_update = true; } if (temp > g_ephemerals.ymax) { g_ephemerals.ymax = temp + 10000; g_ephemerals.need_full_update = true; } } else { snprintf(g_ephemerals.temperature, sizeof(g_ephemerals.temperature), " --,-"); g_ephemerals.prev_temperature = INVALID_TEMPERATURE; } } void display_set_alarm(int celcius) { g_ephemerals.alarm_temp = celcius * 1000; snprintf(g_ephemerals.alarm, sizeof(g_ephemerals.alarm), "%d", celcius); } // Remove any unnecessary samples when timescale has changed. static void cull_samples() { int read = 0, write = 0; int prevtime = 0; while (read < g_ephemerals.samplecount) { if (g_samples[read].time > prevtime + g_ephemerals.timescale) { // Ok, let this sample through prevtime = g_samples[read].time; g_samples[write++] = g_samples[read++]; } else { // Drop this sample read++; } } g_ephemerals.samplecount = write; } // Update ymin/ymax static void update_ylimits() { if (g_ephemerals.samplecount == 0) return; int min = g_samples[0].temperature; int max = g_samples[0].temperature; for (int i = 1; i < g_ephemerals.samplecount; i++) { if (g_samples[i].temperature < min) min = g_samples[i].temperature; if (g_samples[i].temperature > max) max = g_samples[i].temperature; } g_ephemerals.ymin = min - 5000; g_ephemerals.ymax = max + 5000; } static void update_prediction() { g_ephemerals.have_prediction = false; // Take slope from last 10 samples int i0 = g_ephemerals.samplecount - 10; int i1 = g_ephemerals.samplecount - 1; if (i0 < 0) return; int x0 = g_samples[i0].time; int y0 = g_samples[i0].temperature; int x1 = g_samples[i1].time; int y1 = g_samples[i1].temperature; g_ephemerals.pred_x0 = x0; g_ephemerals.pred_y0 = y0; g_ephemerals.pred_uc_per_sec = (y1 - y0) * 1000 / (x1 - x0); // Check the mean absolute error for the last 10 samples to see how // well they can be approximated by a line. int sum = 0; for (int i = i0; i <= i1; i++) { int x = g_samples[i].time; int y = g_samples[i].temperature; int pred = g_ephemerals.pred_y0 + (x - g_ephemerals.pred_x0) * g_ephemerals.pred_uc_per_sec / 1000; int error = y - pred; if (error < 0) error = -error; sum += error; } // Compare the mean error to the slope int mean = sum / (i1 + 1 - i0); int slope = y1 - y0; if (slope < 0) slope = -slope; // Maximum error is 0.2 C plus 1/5 of local slope. if (mean < 200 + slope/5) g_ephemerals.have_prediction = true; } static void update_alarmtime() { g_ephemerals.alarmtime[0] = '\0'; if (!g_ephemerals.have_prediction) return; int t = g_ephemerals.alarm_temp; int x0 = g_ephemerals.pred_x0; int y0 = g_ephemerals.pred_y0; int now = rtc_get(); int x1 = x0 + (t - y0) * 1000 / g_ephemerals.pred_uc_per_sec; int timeleft = x1 - now; if (timeleft < 0 || timeleft > 12 * 3600) return; // Figure out how to round it int round; if (timeleft > 1800) round = 900; else if (timeleft > 600) round = 300; else round = 60; timeleft = (timeleft + round / 2) / round * round; // Format the text if (timeleft > 3600) { int h = timeleft / 3600; int m = (timeleft % 3600) / 60; if (m > 0) { snprintf(g_ephemerals.alarmtime, sizeof(g_ephemerals.alarmtime), "n. %d h %d m", h, m); } else { snprintf(g_ephemerals.alarmtime, sizeof(g_ephemerals.alarmtime), "n. %d h", h); } } else { snprintf(g_ephemerals.alarmtime, sizeof(g_ephemerals.alarmtime), "n. %d min", timeleft / 60); } } static void draw_temp_digit(int i, char c) { const uint8_t charw[5] = {70, 75, 75, 35, 75}; const uint8_t charpos[5] = {0, 70, 145, 220, 255}; int x = 6 + charpos[i]; int y = 65; char buf[2] = {c, '\0'}; gdispFillArea(x, y - 1, charw[i], 130, 1); gdispDrawString(x, y, buf, bigfont, 0); } static void draw_samples(int start) { for (int i = start; i < g_ephemerals.samplecount; i++) { int x1 = g_samples[i].time; int y1 = g_samples[i].temperature; int x0 = x1, y0 = y1; if (i > 0) { x0 = g_samples[i - 1].time; y0 = g_samples[i - 1].temperature; } if (x1 - x0 > g_ephemerals.timescale * 3 && x1 - x0 > 300) continue; // Don't connect samples that are way apart graph_drawline(x0, y0, x1, y1); } } static void draw_alarm(const char *alarm, bool white_on_black, bool full_clear) { int x = 370, y = 60, w = 80, h = 50; if (white_on_black) { // Draw value with white-on-black during changes to avoid persistence. gdispFillArea(x, y, w, h, Black); gdispDrawStringBox(x, y, w, h, alarm, mediumfont, White, justifyRight); } else if (full_clear) { // Intensively clear the background to get rid of persistence for (int i = 0; i < 3; i++) { gdispFillArea(x - 2, y - 2, w + 4, h + 4, White); gdispDrawStringBox(x, y, w, h, alarm, mediumfont, Black, justifyRight); gdispFlush(); } } else { // Just normal draw gdispDrawStringBox(x, y, w, h, alarm, mediumfont, Black, justifyRight); } } static void draw_alarmtime() { int x = 500, y = 60, w = 270, h = 50; gdispFillArea(x - 1, y - 1, w + 2, h + 2, White); gdispDrawStringBox(x, y, w, h, g_ephemerals.alarmtime, mediumfont, Black, justifyRight); } static void draw_lowbatt() { gdispDrawString(400, 140, "Akku vähissä!", mediumfont, 0); } void display_full_redraw() { cull_samples(); update_ylimits(); update_prediction(); update_alarmtime(); gdispSetPowerMode(powerOn); gdispClear(1); g_ephemerals.batt_low = battery_low(); // Draw battery low indicator text if (g_ephemerals.batt_low) { draw_lowbatt(); } // Current temperature gdispDrawRoundedBox(5, 5, 350, 200, 16, 0); gdispDrawString(15, 15, "Nyt ", mediumfont, 0); for (int i = 0; i < 5; i++) { draw_temp_digit(i, g_ephemerals.temperature[i]); } gdispDrawString(295, 15, "°C", mediumfont, 0); // Alarm limit gdispDrawRoundedBox(360, 5, 435, 120, 16, 0); gdispDrawString(370, 15, "Hälytys", mediumfont, 0); gdispDrawString(450, 60, "°C", mediumfont, 0); draw_alarm(g_ephemerals.alarm, false, false); draw_alarmtime(); // Graph graph_drawgrid(0, g_ephemerals.xrange, g_ephemerals.ymin, g_ephemerals.ymax); // Draw all samples draw_samples(0); // Draw prediction if (g_ephemerals.have_prediction) { graph_drawlinear(g_ephemerals.pred_x0, g_ephemerals.pred_y0, g_ephemerals.pred_uc_per_sec); } gdispSetPowerMode(powerOff); g_ephemerals.need_full_update = false; g_prev_ephemerals = g_ephemerals; } void display_partial_redraw() { if (g_ephemerals.need_full_update) { display_full_redraw(); return; } if (memcmp(&g_prev_ephemerals, &g_ephemerals, sizeof(g_ephemerals)) == 0) return; gdispSetPowerMode(powerOn); g_ephemerals.batt_low |= battery_low(); // Draw battery low indicator text if (g_ephemerals.batt_low && !g_prev_ephemerals.batt_low) { draw_lowbatt(); } // Draw current temperature for (int i = 0; i < 5; i++) { if (g_ephemerals.temperature[i] != g_prev_ephemerals.temperature[i]) draw_temp_digit(i, g_ephemerals.temperature[i]); } // Draw new samples draw_samples(g_prev_ephemerals.samplecount); // Draw alarm limit bool alarm_changed = false; if (strcmp(g_ephemerals.alarm, g_prev_ephemerals.alarm) != 0) { alarm_changed = true; draw_alarm(g_ephemerals.alarm, true, false); } else if (g_prev_ephemerals.alarm_white_on_black) { draw_alarm(g_ephemerals.alarm, false, true); } // Draw alarm time estimate if (!alarm_changed) update_alarmtime(); if (strcmp(g_ephemerals.alarmtime, g_prev_ephemerals.alarmtime) != 0) draw_alarmtime(); gdispSetPowerMode(powerOff); memcpy(&g_prev_ephemerals, &g_ephemerals, sizeof(g_ephemerals)); // Track whether we still need to redraw alarm field g_prev_ephemerals.alarm_white_on_black = alarm_changed; g_ephemerals.alarm_white_on_black = false; }