Untitled
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'>👆</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> → + 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 — 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–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 — 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 — 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 — 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> — 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 — 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–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 — 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 — 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 "iyi dozda olmas"+String("ı")+" gereken" 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 — 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 — "+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> — 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 → pompay"+String("ı")+" "+String("ç")+"al"+String("ı")+""+String("ş")+"t"+String("ı")+"r"+String("ı")+"p test s"+String("ü")+"resi → ortalama pompa verisi → + ek → 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 — 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 — 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 — 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 — + 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 — "+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