Sumario
ToggleRUNBOOK OPERACIONAL: Sistema de Controle de Névoa e Monitoramento Climático
1. Visão Geral do Sistema
Este sistema automatiza o acionamento de uma válvula solenóide para gerar névoa de água em um ventilador de quintal, com temporizador e ciclos de névoa, visando o conforto térmico. O controle é feito remotamente via interface Web (HTTP), que também exibe o monitoramento em tempo real e o histórico de temperatura e umidade.
Este projeto pode ser considerado um complemento do artigo “Monitoramento de Temperatura e Umidade com Raspberry Pi e Grafana”, pois usa boa parte da mesma estrutura com Raspberry PI 3b+ e o sensor de temperatura e umidade. Porém, se você não acompanhou o artigo anterior, não tem problemas, pois vou abordar aqui tudo do absoluto zero.
- Plataforma Base: Raspberry Pi 3 Model B (Debian / OS baseado em Linux).
- Controle Lógico: Scripts Python (gpiozero, Adafruit_DHT) orquestrados via Shell Script e PHP.
- Apresentação Web: Servidor Apache2 (página única index.php com interface responsiva, gráficos Chart.js e acionamento via AJAX).
- Armazenamento de Dados: Arquivo CSV local (/etc/grafana/csv/temperatura.csv).
2. Arquitetura de Hardware (Bill of Materials – BOM)
- Controladora: Raspberry Pi 3 Model B (Quad-core, 1GB RAM, Wi-Fi 2.4GHz integrado).
- Sensor Climático: Módulo DHT11 (3 pinos, alimentação 3.3V a 5V).
- Atuador Lógico: Módulo de Relé 1 Canal (HW-803) com Optoacoplador.
- Atuador Físico: Válvula Solenóide de Água DC12V (NF – Normalmente Fechada), tipo de pressão.
- Alimentação Externa (Carga): Fonte Chaveada AC/DC (Entrada 100-240V / Saída 12V DC, 2A).
- Refrigeração: Micro Ventilador (Mini Fan Brushless) DC 5V.
- Cabeamento: Jumpers Dupont (Fêmea-Fêmea) e Fio Paralelo Flexível de Cobre (0.50mm² ou 0.75mm²).
3. Diagrama Físico e Pinagem (Pinout)
O sistema utiliza o header GPIO de 40 pinos do Raspberry Pi. O mapeamento abaixo considera a placa vista de cima (lado do MicroSD na parte inferior esquerda).
(Lado do Cartão MicroSD) [3.3V] 01 [X] [X] 02 [5V] [GPIO 02] 03 [ ] [X] 04 [5V] [GPIO 03] 05 [ ] [X] 06 [GND] [GPIO 04] 07 [ ] [X] 08 [GPIO 14] [GND] 09 [X] [ ] 10 [GPIO 15] [GPIO 17] 11 [ ] [X] 12 [GPIO 18] [GPIO 27] 13 [ ] [X] 14 [GND] ... ...
Mapeamento das Conexões:
- Módulo Sensor DHT11: VCC (Fio Roxo) -> Pino 01 (3.3V). GND (Fio Verde) -> Pino 06 (GND). DATA (Fio Azul) -> Pino 08 (GPIO 14).
- Módulo Relé HW-803 (Jumper obrigatoriamente em “H”): DC+ -> Pino 02 (5V). DC- -> Pino 14 (GND). IN -> Pino 12 (GPIO 18).
- Micro Ventilador (Fan): Cabo Vermelho (+) -> Pino 04 (5V). Cabo Preto (-) -> Pino 09 (GND).
- Circuito Hidráulico (12V): Fonte 12V Positivo (Vermelho) -> Borne COM do Relé. Borne NO do Relé -> Terminal 1 da Válvula Solenóide. Fonte 12V Negativo (Preto) -> Terminal 2 da Válvula Solenóide.
4. Provisionamento Automatizado (Disaster Recovery Lógico)
Em caso de falha catastrófica de SO ou novo hardware, utilize o script monolítico abaixo para provisionar um Raspberry Pi recém-formatado (Debian/Raspberry OS base). Ele instala pacotes, cria diretórios, gera códigos Python/PHP, ajusta o sudoers e cria tarefas cron.
Execução:
- Crie o arquivo:
sudo nano install_clima.sh. - Cole o código abaixo e salve.
#!/bin/bash
# ==============================================================================
# SCRIPT DE INSTALAÇÃO AUTOMATIZADA - PROJETO NÉVOA & CLIMA
# Executar como ROOT (sudo bash install_clima.sh)
# ==============================================================================
echo "Iniciando a automação do ambiente..."
# 1. Instalação de Pacotes Base
echo "[1/7] Instalando dependências do sistema (Apache, PHP, Python)..."
apt-get update
apt-get install -y apache2 php libapache2-mod-php python3 python3-venv python3-dev python3-pip gcc libgpiod2
# 2. Criação de Diretórios
echo "[2/7] Criando estrutura de diretórios..."
mkdir -p /scripts
mkdir -p /etc/grafana/csv
mkdir -p /tmp/dht_saneamento
# 3. Configuração do Python Virtual Environment
echo "[3/7] Criando Virtual Environment e instalando bibliotecas GPIO/DHT..."
python3 -m venv /root/dht_venv
/root/dht_venv/bin/pip install Adafruit_DHT gpiozero
# 4. Criação dos Scripts Backend
echo "[4/7] Escrevendo scripts Shell e Python..."
# -> sensor.py
cat << 'EOF' > /scripts/sensor.py
#!/usr/bin/env python3
import sys, Adafruit_DHT as dht, Adafruit_DHT.Raspberry_Pi as rpi
try:
h, t = dht.read_retry(dht.DHT11, 14, platform=rpi)
if h is not None and t is not None:
print(f"Temperatura: {round(t, 1)}°C\nUmidade: {round(h, 1)}%")
else: sys.exit(1)
except: sys.exit(1)
EOF
# -> leitura_dht.py
cat << 'EOF' > /scripts/leitura_dht.py
#!/usr/bin/env python3
import sys, Adafruit_DHT as dht, Adafruit_DHT.Raspberry_Pi as rpi, json
try:
h, t = dht.read_retry(dht.DHT11, 14, platform=rpi)
if h is not None and t is not None:
print(json.dumps({"success": True, "temperature": round(t, 1), "humidity": round(h, 1)}))
else: print(json.dumps({"success": False, "error": "Falha na leitura."}))
except Exception as e:
print(json.dumps({"success": False, "error": str(e)}))
sys.exit(1)
EOF
# -> rele_loop.py
cat << 'EOF' > /scripts/rele_loop.py
#!/usr/bin/env python3
import sys, time
from gpiozero import OutputDevice
try:
FLUXO = float(sys.argv[1])
with OutputDevice(18, active_high=True) as rele:
rele.on()
time.sleep(FLUXO)
rele.off()
except: sys.exit(1)
EOF
# -> LigaReleGPIO4.sh
cat << 'EOF' > /scripts/LigaReleGPIO4.sh
#!/bin/bash
Intervalo=$1
Fluxo=$2
while true; do
/usr/bin/python3 /scripts/rele_loop.py $Fluxo
sleep $Intervalo
done
EOF
# -> coleta_dht.sh
cat << 'EOF' > /scripts/coleta_dht.sh
#!/bin/bash
PYTHON_ENV="/root/dht_venv/bin/python"
SENSOR_SCRIPT="/scripts/sensor.py"
CSV_FILE="/etc/grafana/csv/temperatura.csv"
DATA=$(TZ='America/Sao_Paulo' date +%Y-%m-%d\ %H:%M:%S)
TEMPERATURA=$("$PYTHON_ENV" "$SENSOR_SCRIPT" 2>&1 | egrep "Temperatura|Umidade" | sed 's/Temperatura: //g; s/Umidade: //g' | sed 's/°C//g; s/%//g' | tr '\n' ';' | sed 's/;$//')
if [ -n "$TEMPERATURA" ]; then echo "$DATA;$TEMPERATURA" >> "$CSV_FILE"; fi
EOF
# -> saneamento_csv.sh
cat << 'EOF' > /scripts/saneamento_csv.sh
#!/bin/bash
CSV="/etc/grafana/csv/temperatura.csv"
TMP_CSV="/tmp/temperatura.tmp"
LOG_DIR="/tmp/dht_saneamento/"
mkdir -p "$LOG_DIR"
LOGTMP="$LOG_DIR/maior90.tmp"
rm -f "$LOGTMP"
HEADER=$(head -n 1 "$CSV")
DATA_LIMITE=$(date -d '90 days ago' +%Y-%m-%d\ %H:%M:%S)
tail -n +2 "$CSV" | awk -F';' -v data_limite="$DATA_LIMITE" '{ if ($1 < data_limite) print $0 }' > "$LOGTMP"
if [ -s "$LOGTMP" ]; then
echo "$HEADER" > "$TMP_CSV"
grep -v -F -f "$LOGTMP" <(tail -n +2 "$CSV") >> "$TMP_CSV"
mv "$TMP_CSV" "$CSV"
fi
EOF
# Ajusta permissões de execução para os scripts
chmod +x /scripts/*
# 5. Inicialização do CSV
echo "[5/7] Configurando CSV base..."
if [ ! -f /etc/grafana/csv/temperatura.csv ]; then
echo "data;temperatura;umidade" > /etc/grafana/csv/temperatura.csv
fi
chmod 666 /etc/grafana/csv/temperatura.csv
# 6. Publicação da Interface Web Unificada (index.php)
echo "[6/7] Publicando página Web..."
cat << 'EOF' > /var/www/html/index.php
<?php
$csv_file = '/etc/grafana/csv/temperatura.csv';
$python_env = '/root/dht_venv/bin/python';
$script_dht = '/scripts/leitura_dht.py';
$script_rele = '/scripts/LigaReleGPIO4.sh';
$pid_file = '/tmp/rele_script.pid';
$log_file = '/tmp/rele_script.log';
$params_file = '/tmp/rele_params.json';
$default_file = '/tmp/rele_defaults.json';
function read_dht_data() {
global $python_env, $script_dht;
$output = shell_exec("sudo $python_env $script_dht 2>&1");
$data = json_decode($output, true);
if ($data && isset($data['success']) && $data['success']) return $data;
return ['success' => false, 'temperature' => 'N/A', 'humidity' => 'N/A', 'error' => $data['error'] ?? 'Falha na leitura.'];
}
function is_script_running() {
global $pid_file, $params_file;
if (file_exists($pid_file)) {
$pid = trim(file_get_contents($pid_file));
exec("ps -p $pid -o pid=", $output);
if (!empty($output)) return true;
@unlink($pid_file); @unlink($params_file);
}
return false;
}
function get_current_params() {
global $params_file;
if (file_exists($params_file)) return json_decode(file_get_contents($params_file), true) ?: ['intervalo' => 15, 'fluxo' => 3];
return ['intervalo' => 15, 'fluxo' => 3];
}
function get_default_params() {
global $default_file;
if (file_exists($default_file)) return json_decode(file_get_contents($default_file), true) ?: ['intervalo' => 15, 'fluxo' => 3];
return ['intervalo' => 15, 'fluxo' => 3];
}
function set_default_params($intervalo, $fluxo) {
global $default_file;
return file_put_contents($default_file, json_encode(['intervalo' => (int)$intervalo, 'fluxo' => (int)$fluxo])) !== false;
}
function start_script($intervalo, $fluxo) {
global $pid_file, $log_file, $params_file, $script_rele;
file_put_contents($params_file, json_encode(['intervalo' => (int)$intervalo, 'fluxo' => (int)$fluxo]));
exec("nohup sudo $script_rele $intervalo $fluxo > $log_file 2>&1 & echo $! > $pid_file");
return file_exists($pid_file) ? trim(file_get_contents($pid_file)) : null;
}
function stop_script() {
global $pid_file, $params_file;
if (file_exists($pid_file)) {
exec("kill " . trim(file_get_contents($pid_file)));
@unlink($pid_file); @unlink($params_file);
return true;
}
return false;
}
function get_csv_history($hours = 24) {
global $csv_file;
$data = [];
if (!file_exists($csv_file)) return $data;
$cutoff = ($hours > 0) ? (time() - ($hours * 3600)) : 0;
if (($handle = fopen($csv_file, "r")) !== FALSE) {
fgetcsv($handle, 1000, ";");
while (($row = fgetcsv($handle, 1000, ";")) !== FALSE) {
if (count($row) >= 3) {
if ($hours == 0 || strtotime(trim($row[0])) >= $cutoff) {
$data[] = ['time' => trim($row[0]), 'temp' => floatval(trim($row[1])), 'hum' => floatval(trim($row[2]))];
}
}
}
fclose($handle);
}
return $data;
}
if (isset($_REQUEST['action'])) {
header('Content-Type: application/json');
$a = $_REQUEST['action'];
if ($a === 'dht_live') { echo json_encode(read_dht_data()); exit; }
if ($a === 'get_csv') { echo json_encode(['success' => true, 'data' => get_csv_history(intval($_GET['hours'] ?? 24))]); exit; }
if ($a === 'shutdown') { exec("sudo shutdown -h now"); echo json_encode(['success' => true]); exit; }
$i = intval($_POST['intervalo'] ?? 0); $f = intval($_POST['fluxo'] ?? 0);
if ($a === 'start') {
if ($i > 0 && $f > 0 && $f < $i) echo json_encode(['success' => (bool)start_script($i, $f)]);
else echo json_encode(['success' => false, 'message' => 'Fluxo deve ser menor que Intervalo.']);
exit;
}
if ($a === 'stop') { echo json_encode(['success' => stop_script()]); exit; }
if ($a === 'set_default') {
if ($i > 0 && $f > 0 && $f < $i) echo json_encode(['success' => set_default_params($i, $f)]);
else echo json_encode(['success' => false, 'message' => 'Valores inválidos.']);
exit;
}
}
$is_running = is_script_running();
$params = $is_running ? get_current_params() : get_default_params();
$dht_data = read_dht_data();
?>
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Controle de Névoa</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: sans-serif; background: #000; color: #fff; padding: 20px; display: flex; flex-direction: column; align-items: center; }
.container { max-width: 650px; width: 100%; display: flex; flex-direction: column; gap: 20px; }
.card { background: #1e1e1e; border: 1px solid #333; border-radius: 12px; padding: 20px; text-align: center; }
select { width: 100%; padding: 10px; margin-top: 5px; border-radius: 6px; font-weight: bold; }
.btn { width: 100%; padding: 14px; border: none; border-radius: 8px; font-size: 1.1rem; font-weight: bold; cursor: pointer; color: #fff; margin-top: 15px; }
.btn-default { background: #007bff; font-size: 0.9rem; padding: 10px; }
.btn-on { background: #28a745; } .btn-off { background: #dc3545; }
.btn-shutdown { background: #b71c1c; }
.status-box { padding: 12px; border-radius: 6px; font-weight: bold; margin-top: 15px; }
.s-run { background: #155724; color: #d4edda; } .s-stop { background: #721c24; color: #f8d7da; }
.chart-container { position: relative; height: 220px; width: 100%; margin-bottom: 20px; }
.tf-btn { background: #333; color: #fff; border: 1px solid #555; padding: 6px; border-radius: 4px; cursor: pointer; margin-right: 5px; }
.tf-btn.active { background: #007bff; }
</style>
</head>
<body>
<div class="container">
<h2>Controle de Névoa & Clima</h2>
<div class="card" style="background: #3e2723; border-color: #ffcc80; color: #ffab40;">
<span style="font-size:1.2rem; font-weight:bold;">
🌡️ <span id="t-val"><?php echo $dht_data['temperature']; ?></span> °C | 💧 <span id="h-val"><?php echo $dht_data['humidity']; ?></span> %
</span>
</div>
<div class="card" style="text-align: left;">
<div style="display:flex; gap:15px;">
<div style="flex:1;"><label>Intervalo:</label>
<select id="i-val">
<?php foreach([5,10,15,30,60,120,180,240,300,600] as $v) echo "<option value='$v'".($v==$params['intervalo']?' selected':'' ).">$v s</option>"; ?>
</select>
</div>
<div style="flex:1;"><label>Fluxo:</label>
<select id="f-val">
<?php foreach([2,3,5,10,15,30,60] as $v) echo "<option value='$v'".($v==$params['fluxo']?' selected':'' ).">$v s</option>"; ?>
</select>
</div>
</div>
<button id="btnDef" class="btn btn-default">Definir como Padrão</button>
<button id="btnTg" class="btn <?php echo $is_running ? 'btn-off' : 'btn-on'; ?>" data-a="<?php echo $is_running ? 'stop' : 'start'; ?>">
<?php echo $is_running ? 'Desligar' : 'Ligar'; ?>
</button>
<div class="status-box <?php echo $is_running ? 's-run' : 's-stop'; ?>">
Status: <strong><?php echo $is_running ? 'LIGADO' : 'DESLIGADO'; ?></strong>
</div>
</div>
<div class="card">
<div style="margin-bottom: 15px;">
<button class="tf-btn" onclick="ld(1,this)">1h</button><button class="tf-btn" onclick="ld(6,this)">6h</button>
<button class="tf-btn active" onclick="ld(24,this)">24h</button><button class="tf-btn" onclick="ld(168,this)">7d</button>
</div>
<div class="chart-container"><canvas id="cT"></canvas></div>
<div class="chart-container"><canvas id="cH"></canvas></div>
</div>
<button class="btn btn-shutdown" onclick="if(confirm('Desligar RPi?')) fetch('index.php?action=shutdown')">⚠️ Desligar RPi</button>
</div>
<script>
let tC = null, hC = null;
const cT = v => v >= 40 ? '#f44336' : (v >= 35 ? '#ffeb3b' : '#3399ff');
const cH = v => v < 30 ? '#f44336' : (v < 40 ? '#ffeb3b' : (v < 60 ? '#3399ff' : (v < 70 ? '#ffeb3b' : '#f44336')));
setInterval(() => {
fetch('index.php?action=dht_live').then(r=>r.json()).then(d=>{
if(d.success){ document.getElementById('t-val').innerText=d.temperature; document.getElementById('h-val').innerText=d.humidity; }
});
}, 10000);
function ld(h, b) {
if(b) { document.querySelectorAll('.tf-btn').forEach(x=>x.classList.remove('active')); b.classList.add('active'); }
fetch('index.php?action=get_csv&hours='+h).then(r=>r.json()).then(r => {
if(!r.success) return;
const lbs = r.data.map(d=>d.time.split(' ')[1].substring(0,5)), ts=r.data.map(d=>d.temp), hs=r.data.map(d=>d.hum);
if(tC) tC.destroy(); if(hC) hC.destroy();
const opts = { responsive:true, maintainAspectRatio:false, plugins:{legend:{display:false}}, scales:{x:{ticks:{color:'#888'}}, y:{ticks:{color:'#888'}}} };
tC = new Chart(document.getElementById('cT'), { type:'line', data:{ labels:lbs, datasets:[{ data:ts, borderWidth:2, pointRadius:2, tension:0.2, segment:{borderColor:c=>cT(c.p1.raw)}, fill:false }] }, options:opts });
hC = new Chart(document.getElementById('cH'), { type:'line', data:{ labels:lbs, datasets:[{ data:hs, borderWidth:2, pointRadius:2, tension:0.2, segment:{borderColor:c=>cH(c.p1.raw)}, fill:false }] }, options:opts });
});
} ld(24);
document.getElementById('btnTg').onclick = function() {
const a=this.dataset.a, i=document.getElementById('i-val').value, f=document.getElementById('f-val').value;
if(a==='start' && parseInt(f) >= parseInt(i)) return alert('Fluxo deve ser MENOR que Intervalo.');
fetch('index.php', { method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:`action=${a}&intervalo=${i}&fluxo=${f}` }).then(()=>location.reload());
};
document.getElementById('btnDef').onclick = function() {
const i=document.getElementById('i-val').value, f=document.getElementById('f-val').value;
if(parseInt(f) >= parseInt(i)) return alert('Fluxo deve ser MENOR que Intervalo.');
fetch('index.php', { method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:`action=set_default&intervalo=${i}&fluxo=${f}` }).then(()=>alert('Padrão Salvo!'));
};
</script>
</body>
</html>
EOF
# 7. Permissões Globais (Sudoers)
echo "[7/7] Configurando permissões do Apache (sudoers) e agendando Cron Jobs..."
# Remove regra antiga se existir e insere a nova
rm -f /etc/sudoers.d/clima_web
echo "www-data ALL=(ALL) NOPASSWD: /sbin/shutdown, /root/dht_venv/bin/python, /scripts/leitura_dht.py, /scripts/LigaReleGPIO4.sh" > /etc/sudoers.d/clima_web
chmod 0440 /etc/sudoers.d/clima_web
# 8. Agendamento do Cron
crontab -l > /tmp/cron_bkp 2>/dev/null || true
sed -i '/coleta_dht.sh/d' /tmp/cron_bkp
sed -i '/saneamento_csv.sh/d' /tmp/cron_bkp
echo "*/5 * * * * /scripts/coleta_dht.sh" >> /tmp/cron_bkp
echo "0 1 * * * /scripts/saneamento_csv.sh" >> /tmp/cron_bkp
crontab /tmp/cron_bkp
rm /tmp/cron_bkp
echo "========================================================================="
echo "INSTALAÇÃO CONCLUÍDA COM SUCESSO!"
echo "O sistema foi mapeado nos pinos GPIO 14 (DHT11) e GPIO 18 (Relé)."
echo "O CSV e a Interface Web estão operacionais."
echo "Você já pode acessar a página através do IP do Raspberry Pi."
echo "========================================================================="
- Execute:
sudo bash install_clima.sh.
Nota de Segurança: O script injeta o conteúdo index.php (contendo todas as funções PHP, CSS e JS do dashboard) e define as regras no /etc/sudoers.d/clima_web. Consulte o arquivo fonte gerado anteriormente na documentação do projeto para a carga completa do heredoc se necessário.
5. Rede e Acesso (Network & Security)
- Reserva DHCP: É estritamente recomendado configurar uma reserva de IP estático no roteador/DHCP server para a interface wlan0 (MAC Address: b8:27:eb:51:95:c4) para garantir o acesso ininterrupto via celular (ex: http://192.168.0.186).
- Acesso Administrativo: Via protocolo SSH (Porta 22 padrão) utilizando usuário root ou sudoer (pi/thiago).
- Autenticação Web: A página unificada no Apache opera sem autenticação, restrita ao acesso interno da LAN.
6. Monitoramento e Logs
Para auditoria de falhas de execução, os logs do sistema estão segregados nos seguintes caminhos absolutos:
- Logs do Servidor Web (Apache2):
/var/log/apache2/error.log. - Logs do Processo de Relé (Em background):
/tmp/rele_script.log. - PID Ativo do Relé:
/tmp/rele_script.pid. - Execuções do Agendador (Cron): Consulte o syslog filtrando o cron via
grep CRON /var/log/syslog.
7. Guia de Solução de Problemas (Troubleshooting)
- Sintoma A: A página Web está extremamente lenta para carregar e acusa “Erro DHT: Falha na leitura”.
Causa Raiz: O script Python do backend (/scripts/leitura_dht.py) não encontrou o sensor na porta GPIO configurada. A biblioteca entra em loop de retentativas.
Resolução: Verifique a integridade física do fio de dados (Pino 08). Garanta que no arquivo /scripts/leitura_dht.py a variável está apontada para a porta correta: PIN = 14. - Sintoma B: A válvula solenóide abre sozinha ao inicializar o Raspberry Pi (Boot).
Causa Raiz: Jumper do módulo de Relé está na posição Low Level Trigger (L) ou script invertido.
Resolução: Movimentar fisicamente o jumper da placa do relé HW-803 para os pinos “H”. Garantir que no script /scripts/rele_loop.py a diretiva seja: active_high=True. - Sintoma C: Ouve-se o “clique” do relé, a luz verde acende, mas não sai névoa/água.
Causa Raiz 1 (Elétrica): A fonte de 12V está fora da tomada ou desarmou.
Causa Raiz 2 (Hidráulica): Válvula solenóide está instalada invertida.
Resolução: Verificar a tensão com multímetro nos bornes da válvula. Inverter fisicamente a válvula na mangueira seguindo a seta indicativa de fluxo d’água no plástico do corpo da solenóide.
8. Manutenção Preventiva e Backups
- Limpeza Hidráulica (Semestral): Inspecionar e limpar o filtro tipo “Y” na entrada da solenóide. Desobstruir os bicos aspersores/nebulizadores deixando-os de molho em solução de limpeza anticalcária (vinagre/água morna) para manter a pressurização da névoa uniforme.
- Limpeza Lógica (Diária Automática): O job cron /scripts/saneamento_csv.sh já purga dados históricos do arquivo /etc/grafana/csv/temperatura.csv superiores a 90 dias para evitar esgotamento de inodes ou espaço em disco.
- Backup Físico (Anual): Recomenda-se realizar uma imagem (clone bit a bit) do cartão MicroSD funcional usando ferramentas como dd (Linux) ou BalenaEtcher para garantir a retomada do serviço em minutos em caso de falha do cartão.





