Fermenter restructure + huge code clean

This commit is contained in:
Patrick Haßel 2024-04-25 10:50:29 +02:00
parent 0cc485577c
commit 1bde26489b
18 changed files with 247 additions and 232 deletions

View File

@ -79,6 +79,20 @@ void configPutUint64_t(const char *name, uint64_t value) {
}
}
uint8_t configGetUint8_t(const char *name, uint8_t fallback) {
if (!config.containsKey(name)) {
return fallback;
}
return config[name];
}
void configPutUint8_t(const char *name, uint8_t value) {
if (config[name] != value) {
config[name] = value;
lastChangeMillis = millis();
}
}
void configPrint() {
info("Config (%s):", lastChangeMillis == 0 ? "PERSISTED" : "TRANSIENT");
for (JsonPair pair: config.as<JsonObject>()) {

View File

@ -9,20 +9,27 @@ void configLoop();
void configReset();
void configLoaded();
String configGetString(const char *name, const char *fallback, bool allowEmpty);
void configPutString(const char *name, const char *value);
double configGetDouble(const char *name, double fallback);
void configPutDouble(const char *name, double value);
uint64_t configGetUint64_t(const char *name, uint64_t fallback);
void configPutUint64_t(const char *name, uint64_t value);
uint8_t configGetUint8_t(const char *name, uint8_t fallback);
void configPutUint8_t(const char *name, uint8_t value);
void configPrint();
#endif

View File

@ -84,7 +84,7 @@ void consoleHandle(char *cmd) {
_debug();
} else if (strcmp(first, "config_reset") == 0) {
configReset();
} else if (!patrix_command((char *) cmd)) {
} else if (!patrixCommand((char *) cmd)) {
info("Unknown command: %s", cmd);
}
}
@ -249,7 +249,7 @@ const char *getFlashChipMode() {
}
}
bool setDouble(const char *name, double *destinationPtr, double min, double max) {
bool cmdReadDouble(const char *name, double *destinationPtr, double min, double max) {
const char *valueStr = strtok(nullptr, "");
if (valueStr == nullptr) {
consolePrintUsage();
@ -274,7 +274,7 @@ bool setDouble(const char *name, double *destinationPtr, double min, double max)
return true;
}
bool setUint64_T(const char *name, uint64_t *destinationPtr, uint64_t min, uint64_t max) {
bool cmdReadU64(const char *name, uint64_t *destinationPtr, uint64_t min, uint64_t max) {
const char *valueStr = strtok(nullptr, "");
if (valueStr == nullptr) {
consolePrintUsage();

View File

@ -9,12 +9,12 @@ void consoleLoop();
void consoleHandle(char *cmd);
bool patrix_command(char *cmd);
bool patrixCommand(char *cmd);
void consolePrintUsage();
bool setDouble(const char *name, double *destinationPtr, double min, double max);
bool cmdReadDouble(const char *name, double *destinationPtr, double min, double max);
bool setUint64_T(const char *name, uint64_t *destinationPtr, uint64_t min, uint64_t max);
bool cmdReadU64(const char *name, uint64_t *destinationPtr, uint64_t min, uint64_t max);
#endif

View File

@ -8,6 +8,12 @@
struct IData {
time_t time;
explicit IData(time_t time) : time(time) {
// -
}
virtual void toJson(JsonObject &json) const = 0;
};
@ -20,16 +26,13 @@ class Cache {
private:
struct Entry {
time_t timestamp = 0;
T data;
};
T buffer[size];
Entry buffer[size];
T *bufferRead = buffer;
Entry *bufferRead = buffer;
T *bufferWrite = buffer;
Entry *bufferWrite = buffer;
T last{};
size_t usage = 0;
@ -40,16 +43,16 @@ public:
// -
}
bool add(const time_t timestamp, const T &data) {
if (usage >= size) {
return false;
}
bufferWrite->timestamp = timestamp;
bufferWrite->data = data;
bool add(T data) {
if (usage < size && data.needsPublish(last)) {
last = data;
*bufferWrite = data;
bufferWrite = (bufferWrite - buffer + 1) % size + buffer;
usage++;
return true;
}
return false;
}
void loop() {
if (usage == 0 || !isTimeSet()) {
@ -57,8 +60,9 @@ public:
}
JsonDocument doc;
JsonObject json = doc.to<JsonObject>();
json["timestamp"] = correctTime(bufferRead->timestamp);
bufferRead->data.toJson(json);
correctTime(bufferRead->time);
json["timestamp"] = bufferRead->time;
bufferRead->toJson(json);
if (mqttPublishData(json)) {
bufferRead = (bufferRead - buffer + 1) % size + buffer;
usage--;

View File

@ -15,36 +15,22 @@ private:
uint64_t address;
const double valueThreshold;
const time_t timeoutSec;
const time_t minIntervalMillis;
double lastSentValue = NAN;
unsigned long lastSentMillis = 0;
double lastValue = NAN;
time_t lastTimestamp = 0;
double lastValidValue = NAN;
time_t lastValidTimestamp = 0;
public:
DallasSensor(Dallas &sensors, const uint64_t address, const double valueThreshold, const time_t timeoutSec, const time_t minIntervalMillis) :
DallasSensor(Dallas &sensors, const uint64_t address, const time_t timeoutSec) :
sensors(sensors),
address(address),
valueThreshold(valueThreshold),
timeoutSec(timeoutSec),
minIntervalMillis(minIntervalMillis) {
timeoutSec(timeoutSec) {
// -
}
bool loop() {
void loop() {
const time_t now = getTime();
if (now - lastTimestamp > timeoutSec) {
lastTimestamp = 0;
@ -53,21 +39,9 @@ public:
if (sensors.isConverted()) {
const double value = sensors.read(address);
const time_t timestamp = sensors.getTimestamp();
const unsigned long millisNow = millis();
const bool doPublish = !isnan(value) && (isnan(lastSentValue) || abs(lastSentValue - value) >= valueThreshold || millisNow - lastSentMillis >= minIntervalMillis);
if (doPublish) {
lastSentValue = value;
lastSentMillis = millisNow;
}
lastValue = value;
lastTimestamp = timestamp;
if (!isnan(value)) {
lastValidValue = value;
lastValidTimestamp = timestamp;
}
return doPublish;
}
return false;
}
[[nodiscard]] double getLastValue() const {
@ -78,14 +52,6 @@ public:
return lastTimestamp;
}
[[nodiscard]] double getLastValidValue() const {
return lastValidValue;
}
[[nodiscard]] time_t getLastValidTimestamp() const {
return lastValidTimestamp;
}
};
#endif

View File

@ -195,18 +195,10 @@ time_t getTime() {
return epochSeconds;
}
time_t correctTime(const time_t value) {
if (!timeSet) {
return value;
void correctTime(time_t &value) {
if (timeSet && value < MIN_EPOCH_SECONDS) {
value = getTime() - preTimeOffset + value;
}
if (value < MIN_EPOCH_SECONDS) {
return getTime() - preTimeOffset + value;
}
return value;
}
bool isOlderThan(time_t time, time_t seconds) {
return getTime() > correctTime(time) + seconds;
}
void getDateTime(char *buffer, size_t size) {

View File

@ -15,9 +15,7 @@ bool isTimeSet();
time_t getTime();
time_t correctTime(time_t value);
bool isOlderThan(time_t time, time_t seconds);
void correctTime(time_t &value);
void getDateTime(char *buffer, size_t size);

View File

@ -16,6 +16,7 @@ lib_deps = https://github.com/milesburton/Arduino-Temperature-Control-Library
Wire
https://github.com/phassel/ArduPID/
https://github.com/wayoda/LedControl
https://github.com/me-no-dev/ESPAsyncWebServer
[env:TEST32]
upload_port = 10.42.0.66

View File

@ -0,0 +1,29 @@
#include "FementerDisplay.h"
#include "LedControl.h"
LedControl lc = LedControl(13, 14, 15, 1);
void writeDecimal(int *digit, double value) {
const int integer = (int) value;
const int decimal = (int) ((value - integer) * 10) % 10;
lc.setDigit(0, (*digit)++, decimal, false);
lc.setDigit(0, (*digit)++, integer % 10, true);
lc.setDigit(0, (*digit)++, integer / 10 % 10, false);
}
void displayLoop(double temperature, double target) {
static unsigned long lastDisplayInit = 0;
if (lastDisplayInit == 0 || millis() - lastDisplayInit > 60 * 60 * 1000) {
lastDisplayInit = millis();
lc.shutdown(0, true);
lc.shutdown(0, false);
lc.setIntensity(0, 4);
lc.clearDisplay(0);
}
int digit = 0;
writeDecimal(&digit, temperature);
digit++;
digit++;
writeDecimal(&digit, target);
}

View File

@ -0,0 +1,6 @@
#ifndef SENSOR3_FEMENTERDISPLAY_H
#define SENSOR3_FEMENTERDISPLAY_H
void displayLoop(double temperature, double target);
#endif

View File

@ -1,170 +1,38 @@
#if defined(FERMENTER) || defined(TEST8266)
#include <Patrix.h>
#include "sensors/Dallas.h"
#include "sensors/DallasSensor.h"
#include "ArduPID.h"
#include "LedControl.h"
#include "FermenterData.h"
#define SENSOR_GPIO D4
#define CONTROL_GPIO D2
#define CONTROL_PWM_BITS 10
#define CONTROL_PWM_MAX (pow(2, CONTROL_PWM_BITS) - 1)
#define PID_DEFAULT_P 1500
#define PID_DEFAULT_I 0
#define PID_DEFAULT_D 0
#define PID_DEFAULT_TARGET 31
#define PID_DEFAULT_OVER 5
Dallas dallas(SENSOR_GPIO);
DallasSensor sensor(dallas, 0x3D0417C1D740FF28, 0.1, 5, 60 * 1000);
LedControl lc = LedControl(13, 14, 15, 1);
ArduPID pid;
double proportional = PID_DEFAULT_P;
double integral = PID_DEFAULT_I;
double derivative = PID_DEFAULT_D;
double temperatureTarget = PID_DEFAULT_TARGET;
double temperatureOver = PID_DEFAULT_OVER;
double temperature = NAN;
double heaterPWM = 0;
uint8_t heaterPercent = 0;
#include "FementerDisplay.h"
#include "FermenterPID.h"
#include "FermenterSensor.h"
Cache<FermenterData, 800> cache;
void writeDecimal(int *digit, double value);
void displayLoop();
void pidLoop();
void patrixSetup() {
dallas.begin();
analogWriteResolution(CONTROL_PWM_BITS);
configLoaded();
pid.begin(&temperature, &heaterPWM, &temperatureTarget, proportional, integral, derivative);
pid.setOutputLimits(0, CONTROL_PWM_MAX);
pid.start();
sensorSetup();
pidSetup(&temperature);
}
void patrixLoop() {
dallas.loop();
bool doPublish = sensor.loop();
displayLoop();
sensorLoop();
pidLoop();
if (doPublish) {
const FermenterData data = {(float) sensor.getLastValue(), (float) temperatureTarget, heaterPercent, (float) proportional, (float) integral, (float) derivative};
cache.add(sensor.getLastTimestamp(), data);
}
displayLoop(temperature, target);
cache.add({getTime(), (float) temperature, (float) target, getHeaterPercent(), (float) proportional, (float) integral, (float) derivative});
cache.loop();
}
void pidLoop() {
temperature = sensor.getLastValue();
if (!isnan(temperature)) {
pid.compute();
} else {
heaterPWM = 0;
}
const char *emergencyStr = "";
if (heaterPWM > 0 && temperature > temperatureTarget + temperatureOver) {
heaterPWM = 0;
emergencyStr = "[EMERGENCY CUTOFF]";
}
analogWrite(CONTROL_GPIO, (int) round(heaterPWM));
heaterPercent = (int) round(100.0 * heaterPWM / CONTROL_PWM_MAX);
static unsigned long lastDebug = 0;
unsigned long now = millis();
if (now - lastDebug >= 1000) {
lastDebug = now;
debug(
"cache: %3d/%d | p: %f | i: %.12f | d: %.12f | target: %4.1f | heater: %3d%% | temperature: %5.2f %s",
cache.getUsage(),
cache.getSize(),
proportional == 0 ? NAN : proportional,
integral == 0 ? NAN : integral,
derivative == 0 ? NAN : derivative,
temperatureTarget,
heaterPercent,
temperature,
emergencyStr
);
}
}
void displayLoop() {
static unsigned long lastDisplayInit = 0;
if (lastDisplayInit == 0 || millis() - lastDisplayInit > 60 * 60 * 1000) {
lastDisplayInit = millis();
lc.shutdown(0, true);
lc.shutdown(0, false);
lc.setIntensity(0, 4);
lc.clearDisplay(0);
}
int digit = 0;
writeDecimal(&digit, temperature);
digit++;
digit++;
writeDecimal(&digit, temperatureTarget);
}
void writeDecimal(int *digit, double value) {
const int integer = (int) value;
const int decimal = (int) ((value - integer) * 10) % 10;
lc.setDigit(0, (*digit)++, decimal, false);
lc.setDigit(0, (*digit)++, integer % 10, true);
lc.setDigit(0, (*digit)++, integer / 10 % 10, false);
}
bool patrix_command(char *first) {
bool patrixCommand(char *first) {
bool result = false;
if (strcmp(first, "over") == 0 || strcmp(first, "o") == 0) {
result = setDouble("over", &temperatureOver, 0, 20);
} else if (strcmp(first, "target") == 0 || strcmp(first, "t") == 0) {
result = setDouble("target", &temperatureTarget, -10, 60);
} else if (strcmp(first, "proportional") == 0 || strcmp(first, "p") == 0) {
result = setDouble("p", &proportional, NAN, NAN);
} else if (strcmp(first, "integral") == 0 || strcmp(first, "i") == 0) {
result = setDouble("i", &integral, NAN, NAN);
} else if (strcmp(first, "derivative") == 0 || strcmp(first, "d") == 0) {
result = setDouble("d", &derivative, NAN, NAN);
} else if (strcmp(first, "pid_reset") == 0) {
pid.reset();
result = true;
}
if (result) {
pid.setCoefficients(proportional, integral, derivative);
if (strcmp(first, "p") == 0) {
result = cmdReadDouble("p", &proportional, 0, NAN);
} else if (strcmp(first, "i") == 0) {
result = cmdReadDouble("i", &integral, 0, NAN);
} else if (strcmp(first, "d") == 0) {
result = cmdReadDouble("d", &derivative, 0, NAN);
} else if (strcmp(first, "t") == 0) {
result = cmdReadDouble("t", &target, -10, 60);
}
return result;
}
void configLoaded() {
proportional = configGetDouble("p", PID_DEFAULT_P);
integral = configGetDouble("i", PID_DEFAULT_I);
derivative = configGetDouble("d", PID_DEFAULT_D);
temperatureTarget = configGetDouble("target", PID_DEFAULT_TARGET);
temperatureOver = configGetDouble("over", PID_DEFAULT_OVER);
pid.setCoefficients(proportional, integral, derivative);
}
#endif

View File

@ -11,7 +11,7 @@ struct FermenterData : IData {
float i;
float d;
FermenterData() {
FermenterData() : IData(0) {
currentCelsius = NAN;
targetCelsius = NAN;
heaterPercent = 0;
@ -21,6 +21,7 @@ struct FermenterData : IData {
}
FermenterData(
time_t time,
float currentCelsius,
float targetCelsius,
uint8_t heaterPercent,
@ -28,6 +29,7 @@ struct FermenterData : IData {
float i,
float d
) :
IData(time),
currentCelsius(currentCelsius),
targetCelsius(targetCelsius),
heaterPercent(heaterPercent),
@ -46,6 +48,18 @@ struct FermenterData : IData {
json["d"] = d;
}
[[nodiscard]] bool needsPublish(FermenterData &last) {
correctTime(time);
correctTime(last.time);
return time - last.time >= 60
|| this->p != last.p
|| this->i != last.i
|| this->d != last.d
|| abs(this->currentCelsius - last.currentCelsius) >= 0.1
|| this->targetCelsius != last.targetCelsius
|| this->heaterPercent != last.heaterPercent;
}
};
#endif

View File

@ -0,0 +1,62 @@
#include "FermenterPID.h"
#include "log.h"
#include "config.h"
#define CONTROL_GPIO D2
#define CONTROL_PWM_BITS 10
#define CONTROL_PWM_MAX (pow(2, CONTROL_PWM_BITS) - 1)
ArduPID pid;
double proportional = 0;
double integral = 0;
double derivative = 0;
double target = 0;
double heater = 0;
void pidSetup(double *temperature) {
analogWriteResolution(CONTROL_PWM_BITS);
proportional = configGetDouble("p", 0);
integral = configGetDouble("i", 0);
derivative = configGetDouble("d", 0);
target = configGetDouble("t", 0);
pid.begin(temperature, &heater, &target, proportional, integral, derivative);
pid.setOutputLimits(0, CONTROL_PWM_MAX);
}
void pidLoop() {
if (!isnan(*pid.input)) {
pid.setCoefficients(proportional, integral, derivative);
pid.compute();
} else {
heater = 0;
}
analogWrite(CONTROL_GPIO, (int) round(heater));
static unsigned long lastDebug = 0;
unsigned long now = millis();
if (now - lastDebug >= 1000) {
lastDebug = now;
debug(
"p: %f | i: %.12f | d: %.12f | target: %4.1f | heater: %3d%% | temperature: %5.2f",
proportional == 0 ? NAN : proportional,
integral == 0 ? NAN : integral,
derivative == 0 ? NAN : derivative,
*pid.setpoint,
getHeaterPercent(),
*pid.input
);
}
}
uint8_t getHeaterPercent() {
return (int) round(100.0 * heater / CONTROL_PWM_MAX);
}

View File

@ -0,0 +1,22 @@
#ifndef SENSOR3_FERMENTERPID_H
#define SENSOR3_FERMENTERPID_H
#include "ArduPID.h"
extern ArduPID pid;
extern double proportional;
extern double integral;
extern double derivative;
extern double target;
void pidSetup(double *temperature);
void pidLoop();
uint8_t getHeaterPercent();
#endif

View File

@ -0,0 +1,22 @@
#include "FermenterSensor.h"
#include "sensors/Dallas.h"
#include "sensors/DallasSensor.h"
#define SENSOR_GPIO D4
Dallas dallas(SENSOR_GPIO);
DallasSensor sensor(dallas, 0x3D0417C1D740FF28, 5);
double temperature = NAN;
void sensorSetup() {
dallas.begin();
}
void sensorLoop() {
dallas.loop();
sensor.loop();
temperature = sensor.getLastValue();
}

View File

@ -0,0 +1,10 @@
#ifndef SENSOR3_FERMENTERSENSOR_H
#define SENSOR3_FERMENTERSENSOR_H
extern double temperature;
void sensorSetup();
void sensorLoop();
#endif

View File

@ -156,20 +156,20 @@ void moistureLoop() {
}
}
bool patrix_command(char *first) {
bool patrixCommand(char *first) {
bool result = false;
if (strcmp(first, "tempHigh") == 0 || strcmp(first, "th") == 0) {
result = setDouble("tempHigh", &tempHigh, NAN, NAN);
result = cmdReadDouble("tempHigh", &tempHigh, NAN, NAN);
} else if (strcmp(first, "tempLow") == 0 || strcmp(first, "tl") == 0) {
result = setDouble("tempLow", &tempLow, NAN, NAN);
result = cmdReadDouble("tempLow", &tempLow, NAN, NAN);
} else if (strcmp(first, "tempMillis") == 0 || strcmp(first, "tm") == 0) {
result = setUint64_T("tempMillis", &tempMillis, NAN, NAN);
result = cmdReadU64("tempMillis", &tempMillis, NAN, NAN);
} else if (strcmp(first, "moistHigh") == 0 || strcmp(first, "mh") == 0) {
result = setDouble("moistHigh", &moistHigh, NAN, NAN);
result = cmdReadDouble("moistHigh", &moistHigh, NAN, NAN);
} else if (strcmp(first, "moistLow") == 0 || strcmp(first, "ml") == 0) {
result = setDouble("moistLow", &moistLow, NAN, NAN);
result = cmdReadDouble("moistLow", &moistLow, NAN, NAN);
} else if (strcmp(first, "moistMillis") == 0 || strcmp(first, "mm") == 0) {
result = setUint64_T("moistMillis", &moistMillis, NAN, NAN);
result = cmdReadU64("moistMillis", &moistMillis, NAN, NAN);
}
return result;
}