From fc95b86f14b1e86a5dbd36cd27376181e5c22ffd Mon Sep 17 00:00:00 2001 From: Diego Freitas Date: Tue, 18 Aug 2026 12:36:34 -0300 Subject: [PATCH] pequenos ajustes no operation control e controle de potencia na bomba pressurizadora --- .../Controls/MapViewControl.xaml.cs | 527 ++++++++++++++---- .../PulverizadorIndicator.xaml.cs | 2 +- .../Monitoramento/PulverizadorViewModel.cs | 38 +- .../Modulos/BombaPressurizadoraModel_v2.h | 186 +++++-- 4 files changed, 606 insertions(+), 147 deletions(-) diff --git a/AgroBase/OperationControl/Controls/MapViewControl.xaml.cs b/AgroBase/OperationControl/Controls/MapViewControl.xaml.cs index 2370c700f..136736f36 100644 --- a/AgroBase/OperationControl/Controls/MapViewControl.xaml.cs +++ b/AgroBase/OperationControl/Controls/MapViewControl.xaml.cs @@ -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 ParseFeatureAttributes(MIFeature feature) { - var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); + var dict = new Dictionary( + 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 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 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 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> ExtrairCoordenadas(MIFeature feature) + { + var coordinates = new List>(); + + 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 + { + longitude, + latitude + }); + } + } + catch + { + // A feature será descartada pelo chamador se não houver + // coordenadas suficientes. + } + + return coordinates; + } + + private double CalcularComprimentoMetros(IReadOnlyList> 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() }; - 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>(); - 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 { 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() }; - 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 RuasSelecionadas, List pontosSelecionados) { if (RuasMapaCarregado.Any()) diff --git a/AgroBase/OperationControl/Controls/Pulverizador/PulverizadorIndicator.xaml.cs b/AgroBase/OperationControl/Controls/Pulverizador/PulverizadorIndicator.xaml.cs index 91f3236b8..3bce1bb60 100644 --- a/AgroBase/OperationControl/Controls/Pulverizador/PulverizadorIndicator.xaml.cs +++ b/AgroBase/OperationControl/Controls/Pulverizador/PulverizadorIndicator.xaml.cs @@ -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; diff --git a/AgroBase/OperationControl/ViewModels/Views/Operacao/Monitoramento/PulverizadorViewModel.cs b/AgroBase/OperationControl/ViewModels/Views/Operacao/Monitoramento/PulverizadorViewModel.cs index 4f5bbee1d..945d6f5e1 100644 --- a/AgroBase/OperationControl/ViewModels/Views/Operacao/Monitoramento/PulverizadorViewModel.cs +++ b/AgroBase/OperationControl/ViewModels/Views/Operacao/Monitoramento/PulverizadorViewModel.cs @@ -11,17 +11,35 @@ namespace OperationControl.ViewModels.Views.Operacao.Monitoramento public class PulverizadorViewModel : INotifyPropertyChanged { public ObservableCollection 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(itens); - CameraErvasFrameTipoSelecionado = CameraErvasFrameTipos.FirstOrDefault(); + CameraErvasFrameTipos = + new ObservableCollection(itens); + + // Inicializa sem enviar comando durante a construção da tela. + _cameraErvasFrameTipoSelecionado = + CameraErvasFrameTipos.FirstOrDefault(); } public ObservableCollection EventosPulverizador { get; set; } = new(); diff --git a/Firmware/Modulos/BombaPressurizadoraModel_v2.h b/Firmware/Modulos/BombaPressurizadoraModel_v2.h index 80e6089c7..41dbff93f 100644 --- a/Firmware/Modulos/BombaPressurizadoraModel_v2.h +++ b/Firmware/Modulos/BombaPressurizadoraModel_v2.h @@ -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;