Compare commits

..

2 Commits

13 changed files with 559 additions and 87 deletions

View File

@ -206,7 +206,7 @@ namespace AgroBase.Forms.IHM.Operacao
?? operacaoParametrizando.Mapa?.RuasPercorrer?.Count ?? operacaoParametrizando.Mapa?.RuasPercorrer?.Count
?? 0; ?? 0;
if (qtdRuas == 0) if (qtdRuas < 2)
{ {
FuncoesIHM.MostrarAlerta( FuncoesIHM.MostrarAlerta(
"Trajetória não parametrizada", "Trajetória não parametrizada",
@ -217,12 +217,29 @@ namespace AgroBase.Forms.IHM.Operacao
} }
} }
if (operacaoIniciada) try
op?.AtualizarParametrosParciais(operacaoParametrizando.Parametros, op); {
else if (operacaoIniciada)
OperacaoModel.CarregarParamerosOperacaoBase(operacaoParametrizando.Parametros); {
op?.AtualizarParametrosParciais(operacaoParametrizando.Parametros, op);
}
else
{
OperacaoModel.CarregarParamerosOperacaoBase(operacaoParametrizando.Parametros);
}
ParametrosSalvos?.Invoke(this, EventArgs.Empty); ParametrosSalvos?.Invoke(this, EventArgs.Empty);
}
catch (InvalidOperationException ex)
{
Variaveis.MostrarLog("[ucOperacaoParametros.BtnSalvar] " + ex);
FuncoesIHM.MostrarAlerta("Não foi possível salvar a operação", ex.Message, MessageBoxIcon.Warning);
}
catch (Exception ex)
{
Variaveis.MostrarLog("[ucOperacaoParametros.BtnSalvar][ERRO] " + ex);
FuncoesIHM.MostrarAlerta("Erro ao salvar parâmetros", "Não foi possível aplicar os parâmetros da operação. Consulte os logs para mais detalhes.", MessageBoxIcon.Error);
}
} }
private async void BtnRedefinir_Click(object sender, EventArgs e) private async void BtnRedefinir_Click(object sender, EventArgs e)

View File

@ -61,6 +61,10 @@ namespace AgroBase.Forms.IHM
this.fldEntreEixos = new AgroBase.Forms.IHM.Operacao.Parametros.Controls.HmiCompactNumericField(); this.fldEntreEixos = new AgroBase.Forms.IHM.Operacao.Parametros.Controls.HmiCompactNumericField();
this.fldAnguloFrontal = new AgroBase.Forms.IHM.Operacao.Parametros.Controls.HmiCompactNumericField(); this.fldAnguloFrontal = new AgroBase.Forms.IHM.Operacao.Parametros.Controls.HmiCompactNumericField();
this.fldAnguloLateral = new AgroBase.Forms.IHM.Operacao.Parametros.Controls.HmiCompactNumericField(); this.fldAnguloLateral = new AgroBase.Forms.IHM.Operacao.Parametros.Controls.HmiCompactNumericField();
this.pnlReferenciaImu = new System.Windows.Forms.Panel();
this.lblOffsetImuRoll = new System.Windows.Forms.Label();
this.lblOffsetImuPitch = new System.Windows.Forms.Label();
this.btnReferenciarImu = new AgroBase.Forms.IHM.Controls.HmiSidebarButton();
this.pnlBateria = new AgroBase.Forms.IHM.Controls.HmiRoundedPanel(); this.pnlBateria = new AgroBase.Forms.IHM.Controls.HmiRoundedPanel();
this.bateriaLayout = new System.Windows.Forms.TableLayoutPanel(); this.bateriaLayout = new System.Windows.Forms.TableLayoutPanel();
this.lblBateria = new System.Windows.Forms.Label(); this.lblBateria = new System.Windows.Forms.Label();
@ -94,6 +98,7 @@ namespace AgroBase.Forms.IHM
this.rightLayout.SuspendLayout(); this.rightLayout.SuspendLayout();
this.pnlGeometria.SuspendLayout(); this.pnlGeometria.SuspendLayout();
this.geometriaLayout.SuspendLayout(); this.geometriaLayout.SuspendLayout();
this.pnlReferenciaImu.SuspendLayout();
this.pnlBateria.SuspendLayout(); this.pnlBateria.SuspendLayout();
this.bateriaLayout.SuspendLayout(); this.bateriaLayout.SuspendLayout();
this.pnlReservatorio.SuspendLayout(); this.pnlReservatorio.SuspendLayout();
@ -524,6 +529,7 @@ namespace AgroBase.Forms.IHM
this.geometriaLayout.Controls.Add(this.fldEntreEixos, 2, 1); this.geometriaLayout.Controls.Add(this.fldEntreEixos, 2, 1);
this.geometriaLayout.Controls.Add(this.fldAnguloFrontal, 0, 2); this.geometriaLayout.Controls.Add(this.fldAnguloFrontal, 0, 2);
this.geometriaLayout.Controls.Add(this.fldAnguloLateral, 1, 2); this.geometriaLayout.Controls.Add(this.fldAnguloLateral, 1, 2);
this.geometriaLayout.Controls.Add(this.pnlReferenciaImu, 2, 2);
this.geometriaLayout.Dock = System.Windows.Forms.DockStyle.Fill; this.geometriaLayout.Dock = System.Windows.Forms.DockStyle.Fill;
this.geometriaLayout.Location = new System.Drawing.Point(10, 6); this.geometriaLayout.Location = new System.Drawing.Point(10, 6);
this.geometriaLayout.Margin = new System.Windows.Forms.Padding(0); this.geometriaLayout.Margin = new System.Windows.Forms.Padding(0);
@ -660,6 +666,58 @@ namespace AgroBase.Forms.IHM
this.fldAnguloLateral.Titulo = "Ângulo máx. lateral"; this.fldAnguloLateral.Titulo = "Ângulo máx. lateral";
this.fldAnguloLateral.Unidade = "°"; this.fldAnguloLateral.Unidade = "°";
// //
// pnlReferenciaImu
//
this.pnlReferenciaImu.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(30)))), ((int)(((byte)(36)))), ((int)(((byte)(41)))));
this.pnlReferenciaImu.Controls.Add(this.lblOffsetImuRoll);
this.pnlReferenciaImu.Controls.Add(this.lblOffsetImuPitch);
this.pnlReferenciaImu.Controls.Add(this.btnReferenciarImu);
this.pnlReferenciaImu.Dock = System.Windows.Forms.DockStyle.Fill;
this.pnlReferenciaImu.Location = new System.Drawing.Point(247, 84);
this.pnlReferenciaImu.Margin = new System.Windows.Forms.Padding(3, 2, 2, 2);
this.pnlReferenciaImu.Name = "pnlReferenciaImu";
this.pnlReferenciaImu.Padding = new System.Windows.Forms.Padding(4, 2, 4, 3);
this.pnlReferenciaImu.Size = new System.Drawing.Size(119, 54);
this.pnlReferenciaImu.TabIndex = 5;
//
// lblOffsetImuRoll
//
this.lblOffsetImuRoll.Font = new System.Drawing.Font("Segoe UI", 7F);
this.lblOffsetImuRoll.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(204)))), ((int)(((byte)(218)))), ((int)(((byte)(211)))));
this.lblOffsetImuRoll.Location = new System.Drawing.Point(5, 2);
this.lblOffsetImuRoll.Name = "lblOffsetImuRoll";
this.lblOffsetImuRoll.Size = new System.Drawing.Size(54, 15);
this.lblOffsetImuRoll.TabIndex = 0;
this.lblOffsetImuRoll.Text = "Roll: 0.00°";
this.lblOffsetImuRoll.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
//
// lblOffsetImuPitch
//
this.lblOffsetImuPitch.Font = new System.Drawing.Font("Segoe UI", 7F);
this.lblOffsetImuPitch.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(204)))), ((int)(((byte)(218)))), ((int)(((byte)(211)))));
this.lblOffsetImuPitch.Location = new System.Drawing.Point(59, 2);
this.lblOffsetImuPitch.Name = "lblOffsetImuPitch";
this.lblOffsetImuPitch.Size = new System.Drawing.Size(56, 15);
this.lblOffsetImuPitch.TabIndex = 1;
this.lblOffsetImuPitch.Text = "Pitch: 0.00°";
this.lblOffsetImuPitch.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
//
// btnReferenciarImu
//
this.btnReferenciarImu.AccentColor = System.Drawing.Color.FromArgb(((int)(((byte)(155)))), ((int)(((byte)(106)))), ((int)(((byte)(222)))));
this.btnReferenciarImu.BackColor = System.Drawing.Color.Transparent;
this.btnReferenciarImu.BorderColor = System.Drawing.Color.FromArgb(((int)(((byte)(120)))), ((int)(((byte)(86)))), ((int)(((byte)(170)))));
this.btnReferenciarImu.Cursor = System.Windows.Forms.Cursors.Hand;
this.btnReferenciarImu.FillColor = System.Drawing.Color.FromArgb(((int)(((byte)(30)))), ((int)(((byte)(36)))), ((int)(((byte)(41)))));
this.btnReferenciarImu.IconText = "↻";
this.btnReferenciarImu.Location = new System.Drawing.Point(4, 18);
this.btnReferenciarImu.Margin = new System.Windows.Forms.Padding(0);
this.btnReferenciarImu.MinimumSize = new System.Drawing.Size(60, 34);
this.btnReferenciarImu.Name = "btnReferenciarImu";
this.btnReferenciarImu.Size = new System.Drawing.Size(111, 34);
this.btnReferenciarImu.TabIndex = 2;
this.btnReferenciarImu.Text = "ZERAR";
//
// pnlBateria // pnlBateria
// //
this.pnlBateria.AccentColor = System.Drawing.Color.FromArgb(((int)(((byte)(72)))), ((int)(((byte)(210)))), ((int)(((byte)(122))))); this.pnlBateria.AccentColor = System.Drawing.Color.FromArgb(((int)(((byte)(72)))), ((int)(((byte)(210)))), ((int)(((byte)(122)))));
@ -1019,6 +1077,7 @@ namespace AgroBase.Forms.IHM
this.rightLayout.ResumeLayout(false); this.rightLayout.ResumeLayout(false);
this.pnlGeometria.ResumeLayout(false); this.pnlGeometria.ResumeLayout(false);
this.geometriaLayout.ResumeLayout(false); this.geometriaLayout.ResumeLayout(false);
this.pnlReferenciaImu.ResumeLayout(false);
this.pnlBateria.ResumeLayout(false); this.pnlBateria.ResumeLayout(false);
this.bateriaLayout.ResumeLayout(false); this.bateriaLayout.ResumeLayout(false);
this.pnlReservatorio.ResumeLayout(false); this.pnlReservatorio.ResumeLayout(false);
@ -1065,6 +1124,10 @@ namespace AgroBase.Forms.IHM
private AgroBase.Forms.IHM.Operacao.Parametros.Controls.HmiCompactNumericField fldEntreEixos; private AgroBase.Forms.IHM.Operacao.Parametros.Controls.HmiCompactNumericField fldEntreEixos;
private AgroBase.Forms.IHM.Operacao.Parametros.Controls.HmiCompactNumericField fldAnguloFrontal; private AgroBase.Forms.IHM.Operacao.Parametros.Controls.HmiCompactNumericField fldAnguloFrontal;
private AgroBase.Forms.IHM.Operacao.Parametros.Controls.HmiCompactNumericField fldAnguloLateral; private AgroBase.Forms.IHM.Operacao.Parametros.Controls.HmiCompactNumericField fldAnguloLateral;
private System.Windows.Forms.Panel pnlReferenciaImu;
private System.Windows.Forms.Label lblOffsetImuRoll;
private System.Windows.Forms.Label lblOffsetImuPitch;
private AgroBase.Forms.IHM.Controls.HmiSidebarButton btnReferenciarImu;
private AgroBase.Forms.IHM.Controls.HmiRoundedPanel pnlBateria; private AgroBase.Forms.IHM.Controls.HmiRoundedPanel pnlBateria;
private System.Windows.Forms.TableLayoutPanel bateriaLayout; private System.Windows.Forms.TableLayoutPanel bateriaLayout;

View File

@ -1,6 +1,7 @@
using AgroBase.Forms.IHM.Controls; using AgroBase.Forms.IHM.Controls;
using AgroBase.Forms.IHM.Operacao.Parametros.Controls; using AgroBase.Forms.IHM.Operacao.Parametros.Controls;
using AgroBase.Models; using AgroBase.Models;
using AgroBase.Models.Components;
using AgroBase.Services.Operadores; using AgroBase.Services.Operadores;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -56,6 +57,7 @@ namespace AgroBase.Forms.IHM
public void AtualizarDadosTela() public void AtualizarDadosTela()
{ {
lblRover.Text = $"ROVER {VariaveisEquipamento.Parametros.serial_number}\r\nConfiguração local"; lblRover.Text = $"ROVER {VariaveisEquipamento.Parametros.serial_number}\r\nConfiguração local";
AtualizarReferenciaImuTela();
} }
@ -102,6 +104,11 @@ namespace AgroBase.Forms.IHM
await ExecutarSeguroAsync("Erro ao testar comunicação!", TestarComunicacaoAsync); await ExecutarSeguroAsync("Erro ao testar comunicação!", TestarComunicacaoAsync);
}; };
btnReferenciarImu.Click += async delegate
{
await ExecutarSeguroAsync("Erro ao referenciar IMU!", ReferenciarImuAsync);
};
cameraFrontal.CameraChanged += cameraFrontal_CameraChanged; cameraFrontal.CameraChanged += cameraFrontal_CameraChanged;
cameraErvas.CameraChanged += cameraErvas_CameraChanged; cameraErvas.CameraChanged += cameraErvas_CameraChanged;
@ -179,6 +186,94 @@ namespace AgroBase.Forms.IHM
campo.DecimalPlaces = casas; campo.DecimalPlaces = casas;
} }
private void AtualizarReferenciaImuTela()
{
lblOffsetImuRoll.Text = $"L: {VariaveisEquipamento.Parametros.imu_roll_offset_deg:+0.00;-0.00;0.00}°";
lblOffsetImuPitch.Text = $"F: {VariaveisEquipamento.Parametros.imu_pitch_offset_deg:+0.00;-0.00;0.00}°";
}
private async Task ReferenciarImuAsync()
{
var resposta = CustomDialog.ShowDialog(
"Referenciar IMU",
"Posicione o rover em uma superfície nivelada e mantenha-o imóvel.\r\n\r\n" +
"A referência atual de inclinação será recalculada.",
MessageBoxIcon.Information,
new Dictionary<(string, DialogResult?), Action>()
{
{ ("OK", DialogResult.OK), () => { } },
{ ("Cancelar", DialogResult.Cancel), () => { } },
}
);
if (resposta != DialogResult.OK)
return;
btnReferenciarImu.Enabled = false;
btnReferenciarImu.Text = "LENDO...";
try
{
const int quantidadeAmostras = 12;
const int intervaloMs = 100;
var rolls = new List<double>();
var pitches = new List<double>();
for (int i = 0; i < quantidadeAmostras; i++)
{
var imu = Variaveis.OperacaoEmAndamento?.Sensoriamento?.IMU;
if (imu != null && imu.valido && imu.IdadeSegundos <= 1.0)
{
rolls.Add(imu.roll);
pitches.Add(imu.pitch);
}
await Task.Delay(intervaloMs);
}
if (rolls.Count < 8)
throw new InvalidOperationException("Não há amostras válidas e recentes suficientes do IMU para realizar a referência.");
double mediaRoll = rolls.Average();
double mediaPitch = pitches.Average();
double desvioRoll = Math.Sqrt(rolls.Average(x => Math.Pow(x - mediaRoll, 2)));
double desvioPitch = Math.Sqrt(pitches.Average(x => Math.Pow(x - mediaPitch, 2)));
if (desvioRoll > 0.50 || desvioPitch > 0.50)
throw new InvalidOperationException(
$"O rover parece estar em movimento. Estabilidade insuficiente (σ roll {desvioRoll:0.00}°, σ pitch {desvioPitch:0.00}°). Mantenha-o imóvel e tente novamente.");
double novoRoll = VariaveisEquipamento.Parametros.imu_roll_offset_deg + mediaRoll;
double novoPitch = VariaveisEquipamento.Parametros.imu_pitch_offset_deg + mediaPitch;
var parametros = VariaveisEquipamento.Parametros;
DefinirParametroGlobal(parametros, novoRoll, "imu_roll_offset_deg");
DefinirParametroGlobal(parametros, novoPitch, "imu_pitch_offset_deg");
if (!parametros.SalvarParametros())
throw new InvalidOperationException("Não foi possível salvar os novos offsets do IMU.");
parametros.AtualizarParametros();
HealthWorkerService.AtualizarDadosOperacao();
AtualizarReferenciaImuTela();
FuncoesIHM.MostrarAlerta(
"IMU referenciado",
$"Referência atualizada com sucesso.\r\n\r\n" +
$"Roll: {novoRoll:+0.00;-0.00;0.00}°\r\n" +
$"Pitch: {novoPitch:+0.00;-0.00;0.00}°",
MessageBoxIcon.Information);
}
finally
{
btnReferenciarImu.Text = "ZERAR";
btnReferenciarImu.Enabled = true;
}
}
#region Câmeras #region Câmeras
private async Task AtualizarCameras() private async Task AtualizarCameras()

View File

@ -3197,15 +3197,55 @@ namespace AgroBase.Models
private void ExecutarComandoMovDirGenerico(OperacaoComandoBaseModel controleBase, OperacaoModel op) private void ExecutarComandoMovDirGenerico(OperacaoComandoBaseModel controleBase, OperacaoModel op)
{ {
op.DefinirEmergencia(controleBase.Emergencia); if (op == null)
return;
/*
* ============================================================
* COMANDO OPERACIONAL DE PARADA
* ============================================================
*
* Pausa/Emergência só podem ser alteradas por um comando
* explicitamente destinado a isso.
*
* MOV/DIR nunca devem limpar esses estados.
*/
if (controleBase.Dispositivo == T_Code.Vzo)
{
op.DefinirEmergencia(controleBase.Emergencia);
if (op.Sensoriamento.Operacao.Emergencia)
return;
op.DefinirPausa(controleBase.Pausa);
return;
}
/*
* Emergência sempre possui autoridade máxima.
*/
if (op.Sensoriamento.Operacao.Emergencia) if (op.Sensoriamento.Operacao.Emergencia)
return; return;
op.DefinirPausa(controleBase.Pausa); bool controleManualAcionado =
op.Parametros?.Controle?.ControleManualAcionado == true;
if (op.Sensoriamento.Operacao.Pausa) /*
* ============================================================
* PAUSA OPERACIONAL
* ============================================================
*
* Pausa impede o AUTOMÁTICO, mas não impede uma intervenção
* manual explicitamente assumida pelo operador.
*
* Também permitimos comandos de soltura/neutro mesmo após
* retirar o controle manual, para nunca deixar comando preso.
*/
if (op.Sensoriamento.Operacao.Pausa && !controleManualAcionado && !controleBase.Solto)
{
return; return;
}
if (controleBase.Controle == null) if (controleBase.Controle == null)
return; return;
@ -3215,12 +3255,20 @@ namespace AgroBase.Models
if (cmdRover == null) if (cmdRover == null)
return; return;
if (cmdRover.Dispositivo == T_Code.Mov && (op.Parametros?.Controle?.MovimentoAutomatico ?? false) && op.Sensoriamento.Operacao.OperacaoIniciada) if (
cmdRover.Dispositivo == T_Code.Mov &&
(op.Parametros?.Controle?.MovimentoAutomatico ?? false) &&
op.Sensoriamento.Operacao.OperacaoIniciada
)
{ {
return; return;
} }
if (cmdRover.Dispositivo == T_Code.Dir && (op.Parametros?.Controle?.DirecionalAutomatico ?? false) && op.Sensoriamento.Operacao.OperacaoIniciada) if (
cmdRover.Dispositivo == T_Code.Dir &&
(op.Parametros?.Controle?.DirecionalAutomatico ?? false) &&
op.Sensoriamento.Operacao.OperacaoIniciada
)
{ {
return; return;
} }
@ -3231,16 +3279,24 @@ namespace AgroBase.Models
return; return;
if (controleBase.Controle.PercentualVelocidadeSP != null) if (controleBase.Controle.PercentualVelocidadeSP != null)
controle.PercentualVelocidadeSP = (double)controleBase .Controle.PercentualVelocidadeSP; controle.PercentualVelocidadeSP =
(double)controleBase.Controle.PercentualVelocidadeSP;
if (controleBase.Controle.AnguloSP != null) if (controleBase.Controle.AnguloSP != null)
controle.Angulo = (double)controleBase.Controle.AnguloSP; controle.Angulo =
(double)controleBase.Controle.AnguloSP;
if (controleBase.Controle.TipoMovimentoDirecional != null) if (controleBase.Controle.TipoMovimentoDirecional != null)
op.DefinirTipoMovimentoControle((TipoMovimentoDirecional)controleBase.Controle.TipoMovimentoDirecional); {
op.DefinirTipoMovimentoControle(
(TipoMovimentoDirecional)
controleBase.Controle.TipoMovimentoDirecional
);
}
if (controleBase.Controle.EmFreio != null) if (controleBase.Controle.EmFreio != null)
controle.EmFreio = (bool)controleBase.Controle.EmFreio; controle.EmFreio =
(bool)controleBase.Controle.EmFreio;
switch (cmdRover.Dispositivo) switch (cmdRover.Dispositivo)
{ {
@ -3248,7 +3304,8 @@ namespace AgroBase.Models
GeneralJoystick.ProcessarDadosControle( GeneralJoystick.ProcessarDadosControle(
cmdRover.botaoJoy, cmdRover.botaoJoy,
controleBase.Solto, controleBase.Solto,
valorDesejado: controle.PercentualVelocidadeSP, valorDesejado:
controle.PercentualVelocidadeSP,
ForcarComando: true ForcarComando: true
); );
break; break;
@ -3257,7 +3314,8 @@ namespace AgroBase.Models
GeneralJoystick.ProcessarDadosControle( GeneralJoystick.ProcessarDadosControle(
cmdRover.botaoJoy, cmdRover.botaoJoy,
controleBase.Solto, controleBase.Solto,
valorDesejado: controle.Angulo, valorDesejado:
controle.Angulo,
ForcarComando: true ForcarComando: true
); );
break; break;

View File

@ -1123,7 +1123,7 @@ namespace AgroBase.Models
{ TipoMovimentoDirecional.Diagnostico, 25 }, { TipoMovimentoDirecional.Diagnostico, 25 },
}; };
public static double AnguloInclinacaoRollMax { get; set; } = 22.0; // Lateral public static double AnguloInclinacaoRollMax { get; set; } = 22.0; // Lateral
public static double AnguloInclinacaoPitchMax { get; set; } = 30.0; // Frontal 40 graus max public static double AnguloInclinacaoPitchMax { get; set; } = 30.0; // Frontal
public static string VozAlerta { get; set; } = "Microsoft Maria Desktop"; public static string VozAlerta { get; set; } = "Microsoft Maria Desktop";
public static bool Pronto => Variaveis.OperacaoEmAndamento?.Parametros?.Controle != null; public static bool Pronto => Variaveis.OperacaoEmAndamento?.Parametros?.Controle != null;
@ -1527,6 +1527,9 @@ namespace AgroBase.Models
public string can_gateway { get; set; } public string can_gateway { get; set; }
public string can_submask { get; set; } public string can_submask { get; set; }
public double imu_roll_offset_deg { get; set; } = 0.0; // Lateral
public double imu_pitch_offset_deg { get; set; } = 0.0; // Frontal
public void AtualizarParametros() public void AtualizarParametros()
{ {
string config_path = Path.Combine(Variaveis.CaminhoSistema, Variaveis.CaminhoParametros, "config.json"); string config_path = Path.Combine(Variaveis.CaminhoSistema, Variaveis.CaminhoParametros, "config.json");
@ -1558,6 +1561,9 @@ namespace AgroBase.Models
camera_caminhho_id = par.camera_caminhho_id; camera_caminhho_id = par.camera_caminhho_id;
camera_ervas_id = par.camera_ervas_id; camera_ervas_id = par.camera_ervas_id;
imu_pitch_offset_deg = par.imu_pitch_offset_deg;
imu_roll_offset_deg = par.imu_roll_offset_deg;
Carregado = true; Carregado = true;
} }
catch (Exception ex) catch (Exception ex)

View File

@ -429,7 +429,9 @@ namespace AgroBase.Services.Operadores
("dir_angulo_direita_sinal", 1), ("dir_angulo_direita_sinal", 1),
("imu_dir_roll_min_aux", 6.0), ("imu_dir_roll_min_aux", 6.0),
("imu_dir_bias_attention_deg", 4.0), ("imu_dir_bias_attention_deg", 4.0),
("imu_dir_bias_risk_deg", 9.0) ("imu_dir_bias_risk_deg", 9.0),
("imu_roll_offset_deg", VariaveisEquipamento.Parametros.imu_roll_offset_deg),
("imu_pitch_offset_deg", VariaveisEquipamento.Parametros.imu_pitch_offset_deg)
); );
} }
catch (Exception ex) catch (Exception ex)

View File

@ -1 +1 @@
{"Carregado":true,"serial_number":"01","base_ip":"20.1.0.2","base_porta_caminho":5000,"base_porta_ervas":5002,"comunicacao_interface":"AG_Hallow","rover_ip":"20.1.0.10","rover_gateway":"20.1.0.1","rover_submask":"255.255.255.0","camera_caminhho_id":"14442C10C143E2D600","camera_ervas_id":["194430108133AC2F00"],"can_interface":"AG_Can","can_ip_pc":"192.168.0.5","can_ip_modulo":"192.168.0.7","can_porta":8235,"can_gateway":"192.168.0.1","can_submask":"255.255.255.0"} {"Carregado":true,"serial_number":"01","base_ip":"20.1.0.2","base_porta_caminho":5000,"base_porta_ervas":5002,"comunicacao_interface":"AG_Hallow","rover_ip":"20.1.0.10","rover_gateway":"20.1.0.1","rover_submask":"255.255.255.0","camera_caminhho_id":"14442C10C143E2D600","camera_ervas_id":["194430108133AC2F00"],"can_interface":"AG_Can","can_ip_pc":"192.168.0.5","can_ip_modulo":"192.168.0.7","can_porta":8235,"can_gateway":"192.168.0.1","can_submask":"255.255.255.0","imu_roll_offset_deg":0.0,"imu_pitch_offset_deg":0.0}

View File

@ -159,14 +159,27 @@ class ModuloDirecional(ModuloDiagnosticoBase):
condicoes = [] condicoes = []
if not conectado: if not conectado:
condicoes = []
if comandar:
condicoes.append({
"valor": 0,
"severidade": 100,
"descricao": (
f"Driver direcional {endereco_str} desconectado "
"enquanto está em uso"
),
})
return { return {
"id": endereco_str, "id": endereco_str,
"label": mod_id, "label": mod_id,
"status": StatusModulo.DESCONECTADO.value, "status": StatusModulo.DESCONECTADO.value,
"saude": 0, "saude": 0,
"motivos": ["Desconectado"], "motivos": ["Desconectado"],
"condicoes_operacionais": [], "condicoes_operacionais": condicoes,
"em_uso": comandar, "em_uso": comandar,
"referenciando": referenciando,
"componentes": { "componentes": {
"conectividade": 0, "conectividade": 0,
"feedback": 0, "feedback": 0,

View File

@ -49,6 +49,7 @@ class CameraIMU(ModuloDiagnosticoBase):
angulo_risk_factor=0.75, angulo_risk_factor=0.75,
angulo_emergency_factor=1.20, angulo_emergency_factor=1.20,
limites_refresh_s=1.0, limites_refresh_s=1.0,
offsets_refresh_s=1.0,
# Taxas continuam independentes do ângulo máximo. # Taxas continuam independentes do ângulo máximo.
roll_rate_attention_dps=25.0, roll_rate_attention_dps=25.0,
@ -87,6 +88,13 @@ class CameraIMU(ModuloDiagnosticoBase):
self.limites_refresh_s = max(float(limites_refresh_s), 0.25) self.limites_refresh_s = max(float(limites_refresh_s), 0.25)
self._ultimo_refresh_limites = 0.0 self._ultimo_refresh_limites = 0.0
# Offsets persistentes de atitude, definidos no equipamento.
# Convenção: valor corrigido = valor medido - offset.
self.offsets_refresh_s = max(float(offsets_refresh_s), 0.25)
self._ultimo_refresh_offsets = 0.0
self.roll_offset_deg = 0.0
self.pitch_offset_deg = 0.0
# Valores iniciais seguros. Serão substituídos pelos dados do Redis. # Valores iniciais seguros. Serão substituídos pelos dados do Redis.
self.roll_max_deg = self.roll_max_default_deg self.roll_max_deg = self.roll_max_default_deg
self.pitch_max_deg = self.pitch_max_default_deg self.pitch_max_deg = self.pitch_max_default_deg
@ -154,6 +162,10 @@ class CameraIMU(ModuloDiagnosticoBase):
self.max_skew_imu_s = float(max_skew_imu_s) self.max_skew_imu_s = float(max_skew_imu_s)
# Carrega configuração persistente antes da primeira atitude publicada.
self._atualizar_limites_equipamento(forcar=True)
self._atualizar_offsets_equipamento(forcar=True)
self.thread = threading.Thread( self.thread = threading.Thread(
target=self.imu_task_loop, target=self.imu_task_loop,
name="CameraIMUFinalWorker", name="CameraIMUFinalWorker",
@ -789,6 +801,77 @@ class CameraIMU(ModuloDiagnosticoBase):
f"{self.pitch_emergency_deg:.1f}°)" f"{self.pitch_emergency_deg:.1f}°)"
) )
def _validar_offset_angulo(self, valor, valor_atual):
"""Valida offset persistente de roll/pitch vindo do equipamento."""
try:
valor = float(valor)
if not math.isfinite(valor):
return float(valor_atual)
# Guard rail: offset mecânico maior que isso indica configuração errada.
if valor < -45.0 or valor > 45.0:
return float(valor_atual)
return valor
except Exception:
return float(valor_atual)
def _atualizar_offsets_equipamento(self, forcar=False):
"""
Atualiza os offsets persistentes da referência mecânica do rover.
Campos esperados em equipamento:
imu_roll_offset_deg
imu_pitch_offset_deg
Convenção:
roll_corrigido = roll_fundido - imu_roll_offset_deg
pitch_corrigido = pitch_fundido - imu_pitch_offset_deg
"""
agora = time.perf_counter()
if (
not forcar
and (agora - self._ultimo_refresh_offsets) < self.offsets_refresh_s
):
return
self._ultimo_refresh_offsets = agora
equipamento = ContextoGlobalRedis.get_equipamento() or {}
roll_anterior = self.roll_offset_deg
pitch_anterior = self.pitch_offset_deg
self.roll_offset_deg = self._validar_offset_angulo(
equipamento.get("imu_roll_offset_deg", 0.0),
self.roll_offset_deg,
)
self.pitch_offset_deg = self._validar_offset_angulo(
equipamento.get("imu_pitch_offset_deg", 0.0),
self.pitch_offset_deg,
)
mudou = (
abs(self.roll_offset_deg - roll_anterior) > 1e-6
or abs(self.pitch_offset_deg - pitch_anterior) > 1e-6
)
if mudou:
# Não deixa o EMA carregar a referência antiga depois de recalibrar.
self._resetar_filtro_fusao()
self.mostrar_log(
"Offsets IMU atualizados | "
f"roll={roll_anterior:.3f}°→{self.roll_offset_deg:.3f}° | "
f"pitch={pitch_anterior:.3f}°→{self.pitch_offset_deg:.3f}°"
)
def _aplicar_offsets_fusao(self, fusao):
"""Aplica a referência mecânica depois da fusão e antes do filtro/risco."""
if fusao is None:
return None
corrigida = dict(fusao)
corrigida["roll"] = float(fusao.get("roll", 0.0)) - self.roll_offset_deg
corrigida["pitch"] = float(fusao.get("pitch", 0.0)) - self.pitch_offset_deg
# Offset constante não altera taxas angulares nem yaw.
return corrigida
# ============================================================ # ============================================================
# Fusão # Fusão
# ============================================================ # ============================================================
@ -1384,6 +1467,16 @@ class CameraIMU(ModuloDiagnosticoBase):
"roll_max_configurado_deg": round(self.roll_max_deg, 3), "roll_max_configurado_deg": round(self.roll_max_deg, 3),
"pitch_max_configurado_deg": round(self.pitch_max_deg, 3), "pitch_max_configurado_deg": round(self.pitch_max_deg, 3),
# Referência mecânica persistente aplicada à atitude final.
"offsets_imu": {
"roll_deg": round(self.roll_offset_deg, 4),
"pitch_deg": round(self.pitch_offset_deg, 4),
"convencao": "corrigido=medido-offset",
"origem": "equipamento",
},
"roll_offset_deg": round(self.roll_offset_deg, 4),
"pitch_offset_deg": round(self.pitch_offset_deg, 4),
"roll_attention_deg": round(self.roll_attention_deg, 3), "roll_attention_deg": round(self.roll_attention_deg, 3),
"roll_risk_deg": round(self.roll_risk_deg, 3), "roll_risk_deg": round(self.roll_risk_deg, 3),
"roll_critical_deg": round(self.roll_critical_deg, 3), "roll_critical_deg": round(self.roll_critical_deg, 3),
@ -1487,6 +1580,7 @@ class CameraIMU(ModuloDiagnosticoBase):
agora = time.time() agora = time.time()
self._atualizar_limites_equipamento() self._atualizar_limites_equipamento()
self._atualizar_offsets_equipamento()
fontes, ignoradas, desabilitadas = self._ler_fontes_imu() fontes, ignoradas, desabilitadas = self._ler_fontes_imu()
@ -1520,6 +1614,11 @@ class CameraIMU(ModuloDiagnosticoBase):
fusao = self._fundir_fontes(fontes, sensores_agree=divergencia["sensors_agree"]) fusao = self._fundir_fontes(fontes, sensores_agree=divergencia["sensors_agree"])
# Corrige a atitude final pela referência mecânica persistente do rover.
# A divergência entre sensores continua sendo avaliada antes do offset,
# pois um offset global não deve mascarar desacordo entre IMUs.
fusao = self._aplicar_offsets_fusao(fusao)
# Atualiza o filtro antes de avaliar o risco. # Atualiza o filtro antes de avaliar o risco.
atitude_filtrada = self._atualizar_filtro_fusao(fusao) atitude_filtrada = self._atualizar_filtro_fusao(fusao)

View File

@ -318,13 +318,31 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
) )
if not conectado and estado_desconectado["nivel"] == "confirmado": if not conectado and estado_desconectado["nivel"] == "confirmado":
condicoes_desconectado = []
if comandar:
condicoes_desconectado.append({
"label": "Conexão OID",
"valor": estado_desconectado["duracao_s"],
"severidade": 100,
"descricao": (
f"Driver de movimentação {mod_id} desconectado "
"enquanto está em uso"
),
"acoes": [
"Verificar comunicação CAN com o OID.",
"Verificar alimentação do driver.",
"Inspecionar conectores e cabeamento do motor.",
],
})
return { return {
"id": endereco_str, "id": endereco_str,
"label": mod_id, "label": mod_id,
"status": StatusModulo.DESCONECTADO.value, "status": StatusModulo.DESCONECTADO.value,
"saude": 0, "saude": 0,
"motivos": ["Desconectado"], "motivos": ["Desconectado"],
"condicoes_operacionais": [], "condicoes_operacionais": condicoes_desconectado,
"em_uso": comandar, "em_uso": comandar,
"conectado_efetivo": False, "conectado_efetivo": False,
"componentes": self._componentes_zero(), "componentes": self._componentes_zero(),

View File

@ -713,36 +713,62 @@ class ContextoGlobalRedis:
ultimo_publicado == cls._int(ultimo_esperado, -999999) ultimo_publicado == cls._int(ultimo_esperado, -999999)
) )
ack_mapa_exato = (
status_mapa == "aplicado"
and revisao_solicitada > 0
and health_rev == revisao_solicitada
and manager_rev == revisao_solicitada
and command_solicitado != ""
and health_cmd == command_solicitado
and manager_cmd == command_solicitado
and health_status == "aplicado"
and manager_status == "aplicado"
and qtd_esperada > 0
and qtd_publicada == qtd_esperada
and manager_qtd == qtd_esperada
and indices_extremos_ok
)
mapa_mpc_ok = ( mapa_mpc_ok = (
not requer_confirmacao_mapa_mpc not requer_confirmacao_mapa_mpc
or ( or (
not mapa_barreira_critica ack_mapa_exato
and mapa_confirmado_flag and mapa_confirmado_flag
and status_mapa == "aplicado" and not mapa_barreira_critica
and revisao_solicitada > 0
and health_rev == revisao_solicitada
and manager_rev == revisao_solicitada
and command_solicitado != ""
and health_cmd == command_solicitado
and manager_cmd == command_solicitado
and health_status == "aplicado"
and manager_status == "aplicado"
and qtd_esperada > 0
and qtd_publicada == qtd_esperada
and manager_qtd == qtd_esperada
and indices_extremos_ok
) )
) )
if not mapa_mpc_ok: if not mapa_mpc_ok:
motivos.append( motivos.append(
"Mapa/MPC ainda não confirmado: " "Mapa/MPC ainda não confirmado: "
f"rev solicitada/health/manager={revisao_solicitada}/{health_rev}/{manager_rev}; " f"confirmado={mapa_confirmado_flag}; "
f"cmd solicitado/health/manager={command_solicitado}/{health_cmd}/{manager_cmd}; " f"barreira={mapa_barreira_critica}; "
f"pontos publicados/esperados/manager={qtd_publicada}/{qtd_esperada}/{manager_qtd}; " f"rev solicitada/health/manager="
f"{revisao_solicitada}/{health_rev}/{manager_rev}; "
f"cmd solicitado/health/manager="
f"{command_solicitado}/{health_cmd}/{manager_cmd}; "
f"pontos publicados/esperados/manager="
f"{qtd_publicada}/{qtd_esperada}/{manager_qtd}; "
f"indices publicado={primeiro_publicado}->{ultimo_publicado}, "
f"esperado={primeiro_esperado}->{ultimo_esperado}, "
f"ok={indices_extremos_ok}; "
f"status={status_mapa}/{health_status}/{manager_status}" f"status={status_mapa}/{health_status}/{manager_status}"
) )
if ack_mapa_exato and not mapa_confirmado_flag:
mostrar_log(
"[MAPSYNC/INCONSISTENCIA] "
f"ACK exato confirmado, mas mapa_confirmado=false. "
f"rev={revisao_solicitada}; cmd={command_solicitado}"
)
if ack_mapa_exato and mapa_barreira_critica:
mostrar_log(
"[MAPSYNC/INCONSISTENCIA] "
f"ACK exato confirmado, mas mapa_barreira_critica permanece ativa. "
f"rev={revisao_solicitada}; cmd={command_solicitado}"
)
detalhes_regras["mapa_mpc"] = { detalhes_regras["mapa_mpc"] = {
"requer_confirmacao": requer_confirmacao_mapa_mpc, "requer_confirmacao": requer_confirmacao_mapa_mpc,
"ok": mapa_mpc_ok, "ok": mapa_mpc_ok,

View File

@ -32,34 +32,60 @@ namespace OperationControl.ViewModels.Views.Operacao.Monitoramento
get => _habilitado; get => _habilitado;
set set
{ {
if (_habilitado != value) if (_habilitado == value)
return;
var r =
VariaveisControleOperacao.RoverEmFoco;
/*
* Ao abandonar o manual, primeiro neutralizamos tudo
* enquanto o rover ainda reconhece autoridade manual.
*/
if (!value)
{ {
_habilitado = value; LimparTeclas();
OnPropertyChanged(nameof(StatusTexto)); EnviarNeutros();
OnPropertyChanged(nameof(TextoAjuda)); }
OnPropertyChanged(nameof(StatusBrush));
var r = VariaveisControleOperacao.RoverEmFoco; _habilitado = value;
if (r != null)
{
r.Controle.ControleManualAcionado = value;
r.Controle.MovimentoAutomatico = value ? false : r.ControleAutomatico;
r.Controle.DirecionalAutomatico = value ? false : r.ControleAutomatico;
VariaveisControleOperacao.EnviarParametrosOperacao(new AgroBase.Models.Operacoes.OperacaoParametrosModel()
{
Controle = VariaveisControleOperacao.RoverEmFoco?.Controle
});
}
if (!value) OnPropertyChanged(nameof(StatusTexto));
{ OnPropertyChanged(nameof(TextoAjuda));
LimparTeclas(); OnPropertyChanged(nameof(StatusBrush));
EnviarNeutros();
} if (r != null)
else {
{ r.Controle.ControleManualAcionado =
VariaveisControleOperacao.EnviarComandoIniciarUDP(VariaveisControleOperacao.RoverEmFoco.IP, true); value;
}
r.Controle.MovimentoAutomatico =
value
? false
: r.ControleAutomatico;
r.Controle.DirecionalAutomatico =
value
? false
: r.ControleAutomatico;
VariaveisControleOperacao
.EnviarParametrosOperacao(
new AgroBase.Models.Operacoes
.OperacaoParametrosModel()
{
Controle = r.Controle
}
);
}
if (value && r != null)
{
VariaveisControleOperacao
.EnviarComandoIniciarUDP(
r.IP,
true
);
} }
} }
} }

View File

@ -653,6 +653,27 @@ namespace OperationControl.ViewModels
return "A operação não foi liberada pelo rover."; return "A operação não foi liberada pelo rover.";
} }
void AdicionarEventoTrajetoria(
string descricao,
int prioridade = 700,
StatusModulo status = StatusModulo.Alerta)
{
descricao = NormalizarTexto(descricao);
if (string.IsNullOrWhiteSpace(descricao))
return;
eventos.Add(new MotivoTopBarModel
{
Prioridade = prioridade,
Fonte = "Trajetoria",
Titulo = "TRAJETÓRIA BLOQUEADA",
Descricao = descricao,
StatusVisual = status,
Modulo = T_Code.Trj
});
}
// ============================================================ // ============================================================
// 0) Comunicação // 0) Comunicação
// ============================================================ // ============================================================
@ -752,22 +773,22 @@ namespace OperationControl.ViewModels
// Parâmetro obrigatório ausente // Parâmetro obrigatório ausente
// ============================================================ // ============================================================
if ( //if (
obj.Operacao != null && // obj.Operacao != null &&
!obj.Operacao.Liberada && // !obj.Operacao.Liberada &&
obj.Operacao.Pausa != true && // obj.Operacao.Pausa != true &&
obj.Operacao.Emergencia != true // obj.Operacao.Emergencia != true
) //)
{ //{
eventos.Add(new MotivoTopBarModel // eventos.Add(new MotivoTopBarModel
{ // {
Prioridade = 850, // Prioridade = 850,
Fonte = "Operacao", // Fonte = "Operacao",
Titulo = "OPERAÇÃO BLOQUEADA", // Titulo = "OPERAÇÃO BLOQUEADA",
Descricao = MotivoOperacaoAtual(), // Descricao = MotivoOperacaoAtual(),
StatusVisual = StatusModulo.Falha // StatusVisual = StatusModulo.Falha
}); // });
} //}
// ============================================================ // ============================================================
// 5) Controle // 5) Controle
@ -809,14 +830,42 @@ namespace OperationControl.ViewModels
foreach (var motivo in motivosTrajetoria) foreach (var motivo in motivosTrajetoria)
{ {
eventos.Add(new MotivoTopBarModel AdicionarEventoTrajetoria(motivo);
}
var impedimentoOperacao = obj.Operacao?.ImpedimentoErro ?? "";
var linhasImpedimento =
impedimentoOperacao
.Split(
new[] { "\r\n", "\n", "\r" },
StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.Trim())
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct()
.ToList();
foreach (var linha in linhasImpedimento)
{
bool relacionadoTrajetoria =
linha.Contains(
"Mapa/MPC",
StringComparison.OrdinalIgnoreCase)
||
linha.Contains(
"trajetória",
StringComparison.OrdinalIgnoreCase)
||
linha.Contains(
"trajetoria",
StringComparison.OrdinalIgnoreCase);
if (relacionadoTrajetoria)
{ {
Prioridade = 700, AdicionarEventoTrajetoria(
Fonte = "Trajetoria", linha,
Titulo = "TRAJETÓRIA BLOQUEADA", prioridade: 710);
Descricao = motivo, }
StatusVisual = StatusModulo.Alerta
});
} }
// ============================================================ // ============================================================