1
0
mirror of https://github.com/revspace/operame synced 2024-10-31 21:47:30 +00:00
- 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
This commit is contained in:
Juerd Waalboer 2021-03-21 07:05:18 +01:00
parent 6ce64f635b
commit fa79001736
7 changed files with 231 additions and 221 deletions

2
.gitignore vendored
View File

@ -1,2 +1,2 @@
.pio .*
platformio-*.ini platformio-*.ini

62
src/display.h Normal file
View File

@ -0,0 +1,62 @@
#include <Arduino.h>
#include <WiFi.h>
#include <SPI.h>
#include <TFT_eSPI.h>
#include <src/logo.h>
#include <list>
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<String>& 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();
}

View File

View File

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@ -3,24 +3,17 @@
#include <MQTT.h> #include <MQTT.h>
#include <SPIFFS.h> #include <SPIFFS.h>
#include <WiFiSettings.h> #include <WiFiSettings.h>
#include <MHZ19.h>
#include <ArduinoOTA.h> #include <ArduinoOTA.h>
#include <SPI.h> #include <src/strings.h>
#include <TFT_eSPI.h> #include <src/display.h>
#include <logo.h> #include <src/sensors.h>
#include <list>
#include <operame_strings.h>
#define LANGUAGE "nl" #define LANGUAGE "nl"
OperameLanguage::Texts T; OperameLanguage::Texts T;
enum Driver { AQC, MHZ };
Driver driver;
MQTTClient mqtt; MQTTClient mqtt;
HardwareSerial hwserial1(1); HardwareSerial hwserial1(1);
TFT_eSPI display; CO2Sensor *sensor;
TFT_eSprite sprite(&display);
MHZ19 mhz;
const int pin_portalbutton = 35; const int pin_portalbutton = 35;
const int pin_demobutton = 0; const int pin_demobutton = 0;
@ -28,7 +21,6 @@ const int pin_backlight = 4;
const int pin_sensor_rx = 27; const int pin_sensor_rx = 27;
const int pin_sensor_tx = 26; const int pin_sensor_tx = 26;
const int pin_pcb_ok = 12; // pulled to GND by PCB trace 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 // Configuration via WiFiSettings
unsigned long mqtt_interval; unsigned long mqtt_interval;
@ -43,51 +35,32 @@ bool wifi_enabled;
bool mqtt_enabled; bool mqtt_enabled;
int max_failures; int max_failures;
void clear_sprite(int bg = TFT_BLACK) { bool button(int pin) {
sprite.fillSprite(bg); if (digitalRead(pin)) return false;
if (WiFi.status() == WL_CONNECTED) { unsigned long start = millis();
sprite.drawRect(0, 0, display.width(), display.height(), TFT_BLUE); 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) { void calibrate() {
clear_sprite(bg); auto lines = T.calibration;
sprite.setTextSize(1); for (int count = 60; count >= 0; count--) {
bool nondigits = false; lines.back() = String(count);
for (int i = 0; i < text.length(); i++) { display(lines, TFT_RED);
char c = text.charAt(i); unsigned long start = millis();
if (c < '0' || c > '9') nondigits = true; 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<String>& lines, int fg = TFT_WHITE, int bg = TFT_BLACK) { sensor->set_zero(); // actually instantaneous
clear_sprite(bg); delay(15000); // give time to read long message
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);
} }
void display_ppm(int ppm) { void display_ppm(int ppm) {
@ -106,148 +79,11 @@ void display_ppm(int ppm) {
if (ppm >= co2_blink && millis() % 2000 < 1000) { if (ppm >= co2_blink && millis() % 2000 < 1000) {
std::swap(fg, bg); std::swap(fg, bg);
} }
display_big(String(ppm), fg, bg); display(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
} }
void ppm_demo() { void ppm_demo() {
display_big("demo!"); display("demo!");
delay(3000); delay(3000);
display_logo(); display_logo();
delay(1000); delay(1000);
@ -285,20 +121,15 @@ void check_demobutton() {
if (button(pin_demobutton)) ppm_demo(); if (button(pin_demobutton)) ppm_demo();
} }
void check_buttons() {
check_portalbutton();
check_demobutton();
}
void setup_ota() { void setup_ota() {
ArduinoOTA.setHostname(WiFiSettings.hostname.c_str()); ArduinoOTA.setHostname(WiFiSettings.hostname.c_str());
ArduinoOTA.setPassword(WiFiSettings.password.c_str()); ArduinoOTA.setPassword(WiFiSettings.password.c_str());
ArduinoOTA.onStart( []() { display_big("OTA", TFT_BLUE); }); ArduinoOTA.onStart( []() { display("OTA", TFT_BLUE); });
ArduinoOTA.onEnd( []() { display_big("OTA done", TFT_GREEN); }); ArduinoOTA.onEnd( []() { display("OTA done", TFT_GREEN); });
ArduinoOTA.onError( [](ota_error_t e) { display_big("OTA failed", TFT_RED); }); ArduinoOTA.onError( [](ota_error_t e) { display("OTA failed", TFT_RED); });
ArduinoOTA.onProgress([](unsigned int p, unsigned int t) { ArduinoOTA.onProgress([](unsigned int p, unsigned int t) {
String pct { (int) ((float) p / t * 100) }; String pct { (int) ((float) p / t * 100) };
display_big(pct + "%"); display(pct + "%");
}); });
ArduinoOTA.begin(); ArduinoOTA.begin();
} }
@ -325,17 +156,17 @@ void setup() {
Serial.println("Operame start"); Serial.println("Operame start");
digitalWrite(pin_backlight, HIGH); digitalWrite(pin_backlight, HIGH);
display.init(); tft.init();
display.fillScreen(TFT_BLACK); tft.fillScreen(TFT_BLACK);
display.setRotation(1); tft.setRotation(1);
sprite.createSprite(display.width(), display.height()); sprite.createSprite(tft.width(), tft.height());
OperameLanguage::select(T, LANGUAGE); OperameLanguage::select(T, LANGUAGE);
if (!SPIFFS.begin(false)) { if (!SPIFFS.begin(false)) {
display_lines(T.first_run, TFT_MAGENTA); display(T.first_run, TFT_MAGENTA);
if (!SPIFFS.format()) { if (!SPIFFS.format()) {
display_big(T.error_format, TFT_RED); display(T.error_format, TFT_RED);
delay(20*1000); delay(20*1000);
} }
} }
@ -351,7 +182,7 @@ void setup() {
OperameLanguage::select(T, WiFiSettings.language); OperameLanguage::select(T, WiFiSettings.language);
while (digitalRead(pin_pcb_ok)) { while (digitalRead(pin_pcb_ok)) {
display_big(T.error_module, TFT_RED); display(T.error_module, TFT_RED);
delay(1000); delay(1000);
} }
@ -360,17 +191,18 @@ void setup() {
hwserial1.begin(9600, SERIAL_8N1, pin_sensor_rx, pin_sensor_tx); hwserial1.begin(9600, SERIAL_8N1, pin_sensor_rx, pin_sensor_tx);
if (aqc_get_co2() >= 0) { sensor = new AQC(&hwserial1);
driver = AQC; sensor->begin();
if (sensor->get_co2() >= 0) {
hwserial1.setTimeout(100); hwserial1.setTimeout(100);
Serial.println("Using AQC driver."); Serial.println("Using AQC driver.");
} else { } else {
driver = MHZ; delete sensor;
mhz_setup(); sensor = new MHZ(&hwserial1);
sensor->begin();
Serial.println("Using MHZ driver."); Serial.println("Using MHZ driver.");
} }
for (auto& str : T.portal_instructions[0]) { for (auto& str : T.portal_instructions[0]) {
str.replace("{ssid}", WiFiSettings.hostname); str.replace("{ssid}", WiFiSettings.hostname);
} }
@ -388,18 +220,18 @@ void setup() {
String server = WiFiSettings.string("mqtt_server", 64, "", T.config_mqtt_server); String server = WiFiSettings.string("mqtt_server", 64, "", T.config_mqtt_server);
int port = WiFiSettings.integer("mqtt_port", 0, 65535, 1883, T.config_mqtt_port); 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); 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_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); mqtt_template = WiFiSettings.string("operame_mqtt_template", "{} PPM", T.config_mqtt_template);
WiFiSettings.info(T.config_template_info); WiFiSettings.info(T.config_template_info);
WiFiSettings.onConnect = [] { WiFiSettings.onConnect = [] {
display_big(T.connecting, TFT_BLUE); display(T.connecting, TFT_BLUE);
check_portalbutton(); check_portalbutton();
return 50; return 50;
}; };
WiFiSettings.onFailure = [] { WiFiSettings.onFailure = [] {
display_big(T.error_wifi, TFT_RED); display(T.error_wifi, TFT_RED);
delay(2000); delay(2000);
}; };
static int portal_phase = 0; static int portal_phase = 0;
@ -418,7 +250,7 @@ void setup() {
if (WiFi.softAPgetStationNum() == 0) portal_phase = 0; if (WiFi.softAPgetStationNum() == 0) portal_phase = 0;
else if (! portal_phase) portal_phase = 1; 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) { if (portal_phase == 0 && millis() - portal_start > 10*60*1000) {
panic(T.error_timeout); panic(T.error_timeout);
@ -442,15 +274,15 @@ void loop() {
static int co2; static int co2;
every(5000) { every(5000) {
co2 = get_co2(); co2 = sensor->get_co2();
Serial.println(co2); Serial.println(co2);
} }
every(50) { every(50) {
if (co2 < 0) { if (co2 < 0) {
display_big(T.error_sensor, TFT_RED); display(T.error_sensor, TFT_RED);
} else if (co2 == 0) { } else if (co2 == 0) {
display_big(T.wait); display(T.wait);
} else { } else {
// some MH-Z19's go to 10000 but the display has space for 4 digits // some MH-Z19's go to 10000 but the display has space for 4 digits
display_ppm(co2 > 9999 ? 9999 : co2); display_ppm(co2 > 9999 ? 9999 : co2);
@ -469,5 +301,7 @@ void loop() {
} }
if (ota_enabled) ArduinoOTA.handle(); if (ota_enabled) ArduinoOTA.handle();
check_buttons();
} check_portalbutton();
check_demobutton();
}

113
src/sensors.h Normal file
View File

@ -0,0 +1,113 @@
#include <Arduino.h>
#include <MHZ19.h>
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();
}
};

View File

@ -1,3 +1,4 @@
#include <Arduino.h>
#include <map> #include <map>
#include <vector> #include <vector>
#include <list> #include <list>