---
title: "Jak jsem stavěl box pro Prusa MK4S - Programování"
categories: 
  - "3d-tisk"
tags: 
  - "arduino-cs"
  - "box-pro-3d-tiskarnu"
  - "c"
  - "ikea-lack-cs"
  - "prusa-mk4s-cs"
coverImage: "mk4s_programming_featured_image_ai.webp"
draft: true
---

[![](images/mk4s_programming_featured_image_ai-1024x819.webp)](https://mouseviator.com/wp-content/uploads/2025/09/mk4s_programming_featured_image_ai.webp)

A jsme tu, v poslední kapitole. Zde se budeme zabývat tím, aby „kryt“ pro 3D tiskárnu Prusa MK4S skutečně něco dělal. No, to hlavní, co má dělat – kryt tiskárny, dělá už teď. Ale přidané vychytávky, jako jsou teplotní senzory, senzor plynu a možná ještě jedna věc, zatím nefungují.

Těmto komponentám musíme sdělit, co mají dělat, což jinými slovy znamená, že je musíme naprogramovat. Jak bylo popsáno v předchozích kapitolách, zvolenou řídicí deskou je Arduino UNO, takže programovací jazyk bude C++.

V této kapitole se očekává, že máte nějaké znalosti programování, programování s Arduinem a C++, protože ponořovat se do toho opravdu není v rozsahu tohoto „příspěvku/článku“. Stejně tak zde není uveden úplný popis zdrojového kódu. Budete se jím muset prokousat sami, pokud ho chcete opravdu pochopit. Já zde popíšu pouze základy – co kód dělá, co by měl dělat, co zatím nedělá atd…

## Co to má dělat?

A teď k tématu, co kód dělá… co jsem od začátku chtěl, aby dělal. V předchozí kapitole jsme zapojili teplotní senzory, senzor plynu, ventilátor a ovládací panel. Všechny tyto komponenty dohromady zvládnou poměrně dost věcí.

Hlavním rozhraním mezi těmito komponentami a námi, lidmi, je ovládací panel. Má displej s rozlišením 16x2 znaků, červenou LED diodu, reproduktor a rotační enkodér s tlačítkem.

Enkodér a tlačítko jsou pak pro nás skutečně jedinou možností, jak v systému provádět nějaké vstupy. Zvolil jsem stejnou logiku, jakou používá ovládací panel tiskáren Prusa (ale nejen jejich). Otáčením enkodéru budeme procházet menu (možnosti) a měnit hodnoty. Tlačítko bude sloužit k přijetí změn a vstupu do menu.

Ovládací panel pracuje ve **dvou režimech**. První, nazvěme ho **informačním režimem**, zobrazuje systémové informace, jako je vnitřní teplota, venkovní teplota, údaj z plynového senzoru, otáčky ventilátoru a hodnoty některých systémových nastavení. To je pro displej 16x2 spousta informací, takže se zobrazují ve „stránkách“, které se „otáčejí“ buď ručně otáčením enkodéru, nebo automaticky po uplynutí definovaného času.

Po stisknutí tlačítka v tomto informačním režimu se ovládací panel (displej) přepne do **režimu „nastavení“**. V tomto režimu můžeme pomocí enkodéru procházet různá systémová nastavení. Opět v podobě „stránek“, kde každá zobrazuje danou možnost s aktuální hodnotou. Pokud po určitou dobu (například 5 sekund) neprovedete žádný vstup enkodérem ani tlačítkem, systém se přepne zpět do informačního režimu.

Chcete-li v režimu nastavení změnit hodnotu jakékoli možnosti, stiskněte znovu tlačítko a tím vstoupíte do **režimu „změny hodnoty“**. Hodnotu možnosti pak můžeme změnit otáčením enkodéru. Pro uložení hodnoty stiskneme tlačítko enkodéru. Změna se uloží a systém se vrátí do režimu „nastavení“, kde můžeme procházet další možnosti nastavení. Systém se také vrátí z režimu „změny hodnoty“ do režimu „nastavení“, pokud po určitou dobu neprovedete žádný vstup pomocí enkodéru.

## "Nastavení"

### Importy, konstanty, další třídy - základy

Pojďme se podívat na některé části hlavního souboru - _Mouseviator\_MK4S\_Control\_System.ino_:

```
#include <Arduino.h>
#include <LiquidCrystal_I2C.h>
#include <RotaryEncoder.h>
#include <Wire.h>
#include <avr/wdt.h>  // For AVR-based boards
#include "common.h"
#include "DHTAsyncSensors.h"
#include "FireWatch.h"
#include "LCD1602Helper.h"
#include "VentilationControl.h"
#include "MQ2GasSensorNonBlocking.h"
#include "SettingsManager.h"
#include "InfoDisplay.h"
#include "UnoPWM.h"
#include "Utils.h"
//#include <util/atomic.h>

// C++ code
//
//I2C initialization
LiquidCrystal_I2C lcd(0x27, 16, 2);
LCD1602Helper lcdHelper(&lcd); //lcd helper has some nice methods to use when doing output to I2C LCD
UnoPWM pwmController(25000); // 25kHz PWM frequency

#define DHT_SENSOR_TYPE_INSIDE DHT_TYPE_22
#define DHT_SENSOR_TYPE_OUTSIDE DHT_TYPE_11

//Definition of PINS
// Rotary Encoder Pins
constexpr byte gasSensAPIN = (byte) A0;        //pin for MQ2 analog output
constexpr byte encoderS1PIN = (byte) A1;
constexpr byte encoderS2PIN = (byte) A2;
constexpr byte fanRPMPIN = (byte) 2;       //pin for reading FAN RPM
constexpr byte gasSensDPIN = (byte) 3;         //pin for MQQ2 digital ouput
constexpr byte insideTempSensPIN = (byte) 4;     //pin for inside DHT22 sensor
constexpr byte ledPWMPIN = (byte) 5; //pin to drive MOSFET by PWM which controls LED stripe
constexpr byte encoderButtonPIN = (byte) 6;      // pin of encoder button
constexpr byte outsideTempSensPIN = (byte) 7;    //pin for outside DHT11 sensor
constexpr byte buzzerPIN = (byte) 8;           //pin for buzzer
constexpr byte fanPWMControlPIN = UnoPWM::PWM_PIN1;    // 25Khz PWM for FAN
constexpr byte statusLEDPIN = (byte) 10;		//pin for status led
constexpr byte fanDoorServoPIN = (byte)11; //pin for fan door servo, pin 11 is on Timer 2
constexpr byte fanPowerRelayPIN = (byte) 12;
constexpr byte powerRelayPIN = (byte) 13;    //relay to switch ON/OFF printer power

// Rotary Encoder
RotaryEncoder encoder(encoderS1PIN, encoderS2PIN, RotaryEncoder::LatchMode::TWO03);
// SettingsManager Instance
SettingsManager settingsManager(&encoder, &lcdHelper, encoderButtonPIN);

//MQ2 Gas Sensor
MQ2GasSensorNonBlocking gasSensor(gasSensAPIN, gasSensDPIN, &lcdHelper);

//DHT temp/humidity sensors
DHTAsyncSensors dhtSensors(insideTempSensPIN, DHT_SENSOR_TYPE_INSIDE, outsideTempSensPIN, DHT_SENSOR_TYPE_OUTSIDE, DEFAULT_MEASUREMENT_INTERVAL,
		DEFAULT_MEASUREMENT_INTERVAL);

//Create FireWatch object
FireWatch fireWatch(&dhtSensors, &settingsManager, &gasSensor, &lcdHelper, powerRelayPIN, buzzerPIN, statusLEDPIN);

//Create ventilation control object
VentilationControl ventControl(fanDoorServoPIN, fanPWMControlPIN, fanPowerRelayPIN, fanRPMPIN, &pwmController, &fireWatch);

InfoDisplay infoDisplay(&ventControl, &fireWatch, &lcdHelper, &encoder, DEFAULT_INFO_PAGE_DISPLAY_TIME);

//whether wdt is enabled or not
bool wdt_enabled = false;
```

Kromě standardní deklarace **_LiquidCrystal\_I2C_** jsou zde definice konstant, většinou pro definování toho, co je připojeno ke kterému PINu na Arduinu. Poté zde vytváříme instance pomocných/funkčních tříd, které byly napsány pro tento a nejen pro tento projekt :) Většinu z nich se také pokusím stručně popsat v následujících částech. Všimněte si, že tyto třídy jsou inicializovány odkazy na různá definovaná čísla PINů Arduina a další třídy, takže je to celé dost propojené. Mnoho konstant a maker je také definováno v souboru "_[common.h](#commonh)_" (používají je, myslím, téměř všechny třídy, tak se na něj také podívejte.

Když mluvíme o definici PINů...

### Přiřazení pinů Arduina UNO

Většina přiřazení PINů nebyla vybrána náhodně, ale proto, že musely být použity, nebo proto, že nezbývaly žádné jiné, které by se daly použít… Podívejme se.:

| Pin | Arduino Název | Funkce | Poznámka |
| --- | --- | --- | --- |
| A0 | Analog 0 | MQ2 detektor plynů – analogový výstup | Analogový vstup |
| A1 | Analog 1 | Rotační enkodér – kanál A | Analogový vstup |
| A2 | Analog 2 | Rotační enkodér – kanál B | Analogový vstup |
| 2 | D2 | Otáčky RPM ventilátoru (tachometer) | Externí přerušení |
| 3 | D3 | MQ2 detektor plynů – digitální výstup | Externí přerušení |
| 4 | D4 | Vnitřní čidlo teploty (DHT22) | Jedno-drátová data |
| 5 | D5 | LED pásek MOSFET PWM | Timer0 PWM |
| 6 | D6 | Tlačítko na rotačním enkodéru | Digitální vstup |
| 7 | D7 | Vnější čidlo teploty (DHT11) | Jedno-drátová data |
| 8 | D8 | Buzzer | Digitální výstup |
| 9 | D9 | PC ventilátor PWM (25 kHz) | **Timer1 – vlastní PWM** |
| 10 | D10 | Status LED | Digitální výstup |
| 11 | D11 | Servo dvířek ventilátoru | **Timer2 – WiderServoTimer2** |
| 12 | D12 | Relé ventilátoru | Digitální výstup |
| 13 | D13 | Relé pro napájení tiskárny | Digitální výstup + LED na desce |

**ATmega328P** (Arduino UNO) má **tři hardwarové časovače**, každý se specifickými funkcemi a mapováním PINů.:

| Timer | Typ | Použití v Arduino | PWM Piny |
| --- | --- | --- | --- |
| Timer0 | 8-bit | `millis()`, `delay()` | D5, D6 |
| Timer1 | 16-bit | Knihovna Servo (výchozí) | D9, D10 |
| Timer2 | 8-bit | Ve výchozím stavu volné | D3, D11 |

#### Proč byly časovače upraveny?

**Timer1 – Řízení ventilátoru počítače (25 kHz PWM).** **Timer1** používáme ke generování 25 kHz PWM signálu pro počítačový ventilátor podle postupu popsaného [zde](https://fdossena.com/?p=ArduinoFanControl/i.md). A to protože:

- Standardnífunkce _analogWrite()_ (~490 Hz) používaná pro zápis PWM signálu není kompatibilní s PC ventilátory

- Ventilátory pro PC vyžadují 25 kHz PWM signál, otevřený kolektor

- Časovač Timer1 (16bitový) je jediný časovač, který je pro to dostatečně přesný

V důsledku toho **nelze použít standardní knihovnu Servo** (použili bychom ji k ovládání serva, které ovládá dvířka ventilátoru), protože se **také spoléhá na Timer1**.

Proto, použijeme slabě upravenou verzi knihovny [ServoTimer2](https://github.com/nabontra/ServoTimer2). Naše knihovna se bude jmenovat [WiderServoTimer2](#WiderServoTimer2). Jelikož **Timer1 není k dispozici**, musí být řízení serva přesunuto jinam a jediný zbývající časovač v Arduinu UNO je Timer2. Použijeme tedy ten, protože::

- je nezávislý na Timer1

- Může generovat přesné impulsy pro serva pomocí přerušení

- Můžeme tak kontrolovat servo přes **pin 11**

Rád bych však poznamenal, že PWM Timer2 (hardwarový) na pinech D3 a D11 je vypnut. To neznamená, že tyto piny nemůžeme použít pro PWM výstup. Znamená to pouze, že k tomu nemůžeme použít funkci **analogWrite()**, protože je těmito změnami vypnutá. Používáme softwarově generované impulsy (které používají **digitalWrite()**).

Vím, že se to může zdát matoucí a není v rozsahu tohoto článku se do toho pouštět hlouběji, protože se jedná o dost nízkoúrovňovou záležitost. Takže to berte tak, jak to je, nebo se budete muset sami ponořit [do dokumentace k Arduinu](https://docs.arduino.cc). Ukázalo se ale, že to byla jediná cesta, jak v tomto projektu použít Arduino UNO.

**Timer0** zůstvává **nezměněn**, takže:

- `millis()`

- `delay()`

- logika založená na čase

- odstraňování odskoků tlačítek

- časování enkodéru

... to vše nadále funguje normálně, což je **velmi důležité!**

#### Asymetrický odečet hodnot ze senzorů teploty/vlhkosti DHT a plynového senzoru MQ2

Klasická knihovna DHT pro Arduino (např. _DHT.h_) provádí **blokující čtení senzorů**, což znamená, že blokuje CPU a deaktivuje přerušení, zatímco čeká na dokončení handshake a přenosu dat z DHT senzoru. V projektu, jako je tento – s **vysokofrekvenčním PWM (25 kHz) pro řízení PC ventilátoru, časováním servopohonu řízeným časovačem Timer2, více zdroji přerušení (čtení otáček ventilátoru, digitální čtení plynového senzoru)** a neblokujícími řídicími smyčkami – mohou takové operace způsobit konflikty časování, zmeškané pulzy, jitter nebo zablokování. Proto používáme naši pomocnou třídu [DHTAsyncSensors](#DHTAsyncSensors), která používá knihovnu [DHT-Async](https://github.com/KushlaVR/DHT-Async). Tato knihovna je navržena pro čtení senzorů DHT neblokujícím způsobem, využívajícím stavový automat, což umožňuje našemu programu pokračovat v provádění dalších časově kritických úloh (jako jsou aktualizace PWM a zpracování přerušení), zatímco probíhá komunikace DHT. To vede ke spolehlivějším údajům o teplotě a vlhkosti v prostředí s vysokým počtem přerušení, kde je nutné zachovat přesné načasování.

Ze stejného důvodu je zde i knihovna nazvaná [MQ2GasSensorNonBlocking](#MQ2GasSensorNonBlocking), která čte hodnoty z plynového senzoru, také asynchronní cestou, jenž způsobuje minimální zpoždění hlavní smyčky.

### Funkce setup()

A níže je uvedena jedna ze dvou základních funkcí každého Arduino projektu , funkce **setup()**:

```
/**
 * Init function
 */
void setup() {
	//debug init, will init Serial.begin if DEBUG is defined
	DEBUG_INIT(9600);

	//init display
	lcd.init();
	lcd.backlight();
	lcd.display();

	//first init settings manager so stored variables are loaded
	settingsManager.init();

	// --- Add here ---
	delay(2000);  // Allow DHT11/DHT22 to stabilize before any reads / anything happens
	// ---------------

    // Strong pull-ups to stabilize signal lines
    pinMode(insideTempSensPIN, INPUT_PULLUP);
    pinMode(outsideTempSensPIN, INPUT_PULLUP);
    digitalWrite(outsideTempSensPIN, HIGH);

    pinMode(ledPWMPIN, OUTPUT);

	//this will configure pins for gas sensor and attach interrupt to pin where gas sensor digital pin is connected
	//will trigger the function when input changes from 1 to 0
	gasSensor.attach();
	gasSensor.onDigitalInputTriggered(onGasDetected);		//set callback
	gasSensor.setSensorWarmupTime(settingsManager.getGasSensWarmupTime());

	//initialize ventilation control
	ventControl.attachServo();
	ventControl.setMaxInsideTemp((float)settingsManager.getMaxInsideTemp());
	ventControl.setMaxTempDifference((float)settingsManager.getMaxTempDifference());

	//setup PWM controller timer, it will setup pin 9 and 10 for 25kHz output
	pwmController.setupTimer();
    //DEBUG_PRINTLN_F(F("tcn1T: %d"), pwmController.getTcnt1Top());
  	//DEBUG_PRINTLN_F(F("tcn2T: %d"), pwmController.getTcnt2Top());
	//configure fan speed reading
	ventControl.attachFanInterrupt();

	//turn LEDs off
	analogWrite(ledPWMPIN, 0);

	//attach fire watch
	fireWatch.attach();

	infoDisplay.init();
	//reset info page display time to stored value
	infoDisplay.setInfoPageDisplayTime((unsigned long)settingsManager.getInfoDisplaySpeed() * MULTIPLIER_1000);

	// Other Settings Manager init
	settingsManager.onVariableChanged(onVariableChanged);

	settingsManager.handleEncoderInput(); // Initial input check to load settings

	//enable power to power relay
	fireWatch.checkStoredFireAlarm();

}
```

### Protokolování a ladění

Úplně první příkaz inicializuje ladění a logování. Makro **DEBUG\_INIT** inicializuje sériový výstup do konzole, který se používá k výstupu ladících zpráv. Logování se provádí pomocí několika dalších maker, která snadno poznáte, protože všechna začínají **DEBUG\_PRINT** a pak něčím dalším. Všechny jsou definovány v souboru ["common.h"](#commonh) , kde lze logování také globálně zapnout/vypnout na tomto řádku:

```
#define MK4SC_DEBUG  // Comment this line to disable debugging
```

Všimněte si, že volání maker pro ladění je v celém kódu většinou zakomentováno, protože celý „firmware“ naráží na paměťové limity Arduina UNO a prostě není dostatek paměti na to, abych je všechny nechal zapnuté. Takže v závěrečných fázích vývoje jsem musel zapnout pouze ta, která jsem potřeboval pro ladění :)

Sketch uses 31934 bytes (99%) of program storage space. Maximum is 32256 bytes.  
Global variables use 1576 bytes (76%) of dynamic memory, leaving 472 bytes for local variables. Maximum is 2048 bytes.

Výše uvedený text je výstupem Arduino IDE po kompilaci celého projektu - skutečně využívá téměř veškerou dostupnou programovou paměť na Arduino UNO.

### Zbývající inicializace

Po inicializaci ladění inicializujeme LCD displej a poté třídu [Settings Manager](#SettingsManager). SettingsManager načte uložená nastavení z EEPROM Arduina, která chceme mít co nejdříve k dispozici, abychom měli hodnoty dostupné pro další třídy.

Pak je zde nastavení módu PINů pro senzory vnitřní a venkovní teploty a zápis počáteční hodnoty do venkovního teplotního čidla. To by sice nemělo být nutné, protože PINy se inicializují vytvořením třídy [DHTAsyncSensors](#DHTAsyncSensors), ale existuje zde taková zvláštní chyba, že venkovní čidlo DHT11 nevrací správná data, dokud není alespoň jednou stisknuto tlačítko rotačního enkodéru. Tímto jsem se snažil s tím pomoct, ale nic se nezměnilo. Ale ani to neškodí, takže jsem to zatím neodstraňoval.

Dále inicializujeme PIN pro PWM výstup LED pásku, inicializujeme senzor plynu, ovládání ventilace a obnovíme některé možnosti z nastavení. Vypneme LED pásek, inicializujeme třídu [Fire watch](#FireWatch) a [informační displej](#InfoDisplay). Připojíme funkci, jenž je volána při každé změně nastavení ve správci nastavení a zkontrolujeme uložený požární poplach. Protože pokud bylo poslední vypnutí způsobeno požárním poplachem, napájecí relé, které napájí tiskárnu, se nespustí, dokud nebude požární poplach ručně vymazán pomocí ovládacího panelu.

V inicializačním kódu jsou uvedeny dvě funkce, které jsou definovány v hlavním _.ino_ souboru. Obě funkce jsou „callback události“.

```
gasSensor.onDigitalInputTriggered(onGasDetected);
```

Funkce [onGasDetected](#onGasDetected) je volána pokud detektor plynů MQ2 detektuje hraniční hodnotu, což signalizuje na digitálním výstupu.

```
// Other Settings Manager init	settingsManager.onVariableChanged(onVariableChanged);
```

A funcke [onVariableChanged](#onVariableChanged) je volána při každé změně nějaké možnosti nastavení přes kontrolní panel a informuje o tom, co se změnilo a jaká je nová hodnota.

### Funkce - onGasDetected()

Digitální výstup plynového senzoru MQ2 by měl spustit událost pouze tehdy, když je detekována přednastavená prahová hodnota koncentrace plynu (fyzicky nastavená potenciometrem na desce). V takovém případě jednoduše předáme tuto informaci třídě [FireWatch](#FireWatch) čímž ji požádáme o spuštění požárního poplachu a následných akcí.

```
/**
 * This function will be called when gas sensor digital output is triggered
 *
 * Have to be careful here as this is code that can run in different thread than the main loop
 */
void onGasDetected() {
	//just set the variable here and deal with it in the functions that are called from within the loop so that we're safe
	//from all the hassle that thus function will be called from function that is being called from interrupt, thus from "interrupt" and
	//thus may be processed along the main code with unexpected behavior if we call other stuff from there
	//bFireHazardDetected is bool and thus writing value to it is atomic
	fireWatch.setAlertMode(FireAlertMode::FIRE_HAZARD, false);
}
```

### Funkce - onVariableChanged()

Tato funkce se používá, když uživatel změní hodnotu nějakého nastavení pomocí ovládacího panelu, k informování a spuštění příslušných akcí v jiných třídách a/nebo kódu.

```
/**
 * This function will be called when some variable is changed using SettingsManager
 */
void onVariableChanged(byte variable, byte newValue) {
	switch (variable) {
	case SettingsManager::VAR_LED_ON:
		//just react to OFF, the other changes will be handled by controlLighting function called from loop
		if (newValue == LIGHT_LED_OFF) {
			//turn LED light off
			analogWrite(ledPWMPIN, LOW);
		}
		break;
	case SettingsManager::VAR_FIRE_ALERT_MODE:
		//If On, it should not be caused by sensors, but by user input
		//switch the value of bFireHazardDetected
		//and let the doFireProtectionCheck() handle the rest
		fireWatch.setAlertMode(static_cast<FireAlertMode>(newValue), (newValue == FireAlertMode::NORMAL) ? false : true);

		//if OFF then we can turn on the power relay
		fireWatch.switchPowerRelayOn();
		break;
	case SettingsManager::VAR_PWR_RELAY_OVERRIDE:
		if (newValue == PWR_RELAY_OVERRIDE_ON) {
			//make sure power is forced on
			fireWatch.switchPowerRelayOn();
		}
		break;
	case SettingsManager::VAR_MAX_TEMP_INSIDE:
		ventControl.setMaxInsideTemp((float)newValue);
		break;
	case SettingsManager::VAR_MAX_TEMP_DIFFERENCE:
		ventControl.setMaxTempDifference((float)newValue);
		break;
	case SettingsManager::VAR_INFO_DISPLAY_SPEED:
		infoDisplay.setInfoPageDisplayTime((unsigned long)newValue * MULTIPLIER_1000);
		break;
	default:
		break;
	}
}
```

Kód zpracovává pouze některé změny proměnných. Mnoho tříd obsahuje odkazy na třídu [SettingsManager](#SettingsManager), a proto dokáží samy detekovat změnu proměnné. Některé změny proměnných jsou však zpracovávány prostřednictvím této funkce. Například změna režimu požárního poplachu. Uživatel může změnit režim požárního poplachu prostřednictvím ovládacího panelu – a otestovat tak systém. V takovém případě musíme třídě [FireWatch](#FireWatch) sdělit, aby se přepnulo relé ovládající napájení tiskárny. Kód také vynutí sepnutí tohoto relé, pokud je aktivní „override“, a informuje třídu [VentilationControl](#VentilationControl), že se změnila hodnota maximální vnitřní teploty nebo maximálního teplotního rozdílu. Také dáva vědět třídě [InfoDisplay](https://mouseviator.com/wp-admin/post.php?post=6652&action=edit#InfoDisplay), když se změní hodnota času pro zobrazení stránky.

## "Hlavní smyčka"

Níže je uveden kód druhé klíčové funkce každého Arduino projektu, funkce, která provádí hlavní smyčku:

```
/**
 * Main loop
 */
void loop() {
	//check WATCH DOG TMER
	check_wdt();

	//calibrate gas sensor
	byte gasSensState = gasSensor.calibrateGasSensor();
	//if calibrating right now, skip everything else
	if (gasSensState == MQ2GasSensorNonBlocking::SENSOR_STATE_CALIBRATING) {
		return;
	}

	// Continuously check encoder input
	//encoder.tick(); // Update encoder state, encoder is used by settings manager and info display
	settingsManager.handleEncoderInput();

	//read sensors
	readSensorValues();

	//do ventilation control
	ventControl.controlVentilation();

	//do fire watch protection
	fireWatch.doFireWatch();

	controlLighting();

	//make sure setup has priority so other code does not kick us out of setup
	if (settingsManager.isSetupOff()) {

		if (!fireWatch.getAlertMode() != FireAlertMode::FIRE_HAZARD) {
			//lastly, display info on LCD
			infoDisplay.displayInfoPage();
		}
	}
}
```

Ani se to nezdá tak složité, že? Není, dokud se nezačnete rýpat do volaných funkcí… a odtud do dalších funkcí a souborů a tříd… čím půjdete hlouběji do sklepa, tím více to může být trochu matoucí.

Ale nebojte se, dostanete se k jádru věci, jeden kousek kódu po druhém.

První řádek volá funkci [check\_wdt()](#check_wdt). Tato funkce je definována v hlavním _.ino_ souboru .

V následujícím kódu:

```
//calibrate gas sensor
	byte gasSensState = gasSensor.calibrateGasSensor();
	//if calibrating right now, skip everything else
	if (gasSensState == MQ2GasSensorNonBlocking::SENSOR_STATE_CALIBRATING) {
		return;
	}
```

voláme funkci pro kalibraci plynového senzoru. Pokud se senzor kalibruje, zbytek kódu v hlavní smyčce přeskočíme, dokud nebude senzor zkalibrován.

Dále zavoláme [správce nastavení](#SettingsManager), který zkontroluje, zda uživatel provedl nějaký vstup pomocí otočného enkodéru nebo tlačítka, a tedy zda chce uživatel něco změnit. To je následující kód:

```
// Continuously check encoder input
	//encoder.tick(); // Update encoder state, encoder is used by settings manager and info display
	settingsManager.handleEncoderInput();
```

náslédně čteme hodnoty senzorů:

```
//read sensors
	readSensorValues();
```

Funkce [readSensorValues()](#readSensorValues) je také definována v hlavním _.ino_ souboru.

Pak je v hlavní smyčce kód, který volá [logiku pro ovládání ventilace](#VentilationControl), [logiku detekce požáru](#FireWatch) a funkci pro ovládání [osvětlení LED páskem](#controlLighting). To jsou následující tři řádky:

```
//do ventilation control
ventControl.controlVentilation();

//do fire watch protection
fireWatch.doFireWatch();

controlLighting();
```

Poslední část kódu v hlavní smyčce je zodpovědná za zobrazení informací na 16x2 LCD displaji ovládacího panelu. To se provádí pomocí pomocné třídy [Info Display](#InfoDisplay), ale pouze pokud ovládací panel není v režimu „nastavení“:

```
//make sure setup has priority so other code does not kick us out of setup
	if (settingsManager.isSetupOff()) {

		if (!fireWatch.getAlertMode() != FireAlertMode::FIRE_HAZARD) {
			//lastly, display info on LCD
			infoDisplay.displayInfoPage();
		}
	}
```

### Funkce - **check\_wdt()**

[Watchdog časovač](https://docs.arduino.cc/libraries/watchdog/) v Arduinu je funkce, která pomáhá zabránit zaseknutí systému automatickým resetováním desky, pokud přestane reagovat. Funguje tak, že vyžaduje pravidelné resety ze strany softwaru. Pokud k resetu nedojde v zadaném čase, watchdog resetuje Arduino. Naše funkce **check\_wdt()**:

```
/**
 * This functions checks if WDT should be enabled and enables it if it is not, disabled if should be disabled and resets each time called when enabled
 * It only has effect after gas sensor is calibrated
 */
void check_wdt() {
	if (gasSensor.isSensorCalibrated()) {
		byte enable_wdt = settingsManager.getEnableWDT();
		//if WDT should be enabled, enable it
		if (enable_wdt == WDT_ON && !wdt_enabled) {
			wdt_enable(WDTO_2S);
			wdt_enabled = true;
		} else if (enable_wdt == WDT_OFF && wdt_enabled) {
			//o if should be disabled?
			wdt_disable();
			wdt_enabled = false;
		}

		//if enabled, reset..
		if (wdt_enabled) {
			wdt_reset();
		}
	}
}
```

dělá přesně to s několika dalšími kontrolami. Zaprvé, funguje to pouze po kalibraci senzoru plynu, protože kalibrace by mohla snadno způsobit, že si hlídací časovač bude myslet, že systém zamrzl, i když tomu tak není. Tuto funkci je také nutné povolit v nastavení přes ovládací panel.

### Funkce - readSensorValues()

```
/**
 * Reads values of all sensors
 *
 */
void readSensorValues() {
	//read values from inside temperature/humidity sensor
	dhtSensors.measureInsideEnvironment();

	//read values from outside temperature/humidity sensor
	dhtSensors.measureOutsideEnvironment();

	//measure gas sensor resistance
	if (gasSensor.isSensorCalibrated()) {
		//measure the resistance
		gasSensor.measureSensorResistance();
	}
}
```

Není nijak zvlášť složitá. Jen volá příslušné třídy, které odvedou samotnou práci.

### Funkce - controlLighting()

Funkce, která ovládá osvětlení LED pásku, je jednoduchá. Prostě zapíše požadovaný PWM výstup na příslušný výstupní PIN. Přidanou funkcí je, že čeká po definovanou dobu - LED\_ON\_DELAY, než se provede první pokus o zápis výstupu. To je z důvodu omezení napěťových špiček. Pokud by k tomu došlo ihned po spuštění desky, nemusí být elektrony v našich obvodech ještě zcela stabilizovány.

```
void controlLighting() {
	//if LED should be ON and we are running sufficiently long
	if (settingsManager.getLEDOn() == LIGHT_LED_ON && (millis() >= LED_ON_DELAY)) {
		//write LED PWM output, it comes in percent from settings manager,	need to re-map it to 255
		byte ledPWMValue = settingsManager.getLEDPWM();

		analogWrite(ledPWMPIN, map(ledPWMValue, 0, 100, 0, 255));
	} else {
		//light should be off
		analogWrite(ledPWMPIN, LOW);
	}
}
```

Všimněte si, že PWM pro LED pásky používá PIN 5, který používá Timer0, a to (s PINem 6) je jediný neupravený PWM výstup v tomto projektu. Více se dozvíte v části: [proč byly časovače upraveny](#why_modified_timers).

## Settings Manager

- Header file: _SettingsManager.h_

- Cpp file: _SettingsManager_.cpp

Třída _SettingManager_ zajišťuje zobrazení a změnu hodnot proměnných nastavení. Také načítá hodnoty těchto nastavení uložené v EEPROM a ukládá je tam a obsahuje funkce pro čtení hodnot pro použití jinými třídami/kódem.

Aktuálně lze změnit 17 proměnných "nastavení". Všechny jsou typu "byte" (protože je to jediný typ, který EEPROM API podporuje) a jsou uloženy v bajtovém poli, ke kterému se přistupuje pomocí indexu. Celkem třída pracuje s 18 proměnnými (jedna je pouze pro čtení).

| **Název indexu:** | VAR\_TEMP\_CONTROL\_MODE |
| --- | --- |
| **Hodnota indexu:** | 0 |
| **Možné hodnoty**: | 0 = AUTO\_MODE   1 = MANUAL\_MODE |
| **Popis**: | _Režim kontroly teploty, automatický nebo manuální_ |

| **Název indexu**: | VAR\_MAX\_TEMP\_INSIDE |
| --- | --- |
| **Hodnota indexu**: | 1 |
| **Možné hodnoty**: | 5 - min(variables\[_VAR\_WARNING\_TEMP_\] - 1, INSIDE\_SENSOR\_MAX\_TEMP) |
| **Popis**: | _Maximální vnitřní teplota. Aby to mělo nějaký účinek, musí být režim větrání nastaven na AUTO. Jakmile vnitřní teplota stoupne nad tuto teplotu, ventilátor se začne otáčet a dvířka se otevřou. Pokud teplota klesne pod tuto hodnotu, ventilátor by se měl zastavit a dveře by se měly zcela zavřít._ |

| **Název indexu**: | VAR\_MAX\_TEMP\_DIFFERENCE |
| --- | --- |
| **Hodnota indexu**: | 2 |
| **Možné hodnoty**: | 1 - max(INSIDE\_SENSOR\_MAX\_TEMP - variables\[_VAR\_MAX\_TEMP\_INSIDE_\] - 1, 1) |
| **Popis**: | Maximální rozdíl mezi vnitřní a venkovní teplotou. Má vliv pouze v AUTOMATICKÉM režimu větrání. Definuje teplotní rozsah, který bude pohánět ventilátor a dveře. Ventilátor se při malém teplotním rozdílu spustí na volnoběh a dveře na 30 stupňů, ale při maximálním rozdílu by měl dosáhnout maximálních otáček a dveře by se měly otevřít na 90 stupňů. |

| **Název indexu**: | VAR\_WARNING\_TEMP |
| --- | --- |
| **Hodnota indexu**: | 3 |
| **Možné hodnoty**: | (_VAR\_MAX\_TEMP\_INSIDE_ + 1) -min(variables\[_VAR\_SHUTDOWN\_TEMP_\] - 1, INSIDE\_SENSOR\_MAX\_TEMP) |
| **Popis**: | _Teplota, při které se spustí požární varování. Minimální i maximální hodnota se liší v závislosti na dalších nastaveních teploty._ |

| **Název indexu**: | VAR\_SHUTDOWN\_TEMP |
| --- | --- |
| **Hodnota indexu**: | 4 |
| **Možné hodnoty**: | (VAR\_WARNING\_TEMP + 1) - 80 |
| **Popis**: | _Teplota, při které se spustí požární poplach, což by mělo vést k vypnutí napájecího relé a vypnutí tiskárny (pokud není aktivní override). Minimální hodnota se liší, maximální hodnota je 80._ |

| **Název indexu**: | VAR\_ALERT\_PPM |
| --- | --- |
| **Hodnota indexu**: | 5 |
| **Možné hodnoty**: | 30-255 |
| **Popis**: | _Hodnota PPM naměřená plynovým senzorem pro spuštění požárního varování. Hodnota se vynásobí 100._ |

| **Název indexu**: | VAR\_SHUTDOWN\_PPM |
| --- | --- |
| **Hodnota indexu**: | 6 |
| **Možné hodnoty** | VAR\_ALERT\_PPM - 255 |
| **Popis**: | _Hodnota PPM naměřená plynovým senzorem pro spuštění požárního poplachu (práh vypnutí tiskárny). Hodnota se vynásobí 100._ |

| **Název indexu**: | VAR\_FAN\_DOOR\_ANGLE |
| --- | --- |
| **Hodnota indexu:** | 7 |
| **Možné hodnoty**: | 0 = OFF   1 - 90   255 = FAN\_DOOR\_ANGLE\_AUTO |
| **Popis**: | _Úhel dveří větracího ventilátoru. Hodnota 255 je pro automatické ovládání._ |

| **Název indexu**: | VAR\_FAN\_DOOR\_SPEED |
| --- | --- |
| **Hodnota indexu**: | 8 |
| **Možné hodnoty**: | 0-255 |
| **Popis**: | _Rychlost, s jakou by se měl měnit úhel dveří ventilátoru. Hodnota se vynásobí 10 a udává, za kolik milisekund by se měl úhel změnit o jeden stupeň._ |

| **Název indexu**: | VAR\_FAN\_PWM |
| --- | --- |
| **Hodnota indexu**: | 9 |
| **Možné hodnoty**: | 0 = OFF   20 - 100 |
| **Popis**: | _PWM výstup pro ventilátor. Je omezen na minimálně 20 %, protože pod touto hodnotou by napětí bylo příliš nízké na to, aby se ventilátor roztočil._ |

| **Název indexu**: | VAR\_LED\_ON |
| --- | --- |
| **Hodnota indexu**: | 10 |
| **Možné hodnoty**: | 0 = LIGHT\_LED\_OFF   1 = LIGHT\_LED\_ON |
| **Popis**: | _Zda je LED pásek ZAPNUTÝ nebo VYPNUTÝ._ |

| **Název indexu**: | VAR\_LED\_PWM |
| --- | --- |
| **Hodnota indexu**: | 11 |
| **Possible value:** | 0 - 100 |
| **Popis**: | _PWM výstup aplikovaný na LED pásek. Od 0 do 100 %._ |

| **Název indexu**: | VAR\_FIRE\_ALERT\_MODE |
| --- | --- |
| **Hodnota indexu**: | 12 |
| **Možné hodnoty**: | 0 = FireAlertMode::NORMAL   1 = FireAlertMode::FIRE\_WARNING   2 = FireAlertMode::FIRE\_HAZARD |
| **Popis**: | _Režim požárního poplachu._ |

| **Název indexu**: | VAR\_GASSENS\_WARMUP\_TIME |
| --- | --- |
| **Hodnota indexu**: | 13 |
| **Možné hodnoty**: | 2 - 255 |
| **Popis**: | _Doba zahřívání plynového senzoru. Hodnota se vynásobí 10 a je udávána v sekundách._ |

| **Index name:** | VAR\_PWR\_RELAY\_OVERRIDE |
| --- | --- |
| **Hodnota indexu**: | 14 |
| **Možné hodnoty** | 0 = PWR\_RELAY\_OVERRIDE\_OFF   1 = PWR\_RELAY\_OVERRIDE\_ON |
| **Popis**: | _Zda je "vyjímka" relé ovládajícího napájení tiskárny zapnuta nebo vypnuta. Pokud je zapnuto, výkonové relé se nepřepne a nevypne, a to ani v případě požárního poplachu. Tato možnost existuje hlavně pro účely ladění a testování, kdy jsem chtěl zabránit vypnutí tiskárny uprostřed tisku kvůli nějaké falešné chybě nebo falešnému odečtu senzoru. Jinak by mělo být vypnuto._ |

| **Název indexu**: | VAR\_ENABLE\_WDT |
| --- | --- |
| **Hodnota indexu**: | 15 |
| **Možné hodnoty**: | 0 = WDT\_OFF   1 = WDT\_ON |
| **Popis**: | _Zda je funkce [watchdog](#check_wdt) časovače povolena či nikoliv._ |

| **Název indexu**: | VAR\_INFO\_DISPLAY\_SPEED |
| --- | --- |
| **Hodnota indexu**: | 16 |
| **Možné hodnoty**: | 0 - 255 |
| **Popis**: | _Rychlost kterou třída [Info Display](#InfoDisplay) mění informační stránky zobrazované na 16x2 LCD displaji. V sekundách._ |

| **Název indexu**: | VAR\_LAST\_FAN\_DOOR\_POSITION |
| --- | --- |
| **Hodnota indexu**: | 17 |
| **Možné hodnoty:** | same as VAR\_FAN\_DOOR\_ANGLE |
| **Popis:** | _Poslední poloha dvířek ventilátoru. Tato hodnota se ukládá, takže při příštím spuštění Arduina UNO se dvířka obnoví do poslední polohy (takže by se ve skutečnosti neměla pohnout). Tato hodnota je pouze pro čtení, takže ji nelze změnit pomocí ovládacího panelu._ |

Z výše uvedené tabulky si myslím, můžete odvodit, že tato nastavení umožňují značnou variabilitu v provozu krytu tiskárny. Probírat všechny detaily kódu je mimo rámec tohoto článku. Třída většinou nabízí veřejné „gettery“ – pro získání hodnot příslušné "proměnné nastavení". Zobrazování stránek nastavení, manipulaci s rotačním enkodérem, vstup tlačítka i změna hodnot proměnných se provádí v privátních funkcích a metodách:

```
// Helper methods
    void loadFromEEPROM();
    void saveToEEPROM();
    void enterVariableDisplayMode();
    void enterVariableSetupMode();
    void updateVariableValue();
    void exitToSetupOff();
    void displayCurrentVariable();
    void saveVariableToEEPROM(byte index, byte value);
    byte readVariableFromEEPROM(byte index);
    void checkButton();
    
    // Value range checks
    byte constrainValue(byte value, byte min, byte max);
    byte constrainAngleValue(byte value);

    void (*onVariableChangedCallback)(byte variable, byte newValue);  // Function pointer for the callback
```

jako jsou ty uvedené výše, definované v souboru _SettingsManager.h_ a implementované v souboru _SettingsManager.cpp_. Informace týkající se čtení a ukládání proměnných z/do EEPROM naleznete také v [dokumentaci ke knihovně EEPROM](https://docs.arduino.cc/learn/built-in-libraries/eeprom/)..

A tady jsou fotky skutečného displeje:

\[rl\_gallery id="6861"\]

## Fire Watch

- Header file: _FireWatch.h_

- Cpp file: _FireWatch.cpp_

Třída FireWatch je hlídač, který nepřetržitě sleduje vnitřní a venkovní teplotu, údaje plynového senzoru a spustí varování/poplach, když se situace zvrtne. K tomu používá odkazy na pomocnou třídu [teplotních senzorů](#DHTAsyncSensors), pomocnou třídu pro [plynový senzor](#MQ2GasSensorNonBlocking), třídu [Settings Manager](#SettingsManager) a další.

Systém může být v jednom ze tří režimů požárního poplachu:

- **FireAlertMode::NORMAL** – Toto je normální režim. Nedochází k žádným nadměrným teplotám ani koncentracím plynu. Napájecí relé je zapnuté, takže 3D tiskárna je napájena.

- **FireAlertMode::FIRE\_WARNING** - Při přepnutí do tohoto režimu vnitřní teplota překročila varovnou teplotu (viz VAR\_WARNING\_TEMP v [SettingsManager](#SettingsManager)) a/nebo hodnota plynového senzoru překročila varovnou úroveň PPM (viz VAR\_ALERT\_PPM v [SettingsManager](#SettingsManager)). Řízení ventilace ([Ventilation Control](#VentilationControl)) tuto situaci také zaznamená a dá ventilátoru povel k maximálním otáčkám a plně otevře ventilační dvířka. Červená LED dioda bude blikat v intervalu 1 sekundy a bzučák bude přehrávat varovný tón, dokud se situace nezmění. Systém zatím očekává pouze nadměrnou teplotu nebo hromadění výparů uvnitř skříně, nikoli požár.

- **FireAlertMode::FIRE\_HAZARD** - Při spadnutí do tohoto režimu vnitřní teplota stoupne nad teplotu vypnutí (viz VAR\_SHUTDOWN\_TEMP v [SettingsManager](#SettingsManager)) a/nebo hodnota plynového senzoru stoupne nad úroveň PPM pro poplach (viz VAR\_SHUTDOWN\_PPM v [SettingsManager](#SettingsManager)). Řízení ventilace ([Ventilation Control](#VentilationControl)) tuto situaci také zachytí a vydá pokyn k zastavení ventilátoru a zavření dvířek ventilátoru. Protože systém nyní tuto situaci považuje za požár, měly by tyto akce pomoci zpomalit jeho šíření. Napájecí relé se vypne, čímž se vypne i 3D tiskárna (pokud není VAR\_PWR\_RELAY\_OVERRIDE nastaveno na ON). Červená LED dioda bude blikat v intervalu 200 milisekund a bzučák bude přehrávat poplachováý tón požáru.

Funkce, která přepíná mezi těmito třemi režimy požárního poplachu, je:

```
/**
	 * This function checks for fire hazard
	 */
	void doFireWatch();
```

a je volána z [hlavní smyčky](#main_loop). Tato třída má pak na starosti bzučák a červenou LED diodu umístěné v kontrolním panelu. Také ovládá relé napájení tiskárny. Režim požárního poplachu lze také změnit pomocí ovládacícho panelu – pro otestování správné funkčnosti systému.

Podívejte se i na tyto další funkce třídy, které také hrají důležitou roli v logice třídy pro detekci požáru:

```
void triggerFireAlarm();
	void resetFireAlarm();
	void toneFireAlarmSiren();
	void toneWarningSiren();
	void switchPowerRelay(bool bOn, const char *lcdMessage, word waitTime);

	void handleAlert(FireAlertMode prevMode, FireAlertMode currMode);

	void updateStatusLED();
```

a tyto veřejné:

```
/**
	 * Checks whether there was a stored file alarm and switched power relay ON/OFF
	 */
	void checkStoredFireAlarm();
	void switchPowerRelayOn();
	void switchPowerRelayOff();
	void setAlertMode(FireAlertMode currMode, bool bChangedFromSettings);
```

## Ventilation Control

- Header file: _VentilationControl.h_

- Cpp file: _VentilationControl.cpp_

Třída VentilationControl je dalším důležitým hráčem v celé „skládačce“ systému řízení tiskárny. Řídí ventilátor pomocí PWM signálu a také dvířka ventilátoru. Aby to mohla dělat, potřebuje znát hodnoty vnitřní teploty a plynového senzoru, takže is uchovává odkaz na tyto senzory – prostřednictvím třídy [FireWatch](#FireWatch). Což je praktické, jelikož tyto třídy musí spolupracovat v případě překročení prahových hodnot varování před/při detekci požáru.

Hlavní funkcí, jenž je volána z [hlavní smyčky](#main_loop), je funkce **controlVentilation()**:

```
/**
 * This function automatically controls ventilation fan speed and vent fan doors based on difference
 * of outside temeperature and inside temperature, and on how fast the converge
 *
 */
void VentilationControl::controlVentilation() {
      // Calculate the temperature difference and convergence rate
	float insideTemperature = fireWatch->getDHTSensors()->getInsideTemperature();
	//float outsideTemperature = fireWatch->getDHTSensors()->getOutsideTemperature();
    float tempDiff = NAN;
    float convergenceRate = NAN;
    SettingsManager* settingsManager = fireWatch->getSettingsManager();

    //if there is fire alarm, make sure doors are closed and fan is OFF, so there is as less air circulation as possible helping
    //starving the fire. Also, close doors and turn off fan if warning threshold is triggered and gas sensor has some PPM reading, that is possibly
    //fire developing
	if ((fireWatch->getAlertMode() == FireAlertMode::FIRE_HAZARD || settingsManager->getFireAlertMode() == FireAlertMode::FIRE_HAZARD) ||
			(fireWatch->getAlertMode() == FireAlertMode::FIRE_WARNING && fireWatch->getGasSensor()->getSensorValuePPM() > settingsManager->getAlertPPM())) {
		turnFanOff();
		//modify servo position only if differs from the one it is in
		/*if (fanDoorServo.read() != MIN_FAN_DOOR_ANGLE) {
			fanDoorServo.write(MIN_FAN_DOOR_ANGLE);
		}*/
		setFanDoorPosition(MIN_FAN_DOOR_ANGLE);
		return;
	}

	//compute temperature difference between inside and outside
    if (!isnan(insideTemperature) && !isnan(tempMaxInside)) {
    	//this will be negative until inside temperature is greater than threshold
    	tempDiff = insideTemperature - tempMaxInside;
    	convergenceRate = (tempDiff - prevTempDiff) / ((millis() - lastTempDiffUpdateTime) / 1000.0); // °C/s
    	lastTempDiffUpdateTime = millis();
    	prevTempDiff = tempDiff;
    }

    // Determine fan duty based on temperature difference and convergence rate, if control mode is set to AUTO
    byte fanDuty = FAN_OFF_DUTY;
    // Determine door position based on temperature difference and if not overridden by settings manager
    byte doorPosition = 0;

    //First check if enclosure is in fire warning mode. If so and there is no gas sensor reading, there is just high temperature inside, force fan to max. If there is
    //also gas sensor reading, fire is probably developing, then turn off the fan so it does not help the fire by moving the air and creating the column effect.
    //otherwise, the enclosure is in normal state, then decide whether by set for auto or manual control
    if (fireWatch->getAlertMode() == FireAlertMode::FIRE_WARNING) {
		if (fireWatch->getGasSensor()->getSensorValuePPM() < settingsManager->getAlertPPM()) {
			//in case of warning, but not critical PPM,  override the doors for max open
			fanDuty = FAN_MAX_DUTY;		//vent it out
			doorPosition = MAX_FAN_DOOR_ANGLE;		//vent it out
		} else {
			//if there is PPM detection, fire is probably developing, close the doors
			fanDuty = FAN_OFF_DUTY;
			doorPosition = MIN_FAN_DOOR_ANGLE;
		}
	} else {
		//Logic for determining fan duty when enclosure is in normal state
		if (settingsManager->getTempControlMode() == SettingsManager::AUTO_MODE) {
			if (!isnan(insideTemperature) && !isnan(tempMaxInside)) {
				fanDuty = calculateFanDuty(tempDiff, convergenceRate);

				/*#ifdef MK4SC_DEBUG
					// Debug output
					static unsigned long lastDebugTime = 0;

					if (DEBUG_TIME_ELAPSED(2000, lastDebugTime)) {

						char valueStr[10];
						dtostrf(insideTemperature, 0, 2, valueStr);

						DEBUG_PRINT_F(F("Insd temp: %s °C"), valueStr);

						dtostrf(outsideTemperature, 0, 2, valueStr);

						DEBUG_PRINT_F(F(", Outsd temp: %s °C"), valueStr);

						dtostrf(tempDiff, 0, 2, valueStr);

						DEBUG_PRINT_F(F(", Temp diff: %S °C"), valueStr);

						dtostrf(convergenceRate, 0, 2, valueStr);

						DEBUG_PRINT_F(F(", Convergence Rate: %s °C/s"), convergenceRate);
						DEBUG_PRINTLN_F(F(", Fan Duty: %d \%"), fanDuty);

						DEBUG_RESET_TIME(lastDebugTime);
					}

				#endif*/
			} else {
				//mode is auto, but temperature readings are invalid, thus, cannot control fan based on temperatures
				fanDuty = FAN_OFF_DUTY;
			}
		} else {
			//will return direct fan PWM duty from  FAN_IDLE_DUTY - FAN_MAX_DUTY
			fanDuty = settingsManager->getFanPWM();
		}

		//Logic to get fan door angle when enclosure is in normal state
		if (settingsManager->getFanDoorAngle() == SettingsManager::FAN_DOOR_ANGLE_AUTO) {
			if (!isnan(tempDiff)) {
				doorPosition = calculateDoorPosition(tempDiff);
			} else {
				//no temp diff means inside, outside or both temperatures are invalid
				doorPosition = MIN_FAN_DOOR_ANGLE;
			}
		} else {
			doorPosition = settingsManager->getFanDoorAngle();
		}
	}

    // Fan control logic
    if (fanDuty >= FAN_IDLE_DUTY) {
        if (!fanOn) {
            turnFanOn(); // Turn on the fan relay
        }
        //if fan is on, we have PWM controller and at least 1 second passed since switching relay ON, start setting up PWM
        if (fanOn && pwmController && ((millis() - fanRelayPowerOnTime) > 1000)) {

          if (lastFanDuty != fanDuty) {
        	  pwmController->setPwmDuty(fanDuty, fanPWMPIN);
        	  lastFanDuty = fanDuty;
          }

          /*#ifdef MK4SC_DEBUG
				// Debug output
				static unsigned long lastDebugTime = 0;

				if (DEBUG_TIME_ELAPSED(2000, lastDebugTime)) {

					DEBUG_PRINTLN_F(F("Fan Duty: %d \%"), fanDuty);

					DEBUG_RESET_TIME(lastDebugTime);
				}

			#endif*/
        }
    } else {
        turnFanOff(); // Turn off the fan relay
    }

    //update fan door speed in case it changed
    word currDoorSpeed = settingsManager->getFanDoorSpeed();
    if (currDoorSpeed != fanDoorSpeed) {
    	fanDoorSpeed = currDoorSpeed;
    }

    //set new door position. This will change over time, not instantly
    setFanDoorPosition(doorPosition);
}
```

I když se výše uvedený „výpisu kódu“ může zdát dlouhý, je tam spousta komentářů :) Funkce potřebuje určit, jaký by měl být PWM výstup ventilátoru a úhel otevření dvířek ventilátoru, a tyto hodnoty nastavit. K tomu je třeba zkontrolovat několik věcí. Jsme ve stavu varování před požárem nebo v požárním poplachu? Třída [FireWatch](#FireWatch) pouze spustí alarmy a v případě potřeby vypne napájení tiskárny. Tato třída bude spolupracovat v tom, že zastaví ventilátor a zavře dvířka ventilátoru (v případě požárního poplachu). V případě varování před požárem použije plný výkon ventilátoru a plně otevře dvířka ventilátoru. Ano, pokud dojde k požáru, dočasně ho urychlí, ale v případě varování před požárem doufáme pouze v nadměrnou teplotu a/nebo nahromadění výparů a chceme pomoci je snížit.

Pokud jsou podmínky normální, funkce zkontroluje, zda je zapnuto automatické řízení ventilace. A z rozdílu vnitřní a maximální vnitřní teploty vypočítá výstup PWM ventilátoru a polohu dvířek ventilátoru. Čím větší je rozdíl (nad maximální vnitřní teplotou), tím větší je výkon ventilátoru a větší úhel dvířek ventilátoru. Dvě funkce, které provádějí příslušné výpočty, jsou:

```
byte calculateFanDuty(float tempDiff, float convergenceRate);
byte calculateDoorPosition(float tempDiff);
```

Pokud je automatický režim vypnut, použije se pouze výkon ventilátoru a úhel dveří ventilátoru nastavený ovládacím panelem (obojí je ve výchozím nastavení vypnuto).

„Konečné nastavení hodnot“ proběhne na konci funkce na následujících řádcích:

```
// Fan control logic
    if (fanDuty >= FAN_IDLE_DUTY) {
        if (!fanOn) {
            turnFanOn(); // Turn on the fan relay
        }
        //if fan is on, we have PWM controller and at least 1 second passed since switching relay ON, start setting up PWM
        if (fanOn && pwmController && ((millis() - fanRelayPowerOnTime) > 1000)) {

          if (lastFanDuty != fanDuty) {
        	  pwmController->setPwmDuty(fanDuty, fanPWMPIN);
        	  lastFanDuty = fanDuty;
          }

          /*#ifdef MK4SC_DEBUG
				// Debug output
				static unsigned long lastDebugTime = 0;

				if (DEBUG_TIME_ELAPSED(2000, lastDebugTime)) {

					DEBUG_PRINTLN_F(F("Fan Duty: %d \%"), fanDuty);

					DEBUG_RESET_TIME(lastDebugTime);
				}

			#endif*/
        }
    } else {
        turnFanOff(); // Turn off the fan relay
    }

    //update fan door speed in case it changed
    word currDoorSpeed = settingsManager->getFanDoorSpeed();
    if (currDoorSpeed != fanDoorSpeed) {
    	fanDoorSpeed = currDoorSpeed;
    }

    //set new door position. This will change over time, not instantly
    setFanDoorPosition(doorPosition);
```

Všimněte si první části. Napájení ventilátoru se zapíná/vypíná pomocí relé. Kód tedy také čeká jednu sekundu, než po zapnutí relé přivede na ventilátor první PWM signál.

Ani nastavení PWM ventilátoru, ani nastavení úhlu dvířek ventilátoru nejsou "prostými" zápisy na příslušný výstup Arduina, ke kterému je připojen signál PWM ventilátoru a signál pro řízení serva dvířek ventilátoru. Ovládání ventilátoru je implementováno na základě [tohoto článku](https://fdossena.com/?p=ArduinoFanControl/i.md) - který vyžadoval "hacky" - úpravy registrů/časovačů Arduina, které se používají k ovládání PWM výstupů na Arduino UNO. Kvůli těmto změnám nebylo možné použít ani "[klasickou servo](https://docs.arduino.cc/libraries/servo/)" knihovnu. "Proč" je ale podrobněji popsáno v této [sekci](#arduino_pin_assignments), takže se podívejte i tam..

## Info Display

- Header file: _InfoDisplay.h_

- Cpp file: _InfoDisplay.cpp_

Zobrazení různých informací na displeji 16x2 je implementováno ve třídě InfoDisplay. Zobrazuje deset různých informačních stránek, které jsou shrnuty níže.:

| **ID:** | INFO\_PAGE\_IATH |
| --- | --- |
| **Hodnota ID:** | 0 |
| **Příklad:** | \-Insid temp/hum- T=23.3,H=53 |
| **Popis**: | _Vnitřní teplota a vlhkost._ |

| **ID:** | INFO\_PAGE\_OATH |
| --- | --- |
| **Hodnota ID**: | 1 |
| **Příklad**: | \-Outsd temp/hum- T=18.9,H=53 |
| **Popis**: | _Vnější teplota a vlhkost._ |

| **ID:** | INFO\_PAGE\_GASSENS |
| --- | --- |
| **Hodnota ID**: | 2 |
| **Příklad**: | \-Gas/smk sensor- C=0 PPM (0%) |
| **Popis**: | _Hodnota senzoru plynu a kouře v ppm a procentech. Nebo doba pro zahřátí senzoru plynu, pokud senzor ještě není kalibrován._ |

| **ID:** | INFO\_PAGE\_VENT1 |
| --- | --- |
| **Hodnota ID**: | 3 |
| **Příklad**: | \-Vent power out- Fan is OFF |
| **Popis**: | _Zobrazuje režim ovládání ventilátopru - AUTO, ZAP, VYP a otáčky ventilátoru._ |

| **ID:** | INFO\_PAGE\_VENT2 |
| --- | --- |
| **Hodnota ID**: | 4 |
| **Příklad**: | \-Vent Temp diff- 23.0/35 (-12.0) |
| **Popis**: | _Zobrazuje vnitřní teplotu, maximální vnitřní teplotu a jejich rozdíl. Pokud je ventilace v režimu AUTO, měla by se spustit/zastavit, jakmile se teploty setkají._ |

| **ID:** | INFO\_PAGE\_VENT3 |
| --- | --- |
| **Hodnota ID**: | 5 |
| **Příklad**: | \-Vent FAN doors- Closed |
| **Popis**: | _Zobrazuje úhel otevření dvířek ventilátoru a to, zda fungují automaticky nebo manuálně._ |

| **ID:** | INFO\_PAGE\_LIGHT |
| --- | --- |
| **Hodnota ID**: | 6 |
| **Příklad**: | \-LED strip ligh- Off |
| **Popis**: | _Zobrazuje, zda je LED pásek zapnutý nebo vypnutý, a jas - % maximálního přivedeného výkonu._ |

| **ID:** | INFO\_PAGE\_POWER\_RELAY |
| --- | --- |
| **Hodnota ID**: | 7 |
| **Příklad**: | \-PWR relay outp- State: On |
| **Popis**: | _Zobrazuje, zda je výkonové relé (pro napájení tiskárny) sepnuto či nikoliv a zda je vyjímka (vynucení zapnutí) povolena či nikoliv._ |

| **ID:** | INFO\_PAGE\_FIRE |
| --- | --- |
| **Hodnota ID**: | 8 |
| **Příklad**: | \-Fire det (W/S)- 60/80C |
| **Popis**: | _Zobrazuje teplotu požárního varování / vypnutí._ |

| **ID:** | INFO\_PAGE\_MEM |
| --- | --- |
| **Hodnota ID**: | 9 |
| **Příklad**: | \-Memory usage - 2048\|1614\|434 |
| **Popis:** | _Zobrazuje využití paměti Arduina - Celková paměť/Použitá paměť/Volná paměť_. |

První řádek displeje vždy zobrazuje „název“ aktuálně zobrazené stránky. Většinou se zkracuje, protože 16 znaků není zrovna na psaní esejí. Třída automaticky přepíná stránky po uplynutí definovaného času, pokud to nastavení umožňuje. Stránky lze také přepínat otáčením otočného ovladače.

Tříd má dvě hlavní funkce:

```
void init();
```

jenž je volána z funkce [setup()](#setup) během inicializace Arduina. A:

```
void displayInfoPage();
```

která je volána z [hlavní smyčky](#main_loop). Ta zobrazuje ony různé informační stránky na na našem 16x2 displeji.

Věřím, že v kódu souboru _InfoDisplay.cpp_, který obsahuje implementaci této třídy, se neskrývá nic extra složitého, takže se zde nebudu zabývat detaily a nechám to na vašem prostudování.

A abychom zas měli nejaké obrázky:

\[rl\_gallery id="6862"\]

## Soubor"common.h"

Soubory _common.h_ a _common.cpp_ obsahují definice konstant a maker, které používá mnoho tříd a kódu v celém tomto projektu. Například:

```
constexpr byte FAN_IDLE_DUTY = 20;   // Minimum duty cycle for idle fan speed
constexpr byte FAN_OFF_DUTY = 0;    // Duty cycle for the fan to stop spinning
constexpr byte FAN_MAX_DUTY = 100;  // Maximum duty cycle for the fan
```

což jsou podle mě docela samovysvětlující konstanty :)

v _.h_ souboru najdete ale také „řetězcové“ konstanty, definované následovně:

```
extern const char SM_STR_FAM_NORMAL[] PROGMEM;
extern const char SM_STR_FAM_WARNING[] PROGMEM;
extern const char SM_STR_FAM_HAZARD[] PROGMEM;
```

což jsou jen jakési „před-definice“, skutečná hodnota je přiřazena až v _.cpp_ souboru:

```
const char SM_STR_FAM_NORMAL[] PROGMEM = "Normal";
const char SM_STR_FAM_WARNING[] PROGMEM = "Fire Warning";
const char SM_STR_FAM_HAZARD[] PROGMEM = "Fire Hazard";
```

Zajímavé je zde použití **PROGMEM**. S takto definovanými konstantami se můžete setkat i v jiných souborech, nejen zde. Možná jste se s tímto klíčovým slovem nikdy nesetkali, stejně jako já - před tímto projektem. Normálně se všechny konstantní hodnoty ukládají do zásobníku. Dostupnou paměť v Arduinu UNO však můžeme poměrně rychle vyčerpat uložením většího množství proměnných typu "string". Klíčové slovo **PROGMEM** říká kompilátoru, aby hodnotu uložil do paměti programu. To ale také mění způsob, jakým s tímto řetězcem můžeme pracovat. Nemůžeme ho jen předat jako normální "_const char\*_" řetězec , ale musíme ho načíst z programové paměti do zásobníku. Viz příklad níže.:

```
lcdHelper->printTextToLCD(_loadFlashString(reinterpret_cast<const __FlashStringHelper*>(SM_STR_FAM_NORMAL)), 1);
```

Výše uvedený kód pochází ze souboru _SettingsManager.cpp_. Zobrazuje uvedený řetězec SM\_STR\_FAM\_NORMAL na prvním řádku 16x2 LCD displeje pomocí třídy [LCD1602Helper](#LCD1602Helper). První argument očekává klasický řetězec "const char\*"_,_ stejně jako mnoho klasických řetězcových funkcí. Musíme však použít funkci **\_loadFlashString** k načtení řetězce uloženého v paměti programu a jeho převodu na „_const char\*_“. Funkce **\_loadFlashString** je také definována v našem souboru _common.h/common.cpp_:

```
char* _loadFlashString(const __FlashStringHelper* flashStr) {
    static char buffer[LCD_STRING_MAX_LEN + 1] = {0};  // +1 for null terminator
    memset(buffer, 0, sizeof(buffer));  // Clear the buffer before copying, this is because left-over chars from previous calls, that would otherwise be there
    strncpy_P(buffer, (PGM_P)flashStr, LCD_STRING_MAX_LEN);
    buffer[LCD_STRING_MAX_LEN] = '\0';  // Ensure null termination
    return buffer;
}
```

Funkce vytvoří char buffer, zkopíruje do něj řetězec z programové paměti a vrátí onen buffer (ukazatel na první znak). Každé volání této funkce vytvoří nový buffer. Existuje podobná funkce, která je vyhrazena pro načítání ladicích zpráv a liší se tím, že znovu používá jeden permanentní buffer.:

```
// Define global buffers (so only one copy exists in the whole program)
char debugBuffer[DEBUG_BUFFER_SIZE];
char debugFlashStrBuffer[DEBUG_BUFFER_SIZE];

void _loadDebugFlashString(const __FlashStringHelper* flashStr, char* buffer, size_t bufferSize) {
    memset(buffer, 0, bufferSize);  // Clear the buffer
    strncpy_P(buffer, (PGM_P)flashStr, bufferSize - 1);
    buffer[bufferSize - 1] = '\0';  // Ensure null termination
}
```

Tato funkce se pak používá v makrech pro logování:

```
#define MK4SC_DEBUG  // Comment this line to disable debugging

#ifdef MK4SC_DEBUG
    #define DEBUG_PRINTER Serial /**< Define where debug output will be printed.**/

	#define DEBUG_BUFFER_SIZE 128  /**< Buffer size for the debug messages. */
    extern char debugBuffer[DEBUG_BUFFER_SIZE];  // Global buffer for all debug macros
    extern char debugFlashStrBuffer[DEBUG_BUFFER_SIZE];  //global buffer for loading debug message stored in PROGMEM

    void _loadDebugFlashString(const __FlashStringHelper* flashStr, char* buffer, size_t bufferSize);

    #define DEBUG_INIT(baud) DEBUG_PRINTER.begin(baud); while (!DEBUG_PRINTER)

    #define DEBUG_PRINT(fmt, ...) { \
        memset(debugBuffer, 0, sizeof(debugBuffer)); \
        int len = snprintf(debugBuffer, sizeof(debugBuffer), fmt, ##__VA_ARGS__); \
        debugBuffer[sizeof(debugBuffer) - 1] = '\0'; \
        if (len > 0 && len < sizeof(debugBuffer)) DEBUG_PRINTER.print(debugBuffer); \
    }

    #define DEBUG_PRINTLN(fmt, ...) { \
        memset(debugBuffer, 0, sizeof(debugBuffer)); \
        int len = snprintf(debugBuffer, sizeof(debugBuffer), fmt, ##__VA_ARGS__); \
        debugBuffer[sizeof(debugBuffer) - 1] = '\0'; \
        if (len > 0 && len < sizeof(debugBuffer)) DEBUG_PRINTER.println(debugBuffer); \
    }

    #define DEBUG_PRINT_F(fmt, ...) { \
        _loadDebugFlashString(fmt, debugFlashStrBuffer, DEBUG_BUFFER_SIZE); \
        DEBUG_PRINT(debugFlashStrBuffer, ##__VA_ARGS__); \
    }

    #define DEBUG_PRINTLN_F(fmt, ...) { \
        _loadDebugFlashString(fmt, debugFlashStrBuffer, DEBUG_BUFFER_SIZE); \
        DEBUG_PRINTLN(debugFlashStrBuffer, ##__VA_ARGS__); \
    }

    // Macro to check if the specified interval has passed
	#define DEBUG_TIME_ELAPSED(interval, lastTimeVar) (millis() - (lastTimeVar) >= (interval))

	// Macro to reset the interval time
	#define DEBUG_RESET_TIME(lastTimeVar) (lastTimeVar = millis())

#else
    #define DEBUG_INIT(baud)
    #define DEBUG_PRINT(fmt, ...)
    #define DEBUG_PRINTLN(fmt, ...)
    #define DEBUG_PRINT_F(fmt, ...)
    #define DEBUG_PRINTLN_F(fmt, ...)
	#define DEBUG_TIME_ELAPSED(interval, lastTimeVar) (false)
    #define DEBUG_RESET_TIME(lastTimeVar)
#endif
```

respektive těmi, které končí na \_F, což signalizuje, že očekávají ladicí zprávu umístěnou v paměti programu.

## "Další" třídy

V tomto projektu se používá několik „dalších“ nebo „pomocných“ tříd, o kterých by bylo záhadno se krátce zmínit.:

### LCD1602Helper

- Header file: _LCD1602Helper.h_

- Cpp file: _LCD1602Helper.cpp_

LCD1602Helper je, jak název napovídá, pomocná třída. Používá se v celém projektu k zobrazení textu a různých hodnot na 16x2 LCD displeji. Níže je její definice:

```
#ifndef LCD1602HELPER_H
#define LCD1602HELPER_H

#include <Arduino.h>
#include <LiquidCrystal_I2C.h>
#include "LCDHelper.h"
#include "common.h"

constexpr byte LCD1602_WIDTH = 16;
constexpr byte LCD1602_ROWS = 2;

class LCD1602Helper : public LCDHelper {
private:
    LiquidCrystal_I2C* lcd; // I2C 1602 LCD display

    //last rows
    char lastRow1[LCD1602_WIDTH + 1] = {0};
    char lastRow2[LCD1602_WIDTH + 1] = {0};

    void printRows(byte row, const char* row1, const char* row2);
    void printRow(const char* text, byte row);

public:
    // Constructor
    LCD1602Helper(LiquidCrystal_I2C* lcd);

    // Overriding base class methods
    void printTextToLCD(const char* text, byte row) override;
    void printFormattedTextToLCD(byte row, const char* fmt, ...) override;
    void displayFloatValue(const char* title, float value, const char* unit) override;
    void displayFloatValue(const char* title, float value, const char* unit, byte precision, byte row) override;
    void display2FloatValues(const char* title1, float value1, const char* unit1, byte precision1,
                             const char* title2, float value2, const char* unit2, byte precision2, byte row) override;
    void displayByteValue(const char* title, byte value, const char* unit) override;
    void displayByteValue(const char* title, byte value, const char* unit, byte row) override;
    virtual void displayWordValue(const char *title, word value, const char *unit) override;
    virtual void displayWordValue(const char *title, word value, const char *unit, byte row) override;
    void clear() override;
    void clearRow(byte row);

    static void sanitizeLCDRow(char* row, size_t length);

    LiquidCrystal_I2C* getLCD();
};

#endif
```

Displej 16x2 je LCD displej se 2 řádky, kde každý může zobrazit 16 znaků. V projektu používáme hlavně proměnné typu bajt, slovo a desetinná čísla, takže si jsem jistý, že si snadno odvodíte, které pomocné metody se používají pro které typy proměnných.

### GenericPWM and UnoPWM

- Header file: _GenericPWM .h_, _UnoPWM.h_

- Cpp file: _GenericPWM .cpp_, _UnoPWM.cpp_

Třídy _GenericPWM_ a _UnoPWM_ zapouzdřují funkcionalitu vlastního/upraveného řízení PWM signálu. V tomto projektu je to zaměřené na řízení PC ventilátoru (píšu o tom v kapitole [Přiřazení pinů Arduina UNO](#arduino_pin_assignments) a v kapitole [Ventilation Control](#VentilationControl)).

Třída _GenericPWM_ je ve skutečnosti „rozhraní“ nebo super-třída:

```
#ifndef GENERIC_PWM_H
#define GENERIC_PWM_H

#include <Arduino.h>

class GenericPWM {
public:
    virtual ~GenericPWM() {}

    virtual void setupTimer() = 0;
    virtual void setPwmDuty(byte duty, byte pwmPin) = 0;
};

#endif
```

pro _UnoPWM_ (a další možné):

```
#ifndef UNO_PWM_H
#define UNO_PWM_H

#include "GenericPWM.h"

/**
 * @class UnoPWM
 * @brief Class for configuring and controlling PWM on an Arduino Uno.
 *
 * This class allows generating PWM signals on pins 3, 9, 10, and 11 with configurable frequencies.
 * It provides fine control over Timer1 (for pins 9 and 10) and Timer2 (for pins 3 and 11), allowing
 * phase-correct PWM operation with adjustable duty cycles.
 *
 * Features:
 * - Supports configuring Timer1 and Timer2 for PWM output.
 * - Allows enabling/disabling individual PWM pins via flags.
 * - Computes appropriate timer top values based on the desired frequency.
 * - Provides functions to set PWM duty cycle dynamically.
 */
class UnoPWM : public GenericPWM {
private:
    word pwmFreqHz; ///< Desired PWM frequency in Hz
    word tcnt1Top;  ///< Computed TOP value for Timer1
    byte tcnt2Top;  ///< Computed TOP value for Timer2

    byte pwmPin1; ///< Pin assigned to Timer1 Channel A (default: 9)
    byte pwmPin2; ///< Pin assigned to Timer1 Channel B (default: 10)
    byte pwmPin3; ///< Pin assigned to Timer2 Channel B (default: 3)
    byte pwmPin4; ///< Pin assigned to Timer2 Channel A (default: 11)

    byte flags; ///< Configuration flags to enable specific timers and PWM pins

    /**
     * @brief Calculates the TOP value for Timer1 and Timer2 based on the desired frequency.
     */
    void calculateTop();

    /**
     * @brief Configures Timer1 for phase-correct PWM operation.
     */
    void setupTimer1();

    /**
     * @brief Configures Timer2 for fast PWM operation.
     */
    void setupTimer2();

public:
    // Default PWM pin assignments
    static constexpr byte PWM_PIN1 = 9;
    static constexpr byte PWM_PIN2 = 10;
    static constexpr byte PWM_PIN3 = 3;
    static constexpr byte PWM_PIN4 = 11;

    // Flags for configuring timers
    static constexpr byte FLAG_CONFIG_TIMER1 = 1;  ///< Enable Timer1 (pins 9, 10)
    static constexpr byte FLAG_CONFIG_TIMER2 = 2;  ///< Enable Timer2 (pins 3, 11)

    // Flags for enabling individual PWM pins
    static constexpr byte FLAG_ENABLE_PIN1 = 4;  ///< Enable PWM on pin 9
    static constexpr byte FLAG_ENABLE_PIN2 = 8;  ///< Enable PWM on pin 10
    static constexpr byte FLAG_ENABLE_PIN3 = 16; ///< Enable PWM on pin 3
    static constexpr byte FLAG_ENABLE_PIN4 = 32; ///< Enable PWM on pin 11

    /**
     * @brief Constructor to initialize the UnoPWM object.
     *
     * @param pwmFreqHz Desired PWM frequency.
     * @param pwmPin1 PWM pin associated with Timer1 Channel A (default: 9).
     * @param pwmPin2 PWM pin associated with Timer1 Channel B (default: 10).
     * @param pwmPin3 PWM pin associated with Timer2 Channel B (default: 3).
     * @param pwmPin4 PWM pin associated with Timer2 Channel A (default: 11).
     * @param flags Configuration flags for enabling timers and PWM pins.
     */
    UnoPWM(word pwmFreqHz, byte pwmPin1 = PWM_PIN1, byte pwmPin2 = PWM_PIN2, byte pwmPin3 = PWM_PIN3, byte pwmPin4 = PWM_PIN4, byte flags = (FLAG_CONFIG_TIMER1 | FLAG_ENABLE_PIN1));

    /**
     * @brief Sets up the timers based on the provided flags.
     */
    void setupTimer() override;

    /**
     * @brief Sets the PWM duty cycle for a given pin.
     *
     * @param duty Duty cycle percentage (0-100).
     * @param pwmPin The pin on which to set the duty cycle.
     */
    void setPwmDuty(byte duty, byte pwmPin) override;

    /**
     * @brief Retrieves the computed TOP value for Timer1.
     *
     * @return Timer1 TOP value.
     */
    word getTcnt1Top() const { return tcnt1Top; }

    /**
     * @brief Retrieves the computed TOP value for Timer2.
     *
     * @return Timer2 TOP value.
     */
    byte getTcnt2Top() const { return tcnt2Top; }
};

#endif
```

_UnoPWM_ je implementace modifikací časovače Timer1 pro Arduino UNO, která je potřebná pro řízení PC ventilátoru pomocí 25KHz PWM signálu, jak je popsáno v článku [How to properly control PWM fans with Arduino](https://fdossena.com/?p=ArduinoFanControl/i.md). Třída je napsána obecnějším způsobem, takže by měla umožňovat i jiné PWM frekvence, nejen 25KHz. V tomto projektu se však používá s 25KHz.

### DHTAsyncSensors

- Header file: _DHTAsyncSensors.h_

- Cpp file: _DHTAsyncSensors.cpp_

_DHTAsyncSensors_ je pomocná třída, která zapouzdřuje dvě instance senzorových proměnných [DHTAsync](https://github.com/KushlaVR/DHT-Async), které reprezentují naše vnitřní a venkovní senzory teploty/vlhkosti. Senzory jsou čteny asynchronně (na rozdíl od standardní knihovny DHT) a pokud jste to výše přehlédli, [zde je důvod](#dht_async_read). Deklarace třídy vypadá následovně:

```
#ifndef DHT_SENSORS_H
#define DHT_SENSORS_H

#include <DHT_Async.h>
#include "common.h"

constexpr word DEFAULT_MEASUREMENT_INTERVAL = 2000;
constexpr word READ_FAIL_TRESHOLD = 6000;

class DHTAsyncSensors {
private:
	DHT_Async insideSensor;
	DHT_Async outsideSensor;

    byte insideSensorPin;
    byte outsideSensorPin;
    byte insideSensorType;
    byte outsideSensorType;

    float insideTemperature;
    float insideHumidity;
    float outsideTemperature;
    float outsideHumidity;

    word insideMeasurementInterval;
    word outsideMeasurementInterval;
    unsigned long lastInsideMeasurementTime;
    unsigned long lastOutsideMeasurementTime;

public:

    DHTAsyncSensors(byte insideSensorPin, byte insideSensorType, byte outsideSensorPin, byte outsideSensorType , word measurementInterval, word outsideMeasurementInterval);

    bool measureInsideEnvironment();
    bool measureOutsideEnvironment();
    bool measureEnvironments();

    float getInsideTemperature() const { return insideTemperature; }
    float getInsideHumidity() const { return insideHumidity; }
    float getOutsideTemperature() const { return outsideTemperature; }
    float getOutsideHumidity() const { return outsideHumidity; }

    bool isInsideValid() const { return !(isnan(insideTemperature) && isnan(insideHumidity)); }
    bool isOutsideValid() const { return !(isnan(outsideTemperature) && isnan(outsideHumidity)); }
    bool areValuesValid() const { return isInsideValid() && isOutsideValid(); }
};

#endif
```

V implementaci se neskrývá nic složitého - takže si to, opět, prostudujte sami :)

### MQ2GasSensorNonBlocking

- Header file: _MQ2GasSensorNonBlocking.h_

- Cpp file: _MQ2GasSensorNonBlocking.cpp_

_MQ2GasSensorNonBlocking_ je třída, která obstarává práci s plynovým senzorem MQ2. Čte hodnotu plynového senzoru pomocí jeho analogového signálu (ale také umožňuje nastavit funkci zpětného volání (callback) pro digitální vstup). Nečte pouze analogovou hodnotu (no, na konci kódu… to dělá), ale dělá to pomocí složitějšího přístupu popsaného podrobněji v článku: [Smoke Detector using MQ2 Gas Sensor and Arduino](https://circuitdigest.com/microcontroller-projects/arduino-smoke-detector-on-pcb-using-mq2-gas-sensor), který zahrnuje vícenásobné odečty, průměrné hodnoty, plynové křivky a další kouzla..

Než bude možné provádět smysluplná měření, musí být senzor zkalibrován, aby se určil jeho základní odpor (R₀) v čistém vzduchu. Tuto kalibrační logiku zpracovává interně třída a spouští se z [hlavní smyčky](#main_loop):

```
//calibrate gas sensor
	byte gasSensState = gasSensor.calibrateGasSensor();
	//if calibrating right now, skip everything else
	if (gasSensState == MQ2GasSensorNonBlocking::SENSOR_STATE_CALIBRATING) {
		return;
	}
```

Kalibrace se spustí po definovaném čase (standardně 180 sekund), po který má být senzor napájen, aby se „zahřál“, a po kterém by jeho hodnoty měly být důvěryhodné. Během kalibrace třída opakovaně vzorkuje analogový pin, filtruje šum průměrováním více hodnot a vypočítává odpor senzoru (Rₛ). Z poměru Rₛ/R₀ může pak kód odhadnout koncentraci plynu v PPM pomocí předdefinovaných křivek plynu. Tato logika je zapouzdřena ve funkcích zodpovědných za kalibraci, výpočet odporu a převod na PPM, čímž se udržuje hlavní kód aplikace čistý a čitelný.

Třída je navržena tak, aby **nebyla blokující**, což znamená, že se vyhýbá dlouhým zpožděním, která by jinak zastavila hlavní smyčku. Periodické vzorkování je řešeno pomocí časových kontrol založených na funkci _millis()_, což umožňuje koexistenci plynového senzoru s dalšími časově kritickými komponentami, jako je PWM řízení ventilátoru, servořízení, přerušení a uživatelský vstup. Kromě analogových odečtů PPM třída také podporuje připojení přerušení na funkci zpětného volání (callback) přes digitální výstup MQ-2, což umožňuje okamžitou reakci na detekci plynu na základě prahové hodnoty a zároveň zachovává nepřetržité analogové monitorování.

Abychom zmínili i nějaký kód, tato soukromá funkce:

```
/**
   * Function does the initial sensor calibration. It performs initial resistance reading.
   *
   * @return When calibration is finished, it will return true. Otherwise, it returns false.
   */
  bool calibrateSensor();
```

provádí onu kalibraci, i když ji spouští veřejná funkce:

```
/**
     * This function performs initial sensor calibration. It waits for sensor warm-up and does initial resistance reading. It is non-blocking.
     * You can tell when sensor is calibrated by calling {@link #isSensorCalibrated()}.
     */
    byte calibrateGasSensor();
```

a tyto dvě se pak používají k odečtení hodnoty senzoru:

```
/**
     * This functions returns the raw sensor analog value.
     */
    word getSensorValue() const;
    /**
     * This functions returns sensor value recalculated to PPM.
     */
    word getSensorValuePPM() const;
```

### WiderServoTimer2

- Header file: _WiderServoTimer2.h_

- Cpp file: _WiderServoTimer2.cpp_

Poslední třída, kterou chci zmínit, je naše vlastní, mírně upravená verze knihovny [ServoTimer2](https://github.com/nabontra/ServoTimer2). Pokud jste ji přehlédli, přečtěte si [zde](#why_modified_timers), proč tuto knihovnu potřebujeme k ovládání serva dvířek ventilátoru a proč nepoužíváme standardní knihovnu [Servo](https://docs.arduino.cc/libraries/servo/).

Jediné změny, které jsem provedl v naší knihovně WiderServoTimer2 oproti původnímu ServoTimer2, jsou změny minimálního a maximálního počtu pulzů.:

```
#define MIN_PULSE_WIDTH       600        // the shortest pulse sent to a servo

#define MAX_PULSE_WIDTH      2400        // the longest pulse sent to a servo
```

Že se ptáte ... proč? V původní knihovně ServoTimer2 je efektivní rozsah šířky impulsů serva konzervativnější než ve standardní Arduino knihovně pro serva a je obvykle omezen na přibližně **750 µs (minimum)** a **2250 µs (maximum)**. I když je tento užší rozsah bezpečný pro mnoho serv, v tomto projektu se ukázal jako nedostatečný, protože **neumožňoval servu dosáhnout požadovaných plných mechanických výchylek**, zejména v blízkosti zavřených a plně otevřených dvířek ventilátoru.

Rozšířením použitelného rozsahu pulzů je nyní servo schopno dosáhnout (téměř) **plného provozního rozsahu 0°–90°**, který vyžaduje mechanismus dvířek ventilátoru. To zajišťuje, že se dvířka mohou plně zavřít (takže teplota neuniká z boxu) a plně otevřít (maximální průtok vzduchu během ventilace). Rozšířené limity lépe odpovídají skutečným možnostem použitého serva a zároveň zůstávají v rámci bezpečných a běžně podporovaných šířek pulzů pro standardní hobby serva. Tato úprava byla nezbytná pro dosažení **správných mechanických koncových poloh**, nikoli zvýšení rychlosti nebo síly, a zlepšuje celkovou spolehlivost systému a přesnost řízení.

### DHTSensors a MQ2GasSensor

- Header file: _DHTSensors .h_, _MQ2GasSensor.h_

- Cpp file: _DHTSensors .cpp_, _MQ2GasSensor.cpp_

Ve staženém archivu si můžete všimnout zdrojových souborů tříd _DHTSensors_ a _MQ2GasSensor_. Jedná se o první verze řídicích tříd, které jsem používal, než jsem zjistil, že způsob, jakým čtou data ze senzorů (blokovací způsob), je hlavním důvodem pomalé zpětné vazby a řízení řídicího panelu. Kód jednoduše neběžel dostatečně často, protože čtení ze senzorů způsobovalo příliš dlouhé zpoždění. Poté byly třídy nahrazeny jejich neblokujícími verzemi - [DHTAsyncSensors](#DHTAsyncSensors) a [MQ2GasSensorNonBlocking](#MQ2GasSensorNonBlocking). Třídy jsem ve zdrojovém kódu ponechal, pro případ, že by jste si chtěli prostudovat rozdíly. Soubory se při kompilaci nepoužívají.


## Chyby...

V kódu se potuluje pár chyb, které jsem ještě nevychytal. Vím o těchto:

- Funkci watch dog časovače jsem moc netestoval / neladil.

- Arduino se občas prostě zasekne.

- Čtení hodnot RPM ventilátoru je trochu nespolehlivé.

- Vnější senzor DHT11 neposkytne platná data, dokud se alespoň jednou nestiskne tlačítko na rotačním enkodéru. Žádný z léků, triků ani řešení, které jsem zkoušel, nefungoval. Pravděpodobně ho prostě vyměním za DHT22, který se používá pro vnitřní senzor, jenž ten funguje bez problémů.

## Odkazy

Odkazy související s programováním Arduina:

- [Arduino Dockumentace](https://docs.arduino.cc)

- [EEPROM Library](https://docs.arduino.cc/learn/built-in-libraries/eeprom/)

- [Knihovna Servo](https://docs.arduino.cc/libraries/servo/) (i když není v tomto projektu použita)

- [Knihovna ServoTimer2](https://github.com/nabontra/ServoTimer2)

- [Knihovna DHT-Async](https://github.com/KushlaVR/DHT-Async)

Odkazy související s ventilačním systémem:

- [PWN Fan controller with temp sensing and button override](https://projecthub.arduino.cc/KaptenJansson/pwn-fan-controller-with-temp-sensing-and-button-override-5306e0)

- [How to properly control PWM fans with Arduino](https://fdossena.com/?p=ArduinoFanControl/i.md)

- [Secrets of Arduino PWM](https://docs.arduino.cc/tutorials/generic/secrets-of-arduino-pwm/)

- [Timer Interrupts and PWM Pins](https://forum.arduino.cc/t/timer-interrupts-and-pwm-pins/316380)

Odkazy související se systémem protipožárního zabezpečení:

- [Smoke Detector using MQ2 Gas Sensor and Arduino](https://circuitdigest.com/microcontroller-projects/arduino-smoke-detector-on-pcb-using-mq2-gas-sensor)

- [How MQ2 Gas/Smoke Sensor Works? & Interface it with Arduino](https://lastminuteengineers.com/mq2-gas-senser-arduino-tutorial/?utm_content=cmp-true#google_vignette)

- [How Does MQ-2 Flammable Gas and Smoke Sensor Work with Arduino?](https://circuitdigest.com/microcontroller-projects/interfacing-mq2-gas-sensor-with-arduino)

Další užitečné odkazy:

- [Arduino UNO R3 dokumentace](https://docs.arduino.cc/hardware/uno-rev3/)

- [ATmega328/P datasheet](https://datasheets.b-cdn.net/files/ATMEGA328-MU-Microchip-datasheet-154741723.pdf)

## Všechny články série:

- [Jak jsem stavěl box pro Prusa MK4S – Výchozí vize](https://mouseviator.com/3d-tisk/jak-jsem-stavel-box-pro-prusa-mk4s-vychozi-vize/)

- [Jak jsem stavěl box pro Prusa MK4S – Tištěné díly](https://mouseviator.com/3d-tisk/jak-jsem-stavel-box-pro-prusa-mk4s-tistene-dily/)  

- [Jak jsem stavěl box pro Prusa MK4S – Zapojení](https://mouseviator.com/3d-tisk/jak-jsem-stavel-box-pro-prusa-mk4s-zapojeni/)

- Jak jsem stavěl box pro Prusa MK4S – Programování – tenhle článek :)
