#ifndef I2CService_h #define I2CService_h #include "SerialService.h" #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; }; 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; } static bool IniciarI2C(bool forcar = false) { GarantirMutex(); 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); } 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); } 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) ); if (i2cTaskHandle == NULL) { xTaskCreatePinnedToCore( I2CService::i2cTaskWrapper, "i2cTask", 4096, nullptr, 2, &i2cTaskHandle, tskNO_AFFINITY ); } return I2CIniciado; } 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 CanalMuxEmQuarentena(uint8_t canalGlobal) { uint8_t indiceMux = canalGlobal / 10; uint8_t canal = canalGlobal % 10; if (indiceMux > 7 || canal > 7) { return true; } bool bloqueado; portENTER_CRITICAL(&_estadoLock); bloqueado = _saudeCanaisMux[indiceMux][canal].bloqueado; portEXIT_CRITICAL(&_estadoLock); 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; } 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; } bool bloqueado; portENTER_CRITICAL(&_estadoLock); bloqueado = _saudeADS[indice].bloqueado; portEXIT_CRITICAL(&_estadoLock); return bloqueado; } static uint8_t RecuperacoesConfirmadasADS(uint8_t endereco = enderecoAds) { int indice = IndiceADS(endereco); if (indice < 0) { return 0; } 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() { String s = ""; s += "i2c=" + String(I2CIniciado ? 1 : 0); s += " usando=" + String(idAtualUsandoI2C); s += " recovery=" + String(_recuperacaoEmAndamento ? 1 : 0); s += " pendente=" + String(_recoveryPendente ? 1 : 0); s += " fails_tx=" + String(_falhasTransacaoConsecutivas); s += " total_recovery=" + String(_totalRecoveries); s += " q_mux=" + String(QuantidadeCanaisMuxEmQuarentena()); s += " q_ads=" + String(QuantidadeADSEmQuarentena()); if (_pinoTcaRdy >= 0) { s += " tca_rdy=" + String(digitalRead(_pinoTcaRdy) == HIGH ? 1 : 0); } 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 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 (i2cMutexReiniciando || _recuperacaoEmAndamento) { 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 (i2cMutexReiniciando || _recuperacaoEmAndamento || !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) ); bool precisaRecuperar = _recoveryPendente; String motivo = _motivoRecoveryPendente; 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; } bool precisaRecuperar = _recoveryPendente; String motivo = _motivoRecoveryPendente; ContextoBarramento contexto = _contextoAtual; if ( contexto.valido && contexto.canalMuxSolicitado >= 0 && !_recoveryPendente ) { DesabilitarMuxAtualSemMutex(); precisaRecuperar = _recoveryPendente; motivo = _motivoRecoveryPendente; } if (precisaRecuperar) { CapturarContextoFalha(); i2cMutexReiniciando = true; } LimparContextoAtual(); idAtualUsandoI2C = -1; tempoEntradaI2C = 0; xSemaphoreGive(i2cMutex); if (precisaRecuperar) { RecuperarBarramento(motivo, true); } } 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"); } 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 TrocarCanalMux(uint8_t endereco, uint8_t canalMux) { 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; } uint8_t canalGlobal = (indiceMux * 10) + canalMux; int canalAnterior = CanalGlobalAtivoAtual(); int sdaAntes = digitalRead(_pinoSDA); int sclAntes = digitalRead(_pinoSCL); int rdyAntes = TCAReadyOk() ? 1 : 0; uint32_t inicio = millis(); if (CanalMuxEmQuarentena(canalGlobal)) { MostrarLog("MUX", "Canal bloqueado por quarentena | canalGlobal=" + String(canalGlobal)); return false; } if (!_muxValidado[indiceMux]) { MostrarLog("MUX", "Validando MUX em 0x" + String(endereco, HEX)); if (!VerificaEnderecoBarramento(endereco)) { MostrarLog("MUX", "MUX nao encontrado em 0x" + String(endereco, HEX)); return false; } _muxValidado[indiceMux] = true; MuxIniciado = true; } if (enderecoMuxAtivo == endereco && canalAtivo == canalMux) { return true; } // Evita deixar dois ramais conectados simultaneamente. if (enderecoMuxAtivo != 0xFF && enderecoMuxAtivo != endereco) { Wire.beginTransmission(enderecoMuxAtivo); Wire.write(0x00); int erroDesabilitar = Wire.endTransmission(); if (erroDesabilitar != 0) { MostrarLog( "MUX", "Falha ao desabilitar MUX anterior 0x" + String(enderecoMuxAtivo, HEX) + " | erro=" + String(erroDesabilitar) ); RegistrarFalhaTransacao("Falha ao desabilitar MUX anterior"); return false; } enderecoMuxAtivo = 0xFF; canalAtivo = -1; } Wire.beginTransmission(endereco); Wire.write((uint8_t)(1U << canalMux)); int erro = Wire.endTransmission(); uint32_t duracao = (uint32_t)(millis() - inicio); int sdaDepois = digitalRead(_pinoSDA); int sclDepois = digitalRead(_pinoSCL); int rdyDepois = TCAReadyOk() ? 1 : 0; MostrarLog( "MUX", "Troca" " | anterior=" + String(canalAnterior) + " solicitado=" + String(canalGlobal) + " erro=" + String(erro) + " duracao=" + String(duracao) + "ms" + " antes[SDA=" + String(sdaAntes) + " SCL=" + String(sclAntes) + " RDY=" + String(rdyAntes) + "]" + " depois[SDA=" + String(sdaDepois) + " SCL=" + String(sclDepois) + " RDY=" + String(rdyDepois) + "]" ); if (erro != 0) { MostrarLog( "MUX", "Falha ao trocar canal | endereco=0x" + String(endereco, HEX) + " canal=" + String(canalMux) + " erro=" + String(erro) ); RegistrarFalhaTransacao("Falha ao trocar canal MUX"); return false; } enderecoMuxAtivo = endereco; canalAtivo = canalMux; RegistrarSucessoTransacao(); MostrarLog( "MUX", "Canal ativo | endereco=0x" + String(endereco, HEX) + " canal=" + String(canalMux) ); return true; } 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 IniciarADS(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; } 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++) { uint32_t inicioLeitura = millis(); int leituraADC = (int)ads.readADC_SingleEnded(canal); uint32_t duracaoLeitura = (uint32_t)(millis() - inicioLeitura); if (!TCAReadyOk() || duracaoLeitura > LimiteLeituraADS_MS) { RegistrarFalhaTransacao( !TCAReadyOk() ? "READY caiu durante leitura ADS" : "Leitura ADS excedeu o limite de tempo" ); leituraFalhou = true; break; } int leitura12bits = map(leituraADC, 0, 32767, 0, 4095); soma += leitura12bits; leiturasConcluidas++; MostrarLog( "ADS", "Canal=" + String(canal) + " ADC=" + String(leituraADC) + " 12bits=" + String(leitura12bits) ); } 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; } 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 = VerificaEnderecoBarramento(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) { MostrarLog("ADS", "Impossivel iniciar ADS1115, I2C nao iniciado"); return false; } if (ADSEmQuarentena(endereco)) { MostrarLog( "ADS", "Inicializacao recusada, ADS em quarentena | endereco=0x" + String(endereco, HEX) ); return false; } MarcarEnderecoADSNoContexto(endereco); if (AdsIniciado && endereco == ultimoEnderecoADS) { return true; } MostrarLog("ADS", "Iniciando ADS1115 em 0x" + String(endereco, HEX)); AdsIniciado = ads.begin(endereco, &Wire); 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; } 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 TaskHandle_t i2cTaskHandle; static bool _recuperacaoEmAndamento; static bool _recoveryPendente; static String _motivoRecoveryPendente; 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; static constexpr uint32_t TimeoutWireMs = 80; static constexpr uint32_t LimiteReadyLowMs = 200; static constexpr uint32_t CooldownRecoveryMs = 1000; static constexpr uint32_t LimiteLeituraADS_MS = 50; 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 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 RecriarMutexSeguro() { if (i2cMutex != nullptr) { vSemaphoreDelete(i2cMutex); i2cMutex = nullptr; } i2cMutex = xSemaphoreCreateMutex(); idAtualUsandoI2C = -1; tempoEntradaI2C = 0; } 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(); _recoveryPendente = true; _motivoRecoveryPendente = motivo; } 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() { 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) { total++; } } } portEXIT_CRITICAL(&_estadoLock); return total; } static uint8_t QuantidadeADSEmQuarentena() { uint8_t total = 0; portENTER_CRITICAL(&_estadoLock); for (int i = 0; i < 4; i++) { if (_saudeADS[i].bloqueado) { total++; } } portEXIT_CRITICAL(&_estadoLock); return total; } static void RegistrarFalhaTransacao(const String& motivo) { _falhasTransacaoConsecutivas++; MostrarLog( "I2C", "Falha transacao #" + String(_falhasTransacaoConsecutivas) + " | " + motivo ); if (_falhasTransacaoConsecutivas >= LimiteFalhasTransacaoRecovery) { AgendarRecovery("Falhas consecutivas no I2C: " + motivo); } } static void RegistrarSucessoTransacao() { _falhasTransacaoConsecutivas = 0; } static bool RecuperarBarramento(const String& motivo, bool ignorarCooldown = false) { if (_recuperacaoEmAndamento) { return false; } if ( !ignorarCooldown && _ultimoRecoveryMs > 0 && !TempoPassou(_ultimoRecoveryMs, CooldownRecoveryMs) ) { return false; } _recuperacaoEmAndamento = true; i2cMutexReiniciando = true; _ultimoRecoveryMs = millis(); _totalRecoveries++; 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; } } // 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); if (barramentoLivre) { RegistrarResponsavelADS(contexto.enderecoADS); responsavelConfirmado = 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) { ResetarMuxSePossivel(); } else if (contexto.enderecoADS != 0xFF && _pinoAdsPwrEn >= 0) { ResetarADSSePossivel(); } barramentoLivre = AguardarBarramentoLivre(100); } // 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; } } 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(); } } return ok; } static void VerificarTCAReady() { if (_pinoTcaRdy < 0 || _recuperacaoEmAndamento) { 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 (_recuperacaoEmAndamento || idAtualUsandoI2C < 0) { return; } uint32_t tempoPreso = (uint32_t)(millis() - tempoEntradaI2C); if (tempoPreso <= LimiteTempoI2C) { return; } if (!_recoveryPendente) { String motivo = "Acesso I2C preso pelo ID " + String(idAtualUsandoI2C) + " por " + String(tempoPreso) + "ms"; MostrarLog("I2C", motivo); AgendarRecovery(motivo); } // 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) { MostrarLog( "I2C", "Task I2C nao retornou apos o timeout; reiniciando ESP por seguranca" ); DelayMs(100); ESP.restart(); } } static void VerificarRecoveryPendente() { if ( !_recoveryPendente || _recuperacaoEmAndamento || idAtualUsandoI2C >= 0 ) { return; } RecuperarBarramento(_motivoRecoveryPendente, 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; } }; 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 = 2000; 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; TaskHandle_t I2CService::i2cTaskHandle = NULL; bool I2CService::i2cMutexReiniciando = false; bool I2CService::_recuperacaoEmAndamento = false; bool I2CService::_recoveryPendente = false; String I2CService::_motivoRecoveryPendente = ""; 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; #endif