shrooms-server/humidifier_controller/humidifier_controller.ino

520 lines
13 KiB
C++

/*
modified on Apr 10, 2021
Modified by MehranMaleki from Arduino Examples
Home
*/
// this is using the HiLetgo 5V 4 Channel Relay Shield
// pinout is apparently 7,6,5,4
#define USE_SHT30
#ifdef USE_SHT30
#include "SHT85.h"
#define SHT85_ADDRESS 0x44
SHT85 sht30;
#else
#include <Wire.h>
#include "DFRobot_SHT20.h"
TwoWire twowire;
DFRobot_SHT20 sht20(&twowire);
#endif
const int PIN_RELAY4 = 4;
const int PIN_RELAY3 = 5;
void fan(bool state) {
int last_state = digitalRead(PIN_RELAY3);
digitalWrite(PIN_RELAY3, state);
if (last_state != state) {
Serial.print(F("f:"));
Serial.println(state);
}
}
void humidifier(bool state) {
int last_state = digitalRead(PIN_RELAY4);
digitalWrite(PIN_RELAY4, state);
if (last_state != state) {
Serial.print(F("h:"));
Serial.println(state);
}
}
constexpr int sample_sz = 20;
struct {
int sample_time_millis = 250;
unsigned long last_sample_time = 0;
float humd = 0.0f, temp = 0.0f;
int volt1, volt2; // corresponds to humdifier and fan state respectively
int manual_mode_ts_millis = 0;
int manual_mode_timeout_millis = 5000; // how long to wait to time out from manual mode
bool manual_mode = false;
struct {
// simple ring buffer to allow
float humd[sample_sz];
int towrite = 0;
bool started = false;
void add(float h) {
if (!started) {
for (auto& val: humd) {
val = h;
}
started = true;
} else {
humd[towrite] = h;
}
towrite = (towrite + 1) % sample_sz;
}
float readLast(int n) {
if (n >= sample_sz) {
n = sample_sz - 1;
}
if (n < 0) {
n = 0;
}
int idx = towrite - 1 - n;
while (idx < 0) {
idx += sample_sz;
}
return humd[idx];
}
} samples;
struct {
float upper = 0.87f;
float lower = 0.83f;
} thresh;
int fan_on_secs = 10;
int fan_cycle_secs = 5*60;
void readSamples() {
last_sample_time = millis();
/*
#ifdef USE_SHT30
sht30.read();
humd = sht30.getHumidity();
temp = sht30.getTemperature();
#else
humd = sht20.readHumidity(); // Read Humidity
temp = sht20.readTemperature(); // Read Temperature
#endif
*/
volt1 = digitalRead(PIN_RELAY4);
volt2 = digitalRead(PIN_RELAY3);
samples.add(humd);
}
float feedforward_coeff = 50;
void controlHumidifier() {
// TODO maybe use linear regression instead of just the last point
const int prev_idx = (samples.towrite - 2 + sample_sz) % sample_sz;
const float cur_h = humd;
const float prev_h = samples.readLast(1);
const float slope = (cur_h - prev_h) * feedforward_coeff;
const float comp_h = cur_h + slope;
if (cur_h < thresh.lower) {
humidifier(true);
}
if (cur_h > thresh.upper) {
humidifier(false);
}
}
void controlFan() {
unsigned long time_secs = millis() / 1000;
if ((time_secs % fan_cycle_secs) < fan_on_secs) {
fan(true);
} else {
fan(false);
}
}
} cur;
void setup()
{
pinMode(PIN_RELAY4, OUTPUT);
pinMode(PIN_RELAY3, OUTPUT);
pinMode(7, OUTPUT);
digitalWrite(7, LOW);
Serial.begin(115200);
#ifdef USE_SHT30
Wire.begin();
sht30.begin(SHT85_ADDRESS);
Wire.setClock(10000);
#else
twowire.setClock(10000);
sht20.initSHT20(); // Init SHT20 Sensor
#endif
delay(100);
//sht20.checkSHT20(); // Check SHT20 Sensor
cur.readSamples();
}
void printStatus() {
Serial.print(F("s:"));
Serial.print(cur.humd);
Serial.print(F(":"));
Serial.print(cur.temp);
Serial.print(F(":"));
Serial.print(cur.volt1);
Serial.print(F(":"));
Serial.print(cur.volt2);
Serial.println(F(""));
}
// this would be nice with AVR supported it..
struct Introspect {
public:
const char *name;
void* val;
enum Ty {
kInt,
kFloat,
kBool
};
Ty ty;
Introspect(): name(nullptr), val(nullptr), ty(Ty::kInt) {}
constexpr Introspect(const char* n, int& v): name(n), val(reinterpret_cast<void*>(&v)), ty(Ty::kInt) {}
constexpr Introspect(const char* n, float& v): name(n), val(reinterpret_cast<void*>(&v)), ty(Ty::kFloat) {}
constexpr Introspect(const char* n, bool& v): name(n), val(reinterpret_cast<void*>(&v)), ty(Ty::kBool) {}
void readVal() const {
switch (ty) {
case kInt: {
const int* v = val;
Serial.print(*v);
break;
}
case kFloat: {
const float* v = val;
Serial.print(*v);
break;
}
case kBool: {
const bool* v = val;
int val = *v;
Serial.print(val);
break;
}
}
}
bool writeVal(const char* buf, int len) {
switch (ty) {
case kInt: {
bool isnegative = false;
bool hasdigit = false;
int tmp = 0;
for (int i = 0; i < len; i++) {
if (i == 0 && buf[i] == '-') {
isnegative = true;
} else if (buf[i] >= '0' && buf[i] <= '9') {
tmp = 10*tmp + (buf[i] - '0');
hasdigit = true;
} else {
return false;
}
}
if (!hasdigit) {
return false;
}
if (isnegative) {
tmp = -tmp;
}
int* v = reinterpret_cast<int*>(val);
*v = tmp;
return true;
}
case kFloat: {
float tmp = 0.0f;
float factor = 0.1f;
int exponent = 0;
bool is_negative = false, has_digit = false;
bool using_exponent = false, has_exponent = false;
bool exp_is_negative = false;
enum State {
kPredecimal,
kPostdecimal, // after the decimal point
kExponent, // after the e
kExponentPostsign, // after the +/-
};
State state = kPredecimal;
for (int i = 0; i < len; i++) {
switch (state) {
case kPredecimal:
if (i == 0 && buf[i] == '-') {
is_negative = true;
} else if (buf[i] >= '0' && buf[i] <= '9') {
has_digit = true;
tmp = 10.0f * tmp + (buf[i] - '0');
} else if (buf[i] == '.') {
state = kPostdecimal;
} else if (buf[i] == 'e') {
state = kExponent;
using_exponent = true;
} else {
return false;
}
break;
case kPostdecimal:
if (buf[i] >= '0' && buf[i] <= '9') {
has_digit = true;
tmp = tmp + factor * (buf[i] - '0');
factor *= 0.1f;
} else if (buf[i] == 'e') {
state = kExponent;
using_exponent = true;
} else {
return false;
}
break;
case kExponent:
if (buf[i] == '+') {
state = kExponentPostsign;
} else if (buf[i] == '-') {
exp_is_negative = true;
state = kExponentPostsign;
} else if (buf[i] >= '0' && buf[i] <= '9') {
has_exponent = true;
exponent = (buf[i] - '0');
state = kExponentPostsign;
} else {
return false;
}
break;
case kExponentPostsign:
if (buf[i] >= '0' && buf[i] <= '9') {
has_exponent = true;
exponent = 10*exponent + (buf[i] - '0');
} else {
return false;
}
break;
}
}
if (!has_digit) return false;
if (using_exponent && !has_exponent) return false;
if (is_negative) {
tmp = -tmp;
}
if (has_exponent) {
if (exp_is_negative) {
for (int i = 0; i < exponent; i++) {
tmp *= 0.1f;
}
} else {
for (int i = 0; i < exponent; i++) {
tmp *= 10.f;
}
}
}
float* v = reinterpret_cast<float*>(val);
*v = tmp;
return true;
}
case kBool: {
if (len < 1) {
return false;
}
bool* v = reinterpret_cast<bool*>(val);
*v = (buf[0] != '0');
return true;
}
}
}
};
#define LIST_PROPS(x) \
x(sample_time_millis, cur.sample_time_millis) \
x(manual_mode_timeout_millis, cur.manual_mode_timeout_millis) \
x(manual_mode, cur.manual_mode) \
x(thresh_upper, cur.thresh.upper) \
x(thresh_lower, cur.thresh.lower) \
x(fan_on_secs, cur.fan_on_secs) \
x(fan_cycle_secs, cur.fan_cycle_secs) \
x(feedforward_coeff, cur.feedforward_coeff) \
#define DEFINE_STRS(n, var) const char n##_str[] PROGMEM = #n;
#define DEFINE_INTRO(n, var) {n##_str, var},
/*
const Introspect introspect_vals[] PROGMEM = {
{("cur_sample_time_millis"), cur.cur_sample_time_millis},
{("manual_mode_timeout_millis"), cur.manual_mode_timeout_millis},
{("manual_mode"), cur.manual_mode},
{("thresh.upper"), cur.thresh.upper},
{("thresh.lower"), cur.thresh.lower},
{("fan_on_secs"), cur.fan_on_secs},
{("fan_cycle_secs"), cur.fan_cycle_secs},
{("feedforward_coeff"), cur.feedforward_coeff},
};
*/
LIST_PROPS(DEFINE_STRS)
const Introspect introspect_vals[] PROGMEM = {
LIST_PROPS(DEFINE_INTRO)
};
bool match(const char* name, int len, Introspect& intro);
bool match(const char* name, int len, Introspect& intro) {
for (int i = 0; i < sizeof(introspect_vals)/sizeof(introspect_vals[0]); i++) {
memcpy_P((void*)&intro, (void*)&introspect_vals[i], sizeof(intro));
int intro_len = strlen_P(intro.name);
if (intro_len != len) {
continue;
}
if (strncmp_P(name, intro.name, len) == 0) {
return true;
}
}
return nullptr;
}
void dumpIntrospect() {
char buf[40];
Introspect intro;
for (int i = 0; i < sizeof(introspect_vals)/sizeof(introspect_vals[0]); i++) {
memcpy_P((void*)&intro, (void*)&introspect_vals[i], sizeof(intro));
Serial.print(F("d:"));
strncpy_P(buf, intro.name, sizeof(buf));
Serial.print(buf);
Serial.print(F(":"));
switch (intro.ty) {
case Introspect::kInt:
Serial.print(F("i"));
break;
case Introspect::kFloat:
Serial.print(F("f"));
break;
case Introspect::kBool:
Serial.print(F("b"));
break;
}
Serial.println(F(""));
}
Serial.println(F("d:done"));
return nullptr;
}
void loop()
{
unsigned long now = millis();
if (now > (cur.manual_mode_ts_millis + cur.manual_mode_timeout_millis)) {
cur.manual_mode = false;
}
if ((now - cur.last_sample_time) >= cur.sample_time_millis) {
cur.last_sample_time = now;
cur.readSamples();
if (!cur.manual_mode) {
cur.controlHumidifier();
cur.controlFan();
}
printStatus();
}
if (Serial.available()) {
const int c = Serial.read();
if (c == 's') {
printStatus();
} else if (c == 'r') {
char buf[40];
int len = Serial.readBytesUntil('\n', buf, sizeof(buf) - 1);
buf[len] = 0;
if (len == 0) {
Serial.println(F("r:"));
Serial.print(buf+1);
Serial.println(F(":invalid name"));
return;
}
Introspect tmp;
if (!match(buf+1, len-1, tmp)) {
Serial.println(F("r:"));
Serial.print(buf+1);
Serial.print(",");
Serial.print(len);
Serial.println(F(":no match"));
return;
}
Serial.print(F("r:"));
Serial.print(buf+1);
Serial.print(F(":"));
tmp.readVal();
Serial.println(F(""));
} else if (c == 'w') {
char buf[40];
int len = Serial.readBytesUntil('\n', buf, sizeof(buf) - 1);
buf[len] = 0;
if (len == 0) {
Serial.println(F("w:no name"));
return;
}
int colon_idx = -1;
for (int i = 1; i < len; i++) {
if (buf[i] == ':') {
colon_idx = i;
break;
}
}
if (colon_idx == -1) {
Serial.println(F("w:invalid command"));
return;
}
buf[colon_idx] = 0;
Introspect tmp;
if (!match(buf+1, colon_idx-1, tmp)) {
Serial.println(F("w:no match"));
return;
}
if (!tmp.writeVal(buf + colon_idx + 1, len - colon_idx - 1)) {
Serial.println(F("w:invalid value"));
return;
}
Serial.print(F("w:"));
Serial.print(buf+1);
Serial.print(F(":"));
tmp.readVal();
Serial.println(F(""));
} else if (c == 'H') humidifier(true);
else if (c == 'h') humidifier(false);
else if (c == 'F') fan(true);
else if (c == 'f') fan(false);
else if (c == 'M') {
cur.manual_mode = true;
cur.manual_mode_ts_millis = millis();
} else if (c == 'm') cur.manual_mode = false;
else if (c == 'd') dumpIntrospect();
}
}