#include "SerialService.h" #include "Pinout.h" #include "IComponenteCAN.h" #ifndef SensorFluxoModel_v2_h #define SensorFluxoModel_v2_h class SensorFluxo_v2 : public ComponenteCAN { public: SensorFluxo_v2( const String& id, uint8_t idNum, int pino, TiposBarramentos barramento = CONTROLADOR ) : _ID(id), ID_Num(idNum), _pino( pino, barramento, _INPUT_PULLUP, DIGITAL ) { } String _ID; uint8_t ID_Num = 0; PinoModel _pino; bool Iniciado = false; /* * Calibração do sensor. * * Equação original: * Q (L/min) = (frequência + 3) / 8.1 */ double FatorCalibracao = 0.776; double VazaoMaximaLMin = 10.0; /* * Após esse tempo sem nenhum pulso válido, * consideramos que não existe fluxo. */ unsigned long TimeoutSemPulsoMs = 700; /* * Ignora pulsos rápidos demais para serem reais. * * 3 ms permite até aproximadamente 333 Hz, * muito acima da frequência esperada para 10 L/min. */ unsigned long IntervaloMinimoPulsoMs = 3; /* * Valores consolidados para telemetria CAN. */ unsigned int _PulsosLeitura = 0; unsigned long _PeriodoTotalLeitura = 0; float _Vazao = 0.0f; bool _FluxoPresente = false; unsigned long _UltimoPulsoMs = 0; /* * Protege aquisição, janela de controle e telemetria. */ portMUX_TYPE _stateMux = portMUX_INITIALIZER_UNLOCKED; void Inicializar() { if (Iniciado) { MostrarLog("Sensor ja inicializado"); return; } _pino.Conectar(); Iniciado = _pino.Definido(); if (!Iniciado) { MostrarLog("Sensor nao iniciado"); return; } ResetarEstado(); MostrarLog("Sensor iniciado"); } void Desligar() { if (!Iniciado) { MostrarLog("Sensor nao esta inicializado"); return; } ResetarEstado(); _pino.Desconectar(); Iniciado = false; MostrarLog("Sensor desligado"); } /* * Deve continuar sendo chamado no loop rápido. * * Detecta somente a borda de subida do sinal. */ void Loop() { if (!Iniciado) return; bool leituraAtual = (_pino.get() == 1); unsigned long agora = millis(); portENTER_CRITICAL(&_stateMux); bool mudouEstado = leituraAtual != _ultimaLeituraDigital; if (mudouEstado) { bool bordaSubida = leituraAtual && !_ultimaLeituraDigital; if (bordaSubida) { RegistrarPulsoInterno(agora); } _ultimaLeituraDigital = leituraAtual; } portEXIT_CRITICAL(&_stateMux); } /* * Atualiza somente o snapshot destinado à telemetria. * * Não interfere na janela utilizada pelo controle. */ void RequisitarDados() { AtualizarLeituraTelemetria(); } std::vector MontarMensagemCAN( CanMessagePosicaoDados posicao ) { std::vector data; data.push_back( static_cast(posicao) ); data.push_back(ID_Num); switch (posicao) { case CanMessagePosicaoDados::Status: { data.push_back(Iniciado ? 1 : 0); break; } case CanMessagePosicaoDados::Dados1: { unsigned int pulsosLeitura; unsigned long periodoLeitura; float vazao; portENTER_CRITICAL(&_stateMux); pulsosLeitura = _PulsosLeitura; periodoLeitura = _PeriodoTotalLeitura; vazao = _Vazao; portEXIT_CRITICAL(&_stateMux); /* * O protocolo atual possui: * * pulsos: 1 byte * período: 2 bytes * vazão: 2 bytes, escala ×100 */ uint8_t pulsosEscalados = pulsosLeitura > 255 ? 255 : (uint8_t)pulsosLeitura; uint16_t periodoEscalado = periodoLeitura > 65535UL ? 65535 : (uint16_t)periodoLeitura; double vazaoEscaladaDouble = round((double)vazao * 100.0); if (vazaoEscaladaDouble < 0.0) vazaoEscaladaDouble = 0.0; if (vazaoEscaladaDouble > 65535.0) vazaoEscaladaDouble = 65535.0; uint16_t vazaoEscalada = (uint16_t)vazaoEscaladaDouble; data.push_back(pulsosEscalados); data.push_back( (uint8_t)(periodoEscalado >> 8) ); data.push_back( (uint8_t)(periodoEscalado & 0xFF) ); data.push_back( (uint8_t)(vazaoEscalada >> 8) ); data.push_back( (uint8_t)(vazaoEscalada & 0xFF) ); break; } default: break; } return data; } /* * Retorna a janela corrente da telemetria, * sem reiniciá-la. * * Isso serve para diagnóstico. A bomba não deve * usar essa janela para controle. */ void ObterAmostragemAtual( unsigned int& pulsos, unsigned long& periodoMs ) { unsigned long agora = millis(); portENTER_CRITICAL(&_stateMux); pulsos = _PulsosTelemetria; periodoMs = _InicioJanelaTelemetriaMs > 0 ? agora - _InicioJanelaTelemetriaMs : 0; portEXIT_CRITICAL(&_stateMux); } /* * Snapshot somente-leitura para a malha da bomba. * * Usa uma janela circular própria de intervalos. * Uma requisição CAN nunca reinicia essa janela. */ void ObterSnapshotControle( float& vazaoMLs, bool& fluxoPresente, unsigned long& ultimoPulsoMs ) { unsigned long somaIntervalosMs; uint8_t quantidadeIntervalos; unsigned long ultimoPulso; portENTER_CRITICAL(&_stateMux); somaIntervalosMs = _SomaIntervalosControleMs; quantidadeIntervalos = _QuantidadeIntervalosControle; ultimoPulso = _UltimoPulsoMs; portEXIT_CRITICAL(&_stateMux); unsigned long agora = millis(); bool pulsoRecente = ultimoPulso > 0 && (agora - ultimoPulso) <= TimeoutSemPulsoMs; /* * Um pulso recente já confirma presença física * de fluxo, mesmo que ainda não existam dois * pulsos para calcular sua frequência. */ fluxoPresente = pulsoRecente; ultimoPulsoMs = ultimoPulso; if ( !pulsoRecente || quantidadeIntervalos == 0 || somaIntervalosMs == 0 ) { vazaoMLs = 0.0f; return; } double periodoMedioMs = (double)somaIntervalosMs / (double)quantidadeIntervalos; double frequenciaHz = 1000.0 / periodoMedioMs; vazaoMLs = CalcularVazaoPorFrequencia( frequenciaHz ); } /* * Conversão única e oficial da frequência para * vazão corrigida. */ float CalcularVazaoPorFrequencia( double frequenciaHz ) const { if ( !isfinite(frequenciaHz) || frequenciaHz <= 0.0 ) { return 0.0f; } double vazaoLMin = (frequenciaHz + 3.0) / 8.1; if (vazaoLMin < 0.0) vazaoLMin = 0.0; if (vazaoLMin > VazaoMaximaLMin) vazaoLMin = VazaoMaximaLMin; double vazaoMLs = vazaoLMin * (1000.0 / 60.0) * FatorCalibracao; if ( !isfinite(vazaoMLs) || vazaoMLs < 0.0 ) { return 0.0f; } return (float)vazaoMLs; } float CalcularVazao( unsigned int pulsos, unsigned long periodoMs ) const { if ( pulsos == 0 || periodoMs == 0 ) { return 0.0f; } double frequenciaHz = ((double)pulsos * 1000.0) / (double)periodoMs; return CalcularVazaoPorFrequencia( frequenciaHz ); } private: bool DebugMode = false; /* * Quantidade de intervalos recentes utilizada * pelo controle da bomba. * * Quatro intervalos oferecem um bom compromisso: * - reduz oscilação; * - não torna a resposta lenta demais com um bico. */ static const uint8_t TAMANHO_JANELA_CONTROLE = 4; unsigned long _IntervalosControleMs[TAMANHO_JANELA_CONTROLE] = { 0, 0, 0, 0 }; uint8_t _IndiceIntervaloControle = 0; uint8_t _QuantidadeIntervalosControle = 0; unsigned long _SomaIntervalosControleMs = 0; /* * Janela exclusiva da telemetria. * * Somente essa janela é zerada por * AtualizarLeituraTelemetria(). */ unsigned int _PulsosTelemetria = 0; unsigned long _InicioJanelaTelemetriaMs = 0; /* * Estado comum de aquisição. */ bool _ultimaLeituraDigital = false; unsigned long _UltimoPulsoValidoMs = 0; void RegistrarPulsoInterno( unsigned long agora ) { /* * Este método é chamado com o mux travado. */ if (_UltimoPulsoValidoMs > 0) { unsigned long intervalo = agora - _UltimoPulsoValidoMs; /* * Ignora bounce ou ruído rápido. * * Não atualizamos o timestamp do último * pulso válido quando o pulso é rejeitado. */ if (intervalo < IntervaloMinimoPulsoMs) { return; } AdicionarIntervaloControle( intervalo ); } _UltimoPulsoValidoMs = agora; _UltimoPulsoMs = agora; if (_PulsosTelemetria < UINT_MAX) { _PulsosTelemetria++; } } void AdicionarIntervaloControle( unsigned long intervaloMs ) { /* * Este método também é chamado com o mux travado. */ if ( _QuantidadeIntervalosControle < TAMANHO_JANELA_CONTROLE ) { _IntervalosControleMs[ _IndiceIntervaloControle ] = intervaloMs; _SomaIntervalosControleMs += intervaloMs; _QuantidadeIntervalosControle++; } else { _SomaIntervalosControleMs -= _IntervalosControleMs[ _IndiceIntervaloControle ]; _IntervalosControleMs[ _IndiceIntervaloControle ] = intervaloMs; _SomaIntervalosControleMs += intervaloMs; } _IndiceIntervaloControle = ( _IndiceIntervaloControle + 1 ) % TAMANHO_JANELA_CONTROLE; } void AtualizarLeituraTelemetria() { if (!Iniciado) return; unsigned long agora = millis(); unsigned int pulsos; unsigned long periodoMs; unsigned long somaIntervalosControle; uint8_t quantidadeIntervalosControle; unsigned long ultimoPulso; portENTER_CRITICAL(&_stateMux); pulsos = _PulsosTelemetria; periodoMs = _InicioJanelaTelemetriaMs > 0 ? agora - _InicioJanelaTelemetriaMs : 0; /* * Reinicia somente a janela de telemetria. */ _PulsosTelemetria = 0; _InicioJanelaTelemetriaMs = agora; /* * A janela de controle é copiada, mas nunca * alterada ou reiniciada aqui. */ somaIntervalosControle = _SomaIntervalosControleMs; quantidadeIntervalosControle = _QuantidadeIntervalosControle; ultimoPulso = _UltimoPulsoMs; portEXIT_CRITICAL(&_stateMux); bool pulsoRecente = ultimoPulso > 0 && (agora - ultimoPulso) <= TimeoutSemPulsoMs; float vazao = 0.0f; /* * Para a telemetria, usamos a média da janela * de aquisição desde a última requisição. */ if ( pulsoRecente && pulsos > 0 && periodoMs > 0 ) { vazao = CalcularVazao( pulsos, periodoMs ); } else if ( pulsoRecente && quantidadeIntervalosControle > 0 && somaIntervalosControle > 0 ) { /* * Fallback: * * Se a janela CAN foi curta e ainda não recebeu * pulso, usa a janela circular do controle. */ double periodoMedioMs = (double)somaIntervalosControle / (double)quantidadeIntervalosControle; double frequenciaHz = 1000.0 / periodoMedioMs; vazao = CalcularVazaoPorFrequencia( frequenciaHz ); } portENTER_CRITICAL(&_stateMux); _PulsosLeitura = pulsos; _PeriodoTotalLeitura = periodoMs; _Vazao = vazao; _FluxoPresente = pulsoRecente; portEXIT_CRITICAL(&_stateMux); } void ResetarEstado() { bool leituraInicial = false; if (_pino.Definido()) { leituraInicial = (_pino.get() == 1); } unsigned long agora = millis(); portENTER_CRITICAL(&_stateMux); _ultimaLeituraDigital = leituraInicial; _UltimoPulsoValidoMs = 0; _UltimoPulsoMs = 0; /* * Limpa janela de controle. */ for ( uint8_t i = 0; i < TAMANHO_JANELA_CONTROLE; i++ ) { _IntervalosControleMs[i] = 0; } _IndiceIntervaloControle = 0; _QuantidadeIntervalosControle = 0; _SomaIntervalosControleMs = 0; /* * Limpa janela de telemetria. */ _PulsosTelemetria = 0; _InicioJanelaTelemetriaMs = agora; /* * Limpa snapshot publicado. */ _PulsosLeitura = 0; _PeriodoTotalLeitura = 0; _Vazao = 0.0f; _FluxoPresente = false; portEXIT_CRITICAL(&_stateMux); } void MostrarLog( String mensagem ) { if (DebugMode) { PrintTela( "[FLX " + _ID + "] " + mensagem ); } } }; #endif