immagine copertina del post


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:

  1. RTC DS3231: Indispensabile. Il timer interno dell’ESP32 drifta troppo. Usiamo la libreria RTClib per gestire il bus I2C (Pin 21/22).

  2. Flask Backend: Configurato per girare su porta 5040. Assicurati che il firewall del Raspberry permetta l’ingresso dei pacchetti dall’IP dell’ESP32.

  3. 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:

Download Rapido

Se non hai Git o non vuoi configurare chiavi SSH, scarica il nostro pacchetto "public" completo con un click.

Scarica justpaste_env.zip

55555555555555555555555555555555555555555

  1. 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

ComandoFunzione
systemctl status haccpVerifica lo stato di salute del servizio.
journalctl -u haccp -fAnalisi dei log in tempo reale (STDOUT).
Sentinel Node Infrastructure - Rev. 2026

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.

MISSION CRITICAL

Sentinel Node V3.5

Il Notaio Digitale che rende obsoleta la SD-Card

STORAGE FRAM

Zero latenza, scritture infinite. Il dato è blindato anche se salta la corrente.

HARDWARE WATCHDOG

Ripristino fisico esterno. Se il firmware si pianta, il sistema reagisce.

RTC DEDICATO

Time-stamping deterministico. Indipendente dai server NTP esterni.

SURGE PROTECTION

Diodi P6KE15A e Schottky. Sopravvive ai picchi industriali (14.89V testati).