diff --git a/humidifier_controller/humidifier_controller.ino b/humidifier_controller/humidifier_controller.ino index 2b733f8..ee1383e 100644 --- a/humidifier_controller/humidifier_controller.ino +++ b/humidifier_controller/humidifier_controller.ino @@ -24,18 +24,121 @@ TwoWire twowire; DFRobot_SHT20 sht20(&twowire); #endif -void fanOn() { - digitalWrite(7, HIGH); -} - -void fanOff() { - digitalWrite(7, LOW); -} - - 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); @@ -54,35 +157,358 @@ void setup() #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(&v)), ty(Ty::kInt) {} + constexpr Introspect(const char* n, float& v): name(n), val(reinterpret_cast(&v)), ty(Ty::kFloat) {} + constexpr Introspect(const char* n, bool& v): name(n), val(reinterpret_cast(&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(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(val); + *v = tmp; + return true; + } + case kBool: { + if (len < 1) { + return false; + } + bool* v = reinterpret_cast(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() { - while (Serial.available()) { + unsigned long now = millis(); + if ((now - cur.last_sample_time) > cur.sample_time_millis) { + cur.last_sample_time = now; + + if (now > (cur.manual_mode_ts_millis + cur.manual_mode_timeout_millis)) { + cur.manual_mode = false; + } + cur.readSamples(); + if (!cur.manual_mode) { + cur.controlHumidifier(); + cur.controlFan(); + printStatus(); + } + } + if (Serial.available()) { const int c = Serial.read(); if (c == 's') { - #ifdef USE_SHT30 - sht30.read(); - float humd = sht30.getHumidity(); - float temp = sht30.getTemperature(); -#else - float humd = sht20.readHumidity(); // Read Humidity - float temp = sht20.readTemperature(); // Read Temperature -#endif - Serial.print(humd); - Serial.print(","); - Serial.print(temp); - Serial.print(","); - Serial.print(digitalRead(PIN_RELAY4)); - Serial.print(","); - Serial.print(digitalRead(PIN_RELAY3)); - Serial.println(""); - } - if (c == 'z') digitalWrite(PIN_RELAY4, LOW); - if (c == 'Z') digitalWrite(PIN_RELAY4, HIGH); - if (c == 'y') digitalWrite(PIN_RELAY3, LOW); - if (c == 'Y') digitalWrite(PIN_RELAY3, HIGH); + 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 == 'z') humidifier(true); + else if (c == 'Z') humidifier(false); + else if (c == 'y') fan(true); + else if (c == 'Y') fan(false); + else if (c == 'd') dumpIntrospect(); } - delay(1); }