3291 lines
92 KiB
C++
3291 lines
92 KiB
C++
#ifndef I2CService_h
|
|
#define I2CService_h
|
|
|
|
#include "SerialService.h"
|
|
#include <Arduino.h>
|
|
#include <Wire.h>
|
|
#include <Adafruit_MCP23X17.h>
|
|
#include <Adafruit_ADS1X15.h>
|
|
#include <freertos/FreeRTOS.h>
|
|
#include <freertos/semphr.h>
|
|
#include <cstring>
|
|
|
|
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 = 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;
|
|
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
|