agrobot_base/Firmware/Modulos/BombaPressurizadoraModel_v2.h

815 lines
21 KiB
C++

#ifndef BombaPressurizadoraModel_v2_h
#define BombaPressurizadoraModel_v2_h
#include <Arduino.h>
#include <vector>
#include <math.h>
#include "SerialService.h"
#include "Pinout.h"
#include "Utils.h"
#include "IComponenteCAN.h"
#include "SensorPressaoModel_v2.h"
#include "BicoPulverizadorModel_v2.h"
struct EstadoAnteriorBomba {
Estado estado = Estado::Desligado;
double potenciaSP = 0.0;
double pressaoSP = 0.0;
ModoControleBomba modoControle = static_cast<ModoControleBomba>(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
)
: _ID(id),
ID_Num(idNum),
_pinoIN1(pinoIN1, barramento, _OUTPUT),
_pinoIN2(pinoIN2, barramento, _OUTPUT),
_canalIN1(canalIN1),
_canalIN2(canalIN2),
_sentido(sentido),
sensorPressao(sensor),
_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;
Estado EstadoLeitura = Estado::Desligado;
int PWM_SP = 0;
int PWM_Set = 0;
int PotenciaLeitura = 0;
double PercentualPotenciaLeitura = 0.0;
Estado _EstadoControle = Estado::Desligado;
double _PressaoSP = 0.0;
double _PotenciaSP = 0.0;
ModoControleBomba ModoControle = static_cast<ModoControleBomba>(0);
/*
Ganhos do controle fechado.
Recomendacao inicial para campo:
- Comece com Kd = 0.0.
- Use o feedforward para carregar a demanda principal.
- Use Kp/Ki apenas para corrigir diferencas pequenas.
*/
double Kp = 1.8;
double Ki = 0.35;
double Kd = 0.0;
double PotenciaMinPID = 18.0;
double PotenciaMaxPID = 100.0;
/*
Potencia minima realmente util da bomba.
Abaixo disso a bomba pode girar, mas nao pressurizar de verdade.
*/
double PotenciaMinAtiva = 22.0;
/*
Tabelas de feedforward.
Indice = quantidade de bicos ligados.
Exemplo: PotenciaBase30Psi[4] = potencia base para 4 bicos ligados em 30 psi.
Valores abaixo sao CHUTES INICIAIS seguros para teste.
Substitua pelos valores medidos em bancada/campo.
*/
double PotenciaBase15Psi[8] = {
0.0, // 0 bicos
20.0, // 1 bico
26.0, // 2 bicos
33.0, // 3 bicos
41.0, // 4 bicos
50.0, // 5 bicos
60.0, // 6 bicos
72.0 // 7 bicos
};
double PotenciaBase30Psi[8] = {
0.0, // 0 bicos
34.0, // 1 bico
45.0, // 2 bicos
57.0, // 3 bicos
69.0, // 4 bicos
81.0, // 5 bicos
93.0, // 6 bicos
100.0 // 7 bicos
};
double PressaoReferenciaBaixaPsi = 15.0;
double PressaoReferenciaAltaPsi = 30.0;
/*
Histerese usada somente no modo ON/OFF antigo.
*/
double HistereseOnOffPsi = 3.0;
/*
Filtro leve para pressao.
0.0 = nao atualiza; 1.0 = sem filtro.
*/
double AlphaFiltroPressao = 0.35;
/*
Limites da correcao PI em cima da tabela.
A tabela deve fazer o grosso. O PI so afina.
*/
double CorrecaoMinPI = -25.0;
double CorrecaoMaxPI = 25.0;
/*
Rampa de potencia.
Subida mais rapida para nao deixar a linha morrer quando abrem bicos.
Descida mais suave para evitar pico quando fecham bicos.
*/
double TaxaSubidaPctPorSegundo = 350.0;
double TaxaDescidaPctPorSegundo = 180.0;
unsigned long IntervaloControleMs = 50;
/*
Por seguranca, a bomba desliga quando nao existe bico aberto em malha fechada.
Se houver bypass/retorno ou valvula reguladora e voce quiser manter linha pre-pressurizada,
altere para false.
*/
bool DesligarSemBicosAbertos = true;
uint8_t QtdBicosLigados = 0;
double PressaoAtual = 0.0;
double PressaoFiltrada = 0.0;
double PotenciaFeedForwardAtual = 0.0;
double CorrecaoPIAtual = 0.0;
double PotenciaFinalAtual = 0.0;
void ConfigurarBicos(BicoPulverizador_v2** bicos, size_t qtdBicos) {
_bicos = bicos;
_qtdBicos = qtdBicos;
}
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
) {
PotenciaBase30Psi[0] = 0.0;
PotenciaBase30Psi[1] = b1;
PotenciaBase30Psi[2] = b2;
PotenciaBase30Psi[3] = b3;
PotenciaBase30Psi[4] = b4;
PotenciaBase30Psi[5] = b5;
PotenciaBase30Psi[6] = b6;
PotenciaBase30Psi[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);
if (PotenciaMinAtiva < PotenciaMinPID) {
PotenciaMinAtiva = PotenciaMinPID;
}
}
void ConfigurarModoControle(ModoControleBomba modo) {
ModoControle = modo;
ResetarControleFechado();
}
void Inicializar() {
if (Iniciado) {
MostrarLog("Bomba ja inicializada");
return;
}
_pinoIN1.Conectar(false);
_pinoIN2.Conectar(false);
bool in1Ok = _pinoIN1.Definido();
bool in2Ok = _pinoIN2.Definido();
if (!in1Ok || !in2Ok) {
Iniciado = false;
MostrarLog("Falha ao iniciar bomba: pinos invalidos");
return;
}
ledcSetup(_canalIN1, PWM_FREQ_HZ, PWM_RESOLUTION_BITS);
ledcSetup(_canalIN2, PWM_FREQ_HZ, PWM_RESOLUTION_BITS);
ledcAttachPin(_pinoIN1.num, _canalIN1);
ledcAttachPin(_pinoIN2.num, _canalIN2);
AplicarSaida(false, 0);
Iniciado = true;
EstadoLeitura = Estado::Desligado;
_EstadoControle = Estado::Desligado;
_ultimoControleMs = millis();
_ultimoTempoDtMs = _ultimoControleMs;
MostrarLog("Bomba iniciada com sucesso");
}
void Desligar() {
if (!Iniciado) {
return;
}
AplicarSaida(false, 0);
_pinoIN1.Desconectar();
_pinoIN2.Desconectar();
Iniciado = false;
EstadoLeitura = Estado::Desligado;
_EstadoControle = Estado::Desligado;
ResetarControleFechado();
MostrarLog("Bomba desligada");
}
void AtualizarControle(
Estado estado,
double potenciaSP,
double pressaoSP = 0.0
) {
_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<uint8_t>(ModoControle) != 0
);
if (_EstadoControle != Estado::Ligado) {
AplicarSaida(false, 0);
ResetarControleFechado();
AtualizarLeitura();
return;
}
if (!MalhaFechada) {
double potencia = _PotenciaSP;
if (potencia > 0.0 && potencia < PotenciaMinAtiva) {
potencia = PotenciaMinAtiva;
}
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;
if (_EstadoControle != Estado::Ligado) {
AplicarSaida(false, 0);
ResetarControleFechado();
AtualizarLeitura();
return;
}
AtualizarPressao();
QtdBicosLigados = ContarBicosLigados();
if (QtdBicosLigados == 0 && DesligarSemBicosAbertos) {
/*
Sem bico aberto nao existe demanda de aplicacao.
Por padrao, mantem a bomba desligada para nao pressurizar linha fechada.
Se houver bypass/retorno/regulador e voce quiser manter a linha pre-pressurizada,
defina DesligarSemBicosAbertos = false.
A rotina de lavagem continua funcionando, porque liga todos os bicos antes da bomba.
*/
if (_PressaoSP > 0.0 && sensorPressao != nullptr) {
AplicarSaida(false, 0);
ResetarControleFechado();
AtualizarLeitura();
return;
}
}
if (sensorPressao == nullptr || _PressaoSP <= 0.0) {
double potencia = _PotenciaSP;
if (potencia > 0.0 && potencia < PotenciaMinAtiva) {
potencia = PotenciaMinAtiva;
}
AplicarPotenciaPercentual(potencia);
AtualizarLeitura();
return;
}
if (static_cast<uint8_t>(ModoControle) == 0) {
ControlarOnOff();
AtualizarLeitura();
return;
}
ControlarFeedForwardPI(dt);
AtualizarLeitura();
}
void AtualizarLeitura() {
if (!Iniciado) {
EstadoLeitura = Estado::Desligado;
PercentualPotenciaLeitura = 0.0;
PotenciaLeitura = 0;
return;
}
PercentualPotenciaLeitura = constrain(PotenciaFinalAtual, 0.0, 100.0);
PotenciaLeitura = (int)round(PercentualPotenciaLeitura);
EstadoLeitura = (PercentualPotenciaLeitura > 2.0) ? Estado::Ligado : Estado::Desligado;
if (sensorPressao != nullptr) {
AtualizarPressao();
}
}
void RequisitarDados() {
AtualizarLeitura();
}
bool ProcessarComando(const std::vector<uint8_t>& 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<ModoControleBomba>(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<uint8_t> MontarMensagemCAN(CanMessagePosicaoDados posicao) {
std::vector<uint8_t> data;
data.push_back(static_cast<uint8_t>(posicao));
data.push_back(ID_Num);
switch (posicao) {
case CanMessagePosicaoDados::Status: {
data.push_back(Iniciado ? 1 : 0);
break;
}
case CanMessagePosicaoDados::Dados1: {
AtualizarLeitura();
data.push_back(static_cast<uint8_t>(EstadoLeitura));
data.push_back(LimitarByte(PercentualPotenciaLeitura));
data.push_back(LimitarByte(_PressaoSP));
data.push_back(LimitarByte(PressaoAtual));
/*
Bytes extras mantendo o pacote em 8 bytes:
- qtd bicos ligados
- potencia feedforward usada antes da correcao PI
*/
data.push_back(QtdBicosLigados);
data.push_back(LimitarByte(PotenciaFeedForwardAtual));
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;
double _integral = 0.0;
double _ultimoErro = 0.0;
double _ultimaPressaoFiltrada = 0.0;
uint8_t _ultimaQtdBicosLigados = 0;
bool _filtroInicializado = false;
uint8_t ContarBicosLigados() {
if (_bicos == nullptr || _qtdBicos == 0) {
return 0;
}
uint8_t qtd = 0;
for (size_t i = 0; i < _qtdBicos; i++) {
BicoPulverizador_v2* bico = _bicos[i];
if (bico == nullptr || !bico->Iniciado) {
continue;
}
bico->AtualizarLeitura();
if (bico->_Status == Estado::Ligado) {
qtd++;
}
}
return qtd;
}
void AtualizarPressao() {
if (sensorPressao == nullptr) {
PressaoAtual = 0.0;
return;
}
sensorPressao->RequisitarDados();
PressaoAtual = sensorPressao->Pressao;
if (!_filtroInicializado) {
PressaoFiltrada = PressaoAtual;
_ultimaPressaoFiltrada = PressaoFiltrada;
_filtroInicializado = true;
return;
}
double alpha = constrain(AlphaFiltroPressao, 0.01, 1.0);
PressaoFiltrada = (alpha * PressaoAtual) + ((1.0 - alpha) * PressaoFiltrada);
}
void ControlarOnOff() {
if (PressaoAtual >= (_PressaoSP + HistereseOnOffPsi)) {
bombaDesligadaPorPressao = true;
}
if (PressaoAtual <= (_PressaoSP - HistereseOnOffPsi)) {
bombaDesligadaPorPressao = false;
}
if (bombaDesligadaPorPressao) {
AplicarSaida(false, 0);
return;
}
double potencia = _PotenciaSP;
if (potencia > 0.0 && potencia < PotenciaMinAtiva) {
potencia = PotenciaMinAtiva;
}
AplicarPotenciaPercentual(potencia);
}
void ControlarFeedForwardPI(double dt) {
if (QtdBicosLigados != _ultimaQtdBicosLigados) {
_ultimaQtdBicosLigados = QtdBicosLigados;
/*
Mudou a demanda hidraulica.
Zera o erro acumulado para evitar integral velha brigando com a nova tabela.
*/
_integral = 0.0;
_ultimoErro = 0.0;
}
double potenciaBase = CalcularPotenciaFeedForward(QtdBicosLigados, _PressaoSP);
PotenciaFeedForwardAtual = potenciaBase;
double erro = _PressaoSP - PressaoFiltrada;
double derivadaMedicao = 0.0;
if (dt > 0.001) {
derivadaMedicao = (PressaoFiltrada - _ultimaPressaoFiltrada) / dt;
}
double correcaoSemIntegralNova =
(Kp * erro) +
(Ki * _integral) -
(Kd * derivadaMedicao);
correcaoSemIntegralNova = constrain(
correcaoSemIntegralNova,
CorrecaoMinPI,
CorrecaoMaxPI
);
double saidaSemIntegralNova = potenciaBase + correcaoSemIntegralNova;
double minimo = (QtdBicosLigados > 0) ? PotenciaMinAtiva : 0.0;
double maximo = PotenciaMaxPID;
bool saturouAlto = saidaSemIntegralNova >= maximo && erro > 0.0;
bool saturouBaixo = saidaSemIntegralNova <= minimo && erro < 0.0;
if (!saturouAlto && !saturouBaixo) {
_integral += erro * dt;
_integral = constrain(_integral, -120.0, 120.0);
}
double correcao =
(Kp * erro) +
(Ki * _integral) -
(Kd * derivadaMedicao);
correcao = constrain(correcao, CorrecaoMinPI, CorrecaoMaxPI);
CorrecaoPIAtual = correcao;
double potenciaDesejada = potenciaBase + correcao;
potenciaDesejada = constrain(potenciaDesejada, minimo, maximo);
double potenciaComRampa = AplicarRampaPercentual(potenciaDesejada, dt);
AplicarPotenciaPercentual(potenciaComRampa);
_ultimoErro = erro;
_ultimaPressaoFiltrada = PressaoFiltrada;
}
double CalcularPotenciaFeedForward(uint8_t qtdBicos, double pressaoSP) {
if (qtdBicos == 0) {
return 0.0;
}
if (qtdBicos > 7) {
qtdBicos = 7;
}
double p15 = PotenciaBase15Psi[qtdBicos];
double p30 = PotenciaBase30Psi[qtdBicos];
bool p15Valida = p15 > 0.1;
bool p30Valida = p30 > 0.1;
if (!p15Valida && !p30Valida) {
return _PotenciaSP;
}
if (!p15Valida) {
return constrain(p30, 0.0, 100.0);
}
if (!p30Valida) {
return constrain(p15, 0.0, 100.0);
}
if (pressaoSP <= PressaoReferenciaBaixaPsi) {
return constrain(p15, 0.0, 100.0);
}
if (pressaoSP >= PressaoReferenciaAltaPsi) {
return constrain(p30, 0.0, 100.0);
}
double t =
(pressaoSP - PressaoReferenciaBaixaPsi) /
(PressaoReferenciaAltaPsi - PressaoReferenciaBaixaPsi);
double potencia = p15 + ((p30 - p15) * t);
return constrain(potencia, 0.0, 100.0);
}
double AplicarRampaPercentual(double potenciaDesejada, double dt) {
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;
}
void ResetarControleFechado() {
_integral = 0.0;
_ultimoErro = 0.0;
_ultimaPressaoFiltrada = PressaoFiltrada;
_ultimaQtdBicosLigados = 0;
CorrecaoPIAtual = 0.0;
PotenciaFeedForwardAtual = 0.0;
bombaDesligadaPorPressao = false;
}
uint8_t LimitarByte(double valor) {
valor = constrain(valor, 0.0, 255.0);
return static_cast<uint8_t>(round(valor));
}
void MostrarLog(String mensagem) {
if (DebugMode) {
PrintTela("[BMB " + _ID + "] " + mensagem);
}
}
};
#endif