#ifndef I2CService_h #define I2CService_h #include "SerialService.h" #include #include #include #include #include #include #include class I2CService { public: static constexpr uint8_t enderecoMux = 0x70; static constexpr uint8_t enderecoMcp = 0x20; static constexpr uint8_t enderecoAds = 0x48; struct EstadoQuarentena { uint8_t recuperacoesConfirmadas = 0; uint32_t inicioJanelaMs = 0; uint32_t ultimaOcorrenciaMs = 0; bool bloqueado = false; }; struct ContextoBarramento { bool valido = false; int idSensor = -1; int canalMuxSolicitado = -1; int canalMuxAtivo = -1; uint8_t enderecoADS = 0xFF; uint32_t inicioMs = 0; }; struct DiagnosticoCAN { bool i2cIniciado = false; bool muxIniciado = false; bool infraestruturaMuxOk = false; bool recoveryEmAndamento = false; bool recoveryPendente = false; bool sdaHigh = false; bool sclHigh = false; bool tcaRdyConfigurado = false; bool tcaReady = false; bool tcaEnConfigurado = false; bool tcaEnHigh = false; bool algumCanalMuxQuarentena = false; bool algumAdsQuarentena = false; uint32_t geracaoBarramento = 0; uint32_t totalRecoveries = 0; uint32_t falhasRecoveryConsecutivas = 0; uint32_t falhasTransacaoConsecutivas = 0; int ultimoIdFalha = -1; int ultimoCanalFalha = -1; uint8_t muxEsperadoMask = 0; uint8_t muxValidadoMask = 0; uint8_t mux0QuarentenaMask = 0; // ADS: // maxLeituraAdsMs = tempo total de parede do ciclo ADS. // maxInicioAdsMs = maior duracao da escrita que inicia a conversao. // maxResultadoAdsMs = maior duracao da leitura do registrador de conversao. // falhasIoAds = NACK/leitura incompleta/erro rapido que nao foi timeout. uint32_t maxLeituraAdsMs = 0; uint32_t maxInicioAdsMs = 0; uint32_t maxResultadoAdsMs = 0; uint32_t timeoutsAds = 0; uint32_t readyDropsAds = 0; uint32_t falhasIoAds = 0; uint32_t wireTimeoutMs = 0; // Recovery local: nao altera a geracao do Wire. uint32_t recoveriesLocaisMux = 0; uint32_t recoveriesLocaisAds = 0; uint32_t falhasLocaisDispositivos = 0; }; static DiagnosticoCAN ObterDiagnosticoCAN() { DiagnosticoCAN d; portENTER_CRITICAL(&_estadoLock); d.i2cIniciado = I2CIniciado; d.muxIniciado = MuxIniciado; d.recoveryEmAndamento = _recuperacaoEmAndamento; d.recoveryPendente = _recoveryPendente; d.geracaoBarramento = _geracaoBarramento; d.totalRecoveries = _totalRecoveries; d.falhasRecoveryConsecutivas = _falhasRecoveryConsecutivas; d.falhasTransacaoConsecutivas = _falhasTransacaoConsecutivas; d.ultimoIdFalha = _ultimoIdFalhaDiag; d.ultimoCanalFalha = _ultimoCanalFalhaDiag; d.muxEsperadoMask = _muxEsperadoMask; uint8_t validMask = 0; for (int i = 0; i < 8; i++) { if (_muxValidado[i]) { validMask |= (uint8_t)(1U << i); } } d.muxValidadoMask = validMask; d.maxLeituraAdsMs = _maxLeituraAdsMs; d.maxInicioAdsMs = _maxInicioAdsMs; d.maxResultadoAdsMs = _maxResultadoAdsMs; d.timeoutsAds = _timeoutsAds; d.readyDropsAds = _readyDropsAds; d.falhasIoAds = _falhasIoAds; d.recoveriesLocaisMux = _recoveriesLocaisMux; d.recoveriesLocaisAds = _recoveriesLocaisAds; d.falhasLocaisDispositivos = _falhasLocaisDispositivos; portEXIT_CRITICAL(&_estadoLock); d.infraestruturaMuxOk = (d.muxEsperadoMask & d.muxValidadoMask) == d.muxEsperadoMask; d.sdaHigh = _pinoSDA >= 0 && digitalRead(_pinoSDA) == HIGH; d.sclHigh = _pinoSCL >= 0 && digitalRead(_pinoSCL) == HIGH; d.tcaRdyConfigurado = _pinoTcaRdy >= 0; d.tcaReady = d.tcaRdyConfigurado && digitalRead(_pinoTcaRdy) == HIGH; d.tcaEnConfigurado = _pinoTcaEn >= 0; d.tcaEnHigh = d.tcaEnConfigurado && digitalRead(_pinoTcaEn) == HIGH; uint8_t quarentenaMask = 0; // SEN atual usa o primeiro MUX, 0x70. for (uint8_t canal = 0; canal < 8; canal++) { if (CanalMuxEmQuarentena(canal)) { quarentenaMask |= (uint8_t)(1U << canal); } } d.mux0QuarentenaMask = quarentenaMask; d.algumCanalMuxQuarentena = quarentenaMask != 0; d.algumAdsQuarentena = QuantidadeADSEmQuarentena() > 0; // getTimeOut() apenas devolve o valor configurado no objeto Wire. d.wireTimeoutMs = I2CIniciado ? Wire.getTimeOut() : 0; return d; } static bool I2CIniciado; static bool MuxIniciado; static bool McpIniciado; static bool AdsIniciado; // Mantidos publicos para compatibilidade com as firmwares atuais. static int canalAtivo; static int idAtualUsandoI2C; static bool i2cMutexReiniciando; static Adafruit_MCP23X17 mcp; static Adafruit_ADS1115 ads; /* * Todos os pinos extras sao opcionais. * * tcaRdy : READY open-drain do TCA4307 (entrada) * tcaEn : EN do TCA4307 (saida, ativo em HIGH) * muxRst : RESET_N do TCA9548A (saida, ativo em LOW) * adsPwrEn : enable do load switch que alimenta o ADS1115 * (saida, considerado ativo em HIGH) */ static bool DefinirPinos(int sda, int scl, int tcaRdy = -1, int tcaEn = -1, int muxRst = -1, int adsPwrEn = -1) { bool mudou = false; if (_pinoSDA != sda) { _pinoSDA = sda; mudou = true; } if (_pinoSCL != scl) { _pinoSCL = scl; mudou = true; } if (_pinoTcaRdy != tcaRdy) { _pinoTcaRdy = tcaRdy; mudou = true; } if (_pinoTcaEn != tcaEn) { _pinoTcaEn = tcaEn; mudou = true; } if (_pinoMuxRst != muxRst) { _pinoMuxRst = muxRst; mudou = true; } if (_pinoAdsPwrEn != adsPwrEn) { _pinoAdsPwrEn = adsPwrEn; mudou = true; } return mudou; } static void DefinirDebug(bool ativo) { DebugMode = ativo; } static void DefinirLimiteTempoI2C(uint32_t limiteMs) { LimiteTempoI2C = limiteMs; } // Cada Wire.begin() bem-sucedido cria uma nova geracao logica do barramento. // Drivers de sensores podem usar este valor para invalidar inicializacoes // feitas antes de um recovery completo. static uint32_t GeracaoBarramento() { uint32_t geracao; portENTER_CRITICAL(&_estadoLock); geracao = _geracaoBarramento; portEXIT_CRITICAL(&_estadoLock); return geracao; } static bool InfraestruturaMuxOk() { uint8_t esperados; portENTER_CRITICAL(&_estadoLock); esperados = _muxEsperadoMask; portEXIT_CRITICAL(&_estadoLock); if (esperados == 0) { return true; } for (int i = 0; i < 8; i++) { if ((esperados & (1U << i)) != 0 && !_muxValidado[i]) { return false; } } return true; } static bool IniciarI2C(bool forcar = false) { GarantirMutexes(); ConfigurarPinosControle(); // Serializa todo o lifecycle Wire.end()/Wire.begin() com os recoveries. if (xSemaphoreTake(lifecycleMutex, pdMS_TO_TICKS(500)) != pdTRUE) { MostrarLog("I2C", "Inicializacao adiada: lifecycle ocupado"); return false; } bool tomouI2C = false; bool resultado = false; do { if (xSemaphoreTake(i2cMutex, pdMS_TO_TICKS(300)) != pdTRUE) { MostrarLog("I2C", "Inicializacao adiada: barramento em uso"); break; } tomouI2C = true; DefinirEstadoRecovery(true, true); bool barramentoLivre = forcar || BarramentoFisicamenteLivre(); // Recuperacoes localizadas antes de desmontar o Wire. if (!barramentoLivre && _pinoMuxRst >= 0) { MostrarLog("I2C", "Barramento preso no inicio; resetando MUX"); ResetarMuxSePossivel(); barramentoLivre = AguardarBarramentoLivre(150); } if (!barramentoLivre && _pinoAdsPwrEn >= 0) { MostrarLog("I2C", "Barramento ainda preso; executando power-cycle do ADS"); ResetarADSSePossivel(); barramentoLivre = AguardarBarramentoLivre(150); } if (!barramentoLivre && _pinoTcaEn >= 0) { MostrarLog("I2C", "Barramento ainda preso; resetando TCA4307"); ResetarTCA4307(false); barramentoLivre = AguardarBarramentoLivre(200); } FinalizarWireAtual(); if (!barramentoLivre && (_pinoTcaEn < 0 || TCAReadyOk())) { MostrarLog("I2C", "Tentando destravamento manual SDA/SCL antes do Wire.begin"); DestravarBarramentoI2C(_pinoSDA, _pinoSCL); barramentoLivre = AguardarBarramentoLivre(100); } if (!barramentoLivre && !forcar) { MostrarLog("I2C", "Wire nao iniciado: linhas continuam ocupadas"); I2CIniciado = false; break; } resultado = IniciarWireNovo(); idAtualUsandoI2C = -1; tempoEntradaI2C = 0; _readyLowDesdeMs = 0; portENTER_CRITICAL(&_estadoLock); _falhasTransacaoConsecutivas = 0; _recoveryPendente = false; _motivoRecoveryPendente[0] = '\0'; portEXIT_CRITICAL(&_estadoLock); LimparContextoAtual(); LimparContextoFalha(); MarcarDispositivosParaRevalidacao(); MostrarLog( "I2C", "I2C inicializado | SDA=" + String(_pinoSDA) + " SCL=" + String(_pinoSCL) + " RDY=" + String(_pinoTcaRdy) + " EN=" + String(_pinoTcaEn) + " MUX_RST=" + String(_pinoMuxRst) + " ADS_PWR_EN=" + String(_pinoAdsPwrEn) + " linhas_livres=" + String(BarramentoFisicamenteLivre() ? 1 : 0) + " geracao=" + String(GeracaoBarramento()) + " res=" + String(resultado ? 1 : 0) ); } while (false); DefinirEstadoRecovery(false, false); if (tomouI2C) { xSemaphoreGive(i2cMutex); } xSemaphoreGive(lifecycleMutex); if (i2cTaskHandle == NULL) { xTaskCreatePinnedToCore( I2CService::i2cTaskWrapper, "i2cTask", 4096, nullptr, 2, &i2cTaskHandle, tskNO_AFFINITY ); } return resultado; } static void VerificarSaudeBarramento() { VerificarTCAReady(); VerificarI2CPreso(); VerificarRecoveryPendente(); } static bool TCA4307Configurado() { return _pinoTcaEn >= 0 || _pinoTcaRdy >= 0; } static bool TCA4307Ready() { return TCAReadyOk(); } static uint32_t TotalRecoveries() { return _totalRecoveries; } static uint32_t FalhasRecoveryConsecutivas() { return _falhasRecoveryConsecutivas; } static bool TentarRecuperar(const String& motivo, bool ignorarCooldown = false) { return RecuperarBarramento(motivo, ignorarCooldown); } /* * Falha de um dispositivo individual NAO e falha global do barramento. * * O proprio model do sensor continua responsavel por: * - contar falhas consecutivas; * - marcar Iniciado=false; * - tentar Inicializar() novamente. * * O I2CService apenas registra a degradacao local. Isso evita que um * unico sensor derrube a geracao de todos os outros. */ static void NotificarFalhaDispositivo(int idSensor, const String& motivo) { RegistrarFalhaLocalDispositivo(idSensor, motivo); } static void NotificarSucessoDispositivo(int idSensor) { RegistrarSucessoLocalDispositivo(idSensor); } /* * Use SOMENTE depois que o model do sensor esgotar suas tentativas locais * de reinicializacao. Esta e a ponte explicita entre recovery local e uma * nova geracao global. * * Nao chamar por amostra invalida isolada. */ static void NotificarFalhaPersistenteDispositivo( int idSensor, const String& motivo ) { RegistrarFalhaLocalDispositivo( idSensor, "persistente | " + motivo ); AgendarRecovery( "Dispositivo nao reviveu apos retries locais | ID " + String(idSensor) + " | " + motivo ); } /* * Por padrao mantemos o ultimo canal do MUX selecionado entre acessos. * No SEN isso e seguro porque os INAs downstream usam 0x40 e os * dispositivos do barramento principal usam outros enderecos. * * Isso reduz praticamente pela metade as escritas no TCA9548A. * Pode ser reativado para outras topologias que precisem isolar o ramal * ao final de cada acesso. */ static void DefinirDesabilitarMuxAoLiberar(bool ativo) { _desabilitarMuxAoLiberar = ativo; } static bool CanalMuxEmQuarentena(uint8_t canalGlobal) { uint8_t indiceMux = canalGlobal / 10; uint8_t canal = canalGlobal % 10; if (indiceMux > 7 || canal > 7) { return true; } const uint32_t agora = millis(); bool bloqueado = false; bool expirou = false; portENTER_CRITICAL(&_estadoLock); EstadoQuarentena& estado = _saudeCanaisMux[indiceMux][canal]; if ( estado.bloqueado && estado.ultimaOcorrenciaMs > 0 && (uint32_t)(agora - estado.ultimaOcorrenciaMs) >= JanelaQuarentenaMs ) { estado = EstadoQuarentena(); expirou = true; } bloqueado = estado.bloqueado; portEXIT_CRITICAL(&_estadoLock); if (expirou) { MostrarLog("MUX", "Quarentena expirada automaticamente | canalGlobal=" + String(canalGlobal)); } return bloqueado; } static uint8_t RecuperacoesConfirmadasCanalMux(uint8_t canalGlobal) { uint8_t indiceMux = canalGlobal / 10; uint8_t canal = canalGlobal % 10; if (indiceMux > 7 || canal > 7) { return 0; } (void)CanalMuxEmQuarentena(canalGlobal); uint8_t total; portENTER_CRITICAL(&_estadoLock); total = _saudeCanaisMux[indiceMux][canal].recuperacoesConfirmadas; portEXIT_CRITICAL(&_estadoLock); return total; } static bool ADSEmQuarentena(uint8_t endereco = enderecoAds) { int indice = IndiceADS(endereco); if (indice < 0) { return true; } const uint32_t agora = millis(); bool bloqueado = false; bool expirou = false; portENTER_CRITICAL(&_estadoLock); EstadoQuarentena& estado = _saudeADS[indice]; if ( estado.bloqueado && estado.ultimaOcorrenciaMs > 0 && (uint32_t)(agora - estado.ultimaOcorrenciaMs) >= JanelaQuarentenaMs ) { estado = EstadoQuarentena(); expirou = true; } bloqueado = estado.bloqueado; portEXIT_CRITICAL(&_estadoLock); if (expirou) { MostrarLog("ADS", "Quarentena expirada automaticamente | endereco=0x" + String(endereco, HEX)); } return bloqueado; } static uint8_t RecuperacoesConfirmadasADS(uint8_t endereco = enderecoAds) { int indice = IndiceADS(endereco); if (indice < 0) { return 0; } (void)ADSEmQuarentena(endereco); uint8_t total; portENTER_CRITICAL(&_estadoLock); total = _saudeADS[indice].recuperacoesConfirmadas; portEXIT_CRITICAL(&_estadoLock); return total; } static void DesbloquearCanalMux(uint8_t canalGlobal) { uint8_t indiceMux = canalGlobal / 10; uint8_t canal = canalGlobal % 10; if (indiceMux > 7 || canal > 7) { return; } portENTER_CRITICAL(&_estadoLock); _saudeCanaisMux[indiceMux][canal] = EstadoQuarentena(); portEXIT_CRITICAL(&_estadoLock); MostrarLog("MUX", "Quarentena removida | canalGlobal=" + String(canalGlobal)); } static void DesbloquearADS(uint8_t endereco = enderecoAds) { int indice = IndiceADS(endereco); if (indice < 0) { return; } portENTER_CRITICAL(&_estadoLock); _saudeADS[indice] = EstadoQuarentena(); portEXIT_CRITICAL(&_estadoLock); MostrarLog("ADS", "Quarentena removida | endereco=0x" + String(endereco, HEX)); } static void LimparTodasQuarentenas() { portENTER_CRITICAL(&_estadoLock); for (int mux = 0; mux < 8; mux++) { for (int canal = 0; canal < 8; canal++) { _saudeCanaisMux[mux][canal] = EstadoQuarentena(); } } for (int i = 0; i < 4; i++) { _saudeADS[i] = EstadoQuarentena(); } portEXIT_CRITICAL(&_estadoLock); MostrarLog("I2C", "Todas as quarentenas foram removidas"); } static String StatusBarramento() { bool recovery; bool pendente; uint32_t falhasTx; uint32_t geracao; uint8_t muxEsperados; portENTER_CRITICAL(&_estadoLock); recovery = _recuperacaoEmAndamento; pendente = _recoveryPendente; falhasTx = _falhasTransacaoConsecutivas; geracao = _geracaoBarramento; muxEsperados = _muxEsperadoMask; portEXIT_CRITICAL(&_estadoLock); String s = ""; s += "i2c=" + String(I2CIniciado ? 1 : 0); s += " gen=" + String(geracao); s += " usando=" + String(idAtualUsandoI2C); s += " recovery=" + String(recovery ? 1 : 0); s += " pendente=" + String(pendente ? 1 : 0); s += " fails_tx=" + String(falhasTx); s += " total_recovery=" + String(_totalRecoveries); s += " falhas_recovery=" + String(_falhasRecoveryConsecutivas); s += " mux=" + String(MuxIniciado ? 1 : 0); s += " mux_ok=" + String(InfraestruturaMuxOk() ? 1 : 0); s += " mux_mask=0x" + String(muxEsperados, HEX); s += " mux_addr=0x" + String(enderecoMuxAtivo, HEX); s += " canal=" + String(CanalGlobalAtivoAtual()); s += " SDA=" + String((_pinoSDA >= 0) ? digitalRead(_pinoSDA) : -1); s += " SCL=" + String((_pinoSCL >= 0) ? digitalRead(_pinoSCL) : -1); s += " q_mux=" + String(QuantidadeCanaisMuxEmQuarentena()); s += " q_ads=" + String(QuantidadeADSEmQuarentena()); s += " rec_local_mux=" + String(_recoveriesLocaisMux); s += " rec_local_ads=" + String(_recoveriesLocaisAds); s += " falhas_locais=" + String(_falhasLocaisDispositivos); if (_pinoTcaRdy >= 0) { s += " tca_rdy=" + String(digitalRead(_pinoTcaRdy) == HIGH ? 1 : 0); } else { s += " tca_rdy=-1"; } if (_pinoTcaEn >= 0) { s += " tca_en=" + String(digitalRead(_pinoTcaEn) == HIGH ? 1 : 0); } else { s += " tca_en=-1"; } return s; } static bool VerificaEnderecoBarramento(byte endereco, uint32_t timeoutMs = 50) { if (!I2CIniciado) { return false; } if (!TCAReadyOk()) { RegistrarFalhaTransacao("READY baixo ao verificar endereco 0x" + String(endereco, HEX)); return false; } unsigned long t0 = millis(); Wire.beginTransmission(endereco); byte error = Wire.endTransmission(); if ((uint32_t)(millis() - t0) > timeoutMs) { MostrarLog("I2C", "Timeout verificando endereco 0x" + String(endereco, HEX)); RegistrarFalhaTransacao("Timeout endereco 0x" + String(endereco, HEX)); return false; } if (error != 0) { RegistrarFalhaTransacao( "Endereco 0x" + String(endereco, HEX) + " respondeu erro " + String(error) ); return false; } RegistrarSucessoTransacao(); return true; } static bool VerificaEnderecoBarramentoPassivo(byte endereco, uint32_t timeoutMs = 60) { /* * IMPORTANTE: * esta função pressupõe que o chamador * já possui ownership do i2cMutex. * * Ausência de um periférico durante retry * NÃO é evidência de falha global. */ if (!I2CIniciado) { return false; } if (!TCAReadyOk()) { return false; } const uint32_t inicio = millis(); Wire.beginTransmission( endereco ); const uint8_t erro = Wire.endTransmission(); const uint32_t duracao = (uint32_t)( millis() - inicio ); if (erro != 0) { return false; } if (duracao >= timeoutMs) { return false; } return true; } static bool SolicitarAcessoI2C(int idSensor = 0, int canalMux = -1, uint32_t timeoutMs = 200) { if (!I2CIniciado) { MostrarLog("I2C", "Acesso negado ID " + String(idSensor) + " | I2C nao inicializado"); return false; } if (canalMux >= 0 && CanalMuxEmQuarentena((uint8_t)canalMux)) { MostrarLog( "I2C", "Acesso negado ID " + String(idSensor) + " | Canal MUX em quarentena: " + String(canalMux) ); return false; } if (RecoveryBloqueandoAcesso()) { MostrarLog("I2C", "Acesso negado ID " + String(idSensor) + " | Recovery em andamento"); return false; } if (!TCAReadyOk()) { MostrarLog("I2C", "Acesso negado ID " + String(idSensor) + " | TCA4307 READY baixo"); AgendarRecovery("READY baixo antes do acesso do ID " + String(idSensor)); return false; } GarantirMutex(); if (xSemaphoreTake(i2cMutex, pdMS_TO_TICKS(timeoutMs)) != pdTRUE) { // Contencao do mutex nao prova falha eletrica no I2C. O watchdog // do proprietario atual e quem decide se o barramento ficou preso. MostrarLog("I2C", "Timeout ao tentar acessar I2C | ID=" + String(idSensor)); return false; } // Uma recuperacao pode ter comecado entre as verificacoes acima e o // recebimento do mutex. if (RecoveryBloqueandoAcesso() || !I2CIniciado) { xSemaphoreGive(i2cMutex); return false; } idAtualUsandoI2C = idSensor; tempoEntradaI2C = millis(); PrepararContextoAtual(idSensor, canalMux); if (canalMux >= 0 && !SelecionarCanalMux((uint8_t)canalMux)) { MostrarLog( "I2C", "Falha ao selecionar canal mux " + String(canalMux) + " | ID=" + String(idSensor) ); String motivo; bool precisaRecuperar = ObterRecoveryPendente(motivo); if (precisaRecuperar) { CapturarContextoFalha(); } LimparContextoAtual(); idAtualUsandoI2C = -1; tempoEntradaI2C = 0; xSemaphoreGive(i2cMutex); if (precisaRecuperar) { RecuperarBarramento(motivo, true); } return false; } if (canalMux >= 0) { ConfirmarCanalMuxNoContexto(canalMux); } return true; } static void LiberarAcessoI2C(int idSensor = 0) { if (idSensor != idAtualUsandoI2C || i2cMutex == nullptr) { return; } String motivo; bool precisaRecuperar = ObterRecoveryPendente(motivo); ContextoBarramento contexto = _contextoAtual; if ( _desabilitarMuxAoLiberar && contexto.valido && contexto.canalMuxSolicitado >= 0 && !RecoveryPendente() ) { DesabilitarMuxAtualSemMutex(); precisaRecuperar = ObterRecoveryPendente(motivo); } if (precisaRecuperar) { CapturarContextoFalha(); DefinirMutexReiniciando(true); } LimparContextoAtual(); idAtualUsandoI2C = -1; tempoEntradaI2C = 0; xSemaphoreGive(i2cMutex); if (precisaRecuperar) { RecuperarBarramento(motivo, true); } } static void RecriarMutex() { // Em produto nao deletamos um mutex FreeRTOS em runtime porque outra // task pode estar aguardando nele. Se o handle sumiu, apenas o recriamos. GarantirMutexes(); MostrarLog("I2C", "Mutexes verificados; recriacao destrutiva desabilitada em runtime"); } static bool WirePodeFinalizar() { if (!I2CIniciado) { pinMode(_pinoSCL, INPUT_PULLUP); pinMode(_pinoSDA, INPUT_PULLUP); DelayMs(1); } return digitalRead(_pinoSCL) == HIGH && digitalRead(_pinoSDA) == HIGH; } static void DestravarBarramentoI2C(int sda, int scl) { MostrarLog("I2C", "Gerando pulsos manuais para destravar SDA/SCL"); pinMode(scl, OUTPUT_OPEN_DRAIN); pinMode(sda, INPUT_PULLUP); digitalWrite(scl, HIGH); delayMicroseconds(5); // 16 pulsos cobrem dispositivos que ficaram no meio de uma palavra // e acompanham a margem utilizada pelo TCA4307. for (int i = 0; i < 16; i++) { digitalWrite(scl, LOW); delayMicroseconds(5); digitalWrite(scl, HIGH); delayMicroseconds(5); if (digitalRead(sda) == HIGH && i >= 8) { break; } } // Condicao STOP manual. pinMode(sda, OUTPUT_OPEN_DRAIN); digitalWrite(sda, LOW); delayMicroseconds(5); digitalWrite(scl, HIGH); delayMicroseconds(5); digitalWrite(sda, HIGH); delayMicroseconds(5); pinMode(scl, INPUT_PULLUP); pinMode(sda, INPUT_PULLUP); DelayMs(5); } static int QuemEstaUsando() { return idAtualUsandoI2C; } static bool LeituraSegura(uint8_t addr, uint8_t* buffer, size_t qtd, uint8_t reg = 0xFF, uint32_t timeoutMs = 50) { if (!I2CIniciado || buffer == nullptr || qtd == 0) { return false; } if (!TCAReadyOk()) { RegistrarFalhaTransacao("READY baixo antes de leitura 0x" + String(addr, HEX)); return false; } unsigned long t0 = millis(); if (reg != 0xFF) { Wire.beginTransmission(addr); Wire.write(reg); int err = Wire.endTransmission(false); if (err != 0) { MostrarLog( "I2C", "Falha endTransmission leitura | addr=0x" + String(addr, HEX) + " err=" + String(err) ); RegistrarFalhaTransacao("Falha endTransmission leitura 0x" + String(addr, HEX)); return false; } } size_t received = Wire.requestFrom((int)addr, (int)qtd); if (received != qtd || (uint32_t)(millis() - t0) > timeoutMs) { MostrarLog("I2C", "Timeout ou leitura incompleta | addr=0x" + String(addr, HEX)); RegistrarFalhaTransacao("Timeout leitura 0x" + String(addr, HEX)); return false; } for (size_t i = 0; i < qtd; i++) { if (!Wire.available()) { MostrarLog("I2C", "Dados insuficientes | addr=0x" + String(addr, HEX)); RegistrarFalhaTransacao("Dados insuficientes leitura 0x" + String(addr, HEX)); return false; } buffer[i] = Wire.read(); } RegistrarSucessoTransacao(); return true; } static bool EscritaSegura(uint8_t addr, const uint8_t* dados, size_t qtd, uint32_t timeoutMs = 50) { if (!I2CIniciado || dados == nullptr || qtd == 0) { return false; } if (!TCAReadyOk()) { RegistrarFalhaTransacao("READY baixo antes de escrita 0x" + String(addr, HEX)); return false; } unsigned long t0 = millis(); Wire.beginTransmission(addr); for (size_t i = 0; i < qtd; i++) { Wire.write(dados[i]); } int resultado = Wire.endTransmission(); if ((uint32_t)(millis() - t0) > timeoutMs || resultado != 0) { MostrarLog( "I2C", "Falha escrita | addr=0x" + String(addr, HEX) + " err=" + String(resultado) ); RegistrarFalhaTransacao("Falha escrita 0x" + String(addr, HEX)); return false; } RegistrarSucessoTransacao(); return true; } static bool SelecionarCanalMux(uint8_t canalGlobal) { uint8_t indiceMux = canalGlobal / 10; uint8_t canal = canalGlobal % 10; if (indiceMux > 7 || canal > 7) { MostrarLog("MUX", "Canal global invalido: " + String(canalGlobal)); return false; } if (CanalMuxEmQuarentena(canalGlobal)) { MostrarLog("MUX", "Canal em quarentena: " + String(canalGlobal)); return false; } return TrocarCanalMux(enderecoMux + indiceMux, canal); } static bool EscreverMascaraMuxSemRegistrar( uint8_t endereco, uint8_t mascara, uint8_t& erro, uint32_t& duracaoMs ) { const uint32_t inicio = millis(); Wire.beginTransmission(endereco); Wire.write(mascara); erro = Wire.endTransmission(); duracaoMs = (uint32_t)(millis() - inicio); return erro == 0; } /* * Recovery local do TCA9548A. * * Reseta SOMENTE o MUX, revalida o endereco e volta a selecionar o * canal desejado. Nao desmonta Wire e nao altera a geracao. */ static bool RecuperarMuxLocalSemGeracao( uint8_t endereco, uint8_t canalMux ) { const int indiceMux = IndiceMux(endereco); if ( indiceMux < 0 || canalMux >= 8 ) { return false; } portENTER_CRITICAL(&_estadoLock); if (_recoveriesLocaisMux < UINT32_MAX) { _recoveriesLocaisMux++; } portEXIT_CRITICAL(&_estadoLock); MostrarLog( "MUX", "Recovery LOCAL | endereco=0x" + String(endereco, HEX) + " canal=" + String(canalMux) + " | geracao=" + String(GeracaoBarramento()) ); if (!ResetarMuxSePossivel()) { return false; } if (!TCAReadyOk()) { return false; } if ( !PingEnderecoSemRegistrar( endereco, 80 ) ) { return false; } _muxValidado[indiceMux] = true; MuxIniciado = true; uint8_t erro = 0xFF; uint32_t duracao = 0; const bool ok = EscreverMascaraMuxSemRegistrar( endereco, (uint8_t)(1U << canalMux), erro, duracao ); if (!ok) { MostrarLog( "MUX", "Recovery LOCAL nao conseguiu selecionar canal" " | erro=" + String(erro) + " duracao=" + String(duracao) + "ms" ); return false; } enderecoMuxAtivo = endereco; canalAtivo = canalMux; MostrarLog( "MUX", "Recovery LOCAL concluido" " | canal=" + String(canalMux) + " | geracao=" + String(GeracaoBarramento()) ); return true; } static bool TrocarCanalMux( uint8_t endereco, uint8_t canalMux ) { const int indiceMux = IndiceMux(endereco); if ( indiceMux < 0 || canalMux >= 8 ) { MostrarLog( "MUX", "Endereco ou canal invalido | endereco=0x" + String(endereco, HEX) + " canal=" + String(canalMux) ); return false; } portENTER_CRITICAL(&_estadoLock); _muxEsperadoMask |= (uint8_t)(1U << indiceMux); portEXIT_CRITICAL(&_estadoLock); const uint8_t canalGlobal = (uint8_t)( (indiceMux * 10) + canalMux ); if ( CanalMuxEmQuarentena( canalGlobal ) ) { MostrarLog( "MUX", "Canal bloqueado por quarentena" " | canalGlobal=" + String(canalGlobal) ); return false; } /* * Se o MUX ainda nao foi validado, primeiro tentamos uma descoberta * passiva. Ausencia momentanea NAO soma falha global. */ if (!_muxValidado[indiceMux]) { const bool encontrado = PingEnderecoSemRegistrar( endereco, 80 ); if (!encontrado) { if ( !RecuperarMuxLocalSemGeracao( endereco, canalMux ) ) { RegistrarFalhaTransacao( "MUX nao respondeu apos recovery local" ); return false; } RegistrarSucessoTransacao(); return true; } _muxValidado[indiceMux] = true; MuxIniciado = true; } if ( enderecoMuxAtivo == endereco && canalAtivo == canalMux ) { return true; } /* * SEN atual possui um unico TCA9548A. Este bloco preserva suporte a * multiplos MUXes: se um endereco diferente ficou selecionado, * desabilita-o antes de ativar o proximo. */ if ( enderecoMuxAtivo != 0xFF && enderecoMuxAtivo != endereco ) { uint8_t erroDesabilitar = 0xFF; uint32_t duracaoDesabilitar = 0; bool desabilitou = EscreverMascaraMuxSemRegistrar( enderecoMuxAtivo, 0x00, erroDesabilitar, duracaoDesabilitar ); if (!desabilitou) { MostrarLog( "MUX", "Falha ao desabilitar MUX anterior" " | endereco=0x" + String(enderecoMuxAtivo, HEX) + " erro=" + String(erroDesabilitar) + " duracao=" + String(duracaoDesabilitar) + "ms" ); RegistrarFalhaTransacao( "Falha ao desabilitar MUX anterior" ); return false; } enderecoMuxAtivo = 0xFF; canalAtivo = -1; } const int canalAnterior = CanalGlobalAtivoAtual(); uint8_t ultimoErro = 0xFF; uint32_t ultimaDuracao = 0; for ( uint8_t tentativa = 0; tentativa < TentativasTrocaMuxAntesResetLocal; tentativa++ ) { const bool ok = EscreverMascaraMuxSemRegistrar( endereco, (uint8_t)(1U << canalMux), ultimoErro, ultimaDuracao ); if (ok) { enderecoMuxAtivo = endereco; canalAtivo = canalMux; RegistrarSucessoTransacao(); return true; } MostrarLog( "MUX", "Falha local ao selecionar canal" " | anterior=" + String(canalAnterior) + " solicitado=" + String(canalGlobal) + " tentativa=" + String(tentativa + 1) + " erro=" + String(ultimoErro) + " duracao=" + String(ultimaDuracao) + "ms" " SDA=" + String(digitalRead(_pinoSDA)) + " SCL=" + String(digitalRead(_pinoSCL)) + " RDY=" + String(TCAReadyOk() ? 1 : 0) ); DelayMs(2); } /* * As tentativas simples falharam. Antes de tocar no Wire, recupera * SOMENTE o MUX e tenta novamente. */ if ( RecuperarMuxLocalSemGeracao( endereco, canalMux ) ) { RegistrarSucessoTransacao(); return true; } /* * Somente agora uma falha do MUX alimenta o contador GLOBAL. * Sao necessarias LimiteFalhasTransacaoRecovery ocorrencias * consecutivas deste nivel para gerar uma nova geracao. */ RegistrarFalhaTransacao( "MUX nao reviveu apos retries e reset local" ); return false; } static int SelecionarCanalADS(uint8_t canalGlobal) { if (canalGlobal > 33) { return -1; } uint8_t endereco = enderecoAds + (canalGlobal / 10); uint8_t canal = canalGlobal % 10; if (canal > 3) { return -1; } if (ADSEmQuarentena(endereco)) { MostrarLog("ADS", "ADS em quarentena | endereco=0x" + String(endereco, HEX)); return -1; } MarcarEnderecoADSNoContexto(endereco); return IniciarADSSemMutex(endereco) ? canal : -1; } static int RealizarLeituraADS(int canalGlobal, int leituras, int idSensor, int canalMux) { if (leituras <= 0) { return -1; } if (!SolicitarAcessoI2C(idSensor, canalMux)) { return -1; } const int canal = SelecionarCanalADS(canalGlobal); if (canal < 0) { LiberarAcessoI2C(idSensor); return -1; } int64_t soma = 0; int leiturasConcluidas = 0; bool leituraFalhou = false; for (int i = 0; i < leituras; i++) { int16_t leituraADC = 0; const bool leituraOk = LerADSSingleShotComTimeout(canal, leituraADC); if (!leituraOk) { TratarFalhaADSLocal( idSensor, "falha de transporte/conversao" ); leituraFalhou = true; break; } /* * Só chegamos aqui se: * * - conversão iniciou * - terminou no prazo * - TCA continuou READY * - resultado foi obtido */ RegistrarSucessoTransacao(); const int leitura12bits = map((int)leituraADC, 0, 32767, 0, 4095); soma += leitura12bits; leiturasConcluidas++; } if ( !leituraFalhou && leiturasConcluidas == leituras ) { ResetarFalhasAdsLocais(); RegistrarSucessoLocalDispositivo(idSensor); } if (RecoveryPendente()) { leituraFalhou = true; } LiberarAcessoI2C(idSensor); if (leituraFalhou || leiturasConcluidas != leituras || leiturasConcluidas == 0) { return -1; } return (int)(soma / leiturasConcluidas); } static void IniciarMUX(byte endereco = enderecoMux, bool Mandatorio = false) { if (!I2CIniciado) { MostrarLog("MUX", "Impossivel iniciar TCA9548A, I2C nao iniciado"); return; } int indiceMux = IndiceMux(endereco); if (indiceMux < 0) { MostrarLog("MUX", "Endereco de MUX invalido: 0x" + String(endereco, HEX)); return; } portENTER_CRITICAL(&_estadoLock); _muxEsperadoMask |= (uint8_t)(1U << indiceMux); portEXIT_CRITICAL(&_estadoLock); if (_muxValidado[indiceMux]) { MostrarLog("MUX", "TCA9548A ja iniciado em 0x" + String(endereco, HEX)); return; } while (true) { MostrarLog("MUX", "Iniciando TCA9548A em 0x" + String(endereco, HEX)); if (!SolicitarAcessoI2C(idSensorMux)) { MostrarLog("MUX", "Falha ao solicitar acesso ao I2C"); if (!Mandatorio) { return; } DelayMs(5000); continue; } bool iniciado = PingEnderecoSemRegistrar(endereco); if (iniciado) { _muxValidado[indiceMux] = true; MuxIniciado = true; enderecoMuxAtivo = 0xFF; canalAtivo = -1; } LiberarAcessoI2C(idSensorMux); if (iniciado) { MostrarLog("MUX", "TCA9548A iniciado em 0x" + String(endereco, HEX)); return; } MostrarLog("MUX", "TCA9548A nao encontrado em 0x" + String(endereco, HEX)); if (!Mandatorio) { MostrarLog("MUX", "TCA9548A nao mandatorio, seguindo fluxo"); return; } MostrarLog("MUX", "Conexao obrigatoria, tentando novamente em 5 segundos"); DelayMs(5000); } } static void IniciarMCP(byte endereco = enderecoMcp, bool Mandatorio = false) { if (!I2CIniciado) { MostrarLog("MCP", "Impossivel iniciar MCP23X17, I2C nao iniciado"); return; } if (McpIniciado) { MostrarLog("MCP", "MCP23X17 ja iniciado"); return; } while (true) { MostrarLog("MCP", "Iniciando MCP23X17 em 0x" + String(endereco, HEX)); if (!SolicitarAcessoI2C(idSensorMcp)) { MostrarLog("MCP", "Falha ao solicitar acesso ao I2C"); if (!Mandatorio) { return; } DelayMs(5000); continue; } McpIniciado = mcp.begin_I2C(endereco, &Wire); if (!McpIniciado) { RegistrarFalhaTransacao("Falha iniciar MCP23X17"); } else { RegistrarSucessoTransacao(); } LiberarAcessoI2C(idSensorMcp); if (McpIniciado) { MostrarLog("MCP", "MCP23X17 iniciado"); return; } MostrarLog("MCP", "Erro ao iniciar MCP23X17"); if (!Mandatorio) { MostrarLog("MCP", "MCP23X17 nao mandatorio, seguindo fluxo"); return; } MostrarLog("MCP", "Conexao obrigatoria, tentando novamente em 5 segundos"); DelayMs(5000); } } static bool IniciarADS(byte endereco = enderecoAds) { if (!I2CIniciado) return false; constexpr int idSensorAds = 202; if (!SolicitarAcessoI2C(idSensorAds, -1)) { return false; } MarcarEnderecoADSNoContexto(endereco); bool ok = IniciarADSSemMutex(endereco); LiberarAcessoI2C(idSensorAds); return ok; } private: static bool DebugMode; static int idSensorMux; static int idSensorMcp; static int _pinoSDA; static int _pinoSCL; static int _pinoTcaRdy; static int _pinoTcaEn; static int _pinoMuxRst; static int _pinoAdsPwrEn; static unsigned long tempoEntradaI2C; static unsigned long LimiteTempoI2C; static byte ultimoEnderecoADS; static uint8_t enderecoMuxAtivo; static bool _muxValidado[8]; static SemaphoreHandle_t i2cMutex; static SemaphoreHandle_t lifecycleMutex; static TaskHandle_t i2cTaskHandle; static bool _recuperacaoEmAndamento; static bool _recoveryPendente; static char _motivoRecoveryPendente[192]; static uint32_t _geracaoBarramento; static uint8_t _muxEsperadoMask; static uint32_t _falhasTransacaoConsecutivas; static uint32_t _falhasRecoveryConsecutivas; static uint32_t _totalRecoveries; static unsigned long _ultimoRecoveryMs; static unsigned long _readyLowDesdeMs; static EstadoQuarentena _saudeCanaisMux[8][8]; static EstadoQuarentena _saudeADS[4]; static ContextoBarramento _contextoAtual; static ContextoBarramento _contextoFalha; static portMUX_TYPE _estadoLock; // No core 2.0.11 existem caminhos do Wire que podem ignorar este // limite e retornar apenas ~1 s depois. Mantemos 25 ms como intencao do // driver, mas o recovery local evita transformar esse stall em reboot // global imediato. static constexpr uint32_t TimeoutWireMs = 25; static constexpr uint32_t LimiteReadyLowMs = 200; static constexpr uint32_t CooldownRecoveryMs = 1000; // O tempo total do ciclo ADS e apenas diagnostico: pode incluir // preempcao/escalonamento do FreeRTOS. Falha eletrica e julgada pelas // fases I2C individuais abaixo. static constexpr uint32_t LimiteLeituraADS_MS = 50; static constexpr uint32_t LimiteFaseADS_MS = 35; static constexpr uint32_t EsperaConversaoADS_MS = 10; static constexpr uint32_t LimiteFalhasTransacaoRecovery = 3; static constexpr uint32_t MaxFalhasRecoveryAntesRestart = 3; static constexpr uint32_t JanelaQuarentenaMs = 120000; static constexpr uint8_t LimiteRecuperacoesMesmoAlvo = 3; static constexpr uint32_t MargemHardRestartMs = 1500; static int _ultimoIdFalhaDiag; static int _ultimoCanalFalhaDiag; static uint32_t _maxLeituraAdsMs; static uint32_t _maxInicioAdsMs; static uint32_t _maxResultadoAdsMs; static uint32_t _timeoutsAds; static uint32_t _readyDropsAds; static uint32_t _falhasIoAds; // Saude/recovery local. Nenhum destes contadores muda a geracao. static uint32_t _recoveriesLocaisMux; static uint32_t _recoveriesLocaisAds; static uint32_t _falhasLocaisDispositivos; static uint8_t _falhasLocaisPorId[256]; static uint8_t _falhasAdsConsecutivas; static uint8_t _falhasRecoveryLocalAdsConsecutivas; static uint32_t _ultimoRecoveryLocalAdsMs; static bool _desabilitarMuxAoLiberar; static constexpr uint8_t LimiteFalhasAdsAntesRecoveryLocal = 3; static constexpr uint8_t LimiteFalhasRecoveryLocalAdsAntesGlobal = 2; static constexpr uint32_t CooldownRecoveryLocalAdsMs = 500; static constexpr uint8_t TentativasTrocaMuxAntesResetLocal = 2; static void MostrarLog(const String& componente, const String& mensagem) { if (DebugMode) { PrintTela("[" + componente + "] " + mensagem); } } static void DelayMs(uint32_t ms) { vTaskDelay(pdMS_TO_TICKS(ms)); } static bool TempoPassou(unsigned long inicio, uint32_t limiteMs) { return (uint32_t)(millis() - inicio) >= limiteMs; } static void GarantirMutex() { if (i2cMutex == nullptr) { i2cMutex = xSemaphoreCreateMutex(); } } static void GarantirMutexes() { GarantirMutex(); if (lifecycleMutex == nullptr) { lifecycleMutex = xSemaphoreCreateMutex(); } } static bool RecoveryEmAndamento() { bool valor; portENTER_CRITICAL(&_estadoLock); valor = _recuperacaoEmAndamento; portEXIT_CRITICAL(&_estadoLock); return valor; } static bool RecoveryPendente() { bool valor; portENTER_CRITICAL(&_estadoLock); valor = _recoveryPendente; portEXIT_CRITICAL(&_estadoLock); return valor; } static void DefinirMutexReiniciando(bool valor) { portENTER_CRITICAL(&_estadoLock); i2cMutexReiniciando = valor; portEXIT_CRITICAL(&_estadoLock); } static bool RecoveryBloqueandoAcesso() { bool valor; portENTER_CRITICAL(&_estadoLock); valor = _recuperacaoEmAndamento || i2cMutexReiniciando; portEXIT_CRITICAL(&_estadoLock); return valor; } static void DefinirEstadoRecovery(bool emAndamento, bool reiniciandoMutex) { portENTER_CRITICAL(&_estadoLock); _recuperacaoEmAndamento = emAndamento; i2cMutexReiniciando = reiniciandoMutex; portEXIT_CRITICAL(&_estadoLock); } static void IncrementarGeracaoBarramento() { portENTER_CRITICAL(&_estadoLock); if (_geracaoBarramento == UINT32_MAX) { _geracaoBarramento = 1; } else { _geracaoBarramento++; if (_geracaoBarramento == 0) { _geracaoBarramento = 1; } } portEXIT_CRITICAL(&_estadoLock); } static void FinalizarWireAtual() { if (I2CIniciado) { Wire.end(); I2CIniciado = false; DelayMs(5); } } static bool IniciarWireNovo() { MostrarLog("I2C", "Iniciando Wire..."); bool iniciou = Wire.begin(_pinoSDA, _pinoSCL); I2CIniciado = iniciou; if (!iniciou) { MostrarLog("I2C", "Wire.begin falhou"); return false; } Wire.setClock(100000); Wire.setTimeOut(TimeoutWireMs); IncrementarGeracaoBarramento(); MostrarLog( "I2C", "Wire configurado | clock=" + String(Wire.getClock()) + " timeout=" + String(Wire.getTimeOut()) + "ms" + " geracao=" + String(GeracaoBarramento()) ); return true; } static bool PingEnderecoSemRegistrar(uint8_t endereco, uint32_t timeoutMs = 50) { if (!I2CIniciado) { return false; } // Não toca no Wire enquanto o TCA4307 indicar // que os dois lados do barramento não estão prontos. if (!TCAReadyOk()) { return false; } const uint32_t inicio = millis(); Wire.beginTransmission(endereco); const uint8_t erro = Wire.endTransmission(); const uint32_t duracao = (uint32_t)(millis() - inicio); return erro == 0 && duracao <= timeoutMs; } static bool ValidarMuxEsperadosSemRegistrar() { uint8_t esperados; portENTER_CRITICAL(&_estadoLock); esperados = _muxEsperadoMask; portEXIT_CRITICAL(&_estadoLock); if (esperados == 0) { return true; } bool todosOk = true; bool algumOk = false; for (int i = 0; i < 8; i++) { if ((esperados & (1U << i)) == 0) { continue; } uint8_t endereco = enderecoMux + i; bool ok = PingEnderecoSemRegistrar(endereco, 60); _muxValidado[i] = ok; algumOk = algumOk || ok; todosOk = todosOk && ok; MostrarLog( "MUX", "Revalidacao pos-recovery | endereco=0x" + String(endereco, HEX) + " ok=" + String(ok ? 1 : 0) ); } MuxIniciado = algumOk; enderecoMuxAtivo = 0xFF; canalAtivo = -1; return todosOk; } static void ConfigurarPinosControle() { if (_pinoTcaEn >= 0) { pinMode(_pinoTcaEn, OUTPUT); digitalWrite(_pinoTcaEn, HIGH); } if (_pinoTcaRdy >= 0) { pinMode(_pinoTcaRdy, INPUT_PULLUP); } if (_pinoMuxRst >= 0) { pinMode(_pinoMuxRst, OUTPUT); digitalWrite(_pinoMuxRst, HIGH); } if (_pinoAdsPwrEn >= 0) { pinMode(_pinoAdsPwrEn, OUTPUT); digitalWrite(_pinoAdsPwrEn, HIGH); DelayMs(10); } } static bool TCAReadyOk() { return _pinoTcaRdy < 0 || digitalRead(_pinoTcaRdy) == HIGH; } static bool EsperarTCAReady(uint32_t timeoutMs) { if (_pinoTcaRdy < 0) { return true; } unsigned long inicio = millis(); while (!TempoPassou(inicio, timeoutMs)) { if (digitalRead(_pinoTcaRdy) == HIGH) { return true; } DelayMs(2); } return digitalRead(_pinoTcaRdy) == HIGH; } static bool ResetarTCA4307(bool logDetalhado = true) { if (_pinoTcaEn < 0) { if (logDetalhado) { MostrarLog("TCA4307", "EN nao configurado, reset indisponivel"); } return false; } if (logDetalhado) { MostrarLog("TCA4307", "Resetando TCA4307 via EN"); } digitalWrite(_pinoTcaEn, LOW); DelayMs(10); digitalWrite(_pinoTcaEn, HIGH); DelayMs(5); bool ready = EsperarTCAReady(200); if (logDetalhado) { MostrarLog("TCA4307", String("READY apos reset: ") + (ready ? "HIGH" : "LOW")); } return ready; } static bool ResetarMuxSePossivel() { enderecoMuxAtivo = 0xFF; canalAtivo = -1; if (_pinoMuxRst < 0) { return false; } MostrarLog("MUX", "Resetando TCA9548A via pino RESET_N"); digitalWrite(_pinoMuxRst, LOW); DelayMs(2); digitalWrite(_pinoMuxRst, HIGH); DelayMs(5); MuxIniciado = false; for (int i = 0; i < 8; i++) { _muxValidado[i] = false; } return true; } static bool ResetarADSSePossivel() { if (_pinoAdsPwrEn < 0) { return false; } MostrarLog("ADS", "Executando power-cycle do ADS1115"); digitalWrite(_pinoAdsPwrEn, LOW); DelayMs(30); digitalWrite(_pinoAdsPwrEn, HIGH); DelayMs(15); AdsIniciado = false; ultimoEnderecoADS = 0xFF; return true; } static void MarcarDispositivosParaRevalidacao() { canalAtivo = -1; enderecoMuxAtivo = 0xFF; MuxIniciado = false; McpIniciado = false; AdsIniciado = false; ultimoEnderecoADS = 0xFF; for (int i = 0; i < 8; i++) { _muxValidado[i] = false; } } static int IndiceMux(uint8_t endereco) { if (endereco < enderecoMux || endereco > enderecoMux + 7) { return -1; } return endereco - enderecoMux; } static int IndiceADS(uint8_t endereco) { if (endereco < enderecoAds || endereco > enderecoAds + 3) { return -1; } return endereco - enderecoAds; } static int CanalGlobalAtivoAtual() { int indiceMux = IndiceMux(enderecoMuxAtivo); if (indiceMux < 0 || canalAtivo < 0 || canalAtivo > 7) { return -1; } return (indiceMux * 10) + canalAtivo; } static void PrepararContextoAtual(int idSensor, int canalMuxSolicitado) { ContextoBarramento contexto; contexto.valido = true; contexto.idSensor = idSensor; contexto.canalMuxSolicitado = canalMuxSolicitado; // Uma transacao direta no barramento principal nao deve herdar // o ultimo canal que ficou selecionado no MUX. contexto.canalMuxAtivo = canalMuxSolicitado >= 0 ? CanalGlobalAtivoAtual() : -1; contexto.enderecoADS = 0xFF; contexto.inicioMs = millis(); portENTER_CRITICAL(&_estadoLock); _contextoAtual = contexto; portEXIT_CRITICAL(&_estadoLock); } static void ConfirmarCanalMuxNoContexto(int canalGlobal) { portENTER_CRITICAL(&_estadoLock); if (_contextoAtual.valido) { _contextoAtual.canalMuxAtivo = canalGlobal; } portEXIT_CRITICAL(&_estadoLock); } static void MarcarEnderecoADSNoContexto(uint8_t endereco) { portENTER_CRITICAL(&_estadoLock); if (_contextoAtual.valido) { _contextoAtual.enderecoADS = endereco; } portEXIT_CRITICAL(&_estadoLock); } static void CapturarContextoFalha() { portENTER_CRITICAL(&_estadoLock); if (_contextoAtual.valido) { _contextoFalha = _contextoAtual; } portEXIT_CRITICAL(&_estadoLock); } static ContextoBarramento ObterContextoFalha() { ContextoBarramento contexto; portENTER_CRITICAL(&_estadoLock); if (_contextoFalha.valido) { contexto = _contextoFalha; } else { contexto = _contextoAtual; } portEXIT_CRITICAL(&_estadoLock); return contexto; } static void LimparContextoAtual() { portENTER_CRITICAL(&_estadoLock); _contextoAtual = ContextoBarramento(); portEXIT_CRITICAL(&_estadoLock); } static void LimparContextoFalha() { portENTER_CRITICAL(&_estadoLock); _contextoFalha = ContextoBarramento(); portEXIT_CRITICAL(&_estadoLock); } static int CanalSuspeitoDoContexto(const ContextoBarramento& contexto) { if (contexto.canalMuxAtivo >= 0) { return contexto.canalMuxAtivo; } return contexto.canalMuxSolicitado; } static bool BarramentoFisicamenteLivre() { return TCAReadyOk() && WirePodeFinalizar(); } static bool AguardarBarramentoLivre(uint32_t timeoutMs) { uint32_t inicio = millis(); while (!TempoPassou(inicio, timeoutMs)) { if (BarramentoFisicamenteLivre()) { return true; } DelayMs(2); } return BarramentoFisicamenteLivre(); } static void AgendarRecovery(const String& motivo) { CapturarContextoFalha(); ContextoBarramento contexto = ObterContextoFalha(); int canalSuspeito = CanalSuspeitoDoContexto( contexto ); portENTER_CRITICAL(&_estadoLock); if (contexto.valido) { _ultimoIdFalhaDiag = contexto.idSensor; _ultimoCanalFalhaDiag = canalSuspeito; } _recoveryPendente = true; strncpy( _motivoRecoveryPendente, motivo.c_str(), sizeof(_motivoRecoveryPendente) - 1 ); _motivoRecoveryPendente[ sizeof(_motivoRecoveryPendente) - 1 ] = '\0'; portEXIT_CRITICAL(&_estadoLock); } static bool ObterRecoveryPendente(String& motivo) { char buffer[sizeof(_motivoRecoveryPendente)]; bool pendente; portENTER_CRITICAL(&_estadoLock); pendente = _recoveryPendente; strncpy(buffer, _motivoRecoveryPendente, sizeof(buffer) - 1); buffer[sizeof(buffer) - 1] = '\0'; portEXIT_CRITICAL(&_estadoLock); motivo = String(buffer); return pendente; } static void LimparRecoveryPendente() { portENTER_CRITICAL(&_estadoLock); _recoveryPendente = false; _motivoRecoveryPendente[0] = '\0'; portEXIT_CRITICAL(&_estadoLock); } static void RegistrarResponsavelCanalMux(uint8_t canalGlobal) { uint8_t indiceMux = canalGlobal / 10; uint8_t canal = canalGlobal % 10; if (indiceMux > 7 || canal > 7) { return; } uint32_t agora = millis(); uint8_t total; bool bloqueado; portENTER_CRITICAL(&_estadoLock); EstadoQuarentena& estado = _saudeCanaisMux[indiceMux][canal]; if ( estado.inicioJanelaMs == 0 || (uint32_t)(agora - estado.inicioJanelaMs) > JanelaQuarentenaMs ) { estado.recuperacoesConfirmadas = 0; estado.inicioJanelaMs = agora; } if (estado.recuperacoesConfirmadas < 255) { estado.recuperacoesConfirmadas++; } estado.ultimaOcorrenciaMs = agora; if (estado.recuperacoesConfirmadas >= LimiteRecuperacoesMesmoAlvo) { estado.bloqueado = true; } total = estado.recuperacoesConfirmadas; bloqueado = estado.bloqueado; portEXIT_CRITICAL(&_estadoLock); MostrarLog( "MUX", "Recovery atribuido ao canal " + String(canalGlobal) + " | ocorrencias=" + String(total) + " | quarentena=" + String(bloqueado ? "SIM" : "NAO") ); } static void RegistrarResponsavelADS(uint8_t endereco) { int indice = IndiceADS(endereco); if (indice < 0) { return; } uint32_t agora = millis(); uint8_t total; bool bloqueado; portENTER_CRITICAL(&_estadoLock); EstadoQuarentena& estado = _saudeADS[indice]; if ( estado.inicioJanelaMs == 0 || (uint32_t)(agora - estado.inicioJanelaMs) > JanelaQuarentenaMs ) { estado.recuperacoesConfirmadas = 0; estado.inicioJanelaMs = agora; } if (estado.recuperacoesConfirmadas < 255) { estado.recuperacoesConfirmadas++; } estado.ultimaOcorrenciaMs = agora; if (estado.recuperacoesConfirmadas >= LimiteRecuperacoesMesmoAlvo) { estado.bloqueado = true; } total = estado.recuperacoesConfirmadas; bloqueado = estado.bloqueado; portEXIT_CRITICAL(&_estadoLock); MostrarLog( "ADS", "Recovery atribuido ao endereco 0x" + String(endereco, HEX) + " | ocorrencias=" + String(total) + " | quarentena=" + String(bloqueado ? "SIM" : "NAO") ); } static uint8_t QuantidadeCanaisMuxEmQuarentena() { const uint32_t agora = millis(); uint8_t total = 0; portENTER_CRITICAL(&_estadoLock); for (int mux = 0; mux < 8; mux++) { for (int canal = 0; canal < 8; canal++) { EstadoQuarentena& estado = _saudeCanaisMux[mux][canal]; if ( estado.bloqueado && estado.ultimaOcorrenciaMs > 0 && (uint32_t)(agora - estado.ultimaOcorrenciaMs) >= JanelaQuarentenaMs ) { estado = EstadoQuarentena(); } if (estado.bloqueado) { total++; } } } portEXIT_CRITICAL(&_estadoLock); return total; } static uint8_t QuantidadeADSEmQuarentena() { const uint32_t agora = millis(); uint8_t total = 0; portENTER_CRITICAL(&_estadoLock); for (int i = 0; i < 4; i++) { EstadoQuarentena& estado = _saudeADS[i]; if ( estado.bloqueado && estado.ultimaOcorrenciaMs > 0 && (uint32_t)(agora - estado.ultimaOcorrenciaMs) >= JanelaQuarentenaMs ) { estado = EstadoQuarentena(); } if (estado.bloqueado) { total++; } } portEXIT_CRITICAL(&_estadoLock); return total; } static void RegistrarFalhaLocalDispositivo( int idSensor, const String& motivo ) { uint8_t consecutivas = 0; portENTER_CRITICAL(&_estadoLock); if (_falhasLocaisDispositivos < UINT32_MAX) { _falhasLocaisDispositivos++; } if (idSensor >= 0 && idSensor <= 255) { uint8_t& valor = _falhasLocaisPorId[(uint8_t)idSensor]; if (valor < 255) { valor++; } consecutivas = valor; } portEXIT_CRITICAL(&_estadoLock); MostrarLog( "I2C/LOCAL", "ID " + String(idSensor) + " | falha local #" + String(consecutivas) + " | " + motivo ); } static void RegistrarSucessoLocalDispositivo(int idSensor) { if (idSensor < 0 || idSensor > 255) { return; } portENTER_CRITICAL(&_estadoLock); _falhasLocaisPorId[(uint8_t)idSensor] = 0; portEXIT_CRITICAL(&_estadoLock); } static void ResetarFalhasAdsLocais() { portENTER_CRITICAL(&_estadoLock); _falhasAdsConsecutivas = 0; _falhasRecoveryLocalAdsConsecutivas = 0; portEXIT_CRITICAL(&_estadoLock); } static void RegistrarFalhaAdsConsecutiva() { portENTER_CRITICAL(&_estadoLock); if (_falhasAdsConsecutivas < 255) { _falhasAdsConsecutivas++; } portEXIT_CRITICAL(&_estadoLock); } static uint8_t FalhasAdsConsecutivas() { uint8_t valor; portENTER_CRITICAL(&_estadoLock); valor = _falhasAdsConsecutivas; portEXIT_CRITICAL(&_estadoLock); return valor; } /* * Recovery local do ADS: * ADS power-cycle -> ping -> begin -> gain/rate * * Nao toca em Wire.end()/Wire.begin(), logo NAO altera a geracao e * nao invalida INAs/MUX/outros sensores. * * Esta funcao e chamada com ownership do i2cMutex. */ static bool RecuperarADSLocalSemGeracao() { const uint32_t agora = millis(); if ( _ultimoRecoveryLocalAdsMs > 0 && !TempoPassou( _ultimoRecoveryLocalAdsMs, CooldownRecoveryLocalAdsMs ) ) { return false; } _ultimoRecoveryLocalAdsMs = agora; portENTER_CRITICAL(&_estadoLock); if (_recoveriesLocaisAds < UINT32_MAX) { _recoveriesLocaisAds++; } portEXIT_CRITICAL(&_estadoLock); MostrarLog( "ADS", "Recovery LOCAL iniciado | sem alterar geracao" ); AdsIniciado = false; ultimoEnderecoADS = 0xFF; if (_pinoAdsPwrEn >= 0) { ResetarADSSePossivel(); } else { DelayMs(2); } const bool pingOk = PingEnderecoSemRegistrar( enderecoAds, 80 ); const bool iniciou = pingOk && IniciarADSSemMutex( enderecoAds ); if (iniciou) { ResetarFalhasAdsLocais(); MostrarLog( "ADS", "Recovery LOCAL concluido | geracao=" + String(GeracaoBarramento()) ); return true; } uint8_t falhasRecovery; portENTER_CRITICAL(&_estadoLock); if ( _falhasRecoveryLocalAdsConsecutivas < 255 ) { _falhasRecoveryLocalAdsConsecutivas++; } falhasRecovery = _falhasRecoveryLocalAdsConsecutivas; portEXIT_CRITICAL(&_estadoLock); MostrarLog( "ADS", "Recovery LOCAL falhou | tentativas=" + String(falhasRecovery) ); if ( falhasRecovery >= LimiteFalhasRecoveryLocalAdsAntesGlobal ) { AgendarRecovery( "ADS nao reviveu apos recoveries locais" ); } return false; } static void TratarFalhaADSLocal(int idSensor, const String& motivo) { RegistrarFalhaLocalDispositivo( idSensor, "ADS | " + motivo ); RegistrarFalhaAdsConsecutiva(); if ( FalhasAdsConsecutivas() >= LimiteFalhasAdsAntesRecoveryLocal ) { (void)RecuperarADSLocalSemGeracao(); } } static void RegistrarFalhaTransacao(const String& motivo) { uint32_t falhas; portENTER_CRITICAL(&_estadoLock); if (_falhasTransacaoConsecutivas < UINT32_MAX) { _falhasTransacaoConsecutivas++; } falhas = _falhasTransacaoConsecutivas; portEXIT_CRITICAL(&_estadoLock); MostrarLog( "I2C", "Falha transacao #" + String(falhas) + " | " + motivo ); if (falhas >= LimiteFalhasTransacaoRecovery) { AgendarRecovery("Falhas consecutivas no I2C: " + motivo); } } static void RegistrarSucessoTransacao() { portENTER_CRITICAL(&_estadoLock); _falhasTransacaoConsecutivas = 0; portEXIT_CRITICAL(&_estadoLock); } static bool RecuperarBarramento(const String& motivo, bool ignorarCooldown = false) { GarantirMutexes(); // Um unico dono pode desmontar/recriar Wire por vez. if (xSemaphoreTake(lifecycleMutex, 0) != pdTRUE) { AgendarRecovery(motivo); return false; } bool tomouI2C = false; bool retorno = false; do { const uint32_t agora = millis(); if ( !ignorarCooldown && _ultimoRecoveryMs > 0 && !TempoPassou(_ultimoRecoveryMs, CooldownRecoveryMs) ) { AgendarRecovery(motivo); break; } DefinirEstadoRecovery(true, true); _ultimoRecoveryMs = agora; _totalRecoveries++; MostrarLog("I2C", "Recovery iniciado | Motivo: " + motivo); if (xSemaphoreTake(i2cMutex, pdMS_TO_TICKS(150)) != pdTRUE) { MostrarLog("I2C", "Recovery adiado porque o mutex ainda esta ocupado"); AgendarRecovery(motivo); break; } tomouI2C = true; ContextoBarramento contexto = ObterContextoFalha(); int canalSuspeito = CanalSuspeitoDoContexto(contexto); bool travadoInicialmente = !BarramentoFisicamenteLivre(); bool barramentoLivre = !travadoInicialmente; bool responsavelConfirmado = false; // 1) Isola o ramal mais provavel primeiro. if (travadoInicialmente && canalSuspeito >= 0 && _pinoMuxRst >= 0) { ResetarMuxSePossivel(); barramentoLivre = AguardarBarramentoLivre(150); if (barramentoLivre) { RegistrarResponsavelCanalMux((uint8_t)canalSuspeito); responsavelConfirmado = true; } } // 2) Mantem suporte legado ao ADS quando houver alimentacao controlavel. if (!barramentoLivre && contexto.enderecoADS != 0xFF && _pinoAdsPwrEn >= 0) { ResetarADSSePossivel(); barramentoLivre = AguardarBarramentoLivre(150); if (barramentoLivre) { RegistrarResponsavelADS(contexto.enderecoADS); responsavelConfirmado = true; } } // Falha logica sem linha presa: reinicia o alvo relacionado, mas nao // atribui quarentena sem evidencia eletrica. if (!travadoInicialmente) { if (canalSuspeito >= 0 && _pinoMuxRst >= 0) { ResetarMuxSePossivel(); } else if (contexto.enderecoADS != 0xFF && _pinoAdsPwrEn >= 0) { ResetarADSSePossivel(); } barramentoLivre = AguardarBarramentoLivre(100); } // 3) Se existir TCA4307, usa a camada de isolamento/buffer global. if (!barramentoLivre && _pinoTcaEn >= 0) { ResetarTCA4307(true); barramentoLivre = AguardarBarramentoLivre(200); } FinalizarWireAtual(); // 4) Ultimo fallback eletrico antes de recriar o controlador I2C. if (!barramentoLivre && (_pinoTcaEn < 0 || TCAReadyOk())) { DestravarBarramentoI2C(_pinoSDA, _pinoSCL); barramentoLivre = AguardarBarramentoLivre(100); } bool wireReiniciado = false; if (barramentoLivre) { wireReiniciado = IniciarWireNovo(); } MarcarDispositivosParaRevalidacao(); // Primeiro criterio: o barramento-base precisa estar realmente vivo. bool baseOk = wireReiniciado && TCAReadyOk() && BarramentoFisicamenteLivre(); // Segundo criterio: se algum MUX ja foi declarado parte da topologia, // ele precisa responder para chamarmos o recovery de completo. bool muxOk = false; if (baseOk) { muxOk = ValidarMuxEsperadosSemRegistrar(); } bool infraestruturaOk = baseOk && muxOk; _readyLowDesdeMs = 0; idAtualUsandoI2C = -1; tempoEntradaI2C = 0; LimparContextoAtual(); portENTER_CRITICAL(&_estadoLock); _falhasTransacaoConsecutivas = 0; portEXIT_CRITICAL(&_estadoLock); if (infraestruturaOk) { LimparRecoveryPendente(); _falhasRecoveryConsecutivas = 0; LimparContextoFalha(); retorno = true; MostrarLog( "I2C", String("Recovery concluido com sucesso") + (responsavelConfirmado ? " | responsavel identificado" : " | responsavel nao confirmado") + " | geracao=" + String(GeracaoBarramento()) ); } else if (baseOk) { // Wire esta saudavel, mas a infraestrutura downstream nao. // Nao reinicia o ESP: deixa o modulo vivo e permite retry de MUX/sensores. LimparRecoveryPendente(); _falhasRecoveryConsecutivas = 0; LimparContextoFalha(); retorno = false; MostrarLog( "I2C", "Recovery restaurou o barramento-base, mas MUX esperado nao respondeu; " "mantendo SEN ativo para revalidacao posterior" ); } else { AgendarRecovery("Nova tentativa apos recovery global sem sucesso"); _falhasRecoveryConsecutivas++; MostrarLog( "I2C", "Recovery global falhou | falhas_consecutivas=" + String(_falhasRecoveryConsecutivas) ); // Restart completo fica reservado a falha do barramento-base, // nunca a simples ausencia de um periferico/MUX. if (_falhasRecoveryConsecutivas >= MaxFalhasRecoveryAntesRestart) { if (tomouI2C) { xSemaphoreGive(i2cMutex); tomouI2C = false; } DefinirEstadoRecovery(false, false); xSemaphoreGive(lifecycleMutex); MostrarLog("I2C", "Barramento-base irrecuperavel; reiniciando ESP"); DelayMs(100); ESP.restart(); return false; } } } while (false); if (tomouI2C) { xSemaphoreGive(i2cMutex); } DefinirEstadoRecovery(false, false); xSemaphoreGive(lifecycleMutex); return retorno; } static void VerificarTCAReady() { if (_pinoTcaRdy < 0 || RecoveryEmAndamento()) { return; } bool ready = digitalRead(_pinoTcaRdy) == HIGH; if (ready) { _readyLowDesdeMs = 0; return; } if (_readyLowDesdeMs == 0) { _readyLowDesdeMs = millis(); return; } if (!TempoPassou(_readyLowDesdeMs, LimiteReadyLowMs)) { return; } String motivo = "TCA4307 READY baixo por " + String(LimiteReadyLowMs) + "ms"; if (idAtualUsandoI2C >= 0) { AgendarRecovery(motivo); return; } AgendarRecovery(motivo); } static void VerificarI2CPreso() { if (RecoveryEmAndamento() || idAtualUsandoI2C < 0) { return; } const uint32_t tempoOwnership = (uint32_t)(millis() - tempoEntradaI2C); if (tempoOwnership <= LimiteTempoI2C) { return; } /* * tempoEntradaI2C mede ownership logico do service, nao uma unica * transferencia Wire. Esse intervalo pode incluir: * * - espera da conversao ADS; * - varias leituras do mesmo sensor; * - preempcao/escalonamento da task. * * Portanto ownership longo, sozinho, NAO prova falha eletrica. * So agenda recovery se tambem houver evidencia fisica no barramento. */ const bool readyOk = TCAReadyOk(); const bool linhasLivres = WirePodeFinalizar(); const bool evidenciaEletrica = !readyOk || !linhasLivres; if (!RecoveryPendente()) { if (evidenciaEletrica) { String motivo = "Ownership I2C prolongado com barramento ocupado | ID " + String(idAtualUsandoI2C) + " | tempo=" + String(tempoOwnership) + "ms" + " | READY=" + String(readyOk ? 1 : 0) + " | SDA=" + String(digitalRead(_pinoSDA)) + " | SCL=" + String(digitalRead(_pinoSCL)); MostrarLog("I2C", motivo); AgendarRecovery(motivo); } else { /* * Barramento fisicamente livre: pode ser task preemptada, * processamento lento ou espera por lock interno de software. * Nao derruba toda a infraestrutura por isso. */ MostrarLog( "I2C", "Ownership prolongado sem evidencia eletrica | ID=" + String(idAtualUsandoI2C) + " tempo=" + String(tempoOwnership) + "ms" ); } } /* * Mesmo com linhas livres, ownership que nunca retorna pode significar * deadlock de software/lock interno do Wire. Mantemos o hard restart * como ultimo recurso, sem tentar deletar mutex em uso. */ if ( tempoOwnership > LimiteTempoI2C + MargemHardRestartMs ) { MostrarLog( "I2C", "Task dona do I2C nao retornou; reiniciando ESP por seguranca" ); DelayMs(100); ESP.restart(); } } static void VerificarRecoveryPendente() { if (RecoveryEmAndamento() || idAtualUsandoI2C >= 0) { return; } String motivo; if (!ObterRecoveryPendente(motivo)) { return; } if (motivo.length() == 0) { motivo = "Recovery pendente"; } RecuperarBarramento(motivo, false); } static void i2cTaskWrapper(void *pvParameters) { (void)pvParameters; I2CService::i2cTask(); } static void i2cTask() { while (true) { VerificarSaudeBarramento(); DelayMs(50); } } static bool DesabilitarMuxAtualSemMutex() { if (enderecoMuxAtivo == 0xFF) { MostrarLog("MUX", "Nenhum MUX marcado como ativo"); canalAtivo = -1; return true; } const uint8_t endereco = enderecoMuxAtivo; const int canalAnterior = canalAtivo; uint32_t inicio = millis(); Wire.beginTransmission(endereco); Wire.write((uint8_t)0x00); int erro = Wire.endTransmission(); uint32_t duracao = (uint32_t)(millis() - inicio); MostrarLog( "MUX", "Desabilitar canais" " | anterior=" + String(canalAnterior) + " erro=" + String(erro) + " duracao=" + String(duracao) + "ms" + " SDA=" + String(digitalRead(_pinoSDA)) + " SCL=" + String(digitalRead(_pinoSCL)) + " RDY=" + String(TCAReadyOk() ? 1 : 0) ); if (erro != 0) { RegistrarFalhaTransacao("Falha ao desabilitar canais do MUX"); return false; } enderecoMuxAtivo = 0xFF; canalAtivo = -1; RegistrarSucessoTransacao(); return true; } static bool IniciarADSSemMutex(byte endereco) { if (!I2CIniciado) return false; if (ADSEmQuarentena(endereco)) return false; if (AdsIniciado && endereco == ultimoEnderecoADS) { return true; } // Descoberta passiva. if (!PingEnderecoSemRegistrar(endereco)) { AdsIniciado = false; ultimoEnderecoADS = 0xFF; return false; } AdsIniciado = ads.begin(endereco, &Wire); if (!AdsIniciado) { ultimoEnderecoADS = 0xFF; return false; } ads.setGain(GAIN_ONE); ads.setDataRate(RATE_ADS1115_128SPS); ultimoEnderecoADS = endereco; return true; } static void AtualizarMaxDuracaoADS(uint32_t inicioMs) { const uint32_t duracao = (uint32_t)(millis() - inicioMs); portENTER_CRITICAL(&_estadoLock); if (duracao > _maxLeituraAdsMs) { _maxLeituraAdsMs = duracao; } portEXIT_CRITICAL(&_estadoLock); } static void AtualizarMaxFaseInicioADS(uint32_t duracaoMs) { portENTER_CRITICAL(&_estadoLock); if (duracaoMs > _maxInicioAdsMs) { _maxInicioAdsMs = duracaoMs; } portEXIT_CRITICAL(&_estadoLock); } static void AtualizarMaxFaseResultadoADS(uint32_t duracaoMs) { portENTER_CRITICAL(&_estadoLock); if (duracaoMs > _maxResultadoAdsMs) { _maxResultadoAdsMs = duracaoMs; } portEXIT_CRITICAL(&_estadoLock); } static void IncrementarTimeoutADS() { portENTER_CRITICAL(&_estadoLock); if (_timeoutsAds < UINT32_MAX) { _timeoutsAds++; } portEXIT_CRITICAL(&_estadoLock); } static void IncrementarFalhaIoADS() { portENTER_CRITICAL(&_estadoLock); if (_falhasIoAds < UINT32_MAX) { _falhasIoAds++; } portEXIT_CRITICAL(&_estadoLock); } static void RegistrarTimeoutADS(uint32_t inicioMs, const char* motivo) { AtualizarMaxDuracaoADS(inicioMs); IncrementarTimeoutADS(); // Timeout isolado do ADS e degradacao LOCAL. MostrarLog( "ADS", String(motivo) + " | tratado como falha local" ); } static void RegistrarReadyDropADS(uint32_t inicioMs) { AtualizarMaxDuracaoADS(inicioMs); portENTER_CRITICAL(&_estadoLock); if (_readyDropsAds < UINT32_MAX) { _readyDropsAds++; } portEXIT_CRITICAL(&_estadoLock); // READY pertence a infraestrutura global do barramento. RegistrarFalhaTransacao("READY caiu durante leitura ADS"); } static void RegistrarFalhaIoADS( uint32_t inicioMs, const char* etapa, int erro, uint32_t duracaoMs ) { AtualizarMaxDuracaoADS(inicioMs); IncrementarFalhaIoADS(); // Erro de I/O do ADS continua local. O chamador decide quando // tentar power-cycle do ADS, sem alterar a geracao. MostrarLog( "ADS", String(etapa) + " | erro=" + String(erro) + " | duracao=" + String(duracaoMs) + "ms" ); } /* * Leitura ADS1115 controlada pelo I2CService. * * Motivos para nao usar conversionComplete(): * - ele executa uma leitura I2C a cada consulta; * - o tempo total medido ao redor dele mistura transferencia I2C com * escalonamento/preempcao da task; * - para 128 SPS a conversao nominal termina em ~7,8 ms. * * Aqui: * 1) escreve CONFIG e dispara single-shot; * 2) aguarda 10 ms sem trafego I2C; * 3) le diretamente o registrador CONVERSION; * 4) mede separadamente as duas fases I2C. * * IMPORTANTE: * O chamador ja possui i2cMutex. Nao chamar esta funcao sem ownership * do I2CService. */ static bool LerADSSingleShotComTimeout(int canal, int16_t& leituraADC) { if (canal < 0 || canal > 3) { return false; } const uint8_t endereco = ultimoEnderecoADS; if ( endereco == 0xFF || IndiceADS(endereco) < 0 ) { MostrarLog( "ADS", "Sem endereco ativo para leitura" ); return false; } const uint32_t inicioCiclo = millis(); if (!TCAReadyOk()) { RegistrarReadyDropADS(inicioCiclo); return false; } /* * Mesmo perfil que configuramos na biblioteca: * - single-ended AINx * - ganho 1 (+/-4,096 V) * - single-shot * - 128 SPS * - comparador desabilitado */ const uint16_t config = ADS1X15_REG_CONFIG_OS_SINGLE | MUX_BY_CHANNEL[canal] | ADS1X15_REG_CONFIG_PGA_4_096V | ADS1X15_REG_CONFIG_MODE_SINGLE | RATE_ADS1115_128SPS | ADS1X15_REG_CONFIG_CMODE_TRAD | ADS1X15_REG_CONFIG_CPOL_ACTVLOW | ADS1X15_REG_CONFIG_CLAT_NONLAT | ADS1X15_REG_CONFIG_CQUE_NONE; // FASE 1: disparar conversao. const uint32_t inicioStart = millis(); Wire.beginTransmission(endereco); Wire.write((uint8_t)ADS1X15_REG_POINTER_CONFIG); Wire.write((uint8_t)(config >> 8)); Wire.write((uint8_t)(config & 0xFF)); const uint8_t erroStart = Wire.endTransmission(); const uint32_t duracaoStart = (uint32_t)(millis() - inicioStart); AtualizarMaxFaseInicioADS(duracaoStart); if (!TCAReadyOk()) { RegistrarReadyDropADS(inicioCiclo); return false; } if ( erroStart == 5 || duracaoStart > LimiteFaseADS_MS ) { RegistrarTimeoutADS( inicioCiclo, "Timeout ao iniciar conversao ADS" ); return false; } if (erroStart != 0) { RegistrarFalhaIoADS( inicioCiclo, "falha ao iniciar conversao", erroStart, duracaoStart ); return false; } /* * 128 SPS => ~7,8 ms nominal. * Esperamos sem consultar CONFIG. Se a task for preemptada e voltar * muito depois, isso NAO e falha I2C; apenas aumenta maxLeituraAdsMs. */ DelayMs(EsperaConversaoADS_MS); if (!TCAReadyOk()) { RegistrarReadyDropADS(inicioCiclo); return false; } // FASE 2: selecionar CONVERSION e ler dois bytes. const uint32_t inicioResultado = millis(); Wire.beginTransmission(endereco); Wire.write((uint8_t)ADS1X15_REG_POINTER_CONVERT); const uint8_t erroPonteiro = Wire.endTransmission(); if (erroPonteiro != 0) { const uint32_t duracaoResultado = (uint32_t)(millis() - inicioResultado); AtualizarMaxFaseResultadoADS(duracaoResultado); if ( erroPonteiro == 5 || duracaoResultado > LimiteFaseADS_MS ) { RegistrarTimeoutADS( inicioCiclo, "Timeout selecionando resultado ADS" ); } else { RegistrarFalhaIoADS( inicioCiclo, "falha selecionando resultado", erroPonteiro, duracaoResultado ); } return false; } const size_t recebidos = Wire.requestFrom( (int)endereco, 2 ); const uint32_t duracaoResultado = (uint32_t)(millis() - inicioResultado); AtualizarMaxFaseResultadoADS(duracaoResultado); if (!TCAReadyOk()) { RegistrarReadyDropADS(inicioCiclo); return false; } if (duracaoResultado > LimiteFaseADS_MS) { RegistrarTimeoutADS( inicioCiclo, "Timeout obtendo resultado ADS" ); return false; } if ( recebidos != 2 || Wire.available() < 2 ) { RegistrarFalhaIoADS( inicioCiclo, "resultado incompleto", (int)recebidos, duracaoResultado ); return false; } const uint16_t bruto = ((uint16_t)Wire.read() << 8) | (uint16_t)Wire.read(); leituraADC = (int16_t)bruto; AtualizarMaxDuracaoADS(inicioCiclo); /* * Em single-ended esperamos valor nao-negativo. * Nao classificamos isso como timeout: e falha de amostra/I/O. */ if (leituraADC < 0) { RegistrarFalhaIoADS( inicioCiclo, "leitura single-ended negativa", -1, (uint32_t)(millis() - inicioCiclo) ); return false; } return true; } }; bool I2CService::DebugMode = false; int I2CService::_pinoSDA = 1; int I2CService::_pinoSCL = 2; int I2CService::_pinoTcaRdy = -1; int I2CService::_pinoTcaEn = -1; int I2CService::_pinoMuxRst = -1; int I2CService::_pinoAdsPwrEn = -1; unsigned long I2CService::LimiteTempoI2C = 4000; bool I2CService::I2CIniciado = false; bool I2CService::MuxIniciado = false; bool I2CService::McpIniciado = false; bool I2CService::AdsIniciado = false; int I2CService::idSensorMux = 200; int I2CService::idSensorMcp = 201; Adafruit_MCP23X17 I2CService::mcp; Adafruit_ADS1115 I2CService::ads; byte I2CService::ultimoEnderecoADS = 0xFF; uint8_t I2CService::enderecoMuxAtivo = 0xFF; bool I2CService::_muxValidado[8] = { false, false, false, false, false, false, false, false }; int I2CService::canalAtivo = -1; int I2CService::idAtualUsandoI2C = -1; unsigned long I2CService::tempoEntradaI2C = 0; SemaphoreHandle_t I2CService::i2cMutex = nullptr; SemaphoreHandle_t I2CService::lifecycleMutex = nullptr; TaskHandle_t I2CService::i2cTaskHandle = NULL; bool I2CService::i2cMutexReiniciando = false; bool I2CService::_recuperacaoEmAndamento = false; bool I2CService::_recoveryPendente = false; char I2CService::_motivoRecoveryPendente[192] = {0}; uint32_t I2CService::_geracaoBarramento = 0; uint8_t I2CService::_muxEsperadoMask = 0; uint32_t I2CService::_falhasTransacaoConsecutivas = 0; uint32_t I2CService::_falhasRecoveryConsecutivas = 0; uint32_t I2CService::_totalRecoveries = 0; unsigned long I2CService::_ultimoRecoveryMs = 0; unsigned long I2CService::_readyLowDesdeMs = 0; I2CService::EstadoQuarentena I2CService::_saudeCanaisMux[8][8] = {}; I2CService::EstadoQuarentena I2CService::_saudeADS[4] = {}; I2CService::ContextoBarramento I2CService::_contextoAtual = {}; I2CService::ContextoBarramento I2CService::_contextoFalha = {}; portMUX_TYPE I2CService::_estadoLock = portMUX_INITIALIZER_UNLOCKED; int I2CService::_ultimoIdFalhaDiag = -1; int I2CService::_ultimoCanalFalhaDiag = -1; uint32_t I2CService::_maxLeituraAdsMs = 0; uint32_t I2CService::_maxInicioAdsMs = 0; uint32_t I2CService::_maxResultadoAdsMs = 0; uint32_t I2CService::_timeoutsAds = 0; uint32_t I2CService::_readyDropsAds = 0; uint32_t I2CService::_falhasIoAds = 0; uint32_t I2CService::_recoveriesLocaisMux = 0; uint32_t I2CService::_recoveriesLocaisAds = 0; uint32_t I2CService::_falhasLocaisDispositivos = 0; uint8_t I2CService::_falhasLocaisPorId[256] = {0}; uint8_t I2CService::_falhasAdsConsecutivas = 0; uint8_t I2CService::_falhasRecoveryLocalAdsConsecutivas = 0; uint32_t I2CService::_ultimoRecoveryLocalAdsMs = 0; bool I2CService::_desabilitarMuxAoLiberar = false; #endif