Untitled

 avatar
unknown
plain_text
19 days ago
83 kB
7
Indexable
#include <WiFi.h>
#include <esp_wifi.h>
#include <esp_mac.h>
#include <stdio.h>
#include <math.h>
#include <DNSServer.h>
#include <WebServer.h>
#include <WebSocketsServer.h>
#include <Update.h>
#include <Preferences.h>

enum AnaLockReason : uint8_t { LOCK_NONE = 0, LOCK_PAT = 1, LOCK_CLOG = 2 };

// --- GİRİŞLER ---
const int PIN_HALL_ADC = 32;      // analog Hall (pompa mıknatısı)
const int PIN_FLOW = 25;
/** Reset butonu (GPIO18): mandal = pompa/arıza kilidi; kilitliyken bu düğmeye 3 sn basınca mandal açılır. */
const int PIN_RESET = 18;

static const float ADC_VREF = 3.3f;
static const int ADC_MAX = 4095;
// Hall için "boşta" ADC (web: AKIM:ZERO — pompa dururken ölçün)
static int akimZeroAdc = 2048;

// Bazı Hall modüllerinde ADC yönü ters olabilir — web'den (inv32)
static bool invertCcaAdc = false;

volatile uint32_t flowPulseCounter = 0;
static uint32_t flowPulseSnapPrev = 0;
unsigned long lastFlowPulseMs = 0;
static uint32_t flowPulseTotalLastSec = 0;
static unsigned long flowRateTickMs = 0;
int flowPps = 0;
static int flowLastWindowDelta = 0;
/** Kısa pencerede (dozaj gibi) pals yoğunluğu — sn başına normalize; tıkanma eşiği ile birleştirilir. */
static int flowPpsDozKisa = 0;
static const unsigned long FLOW_DOZ_PENCERE_MS = 300UL; // ~0.3 sn doz patlaması (patlak izleme)

/** Dozaj: 0→darbe→0 arası toplam darbe; ekran + tıkanma. 2 sn 0 sonra silinir. */
static const unsigned long DOSE_BURST_BITIS_MS = 450UL;
static const unsigned long DOSE_EKRAN_TUT_MS = 2000UL;
static int doseBurstToplam = 0;
static int doseSonGoster = 0;
static unsigned long doseSonDarbeMs = 0;
static unsigned long doseEkranSilMs = 0;
static uint32_t dosePulseSnap = 0;

void IRAM_ATTR isrFlowPulse() { flowPulseCounter++; }

static void doseAkisGuncelle() {
  uint32_t cnt;
  noInterrupts();
  cnt = flowPulseCounter;
  interrupts();
  if (dosePulseSnap == 0) dosePulseSnap = cnt;
  uint32_t d = (cnt >= dosePulseSnap) ? (cnt - dosePulseSnap) : 0;
  dosePulseSnap = cnt;
  unsigned long now = millis();
  if (d > 0) {
    doseBurstToplam += (int)d;
    doseSonDarbeMs = now;
    doseEkranSilMs = 0;
  }
  bool burstDevam =
      doseBurstToplam > 0 && (now - doseSonDarbeMs) < DOSE_BURST_BITIS_MS;
  if (doseBurstToplam > 0 && !burstDevam) {
    doseSonGoster = doseBurstToplam;
    doseBurstToplam = 0;
    doseEkranSilMs = now + DOSE_EKRAN_TUT_MS;
  }
  if (doseSonGoster > 0 && doseBurstToplam == 0 && doseEkranSilMs > 0 &&
      now >= doseEkranSilMs) {
    doseSonGoster = 0;
    doseEkranSilMs = 0;
  }
}

static int doseAkisGoster() {
  return doseBurstToplam > 0 ? doseBurstToplam : doseSonGoster;
}

static bool clogAkisVar() { return doseAkisGoster() > 0; }

static int hallZeroOlcAdc() {
  const int N = 400;
  long sum = 0;
  for (int i = 0; i < N; i++) {
    sum += analogRead(PIN_HALL_ADC);
    delayMicroseconds(200);
  }
  return (int)(sum / N);
}

/** Hall ADC — median 5 */
static int okuHallMedian5() {
  const int N = 5;
  int v[N];
  for (int i = 0; i < N; i++) {
    int raw = analogRead(PIN_HALL_ADC);
    if (raw < 0) raw = 0;
    if (raw > ADC_MAX) raw = ADC_MAX;
    v[i] = invertCcaAdc ? (ADC_MAX - raw) : raw;
    if (i + 1 < N) delayMicroseconds(8);
  }
  for (int i = 1; i < N; i++) {
    int t = v[i];
    int j = i;
    while (j > 0 && v[j - 1] > t) {
      v[j] = v[j - 1];
      j--;
    }
    v[j] = t;
  }
  return v[N / 2];
}

// --- ÇIKIŞLAR (NC röle: kesme / alarm için genelde röle enerji alır = HIGH) ---
const int ROLE_POMPA_ANA = 19;
const int ROLE_AKIS_TIKANIK = 14;
const int ROLE_BORU_PAT = 22;
const int ROLE_POMPA_ARIZA = 23;
/** GPIO14 tıkanma rölesi ters: arıza=LOW (kapalı), normal=HIGH (açık) */
const int CLOG_RELAY_NORMAL = HIGH;
const int CLOG_RELAY_FAULT = LOW;

/** Tek durum LED — arıza yok: 1 sn açık / 1 sn kapalı; arıza: hızlı yanıp sönme */
const int LED_SISTEM = 13;
static const unsigned long LED_NORMAL_YARIM_PERIYOT_MS = 1000UL;
static const unsigned long LED_FAULT_YARIM_PERIYOT_MS = 100UL;

// Pompa ana hattı (NC röle modülü): normalde röle bırakık; boru patlak / tıkanma mandalı → röle çeker (pompa kesilir)
const int PUMP_CUT_LEVEL = HIGH;
const int PUMP_RUN_LEVEL = LOW;

/** Mandal (kilit): patlak/tıkanma sonrası ana pompa kesilir, NVS'te kalır. Açmak = reset butonuna 3 sn. Patlak: 10 dk içinde 3×30 sn uyarı. Tıkanma: anında mandal, sınırsız (yalnız reset). */
static bool anaRoleMandalli = false;
static AnaLockReason anaLockReason = LOCK_NONE;
static unsigned long anaLockSinceMs = 0;
static const unsigned long RESET_HOLD_MS = 3000UL;
static unsigned long resetPressStartMs = 0;
static bool resetAwaitRelease = false;

// Patlak uyarısı: 3 eşik aşımında 30 sn uyarı (ana pompa kesilmez); 10 dk içinde 3 uyarı → ana mandal
static const unsigned long PAT_UYARI_SURE_MS = 30000UL;
static const unsigned long PAT_UYARI_PENCERE_MS = 600000UL;
static const int PAT_UYARI_MANDAL_ADET = 3;
static const int PAT_UYARI_MAX_KAYIT = 6;
static unsigned long patUyariZamanlar[PAT_UYARI_MAX_KAYIT];
static uint8_t patUyariKayit = 0;
static bool patUyariModu = false;
static unsigned long patUyariBaslaMs = 0;
int relPompaSaniye = 10;
/** Pompa arıza rölesi açık kalma süresi (sn) üst sınırı — en fazla 12 saat. */
static const int REL_SANIYE_MAX = 12 * 3600;

// Pompa arızası: belirlenen süre içinde Hall sapması üst eşiği geçmezse alarm (kalibrasyon ADC ile)
int pompaAkimUstLimitMa = 500; // aslında Hall min. sapma ADC (kalibrasyonla)
int pompaArizaSureDk = 1;      // dk (varsayılan)
bool alarmPompaAktif = false;
unsigned long alarmPompaBasla = 0;
int kalanPompaSn = 0;
unsigned long pompaWindowStart = 0;

bool alarmStuckAktif = false;
unsigned long alarmStuckBasla = 0;
int kalanStuckSn = 0;

unsigned long hallSonYuksekMs = 0;

// Pompa arıza kalibrasyon sihirbazı (tıkanma adımları gibi)
// STOP -> DUR -> TEST -> SHOW_AVG -> ASK_ADD -> OBS -> DONE
enum PumpWizardPhase : uint8_t { PZ_STOP = 0, PZ_DUR = 1, PZ_TEST = 2, PZ_SHOW_AVG = 3, PZ_ASK_ADD = 4, PZ_OBS = 5, PZ_DONE = 6 };
uint8_t pumpPh = PZ_STOP;
unsigned long pumpTestEndMs = 0;
unsigned long pumpTestDurationMs = 0;
unsigned long pumpLastSampleMs = 0;
long pumpSumAbsMa = 0;
long pumpSampleCount = 0;
int pumpAvgAbsMa = 0;
int pumpAddMa = 0;
int pumpRunAvgAbsMa = 0;
int pumpCurAbsMa = 0;
const byte DNS_PORT = 53;
IPAddress apIP(192, 168, 4, 1);
DNSServer dnsServer;
WebServer server(80);
WebSocketsServer webSocket = WebSocketsServer(81);
Preferences hafiza;

/** Ana sayfa HTML bir kez üretilir; her HTTP isteğinde getHTML() çağırmak AP'yi ve WS'yi kilitleyebilir. */
String g_apHtmlCache;
static char g_apSsid[32];
/** Sahada birden fazla ESP32 varsa kanal çakışmasını azaltmak için sabit kanal + az istemci. */
static const uint8_t WIFI_AP_CHANNEL = 6;
static const uint8_t WIFI_AP_MAX_CONN = 2;
static const unsigned long WIFI_AP_HEALTH_MS = 15000UL;
static unsigned long wifiApHealthMs = 0;
/** WebSocket içinde uzun ADC ölçümü WiFi yığınını kilitler — loop'ta yapılır. */
static volatile bool pendingAkimZero = false;
static const unsigned long SERIAL_DBG_INTERVAL_MS = 10000UL;

unsigned long lastUpdate = 0;

int sessMin = 4095;
int sessMax = 0;
bool sessInited = false;

int akimSessMinMa = 0;
int akimSessMaxMa = 0;
bool akimSessInited = false;

enum WizardPhase : uint8_t { WZ_IDLE = 0, WZ_TEST = 1, WZ_SHOW_MAX = 2, WZ_ASK_ADD = 3, WZ_ARMED = 4 };
WizardPhase wizardPhase = WZ_IDLE;
unsigned long testEndMs = 0;
unsigned long testDurationMs = 0;
int testPeak = 0;
int patKalibTepe = 0;
bool usePeakThreshold = false;
int peakThreshold = 0;

bool alarmPatAktif = false;
/** Ana mandal sonrası akış hâlâ yüksek: patlak rölesi mandal (reset'e kadar). */
static bool alarmPatMandal = false;
int kalanPatSn = 0;

/** Boru patlak uyarısı: akış eşiğinin üstüne bu kadar kez art arda çıkınca 30 sn uyarı. */
static const int BORU_PAT_ARIZA_UST_CIKIS_SAYISI = 3;
static int boruPatUstCikisSayaci = 0;
static bool boruPatOncekiCcaUstu = false;

static void boruPatUstSayacSifirla() {
  boruPatUstCikisSayaci = 0;
  boruPatOncekiCcaUstu = false;
}

static void patUyariPencereTemizle(unsigned long now) {
  uint8_t w = 0;
  for (uint8_t i = 0; i < patUyariKayit; i++) {
    if (patUyariZamanlar[i] != 0 && (now - patUyariZamanlar[i]) <= PAT_UYARI_PENCERE_MS) {
      if (w != i) patUyariZamanlar[w] = patUyariZamanlar[i];
      w++;
    }
  }
  patUyariKayit = w;
}

static int patUyariPencereSay(unsigned long now) {
  patUyariPencereTemizle(now);
  return (int)patUyariKayit;
}

static void patUyariKayitEkle(unsigned long now) {
  patUyariPencereTemizle(now);
  if (patUyariKayit >= (uint8_t)PAT_UYARI_MAX_KAYIT) {
    for (uint8_t i = 1; i < patUyariKayit; i++)
      patUyariZamanlar[i - 1] = patUyariZamanlar[i];
    patUyariKayit--;
  }
  patUyariZamanlar[patUyariKayit++] = now;
}

static void patUyariKayitSifirla() {
  patUyariKayit = 0;
  for (int i = 0; i < PAT_UYARI_MAX_KAYIT; i++) patUyariZamanlar[i] = 0;
}

static void boruPatAlarmSifirla(bool nvsAnaLockYaz) {
  alarmPatAktif = false;
  alarmPatMandal = false;
  patUyariModu = false;
  patUyariBaslaMs = 0;
  kalanPatSn = 0;
  if (anaLockReason == LOCK_PAT) {
    anaRoleMandalli = false;
    anaLockReason = LOCK_NONE;
    anaLockSinceMs = 0;
    if (nvsAnaLockYaz) hafizaAnaLockTemizle();
  }
  boruPatUstSayacSifirla();
}

const uint8_t CLOG_OFF = 10;
/** Tıkanma: doz darbe toplamı 0 kalırsa CLOG_TIKANMA_SURE_MS sonunda mandal. Kalibrasyon yok. */
static const unsigned long CLOG_TIKANMA_SURE_MS = 90000UL;  // 1,5 dk
uint8_t clogPh = CLOG_OFF;
unsigned long clogTestEndMs = 0;
int clogTestPeak = 0;
int clogThreshold = 0;
int clogObsMin = 1;
unsigned long clogObserveMs = CLOG_TIKANMA_SURE_MS;
unsigned long clogWindowStart = 0;
bool clogArmed = true;

static void clogVarsayilanAyar() {
  clogThreshold = 0;
  clogObsMin = 1;
  clogObserveMs = CLOG_TIKANMA_SURE_MS;
  clogArmed = true;
  clogPh = CLOG_OFF;
  clogTestPeak = 0;
  clogTestEndMs = 0;
}

bool alarmClogAktif = false;
unsigned long alarmClogBasla = 0;
int kalanClogSn = 0;

static void hafizaAnaLockKaydet(AnaLockReason r) {
  hafiza.putBool("anaLock", true);
  hafiza.putUChar("anaLockR", (uint8_t)r);
}

static void hafizaAnaLockTemizle() {
  hafiza.putBool("anaLock", false);
  hafiza.putUChar("anaLockR", (uint8_t)LOCK_NONE);
  hafiza.putBool("patMnd", false);
}

static void hafizaAnaLockYukle() {
  anaRoleMandalli = hafiza.getBool("anaLock", false);
  uint8_t r = hafiza.getUChar("anaLockR", (uint8_t)LOCK_NONE);
  if (!anaRoleMandalli) {
    anaLockReason = LOCK_NONE;
    anaLockSinceMs = 0;
    return;
  }
  anaLockSinceMs = millis();
  if (r == (uint8_t)LOCK_PAT) {
    anaLockReason = LOCK_PAT;
    alarmPatMandal = hafiza.getBool("patMnd", false);
    alarmPatAktif = true;
    kalanPatSn = 0;
  } else if (r == (uint8_t)LOCK_CLOG) {
    anaLockReason = LOCK_CLOG;
    alarmClogAktif = true;
  } else {
    // Eski NVS (yalnizca anaLock, nedensiz): patlak kilidi varsay
    anaLockReason = LOCK_PAT;
    alarmPatMandal = hafiza.getBool("patMnd", false);
    alarmPatAktif = true;
    kalanPatSn = 0;
    hafizaAnaLockKaydet(LOCK_PAT);
  }
}

static void hafizaRelYukle() {
  relPompaSaniye = hafiza.getInt("relPmp", 10);
  if (relPompaSaniye < 1) relPompaSaniye = 1;
  if (relPompaSaniye > REL_SANIYE_MAX) relPompaSaniye = REL_SANIYE_MAX;
}

static void hafizaPompaYukle() {
  pompaAkimUstLimitMa = hafiza.getInt("pLimMa", 500);
  // Eski sürüm uyumu: pSure saniye idi. Yeni: pSureDk dakika.
  pompaArizaSureDk = hafiza.getInt("pSureDk", -1);
  if (pompaArizaSureDk < 0) {
    int oldSn = hafiza.getInt("pSure", 30);
    if (oldSn < 1) oldSn = 1;
    long m = lroundf((float)oldSn / 60.0f);
    if (m < 1) m = 1;
    pompaArizaSureDk = (int)m;
    hafiza.putInt("pSureDk", pompaArizaSureDk);
  }
  if (pompaAkimUstLimitMa < 0) pompaAkimUstLimitMa = 0;
  if (pompaAkimUstLimitMa > 50000) pompaAkimUstLimitMa = 50000;
  if (pompaArizaSureDk < 1) pompaArizaSureDk = 1;
  if (pompaArizaSureDk > 240) pompaArizaSureDk = 240;
}

static void hafizaPompaKaydet() {
  hafiza.putInt("pLimMa", pompaAkimUstLimitMa);
  hafiza.putInt("pSureDk", pompaArizaSureDk);
}

static int pumpTestKalanSn() {
  if (pumpPh != PZ_TEST) return 0;
  long left = (long)(pumpTestEndMs - millis()) / 1000L;
  return left > 0 ? (int)left : 0;
}

void hafizaClogKaydet() {
  hafiza.putBool("clogArm", true);
  hafiza.putInt("clogThr", 0);
  hafiza.putInt("clogObs", 1);
  hafiza.putInt("clogTpk", 0);
}

void hafizaClogYukle() {
  clogVarsayilanAyar();
  clogWindowStart = millis();
}

void clogSifirla() {
  clogVarsayilanAyar();
  clogWindowStart = millis();
  hafizaClogKaydet();
}

/** Pompa arızası varken tıkanma/mandal iptal (motor durunca düşük akış tıkanma sayılmasın). */
static void clogAlarmPompaArizaTemizle() {
  if (!alarmClogAktif && anaLockReason != LOCK_CLOG) return;
  alarmClogAktif = false;
  kalanClogSn = 0;
  clogWindowStart = millis();
  if (anaLockReason == LOCK_CLOG) {
    anaRoleMandalli = false;
    anaLockReason = LOCK_NONE;
    anaLockSinceMs = 0;
    hafizaAnaLockTemizle();
    Serial.println("Tikanma iptal: pompa arizasi oncelikli");
  }
}

/** Mandal veya mandallı arıza çıkışı aktif mi? */
static bool mandalVeyaMandalliArizaAktif() {
  return anaRoleMandalli || alarmPatAktif || alarmPatMandal || alarmClogAktif;
}

/** Reset butonu (3 sn): tüm mandal ve mandallı alarmları aç — mandal = kilit, reset = anahtar. */
static void mandalResetButonuAc() {
  boruPatAlarmSifirla(true);
  anaRoleMandalli = false;
  hafizaAnaLockTemizle();
  anaLockReason = LOCK_NONE;
  anaLockSinceMs = 0;
  alarmClogAktif = false;
  kalanClogSn = 0;
  clogWindowStart = millis();
  boruPatUstSayacSifirla();
  patUyariKayitSifirla();
}

static void pollResetButton() {
  bool pressed = (digitalRead(PIN_RESET) == LOW);
  if (pressed) {
    if (resetPressStartMs == 0)
      resetPressStartMs = millis();
    else if (!resetAwaitRelease && (millis() - resetPressStartMs >= RESET_HOLD_MS)) {
      if (mandalVeyaMandalliArizaAktif()) {
        mandalResetButonuAc();
        Serial.println("Reset: mandal acildi (3 sn)");
      }
      resetAwaitRelease = true;
    }
  } else {
    resetPressStartMs = 0;
    resetAwaitRelease = false;
  }
}

static void pollPendingAkimZero() {
  if (!pendingAkimZero) return;
  pendingAkimZero = false;
  akimZeroAdc = hallZeroOlcAdc();
  hafiza.putInt("iZero", akimZeroAdc);
  akimSessInited = false;
}

/** AP IP kaybolursa (WiFi yığını kilitlenince) tam reset yerine softAP yeniden açılır. */
static void maintainWifiAp() {
  if (millis() - wifiApHealthMs < WIFI_AP_HEALTH_MS) return;
  wifiApHealthMs = millis();
  IPAddress ip = WiFi.softAPIP();
  if (ip[0] == 192 && ip[1] == 168 && ip[2] == 4) return;
  Serial.printf("WiFi AP kayip (IP=%s) — yeniden aciliyor\n", ip.toString().c_str());
  WiFi.softAPdisconnect(true);
  delay(50);
  WiFi.softAP(g_apSsid, "aktek2026", WIFI_AP_CHANNEL, 0, WIFI_AP_MAX_CONN);
  WiFi.softAPConfig(apIP, apIP, IPAddress(255, 255, 255, 0));
  esp_wifi_set_ps(WIFI_PS_NONE);
  dnsServer.start(DNS_PORT, "*", apIP);
}

/** Sensör/LED önce; WiFi işleri kısa zaman diliminde (loop tıkanmasın). */
static void serviceNetworkBrief() {
  const unsigned long budgetMs = 12UL;
  const unsigned long t0 = millis();
  do {
    dnsServer.processNextRequest();
    server.handleClient();
    webSocket.loop();
  } while ((millis() - t0) < budgetMs);
  maintainWifiAp();
}

void updateLedsPatTik() {
  bool fault = alarmPatAktif || alarmClogAktif || alarmPompaAktif || alarmStuckAktif || anaRoleMandalli;
  unsigned long m = millis();
  if (fault)
    digitalWrite(LED_SISTEM, (m / LED_FAULT_YARIM_PERIYOT_MS) % 2 == 0 ? HIGH : LOW);
  else
    digitalWrite(LED_SISTEM, (m / LED_NORMAL_YARIM_PERIYOT_MS) % 2 == 0 ? HIGH : LOW);
}

void updateSessionStats(int v) {
  if (!sessInited) {
    sessMin = sessMax = v;
    sessInited = true;
  } else {
    if (v < sessMin) sessMin = v;
    if (v > sessMax) sessMax = v;
  }
}

static void updateAkimSessionStatsMa(int ma) {
  if (!akimSessInited) {
    akimSessMinMa = akimSessMaxMa = ma;
    akimSessInited = true;
  } else {
    if (ma < akimSessMinMa) akimSessMinMa = ma;
    if (ma > akimSessMaxMa) akimSessMaxMa = ma;
  }
}

int testKalanSaniye() {
  if (wizardPhase != WZ_TEST) return 0;
  long left = (long)(testEndMs - millis()) / 1000L;
  return left > 0 ? (int)left : 0;
}

int clogTestKalanSn() {
  if (clogPh != 2) return 0;
  long left = (long)(clogTestEndMs - millis()) / 1000L;
  return left > 0 ? (int)left : 0;
}

static int clogPencereKalanSn() {
  unsigned long elapsed = millis() - clogWindowStart;
  if (elapsed >= clogObserveMs) return 0;
  return (int)((clogObserveMs - elapsed) / 1000UL);
}

int clogAnaKalanSn() {
  if (!clogArmed || clogObserveMs == 0 || alarmClogAktif) return -1;
  return clogPencereKalanSn();
}

uint8_t clogAnaMod() {
  if (!clogArmed || clogObserveMs == 0 || alarmClogAktif) return 0;
  return 0;
}

void otaUpdatePage() {
  const char *pg = R"OTA(
<!DOCTYPE html><html lang="tr"><head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Yazılım güncelleme — Aktek Elektronik</title>
<style>
*{box-sizing:border-box;}
body{margin:0;min-height:100vh;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
background:linear-gradient(165deg,#f1f5f9 0%,#ffffff 42%,#f8fafc 100%);color:#0f172a;
display:flex;align-items:center;justify-content:center;padding:24px 16px;}
.wrap{width:100%;max-width:440px;}
.card{background:#fff;border-radius:22px;padding:40px 32px 32px;
box-shadow:0 1px 3px rgba(15,23,42,.06),0 20px 50px -12px rgba(0,86,179,.14);
border:1px solid rgba(226,232,240,.9);}
.brand{font-size:11px;font-weight:700;letter-spacing:.22em;color:#64748b;text-transform:uppercase;margin:0 0 4px;}
.mark{font-size:26px;font-weight:800;color:#0056b3;margin:0 0 4px;letter-spacing:-.02em;}
.mark span{color:#007bff;}
.sub{font-size:13px;color:#94a3b8;margin:0 0 28px;}
h1{font-size:1.5rem;font-weight:700;margin:0 0 14px;line-height:1.25;color:#0f172a;}
.lead{font-size:15px;line-height:1.65;color:#475569;margin:0 0 18px;}
.note{font-size:13px;line-height:1.5;color:#64748b;margin:0;padding:14px 16px;background:#f8fafc;border-radius:12px;border:1px solid #e2e8f0;}
.note strong{color:#0f172a;}
.steps{font-size:13px;line-height:1.65;color:#475569;margin:0 0 20px;padding:16px 18px;background:#f8fafc;border-radius:12px;border:1px solid #e2e8f0;text-align:left;}
.steps ol{margin:8px 0 0;padding-left:1.25em;}
.steps li{margin:6px 0;}
.drop{margin:22px 0 10px;padding:22px 16px;border:2px dashed #cbd5e1;border-radius:16px;background:#fafbfc;text-align:center;transition:border-color .2s,background .2s;}
.drop:focus-within{border-color:#007bff;background:#f0f7ff;}
.drop input[type=file]{width:100%;max-width:100%;font-size:14px;color:#334155;}
.btn{margin-top:16px;width:100%;border:0;border-radius:14px;padding:16px 22px;font-size:16px;font-weight:700;cursor:pointer;
background:linear-gradient(135deg,#0056b3,#007bff);color:#fff;
box-shadow:0 6px 20px rgba(0,86,179,.32);transition:transform .15s,box-shadow .15s,filter .15s;}
.btn:hover{filter:brightness(1.04);box-shadow:0 8px 26px rgba(0,86,179,.38);}
.btn:active{transform:scale(.99);}
.btn:disabled{opacity:.55;cursor:not-allowed;transform:none;}
.foot{margin-top:28px;padding-top:22px;border-top:1px solid #f1f5f9;text-align:center;font-size:12px;color:#94a3b8;line-height:1.5;}
.foot em{font-style:normal;font-weight:600;color:#64748b;}
</style></head><body>
<div class="wrap"><div class="card">
<p class="brand">Aktek Elektronik</p>
<p class="mark">AKTEK<span>.</span></p>
<p class="sub">Klor / süreç kontrol — kablosuz yazılım güncelleme</p>
<h1>Yazılım güncelleme</h1>
<p class="lead">Bilgisayarınızda hazırladığınız firmware dosyasını (<strong>.bin</strong>) bu sayfadan cihaza gönderirsiniz. Aşağıdaki adımları sırayla uygulayın.</p>
<div class="steps"><strong>Nasıl yapılır?</strong>
<ol>
<li>Arduino IDE: <em>Sketch → Export Compiled Binary</em> veya PlatformIO ile projeyi derleyip <strong>.bin</strong> dosyasını bulun.</li>
<li>Dosya alanından <strong>.bin</strong> dosyasını seçin (yanlış dosya cihazı bozabilir).</li>
<li><strong>Yükle ve güncelle</strong> düğmesine basın; yükleme bitene kadar bekleyin (birkaç dakika sürebilir).</li>
<li>İşlem başarılıysa cihaz kendini yeniden başlatır; tekrar Wi‑Fi ağına bağlanın.</li>
</ol></div>
<p class="note"><strong>Önemli:</strong> Yükleme boyunca gücü kesmeyin, kabloyu çıkarmayın ve telefonunuzun bu ağa bağlı kaldığından emin olun.</p>
<form method="POST" action="/update" enctype="multipart/form-data" id="f">
<div class="drop"><input type="file" name="update" accept=".bin,.BIN" required></div>
<button type="submit" class="btn" id="b">Yükle ve güncelle</button>
</form>
<p class="foot"><em>Aktek Elektronik</em><br>Endüstriyel ölçüm ve otomasyon çözümleri</p>
</div></div>
<script>document.getElementById('f').onsubmit=function(){var b=document.getElementById('b');b.disabled=true;b.textContent='Yükleniyor…';};</script>
</body></html>
)OTA";
  server.send(200, "text/html", pg);
}

void otaUpdateDone() {
  bool ok = !Update.hasError();
  server.sendHeader("Connection", "close");
  if (ok) {
    server.send(200, "text/plain; charset=utf-8",
                "Tamam. Cihaz yeniden basliyor...");
    delay(400);
    ESP.restart();
  } else {
    String err = Update.hasError() ? Update.errorString() : "Bilinmeyen hata";
    server.send(500, "text/plain; charset=utf-8", err.c_str());
  }
}

void otaUpdateUpload() {
  HTTPUpload &u = server.upload();
  if (u.status == UPLOAD_FILE_START) {
    Serial.printf("OTA basladi: %s\n", u.filename.c_str());
    if (!Update.begin(UPDATE_SIZE_UNKNOWN)) {
      Update.printError(Serial);
    }
  } else if (u.status == UPLOAD_FILE_WRITE) {
    if (Update.write(u.buf, u.currentSize) != u.currentSize) {
      Update.printError(Serial);
    }
  } else if (u.status == UPLOAD_FILE_END) {
    if (Update.end(true)) {
      Serial.printf("OTA bitti: %u bayt\n", u.totalSize);
    } else {
      Update.printError(Serial);
    }
  } else if (u.status == UPLOAD_FILE_ABORTED) {
    Update.abort();
    Serial.println("OTA iptal");
  }
}

void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) {
  if (type != WStype_TEXT) return;
  String msg = (char *)payload;

  if (msg == "AKIM:ZERO") {
    pendingAkimZero = true;
    return;
  }

  if (msg == "RESET:MM") {
    int v = flowPps;
    sessMin = sessMax = v;
    sessInited = true;
    int hallAdc = okuHallMedian5();
    int hDev = abs(hallAdc - akimZeroAdc);
    akimSessMinMa = akimSessMaxMa = hDev;
    akimSessInited = true;
    return;
  }

  if (msg == "MANDAL:AC") {
    if (mandalVeyaMandalliArizaAktif()) {
      mandalResetButonuAc();
      Serial.println("Web: mandal acildi");
    }
    return;
  }

  if (msg.startsWith("TEST:START:")) {
    if (wizardPhase == WZ_TEST) return;
    unsigned long dur = (unsigned long)msg.substring(11).toInt();
    if (dur < 30000UL) dur = 30000UL;
    testDurationMs = dur;
    testEndMs = millis() + dur;
    testPeak = flowPps;
    wizardPhase = WZ_TEST;
    usePeakThreshold = false;
    peakThreshold = 0;
    hafiza.putBool("usePeak", false);
    return;
  }

  if (msg == "WIZ:NEXT" && wizardPhase == WZ_SHOW_MAX) {
    wizardPhase = WZ_ASK_ADD;
    return;
  }

  if (msg.startsWith("WIZ:OFFSET:") && wizardPhase == WZ_ASK_ADD) {
    int add = msg.substring(11).toInt();
    if (add < 0) add = 0;
    peakThreshold = testPeak + add;
    usePeakThreshold = true;
    wizardPhase = WZ_ARMED;
    patKalibTepe = testPeak;
    hafiza.putBool("usePeak", true);
    hafiza.putInt("peakThr", peakThreshold);
    hafiza.putInt("peakTst", patKalibTepe);
    return;
  }

  if (msg == "WIZ:CANCEL") {
    if (wizardPhase == WZ_ARMED) return;
    wizardPhase = WZ_IDLE;
    testPeak = 0;
    return;
  }

  if (msg == "WIZ:CLEARTHR") {
    usePeakThreshold = false;
    peakThreshold = 0;
    patKalibTepe = 0;
    hafiza.putBool("usePeak", false);
    hafiza.putInt("peakThr", 0);
    hafiza.putInt("peakTst", 0);
    wizardPhase = WZ_IDLE;
    return;
  }

  if (msg == "CLOG:CLEAR" || msg == "CLOG:CANCEL" || msg == "CLOG:RECAL") {
    clogSifirla();
    return;
  }

  if (msg.startsWith("SET:RELPMP:")) {
    int rel = msg.substring(11).toInt();
    if (rel < 1) rel = 1;
    if (rel > REL_SANIYE_MAX) rel = REL_SANIYE_MAX;
    relPompaSaniye = rel;
    hafiza.putInt("relPmp", relPompaSaniye);
    return;
  }
  if (msg.startsWith("SET:PUMP:")) {
    // SET:PUMP:<limitMa>,<sureDk>[,<relSn>]  (relSn artık Ayarlar'dan yönetilir; geriye uyum için opsiyonel)
    String r = msg.substring(9);
    int c1 = r.indexOf(',');
    if (c1 <= 0) return;
    int c2 = r.indexOf(',', c1 + 1);
    int lim = r.substring(0, c1).toInt();
    int sure = (c2 > c1) ? r.substring(c1 + 1, c2).toInt() : r.substring(c1 + 1).toInt();
    if (lim < 0) lim = 0;
    if (lim > 50000) lim = 50000;
    if (sure < 1) sure = 1;
    if (sure > 240) sure = 240;
    pompaAkimUstLimitMa = lim;
    pompaArizaSureDk = sure;
    hafizaPompaKaydet();
    // c2 varsa (eski istemci), röleyi de set edelim ama artık tercih edilen yer Ayarlar.
    if (c2 > c1) {
      int rel = r.substring(c2 + 1).toInt();
      if (rel < 1) rel = 1;
      if (rel > REL_SANIYE_MAX) rel = REL_SANIYE_MAX;
      relPompaSaniye = rel;
      hafiza.putInt("relPmp", relPompaSaniye);
    }
    return;
  }

  if (msg.startsWith("SET:INV32:")) {
    int v = msg.substring(10).toInt();
    invertCcaAdc = (v != 0);
    hafiza.putBool("inv32", invertCcaAdc);
    return;
  }

  // Pompa arıza kalibrasyon sihirbazı
  if (msg == "PUMP:OPEN") {
    if (pompaAkimUstLimitMa > 0 && pompaArizaSureDk > 0) pumpPh = PZ_DONE;
    else pumpPh = PZ_STOP;
    pumpTestEndMs = 0;
    pumpTestDurationMs = 0;
    pumpSumAbsMa = 0;
    pumpSampleCount = 0;
    pumpAvgAbsMa = 0;
    pumpAddMa = 0;
    return;
  }
  if (msg == "PUMP:ACK_STOP" && pumpPh == PZ_STOP) {
    pumpPh = PZ_DUR;
    return;
  }
  if (msg.startsWith("PUMP:TEST:") && pumpPh == PZ_DUR) {
    unsigned long dur = (unsigned long)msg.substring(10).toInt();
    if (dur < 30000UL) dur = 30000UL;
    if (dur > 20UL * 60UL * 1000UL) dur = 20UL * 60UL * 1000UL;
    pumpTestDurationMs = dur;
    pumpTestEndMs = millis() + dur;
    pumpLastSampleMs = 0;
    pumpSumAbsMa = 0;
    pumpSampleCount = 0;
    pumpAvgAbsMa = 0;
    pumpAddMa = 0;
    pumpPh = PZ_TEST;
    return;
  }
  if (msg == "PUMP:NEXT" && pumpPh == PZ_SHOW_AVG) {
    pumpPh = PZ_ASK_ADD;
    return;
  }
  if (msg.startsWith("PUMP:ADD:") && pumpPh == PZ_ASK_ADD) {
    int add = msg.substring(9).toInt();
    if (add < 0) add = 0;
    pumpAddMa = add;
    pompaAkimUstLimitMa = pumpAvgAbsMa + pumpAddMa;
    if (pompaAkimUstLimitMa < 0) pompaAkimUstLimitMa = 0;
    if (pompaAkimUstLimitMa > 50000) pompaAkimUstLimitMa = 50000;
    // sıradaki adım: izleme süresi seçimi (dk)
    pumpPh = PZ_OBS;
    return;
  }
  if (msg.startsWith("PUMP:OBS:") && pumpPh == PZ_OBS) {
    int m = msg.substring(9).toInt();
    if (m < 1) m = 1;
    if (m > 240) m = 240;
    pompaArizaSureDk = m;
    hafizaPompaKaydet();
    pumpPh = PZ_DONE;
    return;
  }
  if (msg == "PUMP:CANCEL") {
    if (pumpPh == PZ_DONE && pompaAkimUstLimitMa > 0) return;
    pumpPh = (pompaAkimUstLimitMa > 0 && pompaArizaSureDk > 0) ? PZ_DONE : PZ_STOP;
    pumpTestEndMs = 0;
    pumpTestDurationMs = 0;
    pumpSumAbsMa = 0;
    pumpSampleCount = 0;
    pumpAvgAbsMa = 0;
    pumpAddMa = 0;
    return;
  }
  if (msg == "PUMP:CLEAR") {
    pompaAkimUstLimitMa = 0;
    pumpAddMa = 0;
    pumpAvgAbsMa = 0;
    pumpSumAbsMa = 0;
    pumpSampleCount = 0;
    pumpTestEndMs = 0;
    pumpTestDurationMs = 0;
    pumpPh = PZ_STOP;
    hafizaPompaKaydet();
    return;
  }
}

String getHTML() {
  String html = "<!DOCTYPE html><html><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width, initial-scale=1'>";
  html += "<style>body{font-family:sans-serif;background:#f4f7f6;text-align:center;padding:12px;margin:0;}";
  html += ".container{max-width:420px;margin:auto;background:#fff;padding:18px;border-radius:20px;box-shadow:0 8px 24px rgba(0,0,0,.08);}";
  html += ".row{display:flex;gap:8px;justify-content:center;flex-wrap:wrap;margin:10px 0;}";
  html += ".pill{background:#e9ecef;padding:10px 14px;border-radius:12px;font-weight:bold;min-width:88px;}";
  html += ".pill span{display:block;font-size:11px;color:#666;font-weight:600;}";
  html += ".pill-strip{display:flex;flex-wrap:nowrap;gap:6px;margin:10px 0;justify-content:stretch;}";
  html += ".pill-strip .pill{flex:1 1 0;min-width:0;padding:8px 6px;border-radius:10px;box-sizing:border-box;}";
  html += ".pill-strip .pill span:first-child{font-size:9px;line-height:1.2;}";
  html += ".pill-strip .pill span:last-child{font-size:14px;margin-top:2px;}";
  html += ".live-readouts{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:12px 0;align-items:stretch;}";
  html += ".cca-card{background:linear-gradient(135deg,#0056b3,#007bff);color:#fff;padding:16px 14px 10px;border-radius:16px;margin:12px 0;text-align:center;}";
  html += ".cca-title{font-size:15px;font-weight:bold;letter-spacing:.14em;opacity:.95;}";
  html += ".cca-range{font-size:11px;opacity:.78;margin-top:3px;}";
  html += ".cca-num{font-size:48px;font-weight:bold;line-height:1.15;margin-top:8px;padding-bottom:4px;}";
  html += ".curr-card{background:linear-gradient(135deg,#0b7a28,#22c55e);color:#fff;padding:16px 14px 10px;border-radius:16px;margin:12px 0;text-align:center;}";
  html += ".curr-title{font-size:15px;font-weight:bold;letter-spacing:.14em;opacity:.95;}";
  html += ".curr-num{font-size:48px;font-weight:bold;line-height:1.15;margin-top:8px;padding-bottom:4px;}";
  html += ".live-readouts .cca-card,.live-readouts .curr-card{margin:0;min-width:0;padding:0;text-align:left;background:#f1f5f9;color:#0f172a;border-radius:14px;box-shadow:0 4px 16px rgba(15,23,42,.08),inset 0 1px 0 #fff;overflow:hidden;}";
  html += ".live-readouts .cca-card{border:3px solid #1d4ed8;}";
  html += ".live-readouts .curr-card{border:3px solid #15803d;}";
  html += ".live-readouts .cca-title,.live-readouts .curr-title{font-size:10px;font-weight:800;letter-spacing:.16em;text-transform:uppercase;color:#64748b;padding:10px 12px 0;line-height:1.2;opacity:1;}";
  html += ".live-readouts .cca-num,.live-readouts .curr-num{font-size:clamp(20px,9vw,36px);font-weight:800;font-family:ui-monospace,Cascadia Mono,Consolas,monospace;letter-spacing:.06em;color:#0f172a;background:#e2e8f0;margin:8px 10px 12px;padding:12px 10px;border-radius:10px;line-height:1.1;text-align:center;box-shadow:inset 0 2px 6px rgba(15,23,42,.14);border:1px solid #cbd5e1;}";
  html += ".live-readouts .curr-num .unit{font-size:11px;color:#475569;margin-left:4px;vertical-align:super;font-weight:700;letter-spacing:0;}";
  html += ".unit{font-size:14px;font-weight:800;opacity:.9;margin-left:6px;letter-spacing:.08em;}";
  html += ".curr-range{font-size:11px;opacity:.85;margin-top:6px;font-weight:700;}";
  html += ".btn{background:#0056b3;color:#fff;padding:11px;border:none;border-radius:10px;cursor:pointer;font-weight:bold;width:100%;margin-top:10px;font-size:15px;}";
  html += ".mandal-strip{margin:10px 0 12px;padding:12px 14px;border-radius:12px;background:#e8f5e9;border:2px solid #81c784;display:flex;flex-wrap:wrap;align-items:center;gap:8px 12px;}";
  html += ".mandal-strip.locked{background:#ffebee;border-color:#e57373;}";
  html += ".mandal-strip .mandal-lbl{font-size:13px;font-weight:700;color:#555;}";
  html += ".mandal-strip .mandal-val{font-size:16px;font-weight:800;}";
  html += ".mandal-strip .mandal-sub{font-size:12px;color:#666;flex:1 1 100%;}";
  html += ".btn-mandal-open{background:#c62828;color:#fff;padding:8px 14px;border:none;border-radius:8px;font-weight:bold;font-size:14px;cursor:pointer;margin-left:auto;}";
  html += ".btn:hover{background:#004494;}.btn-sec{background:#6c757d;}.btn-go{background:#28a745;}";
  html += ".btn-dur{background:#fff;color:#0056b3;border:2px solid #0056b3;width:30%;margin:4px 1%;display:inline-block;padding:12px 0;font-weight:bold;min-height:48px;touch-action:manipulation;}";
  html += ".btn-kal{background:linear-gradient(135deg,#c82333,#dc3545);}";
  html += ".btn-clog{background:linear-gradient(135deg,#b8860b,#e6a017);color:#1a1a1a;}";
  html += ".btn-pump{background:linear-gradient(135deg,#0b7a28,#22c55e);color:#fff;}";
  html += ".btn-big-ack{font-size:17px;padding:16px;margin-top:14px;}";
  html += ".alert{background:#dc3545;color:#fff;padding:14px;border-radius:10px;font-weight:bold;margin-top:10px;display:none;font-size:16px;}";
  html += ".alert-clog{background:#856404;color:#fff;}";
  html += ".panel{border:1px solid #dee2e6;border-radius:12px;padding:14px;margin-top:12px;text-align:left;}";
  html += ".panel h3{margin:0 0 10px;color:#0056b3;font-size:17px;}.panel.clog-panel h3{color:#0d5c4d;}";
  html += ".screen{display:none;}.screen.show{display:block;}";
  html += ".cal-step,.clog-step{display:none!important;text-align:left;pointer-events:none;}";
  html += ".cal-step.on,.clog-step.on{display:block!important;pointer-events:auto;}";
  html += ".input-group{margin-bottom:12px;} .input-group label{font-weight:bold;color:#555;display:block;margin-bottom:4px;font-size:13px;}";
  html += ".input-group input{width:100%;padding:8px;border:1px solid #ccc;border-radius:6px;box-sizing:border-box;}";
  html += ".u{font-size:12px;font-weight:800;color:#64748b;margin-left:6px;letter-spacing:.08em;}";
  html += ".hint{font-size:13px;color:#555;line-height:1.45;margin:0 0 10px;}";
  html += ".main-hints{margin-top:20px;padding-top:16px;border-top:1px solid #e2e8f0;text-align:left;}";
  html += ".main-hints .hint:last-child{margin-bottom:0;}";
  html += ".btn-grid2{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:16px;}";
  html += ".btn-grid2 .btn{margin-top:0;width:100%;font-size:14px;padding:14px 10px;min-height:56px;line-height:1.25;box-sizing:border-box;display:flex;align-items:center;justify-content:center;text-align:center;}";
  html += ".big-stop{font-size:clamp(20px,5.5vw,26px);font-weight:800;color:#0d5c4d;line-height:1.25;text-align:center;margin:18px 0 8px;}";
  html += ".btn-obs{width:47%;margin:4px 1%;display:inline-block;padding:12px 4px;font-size:14px;}";
  html += ".clog-main-cd{display:none;margin-top:12px;padding:12px 14px;border-radius:12px;background:#fff8e6;border:1px solid #e6c35c;font-size:15px;font-weight:700;color:#5c4a00;line-height:1.35;}";
  html += ".tour-overlay{position:fixed;inset:0;z-index:9997;background:rgba(15,23,42,.55);display:none;align-items:flex-end;justify-content:center;padding:12px;box-sizing:border-box;}";
  html += ".tour-overlay.tour-on{display:flex;}";
  html += ".tour-panel{background:#fff;border-radius:18px;padding:16px 16px 14px;max-width:400px;width:100%;box-sizing:border-box;box-shadow:0 8px 40px rgba(0,0,0,.28);position:relative;z-index:9998;text-align:left;margin-bottom:max(8px,env(safe-area-inset-bottom,0));}";
  html += ".tour-meta{font-size:11px;font-weight:800;color:#64748b;letter-spacing:.1em;margin:0 0 8px;text-transform:uppercase;}";
  html += ".tour-txt{font-size:15px;line-height:1.55;color:#0f172a;margin:0 0 14px;}";
  html += ".tour-txt strong{color:#0056b3;}";
  html += ".tour-finger{position:fixed;left:0;top:0;display:none;font-size:44px;line-height:1;z-index:10000;pointer-events:none;filter:drop-shadow(0 2px 4px rgba(0,0,0,.4));transform:translate(-50%,0);transform-origin:center bottom;will-change:transform;animation:tourBounce .72s ease-in-out infinite;}";
  html += "@keyframes tourBounce{0%,100%{transform:translate(-50%,0)}50%{transform:translate(-50%,-10px)}}";
  html += ".tour-next{margin-top:0!important;}.tour-skip{margin-top:8px!important;font-size:14px!important;padding:10px!important;}</style>";

  html += "<script>";
  html += "var socket=null,phase=0,tleft=0,tmax=0,thr=0,val=0,wsQ=[],clogPh=10,clogTl=0,clogTpk=0,clogThr=0,clogObs=0,alC=0,kC=0,ccb=0,clogAkSn=-1,clogAkMod=0,lastRelPompa=10,lastInv32=0,inv32Dirty=0,pumpAl=0,pumpK=0,pumpLim=0,pumpSure=0,pumpPh=0,pumpTl=0,pumpAvg=0,pumpRun=0,pumpCur=0,mandalLk=0,mandalNeden=0;";
  html += "function updMandal(){var b=el('mandalStrip'),d=el('mandalDurum'),s=el('mandalSub'),btn=el('mandalAcBtn');if(!d)return;if(mandalLk){d.textContent='Kilitli';d.style.color='#b71c1c';if(b)b.classList.add('locked');if(s)s.textContent=mandalNeden===1?'Neden: boru patlak mandal\\u0131':mandalNeden===2?'Neden: t\\u0131kanma / ak\\u0131\\u015f yok mandal\\u0131':'Neden: mandal aktif';if(btn)btn.style.display='inline-block';}else{d.textContent='A\\u00e7\\u0131k';d.style.color='#2e7d32';if(b)b.classList.remove('locked');if(s)s.textContent='Ana pompa mandalda de\\u011fil';if(btn)btn.style.display='none';}}";
  html += "function mandalAc(){if(!mandalLk){return;}if(confirm('Ana pompa mandal\\u0131n\\u0131 ve ilgili alarmlar\\u0131 a\\u00e7mak istiyor musunuz?'))send('MANDAL:AC');}";
  html += "var wsUrl='ws://192.168.4.1:81/';";
  html += "function el(id){return document.getElementById(id);}";
  html += "var CCA_MAX=4096;";
  html += "function setCca(v){var c=Math.max(0,Math.min(CCA_MAX,parseInt(v,10)||0));var n=el('v0');if(n)n.innerHTML=c;return c;}";
  html += "function setCcb(v){var c=Math.max(0,Math.min(CCA_MAX,parseInt(v,10)||0));ccb=c;return c;}";
  html += "function setAkim(v){var c=parseInt(v,10);if(isNaN(c))c=0;var n=el('i0');if(n)n.innerHTML=c;return c;}";
  html += "function setAkimMinMax(mi,ma){var n1=el('imn'),n2=el('imx');if(n1)n1.innerHTML=parseInt(mi,10)||0;if(n2)n2.innerHTML=parseInt(ma,10)||0;}";
  html += "function setHamOlc(v){var n=el('iham');if(n)n.innerHTML=parseInt(v,10)||0;}";
  html += "function showMain(){el('sMain').style.display='block';el('sCal').classList.remove('show');el('sAyar').classList.remove('show');el('sAkim').classList.remove('show');}";
  html += "function openCal(){el('sAyar').classList.remove('show');el('sMain').style.display='none';el('sCal').classList.add('show');syncCal();}";
  html += "function openAkim(){el('sAyar').classList.remove('show');el('sCal').classList.remove('show');send('WIZ:CANCEL');el('sMain').style.display='none';el('sAkim').classList.add('show');send('PUMP:OPEN');syncPump();}";
  html += "function bindInv32(){var c=el('inv32');if(!c||c._bnd)return;c._bnd=1;c.addEventListener('change',function(){inv32Dirty=1;lastInv32=this.checked?1:0;});}";
  html += "function openAyar(){el('sCal').classList.remove('show');el('sAkim').classList.remove('show');el('sMain').style.display='none';el('sAyar').classList.add('show');if(el('relPompaInAyar'))el('relPompaInAyar').value=lastRelPompa;if(el('inv32'))el('inv32').checked=(lastInv32==1);inv32Dirty=0;bindInv32();}";
  html += "function goMain(){if(el('sAyar').classList.contains('show')){el('sAyar').classList.remove('show');el('sMain').style.display='block';return;}if(el('sAkim').classList.contains('show')){el('sAkim').classList.remove('show');el('sMain').style.display='block';send('PUMP:CANCEL');return;}if(phase===1||phase===2||phase===3)send('WIZ:CANCEL');showMain();}";
  html += "function fmtMMSS(s){s=parseInt(s,10)||0;var m=Math.floor(s/60),r=s%60;return m+':'+(r<10?'0':'')+r;}";
  html += "function updNextDoseStrip(){var cw=el('clogCountMainWrap'),cm=el('clogCountMain'),cs=el('clogCountSub');if(!cw||!cm)return;var ok=!alC&&clogAkSn>=0;if(ok){cw.style.display='block';cm.innerHTML=fmtMMSS(clogAkSn);if(cs)cs.textContent='Doz yok; 1,5 dk dolunca t\\u0131kanma. Doz gelince s\\u0131f\\u0131rlan\\u0131r.';}else{cw.style.display='none';}}";
  html += "function syncCal(){['cDur','cTest','cMax','cAdd','cArm','cSumPat'].forEach(function(id){var n=el(id);if(n)n.classList.remove('on');});";
  html += "if(phase===4&&thr>0){el('cSumPat').classList.add('on');return;}";
  html += "if(phase===0)el('cDur').classList.add('on');else if(phase===1)el('cTest').classList.add('on');else if(phase===2)el('cMax').classList.add('on');else if(phase===3)el('cAdd').classList.add('on');else el('cArm').classList.add('on');}";
  html += "function syncPump(){if(pumpPh>6)return;['p0','p1','p2','p3','p4','p5','p6'].forEach(function(id){var n=el(id);if(n)n.classList.remove('on');});var x=el('p'+pumpPh);if(x)x.classList.add('on');}";
  html += "function apply(csv){var p=csv.split(',');if(p.length<17)return;";
  html += "val=parseInt(p[0],10)||0;var al=parseInt(p[1],10)||0,su=parseInt(p[2],10)||0;";
  html += "var smin=parseInt(p[3],10)||0,smax=parseInt(p[4],10)||0;var ph=parseInt(p[5],10);phase=(isNaN(ph)||ph<0||ph>4)?0:ph;";
  html += "tleft=parseInt(p[6],10)||0;tmax=parseInt(p[7],10)||0;thr=parseInt(p[8],10)||0;";
  html += "setCcb(parseInt(p[9],10)||0);clogPh=parseInt(p[10],10);if(isNaN(clogPh))clogPh=10;";
  html += "clogTl=parseInt(p[11],10)||0;clogTpk=parseInt(p[12],10)||0;clogThr=parseInt(p[13],10)||0;clogObs=parseInt(p[14],10)||0;";
  html += "alC=parseInt(p[15],10)||0;kC=parseInt(p[16],10)||0;";
  html += "if(p.length>17){var _s=parseInt(p[17],10);clogAkSn=isNaN(_s)?-1:_s;}else{clogAkSn=-1;}";
  html += "if(p.length>18){var _m=parseInt(p[18],10);clogAkMod=isNaN(_m)?0:_m;}else{clogAkMod=0;}";
  html += "if(p.length>19)setAkim(parseInt(p[19],10)||0);else setAkim(0);";
  html += "if(p.length>21)setAkimMinMax(p[20],p[21]);else setAkimMinMax(0,0);";
  html += "if(p.length>22)setHamOlc(p[22]);else setHamOlc(0);";
  html += "if(p.length>25){pumpAl=parseInt(p[24],10)||0;pumpK=parseInt(p[25],10)||0;}else{pumpAl=0;pumpK=0;}";
  html += "if(p.length>27){pumpLim=parseInt(p[26],10)||0;pumpSure=parseInt(p[27],10)||0;}else{pumpLim=0;pumpSure=0;}";
  html += "if(p.length>28){var rp2=parseInt(p[28],10);if(!isNaN(rp2)){rp2=Math.max(1,Math.min(" + String(REL_SANIYE_MAX) + ",rp2));lastRelPompa=rp2;if(el('relPompaInAyar')&&!el('sAyar').classList.contains('show'))el('relPompaInAyar').value=rp2;}}";
  html += "if(p.length>33){pumpPh=parseInt(p[29],10)||0;pumpTl=parseInt(p[30],10)||0;pumpAvg=parseInt(p[31],10)||0;pumpRun=parseInt(p[32],10)||0;pumpCur=parseInt(p[33],10)||0;}else{pumpPh=0;pumpTl=0;pumpAvg=0;pumpRun=0;pumpCur=0;}";
  html += "if(p.length>34){var iv=parseInt(p[34],10);lastInv32=isNaN(iv)?0:(iv?1:0);if(el('sAyar').classList.contains('show')&&el('inv32')&&!inv32Dirty)el('inv32').checked=(lastInv32==1);}";
  html += "if(p.length>35)mandalLk=parseInt(p[35],10)||0;if(p.length>36)mandalNeden=parseInt(p[36],10)||0;updMandal();";
  html += "var cca=setCca(val);if(el('vCalClog'))el('vCalClog').innerHTML=ccb;el('mn').innerHTML=smin;el('mx').innerHTML=smax;el('vCal').innerHTML=cca;";
  html += "if(el('sumPatThr'))el('sumPatThr').innerHTML=thr;if(el('sumPatPeak'))el('sumPatPeak').innerHTML=tmax;";
  html += "el('tmax').innerHTML=tmax;el('tmax2').innerHTML=tmax;el('tmaxBig').innerHTML=tmax;el('thrDisp').innerHTML=thr;";
  html += "el('cd').innerHTML=tleft>0?('Kalan \\u00fcre: '+tleft+' sn'):'S\\u00fcre tamamland\\u0131';";
  html += "if(el('clogCd'))el('clogCd').innerHTML=clogTl>0?('Kalan \\u00fcre: '+clogTl+' sn'):'S\\u00fcre tamamland\\u0131';";
  html += "if(el('sCal').classList.contains('show'))syncCal();";
  html += "if(al==1){el('al-pat').style.display='block';el('kalan').innerHTML='Alarm: '+su+' sn';}else{el('al-pat').style.display='none';}";
  html += "if(alC==1){el('al-clog').style.display='block';el('kalanClog').innerHTML='Pompa mandalda. Reset d\\u00fc\\u011fmesine 3 sn bas\\u0131n.';}else{el('al-clog').style.display='none';}";
  html += "if(el('al-pump')){if(pumpAl==1){el('al-pump').style.display='block';el('kalanPump').innerHTML='Alarm: '+pumpK+' sn';}else{el('al-pump').style.display='none';}}";
  html += "if(el('pumpCd'))el('pumpCd').innerHTML=pumpTl>0?('Kalan s\\u00fcre: '+pumpTl+' sn'):'S\\u00fcre tamamland\\u0131';";
  html += "if(el('pumpAvgDisp'))el('pumpAvgDisp').innerHTML=pumpAvg;";
  html += "if(el('pumpLimDisp'))el('pumpLimDisp').innerHTML=pumpLim;";
  html += "if(el('pumpRunDisp'))el('pumpRunDisp').innerHTML=pumpRun;";
  html += "if(el('pumpCurDisp'))el('pumpCurDisp').innerHTML=pumpCur;";
  html += "if(el('sAkim').classList.contains('show'))syncPump();";
  html += "updNextDoseStrip();";
  html += "}";
  html += "function wsFlush(){while(socket&&socket.readyState===1&&wsQ.length)socket.send(wsQ.shift());}";
  html += "function send(m){if(socket&&socket.readyState===1)socket.send(m);else wsQ.push(m);}";
  html += "function connectWs(){try{socket=new WebSocket(wsUrl);socket.onopen=function(){wsFlush();};socket.onmessage=function(e){apply(e.data);};socket.onclose=function(){setTimeout(connectWs,1200);};socket.onerror=function(){try{socket.close();}catch(z){}};}catch(e){setTimeout(connectWs,1200);}}";
  html += "var tourI=0,tourSteps=[];";
  html += "tourSteps.push({t:'<strong>Ho\\u015f geldiniz.</strong> \\u00dcstteki d\\u00f6rt kutuda bu oturumda g\\u00f6r\\u00fclen ak\\u0131\\u015f ve pompa <strong>min / maks</strong> de\\u011ferleri yer al\\u0131r. Yeni \\u00f6l\\u00e7\\u00fcme ba\\u015flarken <strong>Min / Maks s\\u0131f\\u0131rla</strong> d\\u00fc\\u011fmesine bas\\u0131n.',q:'#tourAdim1Hedef',fp:'below',noScroll:1});";
  html += "tourSteps.push({t:'<strong>Anl\\u0131k veriler:</strong> Soldaki panel debimetre <strong>ak\\u0131\\u015f\\u0131</strong>, sa\\u011fdaki panel <strong>pompa verisini</strong> g\\u00f6sterir. Kurulumda \\u00f6nce boru patlama, sonra t\\u0131kanma ve pompa kalibrasyonlar\\u0131n\\u0131 tamamlaman\\u0131z \\u00f6nerilir.',q:'#sMain .live-readouts'});";
  html += "tourSteps.push({t:'<strong>Kalibrasyon ve ayarlar:</strong> K\\u0131rm\\u0131z\\u0131, sar\\u0131 ve ye\\u015fil d\\u00fc\\u011fmeler sihirbazlara a\\u00e7\\u0131l\\u0131r; <strong>Ayarlar</strong> pompa r\\u00f6lesi s\\u00fcresi ve sens\\u00f6r y\\u00f6n\\u00fcn\\u00fc i\\u00e7erir.',q:'#sMain .btn-grid2'});";
  html += "tourSteps.push({t:'<strong>Bilgi alan\\u0131:</strong> En altta her konu i\\u00e7in k\\u0131sa a\\u00e7\\u0131klamalar bulunur. <strong>Kapat</strong> veya <strong>Tamam</strong> ile bu tan\\u0131t\\u0131m\\u0131 bitirebilirsiniz; sayfa her a\\u00e7\\u0131l\\u0131\\u015f\\u0131nda yeniden g\\u00f6sterilir.',q:'#tourMainHints > p.hint:first-of-type',fp:'below'});";
  html += "function tourPos(){var f=el('tourFinger');if(!f)return;var S=tourSteps[tourI];if(!S||!S.q){f.style.display='none';return;}var n=document.querySelector(S.q);if(!n){f.style.display='none';return;}if(!S.noScroll){try{n.scrollIntoView({block:'nearest',inline:'nearest'});}catch(_){}}requestAnimationFrame(function(){var r=n.getBoundingClientRect();if(r.width<1&&r.height<1){f.style.display='none';return;}var cx=r.left+r.width*.5,fh=52,gap=10,pr=220,fp=S.fp||'auto',yb,ya,y;var b=window.innerHeight||document.documentElement.clientHeight||600;var w=window.innerWidth||document.documentElement.clientWidth||400;var ox=0,oy=0;if(fp==='above'){y=r.top-fh-gap;y=Math.max(oy+gap,y);}else if(fp==='below'){y=r.bottom+gap;var room=b-pr;if(y+fh>room){y=room-fh-6;y=Math.max(oy+gap,y);}else y=Math.max(oy+gap,y);}else{yb=r.bottom+gap;ya=r.top-fh-gap;y=yb;if(yb+fh>b-pr&&ya>oy+gap)y=ya;else if(yb+fh>b-pr)y=Math.max(oy+gap,ya);}cx=Math.max(26,Math.min(w-26,cx));y=Math.min(b-fh-10,Math.max(oy+gap,y));f.style.left=cx+'px';f.style.top=y+'px';f.style.display='block';});}";
  html += "function tourShow(){var st=tourSteps[tourI];el('tourMeta').textContent='Ad\\u0131m '+(tourI+1)+' / '+tourSteps.length;el('tourTxt').innerHTML=st.t;var g=el('tourBtnGo');g.textContent=tourI>=tourSteps.length-1?'Tamam':'Devam';requestAnimationFrame(function(){requestAnimationFrame(tourPos);});}";
  html += "function tourStart(){tourI=0;var o=el('tourOv');if(!o)return;o.style.display='flex';requestAnimationFrame(function(){o.classList.add('tour-on');tourShow();});}";
  html += "function tourEnd(){var o=el('tourOv');if(o){o.classList.remove('tour-on');o.style.display='none';}if(el('tourFinger'))el('tourFinger').style.display='none';}";
  html += "function tourNext(){if(tourI>=tourSteps.length-1){tourEnd();return;}tourI++;tourShow();}";
  html += "function initTour(){var n=0;function tryTour(){n++;var o=el('tourOv');if(o&&(o.classList.contains('tour-on')||o.style.display==='flex'))return;var m=el('sMain');if(m&&m.style.display!=='none'&&o){tourStart();return;}if(n<24)setTimeout(tryTour,280);}setTimeout(tryTour,450);}";
  html += "function initTourBoot(){var g=el('tourBtnGo'),sk=el('tourBtnSkip');if(!g||g._t)return;g._t=1;g.addEventListener('click',tourNext);sk.addEventListener('click',tourEnd);function tp(){if(el('tourOv')&&el('tourOv').classList.contains('tour-on'))tourPos();}window.addEventListener('resize',tp);window.addEventListener('scroll',tp,true);if(window.visualViewport)window.visualViewport.addEventListener('resize',tp);initTour();}";
  html += "function bootConnect(){if(document.getElementById('sMain'))connectWs();else setTimeout(bootConnect,50);}";
  html += "if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',function(){bootConnect();bindInv32();initTourBoot();});else{bootConnect();bindInv32();initTourBoot();}";
  html += "function resetMM(){send('RESET:MM');}";
  html += "function pickDur(ms){send('TEST:START:'+ms);}";
  html += "function wizNext(){send('WIZ:NEXT');}";
  html += "function wizOffset(){var o=parseInt(el('offIn').value,10)||0;if(o<0)o=0;send('WIZ:OFFSET:'+o);}";
  html += "function wizCancel(){send('WIZ:CANCEL');}";
  html += "function clearThr(){send('WIZ:CLEARTHR');showMain();}";
  html += "function recalPat(){send('WIZ:CLEARTHR');}";
  html += "function saveRel2(){var mx=" + String(REL_SANIYE_MAX) + ";var cRaw=el('relPompaInAyar').value;var c=parseInt(cRaw,10);if(cRaw===''||isNaN(c))c=lastRelPompa;c=Math.max(1,Math.min(mx,c));lastRelPompa=c;var inv=(el('inv32')&&el('inv32').checked)?1:0;lastInv32=inv;inv32Dirty=0;send('SET:RELPMP:'+c);send('SET:INV32:'+inv);goMain();}";
  html += "function pumpAckStop(){send('PUMP:ACK_STOP');}";
  html += "function pumpPickDur(ms){send('PUMP:TEST:'+ms);}";
  html += "function pumpNext(){send('PUMP:NEXT');}";
  html += "function pumpAdd(){var o=parseInt(el('pumpAddIn').value,10)||0;if(o<0)o=0;send('PUMP:ADD:'+o);}";
  html += "function pumpPickObs(m){send('PUMP:OBS:'+m);}";
  html += "function pumpCancel(){send('PUMP:CANCEL');goMain();}";
  html += "function pumpClear(){send('PUMP:CLEAR');pumpPh=0;syncPump();}";
  html += "</script></head><body>";

  html += "<div class='container'><h2 style='color:#0056b3;margin:0 0 8px;'>AKTEK ELEKTRONİK OTOMASYON</h2>";
  html += "<div id='al-pat' class='alert'>BORU PATLADI!<div class='countdown' id='kalan'></div></div>";
  html += "<div id='al-clog' class='alert alert-clog' style='display:none;'>BORU TIKANDI!<div class='countdown' id='kalanClog'></div></div>";
  html += "<div id='al-pump' class='alert' style='display:none;'>POMPA ARIZASI!<div class='countdown' id='kalanPump'></div></div>";
  html += "<div id='tourFinger' class='tour-finger' aria-hidden='true'>&#128070;</div>";
  html += "<div id='tourOv' class='tour-overlay' style='display:none;'>";
  html += "<div class='tour-panel'><p id='tourMeta' class='tour-meta'></p><p id='tourTxt' class='tour-txt'></p>";
  html += "<button type='button' class='btn btn-go tour-next' id='tourBtnGo'>Devam</button>";
  html += "<button type='button' class='btn btn-sec tour-skip' id='tourBtnSkip'>Kapat</button></div></div>";

  html += "<div id='sMain'>";
  html += "<div id='mandalStrip' class='mandal-strip'><span class='mandal-lbl'>Ana pompa mandal:</span> <span id='mandalDurum' class='mandal-val'>—</span>";
  html += "<button type='button' id='mandalAcBtn' class='btn-mandal-open' style='display:none;' onclick='mandalAc()'>Mandalı aç</button>";
  html += "<span id='mandalSub' class='mandal-sub'>Bağlanıyor…</span></div>";
  html += "<div id='clogCountMainWrap' class='clog-main-cd' style='display:none;'>Doz yok sayacı: <span id='clogCountMain'>0:00</span><br><span id='clogCountSub' style='font-size:12px;font-weight:600;opacity:.9;display:block;margin-top:6px;'></span></div>";
  html += "<div id='tourAdim1Hedef'>";
  html += "<div class='pill-strip'><div class='pill'><span>Akış min</span><span id='mn'>0</span></div>";
  html += "<div class='pill'><span>Akış max</span><span id='mx'>0</span></div>";
  html += "<div class='pill'><span>Pompa min</span><span id='imn'>0</span></div>";
  html += "<div class='pill'><span>Pompa max</span><span id='imx'>0</span></div></div>";
  html += "<button class='btn btn-sec' onclick='resetMM()'>Min / Maks sıfırla</button></div>";
  html += "<div class='live-readouts'>";
  html += "<div class='cca-card'><div class='cca-title'>Son doz (darbe)</div><div class='cca-num' id='v0'>0</div></div>";
  html += "<div class='curr-card'><div class='curr-title'>Pompa verisi</div><div class='curr-num'><span id='i0'>0</span><span class='unit'></span></div></div>";
  html += "</div>";
  html += "<div class='btn-grid2'>";
  html += "<button type='button' class='btn btn-kal' onclick='openCal()'>BORU PATLAMA KALİBRASYONU</button>";
  html += "<button type='button' class='btn btn-pump' onclick='openAkim()'>POMPA ARIZA KALİBRASYONU</button>";
  html += "<button type='button' class='btn btn-sec' onclick='openAyar()'>Ayarlar</button>";
  html += "</div>";
  html += "<div class='main-hints' id='tourMainHints'>";
  html += "<p class='hint'><strong>Boru patlama:</strong> Akış hızı izlenir. Sihirbazda tipik yüksek akışa göre eşik ve pay kurulur; normal çalışmada bu eşiği <strong>aşarsa</strong> alarm verilir. 3 eşik aşımında 30 sn uyarı verilir (pompa çalışır). 10 dk içinde 3 uyarı olursa ana pompa kesilir.</p>";
  html += "<p class='hint'><strong>Akı yok / tıkanma:</strong> Her dozdaki darbeler toplanır (ekranda). <strong>1,5 dk</strong> hiç doz yoksa mandal. Pompa arızasında tıkanma yok.</p>";
  html += "<p class='hint'><strong>Pompa:</strong> Önce pompa <strong>dururken</strong> boşta okuma alınır. Sonra pompayı çalıştırıp test süresinde ortalama <strong>pompa verisi</strong> &rarr; + ek ile &quot;pompa gerçekten hareket ediyor&quot; eşiği tanımlanır; bu eşiğin altında kalırsanız pompa arızası izlenir.</p>";
  html += "</div></div>";

  html += "<div id='sCal' class='screen'>";
  html += "<button type='button' class='btn btn-sec' onclick='goMain()'>Ana sayfaya dön</button>";
  html += "<div id='cSumPat' class='cal-step panel'><h3>Kayıtlı patlama kalibrasyonu</h3>";
  html += "<p class='hint'>Cihazda kayıtlı değerler aşağıdadır. Yeniden ölçm yapmak için <strong>Yeniden kalibre et</strong> ile kayıt silinir ve sihirbaz başlar.</p>";
  html += "<div class='pill' style='max-width:280px;margin:10px auto;text-align:center;'><span>Aktif eşik (patlama)</span><span id='sumPatThr'>0</span></div>";
  html += "<div class='pill' style='max-width:280px;margin:10px auto;text-align:center;'><span>Kalibrasyonda maks. akış</span><span id='sumPatPeak'>0</span></div>";
  html += "<button type='button' class='btn btn-go' onclick='recalPat()'>Yeniden kalibre et</button>";
  html += "<button type='button' class='btn btn-sec' onclick='goMain()'>Ana sayfaya dön</button></div>";
  html += "<div id='cDur' class='cal-step panel'><h3>1 &mdash; Test süresi</h3>";
  html += "<p class='hint'>Sağlıklı sistemde gördüğünüz en yüksek akış hızını (veya patlak senaryosunu güvenle taklit edebiliyorsanız o koşulu) <strong>test süresi</strong> içinde ölçersiniz (30 sn–5 dk). Bu sürede gösterge sürekli güncellenir; kaydedilen <strong>maksimum</strong> değer sonraki adımlarda kullanılır. Bu ekranda durum göstergesi yanıp söner.</p>";
  html += "<button type='button' class='btn-dur' onclick='pickDur(30000)'>30 sn</button>";
  html += "<button type='button' class='btn-dur' onclick='pickDur(60000)'>1 dk</button>";
  html += "<button type='button' class='btn-dur' onclick='pickDur(120000)'>2 dk</button><br>";
  html += "<button type='button' class='btn-dur' onclick='pickDur(180000)'>3 dk</button>";
  html += "<button type='button' class='btn-dur' onclick='pickDur(300000)'>5 dk</button></div>";
  html += "<div id='cTest' class='cal-step panel'><h3>2 &mdash; Test</h3>";
  html += "<p id='cd' style='font-size:24px;font-weight:bold;color:#0056b3;margin:8px 0;'>Kalan: —</p>";
  html += "<p class='hint'>Geri sayım süresince <strong>anlık akış hızı</strong> izlenir. Alttaki değer, bu testte görülen <strong>en yüksek</strong> seviyedir. Test bitince sonraki adıma geçilir. Sorunda <strong>Testi iptal</strong>.</p>";
  html += "<div class='pill' style='margin:10px auto;max-width:260px;text-align:center;'><span>Test süresince maksimum</span><span id='tmax'>0</span></div>";
  html += "<button type='button' class='btn btn-sec' onclick='wizCancel()'>Testi iptal</button></div>";
  html += "<div id='cMax' class='cal-step panel'><h3>3 &mdash; Sonuç</h3>";
  html += "<p class='hint'><strong>Test bitti.</strong> Aşağdaki değer, test süresince kaydedilen <strong>en yüksek akış</strong>. Bir sonraki adımda <strong>+ ek</strong> girersiniz; <strong>patlama eşiği = maksimum + ek</strong> (gürültü payı için).</p>";
  html += "<p style='font-size:28px;font-weight:bold;color:#0056b3;margin:12px 0;text-align:center;' id='tmaxBig'>0</p>";
  html += "<button type='button' class='btn btn-go' onclick='wizNext()'>İleri</button></div>";
  html += "<div id='cAdd' class='cal-step panel'><h3>4 &mdash; Eşik</h3>";
  html += "<p class='hint'>Maksimum (<span id='tmax2'>0</span>) testte görülen zirvedir. <strong>+ ek</strong> ile güvenlik payı ekleyin; <strong>eşik = maks + ek</strong>. Normal çalışmada bu eşiği <strong>herhangi bir anda aşarsa</strong> boru patlak alarmı tetiklenir.</p>";
  html += "<div class='input-group'><label>Tepe değere eklenecek pay (ek)</label><input type='number' id='offIn' value='0' min='0'></div>";
  html += "<button type='button' class='btn btn-go' onclick='wizOffset()'>Eşiği kaydet</button></div>";
  html += "<div id='cArm' class='cal-step panel'><h3>Kalibrasyon tamam</h3>";
  html += "<p class='hint'>Kayıt tamam. <strong>Aktif patlama eşiği:</strong> <span id='thrDisp'>0</span> &mdash; anlık akış: <strong id='vCal'>0</strong>. Ana sayfada izleme devam eder.</p>";
  html += "<p class='hint'>Kalibrasyonu baştan yapmak için <strong>Eşiği kaldır</strong> düğmesine basın; kayıtlı eşik silinir.</p>";
  html += "<button type='button' class='btn' onclick='goMain()'>Ana ekrana dön</button>";
  html += "<button type='button' class='btn btn-sec' onclick='clearThr()'>Eşiği kaldır (yeniden kalibre et)</button></div></div>";

  html += "<div id='sAyar' class='screen'><button type='button' class='btn btn-sec' onclick='goMain()'>Ana sayfaya dön</button>";
  html += "<h3 style='color:#0056b3;margin:12px 0 8px;'>Ayarlar</h3>";
  html += "<p class='hint'>Pompa arızası alarmında röle, girdiğiniz süre (saniye) boyunca aktif kalır. Boru patlama: <strong>30 sn</strong> uyarı, 10 dk içinde <strong>3</strong> uyarıda ana kesilir. Tıkanma: akış <strong>0</strong>, <strong>1,5 dk</strong> sürekli → mandal.</p>";
  html += "<div class='input-group'><label>Pompa arızası rölesi açık kalma süresi <span class='u'>sn</span></label><input type='number' id='relPompaInAyar' min='1' max='";
  html += "<div class='input-group' style='display:flex;align-items:center;gap:10px;'><input type='checkbox' id='inv32' style='width:18px;height:18px;'><label for='inv32' style='margin:0;'>Pompa verisi yönünü ters çevir</label></div>";
  html += "<button type='button' class='btn btn-go' onclick='saveRel2()'>Kaydet</button></div>";

  html += "<div id='sAkim' class='screen'>";
  html += "<button type='button' class='btn btn-sec' onclick='goMain()'>Ana sayfaya dön</button>";
  html += "<p class='hint' style='text-align:left;margin:10px 0 14px;line-height:1.55;'><strong>Pompa kalibrasyonu:</strong> Pompa dururken boşta okuma → pompayı çalıştırıp test süresi → ortalama pompa verisi → + ek → izleme süresi (dakika).</p>";

  html += "<div id='p0' class='clog-step panel clog-panel on'><h3>Güvenlik</h3>";
  html += "<p class='hint' style='text-align:left;'><strong>Pompayı durdurun</strong>; sensör pompa manyetiğini görmemeli. Sonraki adımda boşta değere yakın okuma alınır.</p>";
  html += "<p class='big-stop'>Lütfen pompayı durdurun.</p>";
  html += "<button type='button' class='btn btn-go btn-big-ack' onclick='pumpAckStop()'>Pompayı durdurdum, devam</button></div>";

  html += "<div id='p1' class='clog-step panel clog-panel'><h3>1 &mdash; Test süresi</h3>";
  html += "<p class='hint' style='text-align:left;'>Pompayı normal doz gibi çalıştırın. Seçilen süre boyunca <strong>pompa verisi</strong> örneklenir ve ortalama alınır.</p>";
  html += "<button type='button' class='btn-dur' onclick='pumpPickDur(30000)'>30 sn</button>";
  html += "<button type='button' class='btn-dur' onclick='pumpPickDur(60000)'>1 dk</button>";
  html += "<button type='button' class='btn-dur' onclick='pumpPickDur(120000)'>2 dk</button><br>";
  html += "<button type='button' class='btn-dur' onclick='pumpPickDur(180000)'>3 dk</button>";
  html += "<button type='button' class='btn-dur' onclick='pumpPickDur(300000)'>5 dk</button>";
  html += "<button type='button' class='btn-dur' onclick='pumpPickDur(600000)'>10 dk</button></div>";

  html += "<div id='p2' class='clog-step panel clog-panel'><h3>2 &mdash; Test</h3>";
  html += "<p id='pumpCd' style='font-size:24px;font-weight:bold;color:#0b7a28;margin:8px 0;'>Kalan süre: —</p>";
  html += "<p class='hint' style='text-align:left;'>Test bitince ortalama <strong>pompa verisi</strong> hesaplanır.</p>";
  html += "<div class='row'><div class='pill' style='background:#eafff1;'><span>Ortalama pompa verisi</span><span id='pumpRunDisp'>0</span></div>";
  html += "<div class='pill' style='background:#eafff1;'><span>Anlık pompa verisi</span><span id='pumpCurDisp'>0</span></div></div>";
  html += "<button type='button' class='btn btn-sec' onclick='pumpCancel()'>İptal</button></div>";

  html += "<div id='p3' class='clog-step panel clog-panel'><h3>3 &mdash; Ortalama</h3>";
  html += "<p class='hint' style='text-align:left;'>Test tamam. Ortalama pompa verisi:</p>";
  html += "<p style='font-size:28px;font-weight:bold;color:#0b7a28;text-align:center;' id='pumpAvgDisp'>0</p>";
  html += "<button type='button' class='btn btn-go' onclick='pumpNext()'>İleri</button></div>";

  html += "<div id='p4' class='clog-step panel clog-panel'><h3>4 &mdash; + Ek</h3>";
  html += "<p class='hint' style='text-align:left;'>Ortalamaya ne kadar pay eklensin? Üst limit = ortalama + ek.</p>";
  html += "<div class='input-group'><label>Ek pay</label><input type='number' id='pumpAddIn' value='0' min='0'></div>";
  html += "<button type='button' class='btn btn-go' onclick='pumpAdd()'>Üst limiti kaydet</button></div>";

  html += "<div id='p5' class='clog-step panel clog-panel'><h3>5 &mdash; İzleme</h3>";
  html += "<p class='hint' style='text-align:left;'>Seçtiğiniz <strong>dakika</strong> boyunca cihaz pompa verisini izler. Bu süre içinde değer bir kez bile kayıtlı <strong>üst limiti geçmezse</strong> (pompa beklenen aralıkta değil) <strong>pompa arızası</strong> alarmı verilir.</p>";
  html += "<button type='button' class='btn btn-dur btn-obs' onclick='pumpPickObs(1)'>1 dk</button>";
  html += "<button type='button' class='btn btn-dur btn-obs' onclick='pumpPickObs(5)'>5 dk</button>";
  html += "<button type='button' class='btn btn-dur btn-obs' onclick='pumpPickObs(10)'>10 dk</button>";
  html += "<button type='button' class='btn btn-dur btn-obs' onclick='pumpPickObs(15)'>15 dk</button>";
  html += "<button type='button' class='btn btn-dur btn-obs' onclick='pumpPickObs(20)'>20 dk</button>";
  html += "<button type='button' class='btn btn-dur btn-obs' onclick='pumpPickObs(30)'>30 dk</button>";
  html += "<button type='button' class='btn btn-dur btn-obs' onclick='pumpPickObs(40)'>40 dk</button>";
  html += "<button type='button' class='btn btn-dur btn-obs' onclick='pumpPickObs(60)'>60 dk</button></div>";

  html += "<div id='p6' class='clog-step panel clog-panel'><h3>Özet</h3>";
  html += "<p class='hint' style='text-align:left;'>Kayıtlı minimum pompa verisi: <strong><span id='pumpLimDisp'>0</span></strong><br>";
  html += "Arıza süresi <span class='u'>dk</span>: <strong><span id='pumpSureDisp'>1 dk</span></strong></p>";
  html += "<button type='button' class='btn btn-sec' onclick='pumpClear()'>Yeniden kalibre et (sıfırla)</button>";
  html += "<button type='button' class='btn btn-sec' onclick='goMain()'>Ana sayfaya dön</button></div>";
  html += "<p style='font-size:10px;color:#ccc;margin-top:14px;'>AKTEK ELEKTRONİK ÇÖZÜMLERİ 2026</p></div></body></html>";
  return html;
}

void setup() {
  Serial.begin(115200);
  pinMode(PIN_HALL_ADC, INPUT);
  pinMode(PIN_FLOW, INPUT);
  pinMode(PIN_RESET, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(PIN_FLOW), isrFlowPulse, RISING);
  flowRateTickMs = millis();
  analogReadResolution(12);
  analogSetPinAttenuation(PIN_HALL_ADC, ADC_11db);

  pinMode(LED_SISTEM, OUTPUT);
  pinMode(ROLE_POMPA_ANA, OUTPUT);
  pinMode(ROLE_AKIS_TIKANIK, OUTPUT);
  pinMode(ROLE_BORU_PAT, OUTPUT);
  pinMode(ROLE_POMPA_ARIZA, OUTPUT);
  digitalWrite(LED_SISTEM, HIGH);
  digitalWrite(ROLE_AKIS_TIKANIK, CLOG_RELAY_NORMAL);
  digitalWrite(ROLE_BORU_PAT, LOW);
  // Pompa arıza pini ters: arıza=LOW (kapalı), normal=HIGH (açık)
  digitalWrite(ROLE_POMPA_ARIZA, HIGH);

  hafiza.begin("aktek", false);
  // Hall "boşta" ADC (pompa dururken AKIM:ZERO ile kalibre edin)
  akimZeroAdc = hafiza.getInt("iZero", -1);
  if (akimZeroAdc < 0 || akimZeroAdc > ADC_MAX) akimZeroAdc = 2048;
  usePeakThreshold = hafiza.getBool("usePeak", false);
  peakThreshold = hafiza.getInt("peakThr", 0);
  patKalibTepe = hafiza.getInt("peakTst", 0);
  invertCcaAdc = hafiza.getBool("inv32", false);
  if (usePeakThreshold && peakThreshold > 0) wizardPhase = WZ_ARMED;
  else wizardPhase = WZ_IDLE;
  hafizaClogYukle();
  hafizaRelYukle();
  hafizaPompaYukle();
  hafizaAnaLockYukle();
  digitalWrite(ROLE_AKIS_TIKANIK, alarmClogAktif ? CLOG_RELAY_FAULT : CLOG_RELAY_NORMAL);
  digitalWrite(ROLE_POMPA_ANA, anaRoleMandalli ? PUMP_CUT_LEVEL : PUMP_RUN_LEVEL);
  Serial.printf("Acilis: NVS anaLock=%d nedeni=%u patMnd=%d -> GPIO%d ana=%d\n",
                anaRoleMandalli ? 1 : 0, (unsigned)anaLockReason, alarmPatMandal ? 1 : 0,
                ROLE_POMPA_ANA,
                anaRoleMandalli ? (PUMP_CUT_LEVEL == HIGH ? 1 : 0) : (PUMP_RUN_LEVEL == HIGH ? 1 : 0));
  if (anaRoleMandalli)
    Serial.println("Ana role: NVS kilidi aktif (boru patlak veya akis yok).");
  pompaWindowStart = millis();
  hallSonYuksekMs = millis();

  if (hafiza.getInt("iZero", -1) == -1) {
    akimZeroAdc = hallZeroOlcAdc();
    hafiza.putInt("iZero", akimZeroAdc);
  }

  WiFi.mode(WIFI_AP);
  WiFi.softAPConfig(apIP, apIP, IPAddress(255, 255, 255, 0));
  {
    uint8_t mac[6];
    if (esp_read_mac(mac, ESP_MAC_WIFI_STA) != ESP_OK) {
      uint64_t em = ESP.getEfuseMac();
      for (int i = 0; i < 6; i++)
        mac[i] = (uint8_t)(em >> (8 * (5 - i)));
    }
    snprintf(g_apSsid, sizeof(g_apSsid), "AKTEK_KLOR_KONTROL_%02X%02X%02X",
             mac[3], mac[4], mac[5]);
    WiFi.softAP(g_apSsid, "aktek2026", WIFI_AP_CHANNEL, 0, WIFI_AP_MAX_CONN);
    Serial.printf("AP SSID: %s kanal=%u max=%u\n", g_apSsid, (unsigned)WIFI_AP_CHANNEL,
                  (unsigned)WIFI_AP_MAX_CONN);
  }
  wifiApHealthMs = millis();
  // AP iken Wi‑Fi güç tasarrufu bazen gecikme yaratır; sürekli iş yükünde kapatmak daha stabil.
  esp_wifi_set_ps(WIFI_PS_NONE);

  dnsServer.start(DNS_PORT, "*", apIP);
  g_apHtmlCache = getHTML();
  if (g_apHtmlCache.length() == 0) {
    g_apHtmlCache = "<!DOCTYPE html><html><head><meta charset='UTF-8'></head><body>HTML hatasi</body></html>";
  }
  Serial.printf("AP HTML: %u byte (Web arayuzu)\n", (unsigned)g_apHtmlCache.length());
  server.on("/generate_204", []() { server.send(204); });
  server.on("/gen_204", []() { server.send(204); });
  server.on("/connecttest.txt", []() { server.send(200, "text/plain", "Microsoft Connect Test"); });
  server.on("/ncsi.txt", []() { server.send(200, "text/plain", "Microsoft NCSI"); });
  server.on("/hotspot-detect.html", []() { server.send(200, "text/html", "<HTML><HEAD><TITLE>Success</TITLE></HEAD><BODY>Success</BODY></HTML>"); });
  server.on("/canonical.html", []() { server.send(204); });
  server.on("/success.txt", []() { server.send(200, "text/plain", "success"); });
  server.on("/favicon.ico", []() { server.send(204); });
  server.on("/update", HTTP_GET, otaUpdatePage);
  server.on("/update", HTTP_POST, otaUpdateDone, otaUpdateUpload);
  /** Ana sayfa: tam HTML yalnız burada. onNotFound'da aynı dev dosyayı göndermek telefon captive trafiğinde CPU'yu kilitler. */
  server.on("/", HTTP_GET, []() { server.send(200, "text/html", g_apHtmlCache); });
  server.onNotFound([]() {
    server.send(200, "text/plain", "OK");
  });
  server.begin();
  webSocket.begin();
  webSocket.onEvent(webSocketEvent);
  Serial.println("Hazir.");
}

void loop() {
  pollPendingAkimZero();
  pollResetButton();
  updateLedsPatTik();
  serviceNetworkBrief();

  if (millis() - flowRateTickMs >= 1000UL) {
    int delta = (int)(flowPulseCounter - flowPulseTotalLastSec);
    if (delta < 0) delta = 0;
    flowLastWindowDelta = delta;
    flowPps = delta;
    flowPulseTotalLastSec = flowPulseCounter;
    flowRateTickMs = millis();
  }
  if (flowPulseCounter != flowPulseSnapPrev) {
    lastFlowPulseMs = millis();
    flowPulseSnapPrev = flowPulseCounter;
  }
  doseAkisGuncelle();

  static unsigned long flowDozKisaTickMs = 0;
  static uint32_t flowPulseDozKisaRef = 0;
  static bool flowDozKisaInit = false;
  if (!flowDozKisaInit) {
    noInterrupts();
    flowPulseDozKisaRef = flowPulseCounter;
    interrupts();
    flowDozKisaTickMs = millis();
    flowDozKisaInit = true;
  } else if (millis() - flowDozKisaTickMs >= FLOW_DOZ_PENCERE_MS) {
    noInterrupts();
    uint32_t c = flowPulseCounter;
    interrupts();
    if (c >= flowPulseDozKisaRef) {
      uint32_t d = c - flowPulseDozKisaRef;
      flowPpsDozKisa = (int)(d * 1000UL / FLOW_DOZ_PENCERE_MS);
    } else {
      flowPpsDozKisa = 0;
    }
    flowPulseDozKisaRef = c;
    flowDozKisaTickMs = millis();
  }

  int hallAdc = okuHallMedian5();
  int akimMa = abs(hallAdc - akimZeroAdc);
  int akimAdc = hallAdc;

  int akisDoz = doseAkisGoster();  // son doz toplam darbesi (ekran + tıkanma)
  int cca = flowPps;               // sn başına darbe (boru patlama kalibrasyonu)
  int ccb = (flowPpsDozKisa > flowPps) ? flowPpsDozKisa : flowPps;
  updateSessionStats(akisDoz);
  updateAkimSessionStatsMa(akimMa);

  const int HALL_THR = pompaAkimUstLimitMa;
  bool hallRun = (HALL_THR > 0) && (akimMa > HALL_THR);
  // Tıkanma yalnız pompa çalışırken (Hall) izlenir; pompa arızası öncelikli — motor durunca akış düşüşü tıkanma değildir.
  const bool allowClogMonitor =
      !alarmPompaAktif && (HALL_THR <= 0 || hallRun);

  static unsigned long tHallWeak0 = 0;
  if (hallRun) {
    hallSonYuksekMs = millis();
    tHallWeak0 = 0;
  } else {
    if (tHallWeak0 == 0)
      tHallWeak0 = millis();
  }
  unsigned long durHallWeak = (tHallWeak0 > 0) ? (millis() - tHallWeak0) : 0;
  bool flowActive = (millis() - lastFlowPulseMs) < 3500UL;

  if (wizardPhase == WZ_TEST) {
    if ((long)(millis() - testEndMs) >= 0) {
      wizardPhase = WZ_SHOW_MAX;
    } else if (cca > testPeak) {
      testPeak = cca;
    }
  }

  bool patIzleAktif = usePeakThreshold && peakThreshold > 0 && wizardPhase == WZ_ARMED;
  bool patCcaUstu = patIzleAktif && (cca > peakThreshold);
  if (!patIzleAktif) {
    boruPatUstSayacSifirla();
  } else {
    if (!alarmPatAktif && patCcaUstu && !boruPatOncekiCcaUstu)
      boruPatUstCikisSayaci++;
    boruPatOncekiCcaUstu = patCcaUstu;
  }

  bool tripPat = !alarmPatAktif && !alarmPatMandal && !anaRoleMandalli && patIzleAktif &&
                 patCcaUstu && (boruPatUstCikisSayaci >= BORU_PAT_ARIZA_UST_CIKIS_SAYISI);
  if (tripPat) {
    unsigned long nowPat = millis();
    patUyariKayitEkle(nowPat);
    int uyariSay = patUyariPencereSay(nowPat);
    alarmPatAktif = true;
    alarmPatMandal = false;
    boruPatUstCikisSayaci = 0;
    boruPatOncekiCcaUstu = patCcaUstu;
    if (uyariSay >= PAT_UYARI_MANDAL_ADET) {
      patUyariModu = false;
      patUyariBaslaMs = 0;
      anaRoleMandalli = true;
      hafizaAnaLockKaydet(LOCK_PAT);
      hafiza.putBool("patMnd", false);
      anaLockReason = LOCK_PAT;
      anaLockSinceMs = nowPat;
      kalanPatSn = 0;
      patUyariKayitSifirla();
      Serial.printf("Boru patlak: 10 dk icinde %d uyari — ana pompa mandal\n", uyariSay);
    } else {
      patUyariModu = true;
      patUyariBaslaMs = nowPat;
      Serial.printf("Boru patlak: uyari %d/%d (30 sn, ana pompa acik)\n", uyariSay,
                    PAT_UYARI_MANDAL_ADET);
    }
  }
  if (patUyariModu && alarmPatAktif && !anaRoleMandalli) {
    unsigned long heldUy = millis() - patUyariBaslaMs;
    if (heldUy < PAT_UYARI_SURE_MS) {
      unsigned long leftMs = PAT_UYARI_SURE_MS - heldUy;
      kalanPatSn = (int)((leftMs + 999UL) / 1000UL);
    } else {
      patUyariModu = false;
      alarmPatAktif = false;
      patUyariBaslaMs = 0;
      kalanPatSn = 0;
      boruPatUstSayacSifirla();
      Serial.println("Boru patlak: 30 sn uyari bitti");
    }
  } else if (anaRoleMandalli && anaLockReason == LOCK_PAT) {
    alarmPatAktif = true;
    bool belowPat =
        patIzleAktif && (cca <= peakThreshold) && (ccb <= peakThreshold);
    if (!belowPat) {
      if (!alarmPatMandal) {
        alarmPatMandal = true;
        hafiza.putBool("patMnd", true);
      }
    }
    kalanPatSn = 0;
  } else if (!alarmPatAktif && !alarmPatMandal) {
    kalanPatSn = 0;
  }

  // --- Akis yok / tikanma ---
  if (alarmPompaAktif) clogAlarmPompaArizaTemizle();
  if (alarmClogAktif && !alarmPompaAktif) {
    kalanClogSn = 0;
    if (!anaRoleMandalli || anaLockReason != LOCK_CLOG) {
      anaRoleMandalli = true;
      anaLockReason = LOCK_CLOG;
      if (anaLockSinceMs == 0) anaLockSinceMs = millis();
      hafizaAnaLockKaydet(LOCK_CLOG);
    }
  }
  if (allowClogMonitor) {
    if (clogArmed && clogObserveMs > 0 && !alarmClogAktif) {
      // Doz toplamı > 0 olunca sayaç sıfırlanır; 1,5 dk hiç doz yok → tıkanma
      if (clogAkisVar()) {
        clogWindowStart = millis();
      } else if ((millis() - clogWindowStart) >= clogObserveMs) {
        alarmClogAktif = true;
        alarmClogBasla = millis();
        clogWindowStart = millis();
        anaRoleMandalli = true;
        hafizaAnaLockKaydet(LOCK_CLOG);
        anaLockReason = LOCK_CLOG;
        anaLockSinceMs = millis();
        Serial.println("Tikanma: ana pompa mandal (reset 3 sn ile acilir)");
      }
    }
  }

  // Pompa çevrimi "off" iken akış sürmesi (manyetik zayıf 4–60 sn, yakında pompa görülmüştü)
  static bool stuckLatch = false;
  if (hallRun || !flowActive)
    stuckLatch = false;
  if (!alarmStuckAktif && !stuckLatch && HALL_THR > 0 && flowActive && durHallWeak > 4000UL &&
      durHallWeak < 60000UL && (millis() - hallSonYuksekMs < 120000UL)) {
    alarmStuckAktif = true;
    stuckLatch = true;
    alarmStuckBasla = millis();
    // Not: alarmStuckAktif artık pompa arıza çıkışını sürmez.
    // Pompa arıza pini sadece alarmPompaAktif ile kontrol edilir.
  }
  if (alarmStuckAktif) {
    if (HALL_THR > 0 && flowActive && durHallWeak > 4000UL && durHallWeak < 60000UL && (millis() - hallSonYuksekMs < 120000UL))
      alarmStuckBasla = millis();
    unsigned long g = millis() - alarmStuckBasla;
    unsigned long ms = (unsigned long)relPompaSaniye * 1000UL;
    if (g >= ms) {
      alarmStuckAktif = false;
      kalanStuckSn = 0;
    } else {
      kalanStuckSn = (int)((ms - g) / 1000UL);
    }
  }

  // --- Pompa arızası (Hall uzun süre eşik üstüne çıkmıyor) ---
  if (!alarmPompaAktif && pompaAkimUstLimitMa > 0 && pompaArizaSureDk > 0) {
    if (akimMa > pompaAkimUstLimitMa) {
      pompaWindowStart = millis();
    } else {
      unsigned long winMs = (unsigned long)pompaArizaSureDk * 60UL * 1000UL;
      if ((millis() - pompaWindowStart) >= winMs) {
        alarmPompaAktif = true;
        alarmPompaBasla = millis();
        clogAlarmPompaArizaTemizle();
        digitalWrite(ROLE_POMPA_ARIZA, LOW);
        pompaWindowStart = millis();
        Serial.println("Pompa arizasi: tikanma/mandal yok sayildi");
      }
    }
  }
  if (alarmPompaAktif && pompaAkimUstLimitMa > 0 && akimMa > pompaAkimUstLimitMa) {
    alarmPompaAktif = false;
    kalanPompaSn = 0;
    if (!alarmStuckAktif)
      digitalWrite(ROLE_POMPA_ARIZA, HIGH);
    pompaWindowStart = millis();
  }
  if (alarmPompaAktif) {
    if (pompaAkimUstLimitMa > 0 && akimMa <= pompaAkimUstLimitMa)
      alarmPompaBasla = millis();
    unsigned long g = millis() - alarmPompaBasla;
    unsigned long ms = (unsigned long)relPompaSaniye * 1000UL;
    if (g >= ms) {
      alarmPompaAktif = false;
      kalanPompaSn = 0;
      if (!alarmStuckAktif)
        digitalWrite(ROLE_POMPA_ARIZA, HIGH);
    } else {
      kalanPompaSn = (int)((ms - g) / 1000UL);
    }
  }

  // Ana hat + boru patlak + tıkanma rölesi: tek yerden sür (her karede tutarlı)
  // GPIO19 ana: mandal=HIGH pompayı keser (PUMP_CUT_LEVEL), mandal yok=LOW çalıştırır (PUMP_RUN_LEVEL)
  // GPIO22 boru patlak: uyari veya mandal → HIGH
  // GPIO14 tıkanma ters: arıza=LOW, normal=HIGH
  digitalWrite(ROLE_AKIS_TIKANIK, alarmClogAktif ? CLOG_RELAY_FAULT : CLOG_RELAY_NORMAL);
  digitalWrite(ROLE_BORU_PAT, (alarmPatAktif || alarmPatMandal) ? HIGH : LOW);
  digitalWrite(ROLE_POMPA_ANA, anaRoleMandalli ? PUMP_CUT_LEVEL : PUMP_RUN_LEVEL);

  if (pumpPh == PZ_TEST) {
    if ((long)(millis() - pumpTestEndMs) >= 0) {
      if (pumpSampleCount > 0)
        pumpAvgAbsMa = (int)lroundf((float)pumpSumAbsMa / (float)pumpSampleCount);
      else
        pumpAvgAbsMa = 0;
      pumpRunAvgAbsMa = pumpAvgAbsMa;
      pumpPh = PZ_SHOW_AVG;
    } else {
      unsigned long now = millis();
      if (pumpLastSampleMs == 0 || (now - pumpLastSampleMs) >= 120UL) {
        pumpLastSampleMs = now;
        pumpSumAbsMa += (long)akimMa;
        pumpSampleCount++;
        pumpRunAvgAbsMa =
            (pumpSampleCount > 0) ? (int)lroundf((float)pumpSumAbsMa / (float)pumpSampleCount) : 0;
      }
    }
  }
  pumpCurAbsMa = akimMa;

  static unsigned long serialDbgMs = 0;
  if (millis() - serialDbgMs >= SERIAL_DBG_INTERVAL_MS) {
    serialDbgMs = millis();
    const int pencKlnSn = alarmClogAktif ? -1 : clogAnaKalanSn();
    const unsigned int altStreakS =
        alarmClogAktif
            ? 0U
            : ((clogArmed && !clogAkisVar())
                   ? (unsigned int)((millis() - clogWindowStart) / 1000UL)
                   : 0U);
    const unsigned alm = (unsigned)(alarmPatAktif ? 2 : 0) | (unsigned)(alarmClogAktif ? 4 : 0) |
                          (unsigned)(alarmPompaAktif ? 16 : 0) | (unsigned)(alarmStuckAktif ? 32 : 0);
    const int clogUstDisp =
        clogArmed ? (clogAkisVar() ? 1 : 0) : -1;
    const int clogThrDbg = clogArmed ? clogThreshold : -1;
    const int pinPat = alarmPatAktif ? 1 : 0;
    const int pinAna = anaRoleMandalli ? (PUMP_CUT_LEVEL == HIGH ? 1 : 0) : (PUMP_RUN_LEVEL == HIGH ? 1 : 0);
    Serial.printf(
        "DBG doz=%d pps=%d heap=%u ws=%u | alm=%u lock=%d patAl=%d GPIO%d=%d GPIO%d=%d\n",
        akisDoz, cca, (unsigned)ESP.getFreeHeap(), (unsigned)webSocket.connectedClients(), alm,
        anaRoleMandalli ? 1 : 0, alarmPatAktif ? 1 : 0, ROLE_POMPA_ANA, pinAna, ROLE_BORU_PAT, pinPat);
  }

  if (millis() - lastUpdate >= 150 && webSocket.connectedClients() > 0) {
    uint8_t ph = (uint8_t)wizardPhase;
    int tleft = testKalanSaniye();
    int thrSend = (usePeakThreshold && wizardPhase == WZ_ARMED) ? peakThreshold : 0;
    int tPeakSend = (wizardPhase == WZ_ARMED) ? patKalibTepe : testPeak;
    int ctp = clogTestKalanSn();
    int cthr = clogArmed ? clogThreshold : 0;
    int obs = clogArmed ? clogObsMin : 0;
    int clogAkSn = clogAnaKalanSn();
    uint8_t clogAkMd = clogAnaMod();
    char wsBuf[580];
    int n = snprintf(wsBuf, sizeof(wsBuf),
                     "%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d",
                     akisDoz, alarmPatAktif ? 1 : 0, kalanPatSn, sessMin, sessMax, (int)ph, tleft, tPeakSend, thrSend,
                     ccb, (int)clogPh, ctp, clogTestPeak, cthr, obs, alarmClogAktif ? 1 : 0, kalanClogSn,
                     clogAkSn, (int)clogAkMd, akimMa, akimSessMinMa, akimSessMaxMa, akimAdc,
                     akimZeroAdc, alarmPompaAktif ? 1 : 0, kalanPompaSn, pompaAkimUstLimitMa, pompaArizaSureDk,
                     relPompaSaniye, (int)pumpPh, pumpTestKalanSn(), pumpAvgAbsMa, pumpRunAvgAbsMa, pumpCurAbsMa,
                     invertCcaAdc ? 1 : 0, anaRoleMandalli ? 1 : 0, (int)anaLockReason);
    if (n > 0 && n < (int)sizeof(wsBuf)) {
      webSocket.broadcastTXT((uint8_t *)wsBuf, (size_t)n);
    }
    lastUpdate = millis();
  }
}
Editor is loading...
Leave a Comment