#include "SerialService.h" #include "Pinout.h" #include "Utils.h" #include "IComponenteCAN.h" #include "I2CService.h" #include #include #include #include #ifndef SensorTemperaturaNtcModel #define SensorTemperaturaNtcModel /* * SensorTemperaturaNtc - versão produto * * Topologia física da placa: * * Retorno do motor ---- Rserie 4k7 ----+---- ADC / ADS1115 * | * Rgnd 6k8 * | * GND * * Logo, visto pelo retorno do motor, a placa adiciona uma carga: * * Rcarga = Rserie + Rgnd = 11k5 * * e a tensão realmente lida pelo ADC é: * * Vadc = Vretorno * Rgnd / (Rserie + Rgnd) * * A classe reconstrói Vretorno antes de calcular o NTC. * * * ntcGnd = false: * * 5 V * | * NTC * | * Vretorno ------- Rserie ------- ADC * | | * Rfixo 10k Rgnd * | | * GND GND * * * ntcGnd = true: * * 5 V * | * Rfixo 10k * | * Vretorno ------- Rserie ------- ADC * | | * NTC Rgnd * | | * GND GND * * * Para EXPANSOR_ADS: * - usa geração do Wire/I2C; * - faz uma única aquisição lógica por ciclo; * - preserva a última amostra válida em falhas; * - invalida o lifecycle após falhas consecutivas; * - o retry externo pode reinicializar o sensor. */ class SensorTemperaturaNtc : public ComponenteCAN { public: String ID; uint8_t ID_Num = 0; PinoModel _pino; // Mantido público para compatibilidade com código legado. bool Iniciado = false; int leitura = -1; double temperatura = NAN; // Tensão REAL medida no pino do ADC/ADS, depois do divisor 4k7/6k8. double tensao = NAN; float OffsetTemperatura = 0.0f; SensorTemperaturaNtc() {} /* * Mantém exatamente o contrato histórico do construtor. * * ntcGnd: * false = NTC no lado de 5 V * true = NTC no lado de GND */ SensorTemperaturaNtc( const String& id, uint8_t idNum, int pino, TiposBarramentos tipoBarramento, int canalMux = -1, float offsetTemperatura = 0.0f, bool ntcGnd = false ) : ID(id), ID_Num(idNum), _pino( PinoModel( pino, tipoBarramento, _INPUT, ANALOGICO, idNum, canalMux ) ), OffsetTemperatura(offsetTemperatura), _ntcGnd(ntcGnd), _tipoBarramento(tipoBarramento) { } bool EstaIniciado() { bool iniciado; uint32_t geracaoInicializacao; portENTER_CRITICAL(&_snapshotLock); iniciado = Iniciado; geracaoInicializacao = _geracaoI2CInicializacao; portEXIT_CRITICAL(&_snapshotLock); if (!iniciado) { return false; } // ADC interno não depende do lifecycle do Wire. if (_tipoBarramento != TiposBarramentos::EXPANSOR_ADS) { return true; } const uint32_t geracaoAtual = I2CService::GeracaoBarramento(); return I2CService::I2CIniciado && I2CService::AdsIniciado && geracaoAtual != 0 && geracaoInicializacao == geracaoAtual; } bool PossuiAmostraValida() { bool valor; portENTER_CRITICAL(&_snapshotLock); valor = _possuiAmostraValida; portEXIT_CRITICAL(&_snapshotLock); return valor; } uint8_t FalhasConsecutivas() { uint8_t valor; portENTER_CRITICAL(&_snapshotLock); valor = _falhasConsecutivas; portEXIT_CRITICAL(&_snapshotLock); return valor; } uint32_t TotalFalhasLeitura() { uint32_t valor; portENTER_CRITICAL(&_snapshotLock); valor = _totalFalhasLeitura; portEXIT_CRITICAL(&_snapshotLock); return valor; } uint32_t IdadeUltimaAmostraValidaMs() { uint32_t ultima; bool possui; portENTER_CRITICAL(&_snapshotLock); ultima = _ultimaAmostraValidaMs; possui = _possuiAmostraValida; portEXIT_CRITICAL(&_snapshotLock); if (!possui || ultima == 0) { return UINT32_MAX; } return (uint32_t)(millis() - ultima); } void Inicializar() { if (EstaIniciado()) { MostrarLog("Sensor ja inicializado"); return; } // Limpa lifecycle, mas preserva a última leitura válida. AtualizarEstadoIniciado(false); if (_tipoBarramento == TiposBarramentos::EXPANSOR_ADS) { if (!I2CService::I2CIniciado) { MostrarLog( "Sensor nao iniciado | I2C indisponivel" ); return; } if (!I2CService::AdsIniciado) { MostrarLog( "Sensor nao iniciado | ADS1115 indisponivel" ); return; } } _pino.Conectar(); if (!_pino.Definido() || !_pino.Conectado) { MostrarLog( "Sensor nao iniciado | pino/barramento indisponivel" ); return; } /* * Se algum dia esta classe voltar ao ADC interno, * preserva a configuração histórica de 12 bits / 11 dB. * * Para o ADS1115 isso não é usado. */ if (_tipoBarramento == TiposBarramentos::CONTROLADOR) { analogReadResolution(12); analogSetPinAttenuation( _pino.num, ADC_11db ); } AtualizarEstadoIniciado(true); portENTER_CRITICAL(&_snapshotLock); _falhasConsecutivas = 0; portEXIT_CRITICAL(&_snapshotLock); MostrarLog( "Sensor iniciado" " | canal=" + String(_pino.num) + " | ntc_gnd=" + String(_ntcGnd ? 1 : 0) + " | geracao=" + String(_geracaoI2CInicializacao) ); } void Desligar() { _pino.Desconectar(); AtualizarEstadoIniciado(false); MostrarLog("Sensor desligado"); } void RequisitarDados() { if (!EstaIniciado()) { /* * Caso a geração tenha mudado, força Iniciado=false * para que o retry periódico da firmware enxergue * corretamente a necessidade de reinicialização. */ bool estavaMarcadoIniciado; portENTER_CRITICAL(&_snapshotLock); estavaMarcadoIniciado = Iniciado; portEXIT_CRITICAL(&_snapshotLock); if ( estavaMarcadoIniciado && _tipoBarramento == TiposBarramentos::EXPANSOR_ADS ) { AtualizarEstadoIniciado(false); } return; } AferirTemperatura(); } std::vector MontarMensagemCAN( CanMessagePosicaoDados posicao ) { std::vector data; data.push_back( static_cast(posicao) ); data.push_back(ID_Num); const bool iniciado = EstaIniciado(); int snapLeitura; double snapTemperatura; double snapTensao; bool snapValido; portENTER_CRITICAL(&_snapshotLock); snapLeitura = leitura; snapTemperatura = temperatura; snapTensao = tensao; snapValido = _possuiAmostraValida; portEXIT_CRITICAL(&_snapshotLock); switch (posicao) { case CanMessagePosicaoDados::Status: { data.push_back( iniciado ? 1 : 0 ); break; } case CanMessagePosicaoDados::Dados1: { /* * Mantém o contrato CAN atual: * * [2..3] raw ADC normalizado 0..4095 * [4..5] temperatura * 100 * [6..7] tensão no PINO ADC em mV */ uint16_t leituraAdc = snapValido && snapLeitura >= 0 ? static_cast( snapLeitura ) : 0xFFFF; int16_t temperaturaInt = snapValido && isfinite(snapTemperatura) ? static_cast( constrain( (long)lround( snapTemperatura * 100.0 ), (long)INT16_MIN + 1, (long)INT16_MAX ) ) : INT16_MIN; uint16_t tensaoMv = snapValido && isfinite(snapTensao) && snapTensao >= 0.0 ? static_cast( constrain( (long)lround( snapTensao * 1000.0 ), 0L, 65534L ) ) : 0xFFFF; data.push_back( static_cast( leituraAdc >> 8 ) ); data.push_back( static_cast( leituraAdc & 0xFF ) ); data.push_back( static_cast( (uint16_t)temperaturaInt >> 8 ) ); data.push_back( static_cast( (uint16_t)temperaturaInt & 0xFF ) ); data.push_back( static_cast( tensaoMv >> 8 ) ); data.push_back( static_cast( tensaoMv & 0xFF ) ); break; } default: break; } return data; } private: bool DebugMode = false; /* * false: * 5V -> NTC -> retorno -> Rfixo -> GND * * true: * 5V -> Rfixo -> retorno -> NTC -> GND */ bool _ntcGnd = false; TiposBarramentos _tipoBarramento = TiposBarramentos::CONTROLADOR; portMUX_TYPE _snapshotLock = portMUX_INITIALIZER_UNLOCKED; uint8_t _falhasConsecutivas = 0; uint32_t _totalFalhasLeitura = 0; uint32_t _geracaoI2CInicializacao = 0; bool _possuiAmostraValida = false; uint32_t _ultimaAmostraValidaMs = 0; /* * Hardware real. */ static constexpr float Vs = 5.0f; static constexpr float Rfixo = 10000.0f; static constexpr float Rserie = 4700.0f; static constexpr float Rgnd = 6800.0f; /* * Do ponto de vista do Vretorno: * * retorno -> 4k7 -> ADC -> 6k8 -> GND * * equivale a 11k5 para GND. */ static constexpr float Rcarga = Rserie + Rgnd; /* * ADS1115 atual: * I2CService usa GAIN_ONE. * * readADC_SingleEnded() é convertido no service * de 0..32767 para 0..4095. * * Portanto: * * 4095 <=> 4.096 V */ static constexpr float FullScaleAdsV = 4.096f; /* * O máximo físico no ADC devido ao divisor 4k7/6k8 é: * * 5V * 6k8 / (4k7 + 6k8) * ~= 2.9565 V */ static constexpr float TensaoAdcMaxFisicaV = Vs * Rgnd / Rcarga; static constexpr float MargemTensaoV = 0.010f; static constexpr float Beta = 3950.0f; static constexpr float T0K = 298.15f; static constexpr float Rntc25 = 10000.0f; static constexpr float TemperaturaMinC = -40.0f; static constexpr float TemperaturaMaxC = 150.0f; /* * ADS1115 já é delta-sigma. * 4 amostras por NTC são suficientes para uma grandeza lenta. * * 4 NTC * 4 conversões * 2 Hz = 32 conversões/s. */ static constexpr int QuantidadeAmostras = 3; /* * Filtro final leve. * O valor só entra aqui depois de passar por todas as validações. */ static constexpr float AlphaTemperatura = 0.35f; static constexpr uint8_t LimiteFalhasAntesReinit = 3; void MostrarLog( const String& mensagem ) { if (DebugMode) { PrintTela( "[TMP " + ID + "] " + mensagem ); } } void AtualizarEstadoIniciado( bool iniciado ) { const uint32_t geracao = iniciado && _tipoBarramento == TiposBarramentos::EXPANSOR_ADS ? I2CService::GeracaoBarramento() : 0; portENTER_CRITICAL(&_snapshotLock); Iniciado = iniciado; _geracaoI2CInicializacao = geracao; portEXIT_CRITICAL(&_snapshotLock); } void RegistrarFalhaAquisicao( const String& motivo ) { uint8_t falhas; portENTER_CRITICAL(&_snapshotLock); if ( _falhasConsecutivas < UINT8_MAX ) { _falhasConsecutivas++; } _totalFalhasLeitura++; falhas = _falhasConsecutivas; portEXIT_CRITICAL(&_snapshotLock); MostrarLog( "Falha de leitura " + String(falhas) + "/" + String( LimiteFalhasAntesReinit ) + " | " + motivo ); /* * Nunca sobrescreve o último valor bom com NaN. * * Se a falha persistir, o sensor perde o estado * Iniciado e entra no retry controlado da firmware. */ if ( falhas >= LimiteFalhasAntesReinit ) { AtualizarEstadoIniciado(false); MostrarLog( "Sensor marcado para " "reinicializacao controlada" ); } } void RegistrarSucessoAquisicao() { portENTER_CRITICAL(&_snapshotLock); _falhasConsecutivas = 0; portEXIT_CRITICAL(&_snapshotLock); } /* * Converte o raw 0..4095 recebido do I2CService * para a tensão REAL no pino do ADS1115. */ bool ConverterRawParaTensaoAdc( int raw, float& vAdc ) { if ( raw < 0 || raw > 4095 ) { return false; } if ( _tipoBarramento == TiposBarramentos::EXPANSOR_ADS ) { vAdc = ((float)raw / 4095.0f) * FullScaleAdsV; } else { /* * Fallback histórico para ADC de 12 bits. * Esta versão produto está otimizada para ADS. * * Se voltar ao ADC interno, recomenda-se usar * analogReadMilliVolts() no PinoModel para calibração absoluta. */ vAdc = ((float)raw / 4095.0f) * 3.3f; } if (!isfinite(vAdc)) { return false; } /* * Com o divisor físico, o ADS jamais deveria receber * mais que aproximadamente 2.9565 V. * * Deixamos pequena tolerância para resistores/medição. */ if ( _tipoBarramento == TiposBarramentos::EXPANSOR_ADS && ( vAdc < -MargemTensaoV || vAdc > TensaoAdcMaxFisicaV + 0.050f ) ) { return false; } return true; } /* * Reconstrói a tensão do nó original do sensor, * antes do divisor 4k7/6k8. * * Vadc = * Vretorno * * Rgnd / * (Rserie + Rgnd) * * logo: * * Vretorno = * Vadc * * (Rserie + Rgnd) / * Rgnd */ bool CalcularTensaoRetorno( float vAdc, float& vRetorno ) { if ( !isfinite(vAdc) || vAdc < 0.0f ) { return false; } vRetorno = vAdc * Rcarga / Rgnd; if (!isfinite(vRetorno)) { return false; } if ( vRetorno <= MargemTensaoV || vRetorno >= Vs - MargemTensaoV ) { return false; } return true; } /* * Calcula Rntc considerando também a carga * criada pelo divisor 4k7 + 6k8. */ bool CalcularResistenciaNtc( float vRetorno, float& resistenciaNtc ) { if ( !isfinite(vRetorno) || vRetorno <= MargemTensaoV || vRetorno >= Vs - MargemTensaoV ) { return false; } if (_ntcGnd) { /* * 5V -> Rfixo -> retorno -> NTC -> GND * * Abaixo do retorno temos: * * NTC || Rcarga * * Primeiro calculamos a resistência equivalente * inferior do divisor: * * Req = * Rfixo * * Vretorno / * (Vs - Vretorno) */ const float rEquivalente = Rfixo * vRetorno / (Vs - vRetorno); if ( !isfinite(rEquivalente) || rEquivalente <= 0.0f ) { return false; } /* * 1 / Req = * 1 / Rntc + * 1 / Rcarga */ const float inversoRntc = (1.0f / rEquivalente) - (1.0f / Rcarga); if ( !isfinite(inversoRntc) || inversoRntc <= 0.0f ) { return false; } resistenciaNtc = 1.0f / inversoRntc; } else { /* * 5V -> NTC -> retorno -> Rfixo -> GND * * Abaixo do retorno temos: * * Rfixo || Rcarga */ const float rInferior = (Rfixo * Rcarga) / (Rfixo + Rcarga); if ( !isfinite(rInferior) || rInferior <= 0.0f ) { return false; } /* * Vretorno = * * Vs * * rInferior / * (Rntc + rInferior) * * logo: * * Rntc = * rInferior * * (Vs - Vretorno) / * Vretorno */ resistenciaNtc = rInferior * (Vs - vRetorno) / vRetorno; } return isfinite(resistenciaNtc) && resistenciaNtc > 0.0f; } bool CalcularTemperaturaC( float resistenciaNtc, float& temperaturaC ) { if ( !isfinite(resistenciaNtc) || resistenciaNtc <= 0.0f ) { return false; } const float termoLog = logf( resistenciaNtc / Rntc25 ); if (!isfinite(termoLog)) { return false; } const float inversoT = (1.0f / T0K) + (termoLog / Beta); if ( !isfinite(inversoT) || inversoT <= 0.0f ) { return false; } temperaturaC = (1.0f / inversoT) - 273.15f + OffsetTemperatura; if ( !isfinite(temperaturaC) || temperaturaC < TemperaturaMinC || temperaturaC > TemperaturaMaxC ) { return false; } temperaturaC = roundf( temperaturaC * 100.0f ) / 100.0f; return true; } void PublicarAmostraValida( int novaLeitura, float novaTensaoAdc, float novaTemperatura ) { float temperaturaPublicada = novaTemperatura; portENTER_CRITICAL(&_snapshotLock); if ( _possuiAmostraValida && isfinite(temperatura) ) { temperaturaPublicada = AlphaTemperatura * novaTemperatura + ( 1.0f - AlphaTemperatura ) * (float)temperatura; } leitura = novaLeitura; /* * Mantemos no CAN a tensão que realmente * chegou ao ADC, depois do divisor 4k7/6k8. */ tensao = novaTensaoAdc; temperatura = roundf( temperaturaPublicada * 100.0f ) / 100.0f; _possuiAmostraValida = true; _ultimaAmostraValidaMs = millis(); portEXIT_CRITICAL(&_snapshotLock); } void AferirTemperatura() { /* * UMA única aquisição lógica. * * No EXPANSOR_ADS: * PinoModel::get(N) -> * I2CService::RealizarLeituraADS() * * O ownership do I2C fica mantido durante * todas as N conversões. */ const int novaLeitura = _pino.get( QuantidadeAmostras ); if (novaLeitura < 0) { RegistrarFalhaAquisicao( "falha ao obter amostra ADC" ); return; } float novaTensaoAdc = NAN; if ( !ConverterRawParaTensaoAdc( novaLeitura, novaTensaoAdc ) ) { RegistrarFalhaAquisicao( "raw/tensao ADC invalido" " | raw=" + String(novaLeitura) ); return; } float vRetorno = NAN; if ( !CalcularTensaoRetorno( novaTensaoAdc, vRetorno ) ) { RegistrarFalhaAquisicao( "tensao de retorno invalida" " | raw=" + String(novaLeitura) + " | Vadc=" + String( novaTensaoAdc, 3 ) ); return; } float resistenciaNtc = NAN; if ( !CalcularResistenciaNtc( vRetorno, resistenciaNtc ) ) { RegistrarFalhaAquisicao( "resistencia NTC invalida" " | Vadc=" + String( novaTensaoAdc, 3 ) + " | Vret=" + String( vRetorno, 3 ) ); return; } float novaTemperatura = NAN; if ( !CalcularTemperaturaC( resistenciaNtc, novaTemperatura ) ) { RegistrarFalhaAquisicao( "temperatura fora da faixa" " | Rntc=" + String( resistenciaNtc, 1 ) + " | Vret=" + String( vRetorno, 3 ) ); return; } RegistrarSucessoAquisicao(); PublicarAmostraValida( novaLeitura, novaTensaoAdc, novaTemperatura ); MostrarLog( "Raw=" + String(novaLeitura) + " | Vadc=" + String( novaTensaoAdc, 3 ) + " V | Vret=" + String( vRetorno, 3 ) + " V | Rntc=" + String( resistenciaNtc, 1 ) + " ohm | T=" + String( novaTemperatura, 2 ) + " C" ); } }; #endif