
Introduzione
Il monitoraggio HACCP non è una questione di estetica, ma di integrità del dato. In un ambiente industriale o di conservazione alimentare, un “buco” nei log non è un’opzione. Se il database non riceve dati perché il WiFi è caduto o il clock interno è andato in deriva, il sistema è fallato alla base.
In questo post analizziamo un’architettura di monitoraggio professionale basata su tre pilastri di base:
Edge Layer (ESP32): Gestisce l’acquisizione e la trasmissione. L’uso del chip DS3231 via I2C garantisce che ogni pacchetto dati abbia un timestamp preciso al secondo, indipendentemente dalla connessione NTP.
Network Layer (HTTP/JSON): Il trasporto avviene tramite POST serializzate in JSON verso un endpoint Flask dedicato.
Storage & Visualization Layer (Raspberry Pi): Un server Flask riceve i dati, li scrive su un database SQLite e genera un’interfaccia di controllo in tempo reale con grafici dinamici (Chart.js) e funzioni di export per i report mensili obbligatori.
Non usiamo delay bloccanti. Il sistema lavora su cicli di polling definiti, garantendo la reattività del watchdog e la continuità del servizio. Se cerchi un giocattolo per vedere la temperatura di casa, sei nel posto sbagliato. Se ti serve un sistema che generi log pronti per un’ispezione sanitaria, continua a leggere.

Note Tecniche rapide prima di iniziare:
Il programma consta di vari “snippet” di codice, ma concettualmente è molto semplice e si appoggia su un componente hardware economico ma incredibilmente preciso e su un’architettura software ultra-semplice, che potete facilmente modificare con Gemini Thinking o ChatGPT. Questi i nodi salienti:
RTC DS3231: Indispensabile. Il timer interno dell’ESP32 drifta troppo. Usiamo la libreria RTClib per gestire il bus I2C (Pin 21/22).
Flask Backend: Configurato per girare su porta 5040. Assicurati che il firewall del Raspberry permetta l’ingresso dei pacchetti dall’IP dell’ESP32.
Persistence: SQLite è scelto per la sua natura zero-config e la facilità di backup del singolo file .db.
Licenza del programma
Questo software è rilasciato sotto licenza Robotdazero Custom Liberal.
Cosa puoi fare:
Copiare, modificare e distribuire il codice sorgente, usarlo per i tuoi progetti personali, didattici o prototipi industriali interni. Puoi montarlo e rimontarlo come meglio credi.
Cosa NON puoi fare:
Rivenderlo: Non puoi prendere questo codice (o una sua versione modificata) e venderlo come prodotto chiuso o servizio commerciale senza autorizzazione. In breve: Puoi fare quello che vuoi, basta che non provi a fare soldi con il mio lavoro. Nell’immagine in basso vedi l’output del programma con sensori simulati per brevità. Nella prossima revisione introdurremo la capacità di leggere direttamente un DS1820, un DHT11 e un sensore di pressione e umidità.

Come installare il programma
Dopo avere scaricato da GIT con il comando:
git clone git@github.com:sebadima/public.git
sudo apt update && sudo apt install -y python3-pip python3-venv sqlite3 ufw
python3 -m venv venv
source venv/bin/activate
pip install flask
chmod +x crea_tabella.sh
./crea_tabella.sh
sudo ufw allow 5040/tcp
python3 app.py
se preferisci non usare Github puoi scaricare tuuti i nostri programmi con:
Se non hai Git o non vuoi configurare chiavi SSH, scarica il nostro pacchetto "public" completo con un click.
Scarica justpaste_env.zip55555555555555555555555555555555555555555
- sqlite3 haccp_monitor.db < schema.sql
I Sorgenti per ESP32
Makefile:
all:
pio -f -c vim run
upload:
pio -f -c vim run --target upload
clean:
pio -f -c vim run --target clean
program:
pio -f -c vim run --target program
uploadfs:
pio -f -c vim run --target uploadfs
update:
pio -f -c vim update
platformio.ini
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200
lib_deps =
adafruit/RTClib @ ^2.1.3
adafruit/Adafruit BusIO @ ^1.14.5
bblanchon/ArduinoJson @ ^6.21.3
knolleary/PubSubClient @ ^2.8
SPI
Wire
main.ino (sorgente C++)
#include <WiFi.h>
#include <HTTPClient.h>
#include <Wire.h>
#include "RTClib.h"
const char* ssid = "SSID";
const char* password = "PASSWORD_WIFI";
const char* serverUrl = "http://192.168.1.153:5040/ingest";
RTC_DS3231 rtc;
unsigned long lastMillis = 0;
const long interval = 15000;
void setup() {
Serial.begin(115200);
Wire.begin(21, 22);
if (!rtc.begin()) {
Serial.println("[CRITICAL] RTC_NOT_FOUND");
while (1);
}
WiFi.begin(ssid, password);
Serial.println("[SYSTEM] START_MONITORING");
}
void loop() {
if (millis() - lastMillis >= interval) {
lastMillis = millis();
// 1. LETTURA DATI DAL TIMER E SENSORI SIMULATI
DateTime now = rtc.now();
float t1 = -18.0 + (random(-100, 100) / 100.0);
float t2 = 14.0 + (random(-100, 100) / 100.0);
float hum = 80.0 + (random(-50, 50) / 10.0);
float pres = 1013.25 + (random(-100, 100) / 100.0);
// 2. LOG SERIALE BRUTALE (Sempre attivo)
char timestamp[20];
sprintf(timestamp, "%04d-%02d-%02d %02d:%02d:%02d", now.year(), now.month(), now.day(), now.hour(), now.minute(), now.second());
Serial.print("> DATA_LOG: ");
Serial.print(timestamp);
Serial.print(" | T1: "); Serial.print(t1);
Serial.print(" | T2: "); Serial.print(t2);
Serial.print(" | HUM: "); Serial.print(hum);
Serial.print(" | PRES: "); Serial.println(pres);
// 3. INVIO A FLASK (Se WiFi disponibile)
if (WiFi.status() == WL_CONNECTED) {
HTTPClient http;
http.begin(serverUrl);
http.addHeader("Content-Type", "application/json");
String json = "{\"timestamp\":\"" + String(timestamp) + "\",";
json += "\"t1\":" + String(t1, 2) + ",";
json += "\"t2\":" + String(t2, 2) + ",";
json += "\"hum\":" + String(hum, 2) + ",";
json += "\"pres\":" + String(pres, 2) + "}";
int code = http.POST(json);
Serial.printf("[NETWORK] SEND_STATUS: HTTP_%d\n", code);
http.end();
} else {
Serial.println("[NETWORK] OFFLINE - DATA_NOT_SENT");
}
Serial.println("------------------------------------------------------------------");
}
}
I programmi del server Flask
Il file schema.sql
CREATE TABLE IF NOT EXISTS haccp_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
temp_cella_1 REAL,
temp_cella_2 REAL,
umidita_relativa REAL,
pressione_pa REAL
);
Il file app.py (sorgente flask)
import sqlite3
import csv
import io
from flask import Flask, request, jsonify, Response, render_template_string
from datetime import datetime, timedelta
app = Flask(__name__)
DB_FILE = "haccp_monitor.db"
def query_db(query, args=(), one=False):
with sqlite3.connect(DB_FILE) as conn:
conn.row_factory = sqlite3.Row
cur = conn.execute(query, args)
rv = cur.fetchall()
return (rv[0] if rv else None) if one else rv
@app.route('/ingest', methods=['POST'])
def ingest():
data = request.json
try:
ts = data.get('timestamp')
with sqlite3.connect(DB_FILE) as conn:
if ts:
conn.execute(
"INSERT INTO haccp_log (timestamp, temp_cella_1, temp_cella_2, umidita_relativa, pressione_pa) VALUES (?, ?, ?, ?, ?)",
(ts, data['t1'], data['t2'], data['hum'], data['pres'])
)
else:
conn.execute(
"INSERT INTO haccp_log (temp_cella_1, temp_cella_2, umidita_relativa, pressione_pa) VALUES (?, ?, ?, ?)",
(data['t1'], data['t2'], data['hum'], data['pres'])
)
return jsonify({"status": "OK"}), 201
except Exception as e:
return jsonify({"status": "ERROR", "msg": str(e)}), 500
@app.route('/data')
def get_data():
# Endpoint tecnico per il refresh AJAX del grafico
data = query_db("SELECT * FROM haccp_log ORDER BY timestamp DESC LIMIT 100")
return jsonify([dict(row) for row in data])
@app.route('/export/<period>')
def export_csv(period):
days = {"24h": 1, "week": 7, "month": 30}.get(period, 1)
since = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d %H:%M:%S')
rows = query_db("SELECT * FROM haccp_log WHERE timestamp > ? ORDER BY timestamp ASC", (since,))
output = io.StringIO()
writer = csv.writer(output)
writer.writerow(['ID', 'TIMESTAMP', 'TEMP_CELLA_1', 'TEMP_CELLA_2', 'UMIDITA_REL', 'PRESSIONE_PA'])
for row in rows:
writer.writerow([row['id'], row['timestamp'], row['temp_cella_1'], row['temp_cella_2'], row['umidita_relativa'], row['pressione_pa']])
return Response(output.getvalue(), mimetype="text/csv", headers={"Content-Disposition": f"attachment; filename=haccp_report_{period}.csv"})
@app.route('/')
def index():
data = query_db("SELECT * FROM haccp_log ORDER BY timestamp DESC LIMIT 100")
data_list = [dict(row) for row in data]
return render_template_string(HTML_UI, data=data_list)
HTML_UI = """
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<title>HACCP_LIVE_MONITOR</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body { background: #000; color: #0f0; font-family: 'Courier New', monospace; padding: 20px; }
.container { max-width: 1200px; margin: 0 auto; }
.header { border-bottom: 1px solid #0f0; padding-bottom: 10px; margin-bottom: 20px; }
.btns { margin-bottom: 30px; display: flex; gap: 10px; }
button { background: transparent; border: 1px solid #0f0; color: #0f0; padding: 10px 20px; cursor: pointer; font-weight: bold;}
button:hover { background: #0f0; color: #000; }
.chart-container { background: #050505; border: 1px solid #222; padding: 20px; height: 500px; position: relative; }
.status-led { color: #0f0; font-size: 0.8em; float: right; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<span class="status-led" id="sync-status">SYNC: OK</span>
<h2>[ HACCP_MONITORING_SYSTEM_v1.4 ]</h2>
<p>SISTEMA: TEMPERATURE_CORE | REFRESH: AUTO (15s)</p>
</div>
<div class="btns">
<button onclick="location.href='/export/24h'">LOG_24H</button>
<button onclick="location.href='/export/week'">LOG_SETTIMANA</button>
<button onclick="location.href='/export/month'">REPORT_MENSILE</button>
</div>
<div class="chart-container">
<canvas id="haccpChart"></canvas>
</div>
</div>
<script>
let haccpChart;
const ctx = document.getElementById('haccpChart').getContext('2d');
function initChart(initialData) {
const labels = initialData.map(r => r.timestamp).reverse();
haccpChart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [
{
label: 'Cella 1 (°C)',
data: initialData.map(r => r.temp_cella_1).reverse(),
borderColor: '#f00',
borderWidth: 2,
tension: 0.3,
pointRadius: 2
},
{
label: 'Cella 2 (°C)',
data: initialData.map(r => r.temp_cella_2).reverse(),
borderColor: '#ff0',
borderWidth: 2,
tension: 0.3,
pointRadius: 2
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: false,
scales: {
x: { ticks: { color: '#0f0', maxRotation: 45 }, grid: { color: '#111' } },
y: { ticks: { color: '#0f0' }, grid: { color: '#222' } }
},
plugins: { legend: { labels: { color: '#0f0' } } }
}
});
}
async function refreshData() {
try {
const response = await fetch('/data');
const newData = await response.json();
const labels = newData.map(r => r.timestamp).reverse();
haccpChart.data.labels = labels;
haccpChart.data.datasets[0].data = newData.map(r => r.temp_cella_1).reverse();
haccpChart.data.datasets[1].data = newData.map(r => r.temp_cella_2).reverse();
haccpChart.update('none');
document.getElementById('sync-status').innerText = "LAST_SYNC: " + new Date().toLocaleTimeString();
} catch (e) {
document.getElementById('sync-status').innerText = "SYNC: ERROR";
}
}
// Avvio
const initialData = {{ data|tojson }};
initChart(initialData);
// Refresh ogni 15 secondi (allineato al timer 0x68)
setInterval(refreshData, 15000);
</script>
</body>
</html>
"""
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5040, debug=False)

Il file /etc/systemd/system/haccp.service
[Unit]
Description=Sentinel HACCP Backend Service
After=network.target
[Service]
User=pi
WorkingDirectory=/home/pi/haccp
ExecStart=/usr/bin/python3 app.py
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
Systemd Service: Gestione Professionale del Processo
In ambiente Linux (Raspberry Pi OS), un Service File è lo standard industriale per trasformare uno script Python in un servizio di sistema resiliente. Permette al monitoraggio HACCP di essere autonomo, avviandosi al boot e ripristinandosi automaticamente dopo ogni crash.
Analisi Parametri "Field-Ready"
- 🚀 After=network.target: Garantisce che Flask parta solo quando la rete è pronta.
- 🔄 Restart=always: Monitoraggio attivo 24/7. Se il processo muore, il sistema lo resuscita.
- ⏱️ RestartSec=5: Previene il churning della CPU in caso di errori hardware persistenti.
- 🛡️ User=pi: Sicurezza prima di tutto. Il servizio opera senza privilegi di root non necessari.
Deployment Rapido (Terminale)
Esegui questi comandi atomici per rendere il monitoraggio immortale:
# 1. Creazione del file descrittore sudo nano /etc/systemd/system/haccp.service # 2. Registrazione del nuovo servizio sudo systemctl daemon-reload # 3. Abilitazione all'avvio automatico (Boot) sudo systemctl enable haccp.service # 4. Esecuzione immediata sudo systemctl start haccp.service
Strumenti di Diagnostica
| Comando | Funzione |
|---|---|
| systemctl status haccp | Verifica lo stato di salute del servizio. |
| journalctl -u haccp -f | Analisi dei log in tempo reale (STDOUT). |
Conclusioni
Implementare un sistema di monitoraggio HACCP non significa solo leggere una temperatura, ma garantire che quel dato sia immutabile, tracciabile e sempre disponibile. L’accoppiata ESP32 e Raspberry Pi, se configurata con i criteri che abbiamo visto, trasforma un ammasso di silicio in uno strumento di precisione professionale.
Punti chiave del sistema:
Affidabilità del Tempo: Senza un RTC fisico (DS3231), i tuoi log valgono zero. La deriva software è il nemico numero uno della conformità.
Resilienza della Rete: L’ESP32 logga su seriale anche se il WiFi muore. È la tua “scatola nera” in caso di crash del server.
Proprietà del Dato: I dati sono nel tuo database SQLite, non su un cloud di terzi che potrebbe sparire o chiederti un abbonamento domani.
Questo progetto è la base di partenza. Se vuoi portarlo al livello successivo (Field-Ready), il passo obbligatorio è l’ingegnerizzazione dell’hardware per proteggere i componenti dalle interferenze elettromagnetiche e dagli sbalzi di tensione. Il codice è pronto. Il database è inizializzato. Ora tocca a te alimentare il sistema e smettere di segnare le temperature a mano su fogli di carta che finiranno persi.
Sentinel Node V3.5
Il Notaio Digitale che rende obsoleta la SD-Card
Zero latenza, scritture infinite. Il dato è blindato anche se salta la corrente.
Ripristino fisico esterno. Se il firmware si pianta, il sistema reagisce.
Time-stamping deterministico. Indipendente dai server NTP esterni.
Diodi P6KE15A e Schottky. Sopravvive ai picchi industriali (14.89V testati).