diff --git a/data/Fermenter/http/index.html b/data/Fermenter/http/index.html index 3a933c6..4e23729 100644 --- a/data/Fermenter/http/index.html +++ b/data/Fermenter/http/index.html @@ -1,193 +1,215 @@ - - - - Gärbox - - - + + + -
-
Ist-Temperatur
-
-
+
+
Ist-Temperatur
+
+
+
°C
+
+
+ +
+
Ziel-Temperatur
+
+ + + + +
+
°C
+ + + +
+
-
-
Ziel-Temperatur
-
- - - - -
-
-
°C
-
- - - - -
+
+
Heizung
+
+
+
%
- -
-
Heizung
-
-
-
%
-
-
-
-
W
-
+
+
+
W
+
- - - - + \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index f6fa3ec..096c23f 100644 --- a/platformio.ini +++ b/platformio.ini @@ -10,6 +10,7 @@ lib_deps = https://github.com/milesburton/Arduino-Temperature-Control-Library https://github.com/phassel/ArduPID/ https://github.com/me-no-dev/ESPAsyncWebServer https://github.com/wayoda/LedControl + https://github.com/bblanchon/ArduinoJson build_flags = board_build.filesystem = littlefs monitor_speed = 115200 @@ -48,6 +49,6 @@ board_build.filesystem = ${common.board_build.filesystem} monitor_speed = ${common.monitor_speed} upload_flags = --auth=OtaAuthPatrixFermenter upload_protocol = ${common.upload_protocol} -upload_port = 10.0.0.171 +upload_port = 10.0.0.164 ;upload_port = ${common.upload_port} ;upload_speed = ${common.upload_speed} diff --git a/src/Fermenter.cpp b/src/Fermenter.cpp deleted file mode 100644 index 39fe1ca..0000000 --- a/src/Fermenter.cpp +++ /dev/null @@ -1,197 +0,0 @@ -#ifdef NODE_FERMENTER - -#include -#include -#include - -#include "patrix/DS18B20Sensor.h" -#include "patrix/PIDController.h" -#include "patrix/PWMOutput.h" -#include "patrix/Rotary.h" -#include "patrix/http.h" - -#define HEATER_POWER_W 30 - -#define TARGET_STORE_DELAY_MS 10000 - -void rotaryCallback(int delta); - -DS18B20 ds18b20("DS18B20", D4); - -DS18B20Sensor input(ds18b20, 0, ""); - -PWMOutput heater(D2, "", 100); - -PIDController pid("fermenter", input, heater, UNIT_TEMPERATURE_C, 0, 40, 500, 0.00000002, 0); - -Rotary rotary(D1, D6, rotaryCallback); - -LedControl display(D7, D5, D8, 1); - -auto displayModifyTarget = 0UL; - -double targetStored = NAN; - -auto targetMillis = 0UL; - -void addTarget(double delta) { - pid.addTarget(delta); - if (targetStored != pid.getTarget()) { - targetMillis = millis(); - } else { - targetMillis = 0; - } -} - -void targetFileSetup() { - File file = LittleFS.open("target", "r"); - if (!file) { - Log.error("Failed to load target"); - return; - } - - const String& string = file.readString(); - file.close(); - - if (string == nullptr) { - Log.error("Target file empty"); - return; - } - - const auto value = string.toDouble(); - if (isnan(value)) { - Log.error("Target file does not contain a double"); - return; - } - - pid.setTarget(value); - targetStored = value; - targetMillis = 0; - - Log.info("Target loaded."); -} - -void targetFileLoop() { - if (targetStored != pid.getTarget() && targetMillis != 0 && millis() - targetMillis >= TARGET_STORE_DELAY_MS) { - File file = LittleFS.open("target", "w"); - if (!file) { - Log.error("Failed to store target"); - return; - } - - file.write(String(pid.getTarget()).c_str()); - file.close(); - - targetStored = pid.getTarget(); - targetMillis = 0; - - Log.info("Target stored."); - } -} - -void rotaryCallback(int delta) { - addTarget(delta); - displayModifyTarget = millis(); -} - -void displayPrintf(const char *format, ...) { - va_list args; - va_start(args, format); - char buffer[17]; - vsnprintf(buffer, sizeof buffer, format, args); - int position = 0; - for (char *b = buffer; *b != 0 && b < buffer + sizeof buffer; b++) { - char thisChar = *b; - if (thisChar == 'z' || thisChar == 'Z') { - thisChar = '2'; - } else if (thisChar == 'i' || thisChar == 'I') { - thisChar = '1'; - } - const auto nextIsDot = *(b + 1) == '.'; - display.setChar(0, 7 - position++, thisChar, nextIsDot); - if (nextIsDot) { - b++; - } - } - va_end(args); -} - -void displayLoop() { - const auto now = millis(); - - static unsigned long lastInit = 0; - if (lastInit == 0 || now - lastInit >= 60 * 60 * 1000) { - lastInit = now; - display.shutdown(0, true); - display.shutdown(0, false); - display.setIntensity(0, 2); - display.clearDisplay(0); - } - - if (displayModifyTarget != 0 && now - displayModifyTarget >= 2000) { - displayModifyTarget = 0; - } - - if (displayModifyTarget != 0) { - displayPrintf("ZIEL %4.1f", pid.getTarget()); - } else { - displayPrintf("%4.1f %4.1f", input.getValue(), pid.getTarget()); - } -} - -void httpStatus(AsyncWebServerRequest *request) { - char buffer[256]; - snprintf(buffer, sizeof buffer, R"({"target": %f, "input": %f, "outputPercent": %f, "outputPowerW": %f})", pid.getTarget(), input.getValue(), heater.getPercent(), heater.getPercent() / 100.0 * HEATER_POWER_W); - request->send(200, "application/json", buffer); -} - -void httpTargetAdd(AsyncWebServerRequest *request) { - const auto param = request->getParam("delta"); - if (param == nullptr) { - Log.error("Missing parameter: delta (1)"); - return; - } - - const auto string = param->value(); - if (string == nullptr) { - Log.error("Missing parameter: delta (2)"); - return; - } - - const auto delta = string.toDouble(); - if (isnan(delta)) { - Log.error("Missing parameter: delta (3)"); - return; - } - - addTarget(delta); - - httpStatus(request); -} - -void patrixSetup() { - ds18b20.setup(); - heater.setup(); - rotary.setup(); - - targetFileSetup(); - pid.setup(); - - server.on("/status", httpStatus); - server.on("/status/", httpStatus); - server.on("/target/add", httpTargetAdd); - server.on("/target/add/", httpTargetAdd); -} - -void patrixLoop() { - ds18b20.loop(); - input.loop(); - rotary.loop(); - - targetFileLoop(); - pid.loop(); - - displayLoop(); -} - -#endif diff --git a/src/node/Fermenter/Fermenter.cpp b/src/node/Fermenter/Fermenter.cpp new file mode 100644 index 0000000..0d287a3 --- /dev/null +++ b/src/node/Fermenter/Fermenter.cpp @@ -0,0 +1,28 @@ +#ifdef NODE_FERMENTER + +#include "Fermenter.h" + +void patrixSetup() { + config.read(); + + ds18b20.setup(); + heater.setup(); + rotary.setup(); + + pid.setup(); + httpSetup2(); +} + +void patrixLoop() { + config.loop(); + + ds18b20.loop(); + temperature.loop(); + rotary.loop(); + + pid.loop(); + + displayLoop(); +} + +#endif diff --git a/src/node/Fermenter/Fermenter.h b/src/node/Fermenter/Fermenter.h new file mode 100644 index 0000000..4ba5a2d --- /dev/null +++ b/src/node/Fermenter/Fermenter.h @@ -0,0 +1,68 @@ +#ifndef NODE_FERMENTER_H +#define NODE_FERMENTER_H + +#define HEATER_POWER_W 30 + +/* rotary ----------------------------------------------------------------------------------------- */ + +#include "patrix/Rotary.h" + +extern Rotary rotary; + +void rotaryCallback(int delta); + +/* display ---------------------------------------------------------------------------------------- */ + +#include + +extern unsigned long displayModifyTarget; + +extern LedControl display; + +void displayPrintf(const char* format, ...); + +void displayLoop(); + +/* config ----------------------------------------------------------------------------------------- */ + +#include "patrix/Config.h" + +extern Config config; + +void configCollect(JsonDocument& json); + +void configApply(JsonDocument& json); + +/* pid -------------------------------------------------------------------------------------------- */ + +#include "patrix/DS18B20Sensor.h" +#include "patrix/PIDController.h" +#include "patrix/PWMOutput.h" + +extern DS18B20 ds18b20; + +extern DS18B20Sensor temperature; + +extern PWMOutput heater; + +extern PIDController pid; + +void addTarget(double delta); + +/* http ------------------------------------------------------------------------------------------- */ + +#include "patrix/http.h" + +void httpStatus(AsyncWebServerRequest* request); + +void httpTargetAdd(AsyncWebServerRequest* request); + +void httpSetup2(); + +/* patrix ----------------------------------------------------------------------------------------- */ + +void patrixSetup(); + +void patrixLoop(); + +#endif diff --git a/src/node/Fermenter/config.cpp b/src/node/Fermenter/config.cpp new file mode 100644 index 0000000..bd7d850 --- /dev/null +++ b/src/node/Fermenter/config.cpp @@ -0,0 +1,21 @@ +#ifdef NODE_FERMENTER + +#include "Fermenter.h" + +Config config("/config.json", configCollect, configApply); + +void configCollect(JsonDocument& json) { + json["pid"]["p"] = pid.p; + json["pid"]["i"] = pid.i; + json["pid"]["d"] = pid.d; + json["pid"]["target"] = pid.getTarget(); +} + +void configApply(JsonDocument& json) { + pid.p = json["pid"]["p"].as(); + pid.i = json["pid"]["i"].as(); + pid.d = json["pid"]["d"].as(); + pid.setTarget(json["pid"]["target"].as()); +} + +#endif diff --git a/src/node/Fermenter/display.cpp b/src/node/Fermenter/display.cpp new file mode 100644 index 0000000..5ffc3c3 --- /dev/null +++ b/src/node/Fermenter/display.cpp @@ -0,0 +1,56 @@ +#ifdef NODE_FERMENTER + +#include "Fermenter.h" + +LedControl display(D7, D5, D8, 1); + +unsigned long displayModifyTarget = 0UL; + +void displayPrintf(const char* format, ...) { + va_list args; + va_start(args, format); + char buffer[17]; + vsnprintf(buffer, sizeof buffer, format, args); + auto position = 0; + for (const char* b = buffer; *b != 0 && b < buffer + sizeof buffer; b++) { + auto thisChar = *b; + if (thisChar == 'z' || thisChar == 'Z') { + thisChar = '2'; + } + else if (thisChar == 'i' || thisChar == 'I') { + thisChar = '1'; + } + const auto nextIsDot = *(b + 1) == '.'; + display.setChar(0, 7 - position++, thisChar, nextIsDot); + if (nextIsDot) { + b++; + } + } + va_end(args); +} + +void displayLoop() { + const auto now = millis(); + + static unsigned long lastInit = 0; + if (lastInit == 0 || now - lastInit >= 60 * 60 * 1000) { + lastInit = now; + display.shutdown(0, true); + display.shutdown(0, false); + display.setIntensity(0, 2); + display.clearDisplay(0); + } + + if (displayModifyTarget != 0 && now - displayModifyTarget >= 2000) { + displayModifyTarget = 0; + } + + if (displayModifyTarget != 0) { + displayPrintf("ZIEL %4.1f", pid.getTarget()); + } + else { + displayPrintf("%4.1f %4.1f", temperature.getValue(), pid.getTarget()); + } +} + +#endif diff --git a/src/node/Fermenter/http.cpp b/src/node/Fermenter/http.cpp new file mode 100644 index 0000000..a3656cb --- /dev/null +++ b/src/node/Fermenter/http.cpp @@ -0,0 +1,101 @@ +#ifdef NODE_FERMENTER + +#include "Fermenter.h" + +void httpStatus(AsyncWebServerRequest* request) { + JsonDocument json; + json["pid"]["p"] = pid.p; + json["pid"]["i"] = pid.i; + json["pid"]["d"] = pid.d; + json["pid"]["target"] = pid.getTarget(); + json["temperature"] = temperature.getValue(); + json["heater"]["percent"] = heater.getPercent(); + json["heater"]["powerW"] = heater.getPercent() / 100.0 * HEATER_POWER_W; + + AsyncResponseStream* stream = request->beginResponseStream("application/json"); + serializeJson(json, *stream); + request->send(stream); +} + +void httpTargetAdd(AsyncWebServerRequest* request) { + const auto param = request->getParam("delta"); + if (param == nullptr) { + Log.error("Missing parameter: delta (1)"); + return; + } + + const auto string = param->value(); + if (string == nullptr) { + Log.error("Missing parameter: delta (2)"); + return; + } + + const auto delta = string.toDouble(); + if (isnan(delta)) { + Log.error("Missing parameter: delta (3)"); + return; + } + + addTarget(delta); + + httpStatus(request); +} + +void httpConfigSet(AsyncWebServerRequest* request) { + const auto keyParam = request->getParam("key"); + if (keyParam == nullptr) { + Log.error("Missing parameter: key (1)"); + return; + } + + const auto keyString = keyParam->value(); + if (keyString == nullptr) { + Log.error("Missing parameter: key (2)"); + return; + } + + const auto valueParam = request->getParam("value"); + if (valueParam == nullptr) { + Log.error("Missing parameter: value (1)"); + return; + } + + const auto valueString = valueParam->value(); + if (valueString == nullptr) { + Log.error("Missing parameter: value (2)"); + return; + } + + const auto value = valueString.toDouble(); + if (isnan(value)) { + Log.error("Missing parameter: value (3)"); + return; + } + + if (keyString.equals("p")) { + pid.p = value; + } else if (keyString.equals("i")) { + pid.i = value; + } else if (keyString.equals("d")) { + pid.d = value; + } else if (keyString.equals("target")) { + pid.setTarget(value); + } else { + request->send(400, "text/plain", "unknown key"); + return; + } + config.markDirty(); + + httpStatus(request); +} + +void httpSetup2() { + server.on("/status", httpStatus); + server.on("/status/", httpStatus); + server.on("/target/add", httpTargetAdd); + server.on("/target/add/", httpTargetAdd); + server.on("/config/set", httpConfigSet); + server.on("/config/set/", httpConfigSet); +} + +#endif diff --git a/src/node/Fermenter/pid.cpp b/src/node/Fermenter/pid.cpp new file mode 100644 index 0000000..cd61550 --- /dev/null +++ b/src/node/Fermenter/pid.cpp @@ -0,0 +1,41 @@ +#ifdef NODE_FERMENTER + +#include "Fermenter.h" + +DS18B20 ds18b20( + "DS18B20", + D4 +); + +DS18B20Sensor temperature( + ds18b20, + 0, + "" +); + +PWMOutput heater( + D2, + "", + 100 +); + +PIDController pid( + "fermenter/temperature/target", + "fermenter/temperature/current", + "fermenter/heater", + temperature, + heater, + UNIT_TEMPERATURE_C, + 0, + 40, + 50, + 0, + 0 +); + +void addTarget(const double delta) { + pid.addTarget(delta); + config.markDirty(); +} + +#endif diff --git a/src/node/Fermenter/rotary.cpp b/src/node/Fermenter/rotary.cpp new file mode 100644 index 0000000..fb782cf --- /dev/null +++ b/src/node/Fermenter/rotary.cpp @@ -0,0 +1,12 @@ +#ifdef NODE_FERMENTER + +#include "Fermenter.h" + +Rotary rotary(D1, D6, rotaryCallback); + +void rotaryCallback(const int delta) { + addTarget(delta); + displayModifyTarget = millis(); +} + +#endif diff --git a/src/patrix/Config.h b/src/patrix/Config.h new file mode 100644 index 0000000..21e5457 --- /dev/null +++ b/src/patrix/Config.h @@ -0,0 +1,93 @@ +#ifndef CONFIG_H +#define CONFIG_H + +#include +#include +#include "mqtt.h" + +class Config { + +public: + + typedef void (*handle_json_t)(JsonDocument& json); + +private: + + String path; + + handle_json_t collect; + + handle_json_t apply; + + bool dirty = false; + + unsigned long writeDelayStartMillis = 0UL; + +public: + + unsigned long writeDelayMillis = 10 * 1000UL; + + explicit Config(String path, const handle_json_t collect, const handle_json_t apply): + path(std::move(path)), + collect(collect), apply(apply) { + // + } + + void markDirty() { + dirty = true; + writeDelayStartMillis = max(1UL, millis()); + } + + void loop() { + if (writeDelayStartMillis != 0 && millis() - writeDelayStartMillis >= writeDelayMillis) { + write(); + } + } + + void read() { + File file = LittleFS.open(path, "r"); + if (!file) { + Log.error("Failed to open config file for read: %s", path.c_str()); + return; + } + + JsonDocument json; + deserializeJson(json, file); + file.close(); + + dirty = false; + writeDelayStartMillis = 0; + Log.info("Config read: %s", path.c_str()); + + char buffer[256]; + serializeJson(json, buffer); + Log.info(buffer); + + apply(json); + } + + void write() { + writeDelayStartMillis = 0; + + File file = LittleFS.open(path, "w"); + if (!file) { + Log.error("Failed to open config file for write: %s", path.c_str()); + return; + } + + JsonDocument json; + collect(json); + serializeJson(json, file); + file.close(); + + dirty = false; + Log.info("Config written: %s", path.c_str()); + + char buffer[256]; + serializeJson(json, buffer); + Log.info(buffer); + } + +}; + +#endif diff --git a/src/patrix/DS18B20.h b/src/patrix/DS18B20.h index d3b5397..3ce2913 100644 --- a/src/patrix/DS18B20.h +++ b/src/patrix/DS18B20.h @@ -4,7 +4,6 @@ #include #include "DallasTemperature.h" -#include "IValueSensor.h" #include "mqtt.h" #include "OneWire.h" @@ -45,34 +44,35 @@ public: void loop() { switch (state) { - case READING: - if (bus.isConversionComplete()) { - state = COMPLETE; - if (first) { - first = false; - Log.debug("DS18B20: %d devices", bus.getDeviceCount()); - for (auto index = 0; index < bus.getDeviceCount(); index++) { - uint8_t address[8]; - bus.getAddress(address, index); - char addressHex[19]; - snprintf(addressHex, sizeof(addressHex), "0x%02X%02X%02X%02X%02X%02X%02X%02X", address[7], address[6], address[5], address[4], address[3], address[2], address[1], address[0]); - const auto temperature = bus.getTempC(address); - Log.debug(" %s: %5.1f%cC", addressHex, temperature, 176); - } + case READING: + if (bus.isConversionComplete()) { + state = COMPLETE; + if (first) { + first = false; + Log.debug("DS18B20: %d devices", bus.getDeviceCount()); + for (auto index = 0; index < bus.getDeviceCount(); index++) { + uint8_t address[8]; + bus.getAddress(address, index); + char addressHex[19]; + snprintf(addressHex, sizeof(addressHex), "0x%02X%02X%02X%02X%02X%02X%02X%02X", address[7], address[6], address[5], address[4], address[3], address[2], address[1], address[0]); + const auto temperature = bus.getTempC(address); + Log.debug(" %s: %5.1f%cC", addressHex, temperature, 176); } - } else if (millis() - last > timeout) { - state = ERROR; - Log.error("DS18B20 \"%s\" gpio #%d: timeout", name.c_str(), gpio); } - break; - default: - state = IDLE; - if (last == 0 || millis() - last >= interval) { - last = max(1UL, millis()); - state = READING; - bus.requestTemperatures(); - } - break; + } + else if (millis() - last > timeout) { + state = ERROR; + Log.error("DS18B20 \"%s\" gpio #%d: timeout", name.c_str(), gpio); + } + break; + default: + state = IDLE; + if (last == 0 || millis() - last >= interval) { + last = max(1UL, millis()); + state = READING; + bus.requestTemperatures(); + } + break; } } @@ -84,7 +84,7 @@ public: return temperature == DEVICE_DISCONNECTED_C ? NAN : temperature; } - float getTemperatureByAddress(const uint8_t *address) { + float getTemperatureByAddress(const uint8_t* address) { if (state != COMPLETE) { return NAN; } diff --git a/src/patrix/DS18B20Sensor.h b/src/patrix/DS18B20Sensor.h index 36c46ec..2f0da15 100644 --- a/src/patrix/DS18B20Sensor.h +++ b/src/patrix/DS18B20Sensor.h @@ -1,9 +1,8 @@ #ifndef DS18B20_SENSOR_H #define DS18B20_SENSOR_H -#include - #include "DS18B20.h" +#include "IValueSensor.h" class DS18B20Sensor final : public IValueSensor { @@ -13,7 +12,7 @@ class DS18B20Sensor final : public IValueSensor { const int index; - const uint8_t *address; + const uint8_t* address; float temperature = NAN; @@ -35,13 +34,15 @@ public: if (bus.isComplete()) { if (index >= 0) { temperature = bus.getTemperatureByIndex(index); - } else { + } + else { temperature = bus.getTemperatureByAddress(address); } if (!name.isEmpty()) { mqttPublishValue(name, temperature, UNIT_TEMPERATURE_C); } - } else if (bus.isError()) { + } + else if (bus.isError()) { temperature = NAN; } } diff --git a/src/patrix/PIDController.h b/src/patrix/PIDController.h index 2c4fcb4..1c9371a 100644 --- a/src/patrix/PIDController.h +++ b/src/patrix/PIDController.h @@ -3,20 +3,22 @@ #include -#include - #include "IValueSensor.h" #include "PWMOutput.h" class PIDController { - const String name; + const String targetName; + + const String inputName; + + const String outputName; const IValueSensor& input; PWMOutput& output; - const char *unit; + const char* unit; ArduPID controller; @@ -24,12 +26,6 @@ class PIDController { double maxValue; - double p = 0; - - double i = 0; - - double d = 0; - double inputValue = NAN; double outputPercent = NAN; @@ -40,8 +36,25 @@ class PIDController { public: - PIDController(String name, const IValueSensor& sensor, PWMOutput& pwmOutput, const char *unit, const double minValue, const double maxValue, const double p, const double i, const double d) - : name(std::move(name)), input(sensor), output(pwmOutput), unit(unit), controller(), minValue(minValue), maxValue(maxValue), p(p), i(i), d(d) { + double p = 0; + + double i = 0; + + double d = 0; + + PIDController(String targetName, String inputName, String outputName, const IValueSensor& sensor, PWMOutput& pwmOutput, const char* unit, const double minValue, const double maxValue, const double p, const double i, const double d) : + targetName(std::move(targetName)), + inputName(std::move(inputName)), + outputName(std::move(outputName)), + input(sensor), + output(pwmOutput), + unit(unit), + controller(), + minValue(minValue), + maxValue(maxValue), + p(p), + i(i), + d(d) { // } @@ -60,9 +73,9 @@ public: const auto now = millis(); if (lastSent == 0 || now - lastSent >= 5000) { lastSent = now; - mqttPublishValue(name + "/target", targetValue, unit); - mqttPublishValue(name + "/input", inputValue, unit); - mqttPublishValue(name + "/output", outputPercent, UNIT_PERCENT); + mqttPublishValue(targetName, targetValue, unit); + mqttPublishValue(inputName, inputValue, unit); + mqttPublishValue(outputName, outputPercent, UNIT_PERCENT); } } } @@ -79,7 +92,7 @@ public: double setTarget(double target) { targetValue = max(minValue, min(maxValue, target)); - Log.info("PID \"%s\" target = %.1f", name.c_str(), targetValue); + Log.info("PID %s = %.1f", targetName.c_str(), targetValue); return targetValue; }