From 387aecc02a78f8e9aa7ad4a5c34db1f4391ac505 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Ha=C3=9Fel?= Date: Mon, 17 Feb 2025 21:52:28 +0100 Subject: [PATCH] Fermenter target store + Rotary --- src/Fermenter.cpp | 140 ++++++++++++++++++++++++++++++------- src/patrix/PIDController.h | 26 +++++-- src/patrix/Rotary.h | 48 +++++++++++++ 3 files changed, 183 insertions(+), 31 deletions(-) create mode 100644 src/patrix/Rotary.h diff --git a/src/Fermenter.cpp b/src/Fermenter.cpp index 250118e..b8c5af5 100644 --- a/src/Fermenter.cpp +++ b/src/Fermenter.cpp @@ -3,12 +3,18 @@ #include #include #include +#include #include "patrix/DS18B20Sensor.h" #include "patrix/PIDController.h" #include "patrix/PWMOutput.h" +#include "patrix/Rotary.h" -#define HEATER_POWER_W 30 +#define HEATER_POWER_W 30 + +#define TARGET_STORE_DELAY_MS 10000 + +void rotaryCallback(int delta); AsyncWebServer server(80); @@ -18,20 +24,96 @@ DS18B20Sensor input(ds18b20, 0, ""); PWMOutput heater(D2, "", 100); -PIDController pid("fermenter", input, heater, UNIT_TEMPERATURE_C, 500, 0.00000002, 0); +PIDController pid("fermenter", input, heater, UNIT_TEMPERATURE_C, 0, 40, 500, 0.00000002, 0); -auto displayToggleInterval = 2000UL; +Rotary rotary(D1, D6, rotaryCallback); -auto display = LedControl(13, 14, 15, 1); +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[9]; + char buffer[17]; vsnprintf(buffer, sizeof buffer, format, args); int position = 0; for (char *b = buffer; *b != 0 && b < buffer + sizeof buffer; b++) { - display.setChar(0, position++, *b, false); + 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); } @@ -44,51 +126,47 @@ void displayLoop() { lastInit = now; display.shutdown(0, true); display.shutdown(0, false); - display.setIntensity(0, 4); + display.setIntensity(0, 2); display.clearDisplay(0); } - static auto mode = true; - static auto lastMode = 0UL; - if (lastMode == 0 || now - lastMode >= displayToggleInterval) { - lastMode = now; - mode = !mode; + if (displayModifyTarget != 0 && now - displayModifyTarget >= 2000) { + displayModifyTarget = 0; } - if (mode) { - displayPrintf("%3.0f ", heater.getPercent()); + if (displayModifyTarget != 0) { + displayPrintf("ZIEL %4.1f", pid.getTarget()); } else { - displayPrintf("%-4.1f%4.1f", input.getValue(), pid.targetValue); + 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.targetValue, input.getValue(), heater.getPercent(), heater.getPercent() / 100.0 * HEATER_POWER_W); + 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 delta = request->getParam("delta"); - if (delta == nullptr) { + const auto param = request->getParam("delta"); + if (param == nullptr) { Log.error("Missing parameter: delta (1)"); return; } - const auto string = delta->value(); + const auto string = param->value(); if (string == nullptr) { Log.error("Missing parameter: delta (2)"); return; } - const auto value = string.toDouble(); - if (isnan(value)) { + const auto delta = string.toDouble(); + if (isnan(delta)) { Log.error("Missing parameter: delta (3)"); return; } - pid.targetValue = max(0.0, min(40.0, pid.targetValue + value)); - Log.info("Set targetValue = %.1f%cC", pid.targetValue, 176); + addTarget(delta); httpStatus(request); } @@ -102,15 +180,19 @@ void httpNotFound(AsyncWebServerRequest *request) { } void patrixSetup() { - ds18b20.setup(); - heater.setup(); - pid.setup(); - if (LittleFS.begin()) { Log.info("Filesystem mounted."); } else { Log.error("Failed to mount filesystem!"); } + + ds18b20.setup(); + heater.setup(); + rotary.setup(); + + targetFileSetup(); + pid.setup(); + DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*"); DefaultHeaders::Instance().addHeader("Access-Control-Allow-Methods", "GET, POST, PUT"); DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "Content-Type"); @@ -126,7 +208,11 @@ void patrixSetup() { void patrixLoop() { ds18b20.loop(); input.loop(); + rotary.loop(); + + targetFileLoop(); pid.loop(); + displayLoop(); } diff --git a/src/patrix/PIDController.h b/src/patrix/PIDController.h index 62b8ae4..2c4fcb4 100644 --- a/src/patrix/PIDController.h +++ b/src/patrix/PIDController.h @@ -20,6 +20,10 @@ class PIDController { ArduPID controller; + double minValue; + + double maxValue; + double p = 0; double i = 0; @@ -32,12 +36,12 @@ class PIDController { unsigned long lastSent = 0UL; + double targetValue = 0; + public: - double targetValue = 28; - - PIDController(String name, const IValueSensor& sensor, PWMOutput& pwmOutput, const char *unit, const double p, const double i, const double d) - : name(std::move(name)), input(sensor), output(pwmOutput), unit(unit), controller(), p(p), i(i), d(d) { + 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) { // } @@ -65,6 +69,20 @@ public: output.setPercent(outputPercent); } + double addTarget(const double delta) { + return setTarget(targetValue + delta); + } + + [[nodiscard]] double getTarget() const { + return targetValue; + } + + double setTarget(double target) { + targetValue = max(minValue, min(maxValue, target)); + Log.info("PID \"%s\" target = %.1f", name.c_str(), targetValue); + return targetValue; + } + }; #endif diff --git a/src/patrix/Rotary.h b/src/patrix/Rotary.h new file mode 100644 index 0000000..8e0c12a --- /dev/null +++ b/src/patrix/Rotary.h @@ -0,0 +1,48 @@ +#ifndef HELLIGKEIT_ROTARY_H +#define HELLIGKEIT_ROTARY_H + +#include + +class Rotary { + +public: + + typedef void (*callback_t)(int delta); + +private: + + const uint8_t pinCLK; + + const uint8_t pinDT; + + bool lastCLK = false; + + callback_t callback; + +public: + + Rotary(const uint8_t pinCLK, const uint8_t pinDT, callback_t callback) : pinCLK(pinCLK), pinDT(pinDT), callback(callback) { + // + } + + void setup() { + pinMode(pinCLK, INPUT_PULLUP); + pinMode(pinDT, INPUT_PULLUP); + lastCLK = digitalRead(pinCLK) == HIGH; + } + + void loop() { + const bool currentCLK = digitalRead(pinCLK) == HIGH; + if (currentCLK != lastCLK && currentCLK) { + if ((digitalRead(pinDT) == HIGH) != currentCLK) { + callback(+1); + } else { + callback(-1); + } + } + lastCLK = currentCLK; + } + +}; + +#endif