#ifndef BombaPressurizadoraModel_v2_h #define BombaPressurizadoraModel_v2_h #include #include #include #include "SerialService.h" #include "Pinout.h" #include "Utils.h" #include "IComponenteCAN.h" #include "SensorPressaoModel_v2.h" #include "SensorFluxoModel_v2.h" #include "BicoPulverizadorModel_v2.h" /* BombaPressurizadora_v2 - controle suave por pressao com supervisor de fluxo. Ideia desta versao: - A pressao e o controle principal. - A bomba NAO fica ligando/desligando dentro da malha fechada. - Se existem bicos abertos e nao existe pulso/fluxo recente, o controle aumenta potencia por boost progressivo, mesmo que a pressao esteja acima do SP. - O sensor de fluxo nunca deve ser usado como "chave nervosa" para cortar a bomba. Ele apenas adiciona potencia enquanto nao houver indicio de produto em movimento. - O ON/OFF antigo fica preservado somente quando ModoControle == 0. - O contrato CAN Dados1/Dados2 foi mantido. */ struct EstadoAnteriorBomba { Estado estado = Estado::Desligado; double potenciaSP = 0.0; double pressaoSP = 0.0; ModoControleBomba modoControle = static_cast(0); double kp = 0.0; double ki = 0.0; double kd = 0.0; double potenciaMinPID = 0.0; double potenciaMaxPID = 100.0; }; class BombaPressurizadora_v2 : public ComponenteCAN { public: BombaPressurizadora_v2( const String& id, uint8_t idNum, int pinoIN1, int canalIN1, int pinoIN2, int canalIN2, TiposBarramentos barramento = TiposBarramentos::CONTROLADOR, Sentido sentido = Sentido::Horario, SensorPressao_v2* sensor = nullptr, BicoPulverizador_v2** bicos = nullptr, size_t qtdBicos = 0, SensorFluxo_v2* sensorFluxoLinha = nullptr ) : _ID(id), ID_Num(idNum), _pinoIN1(pinoIN1, barramento, _OUTPUT), _pinoIN2(pinoIN2, barramento, _OUTPUT), _canalIN1(canalIN1), _canalIN2(canalIN2), _sentido(sentido), sensorPressao(sensor), sensorFluxo(sensorFluxoLinha), _bicos(bicos), _qtdBicos(qtdBicos) { } String _ID; uint8_t ID_Num = 0; PinoModel _pinoIN1; PinoModel _pinoIN2; int _canalIN1 = 1; int _canalIN2 = 2; Sentido _sentido = Sentido::Horario; bool Iniciado = false; bool MalhaFechada = false; bool bombaDesligadaPorPressao = false; SensorPressao_v2* sensorPressao = nullptr; SensorFluxo_v2* sensorFluxo = nullptr; Estado EstadoLeitura = Estado::Desligado; Estado _EstadoControle = Estado::Desligado; int PWM_SP = 0; int PWM_Set = 0; int PotenciaLeitura = 0; double PercentualPotenciaLeitura = 0.0; double _PotenciaSP = 0.0; double _PressaoSP = 0.0; ModoControleBomba ModoControle = static_cast(0); /* Ganhos da malha suave. Observacao: o Command2 continua atualizando Kp/Ki/Kd. Para esta versao, comece conservador: Kp ~= 1.0 a 2.5 Ki ~= 0.10 a 0.40 Kd = 0 */ double Kp = 1.10; double Ki = 0.12; double Kd = 0.0; double PotenciaMinPID = 0.0; double PotenciaMaxPID = 100.0; /* Na malha fechada, a saida nunca deve ficar passeando entre 0 e 100. Ela segue uma rampa. */ double TaxaSubidaPctPorSegundo = 220.0; double TaxaDescidaPctPorSegundo = 55.0; unsigned long IntervaloControleMs = 50; /* Se true, com nenhum bico aberto a bomba desliga. Se futuramente houver bypass/retorno e quiser pre-pressurizar linha, altere para false. */ bool DesligarSemBicosAbertos = true; /* Piso eletrico/hidraulico. Nao confundir com boost de fluxo. Este piso apenas evita PWM inutil. */ double PotenciaMinAtiva = 18.0; /* Piso suave por quantidade de bicos. Mantive baixo para nao forcar sobrepressao permanente. O boost de fluxo e quem sobe quando nao houver vazao. */ double PotenciaMinimaComBicosPorQtd[8] = { 0.0, 28.0, 30.0, 38.0, 42.0, 46.0, 50.0, 55.0 }; /* Kick inicial quando sai de 0 bicos para N bicos. Diferente do supervisor de fluxo, isto e apenas uma pancada inicial curta. */ bool HabilitarKickInicial = true; double PotenciaKickPoucosBicosPct = 65.0; double PotenciaKickMuitosBicosPct = 58.0; unsigned long DuracaoKickPoucosBicosMs = 1000; unsigned long DuracaoKickMuitosBicosMs = 650; /* Supervisor de fluxo. Nao corta a bomba. Apenas adiciona boost se houver bico aberto e nao houver indicio recente de fluxo/pulso. */ bool HabilitarSupervisorFluxo = true; double FatorCalibracaoFluxoFirmware = 0.776; double VazaoMinimaOperacaoMLs = 3.0; unsigned long TimeoutPulsoFluxoRecenteMs = 700; unsigned long TempoSemFluxoParaBoostMs = 300; unsigned long TempoComFluxoParaAliviarMs = 800; double BoostFluxoMaxPoucosBicosPct = 40.0; double BoostFluxoMaxMuitosBicosPct = 38.0; double TaxaBoostFluxoSubidaPctPorSegundo = 45.0; double TaxaBoostFluxoDescidaPctPorSegundo = 16.0; /* Enquanto nao houver fluxo, a malha de pressao usa um SP auxiliar minimo. Isto ajuda o PI a subir potencia sem depender apenas do boost. Nao e usado para cortar a bomba. */ double PressaoSPMinSemFluxoPoucosBicosPsi = 22.0; double PressaoSPMinSemFluxoMuitosBicosPsi = 22.0; /* Se nao houver fluxo e a pressao ja estiver muito alta, o boost para de subir. A bomba ainda nao fica ligando/desligando, apenas evita empurrar boost infinito. */ double PressaoMaxAssistenciaFluxoPsi = 85.0; /* Corte duro de protecao. A linha/trava mecanica foram informadas como seguras proximo de 120 psi. */ double PressaoCorteSegurancaPsi = 120.0; double HistereseRearmeSegurancaPsi = 12.0; bool CorteSegurancaAtivo = false; /* ON/OFF legado. Use apenas quando ModoControle == 0. */ double HistereseOnOffPsi = 3.0; /* Mapa empirico mantido como chute inicial/feedforward, nao como dono da decisao. A malha suave funciona mesmo se HabilitarFeedForward == false. */ bool HabilitarFeedForward = true; double PesoFeedForward = 1.0; double PotenciaBase15Psi[8] = { 0.0, 20.0, 40.0, 56.0, 61.0, 68.0, 72.0, // antes estava perto de 75 78.0 // antes estava perto de 88 }; double PotenciaBase22Psi[8] = { 0.0, 35.0, 56.4, 62.5, 65.8, 71.7, 77.5, 100.0 }; double PressaoReferenciaBaixaPsi = 15.0; double PressaoReferenciaAltaPsi = 22.0; double PressaoMaximaMapeadaPorBicos[8] = { 0.0, 92.0, 75.0, 56.0, 41.0, 32.0, 26.0, 22.0 }; double PotenciaBaseMaxPsi[8] = { 0.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0 }; /* Correção opcional da pressão para aproximar ponto de aplicação/manômetro. Se quiser controlar exatamente o sensor, zere esta tabela. */ double CorrecaoPressaoAplicacaoPorBicos[8] = { 0.0, -4.0, -2.0, 0.0, 1.0, 2.0, 3.5, 4.5 }; double AlphaFiltroPressao = 0.35; double DeadbandPressaoPsi = 1.2; /* Estados/telemetria expostos via CAN/log. */ uint8_t QtdBicosLigados = 0; double PressaoSensorAtual = 0.0; double PressaoAtual = 0.0; double PressaoFiltrada = 0.0; double PressaoSPControleAtual = 0.0; bool PressaoSPLimitadoPorCapacidade = false; bool PressaoSPForaDoMapaAtual = false; double PressaoMaximaMapeadaAtual = 0.0; double PotenciaFeedForwardAtual = 0.0; double CorrecaoPIAtual = 0.0; double PotenciaFinalAtual = 0.0; double VazaoFluxoAtualMLs = 0.0; double VazaoFluxoCorrigidaAtualMLs = 0.0; bool FluxoPresenteAtual = false; bool FluxoInsuficienteAtual = false; bool RecuperacaoFluxoAtiva = false; double BoostFluxoAtualPct = 0.0; /* Compatibilidade com versões anteriores. */ bool ArmamentoLinhaAtivo = false; bool LinhaArmadaPorFluxo = false; bool FalhaArmamentoLinhaAtual = false; void ConfigurarBicos(BicoPulverizador_v2** bicos, size_t qtdBicos) { _bicos = bicos; _qtdBicos = qtdBicos; } void ConfigurarSensorFluxo(SensorFluxo_v2* sensor) { sensorFluxo = sensor; ResetarFluxo(); } void ConfigurarMapaPotencia15Psi( double b1, double b2, double b3, double b4, double b5, double b6, double b7 ) { PotenciaBase15Psi[0] = 0.0; PotenciaBase15Psi[1] = b1; PotenciaBase15Psi[2] = b2; PotenciaBase15Psi[3] = b3; PotenciaBase15Psi[4] = b4; PotenciaBase15Psi[5] = b5; PotenciaBase15Psi[6] = b6; PotenciaBase15Psi[7] = b7; } void ConfigurarMapaPotencia30Psi( double b1, double b2, double b3, double b4, double b5, double b6, double b7 ) { PotenciaBase22Psi[0] = 0.0; PotenciaBase22Psi[1] = b1; PotenciaBase22Psi[2] = b2; PotenciaBase22Psi[3] = b3; PotenciaBase22Psi[4] = b4; PotenciaBase22Psi[5] = b5; PotenciaBase22Psi[6] = b6; PotenciaBase22Psi[7] = b7; } void ConfigurarMapaPotenciaAltaPsi( double pressaoAltaPsi, double b1, double b2, double b3, double b4, double b5, double b6, double b7 ) { PressaoReferenciaAltaPsi = constrain(pressaoAltaPsi, PressaoReferenciaBaixaPsi + 0.1, 250.0); ConfigurarMapaPotencia30Psi(b1, b2, b3, b4, b5, b6, b7); } void ConfigurarPressaoMaximaPorBicos( double b1, double b2, double b3, double b4, double b5, double b6, double b7 ) { PressaoMaximaMapeadaPorBicos[0] = 0.0; PressaoMaximaMapeadaPorBicos[1] = b1; PressaoMaximaMapeadaPorBicos[2] = b2; PressaoMaximaMapeadaPorBicos[3] = b3; PressaoMaximaMapeadaPorBicos[4] = b4; PressaoMaximaMapeadaPorBicos[5] = b5; PressaoMaximaMapeadaPorBicos[6] = b6; PressaoMaximaMapeadaPorBicos[7] = b7; } void ConfigurarMapaPotenciaMaxPsi( double b1, double b2, double b3, double b4, double b5, double b6, double b7 ) { PotenciaBaseMaxPsi[0] = 0.0; PotenciaBaseMaxPsi[1] = b1; PotenciaBaseMaxPsi[2] = b2; PotenciaBaseMaxPsi[3] = b3; PotenciaBaseMaxPsi[4] = b4; PotenciaBaseMaxPsi[5] = b5; PotenciaBaseMaxPsi[6] = b6; PotenciaBaseMaxPsi[7] = b7; } void ConfigurarPID( double kp, double ki, double kd, double potenciaMinPID, double potenciaMaxPID ) { Kp = kp; Ki = ki; Kd = kd; PotenciaMinPID = constrain(potenciaMinPID, 0.0, 100.0); PotenciaMaxPID = constrain(potenciaMaxPID, PotenciaMinPID, 100.0); } void ConfigurarModoControle(ModoControleBomba modo) { ModoControle = modo; ResetarControle(); } void Inicializar() { if (Iniciado) { MostrarLog("Bomba ja inicializada"); return; } _pinoIN1.Conectar(false); _pinoIN2.Conectar(false); ledcSetup(_canalIN1, PWM_FREQ_HZ, PWM_RESOLUTION_BITS); ledcSetup(_canalIN2, PWM_FREQ_HZ, PWM_RESOLUTION_BITS); ledcAttachPin(_pinoIN1.num, _canalIN1); ledcAttachPin(_pinoIN2.num, _canalIN2); bool pinosOk = _pinoIN1.Definido() && _pinoIN2.Definido(); if (!pinosOk) { Iniciado = false; MostrarLog("Falha ao iniciar bomba"); return; } Iniciado = true; ResetarControle(); AplicarSaida(false, 0); MostrarLog("Bomba iniciada"); } void Desligar() { if (!Iniciado) { MostrarLog("Bomba nao esta inicializada"); return; } AplicarSaida(false, 0); _pinoIN1.Desconectar(); _pinoIN2.Desconectar(); Iniciado = false; EstadoLeitura = Estado::Desligado; _EstadoControle = Estado::Desligado; ResetarControle(); MostrarLog("Bomba desligada"); } void AtualizarControle( Estado estado, double potenciaSP, double pressaoSP = 0.0 ) { bool mudouEstado = (_EstadoControle != estado); bool mudouPressao = fabs(_PressaoSP - pressaoSP) > 0.01; _EstadoControle = estado; _PotenciaSP = constrain(potenciaSP, 0.0, 100.0); _PressaoSP = constrain(pressaoSP, 0.0, 250.0); MalhaFechada = ( _EstadoControle == Estado::Ligado && sensorPressao != nullptr && _PressaoSP > 0.0 && static_cast(ModoControle) != 0 ); if (mudouEstado || mudouPressao) { ResetarIntegralPressao(); } if (_EstadoControle != Estado::Ligado) { AplicarSaida(false, 0); ResetarControle(); AtualizarLeitura(); return; } if (!MalhaFechada) { double potencia = _PotenciaSP; uint8_t idx = LimitarIndiceBicos(ContarBicosLigados()); if (idx > 0) { potencia = MaiorDouble(potencia, ObterPotenciaMinimaOperacional(idx)); } AplicarPotenciaPercentual(potencia); } } void Ligar(double potenciaSP = 100.0, double pressaoSP = 0.0) { AtualizarControle(Estado::Ligado, potenciaSP, pressaoSP); } void DesligarBomba() { AtualizarControle(Estado::Desligado, 0.0, 0.0); } void Loop() { if (!Iniciado) { return; } unsigned long agora = millis(); if (agora - _ultimoControleMs < IntervaloControleMs) { return; } double dt = CalcularDtSegundos(agora); _ultimoControleMs = agora; QtdBicosLigados = ContarBicosLigados(); AtualizarPressao(); AtualizarFluxo(); if (_EstadoControle != Estado::Ligado) { AplicarSaida(false, 0); ResetarControle(); AtualizarLeitura(); return; } if (TratarCorteSeguranca()) { AtualizarLeitura(); return; } MalhaFechada = ( sensorPressao != nullptr && _PressaoSP > 0.0 && static_cast(ModoControle) != 0 ); if (QtdBicosLigados == 0 && DesligarSemBicosAbertos) { AplicarSaida(false, 0); ResetarEstadosDeBicosFechados(); AtualizarLeitura(); return; } if (!MalhaFechada) { double potencia = _PotenciaSP; if (QtdBicosLigados > 0) { potencia = MaiorDouble(potencia, ObterPotenciaMinimaOperacional(QtdBicosLigados)); potencia = AplicarSupervisorFluxo(potencia, dt); } else { ResetarSupervisorFluxo(); } double potenciaComRampa = AplicarRampaPercentual(potencia, dt); AplicarPotenciaPercentual(potenciaComRampa); AtualizarLeitura(); return; } if (static_cast(ModoControle) == 0) { ControlarOnOffLegado(_PressaoSP, _PotenciaSP, HistereseOnOffPsi); AtualizarLeitura(); return; } ControlarPressaoSuaveComFluxo(dt); AtualizarLeitura(); } void AtualizarLeitura() { if (!Iniciado) { EstadoLeitura = Estado::Desligado; PercentualPotenciaLeitura = 0.0; PotenciaLeitura = 0; return; } QtdBicosLigados = ContarBicosLigados(); if (sensorPressao != nullptr) { AtualizarPressao(); } AtualizarFluxo(); PercentualPotenciaLeitura = constrain(PotenciaFinalAtual, 0.0, 100.0); PotenciaLeitura = (int)round(PercentualPotenciaLeitura); EstadoLeitura = (PercentualPotenciaLeitura > 2.0) ? Estado::Ligado : Estado::Desligado; } void RequisitarDados() { AtualizarLeitura(); } bool ProcessarComando(const std::vector& data) { if (data.size() < 3) { MostrarLog("Comando invalido"); return false; } CanMessagePosicaoDados posicao = (CanMessagePosicaoDados)data[0]; uint8_t idNum = data[1]; if (idNum != ID_Num) { return false; } switch (posicao) { case CanMessagePosicaoDados::Command1: { Estado novoEstado = (Estado)data[2]; double potencia = (data.size() > 3) ? (double)data[3] : _PotenciaSP; double pressao = (data.size() > 4) ? (double)data[4] : _PressaoSP; AtualizarControle(novoEstado, potencia, pressao); return true; } case CanMessagePosicaoDados::Command2: { if (data.size() < 7) { MostrarLog("Command2 invalido para bomba"); return false; } ModoControleBomba modo = static_cast(data[2]); double potenciaMin = (double)data[3]; double kp = ((double)data[4]) / 10.0; double ki = ((double)data[5]) / 10.0; double kd = ((double)data[6]) / 10.0; ConfigurarModoControle(modo); ConfigurarPID(kp, ki, kd, potenciaMin, PotenciaMaxPID); return true; } default: return false; } } 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); data.push_back((MalhaFechada && sensorPressao != nullptr) ? 1 : 0); break; } case CanMessagePosicaoDados::Dados1: { data.push_back(static_cast(EstadoLeitura)); // data[2] data.push_back(LimitarByte(_PotenciaSP)); // data[3] potenciaSp data.push_back(LimitarByte(PercentualPotenciaLeitura)); // data[4] percentualPotencia real data.push_back(LimitarByte(_PressaoSP)); // data[5] pressaoSp pedido data.push_back(LimitarByte(PressaoAtual)); // data[6] pressaoAtual data.push_back(static_cast(ModoControle)); // data[7] modo break; } case CanMessagePosicaoDados::Dados2: { data.push_back(QtdBicosLigados); // data[2] data.push_back(LimitarByte(PotenciaFeedForwardAtual)); // data[3] data.push_back(LimitarByte(PotenciaFinalAtual)); // data[4] data.push_back(LimitarByte(PressaoSensorAtual)); // data[5] data.push_back(LimitarByte(PressaoSPControleAtual)); // data[6] data.push_back(PressaoSPForaDoMapaAtual ? 1 : 0); // data[7] break; } default: break; } return data; } private: static const int PWM_FREQ_HZ = 1000; static const int PWM_RESOLUTION_BITS = 10; static const int PWM_MAX_VALUE = 1023; bool DebugMode = false; BicoPulverizador_v2** _bicos = nullptr; size_t _qtdBicos = 0; unsigned long _ultimoControleMs = 0; unsigned long _ultimoTempoDtMs = 0; bool _filtroInicializado = false; double _ultimaPressaoFiltrada = 0.0; double _integralPressao = 0.0; double _ultimoErroPressao = 0.0; uint8_t _ultimaQtdBicosLigados = 0; bool _kickAtivo = false; unsigned long _inicioKickMs = 0; unsigned long _ultimoPulsoFluxoMs = 0; unsigned long _inicioSemFluxoMs = 0; unsigned long _inicioComFluxoMs = 0; void ResetarControle() { ResetarIntegralPressao(); ResetarSupervisorFluxo(); ResetarKick(); _ultimoControleMs = 0; _ultimoTempoDtMs = 0; _ultimaQtdBicosLigados = 0; _filtroInicializado = false; PressaoSensorAtual = 0.0; PressaoAtual = 0.0; PressaoFiltrada = 0.0; PressaoSPControleAtual = 0.0; PressaoSPLimitadoPorCapacidade = false; PressaoSPForaDoMapaAtual = false; PressaoMaximaMapeadaAtual = 0.0; PotenciaFeedForwardAtual = 0.0; CorrecaoPIAtual = 0.0; bombaDesligadaPorPressao = false; CorteSegurancaAtivo = false; ArmamentoLinhaAtivo = false; LinhaArmadaPorFluxo = false; FalhaArmamentoLinhaAtual = false; ResetarFluxo(); } void ResetarEstadosDeBicosFechados() { ResetarIntegralPressao(); ResetarSupervisorFluxo(); ResetarKick(); PotenciaFeedForwardAtual = 0.0; CorrecaoPIAtual = 0.0; PressaoSPControleAtual = 0.0; PressaoSPForaDoMapaAtual = false; bombaDesligadaPorPressao = false; ArmamentoLinhaAtivo = false; LinhaArmadaPorFluxo = false; FalhaArmamentoLinhaAtual = false; } void ResetarIntegralPressao() { _integralPressao = 0.0; _ultimoErroPressao = 0.0; CorrecaoPIAtual = 0.0; } void ResetarKick() { _kickAtivo = false; _inicioKickMs = 0; } void ResetarFluxo() { VazaoFluxoAtualMLs = 0.0; VazaoFluxoCorrigidaAtualMLs = 0.0; FluxoPresenteAtual = false; FluxoInsuficienteAtual = false; RecuperacaoFluxoAtiva = false; _ultimoPulsoFluxoMs = 0; _inicioSemFluxoMs = 0; _inicioComFluxoMs = 0; BoostFluxoAtualPct = 0.0; } void ResetarSupervisorFluxo() { FluxoInsuficienteAtual = false; RecuperacaoFluxoAtiva = false; _inicioSemFluxoMs = 0; _inicioComFluxoMs = 0; BoostFluxoAtualPct = 0.0; } uint8_t LimitarIndiceBicos(uint8_t idx) { if (idx > 7) { return 7; } return idx; } uint8_t ContarBicosLigados() { if (_bicos == nullptr || _qtdBicos == 0) { return 0; } uint8_t qtd = 0; for (size_t i = 0; i < _qtdBicos; i++) { if (_bicos[i] == nullptr) { continue; } /* A leitura fisica do bico e atualizada no proprio model do bico. Chamamos aqui para que a bomba enxergue o estado fisico mais recente. */ _bicos[i]->AtualizarLeitura(); if (_bicos[i]->_Status == Estado::Ligado) { qtd++; } } return LimitarIndiceBicos(qtd); } void AtualizarPressao() { if (sensorPressao == nullptr) { PressaoSensorAtual = 0.0; PressaoAtual = 0.0; return; } sensorPressao->RequisitarDados(); PressaoSensorAtual = sensorPressao->Pressao; uint8_t idx = LimitarIndiceBicos(QtdBicosLigados); double pressaoAplicacao = PressaoSensorAtual + CorrecaoPressaoAplicacaoPorBicos[idx]; if (pressaoAplicacao < 0.0) { pressaoAplicacao = 0.0; } PressaoAtual = pressaoAplicacao; if (!_filtroInicializado) { PressaoFiltrada = PressaoAtual; _ultimaPressaoFiltrada = PressaoFiltrada; _filtroInicializado = true; return; } double alpha = constrain(AlphaFiltroPressao, 0.01, 1.0); PressaoFiltrada = (alpha * PressaoAtual) + ((1.0 - alpha) * PressaoFiltrada); } bool SensorFluxoDisponivel() { return sensorFluxo != nullptr && sensorFluxo->Iniciado; } void AtualizarFluxo() { VazaoFluxoAtualMLs = 0.0; VazaoFluxoCorrigidaAtualMLs = 0.0; FluxoPresenteAtual = false; if (!SensorFluxoDisponivel()) { return; } /* Nao chama RequisitarDados(), porque aquilo reinicia a janela de amostragem do sensor e pode atrapalhar o log/CAN do FLX. */ sensorFluxo->Loop(); unsigned long agora = millis(); unsigned int pulsos = sensorFluxo->_Pulsos; unsigned long periodoMs = sensorFluxo->_PeriodoTotal; if (pulsos > 0) { _ultimoPulsoFluxoMs = agora; } double vazaoMLs = 0.0; if (pulsos > 0 && periodoMs > 0) { double frequenciaHz = ((double)pulsos) * 1000.0 / ((double)periodoMs); double q_L_min = (frequenciaHz + 3.0) / 8.1; if (q_L_min < 0.0) { q_L_min = 0.0; } if (q_L_min > 20.0) { q_L_min = 20.0; } vazaoMLs = q_L_min * (1000.0 / 60.0); } bool pulsoRecente = _ultimoPulsoFluxoMs > 0 && (agora - _ultimoPulsoFluxoMs) <= TimeoutPulsoFluxoRecenteMs; if (!pulsoRecente) { vazaoMLs = 0.0; } VazaoFluxoAtualMLs = vazaoMLs; VazaoFluxoCorrigidaAtualMLs = vazaoMLs * FatorCalibracaoFluxoFirmware; FluxoPresenteAtual = pulsoRecente || VazaoFluxoCorrigidaAtualMLs >= VazaoMinimaOperacaoMLs; } void IniciarKick(uint8_t idx) { if (!HabilitarKickInicial || idx == 0) { return; } _kickAtivo = true; _inicioKickMs = millis(); ArmamentoLinhaAtivo = true; } double ObterPotenciaKick(uint8_t idx) { idx = LimitarIndiceBicos(idx); if (idx > 0 && idx <= 2) { return constrain(PotenciaKickPoucosBicosPct, 0.0, 100.0); } if (idx >= 3) { return constrain(PotenciaKickMuitosBicosPct, 0.0, 100.0); } return 0.0; } unsigned long ObterDuracaoKickMs(uint8_t idx) { idx = LimitarIndiceBicos(idx); if (idx > 0 && idx <= 2) { return DuracaoKickPoucosBicosMs; } if (idx >= 3) { return DuracaoKickMuitosBicosMs; } return 0; } double AplicarKickInicial(double potenciaDesejada, uint8_t idx) { if (!_kickAtivo || idx == 0) { return potenciaDesejada; } unsigned long duracao = ObterDuracaoKickMs(idx); unsigned long agora = millis(); if (duracao == 0 || (agora - _inicioKickMs) > duracao) { ResetarKick(); ArmamentoLinhaAtivo = false; return potenciaDesejada; } return MaiorDouble(potenciaDesejada, ObterPotenciaKick(idx)); } double ObterPotenciaMinimaOperacional(uint8_t idx) { idx = LimitarIndiceBicos(idx); double minimo = PotenciaMinAtiva; if (idx > 0) { minimo = MaiorDouble(minimo, PotenciaMinimaComBicosPorQtd[idx]); } minimo = MaiorDouble(minimo, PotenciaMinPID); return constrain(minimo, 0.0, PotenciaMaxPID); } double ObterPressaoMaximaMapeada(uint8_t idx) { idx = LimitarIndiceBicos(idx); return PressaoMaximaMapeadaPorBicos[idx]; } double ObterPotenciaMaximaMapeada(uint8_t idx) { idx = LimitarIndiceBicos(idx); double potencia = PotenciaBaseMaxPsi[idx]; if (potencia <= 0.1) { potencia = PotenciaBase22Psi[idx]; } if (potencia <= 0.1) { potencia = PotenciaBase15Psi[idx]; } return constrain(potencia, 0.0, 100.0); } double Interpolar(double x1, double y1, double x2, double y2, double x) { if (fabs(x2 - x1) < 0.001) { return constrain(y2, 0.0, 100.0); } double t = (x - x1) / (x2 - x1); t = constrain(t, 0.0, 1.0); double y = y1 + ((y2 - y1) * t); return constrain(y, 0.0, 100.0); } double CalcularPotenciaFeedForward(uint8_t idx, double pressaoSP) { idx = LimitarIndiceBicos(idx); if (!HabilitarFeedForward || idx == 0 || pressaoSP <= 0.0) { return 0.0; } double p15 = constrain(PotenciaBase15Psi[idx], 0.0, 100.0); double p22 = constrain(PotenciaBase22Psi[idx], 0.0, 100.0); double pMax = ObterPotenciaMaximaMapeada(idx); double pressaoMax = ObterPressaoMaximaMapeada(idx); double potencia = 0.0; if (pressaoMax > 0.0 && pressaoSP >= pressaoMax) { potencia = pMax; } else if (pressaoMax > 0.0 && pressaoMax <= PressaoReferenciaBaixaPsi) { potencia = Interpolar(0.0, 0.0, pressaoMax, pMax, pressaoSP); } else if (pressaoSP <= PressaoReferenciaBaixaPsi) { potencia = p15; } else if (pressaoMax > PressaoReferenciaBaixaPsi && pressaoMax <= PressaoReferenciaAltaPsi) { potencia = Interpolar( PressaoReferenciaBaixaPsi, p15, pressaoMax, pMax, pressaoSP ); } else if (pressaoSP <= PressaoReferenciaAltaPsi) { potencia = Interpolar( PressaoReferenciaBaixaPsi, p15, PressaoReferenciaAltaPsi, p22, pressaoSP ); } else if (pressaoMax > PressaoReferenciaAltaPsi && pressaoSP <= pressaoMax) { potencia = Interpolar( PressaoReferenciaAltaPsi, p22, pressaoMax, pMax, pressaoSP ); } else { potencia = pMax; } return constrain(potencia * constrain(PesoFeedForward, 0.0, 1.0), 0.0, 100.0); } double CalcularPressaoSPControle(uint8_t idx, double pressaoSP) { idx = LimitarIndiceBicos(idx); double spControle = pressaoSP; bool precisaFluxo = idx > 0 && HabilitarSupervisorFluxo && SensorFluxoDisponivel() && !FluxoPresenteAtual; if (precisaFluxo) { if (idx <= 2 && PressaoSPMinSemFluxoPoucosBicosPsi > 0.0) { spControle = MaiorDouble(spControle, PressaoSPMinSemFluxoPoucosBicosPsi); } else if (idx >= 3 && PressaoSPMinSemFluxoMuitosBicosPsi > 0.0) { spControle = MaiorDouble(spControle, PressaoSPMinSemFluxoMuitosBicosPsi); } } return constrain(spControle, 0.0, 250.0); } bool SPForaDoMapa(uint8_t idx, double pressaoSP) { if (idx == 0 || pressaoSP <= 0.0) { return false; } double pressaoMax = ObterPressaoMaximaMapeada(idx); if (pressaoMax <= 0.0) { return false; } return pressaoSP > pressaoMax; } bool TratarCorteSeguranca() { if (sensorPressao == nullptr || PressaoCorteSegurancaPsi <= 0.0) { CorteSegurancaAtivo = false; return false; } double pressaoProtecao = MaiorDouble(PressaoFiltrada, PressaoSensorAtual); if (pressaoProtecao >= PressaoCorteSegurancaPsi) { CorteSegurancaAtivo = true; } if (CorteSegurancaAtivo) { AplicarSaida(false, 0); ResetarIntegralPressao(); bombaDesligadaPorPressao = true; if (pressaoProtecao <= (PressaoCorteSegurancaPsi - HistereseRearmeSegurancaPsi)) { CorteSegurancaAtivo = false; bombaDesligadaPorPressao = false; } return true; } return false; } void ControlarOnOffLegado(double pressaoSP, double potenciaOn, double histeresePsi) { PressaoSPControleAtual = pressaoSP; PotenciaFeedForwardAtual = 0.0; CorrecaoPIAtual = 0.0; double histerese = constrain(histeresePsi, 0.5, 30.0); if (PressaoFiltrada >= (pressaoSP + histerese)) { bombaDesligadaPorPressao = true; } if (PressaoFiltrada <= (pressaoSP - histerese)) { bombaDesligadaPorPressao = false; } if (bombaDesligadaPorPressao) { AplicarSaida(false, 0); return; } double potencia = potenciaOn; if (potencia <= 0.0) { potencia = PotenciaMaxPID; } if (QtdBicosLigados > 0) { potencia = MaiorDouble(potencia, ObterPotenciaMinimaOperacional(QtdBicosLigados)); } potencia = constrain(potencia, 0.0, PotenciaMaxPID); AplicarPotenciaPercentual(potencia); } void ControlarPressaoSuaveComFluxo(double dt) { uint8_t idx = LimitarIndiceBicos(QtdBicosLigados); if (idx != _ultimaQtdBicosLigados) { uint8_t anterior = _ultimaQtdBicosLigados; _ultimaQtdBicosLigados = idx; ResetarIntegralPressao(); if (anterior == 0 && idx > 0) { IniciarKick(idx); } } if (idx == 0) { ResetarEstadosDeBicosFechados(); return; } double spControle = CalcularPressaoSPControle(idx, _PressaoSP); PressaoSPControleAtual = spControle; PressaoMaximaMapeadaAtual = ObterPressaoMaximaMapeada(idx); PressaoSPForaDoMapaAtual = SPForaDoMapa(idx, spControle); PressaoSPLimitadoPorCapacidade = false; double potenciaBase = CalcularPotenciaFeedForward(idx, spControle); PotenciaFeedForwardAtual = potenciaBase; double erro = spControle - PressaoFiltrada; double erroParaPI = erro; if (fabs(erroParaPI) <= DeadbandPressaoPsi) { erroParaPI = 0.0; } double derivadaMedicao = 0.0; if (dt > 0.001) { derivadaMedicao = (PressaoFiltrada - _ultimaPressaoFiltrada) / dt; } double minimo = ObterPotenciaMinimaOperacional(idx); double maximo = PotenciaMaxPID; /* Integrador anti-windup. Integra pouco quando saturado. */ double saidaTeste = potenciaBase + (Kp * erroParaPI) + (Ki * _integralPressao) - (Kd * derivadaMedicao); bool saturadoAlto = saidaTeste >= maximo && erroParaPI > 0.0; bool saturadoBaixo = saidaTeste <= minimo && erroParaPI < 0.0; if (!saturadoAlto && !saturadoBaixo) { _integralPressao += erroParaPI * dt; _integralPressao = constrain(_integralPressao, -220.0, 220.0); } double correcao = (Kp * erroParaPI) + (Ki * _integralPressao) - (Kd * derivadaMedicao); CorrecaoPIAtual = correcao; double potenciaDesejada = potenciaBase + correcao; potenciaDesejada = constrain(potenciaDesejada, minimo, maximo); potenciaDesejada = AplicarSupervisorFluxo(potenciaDesejada, dt); potenciaDesejada = AplicarKickInicial(potenciaDesejada, idx); double potenciaComRampa = AplicarRampaPercentual(potenciaDesejada, dt); AplicarPotenciaPercentual(potenciaComRampa); _ultimoErroPressao = erro; _ultimaPressaoFiltrada = PressaoFiltrada; } double AplicarSupervisorFluxo(double potenciaDesejada, double dt) { uint8_t idx = LimitarIndiceBicos(QtdBicosLigados); if (!HabilitarSupervisorFluxo || !SensorFluxoDisponivel() || idx == 0) { ResetarSupervisorFluxo(); return potenciaDesejada; } unsigned long agora = millis(); bool fluxoOk = FluxoPresenteAtual || VazaoFluxoCorrigidaAtualMLs >= VazaoMinimaOperacaoMLs; if (!fluxoOk) { FluxoInsuficienteAtual = true; if (_inicioSemFluxoMs == 0) { _inicioSemFluxoMs = agora; } _inicioComFluxoMs = 0; if ((agora - _inicioSemFluxoMs) >= TempoSemFluxoParaBoostMs) { RecuperacaoFluxoAtiva = true; } } else { FluxoInsuficienteAtual = false; _inicioSemFluxoMs = 0; if (_inicioComFluxoMs == 0) { _inicioComFluxoMs = agora; } if ((agora - _inicioComFluxoMs) >= TempoComFluxoParaAliviarMs) { RecuperacaoFluxoAtiva = false; } } double maxBoost = (idx <= 2) ? BoostFluxoMaxPoucosBicosPct : BoostFluxoMaxMuitosBicosPct; maxBoost = constrain(maxBoost, 0.0, 100.0); bool permitirSubirBoost = RecuperacaoFluxoAtiva && PressaoFiltrada < PressaoMaxAssistenciaFluxoPsi; if (permitirSubirBoost) { BoostFluxoAtualPct += TaxaBoostFluxoSubidaPctPorSegundo * dt; } else { BoostFluxoAtualPct -= TaxaBoostFluxoDescidaPctPorSegundo * dt; } BoostFluxoAtualPct = constrain(BoostFluxoAtualPct, 0.0, maxBoost); if (BoostFluxoAtualPct > 0.1) { potenciaDesejada = MaiorDouble( potenciaDesejada, ObterPotenciaMinimaOperacional(idx) ); potenciaDesejada += BoostFluxoAtualPct; } return constrain(potenciaDesejada, 0.0, PotenciaMaxPID); } double AplicarRampaPercentual(double potenciaDesejada, double dt) { potenciaDesejada = constrain(potenciaDesejada, 0.0, PotenciaMaxPID); double atual = PotenciaFinalAtual; double delta = potenciaDesejada - atual; if (fabs(delta) < 0.01) { return potenciaDesejada; } double taxa = (delta > 0.0) ? TaxaSubidaPctPorSegundo : TaxaDescidaPctPorSegundo; double maxDelta = taxa * dt; if (fabs(delta) <= maxDelta) { return potenciaDesejada; } return atual + ((delta > 0.0) ? maxDelta : -maxDelta); } double CalcularDtSegundos(unsigned long agoraMs) { if (_ultimoTempoDtMs == 0) { _ultimoTempoDtMs = agoraMs; return ((double)IntervaloControleMs) / 1000.0; } unsigned long deltaMs = agoraMs - _ultimoTempoDtMs; _ultimoTempoDtMs = agoraMs; if (deltaMs == 0) { return ((double)IntervaloControleMs) / 1000.0; } double dt = ((double)deltaMs) / 1000.0; return constrain(dt, 0.005, 0.500); } void AplicarPotenciaPercentual(double percentual) { percentual = constrain(percentual, 0.0, 100.0); if (_EstadoControle != Estado::Ligado || percentual <= 0.0) { AplicarSaida(false, 0); return; } int pwm = PercentualParaPWM(percentual); AplicarSaida(true, pwm); } int PercentualParaPWM(double percentual) { percentual = constrain(percentual, 0.0, 100.0); return (int)round((percentual / 100.0) * PWM_MAX_VALUE); } double PWMParaPercentual(int pwm) { pwm = constrain(pwm, 0, PWM_MAX_VALUE); return (((double)pwm) * 100.0) / ((double)PWM_MAX_VALUE); } void AplicarSaida(bool ligado, int pwm) { pwm = constrain(pwm, 0, PWM_MAX_VALUE); if (!ligado || pwm <= 0) { ledcWrite(_canalIN1, 0); ledcWrite(_canalIN2, 0); PWM_SP = 0; PWM_Set = 0; PotenciaFinalAtual = 0.0; PercentualPotenciaLeitura = 0.0; PotenciaLeitura = 0; EstadoLeitura = Estado::Desligado; return; } if (_sentido == Sentido::Horario) { ledcWrite(_canalIN1, pwm); ledcWrite(_canalIN2, 0); } else { ledcWrite(_canalIN1, 0); ledcWrite(_canalIN2, pwm); } PWM_SP = pwm; PWM_Set = pwm; PotenciaFinalAtual = PWMParaPercentual(pwm); PercentualPotenciaLeitura = PotenciaFinalAtual; PotenciaLeitura = (int)round(PercentualPotenciaLeitura); EstadoLeitura = Estado::Ligado; } uint8_t LimitarByte(double valor) { valor = constrain(valor, 0.0, 255.0); return static_cast(round(valor)); } double MaiorDouble(double a, double b) { return (a > b) ? a : b; } void MostrarLog(String mensagem) { if (DebugMode) { PrintTela("[BMB " + _ID + "] " + mensagem); } } }; #endif