From fa790017363d22e1a0e0ae4ca25ff0a2f024c60b Mon Sep 17 00:00:00 2001 From: Juerd Waalboer Date: Sun, 21 Mar 2021 07:05:18 +0100 Subject: [PATCH] Refactor - Source files moved to src/ - operame.cpp renamed to main.cpp - Display code factored out to separate file - Sensor code factored out to separate file, turned into classes --- .gitignore | 2 +- src/display.h | 62 +++++++ logo.h => src/logo.h | 0 logo.png => src/logo.png | Bin operame.cpp => src/main.cpp | 274 ++++++----------------------- src/sensors.h | 113 ++++++++++++ operame_strings.h => src/strings.h | 1 + 7 files changed, 231 insertions(+), 221 deletions(-) create mode 100644 src/display.h rename logo.h => src/logo.h (100%) rename logo.png => src/logo.png (100%) rename operame.cpp => src/main.cpp (56%) create mode 100644 src/sensors.h rename operame_strings.h => src/strings.h (99%) diff --git a/.gitignore b/.gitignore index cc2b01c..38b15a9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -.pio +.* platformio-*.ini diff --git a/src/display.h b/src/display.h new file mode 100644 index 0000000..9bc6dd0 --- /dev/null +++ b/src/display.h @@ -0,0 +1,62 @@ +#include +#include +#include +#include +#include +#include + +TFT_eSPI tft; +TFT_eSprite sprite(&tft); + +void clear_sprite(int bg = TFT_BLACK) { + sprite.fillSprite(bg); + if (WiFi.status() == WL_CONNECTED) { + sprite.drawRect(0, 0, tft.width(), tft.height(), TFT_BLUE); + } +} + +void display(const String& text, int fg = TFT_WHITE, int bg = TFT_BLACK) { + clear_sprite(bg); + sprite.setTextSize(1); + bool nondigits = false; + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (c < '0' || c > '9') nondigits = true; + } + sprite.setTextFont(nondigits ? 4 : 8); + sprite.setTextSize(nondigits && text.length() < 10 ? 2 : 1); + sprite.setTextDatum(MC_DATUM); + sprite.setTextColor(fg, bg); + sprite.drawString(text, tft.width()/2, tft.height()/2); + + sprite.pushSprite(0, 0); +} + +void display(const std::list& lines, int fg = TFT_WHITE, int bg = TFT_BLACK) { + clear_sprite(bg); + sprite.setTextSize(1); + sprite.setTextFont(4); + sprite.setTextDatum(MC_DATUM); + sprite.setTextColor(fg, bg); + + const int line_height = 32; + int y = tft.height()/2 - (lines.size()-1) * line_height/2; + for (auto line : lines) { + sprite.drawString(line, tft.width()/2, y); + y += line_height; + } + sprite.pushSprite(0, 0); +} + +void display_logo() { + clear_sprite(); + sprite.setSwapBytes(true); + sprite.pushImage(12, 30, 215, 76, OPERAME_LOGO); + sprite.pushSprite(0, 0); +} + +void panic(const String& message) { + display(message, TFT_RED); + delay(5000); + ESP.restart(); +} \ No newline at end of file diff --git a/logo.h b/src/logo.h similarity index 100% rename from logo.h rename to src/logo.h diff --git a/logo.png b/src/logo.png similarity index 100% rename from logo.png rename to src/logo.png diff --git a/operame.cpp b/src/main.cpp similarity index 56% rename from operame.cpp rename to src/main.cpp index 461bd6b..bb3414a 100644 --- a/operame.cpp +++ b/src/main.cpp @@ -3,24 +3,17 @@ #include #include #include -#include #include -#include -#include -#include -#include -#include +#include +#include +#include #define LANGUAGE "nl" OperameLanguage::Texts T; -enum Driver { AQC, MHZ }; -Driver driver; MQTTClient mqtt; HardwareSerial hwserial1(1); -TFT_eSPI display; -TFT_eSprite sprite(&display); -MHZ19 mhz; +CO2Sensor *sensor; const int pin_portalbutton = 35; const int pin_demobutton = 0; @@ -28,7 +21,6 @@ const int pin_backlight = 4; const int pin_sensor_rx = 27; const int pin_sensor_tx = 26; const int pin_pcb_ok = 12; // pulled to GND by PCB trace -int mhz_co2_init = 410; // magic value reported during init // Configuration via WiFiSettings unsigned long mqtt_interval; @@ -43,51 +35,32 @@ bool wifi_enabled; bool mqtt_enabled; int max_failures; -void clear_sprite(int bg = TFT_BLACK) { - sprite.fillSprite(bg); - if (WiFi.status() == WL_CONNECTED) { - sprite.drawRect(0, 0, display.width(), display.height(), TFT_BLUE); +bool button(int pin) { + if (digitalRead(pin)) return false; + unsigned long start = millis(); + while (!digitalRead(pin)) { + if (millis() - start >= 50) display(""); } + return millis() - start >= 50; } -void display_big(const String& text, int fg = TFT_WHITE, int bg = TFT_BLACK) { - clear_sprite(bg); - sprite.setTextSize(1); - bool nondigits = false; - for (int i = 0; i < text.length(); i++) { - char c = text.charAt(i); - if (c < '0' || c > '9') nondigits = true; +void calibrate() { + auto lines = T.calibration; + for (int count = 60; count >= 0; count--) { + lines.back() = String(count); + display(lines, TFT_RED); + unsigned long start = millis(); + while (millis() - start < 1000) { + if (button(pin_demobutton) || button(pin_portalbutton)) return; + } } - sprite.setTextFont(nondigits ? 4 : 8); - sprite.setTextSize(nondigits && text.length() < 10 ? 2 : 1); - sprite.setTextDatum(MC_DATUM); - sprite.setTextColor(fg, bg); - sprite.drawString(text, display.width()/2, display.height()/2); - sprite.pushSprite(0, 0); -} + lines = T.calibrating; + for (auto& line : lines) line.replace("400", String(sensor->co2_zero)); + display(lines, TFT_MAGENTA); -void display_lines(const std::list& lines, int fg = TFT_WHITE, int bg = TFT_BLACK) { - clear_sprite(bg); - sprite.setTextSize(1); - sprite.setTextFont(4); - sprite.setTextDatum(MC_DATUM); - sprite.setTextColor(fg, bg); - - const int line_height = 32; - int y = display.height()/2 - (lines.size()-1) * line_height/2; - for (auto line : lines) { - sprite.drawString(line, display.width()/2, y); - y += line_height; - } - sprite.pushSprite(0, 0); -} - -void display_logo() { - clear_sprite(); - sprite.setSwapBytes(true); - sprite.pushImage(12, 30, 215, 76, OPERAME_LOGO); - sprite.pushSprite(0, 0); + sensor->set_zero(); // actually instantaneous + delay(15000); // give time to read long message } void display_ppm(int ppm) { @@ -106,148 +79,11 @@ void display_ppm(int ppm) { if (ppm >= co2_blink && millis() % 2000 < 1000) { std::swap(fg, bg); } - display_big(String(ppm), fg, bg); -} - -void panic(const String& message) { - display_big(message, TFT_RED); - delay(5000); - ESP.restart(); -} - -bool button(int pin) { - if (digitalRead(pin)) return false; - unsigned long start = millis(); - while (!digitalRead(pin)) { - if (millis() - start >= 50) display_big(""); - } - return millis() - start >= 50; -} - -void flush(Stream& s, int limit = 20) { - // .available() sometimes stays true (why?), hence the limit - - s.flush(); // flush output - while(s.available() && --limit) s.read(); // flush input -} - -int aqc_get_co2() { - static bool initialized = false; - - const uint8_t command[9] = { 0xff, 0x01, 0xc5, 0, 0, 0, 0, 0, 0x3a }; - uint8_t response[9]; - int co2 = -1; - - for (int attempt = 0; attempt < 3; attempt++) { - flush(hwserial1); - hwserial1.write(command, sizeof(command)); - delay(50); - - size_t c = hwserial1.readBytes(response, sizeof(response)); - if (c != sizeof(response) || response[0] != 0xff || response[1] != 0x86) { - continue; - } - uint8_t checksum = 255; - for (int i = 0; i < sizeof(response) - 1; i++) { - checksum -= response[i]; - } - if (response[8] == checksum) { - co2 = response[2] * 256 + response[3]; - break; - } - delay(50); - } - - if (co2 < 0) { - initialized = false; - return co2; - } - - if (!initialized && (co2 == 9999 || co2 == 400)) return 0; - initialized = true; - return co2; -} - -void aqc_set_zero() { - const uint8_t command[9] = { 0xff, 0x01, 0x87, 0, 0, 0, 0, 0, 0x78 }; - flush(hwserial1); - hwserial1.write(command, sizeof(command)); -} - -void mhz_setup() { - mhz.begin(hwserial1); - // mhz.setFilter(true, true); Library filter doesn't handle 0436 - mhz.autoCalibration(true); - char v[5] = {}; - mhz.getVersion(v); - v[4] = '\0'; - if (strcmp("0436", v) == 0) mhz_co2_init = 436; -} - -int mhz_get_co2() { - int co2 = mhz.getCO2(); - int unclamped = mhz.getCO2(false); - - if (mhz.errorCode != RESULT_OK) { - delay(500); - mhz_setup(); - return -1; - } - - // reimplement filter from library, but also checking for 436 because our - // sensors (firmware 0436, coincidence?) return that instead of 410... - if (unclamped == mhz_co2_init && co2 - unclamped >= 10) return 0; - - // No known sensors support >10k PPM (library filter tests for >32767) - if (co2 > 10000 || unclamped > 10000) return 0; - - return co2; -} - -void mhz_set_zero() { - mhz.calibrate(); -} - -int get_co2() { - // <0 means read error, 0 means still initializing, >0 is PPM value - - if (driver == AQC) return aqc_get_co2(); - if (driver == MHZ) return mhz_get_co2(); - - // Should be unreachable - panic(T.error_driver); - return -1; // suppress warning -} - -void set_zero() { - if (driver == AQC) { aqc_set_zero(); return; } - if (driver == MHZ) { mhz_set_zero(); return; } - - // Should be unreachable - panic(T.error_driver); -} - -void calibrate() { - auto lines = T.calibration; - for (int count = 60; count >= 0; count--) { - lines.back() = String(count); - display_lines(lines, TFT_RED); - unsigned long start = millis(); - while (millis() - start < 1000) { - if (button(pin_demobutton) || button(pin_portalbutton)) return; - } - } - - lines = T.calibrating; - if (driver == AQC) for (auto& line : lines) line.replace("400", "425"); - display_lines(lines, TFT_MAGENTA); - - set_zero(); // actually instantaneous - delay(15000); // give time to read long message + display(String(ppm), fg, bg); } void ppm_demo() { - display_big("demo!"); + display("demo!"); delay(3000); display_logo(); delay(1000); @@ -285,20 +121,15 @@ void check_demobutton() { if (button(pin_demobutton)) ppm_demo(); } -void check_buttons() { - check_portalbutton(); - check_demobutton(); -} - void setup_ota() { ArduinoOTA.setHostname(WiFiSettings.hostname.c_str()); ArduinoOTA.setPassword(WiFiSettings.password.c_str()); - ArduinoOTA.onStart( []() { display_big("OTA", TFT_BLUE); }); - ArduinoOTA.onEnd( []() { display_big("OTA done", TFT_GREEN); }); - ArduinoOTA.onError( [](ota_error_t e) { display_big("OTA failed", TFT_RED); }); + ArduinoOTA.onStart( []() { display("OTA", TFT_BLUE); }); + ArduinoOTA.onEnd( []() { display("OTA done", TFT_GREEN); }); + ArduinoOTA.onError( [](ota_error_t e) { display("OTA failed", TFT_RED); }); ArduinoOTA.onProgress([](unsigned int p, unsigned int t) { String pct { (int) ((float) p / t * 100) }; - display_big(pct + "%"); + display(pct + "%"); }); ArduinoOTA.begin(); } @@ -325,17 +156,17 @@ void setup() { Serial.println("Operame start"); digitalWrite(pin_backlight, HIGH); - display.init(); - display.fillScreen(TFT_BLACK); - display.setRotation(1); - sprite.createSprite(display.width(), display.height()); + tft.init(); + tft.fillScreen(TFT_BLACK); + tft.setRotation(1); + sprite.createSprite(tft.width(), tft.height()); OperameLanguage::select(T, LANGUAGE); if (!SPIFFS.begin(false)) { - display_lines(T.first_run, TFT_MAGENTA); + display(T.first_run, TFT_MAGENTA); if (!SPIFFS.format()) { - display_big(T.error_format, TFT_RED); + display(T.error_format, TFT_RED); delay(20*1000); } } @@ -351,7 +182,7 @@ void setup() { OperameLanguage::select(T, WiFiSettings.language); while (digitalRead(pin_pcb_ok)) { - display_big(T.error_module, TFT_RED); + display(T.error_module, TFT_RED); delay(1000); } @@ -360,17 +191,18 @@ void setup() { hwserial1.begin(9600, SERIAL_8N1, pin_sensor_rx, pin_sensor_tx); - if (aqc_get_co2() >= 0) { - driver = AQC; + sensor = new AQC(&hwserial1); + sensor->begin(); + if (sensor->get_co2() >= 0) { hwserial1.setTimeout(100); Serial.println("Using AQC driver."); } else { - driver = MHZ; - mhz_setup(); + delete sensor; + sensor = new MHZ(&hwserial1); + sensor->begin(); Serial.println("Using MHZ driver."); } - for (auto& str : T.portal_instructions[0]) { str.replace("{ssid}", WiFiSettings.hostname); } @@ -388,18 +220,18 @@ void setup() { String server = WiFiSettings.string("mqtt_server", 64, "", T.config_mqtt_server); int port = WiFiSettings.integer("mqtt_port", 0, 65535, 1883, T.config_mqtt_port); max_failures = WiFiSettings.integer("operame_max_failures", 0, 1000, 10, T.config_max_failures); - mqtt_topic = WiFiSettings.string("operame_mqtt_topic", WiFiSettings.hostname, T.config_mqtt_topic); + mqtt_topic = WiFiSettings.string("operame_mqtt_topic", WiFiSettings.hostname, T.config_mqtt_topic); mqtt_interval = 1000UL * WiFiSettings.integer("operame_mqtt_interval", 10, 3600, 60, T.config_mqtt_interval); mqtt_template = WiFiSettings.string("operame_mqtt_template", "{} PPM", T.config_mqtt_template); WiFiSettings.info(T.config_template_info); WiFiSettings.onConnect = [] { - display_big(T.connecting, TFT_BLUE); + display(T.connecting, TFT_BLUE); check_portalbutton(); return 50; }; WiFiSettings.onFailure = [] { - display_big(T.error_wifi, TFT_RED); + display(T.error_wifi, TFT_RED); delay(2000); }; static int portal_phase = 0; @@ -418,7 +250,7 @@ void setup() { if (WiFi.softAPgetStationNum() == 0) portal_phase = 0; else if (! portal_phase) portal_phase = 1; - display_lines(T.portal_instructions[portal_phase], TFT_WHITE, TFT_BLUE); + display(T.portal_instructions[portal_phase], TFT_WHITE, TFT_BLUE); if (portal_phase == 0 && millis() - portal_start > 10*60*1000) { panic(T.error_timeout); @@ -442,15 +274,15 @@ void loop() { static int co2; every(5000) { - co2 = get_co2(); + co2 = sensor->get_co2(); Serial.println(co2); } every(50) { if (co2 < 0) { - display_big(T.error_sensor, TFT_RED); + display(T.error_sensor, TFT_RED); } else if (co2 == 0) { - display_big(T.wait); + display(T.wait); } else { // some MH-Z19's go to 10000 but the display has space for 4 digits display_ppm(co2 > 9999 ? 9999 : co2); @@ -469,5 +301,7 @@ void loop() { } if (ota_enabled) ArduinoOTA.handle(); - check_buttons(); -} + + check_portalbutton(); + check_demobutton(); +} \ No newline at end of file diff --git a/src/sensors.h b/src/sensors.h new file mode 100644 index 0000000..d8ce92e --- /dev/null +++ b/src/sensors.h @@ -0,0 +1,113 @@ +#include +#include + +struct CO2Sensor { + int co2_zero; + virtual ~CO2Sensor() = default; + virtual void begin() = 0; + virtual void set_zero() = 0; + virtual int get_co2() = 0; + // <0 means read error, 0 means still initializing, >0 is PPM value +}; + +struct AQC : CO2Sensor { + int co2_zero = 425; + + Stream *serial; + AQC(Stream *x) : serial(x) {} + + void flush(int limit = 20) { + // .available() sometimes stays true (why?), hence the limit + + serial->flush(); // flush output + while(serial->available() && --limit) serial->read(); // flush input + } + + void begin() { } + + int get_co2() { + static bool initialized = false; + + const uint8_t command[9] = { 0xff, 0x01, 0xc5, 0, 0, 0, 0, 0, 0x3a }; + uint8_t response[9]; + int co2 = -1; + + for (int attempt = 0; attempt < 3; attempt++) { + flush(); + serial->write(command, sizeof(command)); + delay(50); + + size_t c = serial->readBytes(response, sizeof(response)); + if (c != sizeof(response) || response[0] != 0xff || response[1] != 0x86) { + continue; + } + uint8_t checksum = 255; + for (int i = 0; i < sizeof(response) - 1; i++) { + checksum -= response[i]; + } + if (response[8] == checksum) { + co2 = response[2] * 256 + response[3]; + break; + } + delay(50); + } + + if (co2 < 0) { + initialized = false; + return co2; + } + + if (!initialized && (co2 == 9999 || co2 == 400)) return 0; + initialized = true; + return co2; + } + + void set_zero() { + const uint8_t command[9] = { 0xff, 0x01, 0x87, 0, 0, 0, 0, 0, 0x78 }; + flush(); + serial->write(command, sizeof(command)); + } +}; + +struct MHZ : CO2Sensor { + MHZ19 mhz; + int co2_zero = 400; + int co2_init = 410; + + Stream *serial; + MHZ(Stream *x) : serial(x) {} + + void begin() { + mhz.begin(*serial); + // mhz.setFilter(true, true); Library filter doesn't handle 0436 + mhz.autoCalibration(true); + char v[5] = {}; + mhz.getVersion(v); + v[4] = '\0'; + if (strcmp("0436", v) == 0) co2_init = 436; + } + + int get_co2() { + int co2 = mhz.getCO2(); + int unclamped = mhz.getCO2(false); + + if (mhz.errorCode != RESULT_OK) { + delay(500); + setup(); + return -1; + } + + // reimplement filter from library, but also checking for 436 because our + // sensors (firmware 0436, coincidence?) return that instead of 410... + if (unclamped == co2_init && co2 - unclamped >= 10) return 0; + + // No known sensors support >10k PPM (library filter tests for >32767) + if (co2 > 10000 || unclamped > 10000) return 0; + + return co2; + } + + void set_zero() { + mhz.calibrate(); + } +}; \ No newline at end of file diff --git a/operame_strings.h b/src/strings.h similarity index 99% rename from operame_strings.h rename to src/strings.h index 1370aea..1889ff7 100644 --- a/operame_strings.h +++ b/src/strings.h @@ -1,3 +1,4 @@ +#include #include #include #include