Compare commits

...

10 Commits

23 changed files with 777 additions and 268 deletions

View File

@ -2,6 +2,9 @@
#define SENSOR3_PATRIX_H #define SENSOR3_PATRIX_H
#include "base.h" #include "base.h"
#include "config.h"
#include "console.h"
#include "log.h"
void patrixSetup(); void patrixSetup();

View File

@ -65,6 +65,34 @@ void configPutDouble(const char *name, double value) {
} }
} }
uint64_t configGetUint64_t(const char *name, uint64_t fallback) {
if (!config.containsKey(name)) {
return fallback;
}
return config[name];
}
void configPutUint64_t(const char *name, uint64_t value) {
if (config[name] != value) {
config[name] = value;
lastChangeMillis = millis();
}
}
uint8_t configGetUint8_t(const char *name, uint8_t fallback) {
if (!config.containsKey(name)) {
return fallback;
}
return config[name];
}
void configPutUint8_t(const char *name, uint8_t value) {
if (config[name] != value) {
config[name] = value;
lastChangeMillis = millis();
}
}
void configPrint() { void configPrint() {
info("Config (%s):", lastChangeMillis == 0 ? "PERSISTED" : "TRANSIENT"); info("Config (%s):", lastChangeMillis == 0 ? "PERSISTED" : "TRANSIENT");
for (JsonPair pair: config.as<JsonObject>()) { for (JsonPair pair: config.as<JsonObject>()) {

View File

@ -9,16 +9,27 @@ void configLoop();
void configReset(); void configReset();
void configLoaded();
String configGetString(const char *name, const char *fallback, bool allowEmpty); String configGetString(const char *name, const char *fallback, bool allowEmpty);
void configPutString(const char *name, const char *value); void configPutString(const char *name, const char *value);
double configGetDouble(const char *name, double fallback); double configGetDouble(const char *name, double fallback);
void configPutDouble(const char *name, double value); void configPutDouble(const char *name, double value);
uint64_t configGetUint64_t(const char *name, uint64_t fallback);
void configPutUint64_t(const char *name, uint64_t value);
uint8_t configGetUint8_t(const char *name, uint8_t fallback);
void configPutUint8_t(const char *name, uint8_t value);
void configPrint(); void configPrint();
#endif #endif

View File

@ -67,11 +67,11 @@ void consoleLoop() {
void consoleHandle(char *cmd) { void consoleHandle(char *cmd) {
char *first = strtok(cmd, " "); char *first = strtok(cmd, " ");
if (first == nullptr) { if (first == nullptr) {
_usage(); consolePrintUsage();
return; return;
} }
if (strcmp(first, "help") == 0) { if (strcmp(first, "help") == 0) {
_usage(); consolePrintUsage();
} else if (strcmp(first, "wifi") == 0) { } else if (strcmp(first, "wifi") == 0) {
_wifi(); _wifi();
} else if (strcmp(first, "mqtt") == 0) { } else if (strcmp(first, "mqtt") == 0) {
@ -84,12 +84,12 @@ void consoleHandle(char *cmd) {
_debug(); _debug();
} else if (strcmp(first, "config_reset") == 0) { } else if (strcmp(first, "config_reset") == 0) {
configReset(); configReset();
} else if (!patrix_command((char *) cmd)) { } else if (!patrixCommand((char *) cmd)) {
info("Unknown command: %s", cmd); info("Unknown command: %s", cmd);
} }
} }
void _usage() { void consolePrintUsage() {
info(R"(USAGE: info(R"(USAGE:
help help
info info
@ -120,7 +120,7 @@ void _reboot() {
void _mqtt() { void _mqtt() {
char *sub = strtok(nullptr, " "); char *sub = strtok(nullptr, " ");
if (sub == nullptr) { if (sub == nullptr) {
_usage(); consolePrintUsage();
return; return;
} }
if (strcmp(sub, "reconnect") == 0) { if (strcmp(sub, "reconnect") == 0) {
@ -134,7 +134,7 @@ void _mqtt() {
void _wifi() { void _wifi() {
char *sub = strtok(nullptr, " "); char *sub = strtok(nullptr, " ");
if (sub == nullptr) { if (sub == nullptr) {
_usage(); consolePrintUsage();
return; return;
} }
if (strcmp(sub, "reconnect") == 0) { if (strcmp(sub, "reconnect") == 0) {
@ -150,7 +150,7 @@ void _wifi() {
void _setConfigString(const char *name, bool allowEmpty) { void _setConfigString(const char *name, bool allowEmpty) {
char *value = strtok(nullptr, ""); char *value = strtok(nullptr, "");
if (value == nullptr) { if (value == nullptr) {
_usage(); consolePrintUsage();
return; return;
} }
if (!allowEmpty && strcmp(value, "") == 0) { if (!allowEmpty && strcmp(value, "") == 0) {
@ -248,3 +248,49 @@ const char *getFlashChipMode() {
return "[???]"; return "[???]";
} }
} }
bool cmdReadDouble(const char *name, double *destinationPtr, double min, double max) {
const char *valueStr = strtok(nullptr, "");
if (valueStr == nullptr) {
consolePrintUsage();
return false;
}
double value = strtod(valueStr, nullptr);
if (isnan(value)) {
error("Failed to parse double for \"%s\": %s", name, valueStr);
return false;
}
if ((!isnan(min) && value < min) || (!isnan(max) && value > max)) {
error("Value out of range for \"%s\" [%f..%f]: %f", name, min, max, value);
return false;
}
if (*destinationPtr != value) {
*destinationPtr = value;
info("Value for \"%s\" set to: %f", name, value);
configPutDouble(name, value);
} else {
info("Value for \"%s\" unchanged: %f", name, value);
}
return true;
}
bool cmdReadU64(const char *name, uint64_t *destinationPtr, uint64_t min, uint64_t max) {
const char *valueStr = strtok(nullptr, "");
if (valueStr == nullptr) {
consolePrintUsage();
return false;
}
uint64_t value = strtoll(valueStr, nullptr, 10);
if ((min > 0 && value < min) || (max > 0 && value > max)) {
error("Value out of range for \"%s\" [%f..%f]: %f", name, min, max, value);
return false;
}
if (*destinationPtr != value) {
*destinationPtr = value;
info("Value for \"%s\" set to: %f", name, value);
configPutUint64_t(name, value);
} else {
info("Value for \"%s\" unchanged: %f", name, value);
}
return true;
}

View File

@ -1,14 +1,20 @@
#ifndef SENSOR3_CONSOLE_H #ifndef SENSOR3_CONSOLE_H
#define SENSOR3_CONSOLE_H #define SENSOR3_CONSOLE_H
#include <cstdint>
void consoleSetup(); void consoleSetup();
void consoleLoop(); void consoleLoop();
void consoleHandle(char *cmd); void consoleHandle(char *cmd);
bool patrix_command(char *cmd); bool patrixCommand(char *cmd);
void _usage(); void consolePrintUsage();
bool cmdReadDouble(const char *name, double *destinationPtr, double min, double max);
bool cmdReadU64(const char *name, uint64_t *destinationPtr, uint64_t min, uint64_t max);
#endif #endif

View File

@ -3,9 +3,17 @@
#include "base.h" #include "base.h"
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include "wifi.h"
#include "mqtt.h"
struct IData { struct IData {
time_t time;
explicit IData(time_t time) : time(time) {
// -
}
virtual void toJson(JsonObject &json) const = 0; virtual void toJson(JsonObject &json) const = 0;
}; };
@ -18,16 +26,13 @@ class Cache {
private: private:
struct Entry { T buffer[size];
time_t timestamp = 0;
T data;
};
Entry buffer[size]; T *bufferRead = buffer;
Entry *bufferRead = buffer; T *bufferWrite = buffer;
Entry *bufferWrite = buffer; T last{};
size_t usage = 0; size_t usage = 0;
@ -38,25 +43,26 @@ public:
// - // -
} }
bool add(const time_t timestamp, const T &data) { bool add(T data) {
if (usage >= size) { if (usage < size && data.needsPublish(last)) {
return false; last = data;
} *bufferWrite = data;
bufferWrite->timestamp = timestamp;
bufferWrite->data = data;
bufferWrite = (bufferWrite - buffer + 1) % size + buffer; bufferWrite = (bufferWrite - buffer + 1) % size + buffer;
usage++; usage++;
return true; return true;
} }
return false;
}
void loop() { void loop() {
if (usage == 0 || !isTimeSet()) { if (usage == 0 || !isTimeSet()) {
return; return;
} }
JsonDocument json; JsonDocument doc;
json["timestamp"] = correctTime(bufferRead->timestamp); JsonObject json = doc.to<JsonObject>();
JsonObject data = json["data"].to<JsonObject>(); correctTime(bufferRead->time);
bufferRead->data.toJson(data); json["timestamp"] = bufferRead->time;
bufferRead->toJson(json);
if (mqttPublishData(json)) { if (mqttPublishData(json)) {
bufferRead = (bufferRead - buffer + 1) % size + buffer; bufferRead = (bufferRead - buffer + 1) % size + buffer;
usage--; usage--;

28
lib/patrix/sensors/DHT.h Normal file
View File

@ -0,0 +1,28 @@
#ifndef SENSOR3_DHT_H
#define SENSOR3_DHT_H
#include "base.h"
class DHT {
private:
const int pin;
public:
explicit DHT(const int pin) : pin(pin) {
// nothing
}
void begin() {
// TODO
}
void loop(float *temperatureCelsius, uint8_t *humidityRelativePercent, float *humidityAbsoluteMgL) {
// TODO
}
};
#endif

View File

@ -15,36 +15,22 @@ private:
uint64_t address; uint64_t address;
const double valueThreshold;
const time_t timeoutSec; const time_t timeoutSec;
const time_t minIntervalMillis;
double lastSentValue = NAN;
unsigned long lastSentMillis = 0;
double lastValue = NAN; double lastValue = NAN;
time_t lastTimestamp = 0; time_t lastTimestamp = 0;
double lastValidValue = NAN;
time_t lastValidTimestamp = 0;
public: public:
DallasSensor(Dallas &sensors, const uint64_t address, const double valueThreshold, const time_t timeoutSec, const time_t minIntervalMillis) : DallasSensor(Dallas &sensors, const uint64_t address, const time_t timeoutSec) :
sensors(sensors), sensors(sensors),
address(address), address(address),
valueThreshold(valueThreshold), timeoutSec(timeoutSec) {
timeoutSec(timeoutSec),
minIntervalMillis(minIntervalMillis) {
// - // -
} }
bool loop() { void loop() {
const time_t now = getTime(); const time_t now = getTime();
if (now - lastTimestamp > timeoutSec) { if (now - lastTimestamp > timeoutSec) {
lastTimestamp = 0; lastTimestamp = 0;
@ -53,21 +39,9 @@ public:
if (sensors.isConverted()) { if (sensors.isConverted()) {
const double value = sensors.read(address); const double value = sensors.read(address);
const time_t timestamp = sensors.getTimestamp(); const time_t timestamp = sensors.getTimestamp();
const unsigned long millisNow = millis();
const bool doPublish = !isnan(value) && (abs(lastSentValue - value) >= valueThreshold || millisNow - lastSentMillis >= minIntervalMillis);
if (doPublish) {
lastSentValue = value;
lastSentMillis = millisNow;
}
lastValue = value; lastValue = value;
lastTimestamp = timestamp; lastTimestamp = timestamp;
if (!isnan(value)) {
lastValidValue = value;
lastValidTimestamp = timestamp;
} }
return doPublish;
}
return false;
} }
[[nodiscard]] double getLastValue() const { [[nodiscard]] double getLastValue() const {
@ -78,14 +52,6 @@ public:
return lastTimestamp; return lastTimestamp;
} }
[[nodiscard]] double getLastValidValue() const {
return lastValidValue;
}
[[nodiscard]] time_t getLastValidTimestamp() const {
return lastValidTimestamp;
}
}; };
#endif #endif

View File

@ -0,0 +1,22 @@
#ifndef SENSOR3_MOISTURE_H
#define SENSOR3_MOISTURE_H
class Moisture {
private:
const int pin;
public:
Moisture(const int pin) : pin(pin) {
// TODO
}
void loop(float *value) {
// TODO
}
};
#endif

View File

@ -0,0 +1,78 @@
#ifndef SENSOR3_OUTPUT_H
#define SENSOR3_OUTPUT_H
#include "base.h"
class Output {
private:
const int pin;
const bool invert;
uint64_t onSince = 0;
uint64_t offSince = 0;
uint64_t totalOnMillis = 0;
public:
Output(const int pin, const bool invert, const bool initial) : pin(pin), invert(invert) {
pinMode(pin, OUTPUT);
set(initial);
}
void loop() {
uint64_t now = getUptimeMillis();
static uint64_t last = now;
if (isOn()) {
totalOnMillis += now - last;
}
}
[[nodiscard]] bool isOn() const {
return (digitalRead(pin) == HIGH) ^ invert;
}
void on() {
set(true);
}
void off() {
set(false);
}
void set(const bool newState) {
if (isOn() != newState) {
digitalWrite(pin, newState ^ invert ? HIGH : LOW);
if (newState) {
onSince = getUptimeMillis();
} else {
offSince = getUptimeMillis();
}
}
}
[[nodiscard]] uint64_t getOnSince() const {
if (!isOn()) {
return 0;
}
return getUptimeMillis() - onSince;
}
[[nodiscard]] uint64_t getOffSince() const {
if (isOn()) {
return 0;
}
return getUptimeMillis() - offSince;
}
[[nodiscard]] uint64_t getTotalOnMillis() const {
return totalOnMillis;
}
};
#endif

View File

@ -195,18 +195,10 @@ time_t getTime() {
return epochSeconds; return epochSeconds;
} }
time_t correctTime(const time_t value) { void correctTime(time_t &value) {
if (!timeSet) { if (timeSet && value < MIN_EPOCH_SECONDS) {
return value; value = getTime() - preTimeOffset + value;
} }
if (value < MIN_EPOCH_SECONDS) {
return getTime() - preTimeOffset + value;
}
return value;
}
bool isOlderThan(time_t time, time_t seconds) {
return getTime() > correctTime(time) + seconds;
} }
void getDateTime(char *buffer, size_t size) { void getDateTime(char *buffer, size_t size) {

View File

@ -15,9 +15,7 @@ bool isTimeSet();
time_t getTime(); time_t getTime();
time_t correctTime(time_t value); void correctTime(time_t &value);
bool isOlderThan(time_t time, time_t seconds);
void getDateTime(char *buffer, size_t size); void getDateTime(char *buffer, size_t size);

View File

@ -16,6 +16,7 @@ lib_deps = https://github.com/milesburton/Arduino-Temperature-Control-Library
Wire Wire
https://github.com/phassel/ArduPID/ https://github.com/phassel/ArduPID/
https://github.com/wayoda/LedControl https://github.com/wayoda/LedControl
https://github.com/me-no-dev/ESPAsyncWebServer
[env:TEST32] [env:TEST32]
upload_port = 10.42.0.66 upload_port = 10.42.0.66
@ -30,7 +31,7 @@ monitor_port = ${COMMON.monitor_port}
monitor_speed = ${COMMON.monitor_speed} monitor_speed = ${COMMON.monitor_speed}
monitor_filters = ${COMMON.monitor_filters} monitor_filters = ${COMMON.monitor_filters}
lib_deps = ${COMMON.lib_deps} lib_deps = ${COMMON.lib_deps}
build_flags = -D TEST32 -D HOSTNAME=\"TEST32\" -D WIFI_SSID=\"${COMMON.WIFI_SSID}\" -D WIFI_PKEY=\"${COMMON.WIFI_PKEY}\" -D OTA_PASSWORD=\"OtaAuthPatrixTEST32\" -D BOOT_DELAY=false -D DEBUG_LOG=false build_flags = -D TEST32 -D HOSTNAME=\"TEST32\" -D WIFI_SSID=\"${COMMON.WIFI_SSID}\" -D WIFI_PKEY=\"${COMMON.WIFI_PKEY}\" -D OTA_PASSWORD=\"OtaAuthPatrixTEST32\" -D BOOT_DELAY=false -D DEBUG_LOG=true
[env:TEST8266] [env:TEST8266]
upload_port = 10.0.0.162 upload_port = 10.0.0.162
@ -60,4 +61,19 @@ monitor_port = ${COMMON.monitor_port}
monitor_speed = ${COMMON.monitor_speed} monitor_speed = ${COMMON.monitor_speed}
monitor_filters = esp8266_exception_decoder monitor_filters = esp8266_exception_decoder
lib_deps = ${COMMON.lib_deps} lib_deps = ${COMMON.lib_deps}
build_flags = -D FERMENTER -D HOSTNAME=\"Fermenter\" -D WIFI_SSID=\"${COMMON.WIFI_SSID}\" -D WIFI_PKEY=\"${COMMON.WIFI_PKEY}\" -D OTA_PASSWORD=\"OtaAuthPatrixFermenter\" -D BOOT_DELAY=true -D DEBUG_LOG=true build_flags = -D FERMENTER -D HOSTNAME=\"Fermenter\" -D WIFI_SSID=\"${COMMON.WIFI_SSID}\" -D WIFI_PKEY=\"${COMMON.WIFI_PKEY}\" -D OTA_PASSWORD=\"OtaAuthPatrixFermenter\" -D BOOT_DELAY=true -D DEBUG_LOG=false
[env:Greenhouse]
;upload_port = 10.0.0.
;upload_flags = --auth=OtaAuthPatrixGreenhouse
;upload_protocol = ${COMMON.ota_protocol}
upload_port = ${COMMON.usb_port}
upload_speed = ${COMMON.usb_speed}
platform = espressif8266
board = esp12e
framework = ${COMMON.framework}
monitor_port = ${COMMON.monitor_port}
monitor_speed = ${COMMON.monitor_speed}
monitor_filters = esp8266_exception_decoder
lib_deps = ${COMMON.lib_deps}
build_flags = -D GREENHOUSE -D HOSTNAME=\"Greenhouse\" -D WIFI_SSID=\"${COMMON.WIFI_SSID}\" -D WIFI_PKEY=\"${COMMON.WIFI_PKEY}\" -D OTA_PASSWORD=\"OtaAuthPatrixGreenhouse\" -D BOOT_DELAY=true -D DEBUG_LOG=false

View File

@ -0,0 +1,29 @@
#include "FementerDisplay.h"
#include "LedControl.h"
LedControl lc = LedControl(13, 14, 15, 1);
void writeDecimal(int *digit, double value) {
const int integer = (int) value;
const int decimal = (int) ((value - integer) * 10) % 10;
lc.setDigit(0, (*digit)++, decimal, false);
lc.setDigit(0, (*digit)++, integer % 10, true);
lc.setDigit(0, (*digit)++, integer / 10 % 10, false);
}
void displayLoop(double temperature, double target) {
static unsigned long lastDisplayInit = 0;
if (lastDisplayInit == 0 || millis() - lastDisplayInit > 60 * 60 * 1000) {
lastDisplayInit = millis();
lc.shutdown(0, true);
lc.shutdown(0, false);
lc.setIntensity(0, 4);
lc.clearDisplay(0);
}
int digit = 0;
writeDecimal(&digit, temperature);
digit++;
digit++;
writeDecimal(&digit, target);
}

View File

@ -0,0 +1,6 @@
#ifndef SENSOR3_FEMENTERDISPLAY_H
#define SENSOR3_FEMENTERDISPLAY_H
void displayLoop(double temperature, double target);
#endif

View File

@ -1,192 +1,38 @@
#if defined(FERMENTER) || defined(TEST8266) #if defined(FERMENTER) || defined(TEST8266)
#include <Patrix.h> #include <Patrix.h>
#include "sensors/Dallas.h"
#include "sensors/DallasSensor.h"
#include "ArduPID.h"
#include "config.h"
#include "console.h"
#include <Arduino.h>
#include "LedControl.h"
#include "FermenterData.h" #include "FermenterData.h"
#include "FementerDisplay.h"
#include "FermenterPID.h"
#include "FermenterSensor.h"
#define SENSOR_GPIO D4 Cache<FermenterData, 800> cache;
#define CONTROL_GPIO D2
#define CONTROL_PWM_BITS 10
#define CONTROL_PWM_MAX (pow(2, CONTROL_PWM_BITS) - 1)
#define PID_DEFAULT_P 1500
#define PID_DEFAULT_I 0
#define PID_DEFAULT_D 0
#define PID_DEFAULT_TARGET 31
#define PID_DEFAULT_OVER 5
Dallas dallas(SENSOR_GPIO);
DallasSensor sensor(dallas, 0x3D0417C1D740FF28, 0.1, 5, 60 * 1000);
LedControl lc = LedControl(13, 14, 15, 1);
ArduPID pid;
double proportional = PID_DEFAULT_P;
double integral = PID_DEFAULT_I;
double derivative = PID_DEFAULT_D;
double temperatureTarget = PID_DEFAULT_TARGET;
double temperatureOver = PID_DEFAULT_OVER;
double temperatureCurrent = NAN;
double heaterPWM = 0;
Cache<FermenterData, 500> cache;
void writeDecimal(int *digit, double value);
void displayLoop();
void pidLoop();
void patrixSetup() { void patrixSetup() {
dallas.begin(); sensorSetup();
analogWriteResolution(CONTROL_PWM_BITS); pidSetup(&temperature);
configLoaded();
pid.begin(&temperatureCurrent, &heaterPWM, &temperatureTarget, proportional, integral, derivative);
pid.setOutputLimits(0, CONTROL_PWM_MAX);
pid.start();
} }
void patrixLoop() { void patrixLoop() {
dallas.loop(); sensorLoop();
if (sensor.loop()) {
const FermenterData data = {sensor.getLastValue(), temperatureTarget, proportional, integral, derivative};
cache.add(sensor.getLastTimestamp(), data);
}
cache.loop();
displayLoop();
pidLoop(); pidLoop();
displayLoop(temperature, target);
cache.add({getTime(), (float) temperature, (float) target, getHeaterPercent(), (float) proportional, (float) integral, (float) derivative});
cache.loop();
} }
void pidLoop() { bool patrixCommand(char *first) {
temperatureCurrent = sensor.getLastValue();
if (!isnan(temperatureCurrent)) {
pid.compute();
} else {
heaterPWM = 0;
}
const char *emergencyStr = "";
if (heaterPWM > 0 && temperatureCurrent > temperatureTarget + temperatureOver) {
heaterPWM = 0;
emergencyStr = "[EMERGENCY CUTOFF]";
}
analogWrite(CONTROL_GPIO, (int) round(heaterPWM));
static unsigned long lastDebug = 0;
unsigned long now = millis();
if (now - lastDebug >= 1000) {
lastDebug = now;
int heaterPercent = (int) round(100.0 * heaterPWM / CONTROL_PWM_MAX);
debug(
"cache: %3d/%d | p: %f | i: %f | d: %f | target: %4.1f | heater: %3d%% | temperature: %5.2f %s",
cache.getUsage(),
cache.getSize(),
proportional == 0 ? NAN : proportional,
integral == 0 ? NAN : integral,
derivative == 0 ? NAN : derivative,
temperatureTarget,
heaterPercent,
temperatureCurrent,
emergencyStr
);
}
}
void displayLoop() {
static unsigned long lastDisplayInit = 0;
if (lastDisplayInit == 0 || millis() - lastDisplayInit > 60 * 60 * 1000) {
lastDisplayInit = millis();
lc.shutdown(0, true);
lc.shutdown(0, false);
lc.setIntensity(0, 4);
lc.clearDisplay(0);
}
int digit = 0;
writeDecimal(&digit, temperatureCurrent);
digit++;
digit++;
writeDecimal(&digit, temperatureTarget);
}
void writeDecimal(int *digit, double value) {
const int integer = (int) value;
const int decimal = (int) ((value - integer) * 10) % 10;
lc.setDigit(0, (*digit)++, decimal, false);
lc.setDigit(0, (*digit)++, integer % 10, true);
lc.setDigit(0, (*digit)++, integer / 10 % 10, false);
}
bool setDouble(const char *name, double *destinationPtr, double min, double max) {
const char *valueStr = strtok(nullptr, "");
if (valueStr == nullptr) {
_usage();
return false;
}
double value = strtod(valueStr, nullptr);
if (isnan(value)) {
error("Failed to parse double for \"%s\": %s", name, valueStr);
return false;
}
if ((!isnan(min) && value < min) || (!isnan(max) && value > max)) {
error("Value out of range for \"%s\" [%f..%f]: %f", name, min, max, value);
return false;
}
if (*destinationPtr != value) {
*destinationPtr = value;
info("Value for \"%s\" set to: %f", name, value);
configPutDouble(name, value);
} else {
info("Value for \"%s\" unchanged: %f", name, value);
}
return true;
}
bool patrix_command(char *first) {
bool result = false; bool result = false;
if (strcmp(first, "over") == 0 || strcmp(first, "o") == 0) { if (strcmp(first, "p") == 0) {
result = setDouble("over", &temperatureOver, 0, 20); result = cmdReadDouble("p", &proportional, 0, NAN);
} else if (strcmp(first, "target") == 0 || strcmp(first, "t") == 0) { } else if (strcmp(first, "i") == 0) {
result = setDouble("target", &temperatureTarget, -10, 60); result = cmdReadDouble("i", &integral, 0, NAN);
} else if (strcmp(first, "proportional") == 0 || strcmp(first, "p") == 0) { } else if (strcmp(first, "d") == 0) {
result = setDouble("p", &proportional, NAN, NAN); result = cmdReadDouble("d", &derivative, 0, NAN);
} else if (strcmp(first, "integral") == 0 || strcmp(first, "i") == 0) { } else if (strcmp(first, "t") == 0) {
result = setDouble("i", &integral, NAN, NAN); result = cmdReadDouble("t", &target, -10, 60);
} else if (strcmp(first, "derivative") == 0 || strcmp(first, "d") == 0) {
result = setDouble("d", &derivative, NAN, NAN);
}
if (result) {
pid.setCoefficients(proportional, integral, derivative);
} }
return result; return result;
} }
void configLoaded() {
proportional = configGetDouble("p", PID_DEFAULT_P);
integral = configGetDouble("i", PID_DEFAULT_I);
derivative = configGetDouble("d", PID_DEFAULT_D);
temperatureTarget = configGetDouble("target", PID_DEFAULT_TARGET);
temperatureOver = configGetDouble("over", PID_DEFAULT_OVER);
pid.setCoefficients(proportional, integral, derivative);
}
#endif #endif

View File

@ -1,35 +1,65 @@
#ifndef SENSOR3_FERMENTERDATA_H #ifndef SENSOR3_FERMENTER_DATA_H
#define SENSOR3_FERMENTERDATA_H #define SENSOR3_FERMENTER_DATA_H
#include "data.h" #include "data.h"
struct FermenterData : IData { struct FermenterData : IData {
double temperature; float currentCelsius;
double target; float targetCelsius;
double p; uint8_t heaterPercent;
double i; float p;
double d; float i;
float d;
FermenterData() { FermenterData() : IData(0) {
temperature = NAN; currentCelsius = NAN;
target = NAN; targetCelsius = NAN;
heaterPercent = 0;
p = NAN; p = NAN;
i = NAN; i = NAN;
d = NAN; d = NAN;
} }
FermenterData(double temperature, double target, double p, double i, double d) : temperature(temperature), target(target), p(p), i(i), d(d) { FermenterData(
time_t time,
float currentCelsius,
float targetCelsius,
uint8_t heaterPercent,
float p,
float i,
float d
) :
IData(time),
currentCelsius(currentCelsius),
targetCelsius(targetCelsius),
heaterPercent(heaterPercent),
p(p),
i(i),
d(d) {
// - // -
} }
void toJson(JsonObject &json) const override { void toJson(JsonObject &json) const override {
json["temperature"] = temperature; json["currentCelsius"] = currentCelsius;
json["target"] = target; json["targetCelsius"] = targetCelsius;
json["heaterPercent"] = heaterPercent;
json["p"] = p; json["p"] = p;
json["i"] = i; json["i"] = i;
json["d"] = d; json["d"] = d;
} }
[[nodiscard]] bool needsPublish(FermenterData &last) {
correctTime(time);
correctTime(last.time);
return time - last.time >= 60
|| this->p != last.p
|| this->i != last.i
|| this->d != last.d
|| abs(this->currentCelsius - last.currentCelsius) >= 0.1
|| this->targetCelsius != last.targetCelsius
|| this->heaterPercent != last.heaterPercent;
}
}; };
#endif #endif

View File

@ -0,0 +1,62 @@
#include "FermenterPID.h"
#include "log.h"
#include "config.h"
#define CONTROL_GPIO D2
#define CONTROL_PWM_BITS 10
#define CONTROL_PWM_MAX (pow(2, CONTROL_PWM_BITS) - 1)
ArduPID pid;
double proportional = 0;
double integral = 0;
double derivative = 0;
double target = 0;
double heater = 0;
void pidSetup(double *temperature) {
analogWriteResolution(CONTROL_PWM_BITS);
proportional = configGetDouble("p", 0);
integral = configGetDouble("i", 0);
derivative = configGetDouble("d", 0);
target = configGetDouble("t", 0);
pid.begin(temperature, &heater, &target, proportional, integral, derivative);
pid.setOutputLimits(0, CONTROL_PWM_MAX);
}
void pidLoop() {
if (!isnan(*pid.input)) {
pid.setCoefficients(proportional, integral, derivative);
pid.compute();
} else {
heater = 0;
}
analogWrite(CONTROL_GPIO, (int) round(heater));
static unsigned long lastDebug = 0;
unsigned long now = millis();
if (now - lastDebug >= 1000) {
lastDebug = now;
debug(
"p: %f | i: %.12f | d: %.12f | target: %4.1f | heater: %3d%% | temperature: %5.2f",
proportional == 0 ? NAN : proportional,
integral == 0 ? NAN : integral,
derivative == 0 ? NAN : derivative,
*pid.setpoint,
getHeaterPercent(),
*pid.input
);
}
}
uint8_t getHeaterPercent() {
return (int) round(100.0 * heater / CONTROL_PWM_MAX);
}

View File

@ -0,0 +1,22 @@
#ifndef SENSOR3_FERMENTERPID_H
#define SENSOR3_FERMENTERPID_H
#include "ArduPID.h"
extern ArduPID pid;
extern double proportional;
extern double integral;
extern double derivative;
extern double target;
void pidSetup(double *temperature);
void pidLoop();
uint8_t getHeaterPercent();
#endif

View File

@ -0,0 +1,22 @@
#include "FermenterSensor.h"
#include "sensors/Dallas.h"
#include "sensors/DallasSensor.h"
#define SENSOR_GPIO D4
Dallas dallas(SENSOR_GPIO);
DallasSensor sensor(dallas, 0x3D0417C1D740FF28, 5);
double temperature = NAN;
void sensorSetup() {
dallas.begin();
}
void sensorLoop() {
dallas.loop();
sensor.loop();
temperature = sensor.getLastValue();
}

View File

@ -0,0 +1,10 @@
#ifndef SENSOR3_FERMENTERSENSOR_H
#define SENSOR3_FERMENTERSENSOR_H
extern double temperature;
void sensorSetup();
void sensorLoop();
#endif

View File

@ -0,0 +1,187 @@
#if defined(GREENHOUSE)
#include <Patrix.h>
#include "GreenhouseData.h"
#include "sensors/DHT.h"
#include "sensors/Output.h"
#include "sensors/Moisture.h"
#define INSIDE_DHT_PIN 4
#define MOISTURE_PIN 5
#define OUTSIDE_DHT_PIN 6
#define VENTILATOR_PIN 7
#define SPRINKLER_PIN 8
#define TEMPERATURE_HIGH 35
#define TEMPERATURE_LOW 32
#define TEMPERATURE_MILLIS (5 * 60 * 1000)
#define MOISTURE_HIGH 70
#define MOISTURE_LOW 20
#define MOISTURE_MILLIS (5 * 60 * 1000)
GreenhouseData data{};
Cache<GreenhouseData, 800> cache;
DHT insideDHT(INSIDE_DHT_PIN);
Moisture insideMoisture(MOISTURE_PIN);
DHT outsideDHT(OUTSIDE_DHT_PIN);
Output ventilator(VENTILATOR_PIN, true, false);
Output sprinkler(SPRINKLER_PIN, true, false);
double tempHigh = 35;
double tempLow = 32;
uint64_t tempMillis = 5 * 60 * 1000;
uint64_t tempHighSince = 0;
uint64_t tempLowSince = 0;
double moistHigh = 35;
double moistLow = 32;
uint64_t moistMillis = 5 * 60 * 1000;
uint64_t moistHighSince = 0;
uint64_t moistLowSince = 0;
void insideLoop();
void outsideLoop();
void moistureLoop();
void patrixSetup() {
insideDHT.begin();
outsideDHT.begin();
}
void patrixLoop() {
outsideLoop();
insideLoop();
moistureLoop();
ventilator.loop();
data.ventilatorSeconds = ventilator.getTotalOnMillis() / 1000;
sprinkler.loop();
data.sprinklerSeconds = sprinkler.getTotalOnMillis() / 1000;
static unsigned long now = millis();
static unsigned long last = now;
if (now - last >= 60 * 1000) {
last = now;
cache.add(getTime(), data);
cache.loop();
}
}
void outsideLoop() {
float temperature = NAN;
float absolute = NAN;
outsideDHT.loop(&temperature, &data.outsideRelativePercent, &absolute);
data.outsideTemperatureCenti = (int16_t) round(temperature * 100);
data.outsideAbsoluteCenti = (uint16_t) round(absolute * 100);
}
void insideLoop() {
float temperature = NAN;
float absolute = NAN;
insideDHT.loop(&temperature, &data.insideRelativePercent, &absolute);
data.insideTemperatureCenti = (int16_t) round(temperature * 100);
data.insideAbsoluteCenti = (uint16_t) round(absolute * 100);
if (temperature > tempHigh) {
tempLowSince = 0;
if (tempHighSince == 0) {
tempHighSince = getUptimeMillis();
}
if (tempHighSince != 0 && getUptimeMillis() - tempHighSince > tempMillis) {
ventilator.on();
}
}
if (temperature < tempLow) {
tempHighSince = 0;
if (tempLowSince == 0) {
tempLowSince = getUptimeMillis();
}
if (tempLowSince != 0 && getUptimeMillis() - tempLowSince > tempMillis) {
ventilator.off();
}
}
}
void moistureLoop() {
float moisture = NAN;
insideMoisture.loop(&moisture);
data.insideMoisturePercent = (uint16_t) round(moisture * 100);
if (moisture > moistHigh) {
moistLowSince = 0;
if (moistHighSince == 0) {
moistHighSince = getUptimeMillis();
}
if (moistHighSince != 0 && getUptimeMillis() - moistHighSince > moistMillis) {
sprinkler.off();
}
}
if (moisture < moistLow) {
moistHighSince = 0;
if (moistLowSince == 0) {
moistLowSince = getUptimeMillis();
}
if (moistLowSince != 0 && getUptimeMillis() - moistLowSince > moistMillis) {
sprinkler.on();
}
}
}
bool patrixCommand(char *first) {
bool result = false;
if (strcmp(first, "tempHigh") == 0 || strcmp(first, "th") == 0) {
result = cmdReadDouble("tempHigh", &tempHigh, NAN, NAN);
} else if (strcmp(first, "tempLow") == 0 || strcmp(first, "tl") == 0) {
result = cmdReadDouble("tempLow", &tempLow, NAN, NAN);
} else if (strcmp(first, "tempMillis") == 0 || strcmp(first, "tm") == 0) {
result = cmdReadU64("tempMillis", &tempMillis, NAN, NAN);
} else if (strcmp(first, "moistHigh") == 0 || strcmp(first, "mh") == 0) {
result = cmdReadDouble("moistHigh", &moistHigh, NAN, NAN);
} else if (strcmp(first, "moistLow") == 0 || strcmp(first, "ml") == 0) {
result = cmdReadDouble("moistLow", &moistLow, NAN, NAN);
} else if (strcmp(first, "moistMillis") == 0 || strcmp(first, "mm") == 0) {
result = cmdReadU64("moistMillis", &moistMillis, NAN, NAN);
}
return result;
}
void configLoaded() {
tempHigh = configGetDouble("tempHigh", TEMPERATURE_HIGH);
tempLow = configGetDouble("tempLow", TEMPERATURE_LOW);
tempMillis = configGetUint64_t("tempMillis", TEMPERATURE_MILLIS);
moistHigh = configGetDouble("moistHigh", MOISTURE_HIGH);
moistLow = configGetDouble("moistLow", MOISTURE_LOW);
moistMillis = configGetUint64_t("moistMillis", MOISTURE_MILLIS);
}
#endif

View File

@ -0,0 +1,95 @@
#ifndef SENSOR3_GREENHOUSE_DATA_H
#define SENSOR3_GREENHOUSE_DATA_H
#include "data.h"
struct GreenhouseData : IData {
int16_t insideTemperatureCenti;
uint8_t insideRelativePercent;
uint16_t insideAbsoluteCenti;
uint8_t insideMoisturePercent;
int16_t outsideTemperatureCenti;
uint8_t outsideRelativePercent;
uint16_t outsideAbsoluteCenti;
bool ventilatorState;
uint32_t ventilatorSeconds;
bool sprinklerState;
uint32_t sprinklerSeconds;
GreenhouseData() {
insideTemperatureCenti = 0;
insideRelativePercent = 0;
insideAbsoluteCenti = 0;
insideMoisturePercent = 0;
outsideTemperatureCenti = 0;
outsideRelativePercent = 0;
outsideAbsoluteCenti = 0;
ventilatorState = false;
ventilatorSeconds = 0;
sprinklerState = false;
sprinklerSeconds = 0;
}
GreenhouseData(
int16_t insideTemperatureCenti,
uint8_t insideRelativePercent,
uint8_t insideAbsoluteCenti,
uint8_t insideMoisturePercent,
int16_t outsideTemperatureCenti,
uint8_t outsideRelativePercent,
uint8_t outsideAbsoluteCenti,
bool ventilatorState,
uint32_t ventilatorSeconds,
bool sprinklerState,
uint32_t sprinklerSeconds
) :
insideTemperatureCenti(insideTemperatureCenti),
insideRelativePercent(insideRelativePercent),
insideAbsoluteCenti(insideAbsoluteCenti),
insideMoisturePercent(insideMoisturePercent),
outsideTemperatureCenti(outsideTemperatureCenti),
outsideRelativePercent(outsideRelativePercent),
outsideAbsoluteCenti(outsideAbsoluteCenti),
ventilatorState(ventilatorState),
ventilatorSeconds(ventilatorSeconds),
sprinklerState(sprinklerState),
sprinklerSeconds(sprinklerSeconds) {
// -
}
void toJson(JsonObject &json) const override {
JsonObject inside = json["inside"];
inside["temperature"] = insideTemperatureCenti / 100;
inside["relative"] = insideRelativePercent;
inside["absolute"] = insideAbsoluteCenti / 100;
inside["moisture"] = insideMoisturePercent;
JsonObject outside = json["outside"];
outside["temperature"] = outsideTemperatureCenti / 100;
outside["relative"] = outsideRelativePercent;
outside["absolute"] = outsideAbsoluteCenti / 100;
JsonObject ventilator = json["ventilator"];
ventilator["state"] = ventilatorState;
ventilator["totalSeconds"] = ventilatorSeconds;
JsonObject sprinkler = json["sprinkler"];
sprinkler["state"] = sprinklerState;
sprinkler["totalSeconds"] = sprinklerSeconds;
}
};
#endif