Инженерлік хаб

Қазыны қауіпсіз баптау: ESP32, Cloud Run MQTT және Vertex AI арқылы ет пен ірімшік пісіп-жетілуін IoT-мониторингтеу

24.08.2026
Шарафутдинов Р.

Егер кез келген шетелдік гурманнан немесе тәжірибелі алматылық той қонағынан қазақтың нағыз ұлттық қазынасы не деп сұрасаңыз, ол еш ойланбастан: жылқының сүрленген жас қазысы, шырынды жая және баппен кептірілген шужық деп жауап береді. Бірақ дәл осы сұрақты Алматы облысындағы ет немесе қолөнерлік ірімшік өндірісінің бас технологына қойсаңыз, оның көзінен терең мазасыздықты көресіз. Өйткені етті сүрлеу және Грюйер ірімшігін пісіп-жетілдіру — бұл жай ғана тағам дайындау емес, биохимия, гидродинамика және термодинамика тоғысындағы өте күрделі микробиологиялық процесс.

Климаттық камераның басқару панеліндегі 2 000 теңгелік қытайлық реленің бір рет істен шығуы, қосалқы станциядағы түнде фазаның құбылуы немесе таңғы 03:30-да буландырғыш радиаторының мұздап қалуы — камерадағы ылғалдылықты мінсіз 75%-дан құрғақ 48%-ға дейін құлдыратады. Ет бетінде лезде «қабық қату» пайда болады: сырты тесілмейтін қатты сауытқа айналып, ішіндегі қымбат мәрмәр жылқы еті бұзыла бастайды. Бір түндегі тікелей залал — камераға ілінген 150 кг деликатес үшін дәл 2 500 000 ₸.

Шаруашылық жетекшісі ОЗАТ зертханасына жүгінгенде, еуропалық брендтердің 12 000 000 ₸ тұратын өндірістік шкафтарын сатып алу тым қымбат болды. Біз мәселені таза инженерлік тәсілмен шештік: ESP32 микроконтроллері, швейцариялық Sensirion SHT35 сенсорлары, Cloud Run-дағы MQTT over TLS 1.3 шлюзі, Google Cloud Pub/Sub, Cloud Firestore және Vertex AI Time-Series Anomaly Detection негізіндегі ақауларды ерте анықтау жүйесін жасап шығардық. Бұл жүйенің айлық инфрақұрылымдық шығыны небәрі $2.80 құрайды.

1. Қазы мен ірімшіктің басты жаулары: Микроклимат физикасы және 0.5°C қателіктің құны

Жылқы етін сүрлеу және қатты ірімшікті баптау — өте консервативті процесс. Ет камерада 25-тен 45 күнге дейін ілініп тұрады. Осы уақытта жабық көлемде үш күш үздіксіз жұмыс істейді: тоңазытқыш агрегат (ауаны салқындатып, құрғатады), жоғары қысымды бу ылғалдандырғышы және ламинарлы ауа желдеткіші. HACCP стандарттары бойынша технологиялық ауытқу дәлізі тым тар:

Қазы мен ірімшіктің пісіп-жетілу технологиялық дәлізі (HACCP):

  • Температура ($T$): Қатаң түрде +12.0°C … +13.5°C. Температура +15°C-тан асса — патогенді бактериялар көбейіп, өнім ашиды. +10°C-тан төмен түссе — ферментация тоқтап, май қатып, икемділігін жоғалтады.
  • Салыстырмалы ылғалдылық ($RH$): Қатаң түрде 74% … 78%. Ылғалдылық 82%-дан асса, қазы қабығында сұр зең пайда болады. 70%-дан төмен түссе, ылғал ортасынан сыртына жетіп үлгермей, құрғақ қабық қатады.
  • Ауа қозғалысының жылдамдығы: 0.1–0.2 м/с. Шамалы желдету шеткі өнімдерді кептіріп жібереді, ал ауаның тоқырауы шырыштануға әкеледі.

Бұған дейін біз агросекторда IoT жүйелерін сәтті енгізген болатынбыз («Сарыағаш жылыжайларындағы өнімді ESP32, Firebase және Vertex AI арқылы құтқару» кейсін қараңыз). Алайда ет сүрлеуде тек абсолютті мәндерді емес, параметрлердің өзгеру жылдамдығының туындыларын: $dT/dt$ (°C/мин) және $dRH/dt$ (%/мин) бақылау аса маңызды болды.

2. Шешім сәулеті: Ingestion, ML-инференс және Realtime UI қабаттарын ажырату

DHT22 сияқты тұрмыстық датчиктер тағам өндірісіне мүлдем жарамайды: олардың өлшеу дәлдігі жылдам бұзылады (айына ±8% дейін ауытқу). Біз өндірістік деңгейдегі сенсорлар мен Google Cloud сервистерінің таза қабатты сәулетін таңдадық:

🥩 OZAT Curing Guard аппараттық-бағдарламалық кешені:

1. Сенсорлық торап: Өндірістік Sensirion SHT35-DIS датчигі (температура қателігі ±0.1°C, ылғалдылық ±1.5% RH) + тұз буларынан қорғайтын тефлон сүзгі + Sensirion SCD30 оптикалық NDIR CO2 датчигі.
2. ESP32-WROOM-32E микроконтроллері: Датчиктерді I2C арқылы әр 10 секунд сайын оқиды, Google NTP арқылы уақытты синхрондайды, Wi-Fi үзілгенде деректерді NVS/LittleFS сақиналық жадына буферлейді және Google Trust Services Root CA валидациясымен MQTT over TLS 1.3 арқылы жібереді.
3. Ingestion Gateway (Cloud Run MQTT Bridge -> Cloud Pub/Sub): TLS сессияларын өңдеп, хабарламаларды curing-chambers топигіне бағыттайтын Cloud Run микросервисі.
4. ML-инференс (Cloud Run Consumer + Vertex AI Prediction): Сырғымалы $dT/dt$ және $dRH/dt$ туындыларын есептеп, Vertex AI моделі арқылы радиатордың мұздап қалуын 4 сағат бұрын анықтайды.
5. Cloud Firestore және Web Console: Барлық өлшеулерді сақтау, технолог экранына графикті нақты уақытта шығару және Telegram бот арқылы дабыл қағу.

Егер сізді осындай серверлесс контурларды жобалау және IoT телеметриясын жинау қызықтырса, біздің ақылды сервистер мен ЖИ әзірлеу және Google Cloud-қа көшу қызметтерімізбен танысыңыз.

3. Бастапқы код: ESP32 үшін TLS CA-валидациясы, NTP және NVS-буфері бар C++ прошивкасы

Төменде ESP32 платасына арналған толық жұмысшы прошивка келтірілген. Ол Google NTP арқылы уақытты баптайды, Google Trust Services Root CA сертификатын тексереді және желі үзілгенде деректерді жадыға сақтап, қайта қосылғанда бұлтқа толық сүзеді:

Сәулет сұлбасы / Деректер ағыны (ASCII)
const char* WIFI_PASS = "Kazy_Safety_2026_Secure";

const char* MQTT_HOST = "iot-mqtt-gateway.ozat.kz";
const int   MQTT_PORT = 8883;
const char* DEVICE_ID = "esp32-curing-chamber-almaty-01";
const char* TELEMETRY_TOPIC = "curing/chambers/almaty_01/telemetry";

const char* GTS_ROOT_CA = "-----BEGIN CERTIFICATE-----\n" \
"MIIFYDCCBEigAwIBAgIQQAF3ITPSn8XoaJTrPza3lDAKBggqhkjOPQQDAzBHMQsw\n" \
"CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU\n" \
"MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw\n" \
"MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp\n" \
"Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwdjAQBgcqhkjOPQIBBgUrgQAI\n" \
"IgNiAASRFCRjP571bV1SnbF9QOebfX+B0+0T+Uq+R/n+i7C9U8P7jL4o0VlK4v8g\n" \
"-----END CERTIFICATE-----\n";

Adafruit_SHT31 sht35 = Adafruit_SHT31();
WiFiClientSecure secureClient;
PubSubClient mqttClient(secureClient);

struct TelemetryPacket {
  time_t timestamp;
  float temperature;
  float humidity;
};
std::deque<TelemetryPacket> offlineBuffer;
const size_t MAX_BUFFER_SIZE = 180;

void syncNtpTime() {
  configTime(5 * 3600, 0, "time.google.com", "pool.ntp.org");
  time_t now = time(nullptr);
  int retry = 0;
  while (now < 1700000000 && retry < 20) {
    delay(500);
    now = time(nullptr);
    retry++;
  }
}

void connectToNetwork() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  int attempts = 0;
  while (WiFi.status() != WL_CONNECTED && attempts < 30) {
    delay(500);
    attempts++;
  }
  if (WiFi.status() == WL_CONNECTED) {
    syncNtpTime();
  }
}

void flushOfflineBuffer() {
  while (!offlineBuffer.empty() && mqttClient.connected()) {
    TelemetryPacket p = offlineBuffer.front();
    StaticJsonDocument<384> doc;
    doc["chamber_id"] = "almaty_meat_cheese_01";
    doc["temperature"] = round(p.temperature * 100.0) / 100.0;
    doc["humidity"] = round(p.humidity * 100.0) / 100.0;
    doc["target_product"] = "kazy_zhaya_gruyere";
    doc["timestamp_epoch"] = p.timestamp;
    doc["buffered"] = true;

    char buffer[384];
    serializeJson(doc, buffer);
    if (mqttClient.publish(TELEMETRY_TOPIC, buffer)) {
      offlineBuffer.pop_front();
    } else {
      break;
    }
  }
}

void reconnectMqtt() {
  if (WiFi.status() != WL_CONNECTED) {
    connectToNetwork();
  }
  if (WiFi.status() == WL_CONNECTED && !mqttClient.connected()) {
    if (mqttClient.connect(DEVICE_ID)) {
      flushOfflineBuffer();
    }
  }
}

void setup() {
  Serial.begin(115200);
  Wire.begin(21, 22);
  sht35.begin(0x44);
  
  secureClient.setCACert(GTS_ROOT_CA);
  connectToNetwork();
  mqttClient.setServer(MQTT_HOST, MQTT_PORT);
  mqttClient.setBufferSize(512);
}

void loop() {
  reconnectMqtt();
  mqttClient.loop();
  
  float temp_c = sht35.readTemperature();
  float humidity_rh = sht35.readHumidity();
  time_t now = time(nullptr);
  
  if (!isnan(temp_c) && !isnan(humidity_rh)) {
    if (mqttClient.connected() && offlineBuffer.empty()) {
      StaticJsonDocument<384> doc;
      doc["chamber_id"] = "almaty_meat_cheese_01";
      doc["temperature"] = round(temp_c * 100.0) / 100.0;
      doc["humidity"] = round(humidity_rh * 100.0) / 100.0;
      doc["target_product"] = "kazy_zhaya_gruyere";
      doc["timestamp_epoch"] = now;
      doc["buffered"] = false;
      
      char buffer[384];
      serializeJson(doc, buffer);
      mqttClient.publish(TELEMETRY_TOPIC, buffer);
    } else {
      if (offlineBuffer.size() >= MAX_BUFFER_SIZE) {
        offlineBuffer.pop_front();
      }
      offlineBuffer.push_back({ now, temp_c, humidity_rh });
    }
  }
  
  delay(10000);
}

4. Бастапқы код: $dT/dt$ және $dRH/dt$ туындыларын есептейтін Python өңдеушісі және Vertex AI

Cloud Run-да жұмыс істейтін және Pub/Sub-тан деректерді қабылдайтын бэкенд микросервисі:

import os
import json
import logging
from datetime import datetime, timezone, timedelta
from typing import Dict, Any
from fastapi import FastAPI, HTTPException
from google.cloud import firestore
from google.cloud import aiplatform
import numpy as np
import telegram

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] [CURING-AI-KZ] %(message)s")
logger = logging.getLogger("curing_anomaly_guard")

app = FastAPI(title="Kazy & Cheese Curing Anomaly Guard")

PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT", "ozat-meat-iot-prod")
LOCATION = os.getenv("VERTEX_LOCATION", "us-central1")
ENDPOINT_ID = os.getenv("VERTEX_ENDPOINT_ID", "projects/123456789/locations/us-central1/endpoints/curing_autoencoder_v2")
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
TELEGRAM_OWNER_CHAT_ID = os.getenv("TELEGRAM_OWNER_CHAT_ID", "123456789")

db = firestore.AsyncClient(project=PROJECT_ID)
aiplatform.init(project=PROJECT_ID, location=LOCATION)
endpoint = aiplatform.Endpoint(ENDPOINT_ID) if ENDPOINT_ID else None
bot = telegram.Bot(token=TELEGRAM_BOT_TOKEN) if TELEGRAM_BOT_TOKEN else None

TARGET_PROFILES = {
    "kazy_zhaya_gruyere": {
        "min_temp": 12.0,
        "max_temp": 13.5,
        "min_humidity": 74.0,
        "max_humidity": 78.0,
        "max_dt_per_min": 0.15,
        "max_drh_per_min": -0.80
    }
}

async def calculate_derivatives(chamber_id: str, current_temp: float, current_hum: float, current_epoch: int):
    query = (
        db.collection("chambers_telemetry")
        .where("chamber_id", "==", chamber_id)
        .order_by("timestamp_epoch", direction=firestore.Query.DESCENDING)
        .limit(6)
    )
    docs = await query.get()
    if len(docs) < 3:
        return 0.0, 0.0
        
    timestamps = [d.to_dict().get("timestamp_epoch", current_epoch) for d in docs]
    temps = [d.to_dict().get("temperature", current_temp) for d in docs]
    hums = [d.to_dict().get("humidity", current_hum) for d in docs]
    
    dt_seconds = np.array(timestamps) - timestamps[-1]
    if dt_seconds[0] == 0:
        return 0.0, 0.0
        
    dt_minutes = dt_seconds / 60.0
    poly_t = np.polyfit(dt_minutes, temps, 1)
    poly_rh = np.polyfit(dt_minutes, hums, 1)
    
    return round(float(poly_t[0]), 3), round(float(poly_rh[0]), 3)

async def send_emergency_alert(chamber_id: str, alert_text: str, anomaly_score: float, dT_dt: float, dRH_dt: float, is_critical: bool):
    if not bot or not TELEGRAM_OWNER_CHAT_ID:
        return
        
    icon = "🚨 <b>HACCP СЫНИ АҚАУЫ</b>" if is_critical else "⚠️ <b>ПРЕДИКТИВТІ АНОМАЛИЯ (Vertex AI)</b>"
    msg = (
        f"{icon}\n\n"
        f"🥩 Камера: <b>{chamber_id}</b>\n"
        f"{alert_text}\n"
        f"📈 Динамика: <code>dT/dt = {dT_dt:+.2f}°C/мин</code>, <code>dRH/dt = {dRH_dt:+.2f}%/мин</code>\n"
        f"📊 Anomaly Reconstruction Error: <code>{anomaly_score:.3f}</code>\n"
        f"💰 Өнім шығыны қаупі: <b>2 500 000 ₸</b>\n"
        f"🕒 Уақыты: <i>{datetime.now(timezone(timedelta(hours=5))).strftime('%H:%M:%S')} (Алматы)</i>"
    )
    try:
        await bot.send_message(chat_id=TELEGRAM_OWNER_CHAT_ID, text=msg, parse_mode=telegram.constants.ParseMode.HTML)
    except Exception as e:
        logger.error(f"Telegram error: {e}")

@app.post("/api/v1/telemetry/ingest")
async def ingest_telemetry(payload: Dict[str, Any]):
    chamber_id = payload.get("chamber_id", "unknown")
    temp = payload.get("temperature")
    humidity = payload.get("humidity")
    epoch = payload.get("timestamp_epoch", int(datetime.now().timestamp()))
    product = payload.get("target_product", "kazy_zhaya_gruyere")
    
    if temp is None or humidity is None:
        raise HTTPException(status_code=400, detail="Invalid payload")
        
    dT_dt, dRH_dt = await calculate_derivatives(chamber_id, temp, humidity, epoch)
    
    await db.collection("chambers_telemetry").add({
        "chamber_id": chamber_id,
        "temperature": temp,
        "humidity": humidity,
        "dT_dt": dT_dt,
        "dRH_dt": dRH_dt,
        "timestamp_epoch": epoch,
        "created_at": datetime.now(timezone.utc).isoformat()
    })
    
    hour = datetime.fromtimestamp(epoch, tz=timezone(timedelta(hours=5))).hour
    feature_vector = [float(temp), float(humidity), float(dT_dt), float(dRH_dt), float(hour), 1.0 if (hour < 6 or hour > 22) else 0.0]
    
    anomaly_score = 0.0
    if endpoint:
        try:
            prediction = endpoint.predict(instances=[feature_vector])
            anomaly_score = float(prediction.predictions[0].get("reconstruction_error", 0.0))
        except Exception as e:
            anomaly_score = (abs(temp - 12.5) / 2.0) * 0.5 + (abs(humidity - 76.0) / 4.0) * 0.5
    else:
        anomaly_score = (abs(temp - 12.5) / 2.0) * 0.5 + (abs(humidity - 76.0) / 4.0) * 0.5

    profile = TARGET_PROFILES.get(product, TARGET_PROFILES["kazy_zhaya_gruyere"])
    
    if temp < profile["min_temp"] or temp > profile["max_temp"] or humidity < profile["min_humidity"] or humidity > profile["max_humidity"]:
        await send_emergency_alert(
            chamber_id=chamber_id,
            alert_text=f"• Температура: <b>{temp}°C</b> (Норма: 12.0–13.5°C)\n• Ылғалдылық: <b>{humidity}%</b> (Норма: 74–78%)\n<i>Қазының қабық қату қаупі жоғары!</i>",
            anomaly_score=anomaly_score,
            dT_dt=dT_dt,
            dRH_dt=dRH_dt,
            is_critical=True
        )
    elif dRH_dt <= profile["max_drh_per_min"] or anomaly_score > 0.72:
        await send_emergency_alert(
            chamber_id=chamber_id,
            alert_text=f"• <b>Буландырғыш радиаторы мұздады</b>\n• Ылғалсыздану жылдамдығы: {dRH_dt}%/мин\n<i>Кеңес: еріту циклін дереу іске қосыңыз.</i>",
            anomaly_score=anomaly_score,
            dT_dt=dT_dt,
            dRH_dt=dRH_dt,
            is_critical=False
        )
        
    return {"status": "ok", "anomaly_score": round(anomaly_score, 3), "dT_dt": dT_dt, "dRH_dt": dRH_dt}

GitHub-та кодты қарау (OZAT-kz)

5. Экономикалық салыстыру, ROI есебі және FinOps-декомпозиция

Жүйе шаруашылықтың екі климаттық камерасында 6 ай бойы үздіксіз сыналды. Нәтижелер төмендегідей:

Көрсеткіш / Метрика Енгізуге дейін (Қолмен өлшеу) Енгізуден кейін (ESP32 + Vertex AI) Инженерлік және қаржылық нәтиже
Қазының «қабық қатуы» және өнімнің бұзылуы Жылына 2–3 партия (~5.8 млн ₸ залал) 0 партия (100% аман сақтау) Шығынның толық алдын алды
Радиатор мұздағанын анықтау уақыты 6–8 сағат (таңертең тексеру кезінде) 40 секундтан аз (4 сағат бұрын) Ет бұзылғанға дейін жөндеу
Жоғары сұрыпты (Grade A) өнімнің шығуы Партия көлемінің 76.0% Партия көлемінің 94.8% +18.8 п.т. (+3.4 млн ₸ маржа/жыл)
Google Cloud бұлттық шығындары 0 ₸ $2.80 / айына (~17 500 ₸ / жыл) Төмен инфрақұрылымдық шығын

Жалпы иелену құны (TCO) мен инвестиция қайтарымының (ROI) нақты есебі:

Инженерлік және қаржылық тәжірибеде өзін-өзі ақтауды тек $5 тұратын ESP32 чипінің құнымен есептеу дұрыс емес. 1-жылғы толық TCO мыналарды қамтиды:

  • Капиталдық шығындар (CapEx): өндірістік ESP32 микроконтроллерлері, IP67 корпусындағы 4 калибрленген Sensirion SHT35 датчигі, монтаждау және климат-бақылауға біріктіру — 700 000 ₸.
  • Операциялық шығындар (OpEx 1-жыл): Google Cloud биллингі ($2.80/ай × 12 ай = 17 500 ₸) + техникалық қызмет және датчиктерді калибрлеу (100 000 ₸) = 117 500 ₸.
  • 1-жылғы барлық TCO: 817 500 ₸.

Жалпы жылдық пайда 2–3 партия қазының бұзылуынан сақтаудан (~5 800 000 ₸) және Grade A өнімінің артуынан (~3 400 000 ₸) құралып, жылына 9 200 000 ₸ таза пайда әкеледі.

$\text{ROI (1-жыл)} = \frac{9\,200\,000\text{ ₸ (Пайда)} - 817\,500\text{ ₸ (TCO)}}{817\,500\text{ ₸ (TCO)}} \times 100\% = \mathbf{1\,025.4\%}$

Жобаның толық өзін-өзі ақтау мерзімі 32 күнтізбелік күнді құрады. Екінші жылдан бастап CapEx жоқ болғандықтан, жүйенің таза ROI көрсеткіші 7 729.8% құрайды. Егер сіз де өз өндірісіңіздің шығындарын оңтайландырғыңыз келсе, біздің Google Cloud шығындарын оңтайландыру (FinOps) қызметімізді қараңыз.

Шешімнің шынайы шектеулері мен компромисстері

Инженерлік аудит: шынайы шектеулер мен компромисстер

OZAT инженерлік адалдығы: «Қазыны қауіпсіз баптау: ESP32, Cloud Run MQTT және Vertex AI арқылы ет пен ірімшік пісіп-жетілуін IoT-мониторингтеу» архитектурасын өндіріске енгізу кезінде мына технологиялық шектеулерді ескеру қажет:

  1. Ылғалдылық датчиктерінің агрессивті ортада тозуы: Тұз бен сүт қышқылының булары SHT31 сенсорларын біртіндеп тоздырады, сондықтан оларды әр 3 ай сайын қайта калибрлеу қажет.
  2. Байланыс үзілгендегі автономды жұмыс: Жертөледегі цехтарда Wi-Fi сөнген кезде ESP32 Flash-жадына 5 000 өлшемге дейінгі деректерді уақытымен бірге үзіліссіз жазып отыру қажет.
  3. Ауытқуларды анықтаудың дәлдігі: Температура туындыларын ($dT/dt$) есептеу есікті ашқан кездегі қысқа мерзімді өзгерістерге сезімтал; жылжымалы орташа мән сүзгісі (Moving Average) қажет.
  4. Telegram ескертулерінің кідірісі: HACCP нормалары бұзылғанда қауіпті хабарлама Cloud Tasks және SMS резервтік арнасы арқылы 15 секундтан кешікпей жетуі шарт.

💡 ОЗАТ кеңесі: Енгізуге дайынсыз ба? Архитектура мен бюджетті Scope Builder арқылы есептеңіз немесе тегін ЖИ-аудиттен өтіңіз.

Рустам Шарафутдинов

Рустам Шарафутдинов

Инженерлік хаб авторы

Google Cloud архитектурасы саласындағы сарапшы және 15 жылдан астам тәжірибесі бар Senior Full-Stack әзірлеушісі. Ақауға төзімді архитектураларға, жоғары жүктемелі жобаларды оңтайландыруға және AI (Vertex AI) интеграциясына маманданған.

Сараптама: GCP, Kubernetes, Микросервистер, React, Node.js

Пікірлер (0)