Řízení vytápění¶
Na této stránce spojujete senzory, nastavení a výkonovou část v pracovní logiku. Zařízení udržuje v skříni zadanou teplotu, chrání ohřívač před přehřátím a reaguje na příkazy z portálu.
Logika se provádí v loop() vedle údržby sítě. Všechny časovače a prahy jsou neblokující, bez delay().
Co by se mělo stát¶
Chování skříně se skládá ze tří jednoduchých pravidel:
- Údržba teploty. Pokud je vzduch ve skříni chladnější než cíl o hodnotu histereze — zapnout vytápění. Když dosáhne cíle — vypnout.
- Ochrana ohřívače. Termistor kontroluje samotný ohřívač. Pokud se přehřál nad přípustnou hranici — vytápění se vypne nezávisle na teplotě vzduchu.
- Ventilátor. Zapíná se, aby se rozprostřelo teplo po skříni, a vypíná se, když vytápění není potřeba.
Klíče ohřívače a ventilátoru¶
Ohřívač a ventilátor řadič zapíná pomocí klíče: MOSFET modul (verze A) nebo SSR (verze B) — viz Schéma zapojení. Z pohledu kódu je to jednoduše výstup GPIO: HIGH — zapnuto, LOW — vypnuto.
Popišme takový klíč malou strukturou a máme dva výskyty — pro ohřívač a ventilátor. Přidejte to do src/main.cpp (před setup()):
struct GpioOutput {
int pin;
void begin() { pinMode(pin, OUTPUT); digitalWrite(pin, LOW); }
void on() { digitalWrite(pin, HIGH); }
void off() { digitalWrite(pin, LOW); }
};
static GpioOutput myHeater{4}; // GPIO4 — řízení ohřívače
static GpioOutput myFan{5}; // GPIO5 — řízení ventilátoru
Čísla výstupů jsou stejná jako v Schématu zapojení. V setup() oba klíče musí být inicializovány: myHeater.begin(); a myFan.begin();.
Bezpečný stav při spuštění
begin() okamžitě nastaví LOW — ohřívač a ventilátor jsou vypnuty, dokud logika nerozhodne jinak. To je důležité: při zapnutí napájení by se ohřívač neměl náhodou zapnout.
Údržba teploty histerezí¶
Pro skříň na 40–45 °C je dostačující jednoduchá hystereze: vytápění se zapíná a vypíná kolem cíle. To je jednodušší než plný PID a pro měkké udržování tepla funguje spolehlivě.
Cílovou teplotu a hysterezí vezmeme z menu (menu.target_temp, menu.hysteresis) — je již připojeno v kapitole 6. Přidejte příznak stavu a funkci rozhodování:
static bool s_heating = false;
static void controlLoop() {
float air = s_link.telemetry.airTempC[0]; // SHT31
float target = (float)menu.target_temp; // z menu
float hyst = (float)menu.hysteresis; // z menu
if (air < target - hyst) {
s_heating = true; // vychladla — topíme
} else if (air >= target) {
s_heating = false; // dosáhla cíle — stop
}
}
Cílová teplota a hystereze se berou z menu — uživatel je mění z portálu.
Ochrana ohřívače termistorem¶
Vzduch se ohřívá pomalu, ale spirála ohřívače — rychle. Bez samostatného řízení se ohřívač stihne přehřát dříve, než se vzduch dostane na cíl. Proto termistor ohřívače nastavuje tvrdý strop.
static const float HEATER_MAX_C = 80.0f; // strop teploty ohřívače
static void applyHeater() {
float heaterTemp = s_link.telemetry.heaterTempC[0]; // termistor
bool allow = s_heating && heaterTemp < HEATER_MAX_C;
if (allow) {
myHeater.on();
s_link.telemetry.heaterPower01[0] = 1.0f; // odrazit v telemetrii
} else {
myHeater.off();
s_link.telemetry.heaterPower01[0] = 0.0f;
}
}
Strop ohřívače — to je ochrana, ne seřízení klimatu
HEATER_MAX_C omezuje teplotu samotného ohřívače, ne vzduchu. Hodnota závisí na konstrukci ohřívače a materiálech skříně. Vyberte ji s rezervou pod teplotu, při které se deformují tištěné díly — viz Žáruvzdorné materiály.
Pro měkčí ohřev místo zapínání/vypínání „všechno nebo nic" lze řídit výkon pomocí PWM a pole heaterPower01[0] přijímá hodnoty od 0.0 do 1.0. Pro skříň s měkkým udržováním tepla je výše uvedená jednoduchá logika obvykle dostačující.
Ventilátor¶
Ventilátor rozprostírá teplo po skříni. Nejjednodušší logika — zapínáme ho spolu s vytápěním:
static void applyFan() {
bool fanOn = s_heating; // točíme, zatímco topíme
if (fanOn) myFan.on(); else myFan.off();
s_link.telemetry.fanOn[0] = fanOn; // odrazit v telemetrii
}
V sériovém řadiči je ventilátor řízený teplotou se samostatnými prahy zapnutí a vypnutí (například zapnutí při 55 °C, vypnutí při 35 °C), aby se nedvíhal na hranici. Pro skříň lze aplikovat stejný přístup, vázáním prahů na parametry menu.
Montáž v loop()¶
void loop() {
s_link.loop(); // síť a automatické publikování
// senzory (viz krok „Senzory"):
s_climate.tick(millis());
SensorReading c = s_climate.get();
if (c.ok) {
s_link.telemetry.airTempC[0] = c.temperature;
s_link.telemetry.airHumidityPct[0] = c.humidity;
}
s_link.telemetry.heaterTempC[0] = readHeaterTempC();
controlLoop(); // rozhodujeme, topit nebo ne
applyHeater(); // aplikujeme na ohřívač + ochrana
applyFan(); // aplikujeme na ventilátor
}
Pole telemetrie (heaterPower01, fanOn) fasáda publikuje sama — na portálu je vidět, zda zařízení právě topí a zda ventilátor funguje.
Příkazy z portálu¶
Spuštění a zastavení údržby tepla portál posílá jako příkazy. Obslužná rutina se registruje metodou s_link.onCommand(jméno, callback) — po s_link.begin(). Příkazy akcí přicházejí s názvem invoke a polem action (role z menu, např. storage.start / storage.stop).
Pro analýzu JSON jsou potřeba záhlaví <ArduinoJson.h> a <string.h> (pro strcmp) — přidejte je k ostatním #include na začátku souboru. Samotná obslužná rutina se umisťuje do setup():
s_link.onCommand("invoke", [](JsonObjectConst data) {
const char* action = data["action"] | "";
if (strcmp(action, "storage.start") == 0) {
s_heating = true;
s_link.status.mode[0] = iDryer::UnitMode::Storage;
s_link.status.targetTempC[0] = (float)menu.target_temp;
s_link.publishStatusNow();
} else if (strcmp(action, "storage.stop") == 0) {
s_heating = false;
myHeater.off();
s_link.status.mode[0] = iDryer::UnitMode::Idle;
s_link.publishStatusNow();
}
});
storage.start/storage.stop— stejné role, které jste nastavili v menu; portál podle nich kreslí tlačítka.iDryer::UnitMode::Storage— režim měkkého udržování tepla. To je hlavní režim skříně.s_link.status.mode[0]atargetTempC[0]zobrazují na portálu aktuální stav komory.publishStatusNow()volejte po každé změně stavu, aby portál viděl změnu hned, bez čekání na časovač.
Žádné delay() v obslužné rutině
Obslužná rutina onCommand se volá z síťového callbacku. Jakékoli blokování v ní přeruší MQTT-relaci. Měňte příznaky a stav, práci samu dělej v loop().
Úplný src/main.cpp po této kapitole¶
Toto je finální, hotový soubor zařízení. Nové řádky oproti předchozí kapitole jsou označeny // ← kapitola 7. Stejný soubor leží jako hotový příklad ve složce example/09-cabinet/ repozitáře a sestavuje se příkazem pio run -e cabinet.
Co bylo — src/main.cpp po kapitole 6
#include <iDryer.h>
#include <Wire.h>
#include <math.h>
#include "Sht31ClimateSensor.h"
#include <menu_state.h>
static const iDryer::Config CFG = {
.deviceType = iDryer::DeviceType::Dryer,
.unitsCount = 1,
.hasHeater = true,
.hasFan = true,
.hasAirTemp = true,
.hasAirHumidity = true,
.hasHeaterTemp = true,
.telemetryPeriodMs = 5000,
.statusPeriodMs = 10000,
.hardwareVersion = "1.0",
.firmwareVersion = "0.1.0",
.model = "DIY Storage Cabinet",
};
static iDryer::Link s_link(CFG);
static Sht31ClimateSensor s_climate(&Wire);
static bool s_climateOk = false;
static const int THERM_PIN = 2;
static const float SERIES_R = 4700.0f;
static const float NOMINAL_R = 100000.0f;
static const float NOMINAL_T = 25.0f;
static const float BETA = 3950.0f;
static float readHeaterTempC() {
int raw = analogRead(THERM_PIN);
float v = (float)raw / 4095.0f;
float r = SERIES_R * (1.0f - v) / v;
float tK = 1.0f / (1.0f / (NOMINAL_T + 273.15f) + logf(r / NOMINAL_R) / BETA);
return tK - 273.15f;
}
void setup() {
Serial.begin(115200);
Wire.begin(8, 9);
s_climateOk = s_climate.begin();
menu.initDefaults();
s_link.begin();
}
void loop() {
s_link.loop();
if (s_climateOk) {
s_climate.tick(millis());
SensorReading r = s_climate.get();
if (r.ok) {
s_link.telemetry.airTempC[0] = r.temperature;
s_link.telemetry.airHumidityPct[0] = r.humidity;
}
}
s_link.telemetry.heaterTempC[0] = readHeaterTempC();
}
#include <Wire.h>
#include <ArduinoJson.h> // ← kapitola 7 (onCommand: JsonObjectConst)
#include <string.h> // ← kapitola 7 (strcmp)
#include <math.h>
#include <iDryer.h>
#include "Sht31ClimateSensor.h"
#include <menu_state.h>
static const iDryer::Config CFG = {
.deviceType = iDryer::DeviceType::Dryer,
.unitsCount = 1,
.hasHeater = true,
.hasFan = true,
.hasAirTemp = true,
.hasAirHumidity = true,
.hasHeaterTemp = true,
.telemetryPeriodMs = 5000,
.statusPeriodMs = 10000,
.hardwareVersion = "1.0",
.firmwareVersion = "0.1.0",
.model = "DIY Storage Cabinet",
};
static iDryer::Link s_link(CFG);
static Sht31ClimateSensor s_climate(&Wire);
static bool s_climateOk = false;
static const int THERM_PIN = 2;
static const float SERIES_R = 4700.0f;
static const float NOMINAL_R = 100000.0f;
static const float NOMINAL_T = 25.0f;
static const float BETA = 3950.0f;
static float readHeaterTempC() {
int raw = analogRead(THERM_PIN);
float v = (float)raw / 4095.0f;
float r = SERIES_R * (1.0f - v) / v;
float tK = 1.0f / (1.0f / (NOMINAL_T + 273.15f) + logf(r / NOMINAL_R) / BETA);
return tK - 273.15f;
}
// ← kapitola 7: klíče ohřívače a ventilátoru
struct GpioOutput {
int pin;
void begin() { pinMode(pin, OUTPUT); digitalWrite(pin, LOW); }
void on() { digitalWrite(pin, HIGH); }
void off() { digitalWrite(pin, LOW); }
};
static GpioOutput myHeater{4};
static GpioOutput myFan{5};
// ← kapitola 7: logika údržby teploty
static bool s_heating = false;
static const float HEATER_MAX_C = 80.0f;
static void controlLoop() {
float air = s_link.telemetry.airTempC[0];
float target = (float)menu.target_temp;
float hyst = (float)menu.hysteresis;
if (air < target - hyst) s_heating = true;
else if (air >= target) s_heating = false;
}
static void applyHeater() {
float heaterTemp = s_link.telemetry.heaterTempC[0];
bool allow = s_heating && heaterTemp < HEATER_MAX_C;
if (allow) myHeater.on(); else myHeater.off();
s_link.telemetry.heaterPower01[0] = allow ? 1.0f : 0.0f;
}
static void applyFan() {
if (s_heating) myFan.on(); else myFan.off();
s_link.telemetry.fanOn[0] = s_heating;
}
void setup() {
Serial.begin(115200);
Wire.begin(8, 9);
s_climateOk = s_climate.begin();
myHeater.begin(); // ← kapitola 7
myFan.begin(); // ← kapitola 7
menu.initDefaults();
s_link.begin();
s_link.onCommand("invoke", [](JsonObjectConst data) { // ← kapitola 7
const char* action = data["action"] | "";
if (strcmp(action, "storage.start") == 0) {
s_heating = true;
s_link.status.mode[0] = iDryer::UnitMode::Storage;
s_link.status.targetTempC[0] = (float)menu.target_temp;
s_link.publishStatusNow();
} else if (strcmp(action, "storage.stop") == 0) {
s_heating = false;
myHeater.off();
s_link.status.mode[0] = iDryer::UnitMode::Idle;
s_link.publishStatusNow();
}
});
}
void loop() {
s_link.loop();
if (s_climateOk) {
s_climate.tick(millis());
SensorReading r = s_climate.get();
if (r.ok) {
s_link.telemetry.airTempC[0] = r.temperature;
s_link.telemetry.airHumidityPct[0] = r.humidity;
}
}
s_link.telemetry.heaterTempC[0] = readHeaterTempC();
controlLoop(); // ← kapitola 7
applyHeater(); // ← kapitola 7
applyFan(); // ← kapitola 7
}
Kontrola výsledku¶
Po tomto kroku:
- spuštění z portálu převede skříň do režimu Storage, zařízení začne topit;
- teplota vzduchu se přiblíží k cíli a zůstane v mezích histereze;
- ohřívač nepřekročí
HEATER_MAX_C; - ventilátor a výkon topení jsou vidět v telemetrii;
- zastavení z portálu vypne vytápění a převede do režimu Idle.
Co dál¶
Logika je hotova. Zbývá složit zařízení do skříně a zkontrolovat pod napájením — Montáž a kontrola.