Untitled

 avatar
unknown
plain_text
a month ago
100 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>

// --- GİRİŞLER ---
const int PIN_HALL_ADC = 32;      // analog Hall (pompa mıknatısı)
const int PIN_FLOW = 25;
/** Reset: Butona basılı tutma, 3 sn kilidi açar (dahili pull-up). */
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ı

void IRAM_ATTR isrFlowPulse() { flowPulseCounter++; }

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;
/** Vakum arızası — boru patlama ile aynı eşik (peakThreshold): akış bu değerin altına 1 dk hiç inmezse tetiklenir. NC röle. */
const int ROLE_VAKUM_ARIZA = 27;

/** Eşik altına hiç inilmeden geçmesi gereken süre (boru patlama kalibrasyonundaki peakThreshold ile karşılaştırılır). */
static const unsigned long VAKUM_ALTINA_INMEME_MIN_MS = 60UL * 1000UL;
/** Eşik altına iniş gürültüsü: bu kadar ms üst üste “altında” görülmeden 60 sn sayacı sıfırlanmaz (tekrar tetik için). */
static const unsigned long VAKUM_ALTI_DEBOUNCE_MS = 450UL;

/** 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;

/** Boru patlak veya akış yok (tıkanma) sonrası ana röle mandalı; NVS’te saklanır, reset ile açılır. */
static bool anaRoleMandalli = false;
enum AnaLockReason : uint8_t { LOCK_NONE = 0, LOCK_PAT = 1, LOCK_CLOG = 2 };
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;

// Akış yok / tıkanma kaynaklı kilit: 2 dk içinde düzelirse otomatik aç
static const unsigned long AUTO_UNLOCK_CLOG_MS = 120000UL;

int relPatSaniye = 10;
int relTikSaniye = 10;
int relPompaSaniye = 10;
int relVakumSaniye = 10;
/** Boru patlak / tıkanma / pompa / vakum röle açık kalma süresi (sn) üst sınırı — en fazla 12 saat; web ve SET:* ile aynı. */
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 alarmSifonAktif = false;
unsigned long alarmSifonBasla = 0;
int kalanSifonSn = 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;
unsigned long alarmPatBasla = 0;
int kalanPatSn = 0;

/** Boru patlak mandalı: akış eşiğinin üstüne bu kadar kez art arda (her yükseliş için önce eşik altına inilmiş) çıkınca devreye girer. */
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;
}

bool alarmVakumAktif = false;
unsigned long alarmVakumBasla = 0;
int kalanVakumSn = 0;
/** cca >= peakThreshold kesintisiz kaldığı süreyi ölçer; eşik altı debounce sonrası sıfırlanır. */
static unsigned long vakumEsikUstuSeriBaslaMs = 0;
static unsigned long vakumAltindaSeriBaslaMs = 0;

const uint8_t CLOG_OFF = 10;
uint8_t clogPh = CLOG_OFF;
unsigned long clogTestEndMs = 0;
int clogTestPeak = 0;
int clogThreshold = 0;
int clogObsMin = 0;
unsigned long clogObserveMs = 0;
unsigned long clogWindowStart = 0;
bool clogArmed = false;

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

static void hafizaRelYukle() {
  relPatSaniye = hafiza.getInt("relPat", 10);
  relTikSaniye = hafiza.getInt("relTik", 10);
  relPompaSaniye = hafiza.getInt("relPmp", 10);
  relVakumSaniye = hafiza.getInt("relVak", 10);
  if (relPatSaniye < 1) relPatSaniye = 1;
  if (relPatSaniye > REL_SANIYE_MAX) relPatSaniye = REL_SANIYE_MAX;
  if (relTikSaniye < 1) relTikSaniye = 1;
  if (relTikSaniye > REL_SANIYE_MAX) relTikSaniye = REL_SANIYE_MAX;
  if (relPompaSaniye < 1) relPompaSaniye = 1;
  if (relPompaSaniye > REL_SANIYE_MAX) relPompaSaniye = REL_SANIYE_MAX;
  if (relVakumSaniye < 1) relVakumSaniye = 1;
  if (relVakumSaniye > REL_SANIYE_MAX) relVakumSaniye = 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", clogArmed);
  hafiza.putInt("clogThr", clogThreshold);
  hafiza.putInt("clogObs", clogObsMin);
  hafiza.putInt("clogTpk", clogTestPeak);
}

void hafizaClogYukle() {
  clogArmed = hafiza.getBool("clogArm", false);
  clogThreshold = hafiza.getInt("clogThr", 0);
  clogObsMin = hafiza.getInt("clogObs", 0);
  clogTestPeak = 0;
  int om = clogObsMin < 1 ? 1 : clogObsMin;
  clogObserveMs = (unsigned long)om * 60UL * 1000UL;
  if (clogArmed && clogThreshold > 0 && clogObsMin > 0) {
    clogPh = 6;
    clogWindowStart = millis();
    clogTestPeak = hafiza.getInt("clogTpk", 0);
  } else {
    clogArmed = false;
    clogPh = CLOG_OFF;
  }
}

void clogSifirla() {
  clogPh = CLOG_OFF;
  clogTestEndMs = 0;
  clogTestPeak = 0;
  clogThreshold = 0;
  clogObsMin = 0;
  clogObserveMs = 0;
  clogArmed = false;
  clogWindowStart = millis();
  hafiza.putBool("clogArm", false);
  hafiza.putInt("clogThr", 0);
  hafiza.putInt("clogObs", 0);
  hafiza.putInt("clogTpk", 0);
}

static void pollResetButton() {
  bool pressed = (digitalRead(PIN_RESET) == LOW);
  if (pressed) {
    if (resetPressStartMs == 0)
      resetPressStartMs = millis();
    else if (!resetAwaitRelease && (millis() - resetPressStartMs >= RESET_HOLD_MS)) {
      bool any = false;
      if (anaRoleMandalli) {
        anaRoleMandalli = false;
        hafiza.putBool("anaLock", false);
        boruPatUstSayacSifirla();
        anaLockReason = LOCK_NONE;
        anaLockSinceMs = 0;
        any = true;
      }
      // Boru tıkanma alarmı (mandallı): butona basana kadar çıkış aktif kalsın.
      if (alarmClogAktif || alarmSifonAktif) {
        alarmClogAktif = false;
        alarmSifonAktif = false;
        kalanClogSn = 0;
        kalanSifonSn = 0;
        digitalWrite(ROLE_AKIS_TIKANIK, LOW);
        clogWindowStart = millis();
        any = true;
      }
      if (any) Serial.println("Reset: kilit/alarm sıfırlandı (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() {
  // Vakum arızası devre dışı (alarmVakumAktif hesaplamaya katılmaz)
  bool fault = alarmPatAktif || alarmClogAktif || alarmSifonAktif || 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 || clogThreshold <= 0 || clogObserveMs == 0 || alarmClogAktif) return -1;
  // Her zaman alarm ile aynı zaman: eşik altında geçen süre clogWindowStart’tan; eşik üstünde pencere sürekli yenilenir
  return clogPencereKalanSn();
}

uint8_t clogAnaMod() {
  if (!clogArmed || clogThreshold <= 0 || 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.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:RECAL") {
    clogSifirla();
    clogPh = 0;
    return;
  }

  if (msg == "CLOG:OPEN") {
    if (clogArmed && clogThreshold > 0) {
      clogPh = 6;
    } else {
      clogSifirla();
      clogPh = 0;
    }
    return;
  }
  if (msg == "CLOG:ACK_PUMP" && clogPh == 0) {
    clogPh = 1;
    return;
  }
  if (msg.startsWith("CLOG:TEST:") && clogPh == 1) {
    unsigned long dur = (unsigned long)msg.substring(10).toInt();
    if (dur < 30000UL) dur = 30000UL;
    clogTestEndMs = millis() + dur;
    clogTestPeak = 0;
    clogPh = 2;
    return;
  }
  if (msg == "CLOG:NEXT" && clogPh == 3) {
    clogPh = 4;
    return;
  }
  if (msg.startsWith("CLOG:OFF:") && clogPh == 4) {
    int add = msg.substring(9).toInt();
    if (add < 0) add = 0;
    clogThreshold = clogTestPeak + add;
    clogPh = 5;
    return;
  }
  if (msg.startsWith("CLOG:OBS:") && clogPh == 5) {
    int m = msg.substring(9).toInt();
    if (m < 1) m = 1;
    if (m > 240) m = 240;
    clogObsMin = m;
    clogObserveMs = (unsigned long)m * 60UL * 1000UL;
    clogArmed = true;
    clogPh = 6;
    clogWindowStart = millis();
    hafizaClogKaydet();
    return;
  }
  if (msg == "CLOG:CANCEL") {
    if (clogPh == 6 && clogArmed) {
      clogPh = CLOG_OFF;
      return;
    }
    clogSifirla();
    return;
  }
  if (msg == "CLOG:CLEAR") {
    clogSifirla();
    return;
  }

  if (msg.startsWith("SET:REL:")) {
    String r = msg.substring(8);
    int c = r.indexOf(',');
    if (c <= 0) return;
    int a = r.substring(0, c).toInt();              // patlama
    int b = r.substring(c + 1).toInt();             // tıkanma
    if (a < 1) a = 1;
    if (a > REL_SANIYE_MAX) a = REL_SANIYE_MAX;
    if (b < 1) b = 1;
    if (b > REL_SANIYE_MAX) b = REL_SANIYE_MAX;
    relPatSaniye = a;
    relTikSaniye = b;
    hafiza.putInt("relPat", relPatSaniye);
    hafiza.putInt("relTik", relTikSaniye);
    return;
  }

  if (msg.startsWith("SET:RELVAK:")) {
    int v = msg.substring(11).toInt();
    if (v < 1) v = 1;
    if (v > REL_SANIYE_MAX) v = REL_SANIYE_MAX;
    relVakumSaniye = v;
    hafiza.putInt("relVak", relVakumSaniye);
    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 += ".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 += ".alert-vac{background:#5b2d86;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,lastRelPat=10,lastRelTik=10,lastRelPompa=10,lastRelVakum=10,lastInv32=0,inv32Dirty=0,pumpAl=0,pumpK=0,pumpLim=0,pumpSure=0,pumpPh=0,pumpTl=0,pumpAvg=0,pumpRun=0,pumpCur=0,alV=0,kV=0;";
  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('sClog').classList.remove('show');el('sAyar').classList.remove('show');el('sAkim').classList.remove('show');}";
  html += "function openCal(){el('sAyar').classList.remove('show');el('sClog').classList.remove('show');send('CLOG:CANCEL');el('sMain').style.display='none';el('sCal').classList.add('show');syncCal();}";
  html += "function openClog(){el('sAyar').classList.remove('show');el('sCal').classList.remove('show');send('WIZ:CANCEL');el('sMain').style.display='none';el('sClog').classList.add('show');send('CLOG:OPEN');syncClog();updNextDoseStrip();}";
  html += "function openAkim(){el('sAyar').classList.remove('show');el('sCal').classList.remove('show');el('sClog').classList.remove('show');send('WIZ:CANCEL');send('CLOG: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('sClog').classList.remove('show');el('sAkim').classList.remove('show');el('sMain').style.display='none';el('sAyar').classList.add('show');if(el('relPatIn'))el('relPatIn').value=lastRelPat;if(el('relTikIn'))el('relTikIn').value=lastRelTik;if(el('relPompaInAyar'))el('relPompaInAyar').value=lastRelPompa;if(el('relVakumInAyar'))el('relVakumInAyar').value=lastRelVakum;if(el('inv32'))el('inv32').checked=(lastInv32==1);inv32Dirty=0;bindInv32();}";
  html += "function goMain(){if(el('sClog').classList.contains('show')){el('sClog').classList.remove('show');el('sMain').style.display='block';send('CLOG:CANCEL');return;}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=el('sClog').classList.contains('show')&&clogPh===6&&clogObs>0&&!alC&&clogAkSn>=0;if(ok){cw.style.display='block';cm.innerHTML=fmtMMSS(clogAkSn);if(cs)cs.textContent='E\\u015fik alt\\u0131nda ge\\u00e7en s\\u00fcre; \\u00fcst\\u00fcne \\u00e7\\u0131k\\u0131nca 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 syncClog(){if(clogPh>6)return;['g0','g1','g2','g3','g4','g5','g6'].forEach(function(id){el(id).classList.remove('on');});el('g'+clogPh).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){var rp=parseInt(p[19],10);if(!isNaN(rp)){rp=Math.max(1,Math.min(" + String(REL_SANIYE_MAX) + ",rp));lastRelPat=rp;if(!el('sAyar').classList.contains('show')&&el('relPatIn'))el('relPatIn').value=rp;}}";
  html += "if(p.length>20){var rt=parseInt(p[20],10);if(!isNaN(rt)){rt=Math.max(1,Math.min(" + String(REL_SANIYE_MAX) + ",rt));lastRelTik=rt;if(!el('sAyar').classList.contains('show')&&el('relTikIn'))el('relTikIn').value=rt;}}";
  html += "if(p.length>21)setAkim(parseInt(p[21],10)||0);else setAkim(0);";
  html += "if(p.length>23)setAkimMinMax(p[22],p[23]);else setAkimMinMax(0,0);";
  html += "if(p.length>24)setHamOlc(p[24]);else setHamOlc(0);";
  html += "if(p.length>27){pumpAl=parseInt(p[26],10)||0;pumpK=parseInt(p[27],10)||0;}else{pumpAl=0;pumpK=0;}";
  html += "if(p.length>29){pumpLim=parseInt(p[28],10)||0;pumpSure=parseInt(p[29],10)||0;}else{pumpLim=0;pumpSure=0;}";
  html += "if(p.length>30){var rp2=parseInt(p[30],10);if(!isNaN(rp2)){rp2=Math.max(1,Math.min(" + String(REL_SANIYE_MAX) + ",rp2));lastRelPompa=rp2;if(!el('sAyar').classList.contains('show')&&el('relPompaIn'))el('relPompaIn').value=rp2;if(el('relPompaInAyar')&&!el('sAyar').classList.contains('show'))el('relPompaInAyar').value=rp2;}}";
  html += "if(p.length>35){pumpPh=parseInt(p[31],10)||0;pumpTl=parseInt(p[32],10)||0;pumpAvg=parseInt(p[33],10)||0;pumpRun=parseInt(p[34],10)||0;pumpCur=parseInt(p[35],10)||0;}else{pumpPh=0;pumpTl=0;pumpAvg=0;pumpRun=0;pumpCur=0;}";
  html += "if(p.length>36){var iv=parseInt(p[36],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>37){var lk=parseInt(p[37],10)||0;if(el('latchInd'))el('latchInd').style.display=lk?'block':'none';}";
  html += "if(p.length>39){alV=parseInt(p[38],10)||0;kV=parseInt(p[39],10)||0;}else{alV=0;kV=0;}";
  html += "if(p.length>40){var rv=parseInt(p[40],10);if(!isNaN(rv)){rv=Math.max(1,Math.min(" + String(REL_SANIYE_MAX) + ",rv));lastRelVakum=rv;if(!el('sAyar').classList.contains('show')&&el('relVakumInAyar'))el('relVakumInAyar').value=rv;}}";
  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 += "el('clogCd').innerHTML=clogTl>0?('Kalan \\u00fcre: '+clogTl+' sn'):'S\\u00fcre tamamland\\u0131';";
  html += "el('clogTmax').innerHTML=clogTpk;el('clogTmaxBig').innerHTML=clogTpk;el('clogTmax2').innerHTML=clogTpk;";
  html += "el('clogThrDisp').innerHTML=clogThr;el('clogObsDisp').innerHTML=clogObs>0?(clogObs+' dakika'):'\\u2014';";
  html += "if(el('sCal').classList.contains('show'))syncCal();if(el('sClog').classList.contains('show'))syncClog();";
  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='Alarm: '+kC+' sn';}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('al-vak')){if(alV==1){el('al-vak').style.display='block';el('kalanVak').innerHTML='Alarm: '+kV+' sn';}else{el('al-vak').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> r\\u00f6le s\\u00fcreleri 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 clogTubeOk(){send('CLOG:ACK_PUMP');}";
  html += "function clogPickDur(ms){send('CLOG:TEST:'+ms);}";
  html += "function clogCancelTest(){send('CLOG:CANCEL');}";
  html += "function clogNextMax(){send('CLOG:NEXT');}";
  html += "function clogSaveThr(){var o=parseInt(el('clogOffIn').value,10)||0;if(o<0)o=0;send('CLOG:OFF:'+o);}";
  html += "function clogPickObs(m){send('CLOG:OBS:'+m);}";
  html += "function clogResetAll(){send('CLOG:CLEAR');showMain();}";
  html += "function recalPat(){send('WIZ:CLEARTHR');}";
  html += "function clogRecal(){send('CLOG:RECAL');}";
  html += "function saveRel(){var mx=" + String(REL_SANIYE_MAX) + ";var a=parseInt(el('relPatIn').value,10)||10;var b=parseInt(el('relTikIn').value,10)||10;a=Math.max(1,Math.min(mx,a));b=Math.max(1,Math.min(mx,b));lastRelPat=a;lastRelTik=b;send('SET:REL:'+a+','+b);goMain();}";
  html += "function saveRel2(){var mx=" + String(REL_SANIYE_MAX) + ";var a=parseInt(el('relPatIn').value,10);if(isNaN(a))a=lastRelPat;var b=parseInt(el('relTikIn').value,10);if(isNaN(b))b=lastRelTik;var cRaw=el('relPompaInAyar').value;var c=parseInt(cRaw,10);if(cRaw===''||isNaN(c))c=lastRelPompa;var vRaw=el('relVakumInAyar').value;var v=parseInt(vRaw,10);if(vRaw===''||isNaN(v))v=lastRelVakum;a=Math.max(1,Math.min(mx,a));b=Math.max(1,Math.min(mx,b));c=Math.max(1,Math.min(mx,c));v=Math.max(1,Math.min(mx,v));lastRelPat=a;lastRelTik=b;lastRelPompa=c;lastRelVakum=v;var inv=(el('inv32')&&el('inv32').checked)?1:0;lastInv32=inv;inv32Dirty=0;send('SET:REL:'+a+','+b);send('SET:PUMP:'+(pumpLim||0)+','+(pumpSure||1)+','+c);send('SET:RELVAK:'+v);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"+String("İ")+"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='al-vak' class='alert alert-vac' style='display:none;'>VAKUM ARIZASI!<div class='countdown' id='kalanVak'></div></div>";
  html += "<div id='latchInd' class='alert' style='display:none;background:#6b2c2c;border-radius:10px;'>Ana hat r"+String("ö")+"lesi kilitli (ak"+String("ı")+""+String("ş")+" yok veya boru patlak). Kilidi a"+String("ç")+"mak: reset butonuna 3 sn bas"+String("ı")+"l"+String("ı")+" tutun.</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='tourAdim1Hedef'>";
  html += "<div class='pill-strip'><div class='pill'><span>Ak"+String("ı")+""+String("ş")+" min</span><span id='mn'>0</span></div>";
  html += "<div class='pill'><span>Ak"+String("ı")+""+String("ş")+" 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"+String("ı")+"f"+String("ı")+"rla</button></div>";
  html += "<div class='live-readouts'>";
  html += "<div class='cca-card'><div class='cca-title'>Anl"+String("ı")+"k ak"+String("ı")+""+String("ş")+"</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"+String("İ")+"BRASYONU</button>";
  html += "<button type='button' class='btn btn-clog' onclick='openClog()'>BORU TIKANMA KAL"+String("İ")+"BRASYONU</button>";
  html += "<button type='button' class='btn btn-pump' onclick='openAkim()'>POMPA ARIZA KAL"+String("İ")+"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"+String("ı")+""+String("ş")+" h"+String("ı")+"z"+String("ı")+" izlenir. Sihirbazda tipik y"+String("ü")+"ksek ak"+String("ı")+""+String("ş")+"a g"+String("ö")+"re e"+String("ş")+"ik ve pay kurulur; normal "+String("ç")+"al"+String("ı")+""+String("ş")+"mada bu e"+String("ş")+"i"+String("ğ")+"i <strong>a"+String("ş")+"arsa</strong> alarm verilir.</p>";
  html += "<p class='hint'><strong>Vakum:</strong> Boru patlamada kurdu"+String("ğ")+"unuz <strong>ayn"+String("ı")+" e"+String("ş")+"ik</strong> (pals/sn) kullan"+String("ı")+"l"+String("ı")+"r. Ak"+String("ı")+""+String("ş")+" bu e"+String("ş")+"i"+String("ğ")+"in <strong>alt"+String("ı")+"na 1 dakika boyunca hi"+String("ç")+" inmezse</strong> vakum ar"+String("ı")+"zas"+String("ı")+" verilir (s"+String("ü")+"rekli y"+String("ü")+"ksek / vakum kayb"+String("ı")+" gibi durumlar).</p>";
  html += "<p class='hint'><strong>Ak"+String("ı")+" yok / t"+String("ı")+"kanma:</strong> Doz sonras"+String("ı")+" izlemede ak"+String("ı")+""+String("ş")+", kurdu"+String("ğ")+"unuz <strong>minimum</strong> seviyenin alt"+String("ı")+"nda kal"+String("ı")+"rsa (kimyasal boruya gelmiyormu"+String("ş")+" gibi) alarm verilir. Sihirbaz bu e"+String("ş")+"i"+String("ğ")+"i ve izleme s"+String("ü")+"resini kurar.</p>";
  html += "<p class='hint'><strong>Pompa:</strong> Önce pompa <strong>dururken</strong> bo"+String("ş")+"ta okuma al"+String("ı")+"n"+String("ı")+"r. Sonra pompay"+String("ı")+" "+String("ç")+"al"+String("ı")+""+String("ş")+"t"+String("ı")+"r"+String("ı")+"p test s"+String("ü")+"resinde ortalama <strong>pompa verisi</strong> &rarr; + ek ile \"pompa ger"+String("ç")+"ekten hareket ediyor\" e"+String("ş")+"i"+String("ğ")+"i tan"+String("ı")+"mlan"+String("ı")+"r; bu e"+String("ş")+"i"+String("ğ")+"in alt"+String("ı")+"nda kal"+String("ı")+"rsan"+String("ı")+"z pompa ar"+String("ı")+"zas"+String("ı")+" izlenir.</p>";
  html += "</div></div>";

  html += "<div id='sCal' class='screen'>";
  html += "<button type='button' class='btn btn-sec' onclick='goMain()'>Ana sayfaya d"+String("ö")+"n</button>";
  html += "<div id='cSumPat' class='cal-step panel'><h3>Kay"+String("ı")+"tl"+String("ı")+" patlama kalibrasyonu</h3>";
  html += "<p class='hint'>Cihazda kay"+String("ı")+"tl"+String("ı")+" de"+String("ğ")+"erler a"+String("ş")+"a"+String("ğ")+String("ı")+"d"+String("a")+"d"+String("ı")+String("r")+". Yeniden "+String("ö")+"l"+String("ç")+"m yapmak i"+String("ç")+"in <strong>Yeniden kalibre et</strong> ile kay"+String("ı")+"t silinir ve sihirbaz ba"+String("ş")+"lar.</p>";
  html += "<div class='pill' style='max-width:280px;margin:10px auto;text-align:center;'><span>Aktif e"+String("ş")+"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"+String("ı")+""+String("ş")+"</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"+String("ö")+"n</button></div>";
  html += "<div id='cDur' class='cal-step panel'><h3>1 &mdash; Test s"+String("ü")+"resi</h3>";
  html += "<p class='hint'>Sa"+String("ğ")+"l"+String("ı")+"kl"+String("ı")+" sistemde g"+String("ö")+"rd"+String("ü")+String("ğ")+String("ü")+"n"+String("ü")+"z en y"+String("ü")+"ksek ak"+String("ı")+""+String("ş")+" h"+String("ı")+"z"+String("ı")+"n"+String("ı")+" (veya patlak senaryosunu g"+String("ü")+"venle taklit edebiliyorsan"+String("ı")+"z o ko"+String("ş")+"ulu) <strong>test s"+String("ü")+"resi</strong> i"+String("ç")+"inde "+String("ö")+"l"+String("ç")+"ersiniz (30 sn&ndash;5 dk). Bu s"+String("ü")+"rede g"+String("ö")+"sterge s"+String("ü")+"rekli g"+String("ü")+"ncellenir; kaydedilen <strong>maksimum</strong> de"+String("ğ")+"er sonraki ad"+String("ı")+"mlarda kullan"+String("ı")+"l"+String("ı")+"r. Bu ekranda durum g"+String("ö")+"stergesi yan"+String("ı")+"p s"+String("ö")+"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: "+String("—")+"</p>";
  html += "<p class='hint'>Geri say"+String("ı")+"m s"+String("ü")+"resince <strong>anl"+String("ı")+"k ak"+String("ı")+""+String("ş")+" h"+String("ı")+"z"+String("ı")+"</strong> izlenir. Alttaki de"+String("ğ")+"er, bu testte g"+String("ö")+"r"+String("ü")+"len <strong>en y"+String("ü")+"ksek</strong> seviyedir. Test bitince sonraki ad"+String("ı")+"ma ge"+String("ç")+"ilir. Sorunda <strong>Testi iptal</strong>.</p>";
  html += "<div class='pill' style='margin:10px auto;max-width:260px;text-align:center;'><span>Test s"+String("ü")+"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"+String("ç")+"</h3>";
  html += "<p class='hint'><strong>Test bitti.</strong> A"+String("ş")+"a"+String("ğ")+"daki de"+String("ğ")+"er, test s"+String("ü")+"resince kaydedilen <strong>en y"+String("ü")+"ksek ak"+String("ı")+""+String("ş")+"</strong>. Bir sonraki ad"+String("ı")+"mda <strong>+ ek</strong> girersiniz; <strong>patlama e"+String("ş")+"i"+String("ğ")+"i = maksimum + ek</strong> (g"+String("ü")+"r"+String("ü")+"lt"+String("ü")+" pay"+String("ı")+" i"+String("ç")+"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()'>"+String("İ")+"leri</button></div>";
  html += "<div id='cAdd' class='cal-step panel'><h3>4 &mdash; E"+String("ş")+"ik</h3>";
  html += "<p class='hint'>Maksimum (<span id='tmax2'>0</span>) testte g"+String("ö")+"r"+String("ü")+"len zirvedir. <strong>+ ek</strong> ile g"+String("ü")+"venlik pay"+String("ı")+" ekleyin; <strong>e"+String("ş")+"ik = maks + ek</strong>. Normal "+String("ç")+"al"+String("ı")+""+String("ş")+"mada bu e"+String("ş")+"i"+String("ğ")+"i <strong>herhangi bir anda a"+String("ş")+"arsa</strong> boru patlak alarm"+String("ı")+" tetiklenir.</p>";
  html += "<div class='input-group'><label>Tepe de"+String("ğ")+"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"+String("ş")+"i"+String("ğ")+"i kaydet</button></div>";
  html += "<div id='cArm' class='cal-step panel'><h3>Kalibrasyon tamam</h3>";
  html += "<p class='hint'>Kay"+String("ı")+"t tamam. <strong>Aktif patlama e"+String("ş")+"i"+String("ğ")+"i:</strong> <span id='thrDisp'>0</span> &mdash; anl"+String("ı")+"k ak"+String("ı")+""+String("ş")+": <strong id='vCal'>0</strong>. Ana sayfada izleme devam eder.</p>";
  html += "<p class='hint'>Kalibrasyonu ba"+String("ş")+"tan yapmak i"+String("ç")+"in <strong>E"+String("ş")+"i"+String("ğ")+"i kald"+String("ı")+"r</strong> d"+String("ü")+String("ğ")+"mesine bas"+String("ı")+"n; kay"+String("ı")+"tl"+String("ı")+" e"+String("ş")+"ik silinir.</p>";
  html += "<button type='button' class='btn' onclick='goMain()'>Ana ekrana d"+String("ö")+"n</button>";
  html += "<button type='button' class='btn btn-sec' onclick='clearThr()'>E"+String("ş")+"i"+String("ğ")+"i kald"+String("ı")+"r (yeniden kalibre et)</button></div></div>";

  html += "<div id='sClog' class='screen'>";
  html += "<button type='button' class='btn btn-sec' onclick='goMain()'>Ana sayfaya d"+String("ö")+"n</button>";
  html += "<p class='hint' style='text-align:left;margin:10px 0 14px;line-height:1.55;'><strong>T"+String("ı")+"kanma / ak"+String("ı")+""+String("ş")+" yok kalibrasyonu</strong> ak"+String("ı")+""+String("ş")+" h"+String("ı")+"z"+String("ı")+" ile yap"+String("ı")+"l"+String("ı")+"r: boruyu t"+String("ı")+"kayarak ak"+String("ı")+""+String("ş")+" kesilir, pompay"+String("ı")+" "+String("ç")+"al"+String("ı")+""+String("ş")+"t"+String("ı")+"r"+String("ı")+"p tipik \"doz\" seviyesi kaydedilir; sonra bu seviyenin alt"+String("ı")+"na inecek e"+String("ş")+"ik ve doz sonras"+String("ı")+" izleme s"+String("ü")+"resi tan"+String("ı")+"mlan"+String("ı")+"r.</p>";
  html += "<div id='clogCountMainWrap' class='clog-main-cd'>Kalan s"+String("ü")+"re: <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='g0' class='clog-step panel clog-panel on'><h3>G"+String("ü")+"venlik</h3>";
  html += "<p class='hint' style='text-align:left;'>T"+String("ı")+"kanma ad"+String("ı")+"m"+String("ı")+"nda <strong>ak"+String("ı")+""+String("ş")+""+String("ı")+" kesmek</strong> i"+String("ç")+"in boruyu t"+String("ı")+"kay"+String("ı")+"n; g"+String("ö")+"stergede d"+String("ü")+"s"+String("ü")+"k ak"+String("ı")+""+String("ş")+" okunur. S"+String("ı")+"v"+String("ı")+" / bas"+String("ı")+"n"+String("ç")+" i"+String("ç")+"in g"+String("ü")+"venli t"+String("ı")+"kama y"+String("ö")+"ntemi kullan"+String("ı")+"n.</p>";
  html += "<p class='big-stop'>L"+String("ü")+"tfen boruyu t"+String("ı")+"kay"+String("ı")+"n.</p>";
  html += "<button type='button' class='btn btn-go btn-big-ack' onclick='clogTubeOk()'>Boruyu t"+String("ı")+"kad"+String("ı")+"m, devam edebilirim</button></div>";
  html += "<div id='g1' class='clog-step panel clog-panel'><h3>1 &mdash; Ak"+String("ı")+""+String("ş")+" test s"+String("ü")+"resi</h3>";
  html += "<p class='hint' style='text-align:left;'>Bir sonraki ad"+String("ı")+"mda <strong>boru t"+String("ı")+"kal"+String("ı")+" / ak"+String("ı")+""+String("ş")+" k"+String("ı")+"s"+String("ı")+"tl"+String("ı")+"</strong> testi yap"+String("ı")+"lacak. <strong>Test s"+String("ü")+"resi</strong> (30 sn&ndash;5 dk) se"+String("ç")+"in; s"+String("ü")+"re i"+String("ç")+"inde g"+String("ö")+"r"+String("ü")+"len <strong>tepe</strong> de"+String("ğ")+"er e"+String("ş")+"ik i"+String("ç")+"in referans al"+String("ı")+"n"+String("ı")+"r.</p>";
  html += "<button type='button' class='btn-dur' onclick='clogPickDur(30000)'>30 sn</button>";
  html += "<button type='button' class='btn-dur' onclick='clogPickDur(60000)'>1 dk</button>";
  html += "<button type='button' class='btn-dur' onclick='clogPickDur(120000)'>2 dk</button><br>";
  html += "<button type='button' class='btn-dur' onclick='clogPickDur(180000)'>3 dk</button>";
  html += "<button type='button' class='btn-dur' onclick='clogPickDur(240000)'>4 dk</button>";
  html += "<button type='button' class='btn-dur' onclick='clogPickDur(300000)'>5 dk</button></div>";
  html += "<div id='g2' class='clog-step panel clog-panel'><h3>2 &mdash; Test</h3>";
  html += "<p id='clogCd' style='font-size:24px;font-weight:bold;color:#0d5c4d;'>Kalan: "+String("—")+"</p>";
  html += "<p class='hint' style='text-align:left;'><strong>Boru t"+String("ı")+"kal"+String("ı")+" senaryosu:</strong> Ak"+String("ı")+""+String("ş")+" kesilmeli (boruyu t"+String("ı")+"kay"+String("ı")+"n), pompay"+String("ı")+" "+String("ç")+"al"+String("ı")+""+String("ş")+"t"+String("ı")+"r"+String("ı")+"n; d"+String("ü")+"s"+String("ü")+"k ak"+String("ı")+""+String("ş")+" beklenir. S"+String("ü")+"re dolunca veya <strong>"+String("İ")+"leri</strong> ile devam. Sorunda <strong>"+String("İ")+"ptal</strong>.</p>";
  html += "<div class='pill' style='margin:10px auto;max-width:280px;text-align:center;background:#e8f6f3;'><span>Test s"+String("ü")+"resince maksimum</span><span id='clogTmax'>0</span></div>";
  html += "<button type='button' class='btn btn-sec' onclick='clogCancelTest()'>"+String("İ")+"ptal</button></div>";
  html += "<div id='g3' class='clog-step panel clog-panel'><h3>3 &mdash; Sonu"+String("ç")+"</h3>";
  html += "<p class='hint' style='text-align:left;'>Bu de"+String("ğ")+"er, <strong>ad"+String("ı")+"m 2</strong> testinde g"+String("ö")+"r"+String("ü")+"len <strong>en y"+String("ü")+"ksek ak"+String("ı")+""+String("ş")+"</strong> (t"+String("ı")+"kal"+String("ı")+" senaryosu). Sonraki ad"+String("ı")+"mda <strong>ek pay</strong> ile &quot;iyi dozda olmas"+String("ı")+" gereken&quot; minimum e"+String("ş")+"ik olu"+String("ş")+"turulur; izlemede bir kez bile bu e"+String("ş")+"i"+String("ğ")+"in <strong>üst"+String("ü")+"ne "+String("ç")+String("ı")+"kmazsa</strong> t"+String("ı")+"kanma kabul edilir.</p>";
  html += "<p style='font-size:28px;font-weight:bold;color:#0d5c4d;text-align:center;' id='clogTmaxBig'>0</p>";
  html += "<button type='button' class='btn btn-go' onclick='clogNextMax()'>"+String("İ")+"leri</button></div>";
  html += "<div id='g4' class='clog-step panel clog-panel'><h3>4 &mdash; E"+String("ş")+"ik</h3>";
  html += "<p class='hint' style='text-align:left;'>Tepe (<span id='clogTmax2'>0</span>), ad"+String("ı")+"m 2 testinde kaydedilen maksimumdur. <strong>Ek</strong> bu tepeye eklenir; <strong>Hedef e"+String("ş")+"ik = tepe + ek</strong>. <strong>Not:</strong> A"+String("ş")+"a"+String("ğ")+"daki kutuya yaln"+String("ı")+"zca <strong>ek</strong> yaz"+String("ı")+"l"+String("ı")+"r; "+String("ö")+"zet ekran"+String("ı")+"ndaki say"+String("ı")+" (tepe+ek) ger"+String("ç")+"ek kar"+String("ş")+"ıla"+String("ş")+"t"+String("ı")+"rma e"+String("ş")+"i"+String("ğ")+"idir. Canl"+String("ı")+" izlemede bu e"+String("ş")+"i"+String("ğ")+"i bir kez bile <strong>a"+String("ş")+"an</strong> ak"+String("ı")+""+String("ş")+" olmazsa <strong>t"+String("ı")+"kanma</strong> alarm"+String("ı")+" verilir.</p>";
  html += "<div class='input-group'><label>Tepeye eklenecek g"+String("ü")+"venlik pay"+String("ı")+" (ek)</label><input type='number' id='clogOffIn' value='0' min='0'></div>";
  html += "<button type='button' class='btn btn-go' onclick='clogSaveThr()'>T"+String("ı")+"kanma e"+String("ş")+"i"+String("ğ")+"ini kaydet</button></div>";
  html += "<div id='g5' class='clog-step panel clog-panel'><h3>5 &mdash; "+String("İ")+"zleme</h3>";
  html += "<p class='hint' style='text-align:left;'>Her dozdan sonra cihaz, se"+String("ç")+"ti"+String("ğ")+"iniz s"+String("ü")+"re boyunca ak"+String("ı")+""+String("ş")+" h"+String("ı")+"z"+String("ı")+"n"+String("ı")+" izler. Bu pencerede ak"+String("ı")+""+String("ş")+" bir kez bile kay"+String("ı")+"tl"+String("ı")+" minimum e"+String("ş")+"i"+String("ğ")+"in <strong>üst"+String("ü")+"ne "+String("ç")+String("ı")+"kmazsa</strong> (kimyasal boruda beklenen h"+String("ı")+"za ula"+String("ş")+"m"+String("ı")+"yormu"+String("ş")+" gibi) <strong>t"+String("ı")+"kanma / ak"+String("ı")+""+String("ş")+" yok</strong> alarm"+String("ı")+" verilir. S"+String("ü")+"reyi doz aral"+String("ı")+""+String("ğ")+"ınıza g"+String("ö")+"re se"+String("ç")+"in.</p>";
  html += "<button type='button' class='btn btn-dur btn-obs' onclick='clogPickObs(1)'>1 dk</button>";
  html += "<button type='button' class='btn btn-dur btn-obs' onclick='clogPickObs(5)'>5 dk</button>";
  html += "<button type='button' class='btn btn-dur btn-obs' onclick='clogPickObs(10)'>10 dk</button>";
  html += "<button type='button' class='btn btn-dur btn-obs' onclick='clogPickObs(15)'>15 dk</button>";
  html += "<button type='button' class='btn btn-dur btn-obs' onclick='clogPickObs(20)'>20 dk</button>";
  html += "<button type='button' class='btn btn-dur btn-obs' onclick='clogPickObs(30)'>30 dk</button>";
  html += "<button type='button' class='btn btn-dur btn-obs' onclick='clogPickObs(40)'>40 dk</button>";
  html += "<button type='button' class='btn btn-dur btn-obs' onclick='clogPickObs(60)'>60 dk</button></div>";
  html += "<div id='g6' class='clog-step panel clog-panel'><h3>"+String("Ö")+"zet</h3>";
  html += "<p class='hint' style='text-align:left;'>Kay"+String("ı")+"t tamam. <strong>Minimum ak"+String("ı")+""+String("ş")+" e"+String("ş")+"i"+String("ğ")+"i:</strong> <span id='clogThrDisp'>0</span> &mdash; doz sonras"+String("ı")+" izleme: <strong><span id='clogObsDisp'>"+String("—")+"</span></strong>. Anl"+String("ı")+"k ak"+String("ı")+""+String("ş")+": <strong id='vCalClog'>0</strong>.</p>";
  html += "<button type='button' class='btn btn-go' onclick='clogRecal()'>Yeniden kalibre et</button>";
  html += "<button type='button' class='btn' onclick='goMain()'>Ana ekrana d"+String("ö")+"n</button>";
  html += "<button type='button' class='btn btn-sec' onclick='clogResetAll()'>T"+String("ü")+"m ayarlar"+String("ı")+" sil ve ana sayfa</button></div></div>";

  html += "<div id='sAyar' class='screen'><button type='button' class='btn btn-sec' onclick='goMain()'>Ana sayfaya d"+String("ö")+"n</button>";
  html += "<h3 style='color:#0056b3;margin:12px 0 8px;'>Ayarlar</h3>";
  html += "<p class='hint'>Alarmda ilgili r"+String("ö")+"le, girdi"+String("ğ")+"iniz s"+String("ü")+"re (saniye) boyunca aktif kal"+String("ı")+"r. <strong>Not:</strong> Boru patlama patlama s"+String("ü")+"resini; boru t"+String("ı")+"kanma ile <strong>sifon</strong> t"+String("ı")+"kanma s"+String("ü")+"resini; pompa ar"+String("ı")+"zas"+String("ı")+" ile <strong>s"+String("ı")+"k"+String("ı")+""+String("ş")+"m"+String("ı")+" ak"+String("ı")+""+String("ş")+" pompa s"+String("ü")+"resini; <strong>vakum ar"+String("ı")+"zas"+String("ı")+"</strong> kendi s"+String("ü")+"resini kullan"+String("ı")+"r.</p>";
  html += "<div class='input-group'><label>Boru patlama r"+String("ö")+"lesi a"+String("ç")+String("ı")+"k kalma s"+String("ü")+"resi <span class='u'>sn</span></label><input type='number' id='relPatIn' min='1' max='"+String(REL_SANIYE_MAX)+"' value='10'></div>";
  html += "<div class='input-group'><label>Boru t"+String("ı")+"kanma r"+String("ö")+"lesi a"+String("ç")+String("ı")+"k kalma s"+String("ü")+"resi <span class='u'>sn</span></label><input type='number' id='relTikIn' min='1' max='"+String(REL_SANIYE_MAX)+"' value='10'></div>";
  html += "<div class='input-group'><label>Pompa ar"+String("ı")+"zas"+String("ı")+" r"+String("ö")+"lesi a"+String("ç")+String("ı")+"k kalma s"+String("ü")+"resi <span class='u'>sn</span></label><input type='number' id='relPompaInAyar' min='1' max='"+String(REL_SANIYE_MAX)+"' value='10'></div>";
  html += "<div class='input-group'><label>Vakum ar"+String("ı")+"zas"+String("ı")+" r"+String("ö")+"lesi a"+String("ç")+String("ı")+"k kalma s"+String("ü")+"resi <span class='u'>sn</span></label><input type='number' id='relVakumInAyar' min='1' max='"+String(REL_SANIYE_MAX)+"' value='10'></div>";
  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"+String("ö")+"n"+String("ü")+"n"+String("ü")+" ters "+String("ç")+"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"+String("ö")+"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"+String("ş")+"ta okuma &rarr; pompay"+String("ı")+" "+String("ç")+"al"+String("ı")+""+String("ş")+"t"+String("ı")+"r"+String("ı")+"p test s"+String("ü")+"resi &rarr; ortalama pompa verisi &rarr; + ek &rarr; izleme s"+String("ü")+"resi (dakika).</p>";

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

  html += "<div id='p1' class='clog-step panel clog-panel'><h3>1 &mdash; Test s"+String("ü")+"resi</h3>";
  html += "<p class='hint' style='text-align:left;'>Pompay"+String("ı")+" normal doz gibi "+String("ç")+"al"+String("ı")+""+String("ş")+"t"+String("ı")+"r"+String("ı")+"n. Se"+String("ç")+"ilen s"+String("ü")+"re boyunca <strong>pompa verisi</strong> "+String("ö")+"rneklenir ve ortalama al"+String("ı")+"n"+String("ı")+"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"+String("ü")+"re: "+String("—")+"</p>";
  html += "<p class='hint' style='text-align:left;'>Test bitince ortalama <strong>pompa verisi</strong> hesaplan"+String("ı")+"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"+String("ı")+"k pompa verisi</span><span id='pumpCurDisp'>0</span></div></div>";
  html += "<button type='button' class='btn btn-sec' onclick='pumpCancel()'>"+String("İ")+"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()'>"+String("İ")+"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? "+String("Ü")+"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()'>"+String("Ü")+"st limiti kaydet</button></div>";

  html += "<div id='p5' class='clog-step panel clog-panel'><h3>5 &mdash; "+String("İ")+"zleme</h3>";
  html += "<p class='hint' style='text-align:left;'>Se"+String("ç")+"ti"+String("ğ")+"iniz <strong>dakika</strong> boyunca cihaz pompa verisini izler. Bu s"+String("ü")+"re i"+String("ç")+"inde de"+String("ğ")+"er bir kez bile kay"+String("ı")+"tl"+String("ı")+" <strong>üst limiti ge"+String("ç")+"mezse</strong> (pompa beklenen aral"+String("ı")+"kta de"+String("ğ")+"il) <strong>pompa ar"+String("ı")+"zas"+String("ı")+"</strong> alarm"+String("ı")+" 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>"+String("Ö")+"zet</h3>";
  html += "<p class='hint' style='text-align:left;'>Kay"+String("ı")+"tl"+String("ı")+" minimum pompa verisi: <strong><span id='pumpLimDisp'>0</span></strong><br>";
  html += "Ar"+String("ı")+"za s"+String("ü")+"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"+String("ı")+"f"+String("ı")+"rla)</button>";
  html += "<button type='button' class='btn btn-sec' onclick='goMain()'>Ana sayfaya d"+String("ö")+"n</button></div>";
  html += "<p style='font-size:10px;color:#ccc;margin-top:14px;'>AKTEK ELEKTRON"+String("İ")+"K "+String("Ç")+String("Ö")+"Z"+String("Ü")+"MLER"+String("İ")+" 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);
  pinMode(ROLE_VAKUM_ARIZA, OUTPUT);
  digitalWrite(LED_SISTEM, HIGH);
  digitalWrite(ROLE_AKIS_TIKANIK, LOW);
  digitalWrite(ROLE_BORU_PAT, LOW);
  // Pompa arıza pini ters: arıza=LOW (kapalı), normal=HIGH (açık)
  digitalWrite(ROLE_POMPA_ARIZA, HIGH);
  digitalWrite(ROLE_VAKUM_ARIZA, LOW);

  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();
  anaRoleMandalli = hafiza.getBool("anaLock", false);
  digitalWrite(ROLE_POMPA_ANA, anaRoleMandalli ? PUMP_CUT_LEVEL : PUMP_RUN_LEVEL);
  Serial.printf("Açılış: NVS anaLock(mandal)=%d -> GPIO%d ana=%d (1=kes,0=çalış)\n",
                anaRoleMandalli ? 1 : 0, ROLE_POMPA_ANA, anaRoleMandalli ? (PUMP_CUT_LEVEL == HIGH ? 1 : 0) : (PUMP_RUN_LEVEL == HIGH ? 1 : 0));
  if (anaRoleMandalli)
    Serial.println("Ana röle: NVS kilidi aktif (boru patlak veya akış 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>";
  }
  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;
  }

  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 cca = flowPps;
  // Dozaj: ~0.3 sn patlama + uzun susma — anlık yoğunluk 1 sn ortalamasında kaybolmasın
  int ccb = (flowPpsDozKisa > flowPps) ? flowPpsDozKisa : flowPps;
  updateSessionStats(cca);
  updateAkimSessionStatsMa(akimMa);

  const int HALL_THR = pompaAkimUstLimitMa;
  bool hallRun = (HALL_THR > 0) && (akimMa > HALL_THR);
  // Not: "Akış yok / tıkanma" alarmını pompa arızası ile karıştırmamak için,
  // pompa arızası aktifken tıkanma izlemesini bastırıyoruz. Hall eşiği ayarsız/yüksek olursa
  // hallRun=false iken tıkanmayı tamamen kapatmak yanlış alarm kaçırabilir.
  const bool allowClogMonitor = !alarmPompaAktif;

  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;
    }
  }

  if (clogPh == 2) {
    if ((long)(millis() - clogTestEndMs) >= 0) {
      clogPh = 3;
    } else if (ccb > clogTestPeak) {
      clogTestPeak = ccb;
    }
  }

  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 && patIzleAktif && patCcaUstu &&
                 (boruPatUstCikisSayaci >= BORU_PAT_ARIZA_UST_CIKIS_SAYISI);
  if (!alarmPatAktif && tripPat) {
    alarmPatAktif = true;
    alarmPatBasla = millis();
    anaRoleMandalli = true;
    hafiza.putBool("anaLock", true);
    anaLockReason = LOCK_PAT;
    anaLockSinceMs = millis();
    boruPatUstCikisSayaci = 0;
    boruPatOncekiCcaUstu = patCcaUstu;
    Serial.println("Boru patlak: 3. eşik aşımı — ana röle mandalı + alarm");
  }
  if (alarmPatAktif) {
    if (patIzleAktif && cca > peakThreshold)
      alarmPatBasla = millis();
    unsigned long g = millis() - alarmPatBasla;
    unsigned long ms = (unsigned long)relPatSaniye * 1000UL;
    if (g >= ms) {
      alarmPatAktif = false;
      kalanPatSn = 0;
    } else {
      kalanPatSn = (int)((ms - g) / 1000UL);
    }
  }

  // --- Vakum arızası DEVRE DIŞI ---
  // İstenen davranış: vakum arızası hiç oluşmasın ve vakum rölesi hiç sürülmesin.
  alarmVakumAktif = false;
  kalanVakumSn = 0;
  digitalWrite(ROLE_VAKUM_ARIZA, LOW);
  vakumEsikUstuSeriBaslaMs = 0;
  vakumAltindaSeriBaslaMs = 0;

  // --- Akış yok / tıkanma ---
  // Pompa arızası aktifken: akış yok/tıkanma alarmı VERME, çıkışı sürme.
  if (!allowClogMonitor) {
    alarmClogAktif = false;
    alarmSifonAktif = false;
    kalanClogSn = 0;
    kalanSifonSn = 0;
    digitalWrite(ROLE_AKIS_TIKANIK, LOW);
    clogWindowStart = millis();
  } else {
    if (clogArmed && clogThreshold > 0 && clogObserveMs > 0 && !alarmClogAktif) {
      // Tıkanma: max(1sn, doz_kisa) = ccb — kısa dozda cca=0 iken ccb tepki verir; pencere buna göre sıfırlanır.
      static unsigned long clogAboveStartMs = 0;
      const unsigned long CLOG_ABOVE_DEBOUNCE_MS = 600UL;
      if (ccb >= clogThreshold) {
        if (clogAboveStartMs == 0) clogAboveStartMs = millis();
        if ((millis() - clogAboveStartMs) >= CLOG_ABOVE_DEBOUNCE_MS) {
          clogWindowStart = millis();
        }
      } else {
        clogAboveStartMs = 0;
        if ((millis() - clogWindowStart) >= clogObserveMs) {
        alarmClogAktif = true;
        alarmClogBasla = millis();
        digitalWrite(ROLE_AKIS_TIKANIK, HIGH);
        clogWindowStart = millis();
        anaRoleMandalli = true;
        hafiza.putBool("anaLock", true);
        anaLockReason = LOCK_CLOG;
        anaLockSinceMs = millis();
      }
      }
    }
    // Boru tıkanma alarmı mandallı: ROLE_AKIS_TIKANIK butona basana kadar HIGH kalır.
    if (alarmClogAktif) {
      kalanClogSn = 0;
      digitalWrite(ROLE_AKIS_TIKANIK, HIGH);
    }
  }

  // --- Akış yok / tıkanma kaynaklı kilit otomatik açma (2 dk içinde düzelirse) ---
  if (anaRoleMandalli && anaLockReason == LOCK_CLOG && anaLockSinceMs > 0) {
    unsigned long held = millis() - anaLockSinceMs;
    if (held <= AUTO_UNLOCK_CLOG_MS) {
      // Akış tekrar eşik üstüne çıkarsa ve pompa çalışıyorsa otomatik aç
      bool recovered = allowClogMonitor && clogArmed && (clogThreshold > 0) && (ccb >= clogThreshold);
      if (recovered) {
        anaRoleMandalli = false;
        hafiza.putBool("anaLock", false);
        anaLockReason = LOCK_NONE;
        anaLockSinceMs = 0;
        alarmClogAktif = false;
        alarmSifonAktif = false;
        kalanClogSn = 0;
        kalanSifonSn = 0;
        digitalWrite(ROLE_AKIS_TIKANIK, LOW);
        clogWindowStart = millis();
        Serial.println("Auto-unlock: tıkanma/akış yok düzeldi (2 dk içinde)");
      }
    }
  }

  // Sifon: pompa manyetiği uzun süre yok (ör. 3 dk), debimetrede hâlâ akış
  static bool sifonLatch = false;
  if (hallRun || !flowActive)
    sifonLatch = false;
  if (!alarmSifonAktif && !sifonLatch && HALL_THR > 0 && flowActive && durHallWeak > 12000UL &&
      (millis() - hallSonYuksekMs > 180000UL)) {
    alarmSifonAktif = true;
    sifonLatch = true;
    alarmSifonBasla = millis();
    digitalWrite(ROLE_AKIS_TIKANIK, HIGH);
  }
  // Sifon alarmı da aynı çıkışı kullandığı için mandallı tutuyoruz (reset ile temizlenir).
  if (alarmSifonAktif) {
    kalanSifonSn = 0;
    digitalWrite(ROLE_AKIS_TIKANIK, HIGH);
  }

  // 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();
        digitalWrite(ROLE_POMPA_ARIZA, LOW);
        pompaWindowStart = millis();
      }
    }
  }
  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 rölesi: tek yerden sür (her karede tutarlı; unutulan LOW/HIGH kalmaz)
  // GPIO19 ana: mandal=HIGH pompayı keser (PUMP_CUT_LEVEL), mandal yok=LOW çalıştırır (PUMP_RUN_LEVEL)
  // GPIO22 boru patlak: alarmPatAktif süresince HIGH, alarm yokken LOW (mandal ayrıca anaRoleMandalli)
  digitalWrite(ROLE_BORU_PAT, alarmPatAktif ? 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 && clogThreshold > 0 && ccb < clogThreshold)
                   ? (unsigned int)((millis() - clogWindowStart) / 1000UL)
                   : 0U);
    const unsigned alm = (unsigned)(alarmPatAktif ? 2 : 0) | (unsigned)(alarmClogAktif ? 4 : 0) |
                          (unsigned)(alarmSifonAktif ? 8 : 0) | (unsigned)(alarmPompaAktif ? 16 : 0) |
                          (unsigned)(alarmStuckAktif ? 32 : 0);
    const int clogUstDisp =
        (clogArmed && clogThreshold > 0) ? ((ccb >= clogThreshold) ? 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 cca=%d heap=%u ws=%u | alm=%u lock=%d patAl=%d GPIO%d=%d GPIO%d=%d\n",
        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) ? 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,%d,%d,%d,%d",
                     cca, 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, relPatSaniye, relTikSaniye, akimMa, akimSessMinMa, akimSessMaxMa, akimAdc,
                     akimZeroAdc, alarmPompaAktif ? 1 : 0, kalanPompaSn, pompaAkimUstLimitMa, pompaArizaSureDk,
                     relPompaSaniye, (int)pumpPh, pumpTestKalanSn(), pumpAvgAbsMa, pumpRunAvgAbsMa, pumpCurAbsMa,
                     invertCcaAdc ? 1 : 0, anaRoleMandalli ? 1 : 0, 0, 0,
                     relVakumSaniye);
    if (n > 0 && n < (int)sizeof(wsBuf)) {
      webSocket.broadcastTXT((uint8_t *)wsBuf, (size_t)n);
    }
    lastUpdate = millis();
  }
}
Editor is loading...
Leave a Comment