diff --git a/Firmware/Modulos/I2CService.h b/Firmware/Modulos/I2CService.h index 2eb525b50..7f44ac353 100644 --- a/Firmware/Modulos/I2CService.h +++ b/Firmware/Modulos/I2CService.h @@ -8,6 +8,7 @@ #include #include #include +#include class I2CService { public: @@ -31,6 +32,133 @@ class I2CService { 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; + }; + + 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; + + 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; @@ -53,14 +181,7 @@ class I2CService { * 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 - ) { + 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) { @@ -104,79 +225,129 @@ class I2CService { 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) { - GarantirMutex(); + GarantirMutexes(); ConfigurarPinosControle(); - bool barramentoLivre = forcar || BarramentoFisicamenteLivre(); - - // Primeiro tenta as recuperacoes mais localizadas, sem atribuir culpa - // durante a inicializacao. - if (!barramentoLivre && _pinoMuxRst >= 0) { - MostrarLog("I2C", "Barramento preso no inicio; resetando MUX"); - ResetarMuxSePossivel(); - barramentoLivre = AguardarBarramentoLivre(150); + // 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; } - if (!barramentoLivre && _pinoAdsPwrEn >= 0) { - MostrarLog("I2C", "Barramento ainda preso; executando power-cycle do ADS"); - ResetarADSSePossivel(); - barramentoLivre = AguardarBarramentoLivre(150); + 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); } - - if (!barramentoLivre && _pinoTcaEn >= 0) { - MostrarLog("I2C", "Barramento ainda preso; resetando TCA4307"); - ResetarTCA4307(false); - barramentoLivre = AguardarBarramentoLivre(200); - } - - if (I2CIniciado) { - Wire.end(); - I2CIniciado = false; - DelayMs(5); - } - - if (!barramentoLivre && (_pinoTcaEn < 0 || TCAReadyOk())) { - MostrarLog("I2C", "Tentando destravamento manual SDA/SCL antes do Wire.begin"); - DestravarBarramentoI2C(_pinoSDA, _pinoSCL); - barramentoLivre = AguardarBarramentoLivre(100); - } - - MostrarLog("I2C", "Iniciando Wire..."); - I2CIniciado = Wire.begin(_pinoSDA, _pinoSCL); - MostrarLog( - "I2C", - "Wire configurado | clock=" + String(Wire.getClock()) + - " timeout=" + String(Wire.getTimeOut()) + "ms" - ); - - if (I2CIniciado) { - Wire.setClock(100000); - Wire.setTimeOut(TimeoutWireMs); - } - - idAtualUsandoI2C = -1; - tempoEntradaI2C = 0; - _readyLowDesdeMs = 0; - _falhasTransacaoConsecutivas = 0; - _recoveryPendente = false; - _motivoRecoveryPendente = ""; - - 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(barramentoLivre ? 1 : 0) + - " res=" + String(I2CIniciado ? 1 : 0) - ); + xSemaphoreGive(lifecycleMutex); if (i2cTaskHandle == NULL) { xTaskCreatePinnedToCore( @@ -190,7 +361,7 @@ class I2CService { ); } - return I2CIniciado; + return resultado; } static void VerificarSaudeBarramento() { @@ -215,10 +386,7 @@ class I2CService { return _falhasRecoveryConsecutivas; } - static bool TentarRecuperar( - const String& motivo, - bool ignorarCooldown = false - ) { + static bool TentarRecuperar(const String& motivo, bool ignorarCooldown = false) { return RecuperarBarramento(motivo, ignorarCooldown); } @@ -234,12 +402,29 @@ class I2CService { return true; } - bool bloqueado; + const uint32_t agora = millis(); + bool bloqueado = false; + bool expirou = false; portENTER_CRITICAL(&_estadoLock); - bloqueado = _saudeCanaisMux[indiceMux][canal].bloqueado; + 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; } @@ -251,6 +436,8 @@ class I2CService { return 0; } + (void)CanalMuxEmQuarentena(canalGlobal); + uint8_t total; portENTER_CRITICAL(&_estadoLock); @@ -267,12 +454,29 @@ class I2CService { return true; } - bool bloqueado; + const uint32_t agora = millis(); + bool bloqueado = false; + bool expirou = false; portENTER_CRITICAL(&_estadoLock); - bloqueado = _saudeADS[indice].bloqueado; + 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; } @@ -283,6 +487,8 @@ class I2CService { return 0; } + (void)ADSEmQuarentena(endereco); + uint8_t total; portENTER_CRITICAL(&_estadoLock); @@ -340,19 +546,49 @@ class I2CService { } static String StatusBarramento() { - String s = ""; + 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(_recuperacaoEmAndamento ? 1 : 0); - s += " pendente=" + String(_recoveryPendente ? 1 : 0); - s += " fails_tx=" + String(_falhasTransacaoConsecutivas); + 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()); 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; @@ -391,11 +627,51 @@ class I2CService { return true; } - static bool SolicitarAcessoI2C( - int idSensor = 0, - int canalMux = -1, - uint32_t timeoutMs = 200 - ) { + 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; @@ -410,7 +686,7 @@ class I2CService { return false; } - if (i2cMutexReiniciando || _recuperacaoEmAndamento) { + if (RecoveryBloqueandoAcesso()) { MostrarLog("I2C", "Acesso negado ID " + String(idSensor) + " | Recovery em andamento"); return false; } @@ -432,7 +708,7 @@ class I2CService { // Uma recuperacao pode ter comecado entre as verificacoes acima e o // recebimento do mutex. - if (i2cMutexReiniciando || _recuperacaoEmAndamento || !I2CIniciado) { + if (RecoveryBloqueandoAcesso() || !I2CIniciado) { xSemaphoreGive(i2cMutex); return false; } @@ -448,8 +724,8 @@ class I2CService { " | ID=" + String(idSensor) ); - bool precisaRecuperar = _recoveryPendente; - String motivo = _motivoRecoveryPendente; + String motivo; + bool precisaRecuperar = ObterRecoveryPendente(motivo); if (precisaRecuperar) { CapturarContextoFalha(); @@ -479,25 +755,24 @@ class I2CService { return; } - bool precisaRecuperar = _recoveryPendente; - String motivo = _motivoRecoveryPendente; + String motivo; + bool precisaRecuperar = ObterRecoveryPendente(motivo); ContextoBarramento contexto = _contextoAtual; if ( contexto.valido && contexto.canalMuxSolicitado >= 0 && - !_recoveryPendente + !RecoveryPendente() ) { DesabilitarMuxAtualSemMutex(); - precisaRecuperar = _recoveryPendente; - motivo = _motivoRecoveryPendente; + precisaRecuperar = ObterRecoveryPendente(motivo); } if (precisaRecuperar) { CapturarContextoFalha(); - i2cMutexReiniciando = true; + DefinirMutexReiniciando(true); } LimparContextoAtual(); @@ -512,16 +787,10 @@ class I2CService { } static void RecriarMutex() { - if (idAtualUsandoI2C >= 0 || _recuperacaoEmAndamento) { - MostrarLog("I2C", "Mutex nao recriado porque o barramento esta em uso"); - return; - } - - i2cMutexReiniciando = true; - RecriarMutexSeguro(); - i2cMutexReiniciando = false; - - MostrarLog("I2C", "Mutex recriado manualmente"); + // 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() { @@ -574,13 +843,7 @@ class I2CService { return idAtualUsandoI2C; } - static bool LeituraSegura( - uint8_t addr, - uint8_t* buffer, - size_t qtd, - uint8_t reg = 0xFF, - uint32_t timeoutMs = 50 - ) { + 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; } @@ -631,12 +894,7 @@ class I2CService { return true; } - static bool EscritaSegura( - uint8_t addr, - const uint8_t* dados, - size_t qtd, - uint32_t timeoutMs = 50 - ) { + 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; } @@ -698,6 +956,10 @@ class I2CService { return false; } + portENTER_CRITICAL(&_estadoLock); + _muxEsperadoMask |= (uint8_t)(1U << indiceMux); + portEXIT_CRITICAL(&_estadoLock); + uint8_t canalGlobal = (indiceMux * 10) + canalMux; int canalAnterior = CanalGlobalAtivoAtual(); @@ -814,7 +1076,7 @@ class I2CService { MarcarEnderecoADSNoContexto(endereco); - return IniciarADS(endereco) ? canal : -1; + return IniciarADSSemMutex(endereco) ? canal : -1; } static int RealizarLeituraADS(int canalGlobal, int leituras, int idSensor, int canalMux) { @@ -826,7 +1088,7 @@ class I2CService { return -1; } - int canal = SelecionarCanalADS(canalGlobal); + const int canal = SelecionarCanalADS(canalGlobal); if (canal < 0) { LiberarAcessoI2C(idSensor); @@ -835,36 +1097,37 @@ class I2CService { int64_t soma = 0; int leiturasConcluidas = 0; + bool leituraFalhou = false; for (int i = 0; i < leituras; i++) { - uint32_t inicioLeitura = millis(); - int leituraADC = (int)ads.readADC_SingleEnded(canal); - uint32_t duracaoLeitura = (uint32_t)(millis() - inicioLeitura); + int16_t leituraADC = 0; - if (!TCAReadyOk() || duracaoLeitura > LimiteLeituraADS_MS) { - RegistrarFalhaTransacao( - !TCAReadyOk() - ? "READY caiu durante leitura ADS" - : "Leitura ADS excedeu o limite de tempo" - ); + const bool leituraOk = LerADSSingleShotComTimeout(canal, leituraADC); + + if (!leituraOk) { leituraFalhou = true; break; } - int leitura12bits = map(leituraADC, 0, 32767, 0, 4095); - soma += leitura12bits; - leiturasConcluidas++; + /* + * Só chegamos aqui se: + * + * - conversão iniciou + * - terminou no prazo + * - TCA continuou READY + * - resultado foi obtido + */ + RegistrarSucessoTransacao(); - MostrarLog( - "ADS", - "Canal=" + String(canal) + - " ADC=" + String(leituraADC) + - " 12bits=" + String(leitura12bits) - ); + const int leitura12bits = map((int)leituraADC, 0, 32767, 0, 4095); + + soma += leitura12bits; + + leiturasConcluidas++; } - if (_recoveryPendente) { + if (RecoveryPendente()) { leituraFalhou = true; } @@ -890,6 +1153,10 @@ class I2CService { 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; @@ -909,7 +1176,7 @@ class I2CService { continue; } - bool iniciado = VerificaEnderecoBarramento(endereco); + bool iniciado = PingEnderecoSemRegistrar(endereco); if (iniciado) { _muxValidado[indiceMux] = true; @@ -990,44 +1257,27 @@ class I2CService { } static bool IniciarADS(byte endereco = enderecoAds) { - if (!I2CIniciado) { - MostrarLog("ADS", "Impossivel iniciar ADS1115, I2C nao iniciado"); + if (!I2CIniciado) return false; - } - if (ADSEmQuarentena(endereco)) { - MostrarLog( - "ADS", - "Inicializacao recusada, ADS em quarentena | endereco=0x" + - String(endereco, HEX) - ); + constexpr int idSensorAds = 202; + + if (!SolicitarAcessoI2C(idSensorAds, -1)) { return false; } MarcarEnderecoADSNoContexto(endereco); - if (AdsIniciado && endereco == ultimoEnderecoADS) { - return true; - } + bool ok = IniciarADSSemMutex(endereco); - MostrarLog("ADS", "Iniciando ADS1115 em 0x" + String(endereco, HEX)); - AdsIniciado = ads.begin(endereco, &Wire); + LiberarAcessoI2C(idSensorAds); - if (!AdsIniciado) { - ultimoEnderecoADS = 0xFF; - MostrarLog("ADS", "Erro ao iniciar ADS1115 em 0x" + String(endereco, HEX)); - RegistrarFalhaTransacao("Falha iniciar ADS1115"); - return false; - } - - ads.setGain(GAIN_ONE); - ultimoEnderecoADS = endereco; - - MostrarLog("ADS", "ADS1115 iniciado em 0x" + String(endereco, HEX)); - RegistrarSucessoTransacao(); - return true; + return ok; } + + + private: static bool DebugMode; @@ -1049,11 +1299,15 @@ class I2CService { static bool _muxValidado[8]; static SemaphoreHandle_t i2cMutex; + static SemaphoreHandle_t lifecycleMutex; static TaskHandle_t i2cTaskHandle; static bool _recuperacaoEmAndamento; static bool _recoveryPendente; - static String _motivoRecoveryPendente; + static char _motivoRecoveryPendente[192]; + + static uint32_t _geracaoBarramento; + static uint8_t _muxEsperadoMask; static uint32_t _falhasTransacaoConsecutivas; static uint32_t _falhasRecoveryConsecutivas; @@ -1069,16 +1323,33 @@ class I2CService { static ContextoBarramento _contextoFalha; static portMUX_TYPE _estadoLock; - static constexpr uint32_t TimeoutWireMs = 80; + // Transacoes deste SEN sao de poucos bytes a 100 kHz. + // 25 ms ainda e uma margem muito grande, mas limita melhor uma + // transferencia realmente emperrada. + 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; + static void MostrarLog(const String& componente, const String& mensagem) { if (DebugMode) { PrintTela("[" + componente + "] " + mensagem); @@ -1099,15 +1370,153 @@ class I2CService { } } - static void RecriarMutexSeguro() { - if (i2cMutex != nullptr) { - vSemaphoreDelete(i2cMutex); - i2cMutex = nullptr; + 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; } - i2cMutex = xSemaphoreCreateMutex(); - idAtualUsandoI2C = -1; - tempoEntradaI2C = 0; + 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() { @@ -1368,8 +1777,59 @@ class I2CService { 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; - _motivoRecoveryPendente = motivo; + + 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) { @@ -1462,13 +1922,24 @@ class I2CService { } 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++) { - if (_saudeCanaisMux[mux][canal].bloqueado) { + EstadoQuarentena& estado = _saudeCanaisMux[mux][canal]; + + if ( + estado.bloqueado && + estado.ultimaOcorrenciaMs > 0 && + (uint32_t)(agora - estado.ultimaOcorrenciaMs) >= JanelaQuarentenaMs + ) { + estado = EstadoQuarentena(); + } + + if (estado.bloqueado) { total++; } } @@ -1479,12 +1950,23 @@ class I2CService { } static uint8_t QuantidadeADSEmQuarentena() { + const uint32_t agora = millis(); uint8_t total = 0; portENTER_CRITICAL(&_estadoLock); for (int i = 0; i < 4; i++) { - if (_saudeADS[i].bloqueado) { + EstadoQuarentena& estado = _saudeADS[i]; + + if ( + estado.bloqueado && + estado.ultimaOcorrenciaMs > 0 && + (uint32_t)(agora - estado.ultimaOcorrenciaMs) >= JanelaQuarentenaMs + ) { + estado = EstadoQuarentena(); + } + + if (estado.bloqueado) { total++; } } @@ -1494,181 +1976,223 @@ class I2CService { } static void RegistrarFalhaTransacao(const String& motivo) { - _falhasTransacaoConsecutivas++; + uint32_t falhas; + + portENTER_CRITICAL(&_estadoLock); + if (_falhasTransacaoConsecutivas < UINT32_MAX) { + _falhasTransacaoConsecutivas++; + } + falhas = _falhasTransacaoConsecutivas; + portEXIT_CRITICAL(&_estadoLock); MostrarLog( "I2C", - "Falha transacao #" + String(_falhasTransacaoConsecutivas) + + "Falha transacao #" + String(falhas) + " | " + motivo ); - if (_falhasTransacaoConsecutivas >= LimiteFalhasTransacaoRecovery) { + 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) { - if (_recuperacaoEmAndamento) { + GarantirMutexes(); + + // Um unico dono pode desmontar/recriar Wire por vez. + if (xSemaphoreTake(lifecycleMutex, 0) != pdTRUE) { + AgendarRecovery(motivo); return false; } - if ( - !ignorarCooldown && - _ultimoRecoveryMs > 0 && - !TempoPassou(_ultimoRecoveryMs, CooldownRecoveryMs) - ) { - return false; - } + bool tomouI2C = false; + bool retorno = false; - _recuperacaoEmAndamento = true; - i2cMutexReiniciando = true; - _ultimoRecoveryMs = millis(); - _totalRecoveries++; + do { + const uint32_t agora = millis(); - MostrarLog("I2C", "Recovery iniciado | Motivo: " + motivo); - - GarantirMutex(); - - if (xSemaphoreTake(i2cMutex, pdMS_TO_TICKS(150)) != pdTRUE) { - MostrarLog("I2C", "Recovery adiado porque o mutex ainda esta ocupado"); - _recoveryPendente = true; - i2cMutexReiniciando = false; - _recuperacaoEmAndamento = false; - return false; - } - - ContextoBarramento contexto = ObterContextoFalha(); - int canalSuspeito = CanalSuspeitoDoContexto(contexto); - - bool travadoInicialmente = !BarramentoFisicamenteLivre(); - bool barramentoLivre = !travadoInicialmente; - bool responsavelConfirmado = false; - - // 1) Isola primeiro o ramal do MUX, que e a acao mais localizada. - if (travadoInicialmente && canalSuspeito >= 0 && _pinoMuxRst >= 0) { - ResetarMuxSePossivel(); - barramentoLivre = AguardarBarramentoLivre(150); - - if (barramentoLivre) { - RegistrarResponsavelCanalMux((uint8_t)canalSuspeito); - responsavelConfirmado = true; + if ( + !ignorarCooldown && + _ultimoRecoveryMs > 0 && + !TempoPassou(_ultimoRecoveryMs, CooldownRecoveryMs) + ) { + AgendarRecovery(motivo); + break; } - } - // 2) Se o MUX nao resolveu e o ADS estava envolvido, faz power-cycle. - if (!barramentoLivre && contexto.enderecoADS != 0xFF && _pinoAdsPwrEn >= 0) { - ResetarADSSePossivel(); - barramentoLivre = AguardarBarramentoLivre(150); + DefinirEstadoRecovery(true, true); + _ultimoRecoveryMs = agora; + _totalRecoveries++; - if (barramentoLivre) { - RegistrarResponsavelADS(contexto.enderecoADS); - responsavelConfirmado = true; + 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; - // Em falhas logicas, sem linha fisicamente presa, reinicia o alvo - // relacionado, mas nao soma ponto de quarentena. - if (!travadoInicialmente) { - if (canalSuspeito >= 0 && _pinoMuxRst >= 0) { + 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(); - } else if (contexto.enderecoADS != 0xFF && _pinoAdsPwrEn >= 0) { + 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; + } } - barramentoLivre = AguardarBarramentoLivre(100); - } + // 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(); + } - // 3) Recuperacao global pelo TCA4307. - if (!barramentoLivre && _pinoTcaEn >= 0) { - ResetarTCA4307(true); - barramentoLivre = AguardarBarramentoLivre(200); - } - - // Para bit-bang, primeiro tira o periferico Wire do controle dos pinos. - if (I2CIniciado) { - Wire.end(); - I2CIniciado = false; - DelayMs(5); - } - - // 4) Fallback manual. Com TCA desconectado (READY baixo), pulsar o lado - // do ESP nao alcanca o lado OUT, portanto so tenta quando faz sentido. - if (!barramentoLivre && (_pinoTcaEn < 0 || TCAReadyOk())) { - DestravarBarramentoI2C(_pinoSDA, _pinoSCL); - barramentoLivre = AguardarBarramentoLivre(100); - } - - bool wireReiniciado = false; - - if (barramentoLivre) { - I2CIniciado = Wire.begin(_pinoSDA, _pinoSCL); - MostrarLog( - "I2C", - "Wire configurado | clock=" + String(Wire.getClock()) + - " timeout=" + String(Wire.getTimeOut()) + "ms" - ); - - if (I2CIniciado) { - Wire.setClock(100000); - Wire.setTimeOut(TimeoutWireMs); - wireReiniciado = true; + barramentoLivre = AguardarBarramentoLivre(100); } - } - MarcarDispositivosParaRevalidacao(); - - bool ok = wireReiniciado && TCAReadyOk() && BarramentoFisicamenteLivre(); - - _readyLowDesdeMs = 0; - _falhasTransacaoConsecutivas = 0; - idAtualUsandoI2C = -1; - tempoEntradaI2C = 0; - LimparContextoAtual(); - - if (ok) { - _recoveryPendente = false; - _motivoRecoveryPendente = ""; - _falhasRecoveryConsecutivas = 0; - LimparContextoFalha(); - } else { - _recoveryPendente = true; - _motivoRecoveryPendente = "Nova tentativa apos recovery sem sucesso"; - _falhasRecoveryConsecutivas++; - } - - xSemaphoreGive(i2cMutex); - i2cMutexReiniciando = false; - _recuperacaoEmAndamento = false; - - if (ok) { - MostrarLog( - "I2C", - String("Recovery concluido com sucesso") + - (responsavelConfirmado ? " | responsavel identificado" : " | responsavel nao confirmado") - ); - } else { - MostrarLog( - "I2C", - "Recovery falhou | falhas_consecutivas=" + - String(_falhasRecoveryConsecutivas) - ); - - if (_falhasRecoveryConsecutivas >= MaxFalhasRecoveryAntesRestart) { - MostrarLog("I2C", "Limite de recoveries excedido, reiniciando ESP"); - DelayMs(100); - ESP.restart(); + // 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); } - return ok; + DefinirEstadoRecovery(false, false); + xSemaphoreGive(lifecycleMutex); + return retorno; } static void VerificarTCAReady() { - if (_pinoTcaRdy < 0 || _recuperacaoEmAndamento) { + if (_pinoTcaRdy < 0 || RecoveryEmAndamento()) { return; } @@ -1701,49 +2225,94 @@ class I2CService { } static void VerificarI2CPreso() { - if (_recuperacaoEmAndamento || idAtualUsandoI2C < 0) { + if (RecoveryEmAndamento() || idAtualUsandoI2C < 0) { return; } - uint32_t tempoPreso = (uint32_t)(millis() - tempoEntradaI2C); + const uint32_t tempoOwnership = + (uint32_t)(millis() - tempoEntradaI2C); - if (tempoPreso <= LimiteTempoI2C) { + if (tempoOwnership <= LimiteTempoI2C) { return; } - if (!_recoveryPendente) { - String motivo = - "Acesso I2C preso pelo ID " + - String(idAtualUsandoI2C) + - " por " + String(tempoPreso) + "ms"; + /* + * 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; - MostrarLog("I2C", motivo); - AgendarRecovery(motivo); + 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" + ); + } } - // Nunca apaga o mutex enquanto outra task pode estar dentro do Wire. - // O timeout do Wire deve permitir o retorno. Se nem isso acontecer, - // o restart completo e o ultimo recurso seguro. - if (tempoPreso > LimiteTempoI2C + MargemHardRestartMs) { + /* + * 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 I2C nao retornou apos o timeout; reiniciando ESP por seguranca" + "Task dona do I2C nao retornou; reiniciando ESP por seguranca" ); + DelayMs(100); ESP.restart(); } } static void VerificarRecoveryPendente() { - if ( - !_recoveryPendente || - _recuperacaoEmAndamento || - idAtualUsandoI2C >= 0 - ) { + if (RecoveryEmAndamento() || idAtualUsandoI2C >= 0) { return; } - RecuperarBarramento(_motivoRecoveryPendente, false); + String motivo; + if (!ObterRecoveryPendente(motivo)) { + return; + } + + if (motivo.length() == 0) { + motivo = "Recovery pendente"; + } + + RecuperarBarramento(motivo, false); } static void i2cTaskWrapper(void *pvParameters) { @@ -1798,6 +2367,335 @@ class I2CService { 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(); + RegistrarFalhaTransacao(motivo); + } + + static void RegistrarReadyDropADS(uint32_t inicioMs) { + AtualizarMaxDuracaoADS(inicioMs); + + portENTER_CRITICAL(&_estadoLock); + + if (_readyDropsAds < UINT32_MAX) { + _readyDropsAds++; + } + + portEXIT_CRITICAL(&_estadoLock); + + RegistrarFalhaTransacao("READY caiu durante leitura ADS"); + } + + static void RegistrarFalhaIoADS( + uint32_t inicioMs, + const char* etapa, + int erro, + uint32_t duracaoMs + ) { + AtualizarMaxDuracaoADS(inicioMs); + IncrementarFalhaIoADS(); + + RegistrarFalhaTransacao( + String("ADS ") + 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 + ) { + RegistrarFalhaTransacao("ADS sem endereco ativo"); + 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; @@ -1835,13 +2733,17 @@ 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; -String I2CService::_motivoRecoveryPendente = ""; +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; @@ -1858,4 +2760,13 @@ 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; + #endif