Skip to content

Instantly share code, notes, and snippets.

@IgorDePaula
Created October 21, 2025 17:43
Show Gist options
  • Save IgorDePaula/bae42f577827e69a5ecffb06b063045a to your computer and use it in GitHub Desktop.
Save IgorDePaula/bae42f577827e69a5ecffb06b063045a to your computer and use it in GitHub Desktop.

Revisions

  1. IgorDePaula created this gist Oct 21, 2025.
    851 changes: 851 additions & 0 deletions last.ino
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,851 @@
    #include <SPI.h>
    #include <SD.h>
    #include <Wire.h>
    #include <SSD1306.h>
    #include <RTClib.h>
    #include <WiFi.h>
    #include "LoRa.h"
    #include <BLEDevice.h>
    #include <BLEServer.h>
    #include <BLEUtils.h>
    #include <BLE2902.h>

    #include "Preferences.h" // Para salvar SSID/senha

    //lora

    #define BAND 915E6 // Frequência (915MHz para América do Sul)
    #define SCK 5 // GPIO5 -- SX1278's SCK
    #define MISO 19 // GPIO19 -- SX1278's MISnO
    #define MOSI 27 // GPIO27 -- SX1278's MOSI
    #define SS 18 // GPIO18 -- SX1278's CS
    #define RST 14 // GPIO14 -- SX1278's RESET
    #define DI0 26 // GPIO26 -- SX1278's IRQ(Interrupt Request)
    int packetCount = 0;
    int lastSendTime = 0;
    int interval = 2000; // Intervalo entre envios (2 segundos)
    String lastReceived = "";
    int lastRSSI = 0;
    String message = "";
    String boiaState = "";
    bool comunicacaoLora = false;
    int lastLora = 0;
    int loraTimeOut = 3000;
    // Configuração do Display OLED
    #define OLED_SDA 4
    #define OLED_SCL 15
    #define OLED_RST 16
    SSD1306 display(0x3c, OLED_SDA, OLED_SCL);
    SPIClass spiSD(HSPI);
    // Configuração do SD Card - Pinos seguros
    #define SD_CS 17 // Chip Select (CS)
    #define SD_MOSI 23 // Master Out Slave In (MOSI)
    #define SD_MISO 2 // Master In Slave Out (MISO)
    #define SD_SCK 13 // Serial Clock (SCK)

    // RTC DS3231 (usa I2C - SDA=21, SCL=22 por padrão)
    // ATENÇÃO: Como SDA/SCL do display já usam 4/15,
    // vamos usar I2C secundário nos pinos 21/22

    TwoWire I2C_RTC = TwoWire(1);
    RTC_DS3231 rtc;

    //SPIClass sdSPI(VSPI);
    bool sdOk = false;
    bool rtcOk = false;
    int releState = 0;
    // Controle de alternância do display
    unsigned long lastDisplayToggle = 0;
    bool showNetworkInfo = true;

    String resposta = "";
    bool deviceConnected = false;
    char dataHora[32];



    #define RELELIGA 32
    #define RELEDESLIGA 25

    const float THRESHOLD_LIGAR = 3.0;
    const float THRESHOLD_DESLIGAR = 1.0;
    bool pinoLigado = false;
    //flow sensor

    #define FLOW_SENSOR_PIN 33 // GPIO para o sensor de fluxo
    volatile int pulseCount = 0;
    float flowRate = 0.0;
    float flowMilliLitres = 0.0;
    float totalMilliLitres = 0.0;
    unsigned long oldTimeFlow = 0;


    WiFiServer server(80);
    // Interrupção do sensor de fluxo
    void IRAM_ATTR pulseCounter() {
    pulseCount++;
    }

    void inicializaFlowSensor() {
    pinMode(FLOW_SENSOR_PIN, INPUT_PULLUP);
    attachInterrupt(digitalPinToInterrupt(FLOW_SENSOR_PIN), pulseCounter, FALLING);
    oldTimeFlow = millis();
    Serial.println("Sensor de fluxo inicializado no GPIO 33");
    }

    void calculaFluxo() {
    unsigned long currentTime = millis();

    if ((currentTime - oldTimeFlow) >= 1000) { // Calcula a cada 1 segundo
    detachInterrupt(digitalPinToInterrupt(FLOW_SENSOR_PIN));

    // Fator de calibração: pulsos por litro (para YF-S201 é ~7.5 pulsos/L)
    float calibrationFactor = 7.5;

    // Cálculo da vazão
    flowRate = ((1000.0 / (currentTime - oldTimeFlow)) * pulseCount) / calibrationFactor;
    oldTimeFlow = currentTime;

    // Calcula volume em mL no último segundo
    flowMilliLitres = (flowRate / 60) * 1000;

    // Adiciona ao total
    totalMilliLitres += flowMilliLitres;
    if (flowRate > THRESHOLD_LIGAR && !pinoLigado) {
    digitalWrite(RELELIGA, LOW);
    pinoLigado = true;
    delay(100);
    digitalWrite(RELELIGA, HIGH);
    }
    if (flowRate < THRESHOLD_DESLIGAR && pinoLigado) {
    delay(100);
    digitalWrite(RELELIGA, HIGH);
    delay(100);
    digitalWrite(RELEDESLIGA, LOW);
    delay(100);
    digitalWrite(RELEDESLIGA, HIGH);
    pinoLigado = false;
    }

    pulseCount = 0;
    attachInterrupt(digitalPinToInterrupt(FLOW_SENSOR_PIN), pulseCounter, FALLING);
    }
    }

    //lora

    // UUIDs originais para SSID e Senha
    #define UUID_SSID "c505b1de-4a31-11ef-9d90-47f7f9b3a434"
    #define UUID_PASS "c505b49e-4a31-11ef-9d90-47f7f9b3a434"

    #define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"
    // Novas UUIDs para comando scan, resultado e status WiFi
    #define UUID_SCAN_CMD "c505b600-4a31-11ef-9d90-47f7f9b3a434"
    #define UUID_SCAN_RESULT "c505b601-4a31-11ef-9d90-47f7f9b3a434"
    #define UUID_WIFI_STATUS "c505b602-4a31-11ef-9d90-47f7f9b3a434"
    #define UUID_BOMB_CMD "c505b603-4a31-11ef-9d90-47f7f9b3a434"
    #define UUID_BOMB_RESULT "c505b604-4a31-11ef-9d90-47f7f9b3a434"

    BLEServer* pServer;
    BLECharacteristic* pScanCmdChar;
    BLECharacteristic* pBombCmdChar;
    BLECharacteristic* pScanResultChar;
    BLECharacteristic* pWifiStatusChar;
    BLECharacteristic* pBombStatusChar;
    String ip, rede;

    Preferences preferences; // NVS

    BLEAdvertising* pAdvertising;

    uint16_t connId = 0; // guarda o ID da conexão para desconectar
    String performWifiScanAsJson() {
    int n = WiFi.scanNetworks(true); // show_hidden = true
    int o = WiFi.scanComplete();
    if (o >= 0) { // scan terminou
    // gerar JSON e enviar Notify
    WiFi.scanDelete(); // limpa resultado
    }
    String json = "[";
    for (int i = 0; i < n; i++) {
    String ssid = WiFi.SSID(i);
    if (ssid.length() == 0) ssid = "<OCULTO>";
    json += "{\"ssid\":\"" + ssid + "\",\"rssi\":" + String(WiFi.RSSI(i)) + "}";
    if (i < n - 1) json += ",";
    }
    json += "]";
    return json;
    }



    void ligaRele(int rele) {
    digitalWrite(rele, LOW);
    delay(1000);
    digitalWrite(rele, HIGH);
    releState = 0;
    }
    // --- Callbacks para comando SCAN ---
    class ScanCmdCallbacks : public BLECharacteristicCallbacks {
    void onWrite(BLECharacteristic* pCharacteristic) {
    String value = pCharacteristic->getValue();
    if (value == "SCAN") {
    Serial.println("Comando SCAN recebido via BLE...");

    // Pausa Advertising BLE
    pAdvertising->stop();
    delay(200);

    // Scan WiFi
    String jsonResult = performWifiScanAsJson();

    // Reativa Advertising BLE
    pAdvertising->start();

    // Envia resultado via Notify
    pScanResultChar->setValue(jsonResult.c_str());
    pScanResultChar->notify();
    Serial.println("Scan concluído e JSON enviado via BLE Notify:");
    Serial.println(jsonResult);
    }
    if (value == "OFF") {
    WiFi.disconnect(true);
    WiFi.mode(WIFI_OFF);
    resposta = "Wi-Fi desligado";
    Serial.println("Wi-Fi desligado via BLE");
    if (deviceConnected && pCharacteristic != NULL) {
    pCharacteristic->setValue(resposta.c_str());
    pCharacteristic->notify();
    }
    }
    }
    };
    class BombCmdCallbacks : public BLECharacteristicCallbacks {
    void onWrite(BLECharacteristic* pCharacteristic) {

    String value = pCharacteristic->getValue();
    if (value == "ONBOMB") {
    Serial.println("Comando ONBOMB recebido");
    releState = 1;
    String resposta = "Bomba ligada";
    ligaRele(RELELIGA);
    if (deviceConnected && pCharacteristic != NULL) {
    pBombStatusChar->setValue("Bomba ligada");
    pBombStatusChar->notify();
    }
    }
    if (value == "OFFBOMB") {
    Serial.println("Comando OFFBOMB recebido");
    releState = 2;
    ligaRele(RELEDESLIGA);
    if (deviceConnected && pCharacteristic != NULL) {
    pBombStatusChar->setValue("Bomba desligada");
    pBombStatusChar->notify();
    }
    }
    }
    };

    // --- Callbacks para SSID/Password ---
    class WifiCredentialsCallbacks : public BLECharacteristicCallbacks {
    void onWrite(BLECharacteristic* pCharacteristic) {
    String value = pCharacteristic->getValue();
    static String ssid = "";
    static String pass = "";

    if (pCharacteristic->getUUID().toString() == UUID_SSID) {
    ssid = String(value.c_str());
    Serial.println("SSID recebido: " + ssid);
    } else if (pCharacteristic->getUUID().toString() == UUID_PASS) {
    pass = String(value.c_str());
    Serial.println("Senha recebida: " + pass);
    }

    if (ssid.length() > 0 && pass.length() > 0) {
    Serial.println("Tentando conectar à rede WiFi...");
    WiFi.begin(ssid.c_str(), pass.c_str());
    int timeout = 0;
    while (WiFi.status() != WL_CONNECTED && timeout < 20) { // ~10s
    delay(500);
    Serial.print(".");
    timeout++;
    }
    Serial.println();

    if (WiFi.status() == WL_CONNECTED) {
    Serial.println("WiFi conectado! IP: " + WiFi.localIP().toString());
    pWifiStatusChar->setValue("OK");
    pWifiStatusChar->notify();

    // Salva SSID/senha na NVS
    preferences.begin("wifi", false);
    preferences.putString("ssid", ssid);
    preferences.putString("pass", pass);
    preferences.end();
    } else {
    Serial.println("Falha na conexão WiFi");
    pWifiStatusChar->setValue("ERROR");
    pWifiStatusChar->notify();
    }

    ssid = "";
    pass = "";
    }
    }
    };
    // botoes
    class MyServerCallbacks : public BLEServerCallbacks {
    void onConnect(BLEServer* pServer) {
    deviceConnected = true;

    connId = pServer->getConnId();
    Serial.println("Cliente BLE conectado");
    };

    void onDisconnect(BLEServer* pServer) {
    deviceConnected = false;
    Serial.println("Cliente BLE desconectado");
    // pServer->startAdvertising(); // Reinicia advertising

    pAdvertising->start();
    }
    };
    void setup() {
    Serial.begin(115200);
    delay(2000);

    Serial.println("=== HELTEC ESP32 + SD CARD + RTC ===");

    // Inicializa display
    inicializaDisplay();

    // Inicializa I2C secundário para RTC (pinos 21, 22)
    I2C_RTC.begin(21, 22, 100000); // SDA=21, SCL=22, 100kHz
    delay(100);

    // Inicializa I2C secundário para RTC (pinos 21, 22)
    //Wire1.begin(21, 22); // SDA=21, SCL=22
    inicializaLora();
    // Inicializa RTC
    inicializaRTC();

    // Inicializa SD Card
    inicializaSD();

    inicializaFlowSensor();

    // Se tudo OK, grava dados iniciais
    if (sdOk && rtcOk) {
    gravaLogInicial();
    }

    atualizaDisplay();

    pinMode(RELELIGA, OUTPUT);
    pinMode(RELEDESLIGA, OUTPUT);

    digitalWrite(RELELIGA, HIGH);
    digitalWrite(RELEDESLIGA, HIGH);
    // --- Inicializa BLE ---
    BLEDevice::init("Water Pump Ranger");
    pServer = BLEDevice::createServer();

    pServer->setCallbacks(new MyServerCallbacks());
    BLEService* pService = pServer->createService(BLEUUID((uint16_t)0x180A));

    BLECharacteristic* pSSIDChar = pService->createCharacteristic(UUID_SSID, BLECharacteristic::PROPERTY_WRITE);
    BLECharacteristic* pPassChar = pService->createCharacteristic(UUID_PASS, BLECharacteristic::PROPERTY_WRITE);
    pSSIDChar->setCallbacks(new WifiCredentialsCallbacks());
    pPassChar->setCallbacks(new WifiCredentialsCallbacks());

    pScanCmdChar = pService->createCharacteristic(UUID_SCAN_CMD, BLECharacteristic::PROPERTY_WRITE);
    pScanCmdChar->setCallbacks(new ScanCmdCallbacks());

    pBombCmdChar = pService->createCharacteristic(UUID_BOMB_CMD, BLECharacteristic::PROPERTY_WRITE);
    pBombCmdChar->setCallbacks(new BombCmdCallbacks());

    pScanResultChar = pService->createCharacteristic(UUID_SCAN_RESULT, BLECharacteristic::PROPERTY_NOTIFY);
    pScanResultChar->addDescriptor(new BLE2902());

    pBombStatusChar = pService->createCharacteristic(UUID_BOMB_RESULT, BLECharacteristic::PROPERTY_NOTIFY);
    pBombStatusChar->addDescriptor(new BLE2902());

    pWifiStatusChar = pService->createCharacteristic(UUID_WIFI_STATUS, BLECharacteristic::PROPERTY_NOTIFY);
    pWifiStatusChar->addDescriptor(new BLE2902());

    pService->start();

    pAdvertising = BLEDevice::getAdvertising();
    pAdvertising->addServiceUUID(pService->getUUID());
    pAdvertising->start();

    Serial.println("BLE iniciado e advertising ativo.");

    // --- Inicializa WiFi ---
    WiFi.setHostname("Water111-Pump_Ranger");
    WiFi.mode(WIFI_STA);
    WiFi.disconnect(true);
    Serial.println("WiFi pronto.");

    //conecta automaticamente usando NVS ---
    preferences.begin("wifi", false);
    String savedSSID = preferences.getString("ssid", "");
    String savedPass = preferences.getString("pass", "");
    preferences.end();

    if (savedSSID.length() > 0) {
    Serial.println("Reconectando ao WiFi salvo: " + savedSSID);
    WiFi.begin(savedSSID.c_str(), savedPass.c_str());
    int timeout = 0;
    while (WiFi.status() != WL_CONNECTED && timeout < 20) {
    delay(500);
    Serial.print(".");
    timeout++;
    }
    Serial.println();
    if (WiFi.status() == WL_CONNECTED) {
    ip = WiFi.localIP().toString();
    rede = WiFi.SSID();
    Serial.println("Reconexão bem sucedida! IP: " + WiFi.localIP().toString());
    } else {
    Serial.println("Falha na reconexão automática");
    }
    }

    server.begin();
    }
    void inicializaLora() {

    SPI.begin(SCK, MISO, MOSI, SS);
    LoRa.setPins(SS, RST, DI0);

    // Inicializa LoRa
    if (!LoRa.begin(BAND)) {
    Serial.println("Erro ao iniciar LoRa!");
    //displayMessage("ERRO LoRa!", "Verifique", "conexões");
    while (1)
    ;
    }
    LoRa.setSpreadingFactor(10); // SF7 - SF12 (maior = mais alcance, menor velocidade)
    LoRa.setSignalBandwidth(125E3); // 125kHz
    LoRa.setCodingRate4(5); // 4/5
    LoRa.setPreambleLength(8); // Preâmbulo
    LoRa.enableCrc(); // Habilita CRC

    Serial.println("LoRa inicializado com sucesso!");
    Serial.println("Frequência: " + String(BAND / 1E6) + " MHz");
    }
    void sendMessage() {
    //packetCount++;
    //String state = voltagemMean < 30 ? "alto" : "baixo";
    //String message = "Boia em nivel " + state;

    // Envia mensagem
    LoRa.beginPacket();
    LoRa.print(dataHora);
    LoRa.endPacket();

    Serial.println("Enviado: " + String(dataHora));

    //displayMessage("ENVIANDO", message, "");
    };
    void loop() {

    int packetSize = LoRa.parsePacket();
    if (packetSize) {
    receiveMessage();
    }
    if (millis() - lastLora > loraTimeOut) {
    comunicacaoLora = false;
    }
    else{
    comunicacaoLora = true;
    }
    // Envia mensagem a cada intervalo definido
    if (millis() - lastSendTime > interval) {
    sendMessage();
    //releState = 20;
    lastSendTime = millis();
    }


    static unsigned long ultimaGravacao = 0;
    // digitalWrite(ENBTN, HIGH);
    if (millis() - lastDisplayToggle > 2000) {
    lastDisplayToggle = millis();
    showNetworkInfo = !showNetworkInfo;
    }
    // Calcula fluxo de água
    calculaFluxo();
    // Grava dados a cada 10 segundos
    if (millis() - ultimaGravacao > 10000) {
    ultimaGravacao = millis();

    if (sdOk && rtcOk) {
    //gravaDadosComTimestamp();
    }
    }
    atualizaDisplay();
    verifyFlow();
    checaRele();
    //checkBoia();



    //static unsigned long lastCheck = 0;
    // if (millis() - lastCheck > 10000) {
    // lastCheck = millis();
    // if (WiFi.status() != WL_CONNECTED) {
    // Serial.println("WiFi caiu, tentando reconectar...");
    // preferences.begin("wifi", false);
    // String savedSSID = preferences.getString("ssid", "");
    // String savedPass = preferences.getString("pass", "");
    // preferences.end();

    // if (savedSSID.length() > 0) {
    // WiFi.begin(savedSSID.c_str(), savedPass.c_str());
    // int timeout = 0;
    // while (WiFi.status() != WL_CONNECTED && timeout < 20) {
    // delay(500);
    // Serial.print(".");
    // timeout++;
    // }
    // Serial.println();
    // if (WiFi.status() == WL_CONNECTED) {
    // ip = WiFi.localIP().toString();
    // rede = WiFi.SSID();
    // Serial.println("Reconexão WiFi bem sucedida! IP: " + WiFi.localIP().toString());
    // } else {
    // Serial.println("Falha na reconexão WiFi");
    // }
    // }
    // }
    // }
    }

    void registraInformacao() {
    DateTime now = rtc.now();
    char secondString[3]; // Character array to hold the second (e.g., "05", "59")
    // Size 3: 2 digits + null terminator
    char minuteString[3];
    sprintf(secondString, "%02d", now.second());
    sprintf(minuteString, "%02d", now.minute());
    if (minuteString == "00" && secondString == "00") {

    // Nome do arquivo baseado na data
    char nomeArquivo[32];
    sprintf(nomeArquivo, "/dados_%04d_%02d_%02d.csv",
    now.year(), now.month(), now.day());

    // Verifica se é um arquivo novo (precisa de cabeçalho)
    bool arquivoNovo = !SD.exists(nomeArquivo);

    File arquivo = SD.open(nomeArquivo, FILE_APPEND);
    if (!arquivo) {
    Serial.println("Erro ao abrir arquivo de dados");
    return;
    }

    // Adiciona cabeçalho se arquivo novo
    if (arquivoNovo) {
    arquivo.println("timestamp,data,hora,estado bomba,nivel boia,fluxo agua");
    }
    String releStateString = "";
    if (releState == 1) {
    releStateString = "Ligado";
    }
    if (releState == 2) {
    releStateString = "Desligado";
    }
    // Grava dados
    arquivo.printf("%lu,%02d/%02d/%04d,%02d:%02d:%02d,%s,%s,%.1f,\n",
    now.unixtime(),
    now.day(), now.month(), now.year(),
    now.hour(), now.minute(), now.second(),
    releStateString, lastReceived == "1" ? "Alto" : "Baixo", flowRate);
    arquivo.close();
    }
    }
    void checkBoia() {
    if (message == "1") {
    releState = 2;
    }
    }
    void checaRele() {
    // Serial.print("Relestate: ");
    // Serial.println(releState);
    if (releState == 1) {
    digitalWrite(RELELIGA, LOW);
    } else if (releState == 2) {
    digitalWrite(RELEDESLIGA, LOW);
    }

    releState = 0;
    }

    void verifyFlow() {
    Serial.print("Flow na funcao: ");
    Serial.println(flowRate);
    if (flowRate >= 3.0 && releState == 0) {

    //ligaRele(RELELIGA);
    }
    if (flowRate <= 1.0 && releState == 1) {
    releState = 2;
    ligaRele(RELEDESLIGA);

    //flowRate = 0.0;
    }

    registraInformacao();
    }




    void inicializaDisplay() {
    pinMode(OLED_RST, OUTPUT);
    digitalWrite(OLED_RST, LOW);
    delay(20);
    digitalWrite(OLED_RST, HIGH);

    display.init();
    display.flipScreenVertically();
    display.clear();
    display.setFont(ArialMT_Plain_10);
    display.drawString(0, 0, "Inicializando...");
    display.display();

    Serial.println("Display OK");
    }
    void receiveMessage() {

    // Lê mensagem recebida
    while (LoRa.available()) {
    lastLora = millis();
    message += (char)LoRa.read();
    Serial.print("mensagem ");
    Serial.println(message);
    if (message == "1") {
    releState = 2;
    }
    }

    int rssi = LoRa.packetRssi();
    float snr = LoRa.packetSnr();

    lastReceived = message;
    lastRSSI = rssi;
    message = "";


    // Debug serial
    Serial.println("========================================");
    Serial.println("Mensagem recebida: " + message);
    Serial.println("RSSI: " + String(rssi) + " dBm");
    Serial.println("SNR: " + String(snr) + " dB");
    Serial.println("========================================");

    // Atualiza display
    //displayMessage("RECEBIDO", message, "RSSI: " + String(rssi) + "dBm");
    }
    void inicializaRTC() {
    Serial.print("Inicializando RTC DS3231... ");

    if (!rtc.begin(&I2C_RTC)) {
    Serial.println("RTC não encontrado!");
    rtcOk = false;
    return;
    }

    rtcOk = true;
    Serial.println("OK");

    // Verifica se RTC perdeu energia
    if (rtc.lostPower()) {
    Serial.println("RTC perdeu energia, configurando data/hora...");
    // Configura data/hora atual (ajuste conforme necessário)
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
    }

    // Mostra data/hora atual
    DateTime now = rtc.now();
    Serial.printf("Data/Hora atual: %02d/%02d/%04d %02d:%02d:%02d\n",
    now.day(), now.month(), now.year(),
    now.hour(), now.minute(), now.second());

    // Configurações do DS3231
    rtc.disable32K(); // Desabilita saída 32kHz
    rtc.clearAlarm(1); // Limpa alarme 1
    rtc.clearAlarm(2); // Limpa alarme 2
    rtc.writeSqwPinMode(DS3231_OFF); // Desabilita onda quadrada
    }

    void inicializaSD() {
    Serial.print("Inicializando SD Card... ");

    // Configura SPI customizado
    //sdSPI.begin(SD_SCK, SD_MISO, SD_MOSI, SD_CS);
    spiSD.begin(SD_SCK, SD_MISO, SD_MOSI, SD_CS);

    if (!SD.begin(SD_CS, spiSD)) {
    Serial.println("FALHOU!");
    sdOk = false;
    return;
    }

    sdOk = true;
    Serial.println("OK");

    // Mostra informações do cartão
    uint64_t cardSize = SD.cardSize() / (1024 * 1024);
    Serial.printf("Tamanho do cartão: %llu MB\n", cardSize);
    Serial.printf("Espaço usado: %llu MB\n", SD.usedBytes() / (1024 * 1024));
    }

    void gravaLogInicial() {
    DateTime now = rtc.now();

    File arquivo = SD.open("/system_log.txt", FILE_APPEND);
    if (!arquivo) {
    Serial.println("Erro ao abrir log do sistema");
    return;
    }


    arquivo.println("===============================");
    arquivo.printf("SISTEMA INICIADO: %02d/%02d/%04d %02d:%02d:%02d\n",
    now.day(), now.month(), now.year(),
    now.hour(), now.minute(), now.second());
    arquivo.printf("ESP32 ID: %08X\n", (uint32_t)ESP.getEfuseMac());
    arquivo.printf("RAM Livre: %d KB\n", ESP.getFreeHeap() / 1024);
    arquivo.printf("SD Card: %llu MB\n", SD.cardSize() / (1024 * 1024));
    arquivo.println("Status: RTC + SD funcionando");
    if (rede.length() > 0) {
    arquivo.printf("Rede: %s \n", rede);
    }
    if (ip.length() > 0) {
    arquivo.printf("IP: %s \n", ip);
    }
    arquivo.println("===============================");
    arquivo.close();

    Serial.println("Log inicial gravado com sucesso!");
    }

    void gravaDadosComTimestamp() {
    DateTime now = rtc.now();

    // Nome do arquivo baseado na data
    char nomeArquivo[32];
    sprintf(nomeArquivo, "/dados_%04d_%02d_%02d.csv",
    now.year(), now.month(), now.day());

    // Verifica se é um arquivo novo (precisa de cabeçalho)
    bool arquivoNovo = !SD.exists(nomeArquivo);

    File arquivo = SD.open(nomeArquivo, FILE_APPEND);
    if (!arquivo) {
    Serial.println("Erro ao abrir arquivo de dados");
    return;
    }

    // Adiciona cabeçalho se arquivo novo
    if (arquivoNovo) {
    arquivo.println("timestamp,data,hora,temperatura,umidade,bateria,ram_livre");
    }

    // Simula dados de sensores
    float temperatura = 20.0 + random(0, 150) / 10.0; // 20.0 a 35.0°C
    float umidade = 40.0 + random(0, 400) / 10.0; // 40.0 a 80.0%
    int bateria = random(70, 101); // 70% a 100%
    int ramLivre = ESP.getFreeHeap() / 1024; // KB

    // Grava dados
    arquivo.printf("%lu,%02d/%02d/%04d,%02d:%02d:%02d,%.1f,%.1f,%d,%d\n",
    now.unixtime(),
    now.day(), now.month(), now.year(),
    now.hour(), now.minute(), now.second(),
    temperatura, umidade, bateria, ramLivre);
    arquivo.close();

    Serial.printf("Dados gravados: %02d/%02d/%04d %02d:%02d:%02d",
    now.day(), now.month(), now.year(),
    now.hour(), now.minute(), now.second());
    Serial.println("");
    }

    void atualizaDisplay() {
    display.clear();

    // Status dos módulos
    display.drawString(0, 0, "Status Sistema:");
    display.drawString(0, 12, rtcOk ? "RTC: OK ✓" : "RTC: ERRO ✗");
    display.drawString(64, 12, sdOk ? "SD: OK ✓" : "SD: ERRO ✗");

    // Se RTC OK, mostra data/hora
    if (rtcOk) {
    DateTime now = rtc.now();
    //char dataHora[32];
    sprintf(dataHora, "%02d/%02d/%04d %02d:%02d:%02d",
    now.day(), now.month(), now.year(),
    now.hour(), now.minute(), now.second());
    display.drawString(0, 26, dataHora);
    }

    if (showNetworkInfo) {
    String boiaState = lastReceived == "1" ? "alto" : "baixo";
    if (!comunicacaoLora) {
    display.drawString(0, 38, "Boia sem comunicacao");
    } else {
    display.drawString(0, 38, "Boia em nivel " + boiaState);
    }


    // Mostra Wi-Fi e IP
    if (WiFi.status() == WL_CONNECTED) {
    //display.drawString(0, 26, "WiFi: " + WiFi.SSID());
    display.drawString(0, 50, "IP: " + WiFi.localIP().toString());
    }
    } else {
    if (WiFi.status() == WL_CONNECTED) {
    display.drawString(0, 50, "REDE: " + WiFi.SSID());
    }
    char flowText[32];
    sprintf(flowText, "Fluxo: %.1f L/min", flowRate);
    display.drawString(0, 38, flowText);
    }

    char flowText[32];
    sprintf(flowText, "Fluxo: %.1f L/min", flowRate);
    //Serial.println(flowText);
    display.display();
    boiaState = "";
    //lastReceived = "";
    }

    // Funções auxiliares para ajustar RTC manualmente
    void ajustarRTC(int ano, int mes, int dia, int hora, int minuto, int segundo) {
    rtc.adjust(DateTime(ano, mes, dia, hora, minuto, segundo));
    Serial.printf("RTC ajustado para: %02d/%02d/%04d %02d:%02d:%02d\n",
    dia, mes, ano, hora, minuto, segundo);
    }

    // Função para listar arquivos do SD
    void listarArquivos() {
    if (!sdOk) return;

    Serial.println("\n--- ARQUIVOS NO SD CARD ---");
    File root = SD.open("/");
    File arquivo = root.openNextFile();

    while (arquivo) {
    if (!arquivo.isDirectory()) {
    Serial.printf("%-20s %8d bytes\n", arquivo.name(), arquivo.size());
    }
    arquivo = root.openNextFile();
    }

    Serial.printf("Espaço total: %llu MB\n", SD.totalBytes() / (1024 * 1024));
    Serial.printf("Espaço usado: %llu MB\n", SD.usedBytes() / (1024 * 1024));
    }