Fermenter target store + Rotary
This commit is contained in:
parent
c9a379eaae
commit
387aecc02a
@ -3,12 +3,18 @@
|
|||||||
#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);
|
||||||
|
|
||||||
@ -18,20 +24,96 @@ DS18B20Sensor input(ds18b20, 0, "");
|
|||||||
|
|
||||||
PWMOutput heater(D2, "", 100);
|
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, ...) {
|
void displayPrintf(const char *format, ...) {
|
||||||
va_list args;
|
va_list args;
|
||||||
va_start(args, format);
|
va_start(args, format);
|
||||||
char buffer[9];
|
char buffer[17];
|
||||||
vsnprintf(buffer, sizeof buffer, format, args);
|
vsnprintf(buffer, sizeof buffer, format, args);
|
||||||
int position = 0;
|
int position = 0;
|
||||||
for (char *b = buffer; *b != 0 && b < buffer + sizeof buffer; b++) {
|
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);
|
va_end(args);
|
||||||
}
|
}
|
||||||
@ -44,51 +126,47 @@ void displayLoop() {
|
|||||||
lastInit = now;
|
lastInit = now;
|
||||||
display.shutdown(0, true);
|
display.shutdown(0, true);
|
||||||
display.shutdown(0, false);
|
display.shutdown(0, false);
|
||||||
display.setIntensity(0, 4);
|
display.setIntensity(0, 2);
|
||||||
display.clearDisplay(0);
|
display.clearDisplay(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
static auto mode = true;
|
if (displayModifyTarget != 0 && now - displayModifyTarget >= 2000) {
|
||||||
static auto lastMode = 0UL;
|
displayModifyTarget = 0;
|
||||||
if (lastMode == 0 || now - lastMode >= displayToggleInterval) {
|
|
||||||
lastMode = now;
|
|
||||||
mode = !mode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode) {
|
if (displayModifyTarget != 0) {
|
||||||
displayPrintf("%3.0f ", heater.getPercent());
|
displayPrintf("ZIEL %4.1f", pid.getTarget());
|
||||||
} else {
|
} else {
|
||||||
displayPrintf("%-4.1f%4.1f", input.getValue(), pid.targetValue);
|
displayPrintf("%4.1f %4.1f", input.getValue(), pid.getTarget());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void httpStatus(AsyncWebServerRequest *request) {
|
void httpStatus(AsyncWebServerRequest *request) {
|
||||||
char buffer[256];
|
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);
|
request->send(200, "application/json", buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
void httpTargetAdd(AsyncWebServerRequest *request) {
|
void httpTargetAdd(AsyncWebServerRequest *request) {
|
||||||
const auto delta = request->getParam("delta");
|
const auto param = request->getParam("delta");
|
||||||
if (delta == nullptr) {
|
if (param == nullptr) {
|
||||||
Log.error("Missing parameter: delta (1)");
|
Log.error("Missing parameter: delta (1)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto string = delta->value();
|
const auto string = param->value();
|
||||||
if (string == nullptr) {
|
if (string == nullptr) {
|
||||||
Log.error("Missing parameter: delta (2)");
|
Log.error("Missing parameter: delta (2)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto value = string.toDouble();
|
const auto delta = string.toDouble();
|
||||||
if (isnan(value)) {
|
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);
|
||||||
}
|
}
|
||||||
@ -102,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");
|
||||||
@ -126,7 +208,11 @@ void patrixSetup() {
|
|||||||
void patrixLoop() {
|
void patrixLoop() {
|
||||||
ds18b20.loop();
|
ds18b20.loop();
|
||||||
input.loop();
|
input.loop();
|
||||||
|
rotary.loop();
|
||||||
|
|
||||||
|
targetFileLoop();
|
||||||
pid.loop();
|
pid.loop();
|
||||||
|
|
||||||
displayLoop();
|
displayLoop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
48
src/patrix/Rotary.h
Normal file
48
src/patrix/Rotary.h
Normal 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
|
||||||
Loading…
Reference in New Issue
Block a user