Humidistat
Arduino firmware for a humidistat (humidity controller)
Loading...
Searching...
No Matches
GraphicalDisplayUI.h
Go to the documentation of this file.
1#ifndef HUMIDISTAT_GRAPHICALDISPLAYUI_H
2#define HUMIDISTAT_GRAPHICALDISPLAYUI_H
3
4#include <U8g2lib.h>
5#include <SPI.h>
6#include <etl/span.h>
7#undef abs
8#include <cmath>
9
10#include CONFIG_HEADER
11#include "ConfigPar.h"
12#include "advanceEnum.h"
13#include "ControllerUI.h"
14#include "EEPROMConfig.h"
18
23template<class Humidistat_t>
25private:
27 enum class Tab {
28 main,
29 info,
30 config,
31 _last = config,
32 };
33
35 enum class Selection {
36 par,
37 number,
38 actions,
39 };
40
42 enum class Action {
43 save,
44 reset,
45 _last = reset
46 };
47
48 U8G2 &u8g2;
50 Humidistat_t &humidistat;
52
53 // States
55 uint8_t currentPar = 0;
58 uint8_t currentDigit = NUM_DIGITS - 1;
59 uint8_t currentSPProfile = 0;
60
61 uint8_t frame = 0;
62 uint8_t configSaveTimer = 0;
63
66
67 const uint8_t nConfigPars;
69
71 // (declaration, implementation specialised)
72 void drawMain();
73
74 void drawTabInfo() {
75 u8g2.setFont(u8g2_font_6x12_tr);
76
77 // Tab bar
78 u8g2.drawBox(12, 1, 26, 12);
79 u8g2.setDrawColor(0);
80 u8g2.drawStr(13, 10, "Info");
81 u8g2.setDrawColor(1);
82
83 u8g2.drawStr(2, 10, "C.");
84 u8g2.drawStr(40, 10, "C.");
85
86 // Temperature box
87 u8g2.drawVLine(67, 27, 18);
88 u8g2.drawStr(0, 23, "Temperatures");
89 u8g2.drawHLine(0, 26, 128);
90 u8g2.drawStr(0, 35, "Chamber");
91 printf(70, 35, "%3.1f", humidistat.getTemperature());
92 u8g2.drawStr(0, 43, "Thermistors");
93 u8g2.drawHLine(0, 44, 128);
94
95 // Thermistors
96 for (size_t i = 0; i < trs.size(); ++i) {
97 printNTC(70 + 15*i, 43, i);
98 }
99
100 // Setpoint profiles
101 u8g2.drawBox(0, 44, 65, 10);
102 u8g2.setDrawColor(0);
103 u8g2.drawStr(0, 52, "SP profile:");
104 u8g2.setDrawColor(1);
105 u8g2.drawStr(70, 53, config::profiles[currentSPProfile].label);
106
107
108 // Bottom bar
109 u8g2.drawHLine(0, 54, 128);
110 u8g2.setFont(u8g2_font_unifont_t_75);
111 u8g2.drawGlyph(0, 66, 9664);
112 u8g2.drawGlyph(40, 66, 9650);
113 u8g2.drawGlyph(50, 66, 9660);
114 u8g2.setFont(u8g2_font_5x7_tr);
115 u8g2.drawStr(10, 63, "tab");
116 u8g2.drawStr(60, 63, "adj");
117 u8g2.setFont(u8g2_font_6x12_tr);
118 }
119
121 void drawConfig() {
122 u8g2.setFont(u8g2_font_6x12_tr);
123
124 // Tab bar
125 u8g2.drawBox(25, 1, 38, 12);
126 u8g2.setDrawColor(0);
127 u8g2.drawStr(26, 10, "Config");
128 u8g2.setDrawColor(1);
129
130 u8g2.drawStr(2, 10, "C.");
131 u8g2.drawVLine(12, 1, 12);
132 u8g2.drawStr(14, 10, "I.");
133
134 // Print config parameters in scrolling menu
135 for (uint8_t i = 0; i < 4; i++) {
136 // Index of parameter to draw
137 int8_t nPar = currentPar + i - 1;
138
139 // Handling for first and last two parameters
140 if (currentPar == 0) nPar++;
141 if (currentPar == nConfigPars - 2) nPar -= 1;
142 if (currentPar == nConfigPars - 1) nPar -= 2;
143
144 uint8_t row = 22 + i * 10;
145
146 char *buf = configPars[nPar].asprint();
147 u8g2.drawStr(0, row, buf);
148 delete buf;
149
151 uint8_t x, w;
152 // Draw cursor on active parameter/digit
154 x = 0;
155 w = 40;
156 } else if (currentSelection == Selection::number) {
157 x = 66 + currentDigit * 6;
158 // Take into account the decimal separator:
159 // if the current parameter is a float and we're left of the decimal separator, move one block left
162 x -= 6;
163 w = 6;
164 }
165
166 if (nPar == currentPar) {
167 u8g2.setDrawColor(2);
168 u8g2.drawBox(x, row - 8, w, 10);
169 u8g2.setDrawColor(1);
170 }
171 }
172 }
173
174 // Actions
175 u8g2.drawStr(100, 32, "Save");
176 u8g2.drawStr(100, 42, "Reset");
178 uint8_t y;
180 y = 32 - 8;
181 }
183 y = 42 - 8;
184 }
185
186 u8g2.setDrawColor(2);
187 u8g2.drawBox(100, y, 40, 10);
188 u8g2.setDrawColor(1);
189 }
190
191 // Mode
193 u8g2.drawStr(80, 10, "EEPROM");
194
195 if (configSaveTimer != 0) {
196 u8g2.drawStr(85, 22, "saved");
197 u8g2.setCursor(115, 22);
198 u8g2.print(configSaveTimer * refreshInterval / 1000);
199 }
200
201
202 // Bottom bar
203 u8g2.setFont(u8g2_font_unifont_t_75);
204 u8g2.drawHLine(0, 54, 128);
205 u8g2.drawGlyph(0, 66, 9664);
206 u8g2.drawGlyph(30, 66, 9650);
207 u8g2.drawGlyph(36, 66, 9660);
208 u8g2.drawGlyph(65, 66, 9654);
209 u8g2.drawGlyph(98, 66, 9679);
210 u8g2.setFont(u8g2_font_5x7_tr);
212 u8g2.drawStr(10, 63, "tab");
213 u8g2.drawStr(45, 63, "par");
214 u8g2.drawStr(75, 63, "menu");
215 u8g2.drawStr(108, 63, "edit");
216 }
218 u8g2.drawStr(45, 63, "adj");
219 u8g2.drawStr(108, 63, "OK");
220 }
222 u8g2.drawStr(10, 63, "back");
223 u8g2.drawStr(45, 63, "");
224 u8g2.drawStr(75, 63, "back");
225 u8g2.drawStr(108, 63, "OK");
226 }
227 u8g2.setFont(u8g2_font_6x12_tr);
228 }
229
232 u8g2.setFont(u8g2_font_6x12_tr);
233
234 // Tab bar
235 u8g2.drawBox(1, 1, 42, 12);
236 u8g2.setDrawColor(0);
237 u8g2.drawStr(1, 10, "Control");
238 u8g2.setDrawColor(1);
239
240 u8g2.drawStr(44, 10, "I.");
241 u8g2.drawVLine(55, 1, 12);
242 u8g2.drawStr(57, 10, "C.");
243 u8g2.drawVLine(70, 1, 12);
244
245 // Humidity box
246 u8g2.drawVLine(13, 27, 28);
247 u8g2.drawStr(0, 23, "Humidity");
248 u8g2.drawHLine(0, 26, 51);
249
250 u8g2.drawStr(0, 35, "PV");
251
252 if (humidistat.active) {
253 u8g2.drawBox(0, 36, 13, 9);
254 u8g2.setDrawColor(0);
255 }
256 {
257 char buf[] = "SP";
258 if (abs(humidistat.pv - humidistat.sp) > tolerance * 100) {
259 blink(0, 44, buf);
260 } else {
261 u8g2.drawStr(0, 44, buf);
262 }
263 }
264 u8g2.setDrawColor(1);
265
266 printf(14, 35, "%5.1f%%", humidistat.getHumidity());
267 printf(14, 44, "%5.1f%%", humidistat.sp);
268
269 // CV
270 if (!humidistat.active) {
271 u8g2.drawBox(0, 45, 13, 9);
272 u8g2.setDrawColor(0);
273 }
274 u8g2.drawStr(0, 53, "CV");
275 u8g2.setDrawColor(1);
276 printf(20, 53, "%3.0f%%", humidistat.cv * 100);
277
278 // Mode
279 if (humidistat.active)
280 u8g2.drawStr(80, 10, "auto");
281 else
282 u8g2.drawStr(80, 10, "manual");
283
284 // Setpoint profiles
285 if(spr.isRunning()) {
286 u8g2.setFont(u8g2_font_5x7_tr);
287 printf(52, 53, "Prof: %u/%u", spr.getCurrentPoint(), config::profiles[currentSPProfile].profile.size() - 1);
288 u8g2.setFont(u8g2_font_6x12_tr);
289 }
290
291 // Bottom bar
292 u8g2.drawHLine(0, 54, 128);
293 u8g2.setFont(u8g2_font_unifont_t_75);
294 u8g2.drawGlyph(0, 66, 9664);
295 u8g2.drawGlyph(30, 66, 9650);
296 u8g2.drawGlyph(36, 66, 9660);
297 u8g2.drawGlyph(65, 66, 9654);
298 u8g2.drawGlyph(98, 66, 9679);
299 u8g2.setFont(u8g2_font_5x7_tr);
300 u8g2.drawStr(10, 63, "tab");
301 u8g2.drawStr(45, 63, "adj");
302 u8g2.drawStr(75, 63, "prof");
303 u8g2.drawStr(108, 63, "mode");
304 u8g2.setFont(u8g2_font_6x12_tr);
305 }
306
308 void drawTabBar() {
309 u8g2.setFont(u8g2_font_6x12_tr);
310 u8g2.drawFrame(0, 0, 128, 14);
311
312 // Spinning indicator
313 u8g2.setFont(u8g2_font_unifont_t_75);
314 uint8_t i = (frame / 2) % 4;
315 u8g2.drawGlyph(118, 10, 0x25f3 - i);
316 }
317
318 bool handleInput(Buttons state, uint16_t pressedFor) override {
319 // First handle common input actions between tabs
320 if (state == Buttons::NONE) {
321 return false;
322 }
323
324 // Tab-specific handling
325 switch (currentTab) {
326 case Tab::main:
327 return handleInputMain(state, pressedFor);
328 case Tab::info:
329 return handleInputInfo(state, pressedFor);
330 case Tab::config:
331 return handleInputConfig(state, pressedFor);
332 }
333 }
334
336 bool handleInputMain(Buttons state, uint16_t pressedFor) {
337 int8_t delta = 0;
338 if (state == Buttons::LEFT) {
340 return true;
341 } else if (state == Buttons::RIGHT) {
343 spr.toggle();
344 } else if (state == Buttons::UP) {
345 delta = 1;
346 } else if (state == Buttons::DOWN) {
347 delta = -1;
348 } else if (state == Buttons::SELECT) {
349 // Toggle active state
350 humidistat.active = !humidistat.active;
351 return true;
352 }
353
354 // Long press coarse adjustment
355 if (pressedFor > longPressDuration)
356 delta *= adjustStep;
357 if (pressedFor > longPressDuration * 10)
358 delta *= 10;
359
360 if (humidistat.active) {
361 adjustValue(delta, humidistat.sp, 0, 100);
362 } else {
363 adjustValue(delta*0.01, humidistat.cv, humidistat.getCvMin(), humidistat.getCvMax());
364 }
365 return true;
366 }
367
369 bool handleInputInfo(Buttons state, uint16_t pressedFor) {
370 if (state == Buttons::LEFT) {
372 return true;
373 } else if (state == Buttons::UP) {
375 return true;
376 } else if (state == Buttons::DOWN) {
378 return true;
379 }
380 return false;
381 }
382
384 bool handleInputConfig(Buttons state, uint8_t pressedFor) {
385 // Ugly state machine below, maybe refactor?
387 if (state == Buttons::SELECT) {
389 return true;
390 }
391 if (state == Buttons::LEFT) {
393 return true;
394 }
395 if (state == Buttons::RIGHT) {
397 return true;
398 }
399 if (state == Buttons::UP) {
400 // Go up in parameter list
402 // Handle wrap-around
403 if (currentPar == 255) currentPar = nConfigPars - 1;
404 return true;
405 }
406 if (state == Buttons::DOWN) {
407 // Go down in parameter list
409 return true;
410 }
411 } else if (currentSelection == Selection::number) {
412 // Move selected digit left/right
413 if (state == Buttons::LEFT) {
414 currentDigit--;
415 if (currentDigit == 255) currentDigit = NUM_DIGITS - 1;
416 return true;
417 }
418 if (state == Buttons::RIGHT) {
420 return true;
421 }
422 // Go back to parameter selection
423 if (state == Buttons::SELECT) {
425 return true;
426 }
427 // Adjust digit up/down
428 if (state == Buttons::UP) {
430 return true;
431 }
432 if (state == Buttons::DOWN) {
434 return true;
435 }
436 } else if (currentSelection == Selection::actions) {
437 if (state == Buttons::LEFT || state == Buttons::RIGHT) {
439 return true;
440 }
441 if (state == Buttons::UP || state == Buttons::DOWN) {
443 return true;
444 }
445 if (state == Buttons::SELECT) {
446 humidistat.updatePIDParameters();
448 if (configSaveTimer == 0) {
451 }
452 return true;
453 }
456 return true;
457 }
458 }
459 }
460 }
461
462 void draw() override {
463 lastRefreshed = millis();
464
465 u8g2.clearBuffer();
466 drawTabBar();
467 switch (currentTab) {
468 case Tab::main:
469 drawMain();
470 break;
471 case Tab::info:
472 drawTabInfo();
473 break;
474 case Tab::config:
475 drawConfig();
476 break;
477 }
478 u8g2.sendBuffer();
479
480 // Keep track of frames
481 frame++;
482
483 // Decrement cooldown timer
484 if (configSaveTimer != 0)
486 }
487
488 void drawSplash() override {
489 u8g2.setFont(u8g2_font_helvB12_tr);
490 u8g2.drawStr(0, 24, "OpenHumidistat");
491
492 u8g2.setFont(u8g2_font_6x12_tr);
493 u8g2.drawStr(0, 40, "Lars Veldscholte");
494
495 u8g2.setFont(u8g2_font_5x7_tr);
496 u8g2.drawStr(0, 50, "https://github.com/");
497 u8g2.drawStr(0, 60, "OpenHumidistat");
498
499 u8g2.sendBuffer();
500 }
501
502 void drawInfo() override {}
503
504 void clear() override {
505 u8g2.clear();
506 }
507
508 void setCursor(uint8_t col, uint8_t row) override {
509 u8g2.setCursor(col, row);
510 }
511
512public:
533
535 etl::span<const ThermistorReader, 4> trs, EEPROMConfig *eepromConfig,
537 : ControllerUI(u8g2, buttonReader, trs), u8g2(*u8g2), eepromConfig(*eepromConfig),
538 humidistat(*humidistat), spr(*spr), nConfigPars(13), configPars{
539 {&eepromConfig->configStore.HC_Kp, "HC Kp"},
540 {&eepromConfig->configStore.HC_Ki, "HC Ki"},
541 {&eepromConfig->configStore.HC_Kd, "HC Kd"},
542 {&eepromConfig->configStore.HC_Kf, "HC Kf"},
543 {&eepromConfig->configStore.FC_Kp, "FC Kp"},
544 {&eepromConfig->configStore.FC_Ki, "FC Ki"},
545 {&eepromConfig->configStore.FC_Kd, "FC Kd"},
546 {&eepromConfig->configStore.FC_Kf, "FC Kf"},
547 {&eepromConfig->configStore.FC_dt, "FC dt"},
549 {&eepromConfig->configStore.dt, "dt"},
551 {&eepromConfig->configStore.a, "a"},
552 } {}
554
555 void begin() override {
556 SPI.begin();
557 u8g2.begin();
558 }
559};
560
561template<>
563 DrawMainCommon();
564
565 // PID box
566 double pTerm, iTerm, dTerm;
567 humidistat.getTerms(pTerm, iTerm, dTerm);
568
569 u8g2.drawFrame(52, 13, 47, 31);
570 u8g2.drawStr(54, 23, "P");
571 u8g2.drawStr(54, 32, "I");
572 u8g2.drawStr(54, 41, "D");
573 u8g2.drawVLine(60, 13, 31);
574
575 printf(62, 23, "%6.2f", pTerm);
576 printf(62, 32, "%6.2f", iTerm);
577 printf(62, 41, "%6.2f", dTerm);
578
579 // Temperature box
580 u8g2.setCursor(105, 23);
581 u8g2.print(humidistat.getTemperature(), 1);
582}
583
584template<>
586 DrawMainCommon();
587
588 // Flow box
589 u8g2.drawFrame(50, 13, 65, 42);
590 u8g2.drawStr(55, 23, "F");
591
592 {
593 char buf[] = "wet";
594 if (std::abs(humidistat.getInner(0)->pv - humidistat.getInner(0)->sp) > tolerance * 2) {
595 blink(66, 23, buf);
596 } else {
597 u8g2.drawStr(66, 23, buf);
598 }
599 }
600 {
601 char buf[] = "dry";
602 if (std::abs(humidistat.getInner(1)->pv - humidistat.getInner(1)->sp) > tolerance * 2) {
603 blink(91, 23, buf);
604 } else {
605 u8g2.drawStr(91, 23, buf);
606 }
607 }
608
609 u8g2.drawHLine(50, 26, 64);
610 u8g2.drawVLine(64, 13, 32);
611 u8g2.drawVLine(89, 13, 32);
612 u8g2.drawHLine(50, 45, 64);
613
614 u8g2.drawStr(52, 35, "PV");
615 u8g2.drawStr(52, 44, "CV");
616
617 printf(65, 35, "%3.2f", humidistat.getInner(0)->pv);
618 printf(65, 44, "%3.0f%%", humidistat.getInner(0)->cv * 100);
619 printf(90, 35, "%3.2f", humidistat.getInner(1)->pv);
620 printf(90, 44, "%3.0f%%", humidistat.getInner(1)->cv * 100);
621}
622
623#endif //HUMIDISTAT_GRAPHICALDISPLAYUI_H
Buttons
Possible button values.
Definition Buttons.h:5
#define NUM_DIGITS
Definition ConfigPar.h:7
#define NUM_DECIMALS
Definition ConfigPar.h:8
void advanceEnum(T &e, int8_t n=1)
Cycle through (advance) an enum.
Definition advanceEnum.h:11
Read button state from a voltage ladder-style keypad.
Control humidity using cascade PID: outer PID loop sets setpoints of two inner flow controllers,...
A class for storing references to variables of various types (uint8_t, uint16_t, or double).
Definition ConfigPar.h:11
uint8_t magnitude() const
Get magnitude (number of digits before the decimal separator) of variable.
Definition ConfigPar.cpp:33
char * asprint() const
Print "label: value" to string. Automatically allocates string on the heap. Make sure to delete it im...
Definition ConfigPar.cpp:22
void adjust(int16_t delta) const
Add delta to the variable.
Definition ConfigPar.cpp:8
User interface (display and input) for humidistat. Hold references to ButtonReader for keypad input,...
const uint16_t refreshInterval
const double tolerance
static void adjustValue(double delta, double &value, uint8_t min, uint8_t max)
In-/de-crement a variable, while clipping it to [min, max].
etl::span< const ThermistorReader, 4 > trs
unsigned long lastRefreshed
Last time display was updated (in millis)
void printf(uint8_t col, uint8_t row, const char *fmt, T... args)
Print formatted data to display, at (col, row). Calculates lengths and creates appropriate buffer int...
const ButtonReader & buttonReader
void printNTC(uint8_t col, uint8_t row, uint8_t i)
Print temperature read from thermistors. Handles NaN values as 0.
const uint8_t adjustStep
void blink(uint8_t col, uint8_t row, const char *buf)
Print blinking text.
Load/save an (internal) ConfigStore in EEPROM.
void reset()
Reset the config store: overwrite the configStore with the default values.
uint16_t save() const
Saves current content of configStore into EEPROM.
ConfigStore configStore
TUI for 128*64 px graphical display using U8g2. Holds references to a U8g2lib instance for writing to...
void setCursor(uint8_t col, uint8_t row) override
Set cursor to coordinates.
uint8_t configSaveTimer
Timer containing the current value of the cooldown on saving config to EEPROM.
Action
Config tab action definitions.
void clear() override
Clear screen.
GraphicalDisplayUI(U8G2 *u8g2, const ButtonReader *buttonReader, SingleHumidistat *humidistat, etl::span< const ThermistorReader, 4 > trs, EEPROMConfig *eepromConfig, SetpointProfileRunner *spr)
Constructor.
bool handleInputConfig(Buttons state, uint8_t pressedFor)
Handle input on the Config tab.
void drawMain()
Draw the Main tab.
Selection
Config tab selection definitions.
const uint16_t longPressDuration
Humidistat_t & humidistat
bool handleInput(Buttons state, uint16_t pressedFor) override
Handle input.
const uint8_t configSaveCooldown
GraphicalDisplayUI(U8G2 *u8g2, const ButtonReader *buttonReader, CascadeHumidistat *humidistat, etl::span< const ThermistorReader, 4 > trs, EEPROMConfig *eepromConfig, SetpointProfileRunner *spr)
const uint8_t nConfigPars
Total number of config parameters.
uint8_t currentPar
Currently active config parameter.
void drawTabBar()
Draw the tab bar.
void begin() override
Initialize the display.
bool handleInputInfo(Buttons state, uint16_t pressedFor)
Handle input on the Info tab.
EEPROMConfig & eepromConfig
bool handleInputMain(Buttons state, uint16_t pressedFor)
Handle input on the Main tab.
Tab currentTab
Currently active tab.
void drawConfig()
Draw the Config tab.
void draw() override
Draw main interface (main loop).
void drawSplash() override
Draw splash screen.
SetpointProfileRunner & spr
void drawInfo() override
Draw info screen.
const ConfigPar configPars[13]
Array of config parameters.
uint8_t frame
Frame counter (overflows, but that's OK)
void DrawMainCommon()
Draw common elements in Main tab.
'Runs' a setpoint profile.
void setProfile(const etl::span< const Point > &profile)
Set the profile.
bool isRunning() const
Get the run state.
size_t getCurrentPoint() const
Get the index of the current Point in the profile.
void toggle()
Toggle the run state.
Control humidity using PID by driving two solenoid valves. Adjust the public setpoint variable and ca...
constexpr T ipow(T base, unsigned int pow)
Constexpr function for computing integer power.
Definition imath.h:10
SerialLogger< cHumidistat > serialLogger & humidistat
Definition main.cpp:84
ButtonReader buttonReader(config::PIN_BTN, &voltLadder)
ThermistorReader trs[]
Definition main.cpp:25
EEPROMConfig eepromConfig
Definition main.cpp:36
const uint16_t longPressDuration
Duration for counting a press as 'long' (in millis)
Definition config.h:177
const SPProfile profiles[]
Setpoint profile definitions.
Definition config.h:139
const uint8_t configSaveCooldown
Cooldown on saving the config to EEPROM (in refresh cycles)
Definition config.h:180
uint16_t FC_dt
double HC_totalFlowrate
Total flowrate (for cascade controller) (L/min)
double a
Smoothing factor of EMA filter for derivative.
double HC_Kp
Humidity controller PID parameters.
bool loadedFromEEPROM
Whether this has been loaded from EEPROM.
Definition EEPROMConfig.h:9
double FC_Kp
Flow controller PID parameters.
double S_lowValue
Minimum solenoid duty cycle (deadband)
uint16_t dt
Global interval for PID/logger (based on polling rate of sensor, in millis)
const etl::span< const Point > profile
Definition Point.h:16