Fermenter split into multiple files + Fermenter Config
This commit is contained in:
parent
b9d96d3f02
commit
568880398f
@ -1,193 +1,215 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Gärbox</title>
|
<title>Gärbox</title>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
font-size: 16vw;
|
font-size: 16vw;
|
||||||
margin: 0.1em;
|
margin: 0.1em;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
-ms-user-select: none;
|
-ms-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section {
|
.section {
|
||||||
clear: both;
|
clear: both;
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 50%;
|
font-size: 50%;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: gray
|
color: gray
|
||||||
}
|
}
|
||||||
|
|
||||||
.valueAndUnit {
|
.valueAndUnit {
|
||||||
clear: both;
|
clear: both;
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controlPadding {
|
.controlPadding {
|
||||||
float: left;
|
float: left;
|
||||||
padding: 0 0.25em;
|
padding: 0 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.value {
|
.value {
|
||||||
float: left;
|
float: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.unit {
|
.unit {
|
||||||
float: left;
|
float: left;
|
||||||
width: 1.2em;
|
width: 1.2em;
|
||||||
margin-left: 0.25em;
|
margin-left: 0.25em;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
float: left;
|
float: left;
|
||||||
width: 1em;
|
width: 1em;
|
||||||
margin-top: 0.2em;
|
margin-top: 0.2em;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputNull {
|
.inputNull {
|
||||||
color: gray;
|
color: gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputCold {
|
.inputCold {
|
||||||
color: blue;
|
color: blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputGood {
|
.inputGood {
|
||||||
color: green;
|
color: green;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputWarm {
|
.inputWarm {
|
||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="title">Ist-Temperatur</div>
|
<div class="title">Ist-Temperatur</div>
|
||||||
<div class="valueAndUnit">
|
<div class="valueAndUnit">
|
||||||
<div class="value" id="input"></div>
|
<div class="value" id="temperature"></div>
|
||||||
|
<div class="unit">°C</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="title">Ziel-Temperatur</div>
|
||||||
|
<div class="valueAndUnit">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="blue" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" onclick="targetAdd(-0.5)">
|
||||||
|
<path d="M7 12H17"/>
|
||||||
|
<circle cx="12" cy="12" r="10"/>
|
||||||
|
</svg>
|
||||||
|
<div class="controlPadding">
|
||||||
|
<div class="value" id="target"></div>
|
||||||
<div class="unit">°C</div>
|
<div class="unit">°C</div>
|
||||||
</div>
|
</div>
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="red" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" onclick="targetAdd(+0.5)">
|
||||||
|
<path d="M12 7V17M7 12H17"/>
|
||||||
|
<circle cx="12" cy="12" r="10"/>
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="title">Ziel-Temperatur</div>
|
<div class="title">Heizung</div>
|
||||||
<div class="valueAndUnit">
|
<div class="valueAndUnit">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="blue" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" onclick="targetAdd(-0.5)">
|
<div class="value" id="heaterPercent"></div>
|
||||||
<path d="M7 12H17"/>
|
<div class="unit">%</div>
|
||||||
<circle cx="12" cy="12" r="10"/>
|
|
||||||
</svg>
|
|
||||||
<div class="controlPadding">
|
|
||||||
<div class="value" id="target"></div>
|
|
||||||
<div class="unit">°C</div>
|
|
||||||
</div>
|
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="red" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" onclick="targetAdd(+0.5)">
|
|
||||||
<path d="M12 7V17M7 12H17"/>
|
|
||||||
<circle cx="12" cy="12" r="10"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="valueAndUnit">
|
||||||
<div class="section">
|
<div class="value" id="heaterPowerW"></div>
|
||||||
<div class="title">Heizung</div>
|
<div class="unit">W</div>
|
||||||
<div class="valueAndUnit">
|
|
||||||
<div class="value" id="outputPercent"></div>
|
|
||||||
<div class="unit">%</div>
|
|
||||||
</div>
|
|
||||||
<div class="valueAndUnit">
|
|
||||||
<div class="value" id="outputPowerW"></div>
|
|
||||||
<div class="unit">W</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
const input = document.getElementById('input');
|
const htmlTemperature = document.getElementById('temperature');
|
||||||
const target = document.getElementById('target');
|
const htmlTarget = document.getElementById('target');
|
||||||
const outputPercent = document.getElementById('outputPercent');
|
const htmlHeaterPercent = document.getElementById('heaterPercent');
|
||||||
const outputPowerW = document.getElementById('outputPowerW');
|
const htmlHeaterPowerW = document.getElementById('heaterPowerW');
|
||||||
|
|
||||||
function status() {
|
function status() {
|
||||||
get("/status");
|
get("/status");
|
||||||
|
}
|
||||||
|
|
||||||
|
function targetAdd(delta) {
|
||||||
|
get("/target/add?delta=" + delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(path) {
|
||||||
|
const request = new XMLHttpRequest();
|
||||||
|
request.onreadystatechange = function () {
|
||||||
|
if (request.readyState === 4) {
|
||||||
|
update(request.responseText);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
request.open('GET', (location.hostname === "localhost" ? "http://10.0.0.164" : "") + path);
|
||||||
|
request.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(response) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(response);
|
||||||
|
if (data === null
|
||||||
|
|| data === undefined
|
||||||
|
|| !data.hasOwnProperty("pid")
|
||||||
|
|| !data.pid.hasOwnProperty("p")
|
||||||
|
|| !data.pid.hasOwnProperty("i")
|
||||||
|
|| !data.pid.hasOwnProperty("d")
|
||||||
|
|| !data.pid.hasOwnProperty("target")
|
||||||
|
|| !data.hasOwnProperty("temperature")
|
||||||
|
|| !data.hasOwnProperty("heater")
|
||||||
|
|| !data.heater.hasOwnProperty("percent")
|
||||||
|
|| !data.heater.hasOwnProperty("powerW")
|
||||||
|
) {
|
||||||
|
reset("Invalid data");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const targetV = data.pid.target;
|
||||||
|
const inputV = data.temperature;
|
||||||
|
htmlTemperature.innerText = isSet(inputV) ? inputV.toFixed(1) : "- - -";
|
||||||
|
htmlTarget.innerText = targetV.toFixed(1);
|
||||||
|
htmlHeaterPercent.innerText = data.heater.percent.toFixed(0);
|
||||||
|
htmlHeaterPowerW.innerText = data.heater.powerW.toFixed(0);
|
||||||
|
const inputNull = !isSet(inputV);
|
||||||
|
const inputCold = !inputNull && inputV < targetV - 0.5;
|
||||||
|
const inputWarm = !inputNull && inputV > targetV + 0.5;
|
||||||
|
const inputGood = !inputNull && !inputCold && !inputWarm;
|
||||||
|
setClass(htmlTemperature.parentElement, "inputNull", inputNull);
|
||||||
|
setClass(htmlTemperature.parentElement, "inputCold", inputCold);
|
||||||
|
setClass(htmlTemperature.parentElement, "inputGood", inputGood);
|
||||||
|
setClass(htmlTemperature.parentElement, "inputWarm", inputWarm);
|
||||||
|
} catch (e) {
|
||||||
|
reset(e);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function targetAdd(delta) {
|
function isSet(v) {
|
||||||
get("/target/add?delta=" + delta);
|
return v !== null && v !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function get(path) {
|
function reset(e) {
|
||||||
const request = new XMLHttpRequest();
|
console.error("Failed to handle data:", e);
|
||||||
request.onreadystatechange = function () {
|
htmlTemperature.innerText = "- - -";
|
||||||
if (request.readyState === 4) {
|
htmlTarget.innerText = "- - -";
|
||||||
update(request.responseText);
|
htmlHeaterPercent.innerText = "- - -";
|
||||||
}
|
htmlHeaterPowerW.innerText = "- - -";
|
||||||
};
|
}
|
||||||
request.open('GET', (location.hostname === "localhost" ? "http://10.0.0.171" : "") + path);
|
|
||||||
request.send();
|
|
||||||
}
|
|
||||||
|
|
||||||
function update(response) {
|
/**
|
||||||
try {
|
* @param {HTMLElement} element
|
||||||
const data = JSON.parse(response);
|
* @param {string} className
|
||||||
if (data === null || data === undefined || !data.hasOwnProperty("target") || !data.hasOwnProperty("input") || !data.hasOwnProperty("outputPercent") || !data.hasOwnProperty("outputPowerW")) {
|
* @param {boolean} enabled
|
||||||
throw new Error("Invalid data");
|
*/
|
||||||
}
|
function setClass(element, className, enabled) {
|
||||||
input.innerText = data.input == null ? "- - -" : data.input.toFixed(1);
|
if (element.classList.contains(className) !== enabled) {
|
||||||
target.innerText = data.target.toFixed(1);
|
if (enabled) {
|
||||||
outputPercent.innerText = data.outputPercent.toFixed(0);
|
element.classList.add(className);
|
||||||
outputPowerW.innerText = data.outputPowerW.toFixed(0);
|
} else {
|
||||||
const inputNull = data.input === null;
|
element.classList.remove(className);
|
||||||
const inputCold = !inputNull && data.input < data.target - 0.5;
|
|
||||||
const inputWarm = !inputNull && data.input > data.target + 0.5;
|
|
||||||
const inputGood = !inputNull && !inputCold && !inputWarm;
|
|
||||||
setClass(input.parentElement, "inputNull", inputNull);
|
|
||||||
setClass(input.parentElement, "inputCold", inputCold);
|
|
||||||
setClass(input.parentElement, "inputGood", inputGood);
|
|
||||||
setClass(input.parentElement, "inputWarm", inputWarm);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to handle data:", e);
|
|
||||||
input.innerText = "- - -";
|
|
||||||
target.innerText = "- - -";
|
|
||||||
outputPercent.innerText = "- - -";
|
|
||||||
outputPowerW.innerText = "- - -";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
status();
|
||||||
* @param {HTMLElement} element
|
setInterval(() => status(), 2000);
|
||||||
* @param {string} className
|
|
||||||
* @param {boolean} enabled
|
|
||||||
*/
|
|
||||||
function setClass(element, className, enabled) {
|
|
||||||
if (element.classList.contains(className) !== enabled) {
|
|
||||||
if (enabled) {
|
|
||||||
element.classList.add(className);
|
|
||||||
} else {
|
|
||||||
element.classList.remove(className);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
status();
|
</script>
|
||||||
setInterval(() => status(), 2000);
|
|
||||||
|
|
||||||
</script>
|
</body>
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
@ -10,6 +10,7 @@ lib_deps = https://github.com/milesburton/Arduino-Temperature-Control-Library
|
|||||||
https://github.com/phassel/ArduPID/
|
https://github.com/phassel/ArduPID/
|
||||||
https://github.com/me-no-dev/ESPAsyncWebServer
|
https://github.com/me-no-dev/ESPAsyncWebServer
|
||||||
https://github.com/wayoda/LedControl
|
https://github.com/wayoda/LedControl
|
||||||
|
https://github.com/bblanchon/ArduinoJson
|
||||||
build_flags =
|
build_flags =
|
||||||
board_build.filesystem = littlefs
|
board_build.filesystem = littlefs
|
||||||
monitor_speed = 115200
|
monitor_speed = 115200
|
||||||
@ -48,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.171
|
upload_port = 10.0.0.164
|
||||||
;upload_port = ${common.upload_port}
|
;upload_port = ${common.upload_port}
|
||||||
;upload_speed = ${common.upload_speed}
|
;upload_speed = ${common.upload_speed}
|
||||||
|
|||||||
@ -1,197 +0,0 @@
|
|||||||
#ifdef NODE_FERMENTER
|
|
||||||
|
|
||||||
#include <LedControl.h>
|
|
||||||
#include <LittleFS.h>
|
|
||||||
#include <ArduinoOTA.h>
|
|
||||||
|
|
||||||
#include "patrix/DS18B20Sensor.h"
|
|
||||||
#include "patrix/PIDController.h"
|
|
||||||
#include "patrix/PWMOutput.h"
|
|
||||||
#include "patrix/Rotary.h"
|
|
||||||
#include "patrix/http.h"
|
|
||||||
|
|
||||||
#define HEATER_POWER_W 30
|
|
||||||
|
|
||||||
#define TARGET_STORE_DELAY_MS 10000
|
|
||||||
|
|
||||||
void rotaryCallback(int delta);
|
|
||||||
|
|
||||||
DS18B20 ds18b20("DS18B20", D4);
|
|
||||||
|
|
||||||
DS18B20Sensor input(ds18b20, 0, "");
|
|
||||||
|
|
||||||
PWMOutput heater(D2, "", 100);
|
|
||||||
|
|
||||||
PIDController pid("fermenter", input, heater, UNIT_TEMPERATURE_C, 0, 40, 500, 0.00000002, 0);
|
|
||||||
|
|
||||||
Rotary rotary(D1, D6, rotaryCallback);
|
|
||||||
|
|
||||||
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[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)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
addTarget(delta);
|
|
||||||
|
|
||||||
httpStatus(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
void patrixSetup() {
|
|
||||||
ds18b20.setup();
|
|
||||||
heater.setup();
|
|
||||||
rotary.setup();
|
|
||||||
|
|
||||||
targetFileSetup();
|
|
||||||
pid.setup();
|
|
||||||
|
|
||||||
server.on("/status", httpStatus);
|
|
||||||
server.on("/status/", httpStatus);
|
|
||||||
server.on("/target/add", httpTargetAdd);
|
|
||||||
server.on("/target/add/", httpTargetAdd);
|
|
||||||
}
|
|
||||||
|
|
||||||
void patrixLoop() {
|
|
||||||
ds18b20.loop();
|
|
||||||
input.loop();
|
|
||||||
rotary.loop();
|
|
||||||
|
|
||||||
targetFileLoop();
|
|
||||||
pid.loop();
|
|
||||||
|
|
||||||
displayLoop();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
28
src/node/Fermenter/Fermenter.cpp
Normal file
28
src/node/Fermenter/Fermenter.cpp
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
#ifdef NODE_FERMENTER
|
||||||
|
|
||||||
|
#include "Fermenter.h"
|
||||||
|
|
||||||
|
void patrixSetup() {
|
||||||
|
config.read();
|
||||||
|
|
||||||
|
ds18b20.setup();
|
||||||
|
heater.setup();
|
||||||
|
rotary.setup();
|
||||||
|
|
||||||
|
pid.setup();
|
||||||
|
httpSetup2();
|
||||||
|
}
|
||||||
|
|
||||||
|
void patrixLoop() {
|
||||||
|
config.loop();
|
||||||
|
|
||||||
|
ds18b20.loop();
|
||||||
|
temperature.loop();
|
||||||
|
rotary.loop();
|
||||||
|
|
||||||
|
pid.loop();
|
||||||
|
|
||||||
|
displayLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
68
src/node/Fermenter/Fermenter.h
Normal file
68
src/node/Fermenter/Fermenter.h
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
#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
|
||||||
21
src/node/Fermenter/config.cpp
Normal file
21
src/node/Fermenter/config.cpp
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
#ifdef NODE_FERMENTER
|
||||||
|
|
||||||
|
#include "Fermenter.h"
|
||||||
|
|
||||||
|
Config config("/config.json", configCollect, configApply);
|
||||||
|
|
||||||
|
void configCollect(JsonDocument& json) {
|
||||||
|
json["pid"]["p"] = pid.p;
|
||||||
|
json["pid"]["i"] = pid.i;
|
||||||
|
json["pid"]["d"] = pid.d;
|
||||||
|
json["pid"]["target"] = pid.getTarget();
|
||||||
|
}
|
||||||
|
|
||||||
|
void configApply(JsonDocument& json) {
|
||||||
|
pid.p = json["pid"]["p"].as<double>();
|
||||||
|
pid.i = json["pid"]["i"].as<double>();
|
||||||
|
pid.d = json["pid"]["d"].as<double>();
|
||||||
|
pid.setTarget(json["pid"]["target"].as<double>());
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
56
src/node/Fermenter/display.cpp
Normal file
56
src/node/Fermenter/display.cpp
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
#ifdef NODE_FERMENTER
|
||||||
|
|
||||||
|
#include "Fermenter.h"
|
||||||
|
|
||||||
|
LedControl display(D7, D5, D8, 1);
|
||||||
|
|
||||||
|
unsigned long displayModifyTarget = 0UL;
|
||||||
|
|
||||||
|
void displayPrintf(const char* format, ...) {
|
||||||
|
va_list args;
|
||||||
|
va_start(args, format);
|
||||||
|
char buffer[17];
|
||||||
|
vsnprintf(buffer, sizeof buffer, format, args);
|
||||||
|
auto position = 0;
|
||||||
|
for (const char* b = buffer; *b != 0 && b < buffer + sizeof buffer; b++) {
|
||||||
|
auto 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", temperature.getValue(), pid.getTarget());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
101
src/node/Fermenter/http.cpp
Normal file
101
src/node/Fermenter/http.cpp
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
#ifdef NODE_FERMENTER
|
||||||
|
|
||||||
|
#include "Fermenter.h"
|
||||||
|
|
||||||
|
void httpStatus(AsyncWebServerRequest* request) {
|
||||||
|
JsonDocument json;
|
||||||
|
json["pid"]["p"] = pid.p;
|
||||||
|
json["pid"]["i"] = pid.i;
|
||||||
|
json["pid"]["d"] = pid.d;
|
||||||
|
json["pid"]["target"] = pid.getTarget();
|
||||||
|
json["temperature"] = temperature.getValue();
|
||||||
|
json["heater"]["percent"] = heater.getPercent();
|
||||||
|
json["heater"]["powerW"] = heater.getPercent() / 100.0 * HEATER_POWER_W;
|
||||||
|
|
||||||
|
AsyncResponseStream* stream = request->beginResponseStream("application/json");
|
||||||
|
serializeJson(json, *stream);
|
||||||
|
request->send(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
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)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
addTarget(delta);
|
||||||
|
|
||||||
|
httpStatus(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
void httpConfigSet(AsyncWebServerRequest* request) {
|
||||||
|
const auto keyParam = request->getParam("key");
|
||||||
|
if (keyParam == nullptr) {
|
||||||
|
Log.error("Missing parameter: key (1)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto keyString = keyParam->value();
|
||||||
|
if (keyString == nullptr) {
|
||||||
|
Log.error("Missing parameter: key (2)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto valueParam = request->getParam("value");
|
||||||
|
if (valueParam == nullptr) {
|
||||||
|
Log.error("Missing parameter: value (1)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto valueString = valueParam->value();
|
||||||
|
if (valueString == nullptr) {
|
||||||
|
Log.error("Missing parameter: value (2)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto value = valueString.toDouble();
|
||||||
|
if (isnan(value)) {
|
||||||
|
Log.error("Missing parameter: value (3)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyString.equals("p")) {
|
||||||
|
pid.p = value;
|
||||||
|
} else if (keyString.equals("i")) {
|
||||||
|
pid.i = value;
|
||||||
|
} else if (keyString.equals("d")) {
|
||||||
|
pid.d = value;
|
||||||
|
} else if (keyString.equals("target")) {
|
||||||
|
pid.setTarget(value);
|
||||||
|
} else {
|
||||||
|
request->send(400, "text/plain", "unknown key");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
config.markDirty();
|
||||||
|
|
||||||
|
httpStatus(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
void httpSetup2() {
|
||||||
|
server.on("/status", httpStatus);
|
||||||
|
server.on("/status/", httpStatus);
|
||||||
|
server.on("/target/add", httpTargetAdd);
|
||||||
|
server.on("/target/add/", httpTargetAdd);
|
||||||
|
server.on("/config/set", httpConfigSet);
|
||||||
|
server.on("/config/set/", httpConfigSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
41
src/node/Fermenter/pid.cpp
Normal file
41
src/node/Fermenter/pid.cpp
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
#ifdef NODE_FERMENTER
|
||||||
|
|
||||||
|
#include "Fermenter.h"
|
||||||
|
|
||||||
|
DS18B20 ds18b20(
|
||||||
|
"DS18B20",
|
||||||
|
D4
|
||||||
|
);
|
||||||
|
|
||||||
|
DS18B20Sensor temperature(
|
||||||
|
ds18b20,
|
||||||
|
0,
|
||||||
|
""
|
||||||
|
);
|
||||||
|
|
||||||
|
PWMOutput heater(
|
||||||
|
D2,
|
||||||
|
"",
|
||||||
|
100
|
||||||
|
);
|
||||||
|
|
||||||
|
PIDController pid(
|
||||||
|
"fermenter/temperature/target",
|
||||||
|
"fermenter/temperature/current",
|
||||||
|
"fermenter/heater",
|
||||||
|
temperature,
|
||||||
|
heater,
|
||||||
|
UNIT_TEMPERATURE_C,
|
||||||
|
0,
|
||||||
|
40,
|
||||||
|
50,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
void addTarget(const double delta) {
|
||||||
|
pid.addTarget(delta);
|
||||||
|
config.markDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
12
src/node/Fermenter/rotary.cpp
Normal file
12
src/node/Fermenter/rotary.cpp
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
#ifdef NODE_FERMENTER
|
||||||
|
|
||||||
|
#include "Fermenter.h"
|
||||||
|
|
||||||
|
Rotary rotary(D1, D6, rotaryCallback);
|
||||||
|
|
||||||
|
void rotaryCallback(const int delta) {
|
||||||
|
addTarget(delta);
|
||||||
|
displayModifyTarget = millis();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
93
src/patrix/Config.h
Normal file
93
src/patrix/Config.h
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
#ifndef CONFIG_H
|
||||||
|
#define CONFIG_H
|
||||||
|
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <LittleFS.h>
|
||||||
|
#include "mqtt.h"
|
||||||
|
|
||||||
|
class Config {
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
typedef void (*handle_json_t)(JsonDocument& json);
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
String path;
|
||||||
|
|
||||||
|
handle_json_t collect;
|
||||||
|
|
||||||
|
handle_json_t apply;
|
||||||
|
|
||||||
|
bool dirty = false;
|
||||||
|
|
||||||
|
unsigned long writeDelayStartMillis = 0UL;
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
unsigned long writeDelayMillis = 10 * 1000UL;
|
||||||
|
|
||||||
|
explicit Config(String path, const handle_json_t collect, const handle_json_t apply):
|
||||||
|
path(std::move(path)),
|
||||||
|
collect(collect), apply(apply) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
void markDirty() {
|
||||||
|
dirty = true;
|
||||||
|
writeDelayStartMillis = max(1UL, millis());
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
if (writeDelayStartMillis != 0 && millis() - writeDelayStartMillis >= writeDelayMillis) {
|
||||||
|
write();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void read() {
|
||||||
|
File file = LittleFS.open(path, "r");
|
||||||
|
if (!file) {
|
||||||
|
Log.error("Failed to open config file for read: %s", path.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonDocument json;
|
||||||
|
deserializeJson(json, file);
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
dirty = false;
|
||||||
|
writeDelayStartMillis = 0;
|
||||||
|
Log.info("Config read: %s", path.c_str());
|
||||||
|
|
||||||
|
char buffer[256];
|
||||||
|
serializeJson(json, buffer);
|
||||||
|
Log.info(buffer);
|
||||||
|
|
||||||
|
apply(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
void write() {
|
||||||
|
writeDelayStartMillis = 0;
|
||||||
|
|
||||||
|
File file = LittleFS.open(path, "w");
|
||||||
|
if (!file) {
|
||||||
|
Log.error("Failed to open config file for write: %s", path.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonDocument json;
|
||||||
|
collect(json);
|
||||||
|
serializeJson(json, file);
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
dirty = false;
|
||||||
|
Log.info("Config written: %s", path.c_str());
|
||||||
|
|
||||||
|
char buffer[256];
|
||||||
|
serializeJson(json, buffer);
|
||||||
|
Log.info(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
@ -4,7 +4,6 @@
|
|||||||
#include <Adafruit_Sensor.h>
|
#include <Adafruit_Sensor.h>
|
||||||
|
|
||||||
#include "DallasTemperature.h"
|
#include "DallasTemperature.h"
|
||||||
#include "IValueSensor.h"
|
|
||||||
#include "mqtt.h"
|
#include "mqtt.h"
|
||||||
#include "OneWire.h"
|
#include "OneWire.h"
|
||||||
|
|
||||||
@ -45,34 +44,35 @@ public:
|
|||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case READING:
|
case READING:
|
||||||
if (bus.isConversionComplete()) {
|
if (bus.isConversionComplete()) {
|
||||||
state = COMPLETE;
|
state = COMPLETE;
|
||||||
if (first) {
|
if (first) {
|
||||||
first = false;
|
first = false;
|
||||||
Log.debug("DS18B20: %d devices", bus.getDeviceCount());
|
Log.debug("DS18B20: %d devices", bus.getDeviceCount());
|
||||||
for (auto index = 0; index < bus.getDeviceCount(); index++) {
|
for (auto index = 0; index < bus.getDeviceCount(); index++) {
|
||||||
uint8_t address[8];
|
uint8_t address[8];
|
||||||
bus.getAddress(address, index);
|
bus.getAddress(address, index);
|
||||||
char addressHex[19];
|
char addressHex[19];
|
||||||
snprintf(addressHex, sizeof(addressHex), "0x%02X%02X%02X%02X%02X%02X%02X%02X", address[7], address[6], address[5], address[4], address[3], address[2], address[1], address[0]);
|
snprintf(addressHex, sizeof(addressHex), "0x%02X%02X%02X%02X%02X%02X%02X%02X", address[7], address[6], address[5], address[4], address[3], address[2], address[1], address[0]);
|
||||||
const auto temperature = bus.getTempC(address);
|
const auto temperature = bus.getTempC(address);
|
||||||
Log.debug(" %s: %5.1f%cC", addressHex, temperature, 176);
|
Log.debug(" %s: %5.1f%cC", addressHex, temperature, 176);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (millis() - last > timeout) {
|
|
||||||
state = ERROR;
|
|
||||||
Log.error("DS18B20 \"%s\" gpio #%d: timeout", name.c_str(), gpio);
|
|
||||||
}
|
}
|
||||||
break;
|
}
|
||||||
default:
|
else if (millis() - last > timeout) {
|
||||||
state = IDLE;
|
state = ERROR;
|
||||||
if (last == 0 || millis() - last >= interval) {
|
Log.error("DS18B20 \"%s\" gpio #%d: timeout", name.c_str(), gpio);
|
||||||
last = max(1UL, millis());
|
}
|
||||||
state = READING;
|
break;
|
||||||
bus.requestTemperatures();
|
default:
|
||||||
}
|
state = IDLE;
|
||||||
break;
|
if (last == 0 || millis() - last >= interval) {
|
||||||
|
last = max(1UL, millis());
|
||||||
|
state = READING;
|
||||||
|
bus.requestTemperatures();
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,7 +84,7 @@ public:
|
|||||||
return temperature == DEVICE_DISCONNECTED_C ? NAN : temperature;
|
return temperature == DEVICE_DISCONNECTED_C ? NAN : temperature;
|
||||||
}
|
}
|
||||||
|
|
||||||
float getTemperatureByAddress(const uint8_t *address) {
|
float getTemperatureByAddress(const uint8_t* address) {
|
||||||
if (state != COMPLETE) {
|
if (state != COMPLETE) {
|
||||||
return NAN;
|
return NAN;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
#ifndef DS18B20_SENSOR_H
|
#ifndef DS18B20_SENSOR_H
|
||||||
#define DS18B20_SENSOR_H
|
#define DS18B20_SENSOR_H
|
||||||
|
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
#include "DS18B20.h"
|
#include "DS18B20.h"
|
||||||
|
#include "IValueSensor.h"
|
||||||
|
|
||||||
class DS18B20Sensor final : public IValueSensor {
|
class DS18B20Sensor final : public IValueSensor {
|
||||||
|
|
||||||
@ -13,7 +12,7 @@ class DS18B20Sensor final : public IValueSensor {
|
|||||||
|
|
||||||
const int index;
|
const int index;
|
||||||
|
|
||||||
const uint8_t *address;
|
const uint8_t* address;
|
||||||
|
|
||||||
float temperature = NAN;
|
float temperature = NAN;
|
||||||
|
|
||||||
@ -35,13 +34,15 @@ public:
|
|||||||
if (bus.isComplete()) {
|
if (bus.isComplete()) {
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
temperature = bus.getTemperatureByIndex(index);
|
temperature = bus.getTemperatureByIndex(index);
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
temperature = bus.getTemperatureByAddress(address);
|
temperature = bus.getTemperatureByAddress(address);
|
||||||
}
|
}
|
||||||
if (!name.isEmpty()) {
|
if (!name.isEmpty()) {
|
||||||
mqttPublishValue(name, temperature, UNIT_TEMPERATURE_C);
|
mqttPublishValue(name, temperature, UNIT_TEMPERATURE_C);
|
||||||
}
|
}
|
||||||
} else if (bus.isError()) {
|
}
|
||||||
|
else if (bus.isError()) {
|
||||||
temperature = NAN;
|
temperature = NAN;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,20 +3,22 @@
|
|||||||
|
|
||||||
#include <ArduPID.h>
|
#include <ArduPID.h>
|
||||||
|
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
#include "IValueSensor.h"
|
#include "IValueSensor.h"
|
||||||
#include "PWMOutput.h"
|
#include "PWMOutput.h"
|
||||||
|
|
||||||
class PIDController {
|
class PIDController {
|
||||||
|
|
||||||
const String name;
|
const String targetName;
|
||||||
|
|
||||||
|
const String inputName;
|
||||||
|
|
||||||
|
const String outputName;
|
||||||
|
|
||||||
const IValueSensor& input;
|
const IValueSensor& input;
|
||||||
|
|
||||||
PWMOutput& output;
|
PWMOutput& output;
|
||||||
|
|
||||||
const char *unit;
|
const char* unit;
|
||||||
|
|
||||||
ArduPID controller;
|
ArduPID controller;
|
||||||
|
|
||||||
@ -24,12 +26,6 @@ class PIDController {
|
|||||||
|
|
||||||
double maxValue;
|
double maxValue;
|
||||||
|
|
||||||
double p = 0;
|
|
||||||
|
|
||||||
double i = 0;
|
|
||||||
|
|
||||||
double d = 0;
|
|
||||||
|
|
||||||
double inputValue = NAN;
|
double inputValue = NAN;
|
||||||
|
|
||||||
double outputPercent = NAN;
|
double outputPercent = NAN;
|
||||||
@ -40,8 +36,25 @@ class PIDController {
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
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)
|
double p = 0;
|
||||||
: name(std::move(name)), input(sensor), output(pwmOutput), unit(unit), controller(), minValue(minValue), maxValue(maxValue), p(p), i(i), d(d) {
|
|
||||||
|
double i = 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) :
|
||||||
|
targetName(std::move(targetName)),
|
||||||
|
inputName(std::move(inputName)),
|
||||||
|
outputName(std::move(outputName)),
|
||||||
|
input(sensor),
|
||||||
|
output(pwmOutput),
|
||||||
|
unit(unit),
|
||||||
|
controller(),
|
||||||
|
minValue(minValue),
|
||||||
|
maxValue(maxValue),
|
||||||
|
p(p),
|
||||||
|
i(i),
|
||||||
|
d(d) {
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,9 +73,9 @@ public:
|
|||||||
const auto now = millis();
|
const auto now = millis();
|
||||||
if (lastSent == 0 || now - lastSent >= 5000) {
|
if (lastSent == 0 || now - lastSent >= 5000) {
|
||||||
lastSent = now;
|
lastSent = now;
|
||||||
mqttPublishValue(name + "/target", targetValue, unit);
|
mqttPublishValue(targetName, targetValue, unit);
|
||||||
mqttPublishValue(name + "/input", inputValue, unit);
|
mqttPublishValue(inputName, inputValue, unit);
|
||||||
mqttPublishValue(name + "/output", outputPercent, UNIT_PERCENT);
|
mqttPublishValue(outputName, outputPercent, UNIT_PERCENT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -79,7 +92,7 @@ public:
|
|||||||
|
|
||||||
double setTarget(double target) {
|
double setTarget(double target) {
|
||||||
targetValue = max(minValue, min(maxValue, target));
|
targetValue = max(minValue, min(maxValue, target));
|
||||||
Log.info("PID \"%s\" target = %.1f", name.c_str(), targetValue);
|
Log.info("PID %s = %.1f", targetName.c_str(), targetValue);
|
||||||
return targetValue;
|
return targetValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user