pequenos ajustes no operation control e controle de potencia na bomba pressurizadora
This commit is contained in:
parent
3910ae2d35
commit
fc95b86f14
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
||||
/*
|
||||
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
|
||||
);
|
||||
|
||||
double potenciaComRampa = AplicarRampaPercentual(potenciaDesejada, dt);
|
||||
AplicarPotenciaPercentual(potenciaComRampa);
|
||||
|
||||
_ultimoErroPressao = erro;
|
||||
|
|
|
|||
Loading…
Reference in New Issue