pequenos ajustes no operation control e controle de potencia na bomba pressurizadora

This commit is contained in:
Diego Freitas 2026-08-18 12:36:34 -03:00
parent 3910ae2d35
commit fc95b86f14
4 changed files with 606 additions and 147 deletions

View File

@ -1,38 +1,36 @@
using System.IO;
using System.Windows;
using AgroBase.Models;
using BruTile;
using BruTile.MbTiles;
using BruTile.Predefined;
using SQLite;
using NetTopologySuite.IO;
using NetTopologySuite.Features;
using NetTopologySuite.Geometries;
using Mapsui;
using Mapsui.Extensions;
using Mapsui.Layers;
using Mapsui.Manipulations;
using Mapsui.Nts;
using Mapsui.Projections;
using Mapsui.Providers;
using Mapsui.Styles;
using Mapsui.Projections;
using Mapsui.Tiling.Layers;
using Mapsui.Nts;
using Mapsui.UI.Wpf;
// Aliases para evitar conflito com NetTopologySuite.* (NTS)
using UserControl = System.Windows.Controls.UserControl;
using OpenFileDialog = Microsoft.Win32.OpenFileDialog;
using MIFeature = Mapsui.IFeature;
using NetTopologySuite.Features;
using NetTopologySuite.Geometries;
using NetTopologySuite.IO;
using OperationControl.Models;
using SQLite;
using System.Globalization;
using System.IO;
using System.Windows;
using System.Windows.Input;
using static AgroBase.Models.Enums;
using Brush = Mapsui.Styles.Brush;
using Pen = Mapsui.Styles.Pen;
using Color = Mapsui.Styles.Color;
using Font = Mapsui.Styles.Font;
using MIFeature = Mapsui.IFeature;
using NFeature = NetTopologySuite.Features.Feature;
using Mapsui.Manipulations;
using Mapsui.Extensions;
using OperationControl.Models;
using static AgroBase.Models.Enums;
using System.Windows.Input;
using AgroBase.Models;
using OpenFileDialog = Microsoft.Win32.OpenFileDialog;
using Pen = Mapsui.Styles.Pen;
// Aliases para evitar conflito com NetTopologySuite.* (NTS)
using UserControl = System.Windows.Controls.UserControl;
namespace OperationControl.Controls
@ -985,7 +983,6 @@ namespace OperationControl.Controls
{
foiGeradoAutomaticamente = false;
// tenta achar algum campo comum de ID
string? idOriginal =
GetAttr(feature, "Id") ??
GetAttr(feature, "ID") ??
@ -996,35 +993,29 @@ namespace OperationControl.Controls
idOriginal = idOriginal?.Trim();
// caso esteja vazio/null
if (string.IsNullOrWhiteSpace(idOriginal))
// Mantém o ID original quando ele existe e ainda é único.
if (!string.IsNullOrWhiteSpace(idOriginal) &&
idsUsados.Add(idOriginal))
{
string novoId = $"AUTO_{index + 1:D4}";
idsUsados.Add(novoId);
foiGeradoAutomaticamente = true;
return novoId;
}
// se ainda não foi usado, aceita
if (!idsUsados.Contains(idOriginal))
{
idsUsados.Add(idOriginal);
return idOriginal;
}
// se repetiu, mantém rastreabilidade mas torna único
int sufixo = 2;
string candidato;
do
{
candidato = $"{idOriginal}_{sufixo:D2}";
sufixo++;
}
while (idsUsados.Contains(candidato));
// Ausente ou repetido: procura um novo ID estritamente numérico.
int candidatoNumerico = Math.Max(1, index + 1);
idsUsados.Add(candidato);
while (idsUsados.Contains(candidatoNumerico.ToString(
System.Globalization.CultureInfo.InvariantCulture)))
{
candidatoNumerico++;
}
string novoId = candidatoNumerico.ToString(
System.Globalization.CultureInfo.InvariantCulture);
idsUsados.Add(novoId);
foiGeradoAutomaticamente = true;
return candidato;
return novoId;
}
private void OnCenterAreaClicked(object sender, RoutedEventArgs e)
@ -1057,86 +1048,424 @@ namespace OperationControl.Controls
private Dictionary<string, string> ParseFeatureAttributes(MIFeature feature)
{
var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var dict = new Dictionary<string, string>(
StringComparer.OrdinalIgnoreCase
);
if (feature == null)
return dict;
var text = feature.ToStringOfKeyValuePairs();
string? text;
try
{
text = feature.ToStringOfKeyValuePairs();
}
catch
{
return dict;
}
if (string.IsNullOrWhiteSpace(text))
return dict;
var lines = text.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
var lines = text.Split(
new[] { '\n', '\r' },
StringSplitOptions.RemoveEmptyEntries
);
foreach (var line in lines)
{
var parts = line.Split(new[] { ':' }, 2); // divide em "Nome" e "valor"
if (parts.Length == 2)
{
var key = parts[0].Trim();
var value = parts[1].Trim();
if (!string.IsNullOrEmpty(key))
dict[key] = value;
}
int separatorIndex = line.IndexOf(':');
if (separatorIndex <= 0)
continue;
var key = line[..separatorIndex].Trim();
var value = line[(separatorIndex + 1)..].Trim();
if (!string.IsNullOrWhiteSpace(key))
dict[key] = value;
}
return dict;
}
private string? GetAttr(MIFeature feature, string fieldName)
private string? GetAttr(IReadOnlyDictionary<string, string> attributes, string fieldName)
{
var dict = ParseFeatureAttributes(feature);
return dict.TryGetValue(fieldName, out var v) ? v : null;
if (attributes.TryGetValue(fieldName, out var value) &&
!string.IsNullOrWhiteSpace(value))
{
return value.Trim();
}
return null;
}
private string? GetAttr(MIFeature? feature, string fieldName)
{
if (feature == null || string.IsNullOrWhiteSpace(fieldName))
return null;
try
{
var value = feature[fieldName];
if (value == null || value is DBNull)
return null;
var text = Convert.ToString(
value,
System.Globalization.CultureInfo.InvariantCulture
);
return string.IsNullOrWhiteSpace(text)
? null
: text.Trim();
}
catch
{
return null;
}
}
private string GetFirstAttr(IReadOnlyDictionary<string, string> attributes, string fallback, params string[] fieldNames)
{
foreach (var fieldName in fieldNames)
{
var value = GetAttr(attributes, fieldName);
if (!string.IsNullOrWhiteSpace(value))
return value;
}
return fallback;
}
private double GetDoubleAttr(IReadOnlyDictionary<string, string> attributes, double fallback, params string[] fieldNames)
{
foreach (var fieldName in fieldNames)
{
var value = GetAttr(attributes, fieldName);
if (string.IsNullOrWhiteSpace(value))
continue;
// Primeiro tenta o padrão GeoJSON/shapefile: ponto decimal.
if (double.TryParse(
value,
NumberStyles.Float | NumberStyles.AllowThousands,
CultureInfo.InvariantCulture,
out var invariantValue))
{
return invariantValue;
}
// Depois tenta a cultura atual, caso algum mapa use vírgula.
if (double.TryParse(
value,
NumberStyles.Float | NumberStyles.AllowThousands,
CultureInfo.CurrentCulture,
out var currentValue))
{
return currentValue;
}
}
return fallback;
}
private List<List<double>> ExtrairCoordenadas(MIFeature feature)
{
var coordinates = new List<List<double>>();
if (feature == null)
return coordinates;
try
{
dynamic geometry = ((dynamic)feature).Geometry;
if (geometry == null || geometry.CoordinateSequence == null)
return coordinates;
string? sequenceText = geometry.CoordinateSequence.ToString();
if (string.IsNullOrWhiteSpace(sequenceText))
return coordinates;
sequenceText = sequenceText
.Trim()
.TrimStart('(')
.TrimEnd(')');
var pairs = sequenceText.Split(
new[] { ")," },
StringSplitOptions.RemoveEmptyEntries
);
foreach (var pair in pairs)
{
var clean = pair
.Replace("(", "")
.Replace(")", "")
.Trim();
var parts = clean.Split(
',',
StringSplitOptions.RemoveEmptyEntries
);
// PolylineZ pode eventualmente apresentar X,Y,Z.
// Para o GeoJSON do sistema precisamos apenas de longitude/latitude.
if (parts.Length < 2)
continue;
if (!double.TryParse(
parts[0].Trim(),
NumberStyles.Float,
CultureInfo.InvariantCulture,
out var longitude))
{
continue;
}
if (!double.TryParse(
parts[1].Trim(),
NumberStyles.Float,
CultureInfo.InvariantCulture,
out var latitude))
{
continue;
}
if (!double.IsFinite(longitude) ||
!double.IsFinite(latitude))
{
continue;
}
coordinates.Add(new List<double>
{
longitude,
latitude
});
}
}
catch
{
// A feature será descartada pelo chamador se não houver
// coordenadas suficientes.
}
return coordinates;
}
private double CalcularComprimentoMetros(IReadOnlyList<List<double>> coordinates)
{
double comprimento = 0;
for (int i = 1; i < coordinates.Count; i++)
{
var anterior = coordinates[i - 1];
var atual = coordinates[i];
if (anterior.Count < 2 || atual.Count < 2)
continue;
comprimento += CalcularDistanciaHaversine(
anterior[1],
anterior[0],
atual[1],
atual[0]
);
}
return comprimento;
}
private double CalcularDistanciaHaversine(double latitude1, double longitude1, double latitude2, double longitude2)
{
const double raioTerraMetros = 6371000.0;
static double ParaRadianos(double graus) => graus * Math.PI / 180.0;
double dLat = ParaRadianos(latitude2 - latitude1);
double dLon = ParaRadianos(longitude2 - longitude1);
double lat1 = ParaRadianos(latitude1);
double lat2 = ParaRadianos(latitude2);
double a =
Math.Sin(dLat / 2) * Math.Sin(dLat / 2) +
Math.Cos(lat1) *
Math.Cos(lat2) *
Math.Sin(dLon / 2) *
Math.Sin(dLon / 2);
double c = 2 * Math.Atan2(
Math.Sqrt(a),
Math.Sqrt(1 - a)
);
return raioTerraMetros * c;
}
public AgroBase.Models.MapaFeatureCollectionModel CriarDadosMapa(bool selecionadas = false)
{
AgroBase.Models.MapaFeatureCollectionModel _mapa = new AgroBase.Models.MapaFeatureCollectionModel() { type = "FeatureCollection", features = new List<AgroBase.Models.MapaFeatureModel>() };
foreach (var x in RuasMapaCarregado.Where(x => selecionadas ? x.Selected : true))
var mapa = new AgroBase.Models.MapaFeatureCollectionModel
{
string id = ((dynamic)x.Feature).Id.ToString();
string type = ((dynamic)x.Feature).Geometry.GeometryType;
string s = ((dynamic)x.Feature).Geometry.CoordinateSequence.ToString();
s = s.Trim().TrimStart('(').TrimEnd(')');
var pairs = s.Split("),", StringSplitOptions.RemoveEmptyEntries);
var coordinates = new List<List<double>>();
foreach (var p in pairs)
{
var clean = p.Replace("(", "").Replace(")", "").Trim();
var parts = clean.Split(',', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 2 && double.TryParse(parts[0], System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out double lon) && double.TryParse(parts[1], System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out double lat))
{
coordinates.Add(new List<double> { lon, lat });
}
}
AgroBase.Models.MapaFeatureGeometryModel geometry = new AgroBase.Models.MapaFeatureGeometryModel()
{
id = GetAttr(x.Feature, "Id"),
type = type,
coordinates = coordinates
};
_mapa.features.Add(new AgroBase.Models.MapaFeatureModel()
{
type = "Feature",
geometry = geometry,
properties = new AgroBase.Models.MapaFeaturePropertiesModel()
{
Id = GetAttr(x.Feature, "Id"),
Dist1 = double.Parse(GetAttr(x.Feature, "Dist1")),
Dist2 = double.Parse(GetAttr(x.Feature, "Dist2")),
Length = double.Parse(GetAttr(x.Feature, "Length")),
Name = GetAttr(x.Feature, "Name")
}
});
type = "FeatureCollection",
features = new List<AgroBase.Models.MapaFeatureModel>()
};
return _mapa;
if (RuasMapaCarregado == null)
return mapa;
var ruas = RuasMapaCarregado
.Where(x =>
x != null &&
x.Feature != null &&
(!selecionadas || x.Selected))
.ToList();
int indice = 0;
foreach (var rua in ruas)
{
indice++;
var feature = rua.Feature;
var attributes = ParseFeatureAttributes(feature);
var coordinates = ExtrairCoordenadas(feature);
// Uma LineString precisa de pelo menos dois pontos.
if (coordinates.Count < 2)
{
// Se possuir seu sistema de logs, registre aqui:
// MostrarLog($"Feature {indice} ignorada: geometria inválida.");
continue;
}
string featureId;
try
{
featureId =
Convert.ToString(
((dynamic)feature).Id,
CultureInfo.InvariantCulture
) ?? indice.ToString(CultureInfo.InvariantCulture);
}
catch
{
featureId = indice.ToString(CultureInfo.InvariantCulture);
}
string geometryType;
try
{
geometryType =
Convert.ToString(
((dynamic)feature).Geometry.GeometryType,
CultureInfo.InvariantCulture
) ?? "LineString";
}
catch
{
geometryType = "LineString";
}
// Normaliza nomes que algumas bibliotecas retornam.
if (geometryType.Contains(
"LineString",
StringComparison.OrdinalIgnoreCase))
{
geometryType = "LineString";
}
else if (geometryType.Contains(
"PolyLine",
StringComparison.OrdinalIgnoreCase))
{
geometryType = "LineString";
}
string id = GetFirstAttr(
attributes,
featureId,
"Id",
"ID",
"FID",
"OBJECTID"
);
string name = GetFirstAttr(
attributes,
$"Linha {indice}",
"Name",
"Nome",
"LAYER"
);
double calculatedLength =
CalcularComprimentoMetros(coordinates);
double length = GetDoubleAttr(
attributes,
calculatedLength,
"Length",
"Comprimento",
"Shape_Leng",
"SHAPE_LEN"
);
double dist1 = GetDoubleAttr(
attributes,
0.0,
"Dist1"
);
double dist2 = GetDoubleAttr(
attributes,
0.0,
"Dist2"
);
var geometry =
new AgroBase.Models.MapaFeatureGeometryModel
{
id = id,
type = geometryType,
coordinates = coordinates
};
mapa.features.Add(
new AgroBase.Models.MapaFeatureModel
{
type = "Feature",
geometry = geometry,
properties =
new AgroBase.Models.MapaFeaturePropertiesModel
{
Id = id,
Dist1 = dist1,
Dist2 = dist2,
Length = length,
Name = name
}
}
);
}
return mapa;
}
public void CarregarDadosMapa(AgroBase.Models.MapaFeatureCollectionModel Mapa, List<string> RuasSelecionadas, List<double[]> pontosSelecionados)
{
if (RuasMapaCarregado.Any())

View File

@ -27,7 +27,7 @@ namespace OperationControl.Controls
private void BicoCtrl_OnBicoClicked(int index)
{
if (!(VariaveisControleOperacao.RoverEmFoco?.Controle?.PulverizadorAutomatico ?? false))
if ((VariaveisControleOperacao.RoverEmFoco?.Controle?.PulverizadorAutomatico ?? false))
{
System.Windows.MessageBox.Show("Para acionar os bicos manualmente, é necessário que o Pulverizador Automático esteja desligado", "Pulverizador automático", MessageBoxButton.OK, MessageBoxImage.Information);
return;

View File

@ -11,17 +11,35 @@ namespace OperationControl.ViewModels.Views.Operacao.Monitoramento
public class PulverizadorViewModel : INotifyPropertyChanged
{
public ObservableCollection<FrameTipoItem> CameraErvasFrameTipos { get; set; }
private FrameTipoItem _cameraErvasFrameTipoSelecionado;
public FrameTipoItem CameraErvasFrameTipoSelecionado
private FrameTipoItem? _cameraErvasFrameTipoSelecionado;
public FrameTipoItem? CameraErvasFrameTipoSelecionado
{
get => _cameraErvasFrameTipoSelecionado;
set
{
if (_cameraErvasFrameTipoSelecionado != value)
if (value == null ||
ReferenceEquals(_cameraErvasFrameTipoSelecionado, value))
{
_cameraErvasFrameTipoSelecionado = value;
OnPropertyChanged();
VariaveisControleOperacao.EnviarComandoTransmissaoVideo(AgroBase.Models.Enums.T_Code.Cam, true, value.Valor);
return;
}
_cameraErvasFrameTipoSelecionado = value;
OnPropertyChanged();
try
{
VariaveisControleOperacao.EnviarComandoTransmissaoVideo(
T_Code.Cam,
true,
value.Valor
);
}
catch (Exception ex)
{
Models.Variaveis.MostrarLog(
$"Erro ao alterar o tipo de frame da câmera: {ex.Message}"
);
}
}
}
@ -37,8 +55,12 @@ namespace OperationControl.ViewModels.Views.Operacao.Monitoramento
})
.ToList();
CameraErvasFrameTipos = new ObservableCollection<FrameTipoItem>(itens);
CameraErvasFrameTipoSelecionado = CameraErvasFrameTipos.FirstOrDefault();
CameraErvasFrameTipos =
new ObservableCollection<FrameTipoItem>(itens);
// Inicializa sem enviar comando durante a construção da tela.
_cameraErvasFrameTipoSelecionado =
CameraErvasFrameTipos.FirstOrDefault();
}
public ObservableCollection<string> EventosPulverizador { get; set; } = new();

View File

@ -129,7 +129,7 @@ class BombaPressurizadora_v2 : public ComponenteCAN {
Ela segue uma rampa.
*/
double TaxaSubidaPctPorSegundo = 180.0;
double TaxaDescidaPctPorSegundo = 240.0;
double TaxaDescidaPctPorSegundo = 120.0;
unsigned long IntervaloControleMs = 50;
/*
@ -189,12 +189,29 @@ class BombaPressurizadora_v2 : public ComponenteCAN {
double TaxaBoostFluxoDescidaPctPorSegundo = 240.0;
/*
Protecao relativa ao SP. Ela nao substitui o corte absoluto de 120 psi.
A primeira margem cancela kick/boost; a segunda pausa a bomba com histerese.
Alívio suave de sobrepressão.
Não desliga a bomba. Cancela kick/boost e aplica uma redução
proporcional na potência enquanto a pressão estiver acima do SP.
O corte físico completo continua sendo responsabilidade exclusiva
de PressaoCorteSegurancaPsi.
*/
double MargemCancelarAssistenciasPsi = 1.5;
double MargemPausaSobrepressaoPsi = 8.0;
double MargemRearmeSobrepressaoPsi = 1.0;
// Apenas indica que o alívio está efetivamente atuando.
double MargemAtivarAlivioSobrepressaoPsi = 1.2;
// Histerese da flag de diagnóstico.
double MargemMarcarSobrepressaoPsi = 3.0;
double MargemLimparSobrepressaoPsi = 1.0;
// Redução adicional de potência para cada psi acima da margem.
double GanhoAlivioSobrepressaoPctPorPsi = 2.0;
// Limita a intervenção adicional. O PI continua trabalhando junto.
double ReducaoMaximaSobrepressaoPct = 35.0;
bool ProtecaoSobrepressaoRelativaAtiva = false;
/*
@ -1108,21 +1125,48 @@ class BombaPressurizadora_v2 : public ComponenteCAN {
}
double ObterPotenciaMinimaRegulacao(uint8_t idx) {
idx = LimitarIndiceBicos(idx);
(void)idx;
double minimo = PotenciaMinAtiva;
/*
Durante a regulação, permite que o PID desça até o piso elétrico
configurado pelo C#.
if (idx > 0) {
minimo = MaiorDouble(minimo, PotenciaMinimaComBicosPorQtd[idx]);
}
Se PotenciaMinPID = 40%, a bomba continuará ligada, mas poderá
reduzir abaixo dos pisos específicos de 5 ou 6 bicos.
*/
double minimo = MaiorDouble(
PotenciaMinAtiva,
PotenciaMinPID
);
return constrain(minimo, 0.0, PotenciaMaxPID);
return constrain(
minimo,
0.0,
PotenciaMaxPID
);
}
double ObterPotenciaMinimaPartida(uint8_t idx) {
idx = LimitarIndiceBicos(idx);
double minimo = ObterPotenciaMinimaRegulacao(idx);
minimo = MaiorDouble(minimo, PotenciaMinPID);
return constrain(minimo, 0.0, PotenciaMaxPID);
/*
O piso por quantidade de bicos continua sendo usado na partida.
Portanto, preservamos a força necessária para iniciar o fluxo.
*/
if (idx > 0) {
minimo = MaiorDouble(
minimo,
PotenciaMinimaComBicosPorQtd[idx]
);
}
return constrain(
minimo,
0.0,
PotenciaMaxPID
);
}
double ObterPressaoMaximaMapeada(uint8_t idx) {
@ -1273,53 +1317,71 @@ class BombaPressurizadora_v2 : public ComponenteCAN {
return false;
}
bool TratarSobrepressaoRelativa(double pressaoSP) {
double AtualizarAlivioSobrepressao(double pressaoSP) {
if (!SensorPressaoValidoAtual || pressaoSP <= 0.0) {
ProtecaoSobrepressaoRelativaAtiva = false;
return false;
bombaDesligadaPorPressao = false;
return 0.0;
}
/*
Usa a maior pressão entre a leitura rápida e a filtrada.
A leitura rápida permite reagir antes que o filtro acompanhe
completamente a subida da pressão.
*/
double pressaoProtecao = MaiorDouble(
PressaoBrutaControleAtual,
PressaoFiltrada
);
bool atingiuRegiaoDeCancelamento =
double excessoPressao = pressaoProtecao - pressaoSP;
/*
Quando a pressão se aproxima do alvo, kick e boost deixam de
competir com o PID.
*/
bool chegouProximoDoSP =
pressaoProtecao >=
(pressaoSP - MargemCancelarAssistenciasPsi);
if (atingiuRegiaoDeCancelamento) {
if (chegouProximoDoSP) {
ResetarKick();
ResetarSupervisorFluxo();
}
/*
Flag com histerese exclusivamente para diagnóstico/health.
Ela não desliga a bomba.
*/
if (
pressaoProtecao >=
(pressaoSP + MargemPausaSobrepressaoPsi)
excessoPressao >=
MargemMarcarSobrepressaoPsi
) {
ProtecaoSobrepressaoRelativaAtiva = true;
}
if (
else if (
ProtecaoSobrepressaoRelativaAtiva &&
pressaoProtecao <=
(pressaoSP - MargemRearmeSobrepressaoPsi)
excessoPressao <=
MargemLimparSobrepressaoPsi
) {
ProtecaoSobrepressaoRelativaAtiva = false;
}
if (!ProtecaoSobrepressaoRelativaAtiva) {
bombaDesligadaPorPressao = false;
return false;
/*
Somente o corte absoluto pode marcar a bomba como desligada
por pressão.
*/
bombaDesligadaPorPressao = false;
if (
excessoPressao <=
MargemAtivarAlivioSobrepressaoPsi
) {
return 0.0;
}
AplicarSaida(false, 0);
ResetarIntegralPressao();
ResetarKick();
ResetarSupervisorFluxo();
bombaDesligadaPorPressao = true;
return true;
return excessoPressao;
}
void ControlarOnOffLegado(double pressaoSP, double potenciaOn, double histeresePsi) {
@ -1381,10 +1443,7 @@ class BombaPressurizadora_v2 : public ComponenteCAN {
PressaoSPForaDoMapaAtual = SPForaDoMapa(idx, spControle);
PressaoSPLimitadoPorCapacidade = false;
if (TratarSobrepressaoRelativa(spControle)) {
_ultimaPressaoFiltrada = PressaoFiltrada;
return;
}
double excessoSobrepressao = AtualizarAlivioSobrepressao(spControle);
double potenciaBase = CalcularPotenciaFeedForward(idx, spControle);
PotenciaFeedForwardAtual = potenciaBase;
@ -1472,9 +1531,58 @@ class BombaPressurizadora_v2 : public ComponenteCAN {
IniciarKick(idx);
}
potenciaDesejada = AplicarKickInicial(potenciaDesejada, idx);
potenciaDesejada = AplicarKickInicial(
potenciaDesejada,
idx
);
double potenciaComRampa = AplicarRampaPercentual(potenciaDesejada, dt);
/*
Se houver sobrepressão, acrescenta uma correção negativa proporcional.
Isso trabalha junto com o erro negativo do PI. Não substitui o PI e
não zera a bomba.
*/
if (
excessoSobrepressao >
MargemAtivarAlivioSobrepressaoPsi
) {
double excessoEfetivo =
excessoSobrepressao -
MargemAtivarAlivioSobrepressaoPsi;
double reducaoSobrepressao =
excessoEfetivo *
GanhoAlivioSobrepressaoPctPorPsi;
reducaoSobrepressao = constrain(
reducaoSobrepressao,
0.0,
ReducaoMaximaSobrepressaoPct
);
potenciaDesejada -= reducaoSobrepressao;
/*
Mantém a bomba operando no piso configurado, atualmente 40%.
*/
potenciaDesejada = MaiorDouble(
potenciaDesejada,
ObterPotenciaMinimaRegulacao(idx)
);
}
potenciaDesejada = constrain(
potenciaDesejada,
ObterPotenciaMinimaRegulacao(idx),
PotenciaMaxPID
);
double potenciaComRampa =
AplicarRampaPercentual(
potenciaDesejada,
dt
);
AplicarPotenciaPercentual(potenciaComRampa);
_ultimoErroPressao = erro;