Fermenter split into multiple files 2 + Fermenter Program

This commit is contained in:
Patrick Haßel 2025-02-19 12:20:59 +01:00
parent 568880398f
commit 1e1a1c8a5b
23 changed files with 626 additions and 110 deletions

View File

@ -0,0 +1,8 @@
{
"pid": {
"p": 5,
"i": 0,
"d": 0,
"target": 0
}
}

View File

@ -0,0 +1,24 @@
{
"name": "Demo",
"iterations": 1,
"points": [
{
"name": "Aufwärmen",
"start": 20,
"end": 28,
"seconds": 3
},
{
"name": "Halten",
"start": 28,
"end": 28,
"seconds": 3
},
{
"name": "Abkühlen",
"start": 28,
"end": 20,
"seconds": 3
}
]
}

View File

@ -0,0 +1,24 @@
{
"name": "Demo",
"iterations": 2,
"points": [
{
"name": "Aufwärmen",
"start": 20,
"end": 28,
"seconds": 3
},
{
"name": "Halten",
"start": 28,
"end": 28,
"seconds": 3
},
{
"name": "Abkühlen",
"start": 28,
"end": 20,
"seconds": 3
}
]
}

View File

@ -0,0 +1,24 @@
{
"name": "Demo",
"iterations": -1,
"points": [
{
"name": "Aufwärmen",
"start": 20,
"end": 28,
"seconds": 3
},
{
"name": "Halten",
"start": 28,
"end": 28,
"seconds": 3
},
{
"name": "Abkühlen",
"start": 28,
"end": 20,
"seconds": 3
}
]
}

View File

@ -49,6 +49,6 @@ board_build.filesystem = ${common.board_build.filesystem}
monitor_speed = ${common.monitor_speed} monitor_speed = ${common.monitor_speed}
upload_flags = --auth=OtaAuthPatrixFermenter upload_flags = --auth=OtaAuthPatrixFermenter
upload_protocol = ${common.upload_protocol} upload_protocol = ${common.upload_protocol}
upload_port = 10.0.0.164 upload_port = 10.0.0.169
;upload_port = ${common.upload_port} ;upload_port = ${common.upload_port}
;upload_speed = ${common.upload_speed} ;upload_speed = ${common.upload_speed}

View File

@ -1,6 +1,13 @@
#ifdef NODE_FERMENTER #ifdef NODE_FERMENTER
#include "Fermenter.h" #include <patrix/Patrix.h>
#include "config.h"
#include "display.h"
#include "http.h"
#include "pid.h"
#include "Program.h"
#include "rotary.h"
void patrixSetup() { void patrixSetup() {
config.read(); config.read();
@ -18,9 +25,9 @@ void patrixLoop() {
ds18b20.loop(); ds18b20.loop();
temperature.loop(); temperature.loop();
rotary.loop();
pid.loop(); pid.loop();
rotary.loop();
program.loop();
displayLoop(); displayLoop();
} }

View File

@ -1,68 +0,0 @@
#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 <LedControl.h>
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

View File

@ -0,0 +1,7 @@
#ifdef NODE_FERMENTER
#include "Program.h"
Program program;
#endif

View File

@ -0,0 +1,234 @@
#ifndef PROGRAM_H
#define PROGRAM_H
#include <LittleFS.h>
#include "pid.h"
#include "ProgramPoint.h"
class Program {
String name = "";
int iterations = 0;
int iteration = 0;
size_t pointCount = 0;
ProgramPoint* pointList = nullptr;
ProgramPoint* point = nullptr;
bool running = false;
bool paused = false;
unsigned long pauseAlreadyProgressedMillis = 0;
void reset() {
name = "";
iterations = 0;
iteration = 0;
pointCount = 0;
if (pointList != nullptr) {
free(pointList);
pointList = nullptr;
}
point = nullptr;
running = false;
paused = false;
pauseAlreadyProgressedMillis = 0;
}
public:
String wantedName = "";
bool start() {
if (pointList == nullptr || point == nullptr) {
Log.warn("No program loaded.");
return false;
}
if (running) {
Log.warn("Program already running.");
return false;
}
Log.info("Program started: \"%s\"", name.c_str());
running = true;
iteration = 1;
point = pointList;
point->start(0);
return true;
}
bool stop() {
if (pointList == nullptr || point == nullptr) {
Log.warn("No program loaded.");
return false;
}
if (!running) {
Log.warn("Program not running.");
return false;
}
running = false;
pid.setTarget(0);
Log.info("Program stopped.");
return true;
}
bool pause() {
if (pointList == nullptr || point == nullptr) {
Log.warn("No program loaded.");
return false;
}
if (!running) {
Log.warn("Program not running.");
return false;
}
if (paused) {
Log.warn("Program already paused.");
return false;
}
paused = true;
pauseAlreadyProgressedMillis = point->getProgressMillis();
Log.info("Program paused.");
return true;
}
bool resume() {
if (pointList == nullptr || point == nullptr) {
Log.warn("No program loaded.");
return false;
}
if (!running) {
Log.warn("Program not running.");
return false;
}
if (!paused) {
Log.warn("Program not paused.");
return false;
}
paused = false;
point->start(pauseAlreadyProgressedMillis);
Log.info("Program resumed.");
return true;
}
void loop() {
if (!wantedName.isEmpty()) {
load(wantedName);
wantedName = "";
}
if (!running || paused || pointList == nullptr || point == nullptr) {
return;
}
if (point->isComplete()) {
point = (point - pointList + 1) % pointCount + pointList;
if (point == pointList) {
if (iterations < 0) {
Log.info("Program repeat (endless)");
} else if (iteration < iterations) {
iteration++;
Log.info("Program iteration %d/%d", iteration, iterations);
} else {
running = false;
applyTemperature(0, true);
Log.info("Program terminated");
return;
}
}
point->start(0);
}
applyTemperature(point->getTemperature(), false);
}
private:
static void applyTemperature(const double temperature, bool forceLog) {
static auto last = temperature;
forceLog |= abs(temperature - last) >= 1;
if (forceLog) {
last = temperature;
}
pid.setTarget(temperature, forceLog);
}
bool load(const String& programName) {
reset();
const String path = String("/program/") + programName + ".json";
Log.info("Loading program: %s", programName.c_str());
File file = LittleFS.open(path, "r");
if (!file) {
Log.error(" Can't open file: %s", path.c_str());
reset();
return false;
}
JsonDocument json;
deserializeJson(json, file);
file.close();
if (!json.is<JsonObject>()) {
Log.error(" Not a JsonObject.");
reset();
return false;
}
if (json["name"].is<const char*>()) {
Log.info(" %-10s %s", "name", json["name"].as<const char*>());
name = json["name"].as<const char*>();
} else {
Log.error(" Missing attribute: %s", "name");
reset();
return false;
}
if (json["iterations"].is<int>()) {
Log.info(" %-10s %d", "iterations", json["iterations"].as<int>());
iterations = json["iterations"].as<int>();
} else {
Log.error(" Missing attribute: %s", "iterations");
reset();
return false;
}
const JsonArray& jsonPoints = json["points"].as<JsonArray>();
pointCount = jsonPoints.size();
if (pointCount > 0) {
Log.info(" %-10s %d", "points", pointCount);
pointList = static_cast<ProgramPoint*>(malloc(sizeof(ProgramPoint) * pointCount));
if (pointList == nullptr) {
Log.error("Failed to allocate ProgramPoint memory");
reset();
return false;
}
point = pointList;
} else {
Log.error(" Missing attribute: %s", "points");
reset();
return false;
}
int index = 0;
for (JsonVariant point : jsonPoints) {
if (!pointList[index].load(point, index)) {
reset();
return false;
}
index++;
}
Log.info("Program loaded: %s", path.c_str());
return true;
}
};
extern Program program;
#endif

View File

@ -0,0 +1,130 @@
#ifndef PROGRAM_POINT_H
#define PROGRAM_POINT_H
#include <ArduinoJson.h>
#include <patrix/mqtt.h>
inline String durationString(const unsigned long millis) {
const unsigned long seconds = millis / 1000;
const unsigned long minutes = seconds / 60;
const unsigned long hours = minutes / 60;
const unsigned long days = hours / 24;
char buffer[15];
if (days > 0) {
snprintf(buffer, sizeof buffer, "%d. %2d:%02d:%02d", days, hours % 24, minutes % 60, seconds % 60);
} else {
snprintf(buffer, sizeof buffer, "%d:%02d:%02d", hours % 24, minutes % 60, seconds % 60);
}
return {buffer};
}
class ProgramPoint {
public:
int index = -1;
String name = "";
double startTemperature = 0;
double endTemperature = 0;
unsigned long startMillis = 0;
unsigned long durationMillis = 0;
bool load(const JsonVariant& point, const int index) {
reset();
this->index = index;
Log.info(" #%d:", index);
if (!point.is<JsonObject>()) {
Log.error(" Not a JsonObject");
reset();
return false;
}
if (point["name"].is<const char*>()) {
Log.info(" %-10s %s", "name", point["name"].as<const char*>());
name = point["name"].as<const char*>();
} else {
Log.error(" Missing attribute: %s", "name");
reset();
return false;
}
if (point["start"].is<double>()) {
Log.info(" %-10s %.1f%cC", "start", point["start"].as<double>(), 176);
startTemperature = point["start"].as<double>();
} else {
Log.error(" Missing attribute: %s", "start");
reset();
return false;
}
if (point["end"].is<double>()) {
Log.info(" %-10s %.1f%cC", "end", point["end"].as<double>(), 176);
endTemperature = point["end"].as<double>();
} else {
Log.error(" Missing attribute: %s", "end");
reset();
return false;
}
if (point["seconds"].is<unsigned long>()) {
Log.info(" %-10s %d", "seconds", point["seconds"].as<unsigned long>());
durationMillis = point["seconds"].as<unsigned long>() * 1000;
} else if (point["minutes"].is<unsigned long>()) {
Log.info(" %-10s %d", "minutes", point["minutes"].as<unsigned long>());
durationMillis = point["minutes"].as<unsigned long>() * 60 * 1000;
} else if (point["hours"].is<unsigned long>()) {
Log.info(" %-10s %d", "hours", point["hours"].as<unsigned long>());
durationMillis = point["hours"].as<unsigned long>() * 60 * 60 * 1000;
} else if (point["days"].is<unsigned long>()) {
Log.info(" %-10s %d", "days", point["days"].as<unsigned long>());
durationMillis = point["days"].as<unsigned long>() * 24 * 60 * 60 * 1000;
} else {
Log.error(" Missing attribute: %s", "seconds/minutes/hours/days");
reset();
return false;
}
return true;
}
void reset() {
index = -1;
name = "";
startTemperature = 0;
endTemperature = 0;
startMillis = 0;
durationMillis = 0;
}
void start(const unsigned long alreadyProgressedMillis) {
startMillis = max(1UL, millis() - alreadyProgressedMillis);
if (alreadyProgressedMillis == 0) {
Log.info("Starting ProgramPoint: #%d %s, %.1f%cC -> %.1f%cC: \"%s\"", index, durationString(durationMillis), startTemperature, 176, endTemperature, 176, name.c_str());
} else {
Log.info("Resuming ProgramPoint: #%d %s, %.1f%cC -> %.1f%cC: \"%s\", resumeAt=%s", index, durationString(durationMillis), startTemperature, 176, endTemperature, 176, name.c_str(), durationString(alreadyProgressedMillis));
}
}
[[nodiscard]] bool isComplete() const {
return millis() - startMillis >= durationMillis;
}
[[nodiscard]] unsigned long getProgressMillis() const {
return max(0UL, min(durationMillis, millis() - startMillis));
}
[[nodiscard]] double getTemperature() const {
const auto progressMillis = getProgressMillis();
const auto progressRatio = static_cast<double>(progressMillis) / static_cast<double>(durationMillis);
return startTemperature + (endTemperature - startTemperature) * progressRatio;
}
};
#endif

View File

@ -1,6 +1,7 @@
#ifdef NODE_FERMENTER #ifdef NODE_FERMENTER
#include "Fermenter.h" #include "config.h"
#include "pid.h"
Config config("/config.json", configCollect, configApply); Config config("/config.json", configCollect, configApply);

View File

@ -0,0 +1,12 @@
#ifndef CONFIG_H
#define CONFIG_H
#include "patrix/Config.h"
extern Config config;
void configCollect(JsonDocument& json);
void configApply(JsonDocument& json);
#endif

View File

@ -1,6 +1,7 @@
#ifdef NODE_FERMENTER #ifdef NODE_FERMENTER
#include "Fermenter.h" #include "display.h"
#include "pid.h"
LedControl display(D7, D5, D8, 1); LedControl display(D7, D5, D8, 1);
@ -16,8 +17,7 @@ void displayPrintf(const char* format, ...) {
auto thisChar = *b; auto thisChar = *b;
if (thisChar == 'z' || thisChar == 'Z') { if (thisChar == 'z' || thisChar == 'Z') {
thisChar = '2'; thisChar = '2';
} } else if (thisChar == 'i' || thisChar == 'I') {
else if (thisChar == 'i' || thisChar == 'I') {
thisChar = '1'; thisChar = '1';
} }
const auto nextIsDot = *(b + 1) == '.'; const auto nextIsDot = *(b + 1) == '.';
@ -47,8 +47,7 @@ void displayLoop() {
if (displayModifyTarget != 0) { if (displayModifyTarget != 0) {
displayPrintf("ZIEL %4.1f", pid.getTarget()); displayPrintf("ZIEL %4.1f", pid.getTarget());
} } else {
else {
displayPrintf("%4.1f %4.1f", temperature.getValue(), pid.getTarget()); displayPrintf("%4.1f %4.1f", temperature.getValue(), pid.getTarget());
} }
} }

View File

@ -0,0 +1,14 @@
#ifndef DISPLAY_H
#define DISPLAY_H
#include <LedControl.h>
extern unsigned long displayModifyTarget;
extern LedControl display;
void displayPrintf(const char* format, ...);
void displayLoop();
#endif

View File

@ -1,16 +1,19 @@
#ifdef NODE_FERMENTER #ifdef NODE_FERMENTER
#include "Fermenter.h" #include "http.h"
#include "config.h"
#include "pid.h"
#include "Program.h"
void httpStatus(AsyncWebServerRequest* request) { void httpStatus(AsyncWebServerRequest* request) {
JsonDocument json; JsonDocument json;
json["pid"]["p"] = pid.p; json["pid"]["p"] = pid.p;
json["pid"]["i"] = pid.i; json["pid"]["i"] = pid.i;
json["pid"]["d"] = pid.d; json["pid"]["d"] = pid.d;
json["pid"]["target"] = pid.getTarget(); json["pid"]["target"] = pid.getTarget();
json["temperature"] = temperature.getValue(); json["temperature"] = temperature.getValue();
json["heater"]["percent"] = heater.getPercent(); json["heater"]["percent"] = heater.getPercent();
json["heater"]["powerW"] = heater.getPercent() / 100.0 * HEATER_POWER_W; json["heater"]["powerW"] = heater.getPercent() / 100.0 * HEATER_POWER_W;
AsyncResponseStream* stream = request->beginResponseStream("application/json"); AsyncResponseStream* stream = request->beginResponseStream("application/json");
serializeJson(json, *stream); serializeJson(json, *stream);
@ -89,6 +92,43 @@ void httpConfigSet(AsyncWebServerRequest* request) {
httpStatus(request); httpStatus(request);
} }
void httpProgramLoad(AsyncWebServerRequest* request) {
const auto nameParam = request->getParam("name");
if (nameParam == nullptr) {
Log.error("Missing parameter: name (1)");
return;
}
const auto nameString = nameParam->value();
if (nameString == nullptr) {
Log.error("Missing parameter: name (2)");
return;
}
program.wantedName = nameString;
request->send(200);
}
void httpProgramStart(AsyncWebServerRequest* request) {
program.start();
request->send(200);
}
void httpProgramStop(AsyncWebServerRequest* request) {
program.stop();
request->send(200);
}
void httpProgramPause(AsyncWebServerRequest* request) {
program.pause();
request->send(200);
}
void httpProgramResume(AsyncWebServerRequest* request) {
program.resume();
request->send(200);
}
void httpSetup2() { void httpSetup2() {
server.on("/status", httpStatus); server.on("/status", httpStatus);
server.on("/status/", httpStatus); server.on("/status/", httpStatus);
@ -96,6 +136,16 @@ void httpSetup2() {
server.on("/target/add/", httpTargetAdd); server.on("/target/add/", httpTargetAdd);
server.on("/config/set", httpConfigSet); server.on("/config/set", httpConfigSet);
server.on("/config/set/", httpConfigSet); server.on("/config/set/", httpConfigSet);
server.on("/program/load", httpProgramLoad);
server.on("/program/load/", httpProgramLoad);
server.on("/program/start", httpProgramStart);
server.on("/program/start/", httpProgramStart);
server.on("/program/stop", httpProgramStop);
server.on("/program/stop/", httpProgramStop);
server.on("/program/pause", httpProgramPause);
server.on("/program/pause/", httpProgramPause);
server.on("/program/resume", httpProgramResume);
server.on("/program/resume/", httpProgramResume);
} }
#endif #endif

12
src/node/Fermenter/http.h Normal file
View File

@ -0,0 +1,12 @@
#ifndef HTTP_H
#define HTTP_H
#include "patrix/http.h"
void httpStatus(AsyncWebServerRequest* request);
void httpTargetAdd(AsyncWebServerRequest* request);
void httpSetup2();
#endif

View File

@ -1,6 +1,7 @@
#ifdef NODE_FERMENTER #ifdef NODE_FERMENTER
#include "Fermenter.h" #include "pid.h"
#include "config.h"
DS18B20 ds18b20( DS18B20 ds18b20(
"DS18B20", "DS18B20",

20
src/node/Fermenter/pid.h Normal file
View File

@ -0,0 +1,20 @@
#ifndef PID_H
#define PID_H
#include "patrix/DS18B20Sensor.h"
#include "patrix/PIDController.h"
#include "patrix/PWMOutput.h"
#define HEATER_POWER_W 30
extern DS18B20 ds18b20;
extern DS18B20Sensor temperature;
extern PWMOutput heater;
extern PIDController pid;
void addTarget(double delta);
#endif

View File

@ -1,12 +1,14 @@
#ifdef NODE_FERMENTER #ifdef NODE_FERMENTER
#include "Fermenter.h" #include "rotary.h"
#include "display.h"
Rotary rotary(D1, D6, rotaryCallback); #include "pid.h"
void rotaryCallback(const int delta) { void rotaryCallback(const int delta) {
addTarget(delta); pid.addTarget(delta);
displayModifyTarget = millis(); displayModifyTarget = millis();
} }
Rotary rotary(D1, D6, rotaryCallback);
#endif #endif

View File

@ -0,0 +1,8 @@
#ifndef ROTARY_H
#define ROTARY_H
#include "patrix/Rotary.h"
extern Rotary rotary;
#endif

View File

@ -1,8 +1,9 @@
#ifndef CONFIG_H #ifndef PATRIX_CONFIG_H
#define CONFIG_H #define PATRIX_CONFIG_H
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <LittleFS.h> #include <LittleFS.h>
#include "mqtt.h" #include "mqtt.h"
class Config { class Config {
@ -28,8 +29,8 @@ public:
unsigned long writeDelayMillis = 10 * 1000UL; unsigned long writeDelayMillis = 10 * 1000UL;
explicit Config(String path, const handle_json_t collect, const handle_json_t apply): explicit Config(String path, const handle_json_t collect, const handle_json_t apply):
path(std::move(path)), path(std::move(path)),
collect(collect), apply(apply) { collect(collect), apply(apply) {
// //
} }

View File

@ -43,18 +43,18 @@ public:
double d = 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) : 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)), targetName(std::move(targetName)),
inputName(std::move(inputName)), inputName(std::move(inputName)),
outputName(std::move(outputName)), outputName(std::move(outputName)),
input(sensor), input(sensor),
output(pwmOutput), output(pwmOutput),
unit(unit), unit(unit),
controller(), controller(),
minValue(minValue), minValue(minValue),
maxValue(maxValue), maxValue(maxValue),
p(p), p(p),
i(i), i(i),
d(d) { d(d) {
// //
} }
@ -90,9 +90,15 @@ public:
return targetValue; return targetValue;
} }
double setTarget(double target) { double setTarget(const double newTargetValue) {
targetValue = max(minValue, min(maxValue, target)); return setTarget(newTargetValue, true);
Log.info("PID %s = %.1f", targetName.c_str(), targetValue); }
double setTarget(const double newTargetValue, const bool doLog) {
targetValue = max(minValue, min(maxValue, newTargetValue));
if (doLog) {
Log.info("PID %s = %.1f", targetName.c_str(), targetValue);
}
return targetValue; return targetValue;
} }

View File

@ -7,7 +7,7 @@
#define WIFI_SSID "HappyNet" #define WIFI_SSID "HappyNet"
#define WIFI_PASSWORD "1Grausame!Sackratte7" #define WIFI_PASSWORD "1Grausame!Sackratte7"
#define NTP_SERVER "107.189.12.98" /* pool.ntp.org */ #define NTP_SERVER "107.189.12.98" /* pool.ntp.org */
#define BOOT_DELAY_SEC 5 #define BOOT_DELAY_SEC 1
auto wifiConnected = false; auto wifiConnected = false;