Compare commits
25 Commits
Fermenter-
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 80b3c3a0da | |||
| b87b45d22f | |||
| 920c78985d | |||
| a0ed699f4d | |||
| e2c102b16a | |||
| 1732fce143 | |||
| fa4e266bc5 | |||
| 1183be3d83 | |||
| 75755ada63 | |||
| 118bef8f72 | |||
| 528c12c70f | |||
| 67a1a317fc | |||
| da31e2d40a | |||
| 422378c0cb | |||
| 19e81885a9 | |||
| 97f711a22d | |||
| 8c8c35fb5f | |||
| 17b520f3f2 | |||
| 732f88aec3 | |||
| 1e1a1c8a5b | |||
| 568880398f | |||
| b9d96d3f02 | |||
| 933ce71db4 | |||
| 4ca795501b | |||
| bd422c3991 |
8
data/Fermenter/config.json
Normal file
8
data/Fermenter/config.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"pid": {
|
||||
"p": 20,
|
||||
"i": 1e-6,
|
||||
"d": 0,
|
||||
"target": 28
|
||||
}
|
||||
}
|
||||
215
data/Fermenter/http/index.html
Normal file
215
data/Fermenter/http/index.html
Normal file
@ -0,0 +1,215 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Gärbox</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
font-size: 16vw;
|
||||
margin: 0.1em;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
div {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.section {
|
||||
clear: both;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 50%;
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
color: gray
|
||||
}
|
||||
|
||||
.valueAndUnit {
|
||||
clear: both;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.controlPadding {
|
||||
float: left;
|
||||
padding: 0 0.25em;
|
||||
}
|
||||
|
||||
.value {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.unit {
|
||||
float: left;
|
||||
width: 1.2em;
|
||||
margin-left: 0.25em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
svg {
|
||||
float: left;
|
||||
width: 1em;
|
||||
margin-top: 0.2em;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.inputNull {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.inputCold {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
.inputGood {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.inputWarm {
|
||||
color: red;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="section">
|
||||
<div class="title">Ist-Temperatur</div>
|
||||
<div class="valueAndUnit">
|
||||
<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>
|
||||
<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 class="section">
|
||||
<div class="title">Heizung</div>
|
||||
<div class="valueAndUnit">
|
||||
<div class="value" id="heaterPercent"></div>
|
||||
<div class="unit">%</div>
|
||||
</div>
|
||||
<div class="valueAndUnit">
|
||||
<div class="value" id="heaterPowerW"></div>
|
||||
<div class="unit">W</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
const htmlTemperature = document.getElementById('temperature');
|
||||
const htmlTarget = document.getElementById('target');
|
||||
const htmlHeaterPercent = document.getElementById('heaterPercent');
|
||||
const htmlHeaterPowerW = document.getElementById('heaterPowerW');
|
||||
|
||||
function 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 isSet(v) {
|
||||
return v !== null && v !== undefined;
|
||||
}
|
||||
|
||||
function reset(e) {
|
||||
console.error("Failed to handle data:", e);
|
||||
htmlTemperature.innerText = "- - -";
|
||||
htmlTarget.innerText = "- - -";
|
||||
htmlHeaterPercent.innerText = "- - -";
|
||||
htmlHeaterPowerW.innerText = "- - -";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} element
|
||||
* @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();
|
||||
setInterval(() => status(), 2000);
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
24
data/Fermenter/program/demo.json
Normal file
24
data/Fermenter/program/demo.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
24
data/Fermenter/program/demo2.json
Normal file
24
data/Fermenter/program/demo2.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
24
data/Fermenter/program/endless.json
Normal file
24
data/Fermenter/program/endless.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
15
data/Greenhouse/http/favicon.svg
Normal file
15
data/Greenhouse/http/favicon.svg
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<polygon style="fill:#C8DB86;" points="503.83,172.381 435.647,172.381 283.648,59.589 351.831,59.589 "/>
|
||||
<polygon style="fill:#D4ED85;" points="305.435,59.589 457.434,172.381 312.178,172.381 160.169,59.589 "/>
|
||||
<rect x="249.54" y="172.381" style="fill:#FFE6B8;" width="62.638" height="280.031"/>
|
||||
<rect x="8.17" y="172.381" style="fill:#FFF3DC;" width="263.157" height="280.031"/>
|
||||
<rect x="104.001" y="172.381" style="fill:#D4ED85;" width="112.357" height="280.042"/>
|
||||
<rect x="430.298" y="172.381" style="fill:#BCC987;" width="73.532" height="280.031"/>
|
||||
<rect x="312.178" y="172.381" style="fill:#C8DB86;" width="150.8" height="280.031"/>
|
||||
<polygon style="fill:#FFF3DC;" points="312.178,172.381 136.976,172.381 136.976,76.801 160.169,59.589 "/>
|
||||
<polygon style="fill:#FFFFFF;" points="265.782,172.381 8.17,172.381 136.976,76.801 "/>
|
||||
<path style="fill:#9CAC74;" d="M208.178,172.377v85.176h-96.011v-85.176h-16.34v85.176h-2.723c-4.512,0-8.17,3.657-8.17,8.17c0,4.513,3.658,8.17,8.17,8.17h2.723v77.005h-2.723c-4.512,0-8.17,3.657-8.17,8.17s3.658,8.17,8.17,8.17h2.723v85.176h16.34v-85.176h96.011v85.176h16.34V172.377H208.178z M112.167,350.899v-77.005h96.011v77.005H112.167z"/>
|
||||
<path style="fill:#7D9062;" d="M183.667,332.34c-4.512,0-8.17-3.657-8.17-8.17v-23.545c0-4.513,3.658-8.17,8.17-8.17s8.17,3.657,8.17,8.17v23.545C191.838,328.683,188.18,332.34,183.667,332.34z"/>
|
||||
<path style="fill:#7D9062;" d="M508.698,165.817L356.696,53.024c-1.409-1.046-3.116-1.61-4.869-1.61H160.173c-1.754,0-3.461,0.564-4.868,1.61L3.302,165.817C1.224,167.357,0,169.791,0,172.377v280.039c0,4.513,3.658,8.17,8.17,8.17h495.66c4.513,0,8.17-3.657,8.17-8.17V172.377C512,169.791,510.776,167.357,508.698,165.817z M253.301,67.754l129.98,96.452h-68.405L184.893,67.754C184.893,67.754,253.301,67.754,253.301,67.754z M160.173,69.758l127.282,94.449H32.892L160.173,69.758zM16.34,180.547h287.664v263.698H16.34V180.547z M399.832,444.247h-79.486v-263.7h79.486V444.247z M280.721,67.754h68.405l129.982,96.452h-68.406L280.721,67.754z M495.66,444.247h-79.487v-263.7h79.487V444.247z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
272
data/Greenhouse/http/index.html
Normal file
272
data/Greenhouse/http/index.html
Normal file
@ -0,0 +1,272 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.svg">
|
||||
<title>Gewächshaus</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
font-size: 8vw;
|
||||
margin: 0;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
background-color: #5c875c;
|
||||
}
|
||||
|
||||
div {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.heading {
|
||||
padding: 0.25em 0;
|
||||
text-align: center;
|
||||
background-color: #93da93;
|
||||
border-bottom: 1px solid gray;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0.25em;
|
||||
}
|
||||
|
||||
.section {
|
||||
clear: both;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 50%;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.valueAndUnit {
|
||||
clear: both;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.value {
|
||||
float: left;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 1.5em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.unit {
|
||||
float: left;
|
||||
margin-left: 0.25em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.inputNull {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.inputBlue {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
.inputGreen, #lightOn {
|
||||
color: #00ff00;
|
||||
}
|
||||
|
||||
.inputRed, #lightOff {
|
||||
color: #ba0000;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="heading">Gewächshaus</div>
|
||||
|
||||
<div class="content">
|
||||
|
||||
<div class="section">
|
||||
<div class="title">Temperatur</div>
|
||||
<div class="valueAndUnit">
|
||||
<div class="value" id="temperature"></div>
|
||||
<div class="unit">°C</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="title">Relative Luftfeuchte</div>
|
||||
<div class="valueAndUnit">
|
||||
<div class="value" id="relative"></div>
|
||||
<div class="unit">%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="title">Absolute Luftfeuchte</div>
|
||||
<div class="valueAndUnit">
|
||||
<div class="value" id="absolute"></div>
|
||||
<div class="unit">g/m³</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="title">Helligkeit</div>
|
||||
<div class="valueAndUnit">
|
||||
<div class="value" id="illuminance"></div>
|
||||
<div class="unit">lux</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="title">Tür</div>
|
||||
<div class="valueAndUnit">
|
||||
<div class="value" id="door"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="title">Fenster</div>
|
||||
<div class="valueAndUnit">
|
||||
<div class="value" id="windows"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="title">Licht</div>
|
||||
<div class="valueAndUnit">
|
||||
<span id="lightUnknown">?</span>
|
||||
<div id="lightOn">
|
||||
Ein
|
||||
<img alt="Ein" src="on.svg" onclick="get('/light/off')">
|
||||
</div>
|
||||
<div id="lightOff">
|
||||
Aus
|
||||
<img alt="Aus" src="off.svg" onclick="get('/light/on')">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const NO_VALUE = "?";
|
||||
|
||||
const TEMPERATURE_BLUE = 10;
|
||||
const TEMPERATURE_RED = 35;
|
||||
|
||||
const RELATIVE_BLUE = 60;
|
||||
const RELATIVE_RED = 80;
|
||||
|
||||
const htmlTemperature = document.getElementById('temperature');
|
||||
const htmlRelative = document.getElementById('relative');
|
||||
const htmlAbsolute = document.getElementById('absolute');
|
||||
const htmlIlluminance = document.getElementById('illuminance');
|
||||
const htmlDoor = document.getElementById('door');
|
||||
const htmlWindows = document.getElementById('windows');
|
||||
const htmlLightOn = document.getElementById('lightOn');
|
||||
const htmlLightOff = document.getElementById('lightOff');
|
||||
const htmlLightUnknown = document.getElementById('lightUnknown');
|
||||
|
||||
function status() {
|
||||
get("/status");
|
||||
}
|
||||
|
||||
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.160" : "") + path);
|
||||
request.send();
|
||||
}
|
||||
|
||||
function format(value, decimals) {
|
||||
return value?.toLocaleString(undefined, {minimumFractionDigits: decimals, maximumFractionDigits: decimals});
|
||||
}
|
||||
|
||||
function update(response) {
|
||||
try {
|
||||
const data = JSON.parse(response);
|
||||
htmlTemperature.innerText = format(data?.temperature, 1) || NO_VALUE;
|
||||
htmlRelative.innerText = format(data?.relative, 0) || NO_VALUE;
|
||||
htmlAbsolute.innerText = format(data?.absolute, 1) || NO_VALUE;
|
||||
htmlIlluminance.innerText = format(data?.illuminance, 0) || NO_VALUE;
|
||||
|
||||
const doorClosed = data?.door === true;
|
||||
const doorOpened = data?.door === false;
|
||||
htmlDoor.innerText = doorClosed ? 'Geschlosssen' : doorOpened ? 'Offen' : NO_VALUE;
|
||||
setClass(htmlDoor.parentElement, "inputRed", doorOpened);
|
||||
setClass(htmlDoor.parentElement, "inputGreen", doorClosed);
|
||||
|
||||
const windowsClosed = data?.windows === true;
|
||||
const windowsOpened = data?.windows === false;
|
||||
htmlWindows.innerText = windowsClosed ? 'Geschlosssen' : windowsOpened ? 'Offen' : NO_VALUE;
|
||||
setClass(htmlWindows.parentElement, "inputRed", windowsOpened);
|
||||
setClass(htmlWindows.parentElement, "inputGreen", windowsClosed);
|
||||
|
||||
htmlLightOn.style.display = data?.light === true ? 'block' : 'none';
|
||||
htmlLightOff.style.display = data?.light === false ? 'block' : 'none';
|
||||
htmlLightUnknown.style.display = data?.light !== false && data?.light !== true ? 'block' : 'none';
|
||||
|
||||
const relativeNull = !isSet(data?.relative);
|
||||
const relativeBlue = !relativeNull && data?.relative < RELATIVE_BLUE;
|
||||
const relativeRed = !relativeNull && data?.relative > RELATIVE_RED;
|
||||
const relativeGreen = !relativeNull && !relativeBlue && !relativeRed;
|
||||
setClass(htmlRelative.parentElement, "inputNull", relativeNull);
|
||||
setClass(htmlRelative.parentElement, "inputRed", relativeBlue);
|
||||
setClass(htmlRelative.parentElement, "inputGreen", relativeGreen);
|
||||
setClass(htmlRelative.parentElement, "inputBlue", relativeRed);
|
||||
|
||||
const temperatureNull = !isSet(data?.temperature);
|
||||
const temperatureBlue = !temperatureNull && data?.temperature < TEMPERATURE_BLUE;
|
||||
const temperatureRed = !temperatureNull && data?.temperature > TEMPERATURE_RED;
|
||||
const temperatureGreen = !temperatureNull && !temperatureBlue && !temperatureRed;
|
||||
setClass(htmlTemperature.parentElement, "inputNull", temperatureNull);
|
||||
setClass(htmlTemperature.parentElement, "inputBlue", temperatureBlue);
|
||||
setClass(htmlTemperature.parentElement, "inputGreen", temperatureGreen);
|
||||
setClass(htmlTemperature.parentElement, "inputRed", temperatureRed);
|
||||
} catch (e) {
|
||||
reset(e);
|
||||
}
|
||||
}
|
||||
|
||||
function isSet(v) {
|
||||
return v !== null && v !== undefined;
|
||||
}
|
||||
|
||||
function reset(e) {
|
||||
console.error("Failed to handle data:", e);
|
||||
htmlTemperature.innerText = NO_VALUE;
|
||||
htmlRelative.innerText = NO_VALUE;
|
||||
htmlAbsolute.innerText = NO_VALUE;
|
||||
htmlIlluminance.innerText = NO_VALUE;
|
||||
htmlDoor.innerText = NO_VALUE;
|
||||
htmlWindows.innerText = NO_VALUE;
|
||||
htmlLightOn.style.display = 'none';
|
||||
htmlLightOff.style.display = 'none';
|
||||
htmlLightUnknown.style.display = 'block';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} element
|
||||
* @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();
|
||||
setInterval(() => status(), 2000);
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
20
data/Greenhouse/http/off.svg
Normal file
20
data/Greenhouse/http/off.svg
Normal file
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(0 -1028.4)">
|
||||
<path d="m7 1034.4c-3.3137 0-6 2.6-6 6 0 3.3 2.6863 6 6 6h5 5c3.314 0 6-2.7 6-6 0-3.4-2.686-6-6-6h-5-5z" fill="#95a5a6"/>
|
||||
<path d="m7 1033.4c-3.3137 0-6 2.6-6 6 0 3.3 2.6863 6 6 6h5 5c3.314 0 6-2.7 6-6 0-3.4-2.686-6-6-6h-5-5z" fill="#bdc3c7"/>
|
||||
<path d="m7 1035.4c-2.2091 0-4 1.8-4 4s1.7909 4 4 4h5 5c2.209 0 4-1.8 4-4s-1.791-4-4-4h-5-5z" fill="#e74c3c"/>
|
||||
<path d="m7 1035.4c-2.2091 0-4 1.8-4 4 0 0.1 0.0419 0.3 0.0625 0.5 0.2471-2 1.8985-3.5 3.9375-3.5h5 5c2.039 0 3.69 1.5 3.938 3.5 0.02-0.2 0.062-0.4 0.062-0.5 0-2.2-1.791-4-4-4h-5-5z" fill="#c0392b"/>
|
||||
<g transform="translate(-11.5,-11.4)">
|
||||
<g transform="translate(10.5,10.4)">
|
||||
<path d="m13.023 1039.7-0.023 1.2c-0.04 1.9 1.567 3.5 3.5 3.5h1.5c2.209 0 4-1.8 4-4 0-0.4-0.074-0.7-0.156-1z" fill="#7f8c8d"/>
|
||||
<path d="m16.5 1036.4c-1.933 0-3.5 1.5-3.5 3.5 0 1.9 1.567 3.5 3.5 3.5h2c1.933 0 3.5-1.6 3.5-3.5 0-2-1.567-3.5-3.5-3.5h-2z" fill="#ecf0f1"/>
|
||||
<g fill="#bdc3c7">
|
||||
<path d="m15.5 1037.4c-0.276 0-0.5 0.1-0.5 0.4v3.2c0 0.2 0.224 0.4 0.5 0.4s0.5-0.2 0.5-0.4v-3.2c0-0.3-0.224-0.4-0.5-0.4z"/>
|
||||
<path d="m17.5 1037.4c-0.276 0-0.5 0.1-0.5 0.4v3.2c0 0.2 0.224 0.4 0.5 0.4s0.5-0.2 0.5-0.4v-3.2c0-0.3-0.224-0.4-0.5-0.4z"/>
|
||||
<path d="m19.5 1037.4c-0.276 0-0.5 0.1-0.5 0.4v3.2c0 0.2 0.224 0.4 0.5 0.4s0.5-0.2 0.5-0.4v-3.2c0-0.3-0.224-0.4-0.5-0.4z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
20
data/Greenhouse/http/on.svg
Normal file
20
data/Greenhouse/http/on.svg
Normal file
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(0 -1028.4)">
|
||||
<path d="m17 1034.4c3.314 0 6 2.6 6 6 0 3.3-2.686 6-6 6h-5-5c-3.3137 0-6-2.7-6-6 0-3.4 2.6863-6 6-6h5 5z" fill="#95a5a6"/>
|
||||
<path d="m17 1033.4c3.314 0 6 2.6 6 6 0 3.3-2.686 6-6 6h-5-5c-3.3137 0-6-2.7-6-6 0-3.4 2.6863-6 6-6h5 5z" fill="#bdc3c7"/>
|
||||
<path d="m17 1035.4c2.209 0 4 1.8 4 4s-1.791 4-4 4h-5-5c-2.2091 0-4-1.8-4-4s1.7909-4 4-4h5 5z" fill="#2ecc71"/>
|
||||
<path d="m17 1035.4c2.209 0 4 1.8 4 4 0 0.1-0.042 0.3-0.062 0.5-0.248-2-1.899-3.5-3.938-3.5h-5-5c-2.039 0-3.6904 1.5-3.9375 3.5-0.0206-0.2-0.0625-0.4-0.0625-0.5 0-2.2 1.7909-4 4-4h5 5z" fill="#27ae60"/>
|
||||
<g transform="matrix(-1,0,0,1,35.5,-11.4)">
|
||||
<g transform="translate(10.5,10.4)">
|
||||
<path d="m13.023 1039.7-0.023 1.2c-0.04 1.9 1.567 3.5 3.5 3.5h1.5c2.209 0 4-1.8 4-4 0-0.4-0.074-0.7-0.156-1z" fill="#7f8c8d"/>
|
||||
<path d="m16.5 1036.4c-1.933 0-3.5 1.5-3.5 3.5 0 1.9 1.567 3.5 3.5 3.5h2c1.933 0 3.5-1.6 3.5-3.5 0-2-1.567-3.5-3.5-3.5h-2z" fill="#ecf0f1"/>
|
||||
<g fill="#bdc3c7">
|
||||
<path d="m15.5 1037.4c-0.276 0-0.5 0.1-0.5 0.4v3.2c0 0.2 0.224 0.4 0.5 0.4s0.5-0.2 0.5-0.4v-3.2c0-0.3-0.224-0.4-0.5-0.4z"/>
|
||||
<path d="m17.5 1037.4c-0.276 0-0.5 0.1-0.5 0.4v3.2c0 0.2 0.224 0.4 0.5 0.4s0.5-0.2 0.5-0.4v-3.2c0-0.3-0.224-0.4-0.5-0.4z"/>
|
||||
<path d="m19.5 1037.4c-0.276 0-0.5 0.1-0.5 0.4v3.2c0 0.2 0.224 0.4 0.5 0.4s0.5-0.2 0.5-0.4v-3.2c0-0.3-0.224-0.4-0.5-0.4z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@ -1,194 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Gärbox</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
font-size: 16vw;
|
||||
margin: 0.1em;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
div {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.section {
|
||||
clear: both;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 50%;
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
color: gray
|
||||
}
|
||||
|
||||
.valueAndUnit {
|
||||
clear: both;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.controlPadding {
|
||||
float: left;
|
||||
padding: 0 0.25em;
|
||||
}
|
||||
|
||||
.value {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.unit {
|
||||
float: left;
|
||||
width: 1.2em;
|
||||
margin-left: 0.25em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
svg {
|
||||
float: left;
|
||||
width: 1em;
|
||||
margin-top: 0.2em;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.inputNull {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.inputCold {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
.inputGood {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.inputWarm {
|
||||
color: red;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="section">
|
||||
<div class="title">Ist-Temperatur</div>
|
||||
<div class="valueAndUnit">
|
||||
<div class="value" id="input"></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>
|
||||
<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 class="section">
|
||||
<div class="title">Heizung</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>
|
||||
|
||||
<script>
|
||||
|
||||
const input = document.getElementById('input');
|
||||
const target = document.getElementById('target');
|
||||
const outputPercent = document.getElementById('outputPercent');
|
||||
const outputPowerW = document.getElementById('outputPowerW');
|
||||
|
||||
function 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.171" : "") + path);
|
||||
request.send();
|
||||
}
|
||||
|
||||
function update(response) {
|
||||
try {
|
||||
const data = JSON.parse(response);
|
||||
if (data === null || data === undefined || !data.hasOwnProperty("target") || !data.hasOwnProperty("input") || !data.hasOwnProperty("outputPercent") || !data.hasOwnProperty("outputPowerW")) {
|
||||
throw new Error("Invalid data");
|
||||
}
|
||||
input.innerText = data.input == null ? "- - -" : data.input.toFixed(1);
|
||||
target.innerText = data.target.toFixed(1);
|
||||
outputPercent.innerText = data.outputPercent.toFixed(0);
|
||||
outputPowerW.innerText = data.outputPowerW.toFixed(0);
|
||||
const inputNull = data.input === null;
|
||||
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 = "- - -";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} element
|
||||
* @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();
|
||||
setInterval(() => status(), 2000);
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@ -1,12 +1,21 @@
|
||||
[platformio]
|
||||
data_dir = ${PROJECT_DIR}/data/${PIOENV}
|
||||
|
||||
[common]
|
||||
framework = arduino
|
||||
lib_deps = https://github.com/milesburton/Arduino-Temperature-Control-Library
|
||||
lib_deps = SPI
|
||||
https://github.com/adafruit/Adafruit_BusIO
|
||||
https://github.com/milesburton/Arduino-Temperature-Control-Library
|
||||
https://github.com/adafruit/DHT-sensor-library
|
||||
https://github.com/adafruit/Adafruit_TSL2561
|
||||
https://github.com/knolleary/pubsubclient
|
||||
https://github.com/adafruit/Adafruit_BME680
|
||||
https://github.com/adafruit/Adafruit_BMP280
|
||||
https://github.com/adafruit/Adafruit_AHTX0
|
||||
https://github.com/phassel/ArduPID/
|
||||
https://github.com/me-no-dev/ESPAsyncWebServer
|
||||
https://github.com/wayoda/LedControl
|
||||
https://github.com/bblanchon/ArduinoJson
|
||||
build_flags =
|
||||
board_build.filesystem = littlefs
|
||||
monitor_speed = 115200
|
||||
@ -22,18 +31,7 @@ board = esp12e
|
||||
platform = espressif32
|
||||
board = esp32dev
|
||||
|
||||
[env:GreenhouseUSB]
|
||||
platform = ${esp12e.platform}
|
||||
board = ${esp12e.board}
|
||||
framework = ${common.framework}
|
||||
lib_deps = ${common.lib_deps}
|
||||
build_flags = ${common.build_flags} -DNODE_GREENHOUSE -DHOSTNAME=\"Greenhouse\"
|
||||
board_build.filesystem = ${common.board_build.filesystem}
|
||||
monitor_speed = ${common.monitor_speed}
|
||||
upload_port = ${common.upload_port}
|
||||
upload_speed = ${common.upload_speed}
|
||||
|
||||
[env:GreenhouseOTA]
|
||||
[env:Greenhouse]
|
||||
platform = ${esp12e.platform}
|
||||
board = ${esp12e.board}
|
||||
framework = ${common.framework}
|
||||
@ -43,19 +41,10 @@ board_build.filesystem = ${common.board_build.filesystem}
|
||||
monitor_speed = ${common.monitor_speed}
|
||||
upload_protocol = ${common.upload_protocol}
|
||||
upload_port = 10.0.0.160
|
||||
;upload_port = ${common.upload_port}
|
||||
;upload_speed = ${common.upload_speed}
|
||||
|
||||
[env:FermenterUSB]
|
||||
platform = ${esp12e.platform}
|
||||
board = ${esp12e.board}
|
||||
framework = ${common.framework}
|
||||
lib_deps = ${common.lib_deps}
|
||||
build_flags = ${common.build_flags} -DNODE_FERMENTER -DHOSTNAME=\"Fermenter\"
|
||||
board_build.filesystem = ${common.board_build.filesystem}
|
||||
monitor_speed = ${common.monitor_speed}
|
||||
upload_port = ${common.upload_port}
|
||||
upload_speed = ${common.upload_speed}
|
||||
|
||||
[env:FermenterOTA]
|
||||
[env:Fermenter]
|
||||
platform = ${esp12e.platform}
|
||||
board = ${esp12e.board}
|
||||
framework = ${common.framework}
|
||||
@ -65,4 +54,6 @@ board_build.filesystem = ${common.board_build.filesystem}
|
||||
monitor_speed = ${common.monitor_speed}
|
||||
upload_flags = --auth=OtaAuthPatrixFermenter
|
||||
upload_protocol = ${common.upload_protocol}
|
||||
upload_port = 10.0.0.171
|
||||
upload_port = 10.0.0.164
|
||||
;upload_port = ${common.upload_port}
|
||||
;upload_speed = ${common.upload_speed}
|
||||
|
||||
@ -1,219 +0,0 @@
|
||||
#ifdef NODE_FERMENTER
|
||||
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#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"
|
||||
|
||||
#define HEATER_POWER_W 30
|
||||
|
||||
#define TARGET_STORE_DELAY_MS 10000
|
||||
|
||||
void rotaryCallback(int delta);
|
||||
|
||||
AsyncWebServer server(80);
|
||||
|
||||
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 httpNotFound(AsyncWebServerRequest *request) {
|
||||
if (request->method() == HTTP_OPTIONS) {
|
||||
request->send(200);
|
||||
} else {
|
||||
request->send(404, "text/plain", "not found");
|
||||
}
|
||||
}
|
||||
|
||||
void patrixSetup() {
|
||||
if (LittleFS.begin()) {
|
||||
Log.info("Filesystem mounted.");
|
||||
} else {
|
||||
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-Methods", "GET, POST, PUT");
|
||||
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "Content-Type");
|
||||
server.serveStatic("/", LittleFS, "/http/", "max-age=86400").setDefaultFile("index.html");
|
||||
server.on("/status", httpStatus);
|
||||
server.on("/status/", httpStatus);
|
||||
server.on("/target/add", httpTargetAdd);
|
||||
server.on("/target/add/", httpTargetAdd);
|
||||
server.onNotFound(httpNotFound);
|
||||
server.begin();
|
||||
}
|
||||
|
||||
void patrixLoop() {
|
||||
ds18b20.loop();
|
||||
input.loop();
|
||||
rotary.loop();
|
||||
|
||||
targetFileLoop();
|
||||
pid.loop();
|
||||
|
||||
displayLoop();
|
||||
}
|
||||
|
||||
#endif
|
||||
@ -1,24 +0,0 @@
|
||||
#ifdef NODE_GREENHOUSE
|
||||
|
||||
#include "patrix/bme680.h"
|
||||
#include "patrix/tsl2561.h"
|
||||
|
||||
TSL2561 gardenTSL("garden");
|
||||
|
||||
BME680 gardenBME("garden");
|
||||
|
||||
BME680 greenhouseBME("greenhouse");
|
||||
|
||||
void patrixSetup() {
|
||||
gardenTSL.setup();
|
||||
gardenBME.setup();
|
||||
greenhouseBME.setup();
|
||||
}
|
||||
|
||||
void patrixLoop() {
|
||||
gardenTSL.loop();
|
||||
gardenBME.loop();
|
||||
greenhouseBME.loop();
|
||||
}
|
||||
|
||||
#endif
|
||||
35
src/node/Fermenter/Fermenter.cpp
Normal file
35
src/node/Fermenter/Fermenter.cpp
Normal file
@ -0,0 +1,35 @@
|
||||
#ifdef NODE_FERMENTER
|
||||
|
||||
#include <patrix/Patrix.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "display.h"
|
||||
#include "http.h"
|
||||
#include "pid.h"
|
||||
#include "Program.h"
|
||||
#include "rotary.h"
|
||||
|
||||
void patrixSetup() {
|
||||
config.read();
|
||||
|
||||
ds18b20.setup();
|
||||
heater.setup();
|
||||
rotary.setup();
|
||||
|
||||
pidSetup();
|
||||
httpSetup2();
|
||||
}
|
||||
|
||||
void patrixLoop(const boolean mqttJustConnected) {
|
||||
config.loop();
|
||||
|
||||
ds18b20.loop();
|
||||
temperature.loop();
|
||||
pidLoop();
|
||||
rotary.loop();
|
||||
program.loop();
|
||||
|
||||
displayLoop();
|
||||
}
|
||||
|
||||
#endif
|
||||
7
src/node/Fermenter/Program.cpp
Normal file
7
src/node/Fermenter/Program.cpp
Normal file
@ -0,0 +1,7 @@
|
||||
#ifdef NODE_FERMENTER
|
||||
|
||||
#include "Program.h"
|
||||
|
||||
Program program;
|
||||
|
||||
#endif
|
||||
234
src/node/Fermenter/Program.h
Normal file
234
src/node/Fermenter/Program.h
Normal 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
|
||||
130
src/node/Fermenter/ProgramPoint.h
Normal file
130
src/node/Fermenter/ProgramPoint.h
Normal 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, "%lu. %2lu:%02lu:%02lu", days, hours % 24, minutes % 60, seconds % 60);
|
||||
} else {
|
||||
snprintf(buffer, sizeof buffer, "%lu:%02lu:%02lu", 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
|
||||
22
src/node/Fermenter/config.cpp
Normal file
22
src/node/Fermenter/config.cpp
Normal file
@ -0,0 +1,22 @@
|
||||
#ifdef NODE_FERMENTER
|
||||
|
||||
#include "config.h"
|
||||
#include "pid.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
|
||||
12
src/node/Fermenter/config.h
Normal file
12
src/node/Fermenter/config.h
Normal 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
|
||||
55
src/node/Fermenter/display.cpp
Normal file
55
src/node/Fermenter/display.cpp
Normal file
@ -0,0 +1,55 @@
|
||||
#ifdef NODE_FERMENTER
|
||||
|
||||
#include "display.h"
|
||||
#include "pid.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
|
||||
14
src/node/Fermenter/display.h
Normal file
14
src/node/Fermenter/display.h
Normal 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
|
||||
155
src/node/Fermenter/http.cpp
Normal file
155
src/node/Fermenter/http.cpp
Normal file
@ -0,0 +1,155 @@
|
||||
#ifdef NODE_FERMENTER
|
||||
|
||||
#include "http.h"
|
||||
#include "config.h"
|
||||
#include "pid.h"
|
||||
#include "Program.h"
|
||||
|
||||
void httpHistory(AsyncWebServerRequest* request) {
|
||||
AsyncResponseStream* stream = request->beginResponseStream("text/plain");
|
||||
constexpr int size = std::size(history);
|
||||
const History* h = history;
|
||||
for (int i = 0; i < size; i++) {
|
||||
stream->printf("%d/%d/%d\n", h->target, h->temperature, h->heater);
|
||||
h = (h - history + size - 1) % size + history;
|
||||
}
|
||||
request->send(stream);
|
||||
}
|
||||
|
||||
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 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() {
|
||||
server.on("/history", httpHistory);
|
||||
server.on("/status", httpStatus);
|
||||
server.on("/target/add", httpTargetAdd);
|
||||
server.on("/config/set", httpConfigSet);
|
||||
server.on("/program/load", httpProgramLoad);
|
||||
server.on("/program/start", httpProgramStart);
|
||||
server.on("/program/stop", httpProgramStop);
|
||||
server.on("/program/pause", httpProgramPause);
|
||||
server.on("/program/resume", httpProgramResume);
|
||||
}
|
||||
|
||||
#endif
|
||||
12
src/node/Fermenter/http.h
Normal file
12
src/node/Fermenter/http.h
Normal 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
|
||||
64
src/node/Fermenter/pid.cpp
Normal file
64
src/node/Fermenter/pid.cpp
Normal file
@ -0,0 +1,64 @@
|
||||
#ifdef NODE_FERMENTER
|
||||
|
||||
#include "pid.h"
|
||||
#include "config.h"
|
||||
|
||||
History history[3 * 60];
|
||||
|
||||
History* historyPtr = history;
|
||||
|
||||
unsigned long historyLastMinute = 0;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
void pidSetup() {
|
||||
pid.setup();
|
||||
}
|
||||
|
||||
void pidLoop() {
|
||||
pid.loop();
|
||||
const auto currentMinute = millis() / 60000;
|
||||
if (historyLastMinute != currentMinute) {
|
||||
historyLastMinute = currentMinute;
|
||||
historyPtr = (historyPtr - history + 1) % std::size(history) + history;
|
||||
}
|
||||
historyPtr->heater = static_cast<uint8_t>(heater.getPercent());
|
||||
historyPtr->target = static_cast<int16_t>(pid.getTarget() * 10);
|
||||
historyPtr->temperature = static_cast<int16_t>(temperature.getValue() * 10);
|
||||
}
|
||||
|
||||
#endif
|
||||
38
src/node/Fermenter/pid.h
Normal file
38
src/node/Fermenter/pid.h
Normal file
@ -0,0 +1,38 @@
|
||||
#ifndef PID_H
|
||||
#define PID_H
|
||||
|
||||
#include "patrix/DS18B20Sensor.h"
|
||||
#include "patrix/PIDController.h"
|
||||
#include "patrix/PWMOutput.h"
|
||||
|
||||
#define HEATER_POWER_W 30
|
||||
|
||||
struct History {
|
||||
|
||||
int16_t target;
|
||||
|
||||
int16_t temperature;
|
||||
|
||||
uint8_t heater;
|
||||
|
||||
};
|
||||
|
||||
extern History history[3 * 60];
|
||||
|
||||
extern History* historyPtr;
|
||||
|
||||
extern DS18B20 ds18b20;
|
||||
|
||||
extern DS18B20Sensor temperature;
|
||||
|
||||
extern PWMOutput heater;
|
||||
|
||||
extern PIDController pid;
|
||||
|
||||
void addTarget(double delta);
|
||||
|
||||
void pidSetup();
|
||||
|
||||
void pidLoop();
|
||||
|
||||
#endif
|
||||
14
src/node/Fermenter/rotary.cpp
Normal file
14
src/node/Fermenter/rotary.cpp
Normal file
@ -0,0 +1,14 @@
|
||||
#ifdef NODE_FERMENTER
|
||||
|
||||
#include "rotary.h"
|
||||
#include "display.h"
|
||||
#include "pid.h"
|
||||
|
||||
void rotaryCallback(const int delta) {
|
||||
pid.addTarget(delta);
|
||||
displayModifyTarget = millis();
|
||||
}
|
||||
|
||||
Rotary rotary(D1, D6, rotaryCallback);
|
||||
|
||||
#endif
|
||||
8
src/node/Fermenter/rotary.h
Normal file
8
src/node/Fermenter/rotary.h
Normal file
@ -0,0 +1,8 @@
|
||||
#ifndef ROTARY_H
|
||||
#define ROTARY_H
|
||||
|
||||
#include "patrix/Rotary.h"
|
||||
|
||||
extern Rotary rotary;
|
||||
|
||||
#endif
|
||||
15
src/node/Greenhouse/Greenhouse.cpp
Normal file
15
src/node/Greenhouse/Greenhouse.cpp
Normal file
@ -0,0 +1,15 @@
|
||||
#ifdef NODE_GREENHOUSE
|
||||
|
||||
#include "http.h"
|
||||
#include "sensors.h"
|
||||
|
||||
void patrixSetup() {
|
||||
sensorsSetup();
|
||||
httpSetup2();
|
||||
}
|
||||
|
||||
void patrixLoop() {
|
||||
sensorsLoop();
|
||||
}
|
||||
|
||||
#endif
|
||||
39
src/node/Greenhouse/http.cpp
Normal file
39
src/node/Greenhouse/http.cpp
Normal file
@ -0,0 +1,39 @@
|
||||
#ifdef NODE_GREENHOUSE
|
||||
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
#include "http.h"
|
||||
#include "sensors.h"
|
||||
|
||||
void httpStatus(AsyncWebServerRequest* request) {
|
||||
JsonDocument json;
|
||||
json["illuminance"] = greenhouseTSL.getIlluminance();;
|
||||
json["temperature"] = greenhouseDHT22.getTemperature();
|
||||
json["relative"] = greenhouseDHT22.getRelative();
|
||||
json["absolute"] = greenhouseDHT22.getAbsolute();
|
||||
json["door"] = door.getState();
|
||||
json["windows"] = windows.getState();
|
||||
json["light"] = light.getState();
|
||||
|
||||
AsyncResponseStream* stream = request->beginResponseStream("application/json");
|
||||
serializeJson(json, *stream);
|
||||
request->send(stream);
|
||||
}
|
||||
|
||||
void httpLightOn(AsyncWebServerRequest* request) {
|
||||
light.setState(true);
|
||||
httpStatus(request);
|
||||
}
|
||||
|
||||
void httpLightOff(AsyncWebServerRequest* request) {
|
||||
light.setState(false);
|
||||
httpStatus(request);
|
||||
}
|
||||
|
||||
void httpSetup2() {
|
||||
server.on("/status", httpStatus);
|
||||
server.on("/light/on", httpLightOn);
|
||||
server.on("/light/off", httpLightOff);
|
||||
}
|
||||
|
||||
#endif
|
||||
16
src/node/Greenhouse/http.h
Normal file
16
src/node/Greenhouse/http.h
Normal file
@ -0,0 +1,16 @@
|
||||
#ifdef NODE_GREENHOUSE
|
||||
|
||||
#ifndef HTTP_H
|
||||
#define HTTP_H
|
||||
|
||||
#include "patrix/http.h"
|
||||
|
||||
void httpStatus(AsyncWebServerRequest* request);
|
||||
|
||||
void httpTargetAdd(AsyncWebServerRequest* request);
|
||||
|
||||
void httpSetup2();
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
40
src/node/Greenhouse/sensors.cpp
Normal file
40
src/node/Greenhouse/sensors.cpp
Normal file
@ -0,0 +1,40 @@
|
||||
#ifdef NODE_GREENHOUSE
|
||||
|
||||
#include "sensors.h"
|
||||
|
||||
#define DHT22_GPIO D5
|
||||
#define DOOR_GPIO D0
|
||||
#define WINDOWS_GPIO D6
|
||||
#define LIGHT_GPIO D7
|
||||
|
||||
Input door("greenhouse/door", "DOOR",DOOR_GPIO, true);
|
||||
|
||||
Input windows("greenhouse/windows", "WINDOW", WINDOWS_GPIO, true);
|
||||
|
||||
Output light("greenhouse/light", "LIGHT",LIGHT_GPIO, true);
|
||||
|
||||
TSL2561 greenhouseTSL("greenhouse");
|
||||
|
||||
DHT22Sensor greenhouseDHT22("greenhouse", DHT22_GPIO);
|
||||
|
||||
void sensorsSetup() {
|
||||
pinMode(DOOR_GPIO, INPUT_PULLUP);
|
||||
pinMode(WINDOWS_GPIO, INPUT_PULLUP);
|
||||
pinMode(LIGHT_GPIO, OUTPUT);
|
||||
greenhouseTSL.setup();
|
||||
greenhouseDHT22.setup();
|
||||
}
|
||||
|
||||
void sensorsLoop() {
|
||||
door.loop();
|
||||
windows.loop();
|
||||
light.loop();
|
||||
greenhouseTSL.loop();
|
||||
greenhouseDHT22.loop();
|
||||
}
|
||||
|
||||
void mqttReceive(const String& topic, const String& payload) {
|
||||
light.cmd(topic, payload);
|
||||
}
|
||||
|
||||
#endif
|
||||
27
src/node/Greenhouse/sensors.h
Normal file
27
src/node/Greenhouse/sensors.h
Normal file
@ -0,0 +1,27 @@
|
||||
#ifndef SENSORS_H
|
||||
#define SENSORS_H
|
||||
|
||||
#ifdef NODE_GREENHOUSE
|
||||
|
||||
#include "patrix/DHT22.h"
|
||||
#include "patrix/Input.h"
|
||||
#include "patrix/Output.h"
|
||||
#include "patrix/tsl2561.h"
|
||||
|
||||
extern Input door;
|
||||
|
||||
extern Input windows;
|
||||
|
||||
extern Output light;
|
||||
|
||||
extern TSL2561 greenhouseTSL;
|
||||
|
||||
extern DHT22Sensor greenhouseDHT22;
|
||||
|
||||
void sensorsSetup();
|
||||
|
||||
void sensorsLoop();
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
94
src/patrix/Config.h
Normal file
94
src/patrix/Config.h
Normal file
@ -0,0 +1,94 @@
|
||||
#ifndef PATRIX_CONFIG_H
|
||||
#define PATRIX_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
|
||||
81
src/patrix/DHT22.h
Normal file
81
src/patrix/DHT22.h
Normal file
@ -0,0 +1,81 @@
|
||||
#ifndef DHT22_H
|
||||
#define DHT22_H
|
||||
|
||||
#include "DHT_U.h"
|
||||
#include "humidityRelative.h"
|
||||
#include "mqtt.h"
|
||||
|
||||
class DHT22Sensor {
|
||||
|
||||
int pin;
|
||||
|
||||
DHT_Unified dht;
|
||||
|
||||
unsigned long last = 0UL;
|
||||
|
||||
double temperature = NAN;
|
||||
|
||||
double relative = NAN;
|
||||
|
||||
double absolute = NAN;
|
||||
|
||||
public:
|
||||
|
||||
const String name;
|
||||
|
||||
unsigned long intervalMs;
|
||||
|
||||
explicit DHT22Sensor(String name, const int pin, const unsigned long intervalMs = 5000) : pin(pin), dht(pin, DHT22), name(std::move(name)), intervalMs(intervalMs) {
|
||||
//
|
||||
}
|
||||
|
||||
void setup() {
|
||||
dht.begin();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
const auto now = max(1UL, millis());
|
||||
if (now - last >= intervalMs) {
|
||||
sensors_event_t event;
|
||||
|
||||
dht.temperature().getEvent(&event);
|
||||
temperature = event.temperature;
|
||||
if (isnan(temperature)) {
|
||||
absolute = NAN;
|
||||
Log.error("Error reading temperature!");
|
||||
} else {
|
||||
mqttPublishValue(name + "/temperature", temperature, "TEMPERATURE_C");
|
||||
}
|
||||
|
||||
dht.humidity().getEvent(&event);
|
||||
relative = event.relative_humidity;
|
||||
if (isnan(relative)) {
|
||||
absolute = NAN;
|
||||
Log.error("Error reading humidity!");
|
||||
} else {
|
||||
mqttPublishValue(name + "/humidity/relative", relative, "HUMIDITY_RELATIVE_PERCENT");
|
||||
if (!isnan(temperature)) {
|
||||
absolute = calculateHumidityAbsolute(temperature, relative);
|
||||
mqttPublishValue(name + "/humidity/absolute", absolute, "HUMIDITY_ABSOLUTE_GM3");
|
||||
}
|
||||
}
|
||||
|
||||
last = now;
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] double getTemperature() const {
|
||||
return temperature;
|
||||
}
|
||||
|
||||
[[nodiscard]] double getRelative() const {
|
||||
return relative;
|
||||
}
|
||||
|
||||
[[nodiscard]] double getAbsolute() const {
|
||||
return absolute;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
#endif
|
||||
@ -4,7 +4,6 @@
|
||||
#include <Adafruit_Sensor.h>
|
||||
|
||||
#include "DallasTemperature.h"
|
||||
#include "IValueSensor.h"
|
||||
#include "mqtt.h"
|
||||
#include "OneWire.h"
|
||||
|
||||
@ -45,34 +44,35 @@ public:
|
||||
|
||||
void loop() {
|
||||
switch (state) {
|
||||
case READING:
|
||||
if (bus.isConversionComplete()) {
|
||||
state = COMPLETE;
|
||||
if (first) {
|
||||
first = false;
|
||||
Log.debug("DS18B20: %d devices", bus.getDeviceCount());
|
||||
for (auto index = 0; index < bus.getDeviceCount(); index++) {
|
||||
uint8_t address[8];
|
||||
bus.getAddress(address, index);
|
||||
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]);
|
||||
const auto temperature = bus.getTempC(address);
|
||||
Log.debug(" %s: %5.1f%cC", addressHex, temperature, 176);
|
||||
}
|
||||
case READING:
|
||||
if (bus.isConversionComplete()) {
|
||||
state = COMPLETE;
|
||||
if (first) {
|
||||
first = false;
|
||||
Log.debug("DS18B20: %d devices", bus.getDeviceCount());
|
||||
for (auto index = 0; index < bus.getDeviceCount(); index++) {
|
||||
uint8_t address[8];
|
||||
bus.getAddress(address, index);
|
||||
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]);
|
||||
const auto temperature = bus.getTempC(address);
|
||||
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:
|
||||
state = IDLE;
|
||||
if (last == 0 || millis() - last >= interval) {
|
||||
last = max(1UL, millis());
|
||||
state = READING;
|
||||
bus.requestTemperatures();
|
||||
}
|
||||
break;
|
||||
}
|
||||
else if (millis() - last > timeout) {
|
||||
state = ERROR;
|
||||
Log.error("DS18B20 \"%s\" gpio #%d: timeout", name.c_str(), gpio);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
state = IDLE;
|
||||
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;
|
||||
}
|
||||
|
||||
float getTemperatureByAddress(const uint8_t *address) {
|
||||
float getTemperatureByAddress(const uint8_t* address) {
|
||||
if (state != COMPLETE) {
|
||||
return NAN;
|
||||
}
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
#ifndef DS18B20_SENSOR_H
|
||||
#define DS18B20_SENSOR_H
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "DS18B20.h"
|
||||
#include "IValueSensor.h"
|
||||
|
||||
class DS18B20Sensor final : public IValueSensor {
|
||||
|
||||
@ -13,7 +12,7 @@ class DS18B20Sensor final : public IValueSensor {
|
||||
|
||||
const int index;
|
||||
|
||||
const uint8_t *address;
|
||||
const uint8_t* address;
|
||||
|
||||
float temperature = NAN;
|
||||
|
||||
@ -35,13 +34,15 @@ public:
|
||||
if (bus.isComplete()) {
|
||||
if (index >= 0) {
|
||||
temperature = bus.getTemperatureByIndex(index);
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
temperature = bus.getTemperatureByAddress(address);
|
||||
}
|
||||
if (!name.isEmpty()) {
|
||||
mqttPublishValue(name, temperature, UNIT_TEMPERATURE_C);
|
||||
}
|
||||
} else if (bus.isError()) {
|
||||
}
|
||||
else if (bus.isError()) {
|
||||
temperature = NAN;
|
||||
}
|
||||
}
|
||||
|
||||
68
src/patrix/Input.h
Normal file
68
src/patrix/Input.h
Normal file
@ -0,0 +1,68 @@
|
||||
#ifndef INPUT_H
|
||||
#define INPUT_H
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#include "mqtt.h"
|
||||
|
||||
class Input {
|
||||
|
||||
const int pin;
|
||||
|
||||
const boolean inverted;
|
||||
|
||||
unsigned long debounceMs;
|
||||
|
||||
unsigned long sendMs;
|
||||
|
||||
unsigned long lastRead = 0UL;
|
||||
|
||||
unsigned long lastSent = 0UL;
|
||||
|
||||
boolean state = false;
|
||||
|
||||
public:
|
||||
|
||||
const String name;
|
||||
|
||||
const String category;
|
||||
|
||||
explicit Input(String name, String category, const int pin, const boolean inverted, const unsigned long debounceMs = 500, const unsigned long sendMs = 5000) :
|
||||
pin(pin),
|
||||
inverted(inverted),
|
||||
debounceMs(debounceMs),
|
||||
sendMs(sendMs),
|
||||
name(std::move(name)),
|
||||
category(std::move(category)) {
|
||||
//
|
||||
}
|
||||
|
||||
void setup() {
|
||||
pinMode(pin, INPUT_PULLUP);
|
||||
state = inverted ^ (digitalRead(pin) == HIGH);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
const auto now = max(1UL, millis());
|
||||
|
||||
bool changed = false;
|
||||
if (lastRead == 0 || now - lastRead >= debounceMs) {
|
||||
lastRead = now;
|
||||
const auto current = inverted ^ (digitalRead(pin) == HIGH);
|
||||
changed = state != current;
|
||||
state = current;
|
||||
}
|
||||
|
||||
if (changed || lastSent == 0 || now - lastSent >= sendMs) {
|
||||
lastSent = now;
|
||||
mqttPublishSwitcher(name, category, state);
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] boolean getState() const {
|
||||
return state;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
#endif
|
||||
76
src/patrix/Output.h
Normal file
76
src/patrix/Output.h
Normal file
@ -0,0 +1,76 @@
|
||||
#ifndef OUTPUT_H
|
||||
#define OUTPUT_H
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#include "mqtt.h"
|
||||
|
||||
class Output {
|
||||
|
||||
const int pin;
|
||||
|
||||
const boolean inverted;
|
||||
|
||||
unsigned long sendMs;
|
||||
|
||||
unsigned long lastSent = 0UL;
|
||||
|
||||
boolean state = false;
|
||||
|
||||
public:
|
||||
|
||||
const String name;
|
||||
|
||||
const String category;
|
||||
|
||||
explicit Output(String name, String category, const int pin, const boolean inverted, const unsigned long sendMs = 5000) :
|
||||
pin(pin),
|
||||
inverted(inverted),
|
||||
sendMs(sendMs),
|
||||
name(std::move(name)),
|
||||
category(std::move(category)) {
|
||||
//
|
||||
}
|
||||
|
||||
void setup() const {
|
||||
pinMode(pin, OUTPUT);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
const auto now = max(1UL, millis());
|
||||
const auto changed = state != (inverted ^ (digitalRead(pin) == HIGH));
|
||||
if (changed || lastSent == 0 || now - lastSent >= sendMs) {
|
||||
lastSent = now;
|
||||
digitalWrite(pin, inverted ^ state);
|
||||
mqttPublishSwitcher(name, category, state);
|
||||
}
|
||||
}
|
||||
|
||||
void setState(const boolean newState) {
|
||||
if (this->state != newState) {
|
||||
this->state = newState;
|
||||
Log.info("State changed: name=%s, state=%s", name.c_str(), this->state ? "true" : "false");
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] boolean getState() const {
|
||||
return state;
|
||||
}
|
||||
|
||||
void cmd(const String& topic, const String& payload) {
|
||||
const String expected = String("/cmd/" + name + "/setState");
|
||||
if (!topic.equals(expected)) {
|
||||
return;
|
||||
}
|
||||
if (payload.equals("true")) {
|
||||
setState(true);
|
||||
} else if (payload.equals("false")) {
|
||||
setState(false);
|
||||
} else {
|
||||
Log.error("Invalid payload for command: topic=%s, payload=%s", topic.c_str(), payload.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
#endif
|
||||
@ -3,20 +3,22 @@
|
||||
|
||||
#include <ArduPID.h>
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "IValueSensor.h"
|
||||
#include "PWMOutput.h"
|
||||
|
||||
class PIDController {
|
||||
|
||||
const String name;
|
||||
const String targetName;
|
||||
|
||||
const String inputName;
|
||||
|
||||
const String outputName;
|
||||
|
||||
const IValueSensor& input;
|
||||
|
||||
PWMOutput& output;
|
||||
|
||||
const char *unit;
|
||||
const char* unit;
|
||||
|
||||
ArduPID controller;
|
||||
|
||||
@ -24,12 +26,6 @@ class PIDController {
|
||||
|
||||
double maxValue;
|
||||
|
||||
double p = 0;
|
||||
|
||||
double i = 0;
|
||||
|
||||
double d = 0;
|
||||
|
||||
double inputValue = NAN;
|
||||
|
||||
double outputPercent = NAN;
|
||||
@ -40,8 +36,25 @@ class PIDController {
|
||||
|
||||
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)
|
||||
: name(std::move(name)), input(sensor), output(pwmOutput), unit(unit), controller(), minValue(minValue), maxValue(maxValue), p(p), i(i), d(d) {
|
||||
double p = 0;
|
||||
|
||||
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();
|
||||
if (lastSent == 0 || now - lastSent >= 5000) {
|
||||
lastSent = now;
|
||||
mqttPublishValue(name + "/target", targetValue, unit);
|
||||
mqttPublishValue(name + "/input", inputValue, unit);
|
||||
mqttPublishValue(name + "/output", outputPercent, UNIT_PERCENT);
|
||||
mqttPublishValue(targetName, targetValue, unit);
|
||||
mqttPublishValue(inputName, inputValue, unit);
|
||||
mqttPublishValue(outputName, outputPercent, UNIT_PERCENT);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -77,9 +90,15 @@ public:
|
||||
return targetValue;
|
||||
}
|
||||
|
||||
double setTarget(double target) {
|
||||
targetValue = max(minValue, min(maxValue, target));
|
||||
Log.info("PID \"%s\" target = %.1f", name.c_str(), targetValue);
|
||||
double setTarget(const double newTargetValue) {
|
||||
return setTarget(newTargetValue, true);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@ -1,16 +1,28 @@
|
||||
#include <LittleFS.h>
|
||||
|
||||
#include "Patrix.h"
|
||||
#include "http.h"
|
||||
#include "mqtt.h"
|
||||
#include "system.h"
|
||||
|
||||
void setup() {
|
||||
Log.info("Startup.");
|
||||
if (LittleFS.begin()) {
|
||||
Log.info("Filesystem mounted.");
|
||||
} else {
|
||||
Log.error("Failed to mount filesystem!");
|
||||
}
|
||||
wifiConnect();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
systemLoop();
|
||||
wifiLoop();
|
||||
mqttLoop();
|
||||
if (isAfterBootDelay()) {
|
||||
if (isBootDelayComplete()) {
|
||||
if (isSetupTimeAfterBootDelay()) {
|
||||
patrixSetup();
|
||||
httpSetup();
|
||||
}
|
||||
patrixLoop();
|
||||
}
|
||||
|
||||
@ -1,9 +1,6 @@
|
||||
#ifndef PATRIX_H
|
||||
#define PATRIX_H
|
||||
|
||||
#include "wifi.h"
|
||||
#include "mqtt.h"
|
||||
|
||||
void patrixSetup();
|
||||
|
||||
void patrixLoop();
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
#define BME680_H
|
||||
|
||||
#include "Adafruit_BME680.h"
|
||||
#include "humidityRelative.h"
|
||||
#include "mqtt.h"
|
||||
|
||||
class BME680 {
|
||||
@ -16,7 +17,7 @@ public:
|
||||
|
||||
unsigned long intervalMs;
|
||||
|
||||
explicit BME680(String name, const unsigned long interval_ms = 5000) : name(std::move(name)), intervalMs(interval_ms) {
|
||||
explicit BME680(String name, const unsigned long intervalMs = 5000) : name(std::move(name)), intervalMs(intervalMs) {
|
||||
//
|
||||
}
|
||||
|
||||
@ -59,18 +60,6 @@ public:
|
||||
|
||||
}
|
||||
|
||||
static double calculateHumidityAbsolute(const double temperature, const double humidityRelative) {
|
||||
constexpr auto A = 6.112;
|
||||
constexpr auto m = 17.67;
|
||||
constexpr auto Tn = 243.5;
|
||||
constexpr auto Mw = 18.01534;
|
||||
constexpr auto R = 8.314462618;
|
||||
const auto Tk = temperature + 273.15;
|
||||
const auto P_sat = A * exp((m * temperature) / (temperature + Tn));
|
||||
const auto P_act = P_sat * (humidityRelative / 100.0);
|
||||
return (P_act * Mw) / (R * Tk);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
94
src/patrix/bmp280_aht20.h
Normal file
94
src/patrix/bmp280_aht20.h
Normal file
@ -0,0 +1,94 @@
|
||||
#ifndef BMP280_H
|
||||
#define BMP280_H
|
||||
|
||||
#include "Adafruit_BMP280.h"
|
||||
#include "Adafruit_AHTX0.h"
|
||||
#include "humidityRelative.h"
|
||||
#include "mqtt.h"
|
||||
|
||||
class BMP280_AHT20 {
|
||||
|
||||
Adafruit_BMP280 sensor;
|
||||
|
||||
Adafruit_AHTX0 aht20;
|
||||
|
||||
unsigned long last = 0UL;
|
||||
|
||||
public:
|
||||
|
||||
const String name;
|
||||
|
||||
unsigned long intervalMs;
|
||||
|
||||
explicit BMP280_AHT20(String name, const unsigned long intervalMs = 5000) : name(std::move(name)), intervalMs(intervalMs) {
|
||||
//
|
||||
}
|
||||
|
||||
void setup() {
|
||||
bmp280Setup();
|
||||
aht20Setup();
|
||||
}
|
||||
|
||||
void bmp280Setup() {
|
||||
if (sensor.begin()) {
|
||||
Log.info("BMP280 \"%s\": Initialized.", name.c_str());
|
||||
sensor.setSampling(
|
||||
Adafruit_BMP280::MODE_NORMAL,
|
||||
Adafruit_BMP280::SAMPLING_X16,
|
||||
Adafruit_BMP280::SAMPLING_X16,
|
||||
Adafruit_BMP280::FILTER_X16,
|
||||
Adafruit_BMP280::STANDBY_MS_500
|
||||
);
|
||||
} else {
|
||||
Log.error("BMP280 \"%s\": Failed to initialize.", name.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void aht20Setup() {
|
||||
if (aht20.begin()) {
|
||||
Log.info("AHT20 \"%s\": Initialized.", name.c_str());
|
||||
} else {
|
||||
Log.error("AHT20 \"%s\": Failed to initialize.", name.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void loop() {
|
||||
const auto now = max(1UL, millis());
|
||||
if (last != 0 && now - last < intervalMs) {
|
||||
return;
|
||||
}
|
||||
|
||||
last = now;
|
||||
bmp280Read();
|
||||
aht20Read();
|
||||
}
|
||||
|
||||
void bmp280Read() {
|
||||
const auto pressure = sensor.readPressure();
|
||||
if (isnan(pressure)) {
|
||||
Log.error("BMP280 \"%s\": Failed to read", name.c_str());
|
||||
bmp280Setup();
|
||||
return;
|
||||
}
|
||||
|
||||
mqttPublishValue(name + "/pressure", pressure / 100.0, "PRESSURE_HPA");
|
||||
}
|
||||
|
||||
void aht20Read() {
|
||||
sensors_event_t temperature{};
|
||||
sensors_event_t relative{};
|
||||
if (!aht20.getEvent(&relative, &temperature)) {
|
||||
Log.error("AHT20 \"%s\": Failed to read", name.c_str());
|
||||
aht20Setup();
|
||||
return;
|
||||
}
|
||||
|
||||
const auto absolute = calculateHumidityAbsolute(temperature.temperature, relative.relative_humidity);
|
||||
mqttPublishValue(name + "/temperature", temperature.temperature, "TEMPERATURE_C");
|
||||
mqttPublishValue(name + "/relative", relative.relative_humidity, "HUMIDITY_RELATIVE_PERCENT");
|
||||
mqttPublishValue(name + "/absolute", absolute, "HUMIDITY_ABSOLUTE_GM3");
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
#endif
|
||||
36
src/patrix/http.cpp
Normal file
36
src/patrix/http.cpp
Normal file
@ -0,0 +1,36 @@
|
||||
#include "http.h"
|
||||
#include "system.h"
|
||||
|
||||
#include <LittleFS.h>
|
||||
|
||||
AsyncWebServer server(80);
|
||||
|
||||
void httpNotFound(AsyncWebServerRequest* request) {
|
||||
const String path = request->url();
|
||||
if (path.endsWith("/") && path.length() > 1) {
|
||||
request->redirect(path.substring(0, path.length() - 1));
|
||||
} else if (request->method() == HTTP_OPTIONS) {
|
||||
request->send(200);
|
||||
} else {
|
||||
request->send(404, "text/plain", "not found");
|
||||
}
|
||||
}
|
||||
|
||||
void httpRestart([[maybe_unused]] AsyncWebServerRequest* request) {
|
||||
systemRequestRestart();
|
||||
request->send(200, "application/json", "true");
|
||||
}
|
||||
|
||||
void httpSetup() {
|
||||
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*");
|
||||
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Methods", "GET, POST, PUT");
|
||||
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "Content-Type");
|
||||
server.on("/restart", httpRestart);
|
||||
server.serveStatic("/", LittleFS, "/http/", "max-age=86400").setDefaultFile("index.html");
|
||||
server.onNotFound(httpNotFound);
|
||||
server.begin();
|
||||
}
|
||||
|
||||
void httpStop() {
|
||||
server.end();
|
||||
}
|
||||
12
src/patrix/http.h
Normal file
12
src/patrix/http.h
Normal file
@ -0,0 +1,12 @@
|
||||
#ifndef HELLIGKEIT_HTTP_H
|
||||
#define HELLIGKEIT_HTTP_H
|
||||
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
extern AsyncWebServer server;
|
||||
|
||||
void httpSetup();
|
||||
|
||||
void httpStop();
|
||||
|
||||
#endif
|
||||
15
src/patrix/humidityRelative.cpp
Normal file
15
src/patrix/humidityRelative.cpp
Normal file
@ -0,0 +1,15 @@
|
||||
#include "humidityRelative.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
double calculateHumidityAbsolute(const double temperatureCelsius, const double humidityRelativePercent) {
|
||||
constexpr auto A = 6.112;
|
||||
constexpr auto m = 17.67;
|
||||
constexpr auto Tn = 243.5;
|
||||
constexpr auto Mw = 18.01534;
|
||||
constexpr auto R = 8.314462618;
|
||||
const auto Tk = temperatureCelsius + 273.15;
|
||||
const auto P_sat = A * exp((m * temperatureCelsius) / (temperatureCelsius + Tn));
|
||||
const auto P_act = P_sat * humidityRelativePercent;
|
||||
return (P_act * Mw) / (R * Tk);
|
||||
}
|
||||
6
src/patrix/humidityRelative.h
Normal file
6
src/patrix/humidityRelative.h
Normal file
@ -0,0 +1,6 @@
|
||||
#ifndef HUMIDITY_RELATIVE_H
|
||||
#define HUMIDITY_RELATIVE_H
|
||||
|
||||
double calculateHumidityAbsolute(double temperatureCelsius, double humidityRelativePercent);
|
||||
|
||||
#endif
|
||||
@ -3,6 +3,7 @@
|
||||
|
||||
#include <ESP8266WiFi.h>
|
||||
#include "PubSubClient.h"
|
||||
#include "system.h"
|
||||
#include <WiFiClient.h>
|
||||
|
||||
WiFiClient client;
|
||||
@ -16,32 +17,33 @@ unsigned long mqttFailureMillis = 0;
|
||||
// ReSharper disable once CppUseAuto
|
||||
const String logTopic = String("log/") + HOSTNAME;
|
||||
|
||||
// ReSharper disable once CppUseAuto
|
||||
const String cmdTopic = String("cmd/") + HOSTNAME;
|
||||
|
||||
void mqttCallback(char *topic, const uint8_t *payload, unsigned int length) {
|
||||
void mqttCallback(char* topic, const uint8_t* payload, unsigned int length) {
|
||||
char message[500];
|
||||
length = min(sizeof message - 1, length);
|
||||
memcpy(message, payload, length);
|
||||
*(message + length) = 0;
|
||||
Log.info(R"(MQTT received: topic="%s", message="%s")", topic, message);
|
||||
if (strcmp(message, "help") == 0) {
|
||||
Log.info("HELP");
|
||||
Log.info(" %s", "help");
|
||||
Log.info(" %s", "info");
|
||||
Log.info(" %s", "reboot");
|
||||
} else if (strcmp(message, "info") == 0) {
|
||||
Log.info("INFO");
|
||||
Log.info(" %-10s %s", "SSID:", WiFi.SSID().c_str());
|
||||
Log.info(" %-10s %s", "IP:", WiFi.localIP().toString().c_str());
|
||||
Log.info(" %-10s %d", "RSSI:", WiFi.RSSI());
|
||||
Log.info(" %-10s %s", "uptime:", uptimeString().c_str());
|
||||
} else if (strcmp(message, "reboot") == 0) {
|
||||
Log.info("rebooting...");
|
||||
delay(500);
|
||||
mqtt.disconnect();
|
||||
delay(500);
|
||||
EspClass::restart();
|
||||
const String topicStr = String(topic);
|
||||
const String messageStr = String(message);
|
||||
Log.info(R"(MQTT received: topic="%s", message="%s")", topicStr.c_str(), messageStr.c_str());
|
||||
if (topicStr.equals(String("/cmd/") + HOSTNAME)) {
|
||||
if (messageStr.equals("help")) {
|
||||
Log.info("HELP");
|
||||
Log.info(" %s", "help");
|
||||
Log.info(" %s", "info");
|
||||
Log.info(" %s", "restart");
|
||||
} else if (messageStr.equals("info")) {
|
||||
Log.info("INFO");
|
||||
Log.info(" %-10s %s", "SSID:", WiFi.SSID().c_str());
|
||||
Log.info(" %-10s %s", "IP:", WiFi.localIP().toString().c_str());
|
||||
Log.info(" %-10s %d", "RSSI:", WiFi.RSSI());
|
||||
Log.info(" %-10s %s", "uptime:", uptimeString().c_str());
|
||||
} else if (messageStr.equals("restart")) {
|
||||
systemRequestRestart();
|
||||
} else {
|
||||
Log.warn("Unknown node command: %s", messageStr.c_str());
|
||||
}
|
||||
} else {
|
||||
mqttReceive(topicStr, messageStr);
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,7 +54,7 @@ void mqttLoop() {
|
||||
yield();
|
||||
mqttPublish(logTopic, "connected\n");
|
||||
mqtt.setCallback(mqttCallback);
|
||||
mqtt.subscribe(cmdTopic.c_str());
|
||||
mqtt.subscribe("/cmd/#");
|
||||
Log.info("MQTT connected as \"%s\".", HOSTNAME);
|
||||
mqttFailureMillis = 0;
|
||||
} else {
|
||||
@ -62,37 +64,56 @@ void mqttLoop() {
|
||||
}
|
||||
}
|
||||
|
||||
void mqttPublishValue(const String& name, const int32_t value, const char *unit) {
|
||||
void mqttDisconnect() {
|
||||
mqtt.disconnect();
|
||||
}
|
||||
|
||||
void mqttPublishValue(const String& name, const boolean value, const char* unit) {
|
||||
mqttPublishValue(name, String(value ? 1 : 0), unit);
|
||||
}
|
||||
|
||||
void mqttPublishValue(const String& name, const int32_t value, const char* unit) {
|
||||
mqttPublishValue(name, String(value), unit);
|
||||
}
|
||||
|
||||
void mqttPublishValue(const String& name, const int64_t value, const char *unit) {
|
||||
void mqttPublishValue(const String& name, const int64_t value, const char* unit) {
|
||||
mqttPublishValue(name, String(value), unit);
|
||||
}
|
||||
|
||||
void mqttPublishValue(const String& name, const uint32_t value, const char *unit) {
|
||||
void mqttPublishValue(const String& name, const uint32_t value, const char* unit) {
|
||||
mqttPublishValue(name, String(value), unit);
|
||||
}
|
||||
|
||||
void mqttPublishValue(const String& name, const uint64_t value, const char *unit) {
|
||||
void mqttPublishValue(const String& name, const uint64_t value, const char* unit) {
|
||||
mqttPublishValue(name, String(value), unit);
|
||||
}
|
||||
|
||||
void mqttPublishValue(const String& name, const float value, const char *unit) {
|
||||
void mqttPublishValue(const String& name, const float value, const char* unit) {
|
||||
if (isnan(value)) {
|
||||
return;
|
||||
}
|
||||
mqttPublishValue(name, String(value), unit);
|
||||
}
|
||||
|
||||
void mqttPublishValue(const String& name, const double value, const char *unit) {
|
||||
void mqttPublishValue(const String& name, const double value, const char* unit) {
|
||||
if (isnan(value)) {
|
||||
return;
|
||||
}
|
||||
mqttPublishValue(name, String(value), unit);
|
||||
}
|
||||
|
||||
void mqttPublishValue(const String& name, const String& value, const char *unit) {
|
||||
void mqttPublishSwitcher(const String& name, const String& category, const boolean state) {
|
||||
char buffer[200];
|
||||
snprintf(buffer, sizeof buffer, R"({"name": "%s", "type": "SWITCHER", "category": "%s", "timestamp": %lld, "state": %s})",
|
||||
name.c_str(),
|
||||
category.c_str(),
|
||||
time(nullptr),
|
||||
state ? "true" : "false"
|
||||
);
|
||||
mqttPublish(name + "/type/SWITCHER", buffer);
|
||||
}
|
||||
|
||||
void mqttPublishValue(const String& name, const String& value, const char* unit) {
|
||||
char buffer[200];
|
||||
snprintf(buffer, sizeof buffer, R"({"name": "%s", "timestamp": %lld, "value": %s, "unit": "%s"})", name.c_str(), time(nullptr), value.c_str(), unit);
|
||||
mqttPublish(name + "/SimpleJson", buffer);
|
||||
|
||||
@ -28,31 +28,39 @@ extern const String cmdTopic;
|
||||
|
||||
void mqttLoop();
|
||||
|
||||
void mqttPublishValue(const String& name, int32_t value, const char *unit);
|
||||
void mqttDisconnect();
|
||||
|
||||
void mqttPublishValue(const String& name, int64_t value, const char *unit);
|
||||
void mqttPublishValue(const String& name, boolean value, const char* unit);
|
||||
|
||||
void mqttPublishValue(const String& name, uint32_t value, const char *unit);
|
||||
void mqttPublishValue(const String& name, int32_t value, const char* unit);
|
||||
|
||||
void mqttPublishValue(const String& name, uint64_t value, const char *unit);
|
||||
void mqttPublishValue(const String& name, int64_t value, const char* unit);
|
||||
|
||||
void mqttPublishValue(const String& name, float value, const char *unit);
|
||||
void mqttPublishValue(const String& name, uint32_t value, const char* unit);
|
||||
|
||||
void mqttPublishValue(const String& name, double value, const char *unit);
|
||||
void mqttPublishValue(const String& name, uint64_t value, const char* unit);
|
||||
|
||||
void mqttPublishValue(const String& name, const String& value, const char *unit);
|
||||
void mqttPublishValue(const String& name, float value, const char* unit);
|
||||
|
||||
void mqttPublishValue(const String& name, double value, const char* unit);
|
||||
|
||||
void mqttPublishSwitcher(const String& name, const String& category, boolean state);
|
||||
|
||||
void mqttPublishValue(const String& name, const String& value, const char* unit);
|
||||
|
||||
void mqttPublish(const String& topic, const String& payload);
|
||||
|
||||
void mqttReceive(const String& topic, const String& payload);
|
||||
|
||||
class LogClass final : public Stream {
|
||||
|
||||
PubSubClient& mqtt;
|
||||
|
||||
uint8_t buffer[500] = {};
|
||||
|
||||
uint8_t *bufferWrite = buffer;
|
||||
uint8_t* bufferWrite = buffer;
|
||||
|
||||
uint8_t *bufferLast = buffer + sizeof(buffer) - 1;
|
||||
uint8_t* bufferLast = buffer + sizeof(buffer) - 1;
|
||||
|
||||
size_t overflow = 0;
|
||||
|
||||
@ -81,7 +89,7 @@ public:
|
||||
mqttPublish(logTopic, "\n### LOG BUFFER OVERFLOW BY %d BYTES ###\n");
|
||||
overflow = 0;
|
||||
}
|
||||
mqttPublish(logTopic, reinterpret_cast<const char *>(buffer));
|
||||
mqttPublish(logTopic, reinterpret_cast<const char*>(buffer));
|
||||
bufferWrite = buffer;
|
||||
*bufferWrite = 0;
|
||||
due = false;
|
||||
@ -106,28 +114,28 @@ public:
|
||||
OFF, ERROR, WARN, INFO, DEBUG,
|
||||
};
|
||||
|
||||
void debug(const char *format, ...) {
|
||||
void debug(const char* format, ...) {
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
log(DEBUG, format, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
void info(const char *format, ...) {
|
||||
void info(const char* format, ...) {
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
log(INFO, format, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
void warn(const char *format, ...) {
|
||||
void warn(const char* format, ...) {
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
log(WARN, format, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
void error(const char *format, ...) {
|
||||
void error(const char* format, ...) {
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
log(ERROR, format, args);
|
||||
@ -136,14 +144,14 @@ public:
|
||||
|
||||
Level level = DEBUG;
|
||||
|
||||
void log(const Level level, const char *format, const va_list args) {
|
||||
void log(const Level level, const char* format, const va_list args) {
|
||||
if (this->level < level) {
|
||||
return;
|
||||
}
|
||||
|
||||
print(getTimeString().c_str());
|
||||
|
||||
const char *levelName;
|
||||
const char* levelName;
|
||||
switch (level) {
|
||||
case ERROR:
|
||||
levelName = "ERROR";
|
||||
|
||||
24
src/patrix/system.cpp
Normal file
24
src/patrix/system.cpp
Normal file
@ -0,0 +1,24 @@
|
||||
#include "system.h"
|
||||
|
||||
#include "mqtt.h"
|
||||
#include "http.h"
|
||||
|
||||
bool systemRestart = false;
|
||||
|
||||
void systemRequestRestart() {
|
||||
systemRestart = true;
|
||||
}
|
||||
|
||||
void systemLoop() {
|
||||
if (systemRestart) {
|
||||
Log.info("Restarting...");
|
||||
delay(250);
|
||||
|
||||
httpStop();
|
||||
mqttDisconnect();
|
||||
wifiDisconnect();
|
||||
|
||||
delay(250);
|
||||
EspClass::restart();
|
||||
}
|
||||
}
|
||||
8
src/patrix/system.h
Normal file
8
src/patrix/system.h
Normal file
@ -0,0 +1,8 @@
|
||||
#ifndef HELLIGKEIT_SYSTEM_H
|
||||
#define HELLIGKEIT_SYSTEM_H
|
||||
|
||||
void systemRequestRestart();
|
||||
|
||||
void systemLoop();
|
||||
|
||||
#endif
|
||||
@ -2,6 +2,7 @@
|
||||
#define TSL2561_H
|
||||
|
||||
#include "Adafruit_TSL2561_U.h"
|
||||
#include "mqtt.h"
|
||||
|
||||
class TSL2561 {
|
||||
|
||||
@ -9,13 +10,15 @@ class TSL2561 {
|
||||
|
||||
unsigned long last = 0UL;
|
||||
|
||||
int64_t illuminance = -1;
|
||||
|
||||
public:
|
||||
|
||||
const String name;
|
||||
|
||||
unsigned long intervalMs;
|
||||
|
||||
explicit TSL2561(String name, const uint8_t address = TSL2561_ADDR_FLOAT, const unsigned long interval_ms = 5000) : tsl(Adafruit_TSL2561_Unified(address)), name(std::move(name)), intervalMs(interval_ms) {
|
||||
explicit TSL2561(String name, const uint8_t address = TSL2561_ADDR_FLOAT, const unsigned long intervalMs = 5000) : tsl(Adafruit_TSL2561_Unified(address)), name(std::move(name)), intervalMs(intervalMs) {
|
||||
//
|
||||
}
|
||||
|
||||
@ -37,14 +40,19 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] int64_t getIlluminance() const {
|
||||
return illuminance;
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
void read() {
|
||||
uint16_t broadband;
|
||||
uint16_t ir;
|
||||
tsl.getLuminosity(&broadband, &ir);
|
||||
const auto illuminance = tsl.calculateLux(broadband, ir);
|
||||
illuminance = tsl.calculateLux(broadband, ir);
|
||||
if (illuminance == 65536) {
|
||||
illuminance = -1;
|
||||
Log.error("TSL2561 \"%s\": Failed to read.", name.c_str());
|
||||
setup();
|
||||
} else {
|
||||
|
||||
@ -28,15 +28,20 @@ void wifiConnect() {
|
||||
ArduinoOTA.onError([](const ota_error_t error) {
|
||||
const char *name;
|
||||
switch (error) {
|
||||
case OTA_AUTH_ERROR: name = "AUTH";
|
||||
case OTA_AUTH_ERROR:
|
||||
name = "AUTH";
|
||||
break;
|
||||
case OTA_BEGIN_ERROR: name = "BEGIN";
|
||||
case OTA_BEGIN_ERROR:
|
||||
name = "BEGIN";
|
||||
break;
|
||||
case OTA_CONNECT_ERROR: name = "CONNECT";
|
||||
case OTA_CONNECT_ERROR:
|
||||
name = "CONNECT";
|
||||
break;
|
||||
case OTA_RECEIVE_ERROR: name = "RECEIVE";
|
||||
case OTA_RECEIVE_ERROR:
|
||||
name = "RECEIVE";
|
||||
break;
|
||||
case OTA_END_ERROR: name = "END";
|
||||
case OTA_END_ERROR:
|
||||
name = "END";
|
||||
break;
|
||||
default:
|
||||
name = "[???]";
|
||||
@ -133,6 +138,10 @@ void wifiLoop() {
|
||||
timeLoop();
|
||||
}
|
||||
|
||||
void wifiDisconnect() {
|
||||
WiFi.disconnect();
|
||||
}
|
||||
|
||||
String getTimeString() {
|
||||
time_t now;
|
||||
time(&now);
|
||||
@ -156,6 +165,6 @@ bool isSetupTimeAfterBootDelay() {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isAfterBootDelay() {
|
||||
bool isBootDelayComplete() {
|
||||
return bootDelayOver;
|
||||
}
|
||||
|
||||
@ -7,13 +7,15 @@ void wifiConnect();
|
||||
|
||||
void wifiLoop();
|
||||
|
||||
void wifiDisconnect();
|
||||
|
||||
String getTimeString();
|
||||
|
||||
bool isWifiConnected();
|
||||
|
||||
bool isSetupTimeAfterBootDelay();
|
||||
|
||||
bool isAfterBootDelay();
|
||||
bool isBootDelayComplete();
|
||||
|
||||
uint64_t uptimeMillis();
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user