Compare commits

...

3 Commits

Author SHA1 Message Date
387aecc02a Fermenter target store + Rotary 2025-02-17 22:33:40 +01:00
c9a379eaae Fermenter display 2025-02-17 20:08:17 +01:00
c87c46ce9c Fermenter PWM 100Hz 2025-02-17 20:08:07 +01:00
4 changed files with 221 additions and 49 deletions

View File

@ -3,76 +3,170 @@
#include <ESPAsyncWebServer.h> #include <ESPAsyncWebServer.h>
#include <LedControl.h> #include <LedControl.h>
#include <LittleFS.h> #include <LittleFS.h>
#include <ArduinoOTA.h>
#include "patrix/DS18B20Sensor.h" #include "patrix/DS18B20Sensor.h"
#include "patrix/PIDController.h" #include "patrix/PIDController.h"
#include "patrix/PWMOutput.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); AsyncWebServer server(80);
DS18B20 ds18b20("DS18B20", D4); DS18B20 ds18b20("DS18B20", D4);
DS18B20Sensor input(ds18b20, 0, ""); DS18B20Sensor input(ds18b20, 0, "");
PWMOutput heater(D2, ""); 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 display = LedControl(13, 14, 15, 1); Rotary rotary(D1, D6, rotaryCallback);
void displayDecimal(int *digit, const double value) { LedControl display(D7, D5, D8, 1);
const auto integer = static_cast<int>(value);
const auto decimal = static_cast<int>((value - integer) * 10) % 10;
display.setDigit(0, (*digit)++, decimal, false);
display.setDigit(0, (*digit)++, integer % 10, true);
display.setDigit(0, (*digit)++, integer / 10 % 10, false);
}
void displayLoop() { auto displayModifyTarget = 0UL;
static unsigned long lastDisplayInit = 0;
if (lastDisplayInit == 0 || millis() - lastDisplayInit > 60 * 60 * 1000) { double targetStored = NAN;
lastDisplayInit = millis();
display.shutdown(0, true); auto targetMillis = 0UL;
display.shutdown(0, false);
display.setIntensity(0, 4); void addTarget(double delta) {
display.clearDisplay(0); pid.addTarget(delta);
if (targetStored != pid.getTarget()) {
targetMillis = millis();
} else {
targetMillis = 0;
} }
auto digit = 0;
displayDecimal(&digit, input.getValue());
digit++;
digit++;
displayDecimal(&digit, pid.targetValue);
} }
void httpStatus(AsyncWebServerRequest *request) { void targetFileSetup() {
char buffer[256]; File file = LittleFS.open("target", "r");
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); if (!file) {
request->send(200, "application/json", buffer); Log.error("Failed to load target");
}
void httpTargetAdd(AsyncWebServerRequest *request) {
const auto delta = request->getParam("delta");
if (delta == nullptr) {
Log.error("Missing parameter: delta (1)");
return; return;
} }
const auto string = delta->value(); const String& string = file.readString();
file.close();
if (string == nullptr) { if (string == nullptr) {
Log.error("Missing parameter: delta (2)"); Log.error("Target file empty");
return; return;
} }
const auto value = string.toDouble(); const auto value = string.toDouble();
if (isnan(value)) { 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)"); Log.error("Missing parameter: delta (3)");
return; return;
} }
pid.targetValue = max(0.0, min(40.0, pid.targetValue + value)); addTarget(delta);
Log.info("Set targetValue = %.1f%cC", pid.targetValue, 176);
httpStatus(request); httpStatus(request);
} }
@ -86,15 +180,19 @@ void httpNotFound(AsyncWebServerRequest *request) {
} }
void patrixSetup() { void patrixSetup() {
ds18b20.setup();
heater.setup();
pid.setup();
if (LittleFS.begin()) { if (LittleFS.begin()) {
Log.info("Filesystem mounted."); Log.info("Filesystem mounted.");
} else { } else {
Log.error("Failed to mount filesystem!"); 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-Origin", "*");
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Methods", "GET, POST, PUT"); DefaultHeaders::Instance().addHeader("Access-Control-Allow-Methods", "GET, POST, PUT");
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "Content-Type"); DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "Content-Type");
@ -110,7 +208,12 @@ void patrixSetup() {
void patrixLoop() { void patrixLoop() {
ds18b20.loop(); ds18b20.loop();
input.loop(); input.loop();
rotary.loop();
targetFileLoop();
pid.loop(); pid.loop();
displayLoop();
} }
#endif #endif

View File

@ -20,6 +20,10 @@ class PIDController {
ArduPID controller; ArduPID controller;
double minValue;
double maxValue;
double p = 0; double p = 0;
double i = 0; double i = 0;
@ -32,12 +36,12 @@ class PIDController {
unsigned long lastSent = 0UL; unsigned long lastSent = 0UL;
double targetValue = 0;
public: public:
double targetValue = 28; 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) {
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) {
// //
} }
@ -65,6 +69,20 @@ public:
output.setPercent(outputPercent); 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 #endif

View File

@ -10,18 +10,21 @@ class PWMOutput {
String name; String name;
uint32_t frequency = 0;
int value = 0; int value = 0;
double percent = 0; double percent = 0;
public: public:
explicit PWMOutput(const uint8_t gpio, String name): gpio(gpio), name(std::move(name)) { explicit PWMOutput(const uint8_t gpio, String name, uint32_t frequency) : gpio(gpio), name(std::move(name)), frequency(frequency) {
// //
} }
void setup() { void setup() {
analogWriteResolution(CONTROL_PWM_BITS); analogWriteResolution(CONTROL_PWM_BITS);
analogWriteFreq(frequency);
setValue(0); setValue(0);
} }
@ -31,8 +34,8 @@ public:
analogWrite(gpio, value); analogWrite(gpio, value);
} }
void setPercent(const double percent) { void setPercent(const double newPercent) {
setValue(static_cast<int>(percent / 100.0 * CONTROL_PWM_MAX)); setValue(static_cast<int>(newPercent / 100.0 * CONTROL_PWM_MAX));
} }
[[nodiscard]] double getPercent() const { [[nodiscard]] double getPercent() const {

48
src/patrix/Rotary.h Normal file
View File

@ -0,0 +1,48 @@
#ifndef HELLIGKEIT_ROTARY_H
#define HELLIGKEIT_ROTARY_H
#include <Arduino.h>
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