Untitled
unknown
plain_text
a month ago
83 kB
8
Indexable
#include <WiFi.h>
#include <esp_wifi.h>
#include <esp_mac.h>
#include <stdio.h>
#include <math.h>
#include <DNSServer.h>
#include <WebServer.h>
#include <WebSocketsServer.h>
#include <Update.h>
#include <Preferences.h>
enum AnaLockReason : uint8_t { LOCK_NONE = 0, LOCK_PAT = 1, LOCK_CLOG = 2 };
// --- GİRİŞLER ---
const int PIN_HALL_ADC = 32; // analog Hall (pompa mıknatısı)
const int PIN_FLOW = 25;
/** Reset butonu (GPIO18): mandal = pompa/arıza kilidi; kilitliyken bu düğmeye 3 sn basınca mandal açılır. */
const int PIN_RESET = 18;
static const float ADC_VREF = 3.3f;
static const int ADC_MAX = 4095;
// Hall için "boşta" ADC (web: AKIM:ZERO — pompa dururken ölçün)
static int akimZeroAdc = 2048;
// Bazı Hall modüllerinde ADC yönü ters olabilir — web'den (inv32)
static bool invertCcaAdc = false;
volatile uint32_t flowPulseCounter = 0;
static uint32_t flowPulseSnapPrev = 0;
unsigned long lastFlowPulseMs = 0;
static uint32_t flowPulseTotalLastSec = 0;
static unsigned long flowRateTickMs = 0;
int flowPps = 0;
static int flowLastWindowDelta = 0;
/** Kısa pencerede (dozaj gibi) pals yoğunluğu — sn başına normalize; tıkanma eşiği ile birleştirilir. */
static int flowPpsDozKisa = 0;
static const unsigned long FLOW_DOZ_PENCERE_MS = 300UL; // ~0.3 sn doz patlaması (patlak izleme)
/** Dozaj: 0→darbe→0 arası toplam darbe; ekran + tıkanma. 2 sn 0 sonra silinir. */
static const unsigned long DOSE_BURST_BITIS_MS = 450UL;
static const unsigned long DOSE_EKRAN_TUT_MS = 2000UL;
static int doseBurstToplam = 0;
static int doseSonGoster = 0;
static unsigned long doseSonDarbeMs = 0;
static unsigned long doseEkranSilMs = 0;
static uint32_t dosePulseSnap = 0;
void IRAM_ATTR isrFlowPulse() { flowPulseCounter++; }
static void doseAkisGuncelle() {
uint32_t cnt;
noInterrupts();
cnt = flowPulseCounter;
interrupts();
if (dosePulseSnap == 0) dosePulseSnap = cnt;
uint32_t d = (cnt >= dosePulseSnap) ? (cnt - dosePulseSnap) : 0;
dosePulseSnap = cnt;
unsigned long now = millis();
if (d > 0) {
doseBurstToplam += (int)d;
doseSonDarbeMs = now;
doseEkranSilMs = 0;
}
bool burstDevam =
doseBurstToplam > 0 && (now - doseSonDarbeMs) < DOSE_BURST_BITIS_MS;
if (doseBurstToplam > 0 && !burstDevam) {
doseSonGoster = doseBurstToplam;
doseBurstToplam = 0;
doseEkranSilMs = now + DOSE_EKRAN_TUT_MS;
}
if (doseSonGoster > 0 && doseBurstToplam == 0 && doseEkranSilMs > 0 &&
now >= doseEkranSilMs) {
doseSonGoster = 0;
doseEkranSilMs = 0;
}
}
static int doseAkisGoster() {
return doseBurstToplam > 0 ? doseBurstToplam : doseSonGoster;
}
static bool clogAkisVar() { return doseAkisGoster() > 0; }
static int hallZeroOlcAdc() {
const int N = 400;
long sum = 0;
for (int i = 0; i < N; i++) {
sum += analogRead(PIN_HALL_ADC);
delayMicroseconds(200);
}
return (int)(sum / N);
}
/** Hall ADC — median 5 */
static int okuHallMedian5() {
const int N = 5;
int v[N];
for (int i = 0; i < N; i++) {
int raw = analogRead(PIN_HALL_ADC);
if (raw < 0) raw = 0;
if (raw > ADC_MAX) raw = ADC_MAX;
v[i] = invertCcaAdc ? (ADC_MAX - raw) : raw;
if (i + 1 < N) delayMicroseconds(8);
}
for (int i = 1; i < N; i++) {
int t = v[i];
int j = i;
while (j > 0 && v[j - 1] > t) {
v[j] = v[j - 1];
j--;
}
v[j] = t;
}
return v[N / 2];
}
// --- ÇIKIŞLAR (NC röle: kesme / alarm için genelde röle enerji alır = HIGH) ---
const int ROLE_POMPA_ANA = 19;
const int ROLE_AKIS_TIKANIK = 14;
const int ROLE_BORU_PAT = 22;
const int ROLE_POMPA_ARIZA = 23;
/** GPIO14 tıkanma rölesi ters: arıza=LOW (kapalı), normal=HIGH (açık) */
const int CLOG_RELAY_NORMAL = HIGH;
const int CLOG_RELAY_FAULT = LOW;
/** Tek durum LED — arıza yok: 1 sn açık / 1 sn kapalı; arıza: hızlı yanıp sönme */
const int LED_SISTEM = 13;
static const unsigned long LED_NORMAL_YARIM_PERIYOT_MS = 1000UL;
static const unsigned long LED_FAULT_YARIM_PERIYOT_MS = 100UL;
// Pompa ana hattı (NC röle modülü): normalde röle bırakık; boru patlak / tıkanma mandalı → röle çeker (pompa kesilir)
const int PUMP_CUT_LEVEL = HIGH;
const int PUMP_RUN_LEVEL = LOW;
/** Mandal (kilit): patlak/tıkanma sonrası ana pompa kesilir, NVS'te kalır. Açmak = reset butonuna 3 sn. Patlak: 10 dk içinde 3×30 sn uyarı. Tıkanma: anında mandal, sınırsız (yalnız reset). */
static bool anaRoleMandalli = false;
static AnaLockReason anaLockReason = LOCK_NONE;
static unsigned long anaLockSinceMs = 0;
static const unsigned long RESET_HOLD_MS = 3000UL;
static unsigned long resetPressStartMs = 0;
static bool resetAwaitRelease = false;
// Patlak uyarısı: 3 eşik aşımında 30 sn uyarı (ana pompa kesilmez); 10 dk içinde 3 uyarı → ana mandal
static const unsigned long PAT_UYARI_SURE_MS = 30000UL;
static const unsigned long PAT_UYARI_PENCERE_MS = 600000UL;
static const int PAT_UYARI_MANDAL_ADET = 3;
static const int PAT_UYARI_MAX_KAYIT = 6;
static unsigned long patUyariZamanlar[PAT_UYARI_MAX_KAYIT];
static uint8_t patUyariKayit = 0;
static bool patUyariModu = false;
static unsigned long patUyariBaslaMs = 0;
int relPompaSaniye = 10;
/** Pompa arıza rölesi açık kalma süresi (sn) üst sınırı — en fazla 12 saat. */
static const int REL_SANIYE_MAX = 12 * 3600;
// Pompa arızası: belirlenen süre içinde Hall sapması üst eşiği geçmezse alarm (kalibrasyon ADC ile)
int pompaAkimUstLimitMa = 500; // aslında Hall min. sapma ADC (kalibrasyonla)
int pompaArizaSureDk = 1; // dk (varsayılan)
bool alarmPompaAktif = false;
unsigned long alarmPompaBasla = 0;
int kalanPompaSn = 0;
unsigned long pompaWindowStart = 0;
bool alarmStuckAktif = false;
unsigned long alarmStuckBasla = 0;
int kalanStuckSn = 0;
unsigned long hallSonYuksekMs = 0;
// Pompa arıza kalibrasyon sihirbazı (tıkanma adımları gibi)
// STOP -> DUR -> TEST -> SHOW_AVG -> ASK_ADD -> OBS -> DONE
enum PumpWizardPhase : uint8_t { PZ_STOP = 0, PZ_DUR = 1, PZ_TEST = 2, PZ_SHOW_AVG = 3, PZ_ASK_ADD = 4, PZ_OBS = 5, PZ_DONE = 6 };
uint8_t pumpPh = PZ_STOP;
unsigned long pumpTestEndMs = 0;
unsigned long pumpTestDurationMs = 0;
unsigned long pumpLastSampleMs = 0;
long pumpSumAbsMa = 0;
long pumpSampleCount = 0;
int pumpAvgAbsMa = 0;
int pumpAddMa = 0;
int pumpRunAvgAbsMa = 0;
int pumpCurAbsMa = 0;
const byte DNS_PORT = 53;
IPAddress apIP(192, 168, 4, 1);
DNSServer dnsServer;
WebServer server(80);
WebSocketsServer webSocket = WebSocketsServer(81);
Preferences hafiza;
/** Ana sayfa HTML bir kez üretilir; her HTTP isteğinde getHTML() çağırmak AP'yi ve WS'yi kilitleyebilir. */
String g_apHtmlCache;
static char g_apSsid[32];
/** Sahada birden fazla ESP32 varsa kanal çakışmasını azaltmak için sabit kanal + az istemci. */
static const uint8_t WIFI_AP_CHANNEL = 6;
static const uint8_t WIFI_AP_MAX_CONN = 2;
static const unsigned long WIFI_AP_HEALTH_MS = 15000UL;
static unsigned long wifiApHealthMs = 0;
/** WebSocket içinde uzun ADC ölçümü WiFi yığınını kilitler — loop'ta yapılır. */
static volatile bool pendingAkimZero = false;
static const unsigned long SERIAL_DBG_INTERVAL_MS = 10000UL;
unsigned long lastUpdate = 0;
int sessMin = 4095;
int sessMax = 0;
bool sessInited = false;
int akimSessMinMa = 0;
int akimSessMaxMa = 0;
bool akimSessInited = false;
enum WizardPhase : uint8_t { WZ_IDLE = 0, WZ_TEST = 1, WZ_SHOW_MAX = 2, WZ_ASK_ADD = 3, WZ_ARMED = 4 };
WizardPhase wizardPhase = WZ_IDLE;
unsigned long testEndMs = 0;
unsigned long testDurationMs = 0;
int testPeak = 0;
int patKalibTepe = 0;
bool usePeakThreshold = false;
int peakThreshold = 0;
bool alarmPatAktif = false;
/** Ana mandal sonrası akış hâlâ yüksek: patlak rölesi mandal (reset'e kadar). */
static bool alarmPatMandal = false;
int kalanPatSn = 0;
/** Boru patlak uyarısı: akış eşiğinin üstüne bu kadar kez art arda çıkınca 30 sn uyarı. */
static const int BORU_PAT_ARIZA_UST_CIKIS_SAYISI = 3;
static int boruPatUstCikisSayaci = 0;
static bool boruPatOncekiCcaUstu = false;
static void boruPatUstSayacSifirla() {
boruPatUstCikisSayaci = 0;
boruPatOncekiCcaUstu = false;
}
static void patUyariPencereTemizle(unsigned long now) {
uint8_t w = 0;
for (uint8_t i = 0; i < patUyariKayit; i++) {
if (patUyariZamanlar[i] != 0 && (now - patUyariZamanlar[i]) <= PAT_UYARI_PENCERE_MS) {
if (w != i) patUyariZamanlar[w] = patUyariZamanlar[i];
w++;
}
}
patUyariKayit = w;
}
static int patUyariPencereSay(unsigned long now) {
patUyariPencereTemizle(now);
return (int)patUyariKayit;
}
static void patUyariKayitEkle(unsigned long now) {
patUyariPencereTemizle(now);
if (patUyariKayit >= (uint8_t)PAT_UYARI_MAX_KAYIT) {
for (uint8_t i = 1; i < patUyariKayit; i++)
patUyariZamanlar[i - 1] = patUyariZamanlar[i];
patUyariKayit--;
}
patUyariZamanlar[patUyariKayit++] = now;
}
static void patUyariKayitSifirla() {
patUyariKayit = 0;
for (int i = 0; i < PAT_UYARI_MAX_KAYIT; i++) patUyariZamanlar[i] = 0;
}
static void boruPatAlarmSifirla(bool nvsAnaLockYaz) {
alarmPatAktif = false;
alarmPatMandal = false;
patUyariModu = false;
patUyariBaslaMs = 0;
kalanPatSn = 0;
if (anaLockReason == LOCK_PAT) {
anaRoleMandalli = false;
anaLockReason = LOCK_NONE;
anaLockSinceMs = 0;
if (nvsAnaLockYaz) hafizaAnaLockTemizle();
}
boruPatUstSayacSifirla();
}
const uint8_t CLOG_OFF = 10;
/** Tıkanma: doz darbe toplamı 0 kalırsa CLOG_TIKANMA_SURE_MS sonunda mandal. Kalibrasyon yok. */
static const unsigned long CLOG_TIKANMA_SURE_MS = 90000UL; // 1,5 dk
uint8_t clogPh = CLOG_OFF;
unsigned long clogTestEndMs = 0;
int clogTestPeak = 0;
int clogThreshold = 0;
int clogObsMin = 1;
unsigned long clogObserveMs = CLOG_TIKANMA_SURE_MS;
unsigned long clogWindowStart = 0;
bool clogArmed = true;
static void clogVarsayilanAyar() {
clogThreshold = 0;
clogObsMin = 1;
clogObserveMs = CLOG_TIKANMA_SURE_MS;
clogArmed = true;
clogPh = CLOG_OFF;
clogTestPeak = 0;
clogTestEndMs = 0;
}
bool alarmClogAktif = false;
unsigned long alarmClogBasla = 0;
int kalanClogSn = 0;
static void hafizaAnaLockKaydet(AnaLockReason r) {
hafiza.putBool("anaLock", true);
hafiza.putUChar("anaLockR", (uint8_t)r);
}
static void hafizaAnaLockTemizle() {
hafiza.putBool("anaLock", false);
hafiza.putUChar("anaLockR", (uint8_t)LOCK_NONE);
hafiza.putBool("patMnd", false);
}
static void hafizaAnaLockYukle() {
anaRoleMandalli = hafiza.getBool("anaLock", false);
uint8_t r = hafiza.getUChar("anaLockR", (uint8_t)LOCK_NONE);
if (!anaRoleMandalli) {
anaLockReason = LOCK_NONE;
anaLockSinceMs = 0;
return;
}
anaLockSinceMs = millis();
if (r == (uint8_t)LOCK_PAT) {
anaLockReason = LOCK_PAT;
alarmPatMandal = hafiza.getBool("patMnd", false);
alarmPatAktif = true;
kalanPatSn = 0;
} else if (r == (uint8_t)LOCK_CLOG) {
anaLockReason = LOCK_CLOG;
alarmClogAktif = true;
} else {
// Eski NVS (yalnizca anaLock, nedensiz): patlak kilidi varsay
anaLockReason = LOCK_PAT;
alarmPatMandal = hafiza.getBool("patMnd", false);
alarmPatAktif = true;
kalanPatSn = 0;
hafizaAnaLockKaydet(LOCK_PAT);
}
}
static void hafizaRelYukle() {
relPompaSaniye = hafiza.getInt("relPmp", 10);
if (relPompaSaniye < 1) relPompaSaniye = 1;
if (relPompaSaniye > REL_SANIYE_MAX) relPompaSaniye = REL_SANIYE_MAX;
}
static void hafizaPompaYukle() {
pompaAkimUstLimitMa = hafiza.getInt("pLimMa", 500);
// Eski sürüm uyumu: pSure saniye idi. Yeni: pSureDk dakika.
pompaArizaSureDk = hafiza.getInt("pSureDk", -1);
if (pompaArizaSureDk < 0) {
int oldSn = hafiza.getInt("pSure", 30);
if (oldSn < 1) oldSn = 1;
long m = lroundf((float)oldSn / 60.0f);
if (m < 1) m = 1;
pompaArizaSureDk = (int)m;
hafiza.putInt("pSureDk", pompaArizaSureDk);
}
if (pompaAkimUstLimitMa < 0) pompaAkimUstLimitMa = 0;
if (pompaAkimUstLimitMa > 50000) pompaAkimUstLimitMa = 50000;
if (pompaArizaSureDk < 1) pompaArizaSureDk = 1;
if (pompaArizaSureDk > 240) pompaArizaSureDk = 240;
}
static void hafizaPompaKaydet() {
hafiza.putInt("pLimMa", pompaAkimUstLimitMa);
hafiza.putInt("pSureDk", pompaArizaSureDk);
}
static int pumpTestKalanSn() {
if (pumpPh != PZ_TEST) return 0;
long left = (long)(pumpTestEndMs - millis()) / 1000L;
return left > 0 ? (int)left : 0;
}
void hafizaClogKaydet() {
hafiza.putBool("clogArm", true);
hafiza.putInt("clogThr", 0);
hafiza.putInt("clogObs", 1);
hafiza.putInt("clogTpk", 0);
}
void hafizaClogYukle() {
clogVarsayilanAyar();
clogWindowStart = millis();
}
void clogSifirla() {
clogVarsayilanAyar();
clogWindowStart = millis();
hafizaClogKaydet();
}
/** Pompa arızası varken tıkanma/mandal iptal (motor durunca düşük akış tıkanma sayılmasın). */
static void clogAlarmPompaArizaTemizle() {
if (!alarmClogAktif && anaLockReason != LOCK_CLOG) return;
alarmClogAktif = false;
kalanClogSn = 0;
clogWindowStart = millis();
if (anaLockReason == LOCK_CLOG) {
anaRoleMandalli = false;
anaLockReason = LOCK_NONE;
anaLockSinceMs = 0;
hafizaAnaLockTemizle();
Serial.println("Tikanma iptal: pompa arizasi oncelikli");
}
}
/** Mandal veya mandallı arıza çıkışı aktif mi? */
static bool mandalVeyaMandalliArizaAktif() {
return anaRoleMandalli || alarmPatAktif || alarmPatMandal || alarmClogAktif;
}
/** Reset butonu (3 sn): tüm mandal ve mandallı alarmları aç — mandal = kilit, reset = anahtar. */
static void mandalResetButonuAc() {
boruPatAlarmSifirla(true);
anaRoleMandalli = false;
hafizaAnaLockTemizle();
anaLockReason = LOCK_NONE;
anaLockSinceMs = 0;
alarmClogAktif = false;
kalanClogSn = 0;
clogWindowStart = millis();
boruPatUstSayacSifirla();
patUyariKayitSifirla();
}
static void pollResetButton() {
bool pressed = (digitalRead(PIN_RESET) == LOW);
if (pressed) {
if (resetPressStartMs == 0)
resetPressStartMs = millis();
else if (!resetAwaitRelease && (millis() - resetPressStartMs >= RESET_HOLD_MS)) {
if (mandalVeyaMandalliArizaAktif()) {
mandalResetButonuAc();
Serial.println("Reset: mandal acildi (3 sn)");
}
resetAwaitRelease = true;
}
} else {
resetPressStartMs = 0;
resetAwaitRelease = false;
}
}
static void pollPendingAkimZero() {
if (!pendingAkimZero) return;
pendingAkimZero = false;
akimZeroAdc = hallZeroOlcAdc();
hafiza.putInt("iZero", akimZeroAdc);
akimSessInited = false;
}
/** AP IP kaybolursa (WiFi yığını kilitlenince) tam reset yerine softAP yeniden açılır. */
static void maintainWifiAp() {
if (millis() - wifiApHealthMs < WIFI_AP_HEALTH_MS) return;
wifiApHealthMs = millis();
IPAddress ip = WiFi.softAPIP();
if (ip[0] == 192 && ip[1] == 168 && ip[2] == 4) return;
Serial.printf("WiFi AP kayip (IP=%s) — yeniden aciliyor\n", ip.toString().c_str());
WiFi.softAPdisconnect(true);
delay(50);
WiFi.softAP(g_apSsid, "aktek2026", WIFI_AP_CHANNEL, 0, WIFI_AP_MAX_CONN);
WiFi.softAPConfig(apIP, apIP, IPAddress(255, 255, 255, 0));
esp_wifi_set_ps(WIFI_PS_NONE);
dnsServer.start(DNS_PORT, "*", apIP);
}
/** Sensör/LED önce; WiFi işleri kısa zaman diliminde (loop tıkanmasın). */
static void serviceNetworkBrief() {
const unsigned long budgetMs = 12UL;
const unsigned long t0 = millis();
do {
dnsServer.processNextRequest();
server.handleClient();
webSocket.loop();
} while ((millis() - t0) < budgetMs);
maintainWifiAp();
}
void updateLedsPatTik() {
bool fault = alarmPatAktif || alarmClogAktif || alarmPompaAktif || alarmStuckAktif || anaRoleMandalli;
unsigned long m = millis();
if (fault)
digitalWrite(LED_SISTEM, (m / LED_FAULT_YARIM_PERIYOT_MS) % 2 == 0 ? HIGH : LOW);
else
digitalWrite(LED_SISTEM, (m / LED_NORMAL_YARIM_PERIYOT_MS) % 2 == 0 ? HIGH : LOW);
}
void updateSessionStats(int v) {
if (!sessInited) {
sessMin = sessMax = v;
sessInited = true;
} else {
if (v < sessMin) sessMin = v;
if (v > sessMax) sessMax = v;
}
}
static void updateAkimSessionStatsMa(int ma) {
if (!akimSessInited) {
akimSessMinMa = akimSessMaxMa = ma;
akimSessInited = true;
} else {
if (ma < akimSessMinMa) akimSessMinMa = ma;
if (ma > akimSessMaxMa) akimSessMaxMa = ma;
}
}
int testKalanSaniye() {
if (wizardPhase != WZ_TEST) return 0;
long left = (long)(testEndMs - millis()) / 1000L;
return left > 0 ? (int)left : 0;
}
int clogTestKalanSn() {
if (clogPh != 2) return 0;
long left = (long)(clogTestEndMs - millis()) / 1000L;
return left > 0 ? (int)left : 0;
}
static int clogPencereKalanSn() {
unsigned long elapsed = millis() - clogWindowStart;
if (elapsed >= clogObserveMs) return 0;
return (int)((clogObserveMs - elapsed) / 1000UL);
}
int clogAnaKalanSn() {
if (!clogArmed || clogObserveMs == 0 || alarmClogAktif) return -1;
return clogPencereKalanSn();
}
uint8_t clogAnaMod() {
if (!clogArmed || clogObserveMs == 0 || alarmClogAktif) return 0;
return 0;
}
void otaUpdatePage() {
const char *pg = R"OTA(
<!DOCTYPE html><html lang="tr"><head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Yazılım güncelleme — Aktek Elektronik</title>
<style>
*{box-sizing:border-box;}
body{margin:0;min-height:100vh;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
background:linear-gradient(165deg,#f1f5f9 0%,#ffffff 42%,#f8fafc 100%);color:#0f172a;
display:flex;align-items:center;justify-content:center;padding:24px 16px;}
.wrap{width:100%;max-width:440px;}
.card{background:#fff;border-radius:22px;padding:40px 32px 32px;
box-shadow:0 1px 3px rgba(15,23,42,.06),0 20px 50px -12px rgba(0,86,179,.14);
border:1px solid rgba(226,232,240,.9);}
.brand{font-size:11px;font-weight:700;letter-spacing:.22em;color:#64748b;text-transform:uppercase;margin:0 0 4px;}
.mark{font-size:26px;font-weight:800;color:#0056b3;margin:0 0 4px;letter-spacing:-.02em;}
.mark span{color:#007bff;}
.sub{font-size:13px;color:#94a3b8;margin:0 0 28px;}
h1{font-size:1.5rem;font-weight:700;margin:0 0 14px;line-height:1.25;color:#0f172a;}
.lead{font-size:15px;line-height:1.65;color:#475569;margin:0 0 18px;}
.note{font-size:13px;line-height:1.5;color:#64748b;margin:0;padding:14px 16px;background:#f8fafc;border-radius:12px;border:1px solid #e2e8f0;}
.note strong{color:#0f172a;}
.steps{font-size:13px;line-height:1.65;color:#475569;margin:0 0 20px;padding:16px 18px;background:#f8fafc;border-radius:12px;border:1px solid #e2e8f0;text-align:left;}
.steps ol{margin:8px 0 0;padding-left:1.25em;}
.steps li{margin:6px 0;}
.drop{margin:22px 0 10px;padding:22px 16px;border:2px dashed #cbd5e1;border-radius:16px;background:#fafbfc;text-align:center;transition:border-color .2s,background .2s;}
.drop:focus-within{border-color:#007bff;background:#f0f7ff;}
.drop input[type=file]{width:100%;max-width:100%;font-size:14px;color:#334155;}
.btn{margin-top:16px;width:100%;border:0;border-radius:14px;padding:16px 22px;font-size:16px;font-weight:700;cursor:pointer;
background:linear-gradient(135deg,#0056b3,#007bff);color:#fff;
box-shadow:0 6px 20px rgba(0,86,179,.32);transition:transform .15s,box-shadow .15s,filter .15s;}
.btn:hover{filter:brightness(1.04);box-shadow:0 8px 26px rgba(0,86,179,.38);}
.btn:active{transform:scale(.99);}
.btn:disabled{opacity:.55;cursor:not-allowed;transform:none;}
.foot{margin-top:28px;padding-top:22px;border-top:1px solid #f1f5f9;text-align:center;font-size:12px;color:#94a3b8;line-height:1.5;}
.foot em{font-style:normal;font-weight:600;color:#64748b;}
</style></head><body>
<div class="wrap"><div class="card">
<p class="brand">Aktek Elektronik</p>
<p class="mark">AKTEK<span>.</span></p>
<p class="sub">Klor / süreç kontrol — kablosuz yazılım güncelleme</p>
<h1>Yazılım güncelleme</h1>
<p class="lead">Bilgisayarınızda hazırladığınız firmware dosyasını (<strong>.bin</strong>) bu sayfadan cihaza gönderirsiniz. Aşağıdaki adımları sırayla uygulayın.</p>
<div class="steps"><strong>Nasıl yapılır?</strong>
<ol>
<li>Arduino IDE: <em>Sketch → Export Compiled Binary</em> veya PlatformIO ile projeyi derleyip <strong>.bin</strong> dosyasını bulun.</li>
<li>Dosya alanından <strong>.bin</strong> dosyasını seçin (yanlış dosya cihazı bozabilir).</li>
<li><strong>Yükle ve güncelle</strong> düğmesine basın; yükleme bitene kadar bekleyin (birkaç dakika sürebilir).</li>
<li>İşlem başarılıysa cihaz kendini yeniden başlatır; tekrar Wi‑Fi ağına bağlanın.</li>
</ol></div>
<p class="note"><strong>Önemli:</strong> Yükleme boyunca gücü kesmeyin, kabloyu çıkarmayın ve telefonunuzun bu ağa bağlı kaldığından emin olun.</p>
<form method="POST" action="/update" enctype="multipart/form-data" id="f">
<div class="drop"><input type="file" name="update" accept=".bin,.BIN" required></div>
<button type="submit" class="btn" id="b">Yükle ve güncelle</button>
</form>
<p class="foot"><em>Aktek Elektronik</em><br>Endüstriyel ölçüm ve otomasyon çözümleri</p>
</div></div>
<script>document.getElementById('f').onsubmit=function(){var b=document.getElementById('b');b.disabled=true;b.textContent='Yükleniyor…';};</script>
</body></html>
)OTA";
server.send(200, "text/html", pg);
}
void otaUpdateDone() {
bool ok = !Update.hasError();
server.sendHeader("Connection", "close");
if (ok) {
server.send(200, "text/plain; charset=utf-8",
"Tamam. Cihaz yeniden basliyor...");
delay(400);
ESP.restart();
} else {
String err = Update.hasError() ? Update.errorString() : "Bilinmeyen hata";
server.send(500, "text/plain; charset=utf-8", err.c_str());
}
}
void otaUpdateUpload() {
HTTPUpload &u = server.upload();
if (u.status == UPLOAD_FILE_START) {
Serial.printf("OTA basladi: %s\n", u.filename.c_str());
if (!Update.begin(UPDATE_SIZE_UNKNOWN)) {
Update.printError(Serial);
}
} else if (u.status == UPLOAD_FILE_WRITE) {
if (Update.write(u.buf, u.currentSize) != u.currentSize) {
Update.printError(Serial);
}
} else if (u.status == UPLOAD_FILE_END) {
if (Update.end(true)) {
Serial.printf("OTA bitti: %u bayt\n", u.totalSize);
} else {
Update.printError(Serial);
}
} else if (u.status == UPLOAD_FILE_ABORTED) {
Update.abort();
Serial.println("OTA iptal");
}
}
void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) {
if (type != WStype_TEXT) return;
String msg = (char *)payload;
if (msg == "AKIM:ZERO") {
pendingAkimZero = true;
return;
}
if (msg == "RESET:MM") {
int v = flowPps;
sessMin = sessMax = v;
sessInited = true;
int hallAdc = okuHallMedian5();
int hDev = abs(hallAdc - akimZeroAdc);
akimSessMinMa = akimSessMaxMa = hDev;
akimSessInited = true;
return;
}
if (msg == "MANDAL:AC") {
if (mandalVeyaMandalliArizaAktif()) {
mandalResetButonuAc();
Serial.println("Web: mandal acildi");
}
return;
}
if (msg.startsWith("TEST:START:")) {
if (wizardPhase == WZ_TEST) return;
unsigned long dur = (unsigned long)msg.substring(11).toInt();
if (dur < 30000UL) dur = 30000UL;
testDurationMs = dur;
testEndMs = millis() + dur;
testPeak = flowPps;
wizardPhase = WZ_TEST;
usePeakThreshold = false;
peakThreshold = 0;
hafiza.putBool("usePeak", false);
return;
}
if (msg == "WIZ:NEXT" && wizardPhase == WZ_SHOW_MAX) {
wizardPhase = WZ_ASK_ADD;
return;
}
if (msg.startsWith("WIZ:OFFSET:") && wizardPhase == WZ_ASK_ADD) {
int add = msg.substring(11).toInt();
if (add < 0) add = 0;
peakThreshold = testPeak + add;
usePeakThreshold = true;
wizardPhase = WZ_ARMED;
patKalibTepe = testPeak;
hafiza.putBool("usePeak", true);
hafiza.putInt("peakThr", peakThreshold);
hafiza.putInt("peakTst", patKalibTepe);
return;
}
if (msg == "WIZ:CANCEL") {
if (wizardPhase == WZ_ARMED) return;
wizardPhase = WZ_IDLE;
testPeak = 0;
return;
}
if (msg == "WIZ:CLEARTHR") {
usePeakThreshold = false;
peakThreshold = 0;
patKalibTepe = 0;
hafiza.putBool("usePeak", false);
hafiza.putInt("peakThr", 0);
hafiza.putInt("peakTst", 0);
wizardPhase = WZ_IDLE;
return;
}
if (msg == "CLOG:CLEAR" || msg == "CLOG:CANCEL" || msg == "CLOG:RECAL") {
clogSifirla();
return;
}
if (msg.startsWith("SET:RELPMP:")) {
int rel = msg.substring(11).toInt();
if (rel < 1) rel = 1;
if (rel > REL_SANIYE_MAX) rel = REL_SANIYE_MAX;
relPompaSaniye = rel;
hafiza.putInt("relPmp", relPompaSaniye);
return;
}
if (msg.startsWith("SET:PUMP:")) {
// SET:PUMP:<limitMa>,<sureDk>[,<relSn>] (relSn artık Ayarlar'dan yönetilir; geriye uyum için opsiyonel)
String r = msg.substring(9);
int c1 = r.indexOf(',');
if (c1 <= 0) return;
int c2 = r.indexOf(',', c1 + 1);
int lim = r.substring(0, c1).toInt();
int sure = (c2 > c1) ? r.substring(c1 + 1, c2).toInt() : r.substring(c1 + 1).toInt();
if (lim < 0) lim = 0;
if (lim > 50000) lim = 50000;
if (sure < 1) sure = 1;
if (sure > 240) sure = 240;
pompaAkimUstLimitMa = lim;
pompaArizaSureDk = sure;
hafizaPompaKaydet();
// c2 varsa (eski istemci), röleyi de set edelim ama artık tercih edilen yer Ayarlar.
if (c2 > c1) {
int rel = r.substring(c2 + 1).toInt();
if (rel < 1) rel = 1;
if (rel > REL_SANIYE_MAX) rel = REL_SANIYE_MAX;
relPompaSaniye = rel;
hafiza.putInt("relPmp", relPompaSaniye);
}
return;
}
if (msg.startsWith("SET:INV32:")) {
int v = msg.substring(10).toInt();
invertCcaAdc = (v != 0);
hafiza.putBool("inv32", invertCcaAdc);
return;
}
// Pompa arıza kalibrasyon sihirbazı
if (msg == "PUMP:OPEN") {
if (pompaAkimUstLimitMa > 0 && pompaArizaSureDk > 0) pumpPh = PZ_DONE;
else pumpPh = PZ_STOP;
pumpTestEndMs = 0;
pumpTestDurationMs = 0;
pumpSumAbsMa = 0;
pumpSampleCount = 0;
pumpAvgAbsMa = 0;
pumpAddMa = 0;
return;
}
if (msg == "PUMP:ACK_STOP" && pumpPh == PZ_STOP) {
pumpPh = PZ_DUR;
return;
}
if (msg.startsWith("PUMP:TEST:") && pumpPh == PZ_DUR) {
unsigned long dur = (unsigned long)msg.substring(10).toInt();
if (dur < 30000UL) dur = 30000UL;
if (dur > 20UL * 60UL * 1000UL) dur = 20UL * 60UL * 1000UL;
pumpTestDurationMs = dur;
pumpTestEndMs = millis() + dur;
pumpLastSampleMs = 0;
pumpSumAbsMa = 0;
pumpSampleCount = 0;
pumpAvgAbsMa = 0;
pumpAddMa = 0;
pumpPh = PZ_TEST;
return;
}
if (msg == "PUMP:NEXT" && pumpPh == PZ_SHOW_AVG) {
pumpPh = PZ_ASK_ADD;
return;
}
if (msg.startsWith("PUMP:ADD:") && pumpPh == PZ_ASK_ADD) {
int add = msg.substring(9).toInt();
if (add < 0) add = 0;
pumpAddMa = add;
pompaAkimUstLimitMa = pumpAvgAbsMa + pumpAddMa;
if (pompaAkimUstLimitMa < 0) pompaAkimUstLimitMa = 0;
if (pompaAkimUstLimitMa > 50000) pompaAkimUstLimitMa = 50000;
// sıradaki adım: izleme süresi seçimi (dk)
pumpPh = PZ_OBS;
return;
}
if (msg.startsWith("PUMP:OBS:") && pumpPh == PZ_OBS) {
int m = msg.substring(9).toInt();
if (m < 1) m = 1;
if (m > 240) m = 240;
pompaArizaSureDk = m;
hafizaPompaKaydet();
pumpPh = PZ_DONE;
return;
}
if (msg == "PUMP:CANCEL") {
if (pumpPh == PZ_DONE && pompaAkimUstLimitMa > 0) return;
pumpPh = (pompaAkimUstLimitMa > 0 && pompaArizaSureDk > 0) ? PZ_DONE : PZ_STOP;
pumpTestEndMs = 0;
pumpTestDurationMs = 0;
pumpSumAbsMa = 0;
pumpSampleCount = 0;
pumpAvgAbsMa = 0;
pumpAddMa = 0;
return;
}
if (msg == "PUMP:CLEAR") {
pompaAkimUstLimitMa = 0;
pumpAddMa = 0;
pumpAvgAbsMa = 0;
pumpSumAbsMa = 0;
pumpSampleCount = 0;
pumpTestEndMs = 0;
pumpTestDurationMs = 0;
pumpPh = PZ_STOP;
hafizaPompaKaydet();
return;
}
}
String getHTML() {
String html = "<!DOCTYPE html><html><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width, initial-scale=1'>";
html += "<style>body{font-family:sans-serif;background:#f4f7f6;text-align:center;padding:12px;margin:0;}";
html += ".container{max-width:420px;margin:auto;background:#fff;padding:18px;border-radius:20px;box-shadow:0 8px 24px rgba(0,0,0,.08);}";
html += ".row{display:flex;gap:8px;justify-content:center;flex-wrap:wrap;margin:10px 0;}";
html += ".pill{background:#e9ecef;padding:10px 14px;border-radius:12px;font-weight:bold;min-width:88px;}";
html += ".pill span{display:block;font-size:11px;color:#666;font-weight:600;}";
html += ".pill-strip{display:flex;flex-wrap:nowrap;gap:6px;margin:10px 0;justify-content:stretch;}";
html += ".pill-strip .pill{flex:1 1 0;min-width:0;padding:8px 6px;border-radius:10px;box-sizing:border-box;}";
html += ".pill-strip .pill span:first-child{font-size:9px;line-height:1.2;}";
html += ".pill-strip .pill span:last-child{font-size:14px;margin-top:2px;}";
html += ".live-readouts{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:12px 0;align-items:stretch;}";
html += ".cca-card{background:linear-gradient(135deg,#0056b3,#007bff);color:#fff;padding:16px 14px 10px;border-radius:16px;margin:12px 0;text-align:center;}";
html += ".cca-title{font-size:15px;font-weight:bold;letter-spacing:.14em;opacity:.95;}";
html += ".cca-range{font-size:11px;opacity:.78;margin-top:3px;}";
html += ".cca-num{font-size:48px;font-weight:bold;line-height:1.15;margin-top:8px;padding-bottom:4px;}";
html += ".curr-card{background:linear-gradient(135deg,#0b7a28,#22c55e);color:#fff;padding:16px 14px 10px;border-radius:16px;margin:12px 0;text-align:center;}";
html += ".curr-title{font-size:15px;font-weight:bold;letter-spacing:.14em;opacity:.95;}";
html += ".curr-num{font-size:48px;font-weight:bold;line-height:1.15;margin-top:8px;padding-bottom:4px;}";
html += ".live-readouts .cca-card,.live-readouts .curr-card{margin:0;min-width:0;padding:0;text-align:left;background:#f1f5f9;color:#0f172a;border-radius:14px;box-shadow:0 4px 16px rgba(15,23,42,.08),inset 0 1px 0 #fff;overflow:hidden;}";
html += ".live-readouts .cca-card{border:3px solid #1d4ed8;}";
html += ".live-readouts .curr-card{border:3px solid #15803d;}";
html += ".live-readouts .cca-title,.live-readouts .curr-title{font-size:10px;font-weight:800;letter-spacing:.16em;text-transform:uppercase;color:#64748b;padding:10px 12px 0;line-height:1.2;opacity:1;}";
html += ".live-readouts .cca-num,.live-readouts .curr-num{font-size:clamp(20px,9vw,36px);font-weight:800;font-family:ui-monospace,Cascadia Mono,Consolas,monospace;letter-spacing:.06em;color:#0f172a;background:#e2e8f0;margin:8px 10px 12px;padding:12px 10px;border-radius:10px;line-height:1.1;text-align:center;box-shadow:inset 0 2px 6px rgba(15,23,42,.14);border:1px solid #cbd5e1;}";
html += ".live-readouts .curr-num .unit{font-size:11px;color:#475569;margin-left:4px;vertical-align:super;font-weight:700;letter-spacing:0;}";
html += ".unit{font-size:14px;font-weight:800;opacity:.9;margin-left:6px;letter-spacing:.08em;}";
html += ".curr-range{font-size:11px;opacity:.85;margin-top:6px;font-weight:700;}";
html += ".btn{background:#0056b3;color:#fff;padding:11px;border:none;border-radius:10px;cursor:pointer;font-weight:bold;width:100%;margin-top:10px;font-size:15px;}";
html += ".mandal-strip{margin:10px 0 12px;padding:12px 14px;border-radius:12px;background:#e8f5e9;border:2px solid #81c784;display:flex;flex-wrap:wrap;align-items:center;gap:8px 12px;}";
html += ".mandal-strip.locked{background:#ffebee;border-color:#e57373;}";
html += ".mandal-strip .mandal-lbl{font-size:13px;font-weight:700;color:#555;}";
html += ".mandal-strip .mandal-val{font-size:16px;font-weight:800;}";
html += ".mandal-strip .mandal-sub{font-size:12px;color:#666;flex:1 1 100%;}";
html += ".btn-mandal-open{background:#c62828;color:#fff;padding:8px 14px;border:none;border-radius:8px;font-weight:bold;font-size:14px;cursor:pointer;margin-left:auto;}";
html += ".btn:hover{background:#004494;}.btn-sec{background:#6c757d;}.btn-go{background:#28a745;}";
html += ".btn-dur{background:#fff;color:#0056b3;border:2px solid #0056b3;width:30%;margin:4px 1%;display:inline-block;padding:12px 0;font-weight:bold;min-height:48px;touch-action:manipulation;}";
html += ".btn-kal{background:linear-gradient(135deg,#c82333,#dc3545);}";
html += ".btn-clog{background:linear-gradient(135deg,#b8860b,#e6a017);color:#1a1a1a;}";
html += ".btn-pump{background:linear-gradient(135deg,#0b7a28,#22c55e);color:#fff;}";
html += ".btn-big-ack{font-size:17px;padding:16px;margin-top:14px;}";
html += ".alert{background:#dc3545;color:#fff;padding:14px;border-radius:10px;font-weight:bold;margin-top:10px;display:none;font-size:16px;}";
html += ".alert-clog{background:#856404;color:#fff;}";
html += ".panel{border:1px solid #dee2e6;border-radius:12px;padding:14px;margin-top:12px;text-align:left;}";
html += ".panel h3{margin:0 0 10px;color:#0056b3;font-size:17px;}.panel.clog-panel h3{color:#0d5c4d;}";
html += ".screen{display:none;}.screen.show{display:block;}";
html += ".cal-step,.clog-step{display:none!important;text-align:left;pointer-events:none;}";
html += ".cal-step.on,.clog-step.on{display:block!important;pointer-events:auto;}";
html += ".input-group{margin-bottom:12px;} .input-group label{font-weight:bold;color:#555;display:block;margin-bottom:4px;font-size:13px;}";
html += ".input-group input{width:100%;padding:8px;border:1px solid #ccc;border-radius:6px;box-sizing:border-box;}";
html += ".u{font-size:12px;font-weight:800;color:#64748b;margin-left:6px;letter-spacing:.08em;}";
html += ".hint{font-size:13px;color:#555;line-height:1.45;margin:0 0 10px;}";
html += ".main-hints{margin-top:20px;padding-top:16px;border-top:1px solid #e2e8f0;text-align:left;}";
html += ".main-hints .hint:last-child{margin-bottom:0;}";
html += ".btn-grid2{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:16px;}";
html += ".btn-grid2 .btn{margin-top:0;width:100%;font-size:14px;padding:14px 10px;min-height:56px;line-height:1.25;box-sizing:border-box;display:flex;align-items:center;justify-content:center;text-align:center;}";
html += ".big-stop{font-size:clamp(20px,5.5vw,26px);font-weight:800;color:#0d5c4d;line-height:1.25;text-align:center;margin:18px 0 8px;}";
html += ".btn-obs{width:47%;margin:4px 1%;display:inline-block;padding:12px 4px;font-size:14px;}";
html += ".clog-main-cd{display:none;margin-top:12px;padding:12px 14px;border-radius:12px;background:#fff8e6;border:1px solid #e6c35c;font-size:15px;font-weight:700;color:#5c4a00;line-height:1.35;}";
html += ".tour-overlay{position:fixed;inset:0;z-index:9997;background:rgba(15,23,42,.55);display:none;align-items:flex-end;justify-content:center;padding:12px;box-sizing:border-box;}";
html += ".tour-overlay.tour-on{display:flex;}";
html += ".tour-panel{background:#fff;border-radius:18px;padding:16px 16px 14px;max-width:400px;width:100%;box-sizing:border-box;box-shadow:0 8px 40px rgba(0,0,0,.28);position:relative;z-index:9998;text-align:left;margin-bottom:max(8px,env(safe-area-inset-bottom,0));}";
html += ".tour-meta{font-size:11px;font-weight:800;color:#64748b;letter-spacing:.1em;margin:0 0 8px;text-transform:uppercase;}";
html += ".tour-txt{font-size:15px;line-height:1.55;color:#0f172a;margin:0 0 14px;}";
html += ".tour-txt strong{color:#0056b3;}";
html += ".tour-finger{position:fixed;left:0;top:0;display:none;font-size:44px;line-height:1;z-index:10000;pointer-events:none;filter:drop-shadow(0 2px 4px rgba(0,0,0,.4));transform:translate(-50%,0);transform-origin:center bottom;will-change:transform;animation:tourBounce .72s ease-in-out infinite;}";
html += "@keyframes tourBounce{0%,100%{transform:translate(-50%,0)}50%{transform:translate(-50%,-10px)}}";
html += ".tour-next{margin-top:0!important;}.tour-skip{margin-top:8px!important;font-size:14px!important;padding:10px!important;}</style>";
html += "<script>";
html += "var socket=null,phase=0,tleft=0,tmax=0,thr=0,val=0,wsQ=[],clogPh=10,clogTl=0,clogTpk=0,clogThr=0,clogObs=0,alC=0,kC=0,ccb=0,clogAkSn=-1,clogAkMod=0,lastRelPompa=10,lastInv32=0,inv32Dirty=0,pumpAl=0,pumpK=0,pumpLim=0,pumpSure=0,pumpPh=0,pumpTl=0,pumpAvg=0,pumpRun=0,pumpCur=0,mandalLk=0,mandalNeden=0;";
html += "function updMandal(){var b=el('mandalStrip'),d=el('mandalDurum'),s=el('mandalSub'),btn=el('mandalAcBtn');if(!d)return;if(mandalLk){d.textContent='Kilitli';d.style.color='#b71c1c';if(b)b.classList.add('locked');if(s)s.textContent=mandalNeden===1?'Neden: boru patlak mandal\\u0131':mandalNeden===2?'Neden: t\\u0131kanma / ak\\u0131\\u015f yok mandal\\u0131':'Neden: mandal aktif';if(btn)btn.style.display='inline-block';}else{d.textContent='A\\u00e7\\u0131k';d.style.color='#2e7d32';if(b)b.classList.remove('locked');if(s)s.textContent='Ana pompa mandalda de\\u011fil';if(btn)btn.style.display='none';}}";
html += "function mandalAc(){if(!mandalLk){return;}if(confirm('Ana pompa mandal\\u0131n\\u0131 ve ilgili alarmlar\\u0131 a\\u00e7mak istiyor musunuz?'))send('MANDAL:AC');}";
html += "var wsUrl='ws://192.168.4.1:81/';";
html += "function el(id){return document.getElementById(id);}";
html += "var CCA_MAX=4096;";
html += "function setCca(v){var c=Math.max(0,Math.min(CCA_MAX,parseInt(v,10)||0));var n=el('v0');if(n)n.innerHTML=c;return c;}";
html += "function setCcb(v){var c=Math.max(0,Math.min(CCA_MAX,parseInt(v,10)||0));ccb=c;return c;}";
html += "function setAkim(v){var c=parseInt(v,10);if(isNaN(c))c=0;var n=el('i0');if(n)n.innerHTML=c;return c;}";
html += "function setAkimMinMax(mi,ma){var n1=el('imn'),n2=el('imx');if(n1)n1.innerHTML=parseInt(mi,10)||0;if(n2)n2.innerHTML=parseInt(ma,10)||0;}";
html += "function setHamOlc(v){var n=el('iham');if(n)n.innerHTML=parseInt(v,10)||0;}";
html += "function showMain(){el('sMain').style.display='block';el('sCal').classList.remove('show');el('sAyar').classList.remove('show');el('sAkim').classList.remove('show');}";
html += "function openCal(){el('sAyar').classList.remove('show');el('sMain').style.display='none';el('sCal').classList.add('show');syncCal();}";
html += "function openAkim(){el('sAyar').classList.remove('show');el('sCal').classList.remove('show');send('WIZ:CANCEL');el('sMain').style.display='none';el('sAkim').classList.add('show');send('PUMP:OPEN');syncPump();}";
html += "function bindInv32(){var c=el('inv32');if(!c||c._bnd)return;c._bnd=1;c.addEventListener('change',function(){inv32Dirty=1;lastInv32=this.checked?1:0;});}";
html += "function openAyar(){el('sCal').classList.remove('show');el('sAkim').classList.remove('show');el('sMain').style.display='none';el('sAyar').classList.add('show');if(el('relPompaInAyar'))el('relPompaInAyar').value=lastRelPompa;if(el('inv32'))el('inv32').checked=(lastInv32==1);inv32Dirty=0;bindInv32();}";
html += "function goMain(){if(el('sAyar').classList.contains('show')){el('sAyar').classList.remove('show');el('sMain').style.display='block';return;}if(el('sAkim').classList.contains('show')){el('sAkim').classList.remove('show');el('sMain').style.display='block';send('PUMP:CANCEL');return;}if(phase===1||phase===2||phase===3)send('WIZ:CANCEL');showMain();}";
html += "function fmtMMSS(s){s=parseInt(s,10)||0;var m=Math.floor(s/60),r=s%60;return m+':'+(r<10?'0':'')+r;}";
html += "function updNextDoseStrip(){var cw=el('clogCountMainWrap'),cm=el('clogCountMain'),cs=el('clogCountSub');if(!cw||!cm)return;var ok=!alC&&clogAkSn>=0;if(ok){cw.style.display='block';cm.innerHTML=fmtMMSS(clogAkSn);if(cs)cs.textContent='Doz yok; 1,5 dk dolunca t\\u0131kanma. Doz gelince s\\u0131f\\u0131rlan\\u0131r.';}else{cw.style.display='none';}}";
html += "function syncCal(){['cDur','cTest','cMax','cAdd','cArm','cSumPat'].forEach(function(id){var n=el(id);if(n)n.classList.remove('on');});";
html += "if(phase===4&&thr>0){el('cSumPat').classList.add('on');return;}";
html += "if(phase===0)el('cDur').classList.add('on');else if(phase===1)el('cTest').classList.add('on');else if(phase===2)el('cMax').classList.add('on');else if(phase===3)el('cAdd').classList.add('on');else el('cArm').classList.add('on');}";
html += "function syncPump(){if(pumpPh>6)return;['p0','p1','p2','p3','p4','p5','p6'].forEach(function(id){var n=el(id);if(n)n.classList.remove('on');});var x=el('p'+pumpPh);if(x)x.classList.add('on');}";
html += "function apply(csv){var p=csv.split(',');if(p.length<17)return;";
html += "val=parseInt(p[0],10)||0;var al=parseInt(p[1],10)||0,su=parseInt(p[2],10)||0;";
html += "var smin=parseInt(p[3],10)||0,smax=parseInt(p[4],10)||0;var ph=parseInt(p[5],10);phase=(isNaN(ph)||ph<0||ph>4)?0:ph;";
html += "tleft=parseInt(p[6],10)||0;tmax=parseInt(p[7],10)||0;thr=parseInt(p[8],10)||0;";
html += "setCcb(parseInt(p[9],10)||0);clogPh=parseInt(p[10],10);if(isNaN(clogPh))clogPh=10;";
html += "clogTl=parseInt(p[11],10)||0;clogTpk=parseInt(p[12],10)||0;clogThr=parseInt(p[13],10)||0;clogObs=parseInt(p[14],10)||0;";
html += "alC=parseInt(p[15],10)||0;kC=parseInt(p[16],10)||0;";
html += "if(p.length>17){var _s=parseInt(p[17],10);clogAkSn=isNaN(_s)?-1:_s;}else{clogAkSn=-1;}";
html += "if(p.length>18){var _m=parseInt(p[18],10);clogAkMod=isNaN(_m)?0:_m;}else{clogAkMod=0;}";
html += "if(p.length>19)setAkim(parseInt(p[19],10)||0);else setAkim(0);";
html += "if(p.length>21)setAkimMinMax(p[20],p[21]);else setAkimMinMax(0,0);";
html += "if(p.length>22)setHamOlc(p[22]);else setHamOlc(0);";
html += "if(p.length>25){pumpAl=parseInt(p[24],10)||0;pumpK=parseInt(p[25],10)||0;}else{pumpAl=0;pumpK=0;}";
html += "if(p.length>27){pumpLim=parseInt(p[26],10)||0;pumpSure=parseInt(p[27],10)||0;}else{pumpLim=0;pumpSure=0;}";
html += "if(p.length>28){var rp2=parseInt(p[28],10);if(!isNaN(rp2)){rp2=Math.max(1,Math.min(" + String(REL_SANIYE_MAX) + ",rp2));lastRelPompa=rp2;if(el('relPompaInAyar')&&!el('sAyar').classList.contains('show'))el('relPompaInAyar').value=rp2;}}";
html += "if(p.length>33){pumpPh=parseInt(p[29],10)||0;pumpTl=parseInt(p[30],10)||0;pumpAvg=parseInt(p[31],10)||0;pumpRun=parseInt(p[32],10)||0;pumpCur=parseInt(p[33],10)||0;}else{pumpPh=0;pumpTl=0;pumpAvg=0;pumpRun=0;pumpCur=0;}";
html += "if(p.length>34){var iv=parseInt(p[34],10);lastInv32=isNaN(iv)?0:(iv?1:0);if(el('sAyar').classList.contains('show')&&el('inv32')&&!inv32Dirty)el('inv32').checked=(lastInv32==1);}";
html += "if(p.length>35)mandalLk=parseInt(p[35],10)||0;if(p.length>36)mandalNeden=parseInt(p[36],10)||0;updMandal();";
html += "var cca=setCca(val);if(el('vCalClog'))el('vCalClog').innerHTML=ccb;el('mn').innerHTML=smin;el('mx').innerHTML=smax;el('vCal').innerHTML=cca;";
html += "if(el('sumPatThr'))el('sumPatThr').innerHTML=thr;if(el('sumPatPeak'))el('sumPatPeak').innerHTML=tmax;";
html += "el('tmax').innerHTML=tmax;el('tmax2').innerHTML=tmax;el('tmaxBig').innerHTML=tmax;el('thrDisp').innerHTML=thr;";
html += "el('cd').innerHTML=tleft>0?('Kalan \\u00fcre: '+tleft+' sn'):'S\\u00fcre tamamland\\u0131';";
html += "if(el('clogCd'))el('clogCd').innerHTML=clogTl>0?('Kalan \\u00fcre: '+clogTl+' sn'):'S\\u00fcre tamamland\\u0131';";
html += "if(el('sCal').classList.contains('show'))syncCal();";
html += "if(al==1){el('al-pat').style.display='block';el('kalan').innerHTML='Alarm: '+su+' sn';}else{el('al-pat').style.display='none';}";
html += "if(alC==1){el('al-clog').style.display='block';el('kalanClog').innerHTML='Pompa mandalda. Reset d\\u00fc\\u011fmesine 3 sn bas\\u0131n.';}else{el('al-clog').style.display='none';}";
html += "if(el('al-pump')){if(pumpAl==1){el('al-pump').style.display='block';el('kalanPump').innerHTML='Alarm: '+pumpK+' sn';}else{el('al-pump').style.display='none';}}";
html += "if(el('pumpCd'))el('pumpCd').innerHTML=pumpTl>0?('Kalan s\\u00fcre: '+pumpTl+' sn'):'S\\u00fcre tamamland\\u0131';";
html += "if(el('pumpAvgDisp'))el('pumpAvgDisp').innerHTML=pumpAvg;";
html += "if(el('pumpLimDisp'))el('pumpLimDisp').innerHTML=pumpLim;";
html += "if(el('pumpRunDisp'))el('pumpRunDisp').innerHTML=pumpRun;";
html += "if(el('pumpCurDisp'))el('pumpCurDisp').innerHTML=pumpCur;";
html += "if(el('sAkim').classList.contains('show'))syncPump();";
html += "updNextDoseStrip();";
html += "}";
html += "function wsFlush(){while(socket&&socket.readyState===1&&wsQ.length)socket.send(wsQ.shift());}";
html += "function send(m){if(socket&&socket.readyState===1)socket.send(m);else wsQ.push(m);}";
html += "function connectWs(){try{socket=new WebSocket(wsUrl);socket.onopen=function(){wsFlush();};socket.onmessage=function(e){apply(e.data);};socket.onclose=function(){setTimeout(connectWs,1200);};socket.onerror=function(){try{socket.close();}catch(z){}};}catch(e){setTimeout(connectWs,1200);}}";
html += "var tourI=0,tourSteps=[];";
html += "tourSteps.push({t:'<strong>Ho\\u015f geldiniz.</strong> \\u00dcstteki d\\u00f6rt kutuda bu oturumda g\\u00f6r\\u00fclen ak\\u0131\\u015f ve pompa <strong>min / maks</strong> de\\u011ferleri yer al\\u0131r. Yeni \\u00f6l\\u00e7\\u00fcme ba\\u015flarken <strong>Min / Maks s\\u0131f\\u0131rla</strong> d\\u00fc\\u011fmesine bas\\u0131n.',q:'#tourAdim1Hedef',fp:'below',noScroll:1});";
html += "tourSteps.push({t:'<strong>Anl\\u0131k veriler:</strong> Soldaki panel debimetre <strong>ak\\u0131\\u015f\\u0131</strong>, sa\\u011fdaki panel <strong>pompa verisini</strong> g\\u00f6sterir. Kurulumda \\u00f6nce boru patlama, sonra t\\u0131kanma ve pompa kalibrasyonlar\\u0131n\\u0131 tamamlaman\\u0131z \\u00f6nerilir.',q:'#sMain .live-readouts'});";
html += "tourSteps.push({t:'<strong>Kalibrasyon ve ayarlar:</strong> K\\u0131rm\\u0131z\\u0131, sar\\u0131 ve ye\\u015fil d\\u00fc\\u011fmeler sihirbazlara a\\u00e7\\u0131l\\u0131r; <strong>Ayarlar</strong> pompa r\\u00f6lesi s\\u00fcresi ve sens\\u00f6r y\\u00f6n\\u00fcn\\u00fc i\\u00e7erir.',q:'#sMain .btn-grid2'});";
html += "tourSteps.push({t:'<strong>Bilgi alan\\u0131:</strong> En altta her konu i\\u00e7in k\\u0131sa a\\u00e7\\u0131klamalar bulunur. <strong>Kapat</strong> veya <strong>Tamam</strong> ile bu tan\\u0131t\\u0131m\\u0131 bitirebilirsiniz; sayfa her a\\u00e7\\u0131l\\u0131\\u015f\\u0131nda yeniden g\\u00f6sterilir.',q:'#tourMainHints > p.hint:first-of-type',fp:'below'});";
html += "function tourPos(){var f=el('tourFinger');if(!f)return;var S=tourSteps[tourI];if(!S||!S.q){f.style.display='none';return;}var n=document.querySelector(S.q);if(!n){f.style.display='none';return;}if(!S.noScroll){try{n.scrollIntoView({block:'nearest',inline:'nearest'});}catch(_){}}requestAnimationFrame(function(){var r=n.getBoundingClientRect();if(r.width<1&&r.height<1){f.style.display='none';return;}var cx=r.left+r.width*.5,fh=52,gap=10,pr=220,fp=S.fp||'auto',yb,ya,y;var b=window.innerHeight||document.documentElement.clientHeight||600;var w=window.innerWidth||document.documentElement.clientWidth||400;var ox=0,oy=0;if(fp==='above'){y=r.top-fh-gap;y=Math.max(oy+gap,y);}else if(fp==='below'){y=r.bottom+gap;var room=b-pr;if(y+fh>room){y=room-fh-6;y=Math.max(oy+gap,y);}else y=Math.max(oy+gap,y);}else{yb=r.bottom+gap;ya=r.top-fh-gap;y=yb;if(yb+fh>b-pr&&ya>oy+gap)y=ya;else if(yb+fh>b-pr)y=Math.max(oy+gap,ya);}cx=Math.max(26,Math.min(w-26,cx));y=Math.min(b-fh-10,Math.max(oy+gap,y));f.style.left=cx+'px';f.style.top=y+'px';f.style.display='block';});}";
html += "function tourShow(){var st=tourSteps[tourI];el('tourMeta').textContent='Ad\\u0131m '+(tourI+1)+' / '+tourSteps.length;el('tourTxt').innerHTML=st.t;var g=el('tourBtnGo');g.textContent=tourI>=tourSteps.length-1?'Tamam':'Devam';requestAnimationFrame(function(){requestAnimationFrame(tourPos);});}";
html += "function tourStart(){tourI=0;var o=el('tourOv');if(!o)return;o.style.display='flex';requestAnimationFrame(function(){o.classList.add('tour-on');tourShow();});}";
html += "function tourEnd(){var o=el('tourOv');if(o){o.classList.remove('tour-on');o.style.display='none';}if(el('tourFinger'))el('tourFinger').style.display='none';}";
html += "function tourNext(){if(tourI>=tourSteps.length-1){tourEnd();return;}tourI++;tourShow();}";
html += "function initTour(){var n=0;function tryTour(){n++;var o=el('tourOv');if(o&&(o.classList.contains('tour-on')||o.style.display==='flex'))return;var m=el('sMain');if(m&&m.style.display!=='none'&&o){tourStart();return;}if(n<24)setTimeout(tryTour,280);}setTimeout(tryTour,450);}";
html += "function initTourBoot(){var g=el('tourBtnGo'),sk=el('tourBtnSkip');if(!g||g._t)return;g._t=1;g.addEventListener('click',tourNext);sk.addEventListener('click',tourEnd);function tp(){if(el('tourOv')&&el('tourOv').classList.contains('tour-on'))tourPos();}window.addEventListener('resize',tp);window.addEventListener('scroll',tp,true);if(window.visualViewport)window.visualViewport.addEventListener('resize',tp);initTour();}";
html += "function bootConnect(){if(document.getElementById('sMain'))connectWs();else setTimeout(bootConnect,50);}";
html += "if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',function(){bootConnect();bindInv32();initTourBoot();});else{bootConnect();bindInv32();initTourBoot();}";
html += "function resetMM(){send('RESET:MM');}";
html += "function pickDur(ms){send('TEST:START:'+ms);}";
html += "function wizNext(){send('WIZ:NEXT');}";
html += "function wizOffset(){var o=parseInt(el('offIn').value,10)||0;if(o<0)o=0;send('WIZ:OFFSET:'+o);}";
html += "function wizCancel(){send('WIZ:CANCEL');}";
html += "function clearThr(){send('WIZ:CLEARTHR');showMain();}";
html += "function recalPat(){send('WIZ:CLEARTHR');}";
html += "function saveRel2(){var mx=" + String(REL_SANIYE_MAX) + ";var cRaw=el('relPompaInAyar').value;var c=parseInt(cRaw,10);if(cRaw===''||isNaN(c))c=lastRelPompa;c=Math.max(1,Math.min(mx,c));lastRelPompa=c;var inv=(el('inv32')&&el('inv32').checked)?1:0;lastInv32=inv;inv32Dirty=0;send('SET:RELPMP:'+c);send('SET:INV32:'+inv);goMain();}";
html += "function pumpAckStop(){send('PUMP:ACK_STOP');}";
html += "function pumpPickDur(ms){send('PUMP:TEST:'+ms);}";
html += "function pumpNext(){send('PUMP:NEXT');}";
html += "function pumpAdd(){var o=parseInt(el('pumpAddIn').value,10)||0;if(o<0)o=0;send('PUMP:ADD:'+o);}";
html += "function pumpPickObs(m){send('PUMP:OBS:'+m);}";
html += "function pumpCancel(){send('PUMP:CANCEL');goMain();}";
html += "function pumpClear(){send('PUMP:CLEAR');pumpPh=0;syncPump();}";
html += "</script></head><body>";
html += "<div class='container'><h2 style='color:#0056b3;margin:0 0 8px;'>AKTEK ELEKTRONİK OTOMASYON</h2>";
html += "<div id='al-pat' class='alert'>BORU PATLADI!<div class='countdown' id='kalan'></div></div>";
html += "<div id='al-clog' class='alert alert-clog' style='display:none;'>BORU TIKANDI!<div class='countdown' id='kalanClog'></div></div>";
html += "<div id='al-pump' class='alert' style='display:none;'>POMPA ARIZASI!<div class='countdown' id='kalanPump'></div></div>";
html += "<div id='tourFinger' class='tour-finger' aria-hidden='true'>👆</div>";
html += "<div id='tourOv' class='tour-overlay' style='display:none;'>";
html += "<div class='tour-panel'><p id='tourMeta' class='tour-meta'></p><p id='tourTxt' class='tour-txt'></p>";
html += "<button type='button' class='btn btn-go tour-next' id='tourBtnGo'>Devam</button>";
html += "<button type='button' class='btn btn-sec tour-skip' id='tourBtnSkip'>Kapat</button></div></div>";
html += "<div id='sMain'>";
html += "<div id='mandalStrip' class='mandal-strip'><span class='mandal-lbl'>Ana pompa mandal:</span> <span id='mandalDurum' class='mandal-val'>—</span>";
html += "<button type='button' id='mandalAcBtn' class='btn-mandal-open' style='display:none;' onclick='mandalAc()'>Mandalı aç</button>";
html += "<span id='mandalSub' class='mandal-sub'>Bağlanıyor…</span></div>";
html += "<div id='clogCountMainWrap' class='clog-main-cd' style='display:none;'>Doz yok sayacı: <span id='clogCountMain'>0:00</span><br><span id='clogCountSub' style='font-size:12px;font-weight:600;opacity:.9;display:block;margin-top:6px;'></span></div>";
html += "<div id='tourAdim1Hedef'>";
html += "<div class='pill-strip'><div class='pill'><span>Akış min</span><span id='mn'>0</span></div>";
html += "<div class='pill'><span>Akış max</span><span id='mx'>0</span></div>";
html += "<div class='pill'><span>Pompa min</span><span id='imn'>0</span></div>";
html += "<div class='pill'><span>Pompa max</span><span id='imx'>0</span></div></div>";
html += "<button class='btn btn-sec' onclick='resetMM()'>Min / Maks sıfırla</button></div>";
html += "<div class='live-readouts'>";
html += "<div class='cca-card'><div class='cca-title'>Son doz (darbe)</div><div class='cca-num' id='v0'>0</div></div>";
html += "<div class='curr-card'><div class='curr-title'>Pompa verisi</div><div class='curr-num'><span id='i0'>0</span><span class='unit'></span></div></div>";
html += "</div>";
html += "<div class='btn-grid2'>";
html += "<button type='button' class='btn btn-kal' onclick='openCal()'>BORU PATLAMA KALİBRASYONU</button>";
html += "<button type='button' class='btn btn-pump' onclick='openAkim()'>POMPA ARIZA KALİBRASYONU</button>";
html += "<button type='button' class='btn btn-sec' onclick='openAyar()'>Ayarlar</button>";
html += "</div>";
html += "<div class='main-hints' id='tourMainHints'>";
html += "<p class='hint'><strong>Boru patlama:</strong> Akış hızı izlenir. Sihirbazda tipik yüksek akışa göre eşik ve pay kurulur; normal çalışmada bu eşiği <strong>aşarsa</strong> alarm verilir. 3 eşik aşımında 30 sn uyarı verilir (pompa çalışır). 10 dk içinde 3 uyarı olursa ana pompa kesilir.</p>";
html += "<p class='hint'><strong>Akı yok / tıkanma:</strong> Her dozdaki darbeler toplanır (ekranda). <strong>1,5 dk</strong> hiç doz yoksa mandal. Pompa arızasında tıkanma yok.</p>";
html += "<p class='hint'><strong>Pompa:</strong> Önce pompa <strong>dururken</strong> boşta okuma alınır. Sonra pompayı çalıştırıp test süresinde ortalama <strong>pompa verisi</strong> → + ek ile "pompa gerçekten hareket ediyor" eşiği tanımlanır; bu eşiğin altında kalırsanız pompa arızası izlenir.</p>";
html += "</div></div>";
html += "<div id='sCal' class='screen'>";
html += "<button type='button' class='btn btn-sec' onclick='goMain()'>Ana sayfaya dön</button>";
html += "<div id='cSumPat' class='cal-step panel'><h3>Kayıtlı patlama kalibrasyonu</h3>";
html += "<p class='hint'>Cihazda kayıtlı değerler aşağıdadır. Yeniden ölçm yapmak için <strong>Yeniden kalibre et</strong> ile kayıt silinir ve sihirbaz başlar.</p>";
html += "<div class='pill' style='max-width:280px;margin:10px auto;text-align:center;'><span>Aktif eşik (patlama)</span><span id='sumPatThr'>0</span></div>";
html += "<div class='pill' style='max-width:280px;margin:10px auto;text-align:center;'><span>Kalibrasyonda maks. akış</span><span id='sumPatPeak'>0</span></div>";
html += "<button type='button' class='btn btn-go' onclick='recalPat()'>Yeniden kalibre et</button>";
html += "<button type='button' class='btn btn-sec' onclick='goMain()'>Ana sayfaya dön</button></div>";
html += "<div id='cDur' class='cal-step panel'><h3>1 — Test süresi</h3>";
html += "<p class='hint'>Sağlıklı sistemde gördüğünüz en yüksek akış hızını (veya patlak senaryosunu güvenle taklit edebiliyorsanız o koşulu) <strong>test süresi</strong> içinde ölçersiniz (30 sn–5 dk). Bu sürede gösterge sürekli güncellenir; kaydedilen <strong>maksimum</strong> değer sonraki adımlarda kullanılır. Bu ekranda durum göstergesi yanıp söner.</p>";
html += "<button type='button' class='btn-dur' onclick='pickDur(30000)'>30 sn</button>";
html += "<button type='button' class='btn-dur' onclick='pickDur(60000)'>1 dk</button>";
html += "<button type='button' class='btn-dur' onclick='pickDur(120000)'>2 dk</button><br>";
html += "<button type='button' class='btn-dur' onclick='pickDur(180000)'>3 dk</button>";
html += "<button type='button' class='btn-dur' onclick='pickDur(300000)'>5 dk</button></div>";
html += "<div id='cTest' class='cal-step panel'><h3>2 — Test</h3>";
html += "<p id='cd' style='font-size:24px;font-weight:bold;color:#0056b3;margin:8px 0;'>Kalan: —</p>";
html += "<p class='hint'>Geri sayım süresince <strong>anlık akış hızı</strong> izlenir. Alttaki değer, bu testte görülen <strong>en yüksek</strong> seviyedir. Test bitince sonraki adıma geçilir. Sorunda <strong>Testi iptal</strong>.</p>";
html += "<div class='pill' style='margin:10px auto;max-width:260px;text-align:center;'><span>Test süresince maksimum</span><span id='tmax'>0</span></div>";
html += "<button type='button' class='btn btn-sec' onclick='wizCancel()'>Testi iptal</button></div>";
html += "<div id='cMax' class='cal-step panel'><h3>3 — Sonuç</h3>";
html += "<p class='hint'><strong>Test bitti.</strong> Aşağdaki değer, test süresince kaydedilen <strong>en yüksek akış</strong>. Bir sonraki adımda <strong>+ ek</strong> girersiniz; <strong>patlama eşiği = maksimum + ek</strong> (gürültü payı için).</p>";
html += "<p style='font-size:28px;font-weight:bold;color:#0056b3;margin:12px 0;text-align:center;' id='tmaxBig'>0</p>";
html += "<button type='button' class='btn btn-go' onclick='wizNext()'>İleri</button></div>";
html += "<div id='cAdd' class='cal-step panel'><h3>4 — Eşik</h3>";
html += "<p class='hint'>Maksimum (<span id='tmax2'>0</span>) testte görülen zirvedir. <strong>+ ek</strong> ile güvenlik payı ekleyin; <strong>eşik = maks + ek</strong>. Normal çalışmada bu eşiği <strong>herhangi bir anda aşarsa</strong> boru patlak alarmı tetiklenir.</p>";
html += "<div class='input-group'><label>Tepe değere eklenecek pay (ek)</label><input type='number' id='offIn' value='0' min='0'></div>";
html += "<button type='button' class='btn btn-go' onclick='wizOffset()'>Eşiği kaydet</button></div>";
html += "<div id='cArm' class='cal-step panel'><h3>Kalibrasyon tamam</h3>";
html += "<p class='hint'>Kayıt tamam. <strong>Aktif patlama eşiği:</strong> <span id='thrDisp'>0</span> — anlık akış: <strong id='vCal'>0</strong>. Ana sayfada izleme devam eder.</p>";
html += "<p class='hint'>Kalibrasyonu baştan yapmak için <strong>Eşiği kaldır</strong> düğmesine basın; kayıtlı eşik silinir.</p>";
html += "<button type='button' class='btn' onclick='goMain()'>Ana ekrana dön</button>";
html += "<button type='button' class='btn btn-sec' onclick='clearThr()'>Eşiği kaldır (yeniden kalibre et)</button></div></div>";
html += "<div id='sAyar' class='screen'><button type='button' class='btn btn-sec' onclick='goMain()'>Ana sayfaya dön</button>";
html += "<h3 style='color:#0056b3;margin:12px 0 8px;'>Ayarlar</h3>";
html += "<p class='hint'>Pompa arızası alarmında röle, girdiğiniz süre (saniye) boyunca aktif kalır. Boru patlama: <strong>30 sn</strong> uyarı, 10 dk içinde <strong>3</strong> uyarıda ana kesilir. Tıkanma: akış <strong>0</strong>, <strong>1,5 dk</strong> sürekli → mandal.</p>";
html += "<div class='input-group'><label>Pompa arızası rölesi açık kalma süresi <span class='u'>sn</span></label><input type='number' id='relPompaInAyar' min='1' max='";
html += "<div class='input-group' style='display:flex;align-items:center;gap:10px;'><input type='checkbox' id='inv32' style='width:18px;height:18px;'><label for='inv32' style='margin:0;'>Pompa verisi yönünü ters çevir</label></div>";
html += "<button type='button' class='btn btn-go' onclick='saveRel2()'>Kaydet</button></div>";
html += "<div id='sAkim' class='screen'>";
html += "<button type='button' class='btn btn-sec' onclick='goMain()'>Ana sayfaya dön</button>";
html += "<p class='hint' style='text-align:left;margin:10px 0 14px;line-height:1.55;'><strong>Pompa kalibrasyonu:</strong> Pompa dururken boşta okuma → pompayı çalıştırıp test süresi → ortalama pompa verisi → + ek → izleme süresi (dakika).</p>";
html += "<div id='p0' class='clog-step panel clog-panel on'><h3>Güvenlik</h3>";
html += "<p class='hint' style='text-align:left;'><strong>Pompayı durdurun</strong>; sensör pompa manyetiğini görmemeli. Sonraki adımda boşta değere yakın okuma alınır.</p>";
html += "<p class='big-stop'>Lütfen pompayı durdurun.</p>";
html += "<button type='button' class='btn btn-go btn-big-ack' onclick='pumpAckStop()'>Pompayı durdurdum, devam</button></div>";
html += "<div id='p1' class='clog-step panel clog-panel'><h3>1 — Test süresi</h3>";
html += "<p class='hint' style='text-align:left;'>Pompayı normal doz gibi çalıştırın. Seçilen süre boyunca <strong>pompa verisi</strong> örneklenir ve ortalama alınır.</p>";
html += "<button type='button' class='btn-dur' onclick='pumpPickDur(30000)'>30 sn</button>";
html += "<button type='button' class='btn-dur' onclick='pumpPickDur(60000)'>1 dk</button>";
html += "<button type='button' class='btn-dur' onclick='pumpPickDur(120000)'>2 dk</button><br>";
html += "<button type='button' class='btn-dur' onclick='pumpPickDur(180000)'>3 dk</button>";
html += "<button type='button' class='btn-dur' onclick='pumpPickDur(300000)'>5 dk</button>";
html += "<button type='button' class='btn-dur' onclick='pumpPickDur(600000)'>10 dk</button></div>";
html += "<div id='p2' class='clog-step panel clog-panel'><h3>2 — Test</h3>";
html += "<p id='pumpCd' style='font-size:24px;font-weight:bold;color:#0b7a28;margin:8px 0;'>Kalan süre: —</p>";
html += "<p class='hint' style='text-align:left;'>Test bitince ortalama <strong>pompa verisi</strong> hesaplanır.</p>";
html += "<div class='row'><div class='pill' style='background:#eafff1;'><span>Ortalama pompa verisi</span><span id='pumpRunDisp'>0</span></div>";
html += "<div class='pill' style='background:#eafff1;'><span>Anlık pompa verisi</span><span id='pumpCurDisp'>0</span></div></div>";
html += "<button type='button' class='btn btn-sec' onclick='pumpCancel()'>İptal</button></div>";
html += "<div id='p3' class='clog-step panel clog-panel'><h3>3 — Ortalama</h3>";
html += "<p class='hint' style='text-align:left;'>Test tamam. Ortalama pompa verisi:</p>";
html += "<p style='font-size:28px;font-weight:bold;color:#0b7a28;text-align:center;' id='pumpAvgDisp'>0</p>";
html += "<button type='button' class='btn btn-go' onclick='pumpNext()'>İleri</button></div>";
html += "<div id='p4' class='clog-step panel clog-panel'><h3>4 — + Ek</h3>";
html += "<p class='hint' style='text-align:left;'>Ortalamaya ne kadar pay eklensin? Üst limit = ortalama + ek.</p>";
html += "<div class='input-group'><label>Ek pay</label><input type='number' id='pumpAddIn' value='0' min='0'></div>";
html += "<button type='button' class='btn btn-go' onclick='pumpAdd()'>Üst limiti kaydet</button></div>";
html += "<div id='p5' class='clog-step panel clog-panel'><h3>5 — İzleme</h3>";
html += "<p class='hint' style='text-align:left;'>Seçtiğiniz <strong>dakika</strong> boyunca cihaz pompa verisini izler. Bu süre içinde değer bir kez bile kayıtlı <strong>üst limiti geçmezse</strong> (pompa beklenen aralıkta değil) <strong>pompa arızası</strong> alarmı verilir.</p>";
html += "<button type='button' class='btn btn-dur btn-obs' onclick='pumpPickObs(1)'>1 dk</button>";
html += "<button type='button' class='btn btn-dur btn-obs' onclick='pumpPickObs(5)'>5 dk</button>";
html += "<button type='button' class='btn btn-dur btn-obs' onclick='pumpPickObs(10)'>10 dk</button>";
html += "<button type='button' class='btn btn-dur btn-obs' onclick='pumpPickObs(15)'>15 dk</button>";
html += "<button type='button' class='btn btn-dur btn-obs' onclick='pumpPickObs(20)'>20 dk</button>";
html += "<button type='button' class='btn btn-dur btn-obs' onclick='pumpPickObs(30)'>30 dk</button>";
html += "<button type='button' class='btn btn-dur btn-obs' onclick='pumpPickObs(40)'>40 dk</button>";
html += "<button type='button' class='btn btn-dur btn-obs' onclick='pumpPickObs(60)'>60 dk</button></div>";
html += "<div id='p6' class='clog-step panel clog-panel'><h3>Özet</h3>";
html += "<p class='hint' style='text-align:left;'>Kayıtlı minimum pompa verisi: <strong><span id='pumpLimDisp'>0</span></strong><br>";
html += "Arıza süresi <span class='u'>dk</span>: <strong><span id='pumpSureDisp'>1 dk</span></strong></p>";
html += "<button type='button' class='btn btn-sec' onclick='pumpClear()'>Yeniden kalibre et (sıfırla)</button>";
html += "<button type='button' class='btn btn-sec' onclick='goMain()'>Ana sayfaya dön</button></div>";
html += "<p style='font-size:10px;color:#ccc;margin-top:14px;'>AKTEK ELEKTRONİK ÇÖZÜMLERİ 2026</p></div></body></html>";
return html;
}
void setup() {
Serial.begin(115200);
pinMode(PIN_HALL_ADC, INPUT);
pinMode(PIN_FLOW, INPUT);
pinMode(PIN_RESET, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(PIN_FLOW), isrFlowPulse, RISING);
flowRateTickMs = millis();
analogReadResolution(12);
analogSetPinAttenuation(PIN_HALL_ADC, ADC_11db);
pinMode(LED_SISTEM, OUTPUT);
pinMode(ROLE_POMPA_ANA, OUTPUT);
pinMode(ROLE_AKIS_TIKANIK, OUTPUT);
pinMode(ROLE_BORU_PAT, OUTPUT);
pinMode(ROLE_POMPA_ARIZA, OUTPUT);
digitalWrite(LED_SISTEM, HIGH);
digitalWrite(ROLE_AKIS_TIKANIK, CLOG_RELAY_NORMAL);
digitalWrite(ROLE_BORU_PAT, LOW);
// Pompa arıza pini ters: arıza=LOW (kapalı), normal=HIGH (açık)
digitalWrite(ROLE_POMPA_ARIZA, HIGH);
hafiza.begin("aktek", false);
// Hall "boşta" ADC (pompa dururken AKIM:ZERO ile kalibre edin)
akimZeroAdc = hafiza.getInt("iZero", -1);
if (akimZeroAdc < 0 || akimZeroAdc > ADC_MAX) akimZeroAdc = 2048;
usePeakThreshold = hafiza.getBool("usePeak", false);
peakThreshold = hafiza.getInt("peakThr", 0);
patKalibTepe = hafiza.getInt("peakTst", 0);
invertCcaAdc = hafiza.getBool("inv32", false);
if (usePeakThreshold && peakThreshold > 0) wizardPhase = WZ_ARMED;
else wizardPhase = WZ_IDLE;
hafizaClogYukle();
hafizaRelYukle();
hafizaPompaYukle();
hafizaAnaLockYukle();
digitalWrite(ROLE_AKIS_TIKANIK, alarmClogAktif ? CLOG_RELAY_FAULT : CLOG_RELAY_NORMAL);
digitalWrite(ROLE_POMPA_ANA, anaRoleMandalli ? PUMP_CUT_LEVEL : PUMP_RUN_LEVEL);
Serial.printf("Acilis: NVS anaLock=%d nedeni=%u patMnd=%d -> GPIO%d ana=%d\n",
anaRoleMandalli ? 1 : 0, (unsigned)anaLockReason, alarmPatMandal ? 1 : 0,
ROLE_POMPA_ANA,
anaRoleMandalli ? (PUMP_CUT_LEVEL == HIGH ? 1 : 0) : (PUMP_RUN_LEVEL == HIGH ? 1 : 0));
if (anaRoleMandalli)
Serial.println("Ana role: NVS kilidi aktif (boru patlak veya akis yok).");
pompaWindowStart = millis();
hallSonYuksekMs = millis();
if (hafiza.getInt("iZero", -1) == -1) {
akimZeroAdc = hallZeroOlcAdc();
hafiza.putInt("iZero", akimZeroAdc);
}
WiFi.mode(WIFI_AP);
WiFi.softAPConfig(apIP, apIP, IPAddress(255, 255, 255, 0));
{
uint8_t mac[6];
if (esp_read_mac(mac, ESP_MAC_WIFI_STA) != ESP_OK) {
uint64_t em = ESP.getEfuseMac();
for (int i = 0; i < 6; i++)
mac[i] = (uint8_t)(em >> (8 * (5 - i)));
}
snprintf(g_apSsid, sizeof(g_apSsid), "AKTEK_KLOR_KONTROL_%02X%02X%02X",
mac[3], mac[4], mac[5]);
WiFi.softAP(g_apSsid, "aktek2026", WIFI_AP_CHANNEL, 0, WIFI_AP_MAX_CONN);
Serial.printf("AP SSID: %s kanal=%u max=%u\n", g_apSsid, (unsigned)WIFI_AP_CHANNEL,
(unsigned)WIFI_AP_MAX_CONN);
}
wifiApHealthMs = millis();
// AP iken Wi‑Fi güç tasarrufu bazen gecikme yaratır; sürekli iş yükünde kapatmak daha stabil.
esp_wifi_set_ps(WIFI_PS_NONE);
dnsServer.start(DNS_PORT, "*", apIP);
g_apHtmlCache = getHTML();
if (g_apHtmlCache.length() == 0) {
g_apHtmlCache = "<!DOCTYPE html><html><head><meta charset='UTF-8'></head><body>HTML hatasi</body></html>";
}
Serial.printf("AP HTML: %u byte (Web arayuzu)\n", (unsigned)g_apHtmlCache.length());
server.on("/generate_204", []() { server.send(204); });
server.on("/gen_204", []() { server.send(204); });
server.on("/connecttest.txt", []() { server.send(200, "text/plain", "Microsoft Connect Test"); });
server.on("/ncsi.txt", []() { server.send(200, "text/plain", "Microsoft NCSI"); });
server.on("/hotspot-detect.html", []() { server.send(200, "text/html", "<HTML><HEAD><TITLE>Success</TITLE></HEAD><BODY>Success</BODY></HTML>"); });
server.on("/canonical.html", []() { server.send(204); });
server.on("/success.txt", []() { server.send(200, "text/plain", "success"); });
server.on("/favicon.ico", []() { server.send(204); });
server.on("/update", HTTP_GET, otaUpdatePage);
server.on("/update", HTTP_POST, otaUpdateDone, otaUpdateUpload);
/** Ana sayfa: tam HTML yalnız burada. onNotFound'da aynı dev dosyayı göndermek telefon captive trafiğinde CPU'yu kilitler. */
server.on("/", HTTP_GET, []() { server.send(200, "text/html", g_apHtmlCache); });
server.onNotFound([]() {
server.send(200, "text/plain", "OK");
});
server.begin();
webSocket.begin();
webSocket.onEvent(webSocketEvent);
Serial.println("Hazir.");
}
void loop() {
pollPendingAkimZero();
pollResetButton();
updateLedsPatTik();
serviceNetworkBrief();
if (millis() - flowRateTickMs >= 1000UL) {
int delta = (int)(flowPulseCounter - flowPulseTotalLastSec);
if (delta < 0) delta = 0;
flowLastWindowDelta = delta;
flowPps = delta;
flowPulseTotalLastSec = flowPulseCounter;
flowRateTickMs = millis();
}
if (flowPulseCounter != flowPulseSnapPrev) {
lastFlowPulseMs = millis();
flowPulseSnapPrev = flowPulseCounter;
}
doseAkisGuncelle();
static unsigned long flowDozKisaTickMs = 0;
static uint32_t flowPulseDozKisaRef = 0;
static bool flowDozKisaInit = false;
if (!flowDozKisaInit) {
noInterrupts();
flowPulseDozKisaRef = flowPulseCounter;
interrupts();
flowDozKisaTickMs = millis();
flowDozKisaInit = true;
} else if (millis() - flowDozKisaTickMs >= FLOW_DOZ_PENCERE_MS) {
noInterrupts();
uint32_t c = flowPulseCounter;
interrupts();
if (c >= flowPulseDozKisaRef) {
uint32_t d = c - flowPulseDozKisaRef;
flowPpsDozKisa = (int)(d * 1000UL / FLOW_DOZ_PENCERE_MS);
} else {
flowPpsDozKisa = 0;
}
flowPulseDozKisaRef = c;
flowDozKisaTickMs = millis();
}
int hallAdc = okuHallMedian5();
int akimMa = abs(hallAdc - akimZeroAdc);
int akimAdc = hallAdc;
int akisDoz = doseAkisGoster(); // son doz toplam darbesi (ekran + tıkanma)
int cca = flowPps; // sn başına darbe (boru patlama kalibrasyonu)
int ccb = (flowPpsDozKisa > flowPps) ? flowPpsDozKisa : flowPps;
updateSessionStats(akisDoz);
updateAkimSessionStatsMa(akimMa);
const int HALL_THR = pompaAkimUstLimitMa;
bool hallRun = (HALL_THR > 0) && (akimMa > HALL_THR);
// Tıkanma yalnız pompa çalışırken (Hall) izlenir; pompa arızası öncelikli — motor durunca akış düşüşü tıkanma değildir.
const bool allowClogMonitor =
!alarmPompaAktif && (HALL_THR <= 0 || hallRun);
static unsigned long tHallWeak0 = 0;
if (hallRun) {
hallSonYuksekMs = millis();
tHallWeak0 = 0;
} else {
if (tHallWeak0 == 0)
tHallWeak0 = millis();
}
unsigned long durHallWeak = (tHallWeak0 > 0) ? (millis() - tHallWeak0) : 0;
bool flowActive = (millis() - lastFlowPulseMs) < 3500UL;
if (wizardPhase == WZ_TEST) {
if ((long)(millis() - testEndMs) >= 0) {
wizardPhase = WZ_SHOW_MAX;
} else if (cca > testPeak) {
testPeak = cca;
}
}
bool patIzleAktif = usePeakThreshold && peakThreshold > 0 && wizardPhase == WZ_ARMED;
bool patCcaUstu = patIzleAktif && (cca > peakThreshold);
if (!patIzleAktif) {
boruPatUstSayacSifirla();
} else {
if (!alarmPatAktif && patCcaUstu && !boruPatOncekiCcaUstu)
boruPatUstCikisSayaci++;
boruPatOncekiCcaUstu = patCcaUstu;
}
bool tripPat = !alarmPatAktif && !alarmPatMandal && !anaRoleMandalli && patIzleAktif &&
patCcaUstu && (boruPatUstCikisSayaci >= BORU_PAT_ARIZA_UST_CIKIS_SAYISI);
if (tripPat) {
unsigned long nowPat = millis();
patUyariKayitEkle(nowPat);
int uyariSay = patUyariPencereSay(nowPat);
alarmPatAktif = true;
alarmPatMandal = false;
boruPatUstCikisSayaci = 0;
boruPatOncekiCcaUstu = patCcaUstu;
if (uyariSay >= PAT_UYARI_MANDAL_ADET) {
patUyariModu = false;
patUyariBaslaMs = 0;
anaRoleMandalli = true;
hafizaAnaLockKaydet(LOCK_PAT);
hafiza.putBool("patMnd", false);
anaLockReason = LOCK_PAT;
anaLockSinceMs = nowPat;
kalanPatSn = 0;
patUyariKayitSifirla();
Serial.printf("Boru patlak: 10 dk icinde %d uyari — ana pompa mandal\n", uyariSay);
} else {
patUyariModu = true;
patUyariBaslaMs = nowPat;
Serial.printf("Boru patlak: uyari %d/%d (30 sn, ana pompa acik)\n", uyariSay,
PAT_UYARI_MANDAL_ADET);
}
}
if (patUyariModu && alarmPatAktif && !anaRoleMandalli) {
unsigned long heldUy = millis() - patUyariBaslaMs;
if (heldUy < PAT_UYARI_SURE_MS) {
unsigned long leftMs = PAT_UYARI_SURE_MS - heldUy;
kalanPatSn = (int)((leftMs + 999UL) / 1000UL);
} else {
patUyariModu = false;
alarmPatAktif = false;
patUyariBaslaMs = 0;
kalanPatSn = 0;
boruPatUstSayacSifirla();
Serial.println("Boru patlak: 30 sn uyari bitti");
}
} else if (anaRoleMandalli && anaLockReason == LOCK_PAT) {
alarmPatAktif = true;
bool belowPat =
patIzleAktif && (cca <= peakThreshold) && (ccb <= peakThreshold);
if (!belowPat) {
if (!alarmPatMandal) {
alarmPatMandal = true;
hafiza.putBool("patMnd", true);
}
}
kalanPatSn = 0;
} else if (!alarmPatAktif && !alarmPatMandal) {
kalanPatSn = 0;
}
// --- Akis yok / tikanma ---
if (alarmPompaAktif) clogAlarmPompaArizaTemizle();
if (alarmClogAktif && !alarmPompaAktif) {
kalanClogSn = 0;
if (!anaRoleMandalli || anaLockReason != LOCK_CLOG) {
anaRoleMandalli = true;
anaLockReason = LOCK_CLOG;
if (anaLockSinceMs == 0) anaLockSinceMs = millis();
hafizaAnaLockKaydet(LOCK_CLOG);
}
}
if (allowClogMonitor) {
if (clogArmed && clogObserveMs > 0 && !alarmClogAktif) {
// Doz toplamı > 0 olunca sayaç sıfırlanır; 1,5 dk hiç doz yok → tıkanma
if (clogAkisVar()) {
clogWindowStart = millis();
} else if ((millis() - clogWindowStart) >= clogObserveMs) {
alarmClogAktif = true;
alarmClogBasla = millis();
clogWindowStart = millis();
anaRoleMandalli = true;
hafizaAnaLockKaydet(LOCK_CLOG);
anaLockReason = LOCK_CLOG;
anaLockSinceMs = millis();
Serial.println("Tikanma: ana pompa mandal (reset 3 sn ile acilir)");
}
}
}
// Pompa çevrimi "off" iken akış sürmesi (manyetik zayıf 4–60 sn, yakında pompa görülmüştü)
static bool stuckLatch = false;
if (hallRun || !flowActive)
stuckLatch = false;
if (!alarmStuckAktif && !stuckLatch && HALL_THR > 0 && flowActive && durHallWeak > 4000UL &&
durHallWeak < 60000UL && (millis() - hallSonYuksekMs < 120000UL)) {
alarmStuckAktif = true;
stuckLatch = true;
alarmStuckBasla = millis();
// Not: alarmStuckAktif artık pompa arıza çıkışını sürmez.
// Pompa arıza pini sadece alarmPompaAktif ile kontrol edilir.
}
if (alarmStuckAktif) {
if (HALL_THR > 0 && flowActive && durHallWeak > 4000UL && durHallWeak < 60000UL && (millis() - hallSonYuksekMs < 120000UL))
alarmStuckBasla = millis();
unsigned long g = millis() - alarmStuckBasla;
unsigned long ms = (unsigned long)relPompaSaniye * 1000UL;
if (g >= ms) {
alarmStuckAktif = false;
kalanStuckSn = 0;
} else {
kalanStuckSn = (int)((ms - g) / 1000UL);
}
}
// --- Pompa arızası (Hall uzun süre eşik üstüne çıkmıyor) ---
if (!alarmPompaAktif && pompaAkimUstLimitMa > 0 && pompaArizaSureDk > 0) {
if (akimMa > pompaAkimUstLimitMa) {
pompaWindowStart = millis();
} else {
unsigned long winMs = (unsigned long)pompaArizaSureDk * 60UL * 1000UL;
if ((millis() - pompaWindowStart) >= winMs) {
alarmPompaAktif = true;
alarmPompaBasla = millis();
clogAlarmPompaArizaTemizle();
digitalWrite(ROLE_POMPA_ARIZA, LOW);
pompaWindowStart = millis();
Serial.println("Pompa arizasi: tikanma/mandal yok sayildi");
}
}
}
if (alarmPompaAktif && pompaAkimUstLimitMa > 0 && akimMa > pompaAkimUstLimitMa) {
alarmPompaAktif = false;
kalanPompaSn = 0;
if (!alarmStuckAktif)
digitalWrite(ROLE_POMPA_ARIZA, HIGH);
pompaWindowStart = millis();
}
if (alarmPompaAktif) {
if (pompaAkimUstLimitMa > 0 && akimMa <= pompaAkimUstLimitMa)
alarmPompaBasla = millis();
unsigned long g = millis() - alarmPompaBasla;
unsigned long ms = (unsigned long)relPompaSaniye * 1000UL;
if (g >= ms) {
alarmPompaAktif = false;
kalanPompaSn = 0;
if (!alarmStuckAktif)
digitalWrite(ROLE_POMPA_ARIZA, HIGH);
} else {
kalanPompaSn = (int)((ms - g) / 1000UL);
}
}
// Ana hat + boru patlak + tıkanma rölesi: tek yerden sür (her karede tutarlı)
// GPIO19 ana: mandal=HIGH pompayı keser (PUMP_CUT_LEVEL), mandal yok=LOW çalıştırır (PUMP_RUN_LEVEL)
// GPIO22 boru patlak: uyari veya mandal → HIGH
// GPIO14 tıkanma ters: arıza=LOW, normal=HIGH
digitalWrite(ROLE_AKIS_TIKANIK, alarmClogAktif ? CLOG_RELAY_FAULT : CLOG_RELAY_NORMAL);
digitalWrite(ROLE_BORU_PAT, (alarmPatAktif || alarmPatMandal) ? HIGH : LOW);
digitalWrite(ROLE_POMPA_ANA, anaRoleMandalli ? PUMP_CUT_LEVEL : PUMP_RUN_LEVEL);
if (pumpPh == PZ_TEST) {
if ((long)(millis() - pumpTestEndMs) >= 0) {
if (pumpSampleCount > 0)
pumpAvgAbsMa = (int)lroundf((float)pumpSumAbsMa / (float)pumpSampleCount);
else
pumpAvgAbsMa = 0;
pumpRunAvgAbsMa = pumpAvgAbsMa;
pumpPh = PZ_SHOW_AVG;
} else {
unsigned long now = millis();
if (pumpLastSampleMs == 0 || (now - pumpLastSampleMs) >= 120UL) {
pumpLastSampleMs = now;
pumpSumAbsMa += (long)akimMa;
pumpSampleCount++;
pumpRunAvgAbsMa =
(pumpSampleCount > 0) ? (int)lroundf((float)pumpSumAbsMa / (float)pumpSampleCount) : 0;
}
}
}
pumpCurAbsMa = akimMa;
static unsigned long serialDbgMs = 0;
if (millis() - serialDbgMs >= SERIAL_DBG_INTERVAL_MS) {
serialDbgMs = millis();
const int pencKlnSn = alarmClogAktif ? -1 : clogAnaKalanSn();
const unsigned int altStreakS =
alarmClogAktif
? 0U
: ((clogArmed && !clogAkisVar())
? (unsigned int)((millis() - clogWindowStart) / 1000UL)
: 0U);
const unsigned alm = (unsigned)(alarmPatAktif ? 2 : 0) | (unsigned)(alarmClogAktif ? 4 : 0) |
(unsigned)(alarmPompaAktif ? 16 : 0) | (unsigned)(alarmStuckAktif ? 32 : 0);
const int clogUstDisp =
clogArmed ? (clogAkisVar() ? 1 : 0) : -1;
const int clogThrDbg = clogArmed ? clogThreshold : -1;
const int pinPat = alarmPatAktif ? 1 : 0;
const int pinAna = anaRoleMandalli ? (PUMP_CUT_LEVEL == HIGH ? 1 : 0) : (PUMP_RUN_LEVEL == HIGH ? 1 : 0);
Serial.printf(
"DBG doz=%d pps=%d heap=%u ws=%u | alm=%u lock=%d patAl=%d GPIO%d=%d GPIO%d=%d\n",
akisDoz, cca, (unsigned)ESP.getFreeHeap(), (unsigned)webSocket.connectedClients(), alm,
anaRoleMandalli ? 1 : 0, alarmPatAktif ? 1 : 0, ROLE_POMPA_ANA, pinAna, ROLE_BORU_PAT, pinPat);
}
if (millis() - lastUpdate >= 150 && webSocket.connectedClients() > 0) {
uint8_t ph = (uint8_t)wizardPhase;
int tleft = testKalanSaniye();
int thrSend = (usePeakThreshold && wizardPhase == WZ_ARMED) ? peakThreshold : 0;
int tPeakSend = (wizardPhase == WZ_ARMED) ? patKalibTepe : testPeak;
int ctp = clogTestKalanSn();
int cthr = clogArmed ? clogThreshold : 0;
int obs = clogArmed ? clogObsMin : 0;
int clogAkSn = clogAnaKalanSn();
uint8_t clogAkMd = clogAnaMod();
char wsBuf[580];
int n = snprintf(wsBuf, sizeof(wsBuf),
"%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d",
akisDoz, alarmPatAktif ? 1 : 0, kalanPatSn, sessMin, sessMax, (int)ph, tleft, tPeakSend, thrSend,
ccb, (int)clogPh, ctp, clogTestPeak, cthr, obs, alarmClogAktif ? 1 : 0, kalanClogSn,
clogAkSn, (int)clogAkMd, akimMa, akimSessMinMa, akimSessMaxMa, akimAdc,
akimZeroAdc, alarmPompaAktif ? 1 : 0, kalanPompaSn, pompaAkimUstLimitMa, pompaArizaSureDk,
relPompaSaniye, (int)pumpPh, pumpTestKalanSn(), pumpAvgAbsMa, pumpRunAvgAbsMa, pumpCurAbsMa,
invertCcaAdc ? 1 : 0, anaRoleMandalli ? 1 : 0, (int)anaLockReason);
if (n > 0 && n < (int)sizeof(wsBuf)) {
webSocket.broadcastTXT((uint8_t *)wsBuf, (size_t)n);
}
lastUpdate = millis();
}
}
Editor is loading...
Leave a Comment