logo elektroda
logo elektroda
X
logo elektroda

[Solved] BK7238 (T1) – “Getting bus failed” following a memory wipe error. Is the chip in a soft-brick state?

LinearBridge1 384 2
ADVERTISEMENT
This content has been translated flag-pl » flag-en View the original version here
  • Soft-brick after erase failure during OpenBeken flash

    #1 21936203
    LinearBridge1
    Level 2  
    Posts: 3
    Hello everyone,

    I’m having a persistent problem when trying to upload OpenBeken onto a Tuya smart prepayment switch equipped with the BK7238 (T1) chip. I’ve spent many hours on this and tried almost every diagnostic method available, but I’ve completely hit a dead end. I would be very grateful for any advice from the experts on this forum.

    Below is a detailed description of my hardware, the nature of the problem and all the steps I have taken so far.

    1. Hardware and configuration

    Target device: Tuya smart prepaid switch (based on TuyaMCU, 9600 baud transmission speed).

    Wi-Fi module: BK7238 (internal T1 platform).

    Power supply: External laboratory power supply set to a stable 3.3 V (capable of delivering high current), monitored using an ammeter.

    UART adapter: Initially an FT232RL (which later failed the loopback test); I am currently using an Arduino Uno as a USB-TTL adapter in pass-through mode (the RESET pin connected to GND).

    Connections: The common ground (GND) is 100% correctly connected between the laboratory power supply, the circuit and the UART adapter.

    2. Timeline (how the problem began)

    Phase 1 (Complete success):
    I successfully connected to the chip and read the original firmware using the BK7231 GUI Flash Tool. The backup was created correctly, and the tool read the Tuya configuration without any issues (Platform: T1, Baud: 9600, presence of TuyaMCU confirmed).

    Phase 2 (Failure):
    I clicked the flash button to upload the OpenBeken firmware. The process began but ended in an error precisely during the first stage of memory erasure, with the following message:

    
    failed, will retry! Erasing block 0x00...failed with serial.BytesToRead 0 (expected 16)
    


    Phase 3 (Current status):
    Since this error occurred during memory erasure, the chip appears to be in a state of partial software failure (‘soft brick’). Every time I try to reprogram it, I get an endless loop of errors:

    
    Getting bus failed, will try again - 0/100!
    ...
    Getting bus failed, will try again - 99/100!
    Failed to get bus!
    


    3. Diagnostic steps already carried out (over 100 attempts!)

    To save you time, below are all the tests I have already carried out:

    Power cycle synchronisation:
    I left the 3.3 V cable disconnected, clicked the ‘Flash’ button in the GUI, and precisely at the moment the message appeared:

    
    Getting bus... 0/100
    


    I connected the 3.3 V cable to produce a sharp rising edge on the power supply. There was no response.

    Resetting the CEN pin:
    I left the power on, started the flashing process and, for about 0.5 seconds, shorted the CEN pin to GND during the “Getting bus” loop. I also tried the ‘Woodpecker’ method (rapid, repeated shorting of the pin to GND). To no avail.

    Diagnostics using an ammeter:
    The chip draws a normal current in the range of 30 mA to 80 mA during operation, which indicates that it has not been physically damaged. When I short-circuit the CEN pin to GND, the current draw drops to around 0 mA, which confirms that the internal reset circuit is functioning correctly.

    Baud rates:
    I tried changing the programmer’s baud rate to 115200 baud, and then setting it back to 921600 baud.

    Changing the UART adapter:
    After the loopback test on my FTDI adapter failed, I switched to using an Arduino Uno (RESET connected to GND) as a serial bridge. (Note: in this configuration, I connected TX to TX and RX to RX as required for this workaround).

    Hard Recovery:
    Is there a specific ‘hard recovery’ method or an alternative tool (such as hid_download_py or Beken Writer 1.60) for the BK7238 chip, which failed precisely during the following operation:

    
    Erasing block 0x00
    


    I have a safely stored backup of the original firmware, but I simply need to re-establish communication with the bus.

    Thank you in advance for your time, and for any help and expert advice!


    BK7238 (T1) – “Getting bus failed” following a memory wipe error. Is the chip in a soft-brick state?
    BK7238 (T1) – “Getting bus failed” following a memory wipe error. Is the chip in a soft-brick state?
    BK7238 (T1) – “Getting bus failed” following a memory wipe error. Is the chip in a soft-brick state?
    BK7238 (T1) – “Getting bus failed” following a memory wipe error. Is the chip in a soft-brick state? BK7238 (T1) – “Getting bus failed” following a memory wipe error. Is the chip in a soft-brick state? BK7238 (T1) – “Getting bus failed” following a memory wipe error. Is the chip in a soft-brick state?
    Helpful post? Buy me a coffee.
  • ADVERTISEMENT
  • ESP-WROOM-02 bridge replaces burnt BK7238 module

    #3 21941004
    LinearBridge1
    Level 2  
    Posts: 3
    Wiring diagram of ESP‑WROOM‑02 with a USB‑TTL adapter and a Tuya MCU board, using colored jumper wires. Blue PCB in a white casing with soldered wires and a strip of clear tape on top Screenshot of an Arabic energy dashboard showing 219.7 V, 0.00 A, 50.0 Hz, and an energy consumption chart.  

    Rescuing a Smart Breaker: Upgrading from a burnt-out BK7238 to an ESP-WROOM-02
    Hardware failures happen, but they don’t have to spell the end of a device’s life. Recently, a smart circuit breaker suffered an electrical short circuit that completely fried its internal Wi-Fi module, a BK7238 (T1) chip.

    Instead of throwing the circuit breaker away, we solved the problem by replacing the burnt-out chip with a standard ESP-WROOM-02 (ESP8266) module. We hardwired it to the circuit breaker’s main microcontroller (MCU) and wrote custom C++ firmware to act as a bridge between the local hardware and a custom cloud server.

    Here is a breakdown of how this system was built, the core architecture of the custom firmware, and how it handles real-time bidirectional communication.

    System Architecture and Code Structure
    The firmware is designed to be lightweight, non-blocking and highly resilient. It is divided into four main logical components:

    1. Configuration (Settings & Credentials)
    The first file acts as the blueprint for the system. It stores all the crucial environment variables:

    • Network Credentials: Wi-Fi SSID and passwords.
    • WebSocket Server Details: Hostname, port, and specific paths required to connect to the custom Laravel Echo/Pusher backend.
    • Hardware timings: Reconnection delays, baud rates (9600 for Tuya serial), and timeout limits to keep the system stable.
    2. The Tuya MCU Protocol Bridge (Header & Implementation)
    The original BK7238 chip communicated with the circuit breaker’s main processor using the standard Tuya MCU Serial Protocol . Because we swapped the chip, our ESP-WROOM-02 had to ‘speak’ this exact language.

    • Packet Parsing: The code listens to the serial stream byte by byte without blocking the main thread. It looks for the Tuya frame headers (
      0x55 0xAA
      ), validates the payload length, and calculates the checksum to ensure data integrity.
    • Data Points (DPs): It decodes incoming telemetry from the circuit breaker—such as voltage, current, power and switch status—and maps them to specific Data Point IDs (e.g., DP 16 for the switch, DP 6 for power metrics).
    • Command Execution: It packages commands (such as switching the relay on or off) into properly formatted serial packets and sends them back to the MCU.
    3. The Main Logic: Wi-Fi, Web Server, and WebSockets
    The final piece of the puzzle is the main application file, which connects the local serial data to the global internet.

    • Local Web Dashboard: The ESP hosts a lightweight web server on Port 80. If you navigate to the ESP’s local IP address, you’ll see a clean HTML dashboard displaying real-time network status, MCU connection health and a live event log.
    • Offline RAM Buffering: If the internet connection is lost, the system does not lose any data. It captures the circuit breaker’s state (voltage, power, etc.) and saves it to a ring buffer in RAM. When the WebSocket connection is restored, it gradually drains this buffer and sends the backlog to the server.
    How Real-Time Communication Works
    The most critical feature of this project is the real-time, low-latency communication between the physical circuit breaker and the cloud server.

    1. Pushing Telemetry (Device to Cloud) Whenever the ESP receives an updated data point from the MCU (for example, a change in power consumption), it triggers the
    pushStateToServer
    function.

    • The system formats all the current electrical metrics into a structured JSON payload.
    • It sends this JSON payload over an active WebSocket connection using the Pusher protocol format, targeting a specific channel (
      client-device.updated
      ).
    2. Receiving Commands (Cloud to Device) The ESP maintains a persistent WebSocket connection and subscribes to a specific channel. When a user clicks a button on the web or mobile app, the server pushes a JSON event down the socket.

    Get the Full Project
    This firmware completely liberates the smart circuit breaker from the proprietary Tuya ecosystem, routing all data securely to a custom, self-hosted backend.

    The custom backend managing this real-time WebSocket connection, device states and user interactions is built using Laravel .

    If you are interested in the complete Laravel backend code used to power this project, or if you require custom programming and IoT solutions, please contact me directly at: https://www.barmgely.com/

    
    config.h
    
    #pragma once
    
    // ============================================================
    //  إعدادات عامة - عدّلها حسب مشروعك
    // ============================================================
    
    #define BREAKER_ID          "0000"
    
    // عنوان السيرفر والمنفذ والمسار بناءً على الرابط الجديد
    #define WS_HOST              "0000.barmgely.com"
    #define WS_PORT              80
    #define WS_PATH              "/app/0000?protocol=7&client=js&version=8.4.0&flash=false"
    #define WS_USE_SSL           false
    
    // إعادة محاولة الاتصال بالويب سوكيت كل كم مللي ثانية عند الانقطاع
    #define WS_RECONNECT_MS      3000
    
    // اسم جهاز الـ OTA وكلمة المرور
    #define OTA_HOSTNAME         "breaker-0000"
    #define OTA_PASSWORD         "******"
    
    #define WM_AP_NAME           "Breaker-2212-Setup"
    #define WM_AP_PASSWORD       "******"          // 8 أحرف على الأقل
    #define WM_CONFIG_TIMEOUT_S  180                 // مهلة صفحة الإعداد بالثواني
    
    #define TUYA_BAUDRATE        9600
    #define STATE_PUSH_MIN_INTERVAL_MS   250
    #define POST_COMMAND_VERIFY_DELAY_MS 400
    #define MCU_TIMEOUT_MS       15000
    
    #define WIFI_SSID "Barmgely ADSL"
    #define WIFI_PASSWORD "*******"
    
    
    
    TuyaMCU.cpp
    #include "TuyaMCU.h"
    
    TuyaMCUClient::TuyaMCUClient(Stream& stream) : _stream(stream) {}
    
    void TuyaMCUClient::begin(DPCallback cb) {
    _onDP = cb;
    _state = ParseState::WAIT_55;
    }
    
    uint8_t TuyaMCUClient::_calcChecksum(const uint8_t* buf, uint16_t len) const {
    uint32_t sum = 0;
    for (uint16_t i = 0; i < len; i++) sum += buf[i];
    return (uint8_t)(sum & 0xFF);
    }
    
    bool TuyaMCUClient::sendPacket(uint8_t command, const uint8_t* payload, uint16_t len) {
    // رأس الإطار + الطول (بدون الـ checksum بعد) لحساب المجموع بشكل تدريجي
    uint8_t header[6] = {
    0x55, 0xAA, 0x00, command,
    (uint8_t)((len >> 8) & 0xFF), (uint8_t)(len & 0xFF)
    };
    
    uint32_t sum = 0;
    for (uint8_t i = 0; i < 6; i++) sum += header[i];
    for (uint16_t i = 0; i < len; i++) sum += payload[i];
    uint8_t checksum = (uint8_t)(sum & 0xFF);
    
    // نكتب مباشرة على الـ Stream دون تجميع بافر كبير - يوفر ذاكرة على شريحة ضعيفة
    _stream.write(header, 6);
    if (len > 0 && payload != nullptr) {
    _stream.write(payload, len);
    }
    _stream.write(&checksum, 1);
    _stream.flush();
    return true;
    }
    
    bool TuyaMCUClient::sendDPBool(uint8_t dpId, bool value) {
    uint8_t val = value ? 1 : 0;
    uint8_t payload[5] = { dpId, TuyaDPType::BOOLEAN, 0x00, 0x01, val };
    return sendPacket(TuyaCommand::COMMAND, payload, 5);
    }
    
    bool TuyaMCUClient::sendDPEnum(uint8_t dpId, uint8_t value) {
    uint8_t payload[5] = { dpId, TuyaDPType::ENUM, 0x00, 0x01, value };
    return sendPacket(TuyaCommand::COMMAND, payload, 5);
    }
    
    bool TuyaMCUClient::sendDPValue(uint8_t dpId, int32_t value) {
    uint32_t uv = (uint32_t)value;
    uint8_t payload[8] = {
    dpId, TuyaDPType::VALUE, 0x00, 0x04,
    (uint8_t)((uv >> 24) & 0xFF), (uint8_t)((uv >> 16) & 0xFF),
    (uint8_t)((uv >> 8) & 0xFF),  (uint8_t)(uv & 0xFF)
    };
    return sendPacket(TuyaCommand::COMMAND, payload, 8);
    }
    
    bool TuyaMCUClient::sendDPRaw(uint8_t dpId, const uint8_t* data, uint16_t len) {
    uint8_t payload[4 + 32]; // حد أعلى معقول لبيانات RAW
    if (len > 32) return false;
    payload[0] = dpId;
    payload[1] = TuyaDPType::RAW;
    payload[2] = (uint8_t)((len >> 8) & 0xFF);
    payload[3] = (uint8_t)(len & 0xFF);
    memcpy(&payload[4], data, len);
    return sendPacket(TuyaCommand::COMMAND, payload, 4 + len);
    }
    
    bool TuyaMCUClient::queryAllStates() {
    return sendPacket(TuyaCommand::QUERY_STATUS);
    }
    
    void TuyaMCUClient::update() {
    // نقرأ كل ما هو متاح في هذا الاستدعاء فقط - غير محظورة أبدًا
    while (_stream.available() > 0) {
    int b = _stream.read();
    if (b < 0) break;
    _handleByte((uint8_t)b);
    }
    }
    
    void TuyaMCUClient::_handleByte(uint8_t b) {
    switch (_state) {
    case ParseState::WAIT_55:
    if (b == 0x55) _state = ParseState::WAIT_AA;
    break;
    case ParseState::WAIT_AA:
    _state = (b == 0xAA) ? ParseState::VERSION : ParseState::WAIT_55;
    break;
    case ParseState::VERSION:
    _version = b;
    _state = ParseState::COMMAND;
    break;
    case ParseState::COMMAND:
    _command = b;
    _state = ParseState::LEN_HI;
    break;
    case ParseState::LEN_HI:
    _payloadLen = ((uint16_t)b) << 8;
    _state = ParseState::LEN_LO;
    break;
    case ParseState::LEN_LO:
    _payloadLen |= b;
    _payloadIdx = 0;
    if (_payloadLen > MAX_PAYLOAD) {
    // إطار أكبر من المتوقع - نتجاهله لحماية الذاكرة المحدودة
    _state = ParseState::WAIT_55;
    } else {
    _state = (_payloadLen == 0) ? ParseState::CHECKSUM : ParseState::PAYLOAD;
    }
    break;
    case ParseState::PAYLOAD:
    _payload[_payloadIdx++] = b;
    if (_payloadIdx >= _payloadLen) _state = ParseState::CHECKSUM;
    break;
    case ParseState::CHECKSUM: {
    uint8_t header[6] = {
    0x55, 0xAA, _version, _command,
    (uint8_t)((_payloadLen >> 8) & 0xFF), (uint8_t)(_payloadLen & 0xFF)
    };
    uint32_t sum = 0;
    for (uint8_t i = 0; i < 6; i++) sum += header[i];
    for (uint16_t i = 0; i < _payloadLen; i++) sum += _payload[i];
    uint8_t expected = (uint8_t)(sum & 0xFF);
    
    if (expected == b) {
    _processValidPacket(_command, _payload, _payloadLen);
    }
    _state = ParseState::WAIT_55;
    break;
    }
    }
    }
    
    void TuyaMCUClient::_processValidPacket(uint8_t command, const uint8_t* payload, uint16_t len) {
    if (command == TuyaCommand::HEARTBEAT) {
    _lastHeartbeatMs = millis();
    uint8_t ack = 0x01;
    sendPacket(TuyaCommand::HEARTBEAT, &ack, 1);
    
    if (!_initDone) {
    uint8_t wifiOk = 0x04; // متصل بالإنترنت + الكلاود
    sendPacket(TuyaCommand::WIFI_STATE, &wifiOk, 1);
    queryAllStates();
    _initDone = true;
    }
    return;
    }
    
    if (command == TuyaCommand::STATUS_REPORT || command == TuyaCommand::COMMAND) {
    uint16_t index = 0;
    while (index + 4 <= len) {
    uint8_t dpId   = payload[index];
    uint8_t dpType = payload[index + 1];
    uint16_t dpLen = (((uint16_t)payload[index + 2]) << 8) | payload[index + 3];
    
    if (index + 4 + dpLen > len) break; // إطار غير مكتمل - حماية من تجاوز الحدود
    
    const uint8_t* dpData = &payload[index + 4];
    if (_onDP) _onDP(dpId, dpType, dpData, (uint8_t)dpLen);
    
    index += 4 + dpLen;
    }
    }
    }
    
    
    
    TuyaMCU.h
    #pragma once
    #include
    #include
    
    // ============================================================
    //  بروتوكول Tuya MCU (منفّذ كما في الكود الأصلي بايثون)
    //  إطار: 55 AA ver cmd len_hi len_lo [payload...] checksum
    // ============================================================
    
    namespace TuyaCommand {
    constexpr uint8_t HEARTBEAT      = 0x00;
    constexpr uint8_t PRODUCT_INFO   = 0x01;
    constexpr uint8_t WORK_MODE      = 0x02;
    constexpr uint8_t WIFI_STATE     = 0x03;
    constexpr uint8_t RESET_WIFI     = 0x04;
    constexpr uint8_t COMMAND        = 0x06;
    constexpr uint8_t STATUS_REPORT  = 0x07;
    constexpr uint8_t QUERY_STATUS   = 0x08;
    }
    
    namespace TuyaDPType {
    constexpr uint8_t RAW     = 0x00;
    constexpr uint8_t BOOLEAN = 0x01;
    constexpr uint8_t VALUE   = 0x02;
    constexpr uint8_t STRING  = 0x03;
    constexpr uint8_t ENUM    = 0x04;
    constexpr uint8_t BITMAP  = 0x05;
    }
    
    // استدعاء عند استقبال DP صالح: (dp_id, dp_type, data, len)
    using DPCallback = std::function;
    
    class TuyaMCUClient {
    public:
    // stream: عادةً &Serial (UART0) المتصل فعليًا بأطراف TX/RX الخاصة بـ MCU
    explicit TuyaMCUClient(Stream& stream);
    
    void begin(DPCallback cb);
    
    // يجب استدعاؤها في كل loop() - غير محظورة (non-blocking) بالكامل
    void update();
    
    // إرسال إطار خام
    bool sendPacket(uint8_t command, const uint8_t* payload = nullptr, uint16_t len = 0);
    
    // إرسال أمر DP (Boolean / Enum = بايت واحد، Value = 4 بايت signed)
    bool sendDPBool(uint8_t dpId, bool value);
    bool sendDPEnum(uint8_t dpId, uint8_t value);
    bool sendDPValue(uint8_t dpId, int32_t value);
    bool sendDPRaw(uint8_t dpId, const uint8_t* data, uint16_t len);
    
    bool queryAllStates();
    
    // آخر مرة وصل فيها Heartbeat من الـ MCU (لمعرفة إن كان متصلًا)
    uint32_t lastHeartbeatMs() const { return _lastHeartbeatMs; }
    
    private:
    Stream& _stream;
    DPCallback _onDP;
    bool _initDone = false;
    uint32_t _lastHeartbeatMs = 0;
    
    // حالة الآلة التي تقرأ البايتات الواردة بايت-بايت بدون توقف التنفيذ
    enum class ParseState : uint8_t {
    WAIT_55, WAIT_AA, VERSION, COMMAND, LEN_HI, LEN_LO, PAYLOAD, CHECKSUM
    };
    ParseState _state = ParseState::WAIT_55;
    uint8_t  _version = 0;
    uint8_t  _command = 0;
    uint16_t _payloadLen = 0;
    uint16_t _payloadIdx = 0;
    static constexpr uint16_t MAX_PAYLOAD = 128; // كافٍ جدًا لإطارات Tuya القياسية
    uint8_t  _payload[MAX_PAYLOAD];
    
    uint8_t _calcChecksum(const uint8_t* buf, uint16_t len) const;
    void _handleByte(uint8_t b);
    void _processValidPacket(uint8_t command, const uint8_t* payload, uint16_t len);
    };
    
    
    
    esp-wroom.ino
    
    // ============================================================
    //  ESP-WROOM-02 (ESP8266) <-> Tuya MCU Bridge
    //  + Offline Buffering (RAM ONLY - Flash/LittleFS removed) for WebSocket outages
    //  + Command execution ACK
    //  + Periodic channel re-subscribe (Laravel Echo / Pusher)
    // ============================================================
    
    #include
    #include
    #include   // مكتبة سيرفر الويب المضافة
    #include
    #include   // Links2004/WebSockets
    #include
    #include   // مكتبة الوقت المدمجة (لا تحتاج تثبيت)
    
    #include "config.h"
    #include "TuyaMCU.h"
    
    // ============================================================
    //  نظام تسجيل الأحداث لعرضه في صفحة الويب (Logs System)
    // ============================================================
    #define MAX_LOG_LINES 20
    String webLogs[MAX_LOG_LINES];
    int logIndex = 0;
    
    #define DBG Serial1
    
    void addLog(const char* format, ...) {
    char buffer[256];
    va_list args;
    va_start(args, format);
    vsnprintf(buffer, sizeof(buffer), format, args);
    va_end(args);
    
    // طباعة عبر المنفذ التسلسلي (Serial1) كالمعتاد
    DBG.println(buffer);
    
    // حفظ السجل في المصفوفة لعرضه في صفحة الويب
    webLogs[logIndex] = String(buffer);
    logIndex = (logIndex + 1) % MAX_LOG_LINES;
    }
    
    // استبدال دالة LOGI القديمة لتستخدم نظام التسجيل الجديد
    #define LOGI(...) addLog(__VA_ARGS__)
    
    // ============================================================
    //  إعدادات المخزن الاحتياطي (Offline Buffer) — RAM فقط
    // ============================================================
    // - يُستخدم فقط عند فقدان اتصال WebSocket (انقطاع نت/واي فاي).
    // - تم إلغاء الاعتماد على أي تخزين دائم (Flash/LittleFS) بالكامل بناءً على
    //   طلبك؛ المخزن الآن حلقي (Ring Buffer) في الذاكرة (RAM) فقط بسقف
    //   MAX_OFFLINE_RECORDS سجل، وبالتالي يُفقد محتواه عند انقطاع الكهرباء
    //   أو إعادة تشغيل الجهاز (مقايضة طبيعية مقابل عدم الكتابة على الفلاش).
    // - يُسجَّل سجل واحد كل فترة (OFFLINE_SAVE_INTERVAL_MS) طالما الاتصال منقطع.
    #define MAX_OFFLINE_RECORDS 20
    #define OFFLINE_SAVE_INTERVAL_MS (30UL * 60UL * 1000UL)  //للإنتاج = كل نصف ساعة
    #define OFFLINE_SEND_DELAY_MS 400UL                      // فاصل بسيط بين إرسال كل سجل مخزّن عند العودة
    
    // ============================================================
    //  إعدادات سرعة إرسال البيانات (Rate Limiting)
    // ============================================================
    #define TELEMETRY_INTERVAL_MS (60UL * 1000UL)
    bool commandResponsePending = false; // هل يوجد استجابة أمر ننتظر إرسالها؟
    uint32_t commandResponseTime = 0;    // وقت إرسال الاستجابة
    
    uint32_t lastOfflineSaveMs = 0;
    
    bool offlineDraining = false;
    uint32_t lastOfflineSendMs = 0;
    
    // سجل احتياطي واحد
    struct OfflineRecordData {
    char reason[20];
    float voltage, current, power, balance, lastChargeAmount, totalEnergy;
    float supplyFrequency, powerFactor, leakageCurrent, tempZone1;
    int fault, relayStatus;
    bool switchStatus, prepayStatus;
    uint32_t uptimeMs;
    char timestamp[25];
    };
    // إضافة هذه التعريفات تمنع الأردوينو من إنشائها تلقائياً قبل تعريف الـ struct
    OfflineRecordData captureOfflineRecord(const char* reason);
    void saveOfflineRecordToRam(const OfflineRecordData& rec);
    void sendOfflineTelemetry(const OfflineRecordData& rec);
    
    // مخزن الـ RAM الاحتياطي (حلقي/Ring Buffer) — المصدر الوحيد الآن
    OfflineRecordData ramOfflineBuf[MAX_OFFLINE_RECORDS];
    uint8_t ramOfflineCount = 0;  // عدد السجلات المشغولة حالياً (0..MAX_OFFLINE_RECORDS)
    uint8_t ramOfflineHead = 0;   // فهرس أقدم سجل
    
    // ============================================================
    //  إعدادات إعادة الاشتراك الدوري بالقناة (لضمان استقبال Laravel Echo/Pusher)
    // ============================================================
    #define CHANNEL_RESUBSCRIBE_INTERVAL_MS (10UL * 60UL * 1000UL)  // كل 10 دقائق أثناء الاتصال
    uint32_t lastSubscribeMs = 0;
    
    // ============================================================
    //  الكائنات الرئيسية
    // ============================================================
    TuyaMCUClient tuya(Serial);
    WebSocketsClient webSocket;
    ESP8266WebServer server(80);  // سيرفر الويب على المنفذ 80
    
    bool wsConnected = false;
    uint32_t lastStatePush = 0;
    bool stateDirty = false;
    uint32_t pendingVerifyAt = 0;
    
    // آخر وقت وصل فيه أي DP فعلي من شريحة Tuya (بغض النظر عن heartbeat الداخلي في المكتبة)
    uint32_t lastDpReceivedMs = 0;
    
    // تعريف مسبق (forward declaration) — الدالة نفسها معرّفة لاحقاً في الملف،
    // لكن serviceOfflineDrain() (المعرّفة قبلها) تحتاج تستدعيها.
    void pushStateToServer(const char* reason = "update");
    
    // ============================================================
    //  حالة الجهاز
    // ============================================================
    struct DeviceState {
    float voltage = 0;
    float current = 0;
    float power = 0;
    float balance = 0;
    float lastChargeAmount = 0;
    float totalEnergy = 0;
    float supplyFrequency = 0;
    float powerFactor = 0;
    int fault = 0;
    float leakageCurrent = 0;
    bool switchStatus = false;
    bool prepayStatus = false;
    float tempZone1 = 0;
    int relayStatus = 0;
    } state;
    
    // ============================================================
    //  دالة جلب الوقت الحالي المنسق
    // ============================================================
    String getCurrentTime() {
    time_t now = time(nullptr);
    // إذا كانت القيمة أقل من 100000، يعني أن الشريحة لم تتزامن مع خادم الإنترنت بعد
    if (now < 100000) return "Syncing...";
    
    struct tm timeinfo;
    localtime_r(&now, &timeinfo);
    
    char buf[25];
    // التنسيق: YYYY-MM-DD HH:MM:SS
    snprintf(buf, sizeof(buf), "%04d-%02d-%02d %02d:%02d:%02d",
    timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday,
    timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
    
    return String(buf);
    }
    
    // ============================================================
    //  آخر نشاط فعلي من شريحة Tuya (heartbeat داخلي من المكتبة OR أي DP وصل فعلياً)
    // ============================================================
    uint32_t mcuLastActivityMs() {
    uint32_t hb = tuya.lastHeartbeatMs();
    return (lastDpReceivedMs > hb) ? lastDpReceivedMs : hb;
    }
    
    bool mcuIsResponding() {
    uint32_t last = mcuLastActivityMs();
    return (last > 0) && ((millis() - last) < 15000);
    }
    
    // ============================================================
    //  إدارة المخزن الاحتياطي (RAM فقط)
    // ============================================================
    
    // عدد السجلات المخزّنة حالياً في الذاكرة
    int offlineBufferedCount() {
    return ramOfflineCount;
    }
    
    // التقاط لقطة من حالة الجهاز الحالية في هيكل موحّد
    OfflineRecordData captureOfflineRecord(const char* reason) {
    OfflineRecordData r;
    strncpy(r.reason, reason, sizeof(r.reason) - 1);
    r.reason[sizeof(r.reason) - 1] = 0;
    
    r.voltage = state.voltage;
    r.current = state.current;
    r.power = state.power;
    r.balance = state.balance;
    r.lastChargeAmount = state.lastChargeAmount;
    r.totalEnergy = state.totalEnergy;
    r.supplyFrequency = state.supplyFrequency;
    r.powerFactor = state.powerFactor;
    r.fault = state.fault;
    r.leakageCurrent = state.leakageCurrent;
    r.switchStatus = state.switchStatus;
    r.prepayStatus = state.prepayStatus;
    r.tempZone1 = state.tempZone1;
    r.relayStatus = state.relayStatus;
    r.uptimeMs = millis();
    
    String ts = getCurrentTime();
    strncpy(r.timestamp, ts.c_str(), sizeof(r.timestamp) - 1);
    r.timestamp[sizeof(r.timestamp) - 1] = 0;
    
    return r;
    }
    
    // حفظ سجل في حلقة RAM — يُستبدل الأقدم تلقائياً عند الامتلاء
    void saveOfflineRecordToRam(const OfflineRecordData& rec) {
    if (ramOfflineCount >= MAX_OFFLINE_RECORDS) {
    ramOfflineHead = (ramOfflineHead + 1) % MAX_OFFLINE_RECORDS;  // احذف الأقدم (تقدّم الرأس)
    ramOfflineCount--;
    LOGI("[OfflineBuf-RAM] Buffer full, dropped oldest record");
    }
    uint8_t idx = (ramOfflineHead + ramOfflineCount) % MAX_OFFLINE_RECORDS;
    ramOfflineBuf[idx] = rec;
    ramOfflineCount++;
    
    LOGI("[OfflineBuf-RAM] Saved offline record (reason=%s) [%d/%d]",
    rec.reason, ramOfflineCount, MAX_OFFLINE_RECORDS);
    }
    
    // نقطة الدخول الموحّدة لحفظ سجل احتياطي (تُستدعى فقط أثناء انقطاع الاتصال)
    void saveOfflineRecord(const char* reason) {
    OfflineRecordData rec = captureOfflineRecord(reason);
    saveOfflineRecordToRam(rec);
    }
    
    // بناء وإرسال رسالة تليمتري بنفس هيكلية pushStateToServer من سجل موحّد (يُستخدم عند التفريغ)
    void sendOfflineTelemetry(const OfflineRecordData& rec) {
    StaticJsonDocument<1024> doc;
    doc["event"] = "telemetry";
    doc["id"] = "esp-da683d";
    doc["channel"] = "client-device.updated";
    
    JsonArray devices = doc.createNestedArray("devices");
    JsonObject device0 = devices.createNestedObject();
    device0["id"] = 0;
    device0["voltage"] = rec.voltage;
    device0["current"] = rec.current;
    device0["power"] = rec.power;
    device0["balance"] = rec.balance;
    device0["last_charge_amount"] = rec.lastChargeAmount;
    device0["total_energy"] = rec.totalEnergy;
    device0["supply_frequency"] = rec.supplyFrequency;
    device0["power_factor"] = rec.powerFactor;
    device0["fault"] = rec.fault;
    device0["leakage_current"] = rec.leakageCurrent;
    device0["switch_status"] = rec.switchStatus;
    device0["prepay_status"] = rec.prepayStatus;
    device0["temp_zone_1"] = rec.tempZone1;
    device0["relay_status"] = rec.relayStatus;
    device0["reason"] = "offline_backup";  // يوضّح للسيرفر أن هذا سجل مؤجَّل
    device0["uptime_ms"] = rec.uptimeMs;
    device0["timestamp"] = rec.timestamp;  // وقت الالتقاط الفعلي وليس وقت الإرسال
    
    String out;
    serializeJson(doc, out);
    webSocket.sendTXT(out);
    
    LOGI("[OfflineBuf] Resent buffered record captured_at=%s", rec.timestamp);
    }
    
    // إرسال أقدم سجل في حلقة RAM ثم إزالته منها
    void sendOfflineRecordRam() {
    if (ramOfflineCount == 0) return;
    OfflineRecordData rec = ramOfflineBuf[ramOfflineHead];
    
    sendOfflineTelemetry(rec);
    
    ramOfflineHead = (ramOfflineHead + 1) % MAX_OFFLINE_RECORDS;
    ramOfflineCount--;
    }
    
    // بدء تفريغ المخزن الاحتياطي بعد عودة الاتصال
    void startOfflineDrain() {
    if (ramOfflineCount == 0) {
    offlineDraining = false;
    return;
    }
    
    offlineDraining = true;
    lastOfflineSendMs = 0;  // إرسال أول سجل فوراً في أول استدعاء لـ serviceOfflineDrain()
    LOGI("[OfflineBuf] Reconnected, draining %d buffered record(s) from RAM...",
    offlineBufferedCount());
    }
    
    // يُستدعى من loop() لتفريغ السجلات المخزّنة تدريجياً وبفواصل زمنية بسيطة (بدون حجب الحلقة الرئيسية)
    // ملاحظة إصلاح الخلل: عند اكتمال التفريغ (أو عدم وجود شيء ليُفرَّغ أصلاً) نرسل فوراً
    // حالة الجهاز الحيّة الحالية بدل الانتظار لتغيّر DP جديد قادم من الشريحة — وهذا يحل مشكلة
    // "بعد عودة الاتصال لا تُرسل القراءة الحيّة الجديدة في الوقت الفعلي".
    void serviceOfflineDrain() {
    if (!offlineDraining) return;
    
    if (!wsConnected) {
    // انقطع الاتصال أثناء التفريغ؛ نتوقف ونكمل لاحقاً عند إعادة الاتصال
    offlineDraining = false;
    return;
    }
    
    uint32_t now = millis();
    if (now - lastOfflineSendMs < OFFLINE_SEND_DELAY_MS) return;
    
    if (ramOfflineCount == 0) {
    offlineDraining = false;
    LOGI("[OfflineBuf] Drain complete (RAM), resuming real-time telemetry");
    // << الإصلاح: ادفع الحالة الحيّة فوراً بدل انتظار stateDirty/DP جديد
    pushStateToServer("drain_complete");
    return;
    }
    
    sendOfflineRecordRam();
    lastOfflineSendMs = now;
    }
    
    // ============================================================
    //  إعداد واجهة الويب (Web Server HTML)
    // ============================================================
    void handleRoot() {
    String html = "";
    html += "";
    html += " ";
    html += "لوحة تحكم القاطع";
    html += "";
    html += "body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f4f7f6; color: #333; margin: 0; padding: 20px; }";
    html += ".container { max-width: 800px; margin: 0 auto; background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); }";
    html += "h2 { color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 10px; }";
    html += ".status { display: flex; justify-content: space-between; flex-wrap: wrap; margin-bottom: 20px; }";
    html += ".card { background: #ecf0f1; padding: 15px; border-radius: 6px; width: 30%; min-width: 200px; margin-bottom: 10px; }";
    html += ".log-box { background: #2c3e50; color: #ecf0f1; padding: 15px; height: 300px; overflow-y: scroll; border-radius: 6px; font-family: monospace; direction: ltr; text-align: left; }";
    html += ".success { color: #27ae60; font-weight: bold; }";
    html += ".danger { color: #e74c3c; font-weight: bold; }";
    html += ".warn { color: #f39c12; font-weight: bold; }";
    html += "";
    
    html += "";
    html += "حالة جسر ESP-Tuya";
    
    // قسم بطاقات الحالة
    html += "";
    html += "الشبكة (WiFi):" + String(WIFI_SSID) + "IP: " + WiFi.localIP().toString() + "الوقت: " + getCurrentTime() + "";
    html += "السيرفر (WebSocket):" + String(wsConnected ? "متصل" : "غير متصل") + "";
    html += "شريحة القاطع (Tuya):" + String(mcuIsResponding() ? "تستجيب" : "لا توجد استجابة") + "";
    
    // بطاقة المخزن الاحتياطي (RAM فقط الآن)
    String bufStatus;
    int cnt = offlineBufferedCount();
    if (offlineDraining) {
    bufStatus = "جارِ إرسال " + String(cnt) + " سجل مؤجل...";
    } else if (cnt > 0) {
    bufStatus = "" + String(cnt) + " / " + String(MAX_OFFLINE_RECORDS) + " سجل مخزّن";
    } else {
    bufStatus = "فارغ";
    }
    
    html += "المخزن الاحتياطي:" + bufStatus + "";
    html += "";
    
    // قسم سجل الأحداث
    html += "سجل الأحداث (Logs):";
    html += "";
    
    // طباعة السجل بالترتيب (من الأقدم للأحدث)
    int startIdx = logIndex;
    for (int i = 0; i < MAX_LOG_LINES; i++) {
    int idx = (startIdx + i) % MAX_LOG_LINES;
    if (webLogs[idx].length() > 0) {
    html += webLogs[idx] + "";
    }
    }
    
    html += "";
    
    server.send(200, "text/html", html);
    }
    
    // ============================================================
    //  إرسال حالة الجهاز الحالية إلى السيرفر بالهيكلية الجديدة
    // ============================================================
    void pushStateToServer(const char* reason) {
    if (!wsConnected) return;
    
    // === طباعة القيم في اللوج (مقسمة لسطرين لسهولة القراءة وتجنب تجاوز حجم البفر) ===
    LOGI("[Pusher] Reason: %s | V:%.1f | I:%.3fA | W:%.1fW | kWh:%.2f",
    reason, state.voltage, state.current, state.power, state.totalEnergy);
    LOGI("[Pusher] Sw:%d | Pre:%d | Rly:%d | Bal:%.2f | Temp:%.1f",
    state.switchStatus, state.prepayStatus, state.relayStatus, state.balance, state.tempZone1);
    // =========================================================================================
    
    StaticJsonDocument<1024> doc;
    
    doc["event"] = "telemetry";
    doc["id"] = "esp-da683d";
    doc["channel"] = "client-device.updated";
    
    JsonArray devices = doc.createNestedArray("devices");
    JsonObject device0 = devices.createNestedObject();
    
    device0["id"] = 0;
    device0["voltage"] = state.voltage;
    device0["current"] = state.current;
    device0["power"] = state.power;
    device0["balance"] = state.balance;
    device0["last_charge_amount"] = state.lastChargeAmount;
    device0["total_energy"] = state.totalEnergy;
    device0["supply_frequency"] = state.supplyFrequency;
    device0["power_factor"] = state.powerFactor;
    device0["fault"] = state.fault;
    device0["leakage_current"] = state.leakageCurrent;
    device0["switch_status"] = state.switchStatus;
    device0["prepay_status"] = state.prepayStatus;
    device0["temp_zone_1"] = state.tempZone1;
    device0["relay_status"] = state.relayStatus;
    device0["reason"] = reason;
    device0["uptime_ms"] = millis();
    device0["timestamp"] = getCurrentTime();  // << إضافة الوقت الحالي للبايلود
    
    String out;
    serializeJson(doc, out);
    webSocket.sendTXT(out);
    
    lastStatePush = millis();
    stateDirty = false;
    
    // احتفظنا بهذا السطر كما هو ليؤكد إرسال البيانات
    LOGI("[Pusher] State pushed: %s at %s", reason, getCurrentTime().c_str());
    
    // ملاحظة: إذا أردت رؤية شكل الـ JSON كاملاً في (Serial Monitor) فقط دون صفحة الويب، يمكنك إزالة التعليق عن السطر التالي:
    // DBG.println(out);
    }
    
    // ============================================================
    //  فك ترميز DP القادم من Tuya MCU
    // ============================================================
    void onDPReceived(uint8_t dpId, uint8_t dpType, const uint8_t* data, uint8_t len) {
    // أي DP يصل فعلياً = دليل أن الشريحة حية، بغض النظر عن heartbeat الداخلي في المكتبة
    lastDpReceivedMs = millis();
    
    bool updated = false;
    
    auto readBE = [&](uint8_t n) -> uint32_t {
    uint32_t v = 0;
    for (uint8_t i = 0; i < n && i < len; i++) v = (v << 8) | data[i];
    return v;
    };
    
    if (dpId == 16 && dpType == TuyaDPType::BOOLEAN && len >= 1) {
    state.switchStatus = data[0] != 0;
    updated = true;
    } else if (dpId == 11 && dpType == TuyaDPType::BOOLEAN && len >= 1) {
    state.prepayStatus = data[0] != 0;
    updated = true;
    } else if (dpId == 6 && dpType == TuyaDPType::RAW && len >= 8) {
    uint16_t v = (data[0] << 8) | data[1];
    uint32_t i3 = ((uint32_t)data[2] << 16) | (data[3] << 8) | data[4];
    uint32_t p3 = ((uint32_t)data[5] << 16) | (data[6] << 8) | data[7];
    state.voltage = v / 10.0f;
    state.current = i3 / 1000.0f;
    state.power = (float)p3;
    updated = true;
    } else if (dpId == 13 && dpType == TuyaDPType::VALUE) {
    float v = readBE(len) / 100.0f;
    state.balance = v;
    state.lastChargeAmount = v;
    updated = true;
    } else if (dpId == 32 && dpType == TuyaDPType::VALUE) {
    state.supplyFrequency = readBE(len) / 10.0f;
    updated = true;
    } else if (dpId == 50 && dpType == TuyaDPType::VALUE) {
    state.powerFactor = (float)readBE(len);
    updated = true;
    } else if (dpId == 9 && dpType == TuyaDPType::VALUE) {
    state.fault = (int)readBE(len);
    updated = true;
    } else if (dpId == 15 && dpType == TuyaDPType::VALUE) {
    state.leakageCurrent = (float)readBE(len);
    updated = true;
    } else if (dpId == 25 && dpType == TuyaDPType::VALUE) {
    state.tempZone1 = (float)readBE(len);
    updated = true;
    } else if (dpId == 101 && dpType == TuyaDPType::ENUM) {
    state.relayStatus = (int)readBE(len);
    updated = true;
    } else if (dpId == 1 && dpType == TuyaDPType::VALUE) {
    state.totalEnergy = readBE(len) / 100.0f;
    updated = true;
    }
    
    if (updated) {
    stateDirty = true;
    }
    }
    
    // ============================================================
    //  إرسال إشعار نجاح/فشل تنفيذ أمر إلى السيرفر عبر نفس القناة
    // ============================================================
    void sendCommandAck(const char* cmd, bool success, const int id) {
    if (!wsConnected) return;
    
    StaticJsonDocument<256> doc;
    doc["event"] = "command_ack";
    doc["channel"] = "devices";
    doc["id"] = "esp-da683d";
    doc["log_id"] = id;
    
    JsonObject data = doc.createNestedObject("data");
    data["cmd"] = cmd;
    data["success"] = success;
    data["timestamp"] = getCurrentTime();
    
    String out;
    serializeJson(doc, out);
    webSocket.sendTXT(out);
    
    LOGI("[CMD] Ack sent: %s -> %s", cmd, success ? "success" : "failed");
    
    }
    
    // ============================================================
    //  تنفيذ الأوامر القادمة من السيرفر
    // ============================================================
    void handleServerCommand(const JsonDocument& doc) {
    const char* cmd = doc["cmd"] | "";
    const char* device_id = doc["device_id"] | "";
    
    if(strcmp(device_id, "esp-da683d") != 0){
    return;
    }
    
    bool handled = true;
    
    if (strcmp(cmd, "switch") == 0) {
    bool status = doc["value"] | false;
    tuya.sendDPBool(16, status);
    } else if (strcmp(cmd, "switch_prepayment") == 0) {
    bool status = doc["value"] | false;
    tuya.sendDPBool(11, status);
    } else if (strcmp(cmd, "charge_energy") == 0) {
    float amount = doc["value"] | 0.0f;
    tuya.sendDPValue(14, (int32_t)(amount * 100.0f));
    } else if (strcmp(cmd, "clear_energy") == 0) {
    tuya.sendDPBool(12, true);
    } else if (strcmp(cmd, "set_relay_status") == 0) {
    int status = doc["value"] | 0;
    tuya.sendDPEnum(101, (uint8_t)status);
    } else if (strcmp(cmd, "query_state") == 0) {
    tuya.queryAllStates();
    } else {
    LOGI("[WARN] Unknown command: %s", cmd);
    handled = false;
    }
    
    if (!handled) {
    sendCommandAck(cmd, false, doc["log_id"] | 0);
    return;
    }
    
    pendingVerifyAt = millis() + POST_COMMAND_VERIFY_DELAY_MS;
    LOGI("[CMD] Executed: %s", cmd);
    
    // تفعيل مؤقت لانتظار ثانية واحدة حتى تستقر الشريحة وترسل كل قراءاتها
    commandResponsePending = true;
    commandResponseTime = millis() + 1000; // 1000 ملي ثانية = ثانية واحدة
    sendCommandAck(cmd, true,doc["log_id"] | 0);
    }
    
    // ============================================================
    //  الاشتراك في قناة الأجهزة (يُستدعى عند الاتصال ودورياً أثناءه)
    // ============================================================
    void subscribeToChannel() {
    if (!wsConnected) return;
    webSocket.sendTXT("{\"event\":\"pusher:subscribe\",\"data\":{\"channel\":\"devices\"}}");
    lastSubscribeMs = millis();
    LOGI("[WS] Subscribe request sent for channel 'devices'");
    }
    
    // ============================================================
    //  أحداث WebSocket
    // ============================================================
    void onWsEvent(WStype_t type, uint8_t* payload, size_t length) {
    switch (type) {
    case WStype_CONNECTED:
    wsConnected = true;
    LOGI("[WS] Connected");
    subscribeToChannel();
    
    // إذا كان هناك سجلات مخزّنة من فترة الانقطاع، أرسلها أولاً بترتيبها الزمني
    // (serviceOfflineDrain سيدفع الحالة الحيّة تلقائياً فور اكتمال التفريغ).
    // غير ذلك، أرسل الحالة الحية مباشرة فوراً — هذا هو الإصلاح الأساسي لضمان
    // عدم "تجمّد" القراءات بعد عودة الاتصال.
    if (offlineBufferedCount() > 0) {
    startOfflineDrain();
    } else {
    pushStateToServer("connected");
    }
    break;
    
    case WStype_DISCONNECTED:
    wsConnected = false;
    offlineDraining = false;  // سنعيد المحاولة عند إعادة الاتصال
    LOGI("[WS] Disconnected");
    break;
    
    case WStype_TEXT:
    {
    StaticJsonDocument<1024> doc;
    DeserializationError err = deserializeJson(doc, payload, length);
    if (err) {
    LOGI("[WS] Outer JSON parse error");
    return;
    }
    
    const char* eventName = doc["event"];
    
    if (eventName && strcmp(eventName, "pusher_internal:subscription_succeeded") == 0) {
    LOGI("[WS] Subscribed to channel: devices");
    return;
    }
    
    if (eventName && strcmp(eventName, "device.updated") == 0) {
    const char* innerDataStr = doc["data"];
    if (innerDataStr) {
    StaticJsonDocument<512> innerDoc;
    DeserializationError innerErr = deserializeJson(innerDoc, innerDataStr);
    if (!innerErr) {
    handleServerCommand(innerDoc);
    } else {
    LOGI("[WS] Inner JSON parse error");
    }
    }
    }
    break;
    }
    
    case WStype_ERROR:
    LOGI("[WS] Connection Error");
    break;
    
    default:
    break;
    }
    }
    
    // ============================================================
    //  إعداد OTA
    // ============================================================
    void setupOTA() {
    ArduinoOTA.setHostname(OTA_HOSTNAME);
    // ArduinoOTA.setPassword(OTA_PASSWORD);
    
    ArduinoOTA.onStart([]() {
    LOGI("[OTA] Update starting...");
    });
    ArduinoOTA.onEnd([]() {
    LOGI("[OTA] Update finished, rebooting.");
    });
    ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
    static int lastPct = -1;
    int pct = (progress * 100) / total;
    if (pct != lastPct && pct % 10 == 0) {
    LOGI("[OTA] Progress: %d%%", pct);
    lastPct = pct;
    }
    });
    ArduinoOTA.onError([](ota_error_t error) {
    LOGI("[OTA] Error[%u]", error);
    });
    
    ArduinoOTA.begin();
    }
    
    // ============================================================
    //  إعداد WiFi وتزامن الوقت
    // ============================================================
    void setupWiFi() {
    WiFi.mode(WIFI_STA);
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
    LOGI("[WiFi] Connecting to %s", WIFI_SSID);
    
    uint8_t retryCount = 0;
    while (WiFi.status() != WL_CONNECTED && retryCount < 50) {
    delay(500);
    DBG.print(".");
    retryCount++;
    }
    DBG.println();
    
    if (WiFi.status() != WL_CONNECTED) {
    LOGI("[WiFi] Failed to connect, restarting...");
    delay(2000);
    ESP.restart();
    }
    
    LOGI("[WiFi] Connected. IP: %s", WiFi.localIP().toString().c_str());
    WiFi.setAutoReconnect(true);
    WiFi.persistent(true);
    
    // === تهيئة مزامنة الوقت من الإنترنت (NTP) ===
    // الإزاحة: 3 ساعات * 3600 ثانية = 10800 (توقيت اليمن UTC+3)
    configTime(3 * 3600, 0, "pool.ntp.org", "time.nist.gov");
    LOGI("[NTP] Requesting time sync...");
    }
    
    // ============================================================
    //  setup / loop
    // ============================================================
    void setup() {
    Serial.begin(TUYA_BAUDRATE);
    Serial1.begin(115200);
    delay(200);
    
    LOGI("[BOOT] ESP Tuya Bridge starting...");
    LOGI("[OfflineBuf] Using RAM-only buffer (max %d records). No Flash/LittleFS in use.",
    MAX_OFFLINE_RECORDS);
    
    tuya.begin(onDPReceived);
    setupWiFi();
    setupOTA();
    
    // تشغيل سيرفر الويب وتخصيص مسار الروت "/"
    server.on("/", handleRoot);
    server.begin();
    LOGI("[Web] Server started on port 80");
    
    if (WS_USE_SSL) {
    webSocket.beginSSL(WS_HOST, WS_PORT, WS_PATH);
    } else {
    webSocket.begin(WS_HOST, WS_PORT, WS_PATH);
    }
    
    webSocket.onEvent(onWsEvent);
    webSocket.setReconnectInterval(WS_RECONNECT_MS);
    webSocket.enableHeartbeat(15000, 3000, 2);
    
    LOGI("[BOOT] Setup complete.");
    }
    
    void loop() {
    ArduinoOTA.handle();
    webSocket.loop();
    tuya.update();
    server.handleClient();  // الاستماع لطلبات المتصفح
    
    uint32_t now = millis();
    
    // 1) تفريغ المخزن الاحتياطي تدريجياً إذا كنا في وضع "draining" بعد عودة الاتصال
    serviceOfflineDrain();
    
    // 2) أثناء الانقطاع: احفظ لقطة كل فترة (بحد أقصى MAX_OFFLINE_RECORDS سجل، الأقدم يُستبدل أولاً)
    if (!wsConnected) {
    if (lastOfflineSaveMs == 0 || (now - lastOfflineSaveMs) >= OFFLINE_SAVE_INTERVAL_MS) {
    saveOfflineRecord("offline_periodic");
    lastOfflineSaveMs = now;
    }
    } else {
    lastOfflineSaveMs = 0;  // إعادة الضبط حتى تبدأ الفترة من جديد عند الانقطاع القادم
    }
    // ============================================================
    // 3) الإرسال للسيرفر
    // ============================================================
    now = millis(); // أزلنا كلمة unsigned long لأن المتغير معرّف مسبقاً
    // أ) إرسال استجابة الأمر (مرة واحدة فقط بعد مرور ثانية من استلام الأمر)
    if (commandResponsePending && (now >= commandResponseTime)) {
    if (wsConnected && !offlineDraining) {
    pushStateToServer("command_response");
    }
    commandResponsePending = false; // إغلاق الطلب لضمان عدم إرساله مرة أخرى
    }
    
    // ب) الإرسال الدوري الطبيعي (كل 60 ثانية)
    bool timeToPush = (now - lastStatePush) >= TELEMETRY_INTERVAL_MS;
    if (stateDirty && wsConnected && !offlineDraining && timeToPush) {
    pushStateToServer("periodic_update");
    }
    
    // 4) إعادة إرسال طلب الاشتراك بالقناة دورياً أثناء الاتصال (يحافظ على استقبال البيانات عبر Laravel Echo/Pusher
    //    في حال انتهت صلاحية الجلسة على السيرفر أو أُعيد تشغيله دون قطع اتصال الـ WebSocket نفسه)
    if (wsConnected && (now - lastSubscribeMs >= CHANNEL_RESUBSCRIBE_INTERVAL_MS)) {
    subscribeToChannel();
    }
    
    if (pendingVerifyAt != 0 && (int32_t)(now - pendingVerifyAt) >= 0) {
    tuya.queryAllStates();
    pendingVerifyAt = 0;
    }
    
    static uint32_t lastMcuWarn = 0;
    if (mcuLastActivityMs() != 0 && (now - mcuLastActivityMs()) > MCU_TIMEOUT_MS && (now - lastMcuWarn) > 10000) {
    LOGI("[WARN] No heartbeat from Tuya MCU");
    lastMcuWarn = now;
    }
    yield();
    }
    
    
    Helpful post? Buy me a coffee.
ADVERTISEMENT