ajustes mpc e trajetória, manobra mais suave

This commit is contained in:
Diego Freitas 2026-09-15 20:49:57 -03:00
parent 679f344e39
commit 295057ca67
17 changed files with 1096 additions and 268 deletions

4
.gitignore vendored
View File

@ -93,3 +93,7 @@ AgroBase/AgroBase/bin/x64/Debug/Python/venv/
/Exemplos /Exemplos
/Python/raspi/cam_2/imx296_pi/ /Python/raspi/cam_2/imx296_pi/
/Python/raspi/cam_3/imx296_pi/ /Python/raspi/cam_3/imx296_pi/
Python/OAK/datasets/oak-fcc-3/audit/
/Python/OAK/datasets/oak-fcc-3/benchmarks_async
/Python/OAK/datasets/oak-d/benchmark_corridor
/AgroBase/livox_visual_debugger/x64/Debug

View File

@ -105,7 +105,7 @@ namespace AgroBase.Forms.IHM.Operacao.Parametros
_mapa.TipoMapa = op.Parametros.TipoMapa; _mapa.TipoMapa = op.Parametros.TipoMapa;
await _mapa.DefinirRuasSelecionadasAsync(ruasSelecionadas, false); await _mapa.DefinirRuasSelecionadasAsync(op, ruasSelecionadas, false);
lblMapaPlaceholder.Visible = false; lblMapaPlaceholder.Visible = false;

View File

@ -169,7 +169,7 @@ namespace AgroBase.Forms.IHM.Operacao
mapa.TipoMapa = op.Parametros?.TipoMapa ?? Enums.TipoMapaOperacao.Indefinido; mapa.TipoMapa = op.Parametros?.TipoMapa ?? Enums.TipoMapaOperacao.Indefinido;
await mapa.DefinirRuasSelecionadasAsync(ruasSelecionadas, false); await mapa.DefinirRuasSelecionadasAsync(op, ruasSelecionadas, false);
} }
else else
{ {

View File

@ -197,7 +197,7 @@ namespace AgroBase.Forms.IHM.Operacao
if (mapaAtual != null) if (mapaAtual != null)
{ {
novaOperacao.Mapa = mapaAtual; novaOperacao.Mapa = mapaAtual;
await mapaAtual.LimparParametrizacaoMapaAsync(); await mapaAtual.LimparParametrizacaoMapaAsync(novaOperacao);
} }
/* /*

View File

@ -367,6 +367,11 @@
this.tabOrientacao = new System.Windows.Forms.TabPage(); this.tabOrientacao = new System.Windows.Forms.TabPage();
this.pnlInclinacao = new System.Windows.Forms.Panel(); this.pnlInclinacao = new System.Windows.Forms.Panel();
this.pnl3D = new System.Windows.Forms.Panel(); this.pnl3D = new System.Windows.Forms.Panel();
this.pnlOrientacaoDados = new System.Windows.Forms.Panel();
this.txtImuLateral = new System.Windows.Forms.TextBox();
this.txtImuFrontal = new System.Windows.Forms.TextBox();
this.label147 = new System.Windows.Forms.Label();
this.label146 = new System.Windows.Forms.Label();
this.tabSonar = new System.Windows.Forms.TabPage(); this.tabSonar = new System.Windows.Forms.TabPage();
this.chbSonarDeteccoes = new System.Windows.Forms.CheckBox(); this.chbSonarDeteccoes = new System.Windows.Forms.CheckBox();
this.tabControl3 = new System.Windows.Forms.TabControl(); this.tabControl3 = new System.Windows.Forms.TabControl();
@ -531,11 +536,7 @@
this.label36 = new System.Windows.Forms.Label(); this.label36 = new System.Windows.Forms.Label();
this.txtMomento = new System.Windows.Forms.TextBox(); this.txtMomento = new System.Windows.Forms.TextBox();
this.btnPlay = new System.Windows.Forms.Button(); this.btnPlay = new System.Windows.Forms.Button();
this.pnlOrientacaoDados = new System.Windows.Forms.Panel(); this.btnCarregarMapa = new System.Windows.Forms.Button();
this.label146 = new System.Windows.Forms.Label();
this.label147 = new System.Windows.Forms.Label();
this.txtImuFrontal = new System.Windows.Forms.TextBox();
this.txtImuLateral = new System.Windows.Forms.TextBox();
((System.ComponentModel.ISupportInitialize)(this.trbMomento)).BeginInit(); ((System.ComponentModel.ISupportInitialize)(this.trbMomento)).BeginInit();
this.panel1.SuspendLayout(); this.panel1.SuspendLayout();
this.tabControl4.SuspendLayout(); this.tabControl4.SuspendLayout();
@ -590,6 +591,7 @@
((System.ComponentModel.ISupportInitialize)(this.picRobo)).BeginInit(); ((System.ComponentModel.ISupportInitialize)(this.picRobo)).BeginInit();
this.tabOrientacao.SuspendLayout(); this.tabOrientacao.SuspendLayout();
this.pnl3D.SuspendLayout(); this.pnl3D.SuspendLayout();
this.pnlOrientacaoDados.SuspendLayout();
this.tabSonar.SuspendLayout(); this.tabSonar.SuspendLayout();
this.tabControl3.SuspendLayout(); this.tabControl3.SuspendLayout();
this.tabSonarRGB.SuspendLayout(); this.tabSonarRGB.SuspendLayout();
@ -616,7 +618,6 @@
this.panel6.SuspendLayout(); this.panel6.SuspendLayout();
this.pnlInformacoesOperacao.SuspendLayout(); this.pnlInformacoesOperacao.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.picCameraCaminho)).BeginInit(); ((System.ComponentModel.ISupportInitialize)(this.picCameraCaminho)).BeginInit();
this.pnlOrientacaoDados.SuspendLayout();
this.SuspendLayout(); this.SuspendLayout();
// //
// trbMomento // trbMomento
@ -4131,6 +4132,51 @@
this.pnl3D.Size = new System.Drawing.Size(486, 512); this.pnl3D.Size = new System.Drawing.Size(486, 512);
this.pnl3D.TabIndex = 1; this.pnl3D.TabIndex = 1;
// //
// pnlOrientacaoDados
//
this.pnlOrientacaoDados.Controls.Add(this.txtImuLateral);
this.pnlOrientacaoDados.Controls.Add(this.txtImuFrontal);
this.pnlOrientacaoDados.Controls.Add(this.label147);
this.pnlOrientacaoDados.Controls.Add(this.label146);
this.pnlOrientacaoDados.Location = new System.Drawing.Point(1, 362);
this.pnlOrientacaoDados.Name = "pnlOrientacaoDados";
this.pnlOrientacaoDados.Size = new System.Drawing.Size(150, 150);
this.pnlOrientacaoDados.TabIndex = 0;
//
// txtImuLateral
//
this.txtImuLateral.Location = new System.Drawing.Point(6, 87);
this.txtImuLateral.Name = "txtImuLateral";
this.txtImuLateral.Size = new System.Drawing.Size(100, 20);
this.txtImuLateral.TabIndex = 3;
this.txtImuLateral.TextAlign = System.Windows.Forms.HorizontalAlignment.Right;
//
// txtImuFrontal
//
this.txtImuFrontal.Location = new System.Drawing.Point(6, 32);
this.txtImuFrontal.Name = "txtImuFrontal";
this.txtImuFrontal.Size = new System.Drawing.Size(100, 20);
this.txtImuFrontal.TabIndex = 1;
this.txtImuFrontal.TextAlign = System.Windows.Forms.HorizontalAlignment.Right;
//
// label147
//
this.label147.AutoSize = true;
this.label147.Location = new System.Drawing.Point(3, 71);
this.label147.Name = "label147";
this.label147.Size = new System.Drawing.Size(39, 13);
this.label147.TabIndex = 2;
this.label147.Text = "Lateral";
//
// label146
//
this.label146.AutoSize = true;
this.label146.Location = new System.Drawing.Point(3, 16);
this.label146.Name = "label146";
this.label146.Size = new System.Drawing.Size(39, 13);
this.label146.TabIndex = 1;
this.label146.Text = "Frontal";
//
// tabSonar // tabSonar
// //
this.tabSonar.Controls.Add(this.chbSonarDeteccoes); this.tabSonar.Controls.Add(this.chbSonarDeteccoes);
@ -5875,10 +5921,10 @@
// //
// btnCarregarOperacao // btnCarregarOperacao
// //
this.btnCarregarOperacao.Location = new System.Drawing.Point(1180, 587); this.btnCarregarOperacao.Location = new System.Drawing.Point(1138, 587);
this.btnCarregarOperacao.Margin = new System.Windows.Forms.Padding(2); this.btnCarregarOperacao.Margin = new System.Windows.Forms.Padding(2);
this.btnCarregarOperacao.Name = "btnCarregarOperacao"; this.btnCarregarOperacao.Name = "btnCarregarOperacao";
this.btnCarregarOperacao.Size = new System.Drawing.Size(128, 28); this.btnCarregarOperacao.Size = new System.Drawing.Size(115, 28);
this.btnCarregarOperacao.TabIndex = 4; this.btnCarregarOperacao.TabIndex = 4;
this.btnCarregarOperacao.Text = "Carregar Opereação"; this.btnCarregarOperacao.Text = "Carregar Opereação";
this.btnCarregarOperacao.UseVisualStyleBackColor = true; this.btnCarregarOperacao.UseVisualStyleBackColor = true;
@ -5907,7 +5953,7 @@
// //
// btnPlay // btnPlay
// //
this.btnPlay.Location = new System.Drawing.Point(1120, 587); this.btnPlay.Location = new System.Drawing.Point(1078, 587);
this.btnPlay.Margin = new System.Windows.Forms.Padding(2); this.btnPlay.Margin = new System.Windows.Forms.Padding(2);
this.btnPlay.Name = "btnPlay"; this.btnPlay.Name = "btnPlay";
this.btnPlay.Size = new System.Drawing.Size(56, 28); this.btnPlay.Size = new System.Drawing.Size(56, 28);
@ -5916,56 +5962,23 @@
this.btnPlay.UseVisualStyleBackColor = true; this.btnPlay.UseVisualStyleBackColor = true;
this.btnPlay.Click += new System.EventHandler(this.btnPlay_Click); this.btnPlay.Click += new System.EventHandler(this.btnPlay_Click);
// //
// pnlOrientacaoDados // btnCarregarMapa
// //
this.pnlOrientacaoDados.Controls.Add(this.txtImuLateral); this.btnCarregarMapa.Location = new System.Drawing.Point(1257, 587);
this.pnlOrientacaoDados.Controls.Add(this.txtImuFrontal); this.btnCarregarMapa.Margin = new System.Windows.Forms.Padding(2);
this.pnlOrientacaoDados.Controls.Add(this.label147); this.btnCarregarMapa.Name = "btnCarregarMapa";
this.pnlOrientacaoDados.Controls.Add(this.label146); this.btnCarregarMapa.Size = new System.Drawing.Size(77, 28);
this.pnlOrientacaoDados.Location = new System.Drawing.Point(1, 362); this.btnCarregarMapa.TabIndex = 96;
this.pnlOrientacaoDados.Name = "pnlOrientacaoDados"; this.btnCarregarMapa.Text = "Mapa";
this.pnlOrientacaoDados.Size = new System.Drawing.Size(150, 150); this.btnCarregarMapa.UseVisualStyleBackColor = true;
this.pnlOrientacaoDados.TabIndex = 0; this.btnCarregarMapa.Click += new System.EventHandler(this.btnCarregarMapa_Click);
//
// label146
//
this.label146.AutoSize = true;
this.label146.Location = new System.Drawing.Point(3, 16);
this.label146.Name = "label146";
this.label146.Size = new System.Drawing.Size(39, 13);
this.label146.TabIndex = 1;
this.label146.Text = "Frontal";
//
// label147
//
this.label147.AutoSize = true;
this.label147.Location = new System.Drawing.Point(3, 71);
this.label147.Name = "label147";
this.label147.Size = new System.Drawing.Size(39, 13);
this.label147.TabIndex = 2;
this.label147.Text = "Lateral";
//
// txtImuFrontal
//
this.txtImuFrontal.Location = new System.Drawing.Point(6, 32);
this.txtImuFrontal.Name = "txtImuFrontal";
this.txtImuFrontal.Size = new System.Drawing.Size(100, 20);
this.txtImuFrontal.TabIndex = 1;
this.txtImuFrontal.TextAlign = System.Windows.Forms.HorizontalAlignment.Right;
//
// txtImuLateral
//
this.txtImuLateral.Location = new System.Drawing.Point(6, 87);
this.txtImuLateral.Name = "txtImuLateral";
this.txtImuLateral.Size = new System.Drawing.Size(100, 20);
this.txtImuLateral.TabIndex = 3;
this.txtImuLateral.TextAlign = System.Windows.Forms.HorizontalAlignment.Right;
// //
// frmResultadosOperacao // frmResultadosOperacao
// //
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(1345, 632); this.ClientSize = new System.Drawing.Size(1345, 632);
this.Controls.Add(this.btnCarregarMapa);
this.Controls.Add(this.btnPlay); this.Controls.Add(this.btnPlay);
this.Controls.Add(this.txtMomento); this.Controls.Add(this.txtMomento);
this.Controls.Add(this.label36); this.Controls.Add(this.label36);
@ -6061,6 +6074,8 @@
((System.ComponentModel.ISupportInitialize)(this.picRobo)).EndInit(); ((System.ComponentModel.ISupportInitialize)(this.picRobo)).EndInit();
this.tabOrientacao.ResumeLayout(false); this.tabOrientacao.ResumeLayout(false);
this.pnl3D.ResumeLayout(false); this.pnl3D.ResumeLayout(false);
this.pnlOrientacaoDados.ResumeLayout(false);
this.pnlOrientacaoDados.PerformLayout();
this.tabSonar.ResumeLayout(false); this.tabSonar.ResumeLayout(false);
this.tabSonar.PerformLayout(); this.tabSonar.PerformLayout();
this.tabControl3.ResumeLayout(false); this.tabControl3.ResumeLayout(false);
@ -6096,8 +6111,6 @@
this.pnlInformacoesOperacao.ResumeLayout(false); this.pnlInformacoesOperacao.ResumeLayout(false);
this.pnlInformacoesOperacao.PerformLayout(); this.pnlInformacoesOperacao.PerformLayout();
((System.ComponentModel.ISupportInitialize)(this.picCameraCaminho)).EndInit(); ((System.ComponentModel.ISupportInitialize)(this.picCameraCaminho)).EndInit();
this.pnlOrientacaoDados.ResumeLayout(false);
this.pnlOrientacaoDados.PerformLayout();
this.ResumeLayout(false); this.ResumeLayout(false);
this.PerformLayout(); this.PerformLayout();
@ -6588,5 +6601,6 @@
private System.Windows.Forms.TextBox txtImuFrontal; private System.Windows.Forms.TextBox txtImuFrontal;
private System.Windows.Forms.Label label147; private System.Windows.Forms.Label label147;
private System.Windows.Forms.Label label146; private System.Windows.Forms.Label label146;
private System.Windows.Forms.Button btnCarregarMapa;
} }
} }

View File

@ -1,5 +1,6 @@
using AgroBase.Models; using AgroBase.Models;
using AgroBase.Models.Modules; using AgroBase.Models.Modules;
using AgroBase.Models.Operacoes;
using AgroBase.Models.Operadores; using AgroBase.Models.Operadores;
using AgroBase.Properties; using AgroBase.Properties;
using AgroBase.Services; using AgroBase.Services;
@ -12,6 +13,7 @@ using System.Drawing.Drawing2D;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
@ -53,9 +55,14 @@ namespace AgroBase.Forms.Operacoes
DateTime MomentoAtual = DateTime.Now; DateTime MomentoAtual = DateTime.Now;
AsyncTaskTimerModel tmrPlay; AsyncTaskTimerModel tmrPlay;
private List<List<GPSModel>> _ruasMapaReplay = new List<List<GPSModel>>();
private List<PontoTrajetoriaModel> _trajetoriaFixaReplay = new List<PontoTrajetoriaModel>();
MapasModel Mapa = new MapasModel(); MapasModel Mapa = new MapasModel();
private Visualizador3DService visualizador3D; //TrajetoriaMapaOperacaoModel Trajetoria = new TrajetoriaMapaOperacaoModel(new List<List<GPSModel>>());
private MapaDinamicoModel MapaDinamico; private MapaDinamicoModel MapaDinamico;
private Visualizador3DService visualizador3D;
private int idxMomentoAtual private int idxMomentoAtual
{ {
get get
@ -165,12 +172,46 @@ namespace AgroBase.Forms.Operacoes
LogsOperacao = JsonConvert.DeserializeObject<OperacaoSensoriamentoLogModel[]>(OperacaoModel.DeserializarDadosOperacao(ofd.FileName, data.operacao)).ToList(); LogsOperacao = JsonConvert.DeserializeObject<OperacaoSensoriamentoLogModel[]>(OperacaoModel.DeserializarDadosOperacao(ofd.FileName, data.operacao)).ToList();
LogsGPS = JsonConvert.DeserializeObject<GPSModel[]>(OperacaoModel.DeserializarDadosOperacao(ofd.FileName, Enums.T_Code.Gps.ToString())).ToList(); LogsGPS = JsonConvert.DeserializeObject<GPSModel[]>(OperacaoModel.DeserializarDadosOperacao(ofd.FileName, Enums.T_Code.Gps.ToString())).ToList();
GPSModel pos = GPSService.UltimaLeitura.Clone();
GPSService.UltimaLeitura = LogsGPS.FirstOrDefault(); string caminhoOpr = Path.Combine(
bool sucesso = op.CarregarParametrizacaoOperacao(null, null, ofd.FileName.Replace("data.lgop", "operacao.opr")); Path.GetDirectoryName(ofd.FileName),
bool condicaoOperacaoCarregada() => op.Trajetoria?.CorredorAtual != null; "operacao.opr"
bool resultado = await FuncoesGlobais.AguardarCondicaoAsync(condicaoOperacaoCarregada, 15000, 200); );
GPSService.UltimaLeitura = pos;
OperacaoParametrosModel parametrosOperacao = null;
if (File.Exists(caminhoOpr))
{
parametrosOperacao =
JsonConvert.DeserializeObject<OperacaoParametrosModel>(
File.ReadAllText(caminhoOpr, Encoding.UTF8)
);
if (parametrosOperacao != null)
{
GPSModel posAnterior =
GPSService.UltimaLeitura?.Clone();
try
{
// Importantíssimo:
// a trajetória será projetada tomando como referência
// a posição inicial real daquela operação.
GPSService.UltimaLeitura =
LogsGPS.FirstOrDefault()?.Clone();
Variaveis.OperacaoEmAndamento = OperacaoModel.CarregarParamerosOperacaoBase(
parametrosOperacao
);
}
finally
{
if (posAnterior != null)
GPSService.UltimaLeitura = posAnterior;
}
}
}
LogsControle = JsonConvert.DeserializeObject<OperacaoControleModel[]>(OperacaoModel.DeserializarDadosOperacao(ofd.FileName, "controle")).ToList(); LogsControle = JsonConvert.DeserializeObject<OperacaoControleModel[]>(OperacaoModel.DeserializarDadosOperacao(ofd.FileName, "controle")).ToList();
LogsCooler = JsonConvert.DeserializeObject<CoolerControlDataModel[]>(OperacaoModel.DeserializarDadosOperacao(ofd.FileName, "cooler")).ToList(); LogsCooler = JsonConvert.DeserializeObject<CoolerControlDataModel[]>(OperacaoModel.DeserializarDadosOperacao(ofd.FileName, "cooler")).ToList();
@ -239,7 +280,7 @@ namespace AgroBase.Forms.Operacoes
}; };
MapasService.CriarArquivoMapa(new List<List<GPSModel>>() { LogsGPS.ToList() }, 1517, data.id); MapasService.CriarArquivoMapa(new List<List<GPSModel>>() { LogsGPS.ToList() }, 1517, data.id);
Mapa.btnCarregar_Click(sender, e, Path.Combine(Variaveis.CaminhoSistema, Variaveis.CaminhoMapasConvertidos + data.id + ".json")); Mapa.btnCarregar_Click(sender, e, Path.Combine(Variaveis.CaminhoSistema, Variaveis.CaminhoMapasConvertidos + data.id + ".json"));
trbMomento.Maximum = LogsOperacao.Count - 1; trbMomento.Maximum = LogsOperacao.Count - 1;
trbMomento.Minimum = 0; trbMomento.Minimum = 0;
trbMomento.Value = 0; trbMomento.Value = 0;
@ -1309,28 +1350,60 @@ namespace AgroBase.Forms.Operacoes
pnlOrientacaoGPS.Invalidate(); pnlOrientacaoGPS.Invalidate();
var _trajaetoria = LogsGPS.Select(x => new PontoTrajetoriaModel(Enums.TipoPontoRua.Indefinido)
{
Posicao = x
})
.ToList();
var LogOpe = LogsOperacao[idxMomentoAtual]; var LogOpe = LogsOperacao[idxMomentoAtual];
var LogSnr = LogsVisualWorker[idxMomentoAtual]; var LogSnr = LogsVisualWorker[idxMomentoAtual];
var LogTrj = LogsTrajetoria[idxMomentoAtual]; var LogTrj = LogsTrajetoria[idxMomentoAtual];
var LogCtrl = LogsControle[idxMomentoAtual]; var LogCtrl = LogsControle[idxMomentoAtual];
var trajetoriaFixa =
(_trajetoriaFixaReplay?.Count ?? 0) > 0
? _trajetoriaFixaReplay
: Variaveis.OperacaoEmAndamento
?.Trajetoria
?._TrajetoriaFixa
?? new List<PontoTrajetoriaModel>();
var trajetoriaDinamica =
MontarTrajetoriaDinamicaReplay(
Log,
LogTrj
);
var ruasMapa =
(_ruasMapaReplay?.Count ?? 0) > 0
? _ruasMapaReplay
: Variaveis.OperacaoEmAndamento
?.Mapa
?.TrajetoriaMapa
?? new List<List<GPSModel>>();
var rastroRover =
LogsGPS
.Take(idxMomentoAtual + 1)
.ToList();
Variaveis.OperacaoEmAndamento.Trajetoria.LoopAtualizaDados();
MapaDinamico.AtualizarDados( MapaDinamico.AtualizarDados(
//(LogSnr?.Iniciado ?? false) && LogSnr.Resumo.DirecaoDesvio != Enums.Direcao.Frente ? LogSnr.Resumo.ObstaculoCritico : null, null,
null,
(float)LogTrj.AnguloCaminho, (float)LogTrj.AnguloCaminho,
(float)Log.AnguloCarroDefinido, (float)Log.AnguloCarroDefinido,
Log, Log,
_trajaetoria,
Variaveis.OperacaoEmAndamento.Trajetoria?._TrajetoriaFixa, // Roxo: trajetória ainda restante naquele instante
new List<GPSModel>(LogsGPS.Take(LogsGPS.IndexOf(Log) + 1)), trajetoriaDinamica,
Variaveis.OperacaoEmAndamento.Mapa.TrajetoriaMapa,
// Verde/azul: trajetória fixa completa projetada
trajetoriaFixa,
// Laranja: caminho realmente percorrido até aquele instante
rastroRover,
// Azul/preto: linhas originais do mapa
ruasMapa,
// Verde tracejado: simulação MPC daquele frame
LogCtrl.SimulacaoMPC LogCtrl.SimulacaoMPC
); );
} }
@ -2334,6 +2407,48 @@ namespace AgroBase.Forms.Operacoes
} }
} }
} }
private async void btnCarregarMapa_Click(object sender, EventArgs e)
{
}
private List<PontoTrajetoriaModel> MontarTrajetoriaDinamicaReplay(GPSModel posicaoAtual, OperacaoSensoriamentoLogTrajetoriaModel logTrajetoria)
{
var resultado = new List<PontoTrajetoriaModel>();
if (posicaoAtual != null)
{
resultado.Add(
new PontoTrajetoriaModel(Enums.TipoPontoRua.PosicaoRobo)
{
Posicao = posicaoAtual,
LarguraCorredor = 1.0,
idxCorredor = logTrajetoria?.CorredorAtual?.Idx ?? 0,
Visitado = true
}
);
}
if (_trajetoriaFixaReplay == null ||
_trajetoriaFixaReplay.Count == 0)
{
return resultado;
}
int idxProximo =
logTrajetoria?.ProximoPonto?.idxPonto ?? 0;
resultado.AddRange(
_trajetoriaFixaReplay
.Where(x =>
x != null &&
x.idxPonto >= idxProximo)
);
return resultado;
}
} }
} }

View File

@ -88,7 +88,7 @@ namespace AgroBase.Forms
txtAnguloGPS.Text = anguloInicial.ToString("0.00"); txtAnguloGPS.Text = anguloInicial.ToString("0.00");
txtDistanciaGPS.Text = distanciaInicial.ToString("0.00"); txtDistanciaGPS.Text = distanciaInicial.ToString("0.00");
Variaveis.OperacaoEmAndamento.GPSTrajetoria = new System.Collections.Generic.List<GPSModel>(); Variaveis.OperacaoEmAndamento.GPSTrajetoria = new System.Collections.Generic.List<GPSModel>();
Variaveis.OperacaoEmAndamento.Trajetoria = new TrajetoriaMapaOperacaoModel(Variaveis.OperacaoEmAndamento.Mapa.TrajetoriaMapa); Variaveis.OperacaoEmAndamento.Trajetoria = new TrajetoriaMapaOperacaoModel(Variaveis.OperacaoEmAndamento, Variaveis.OperacaoEmAndamento.Mapa.TrajetoriaMapa);
Variaveis.OperacaoEmAndamento.Trajetoria.ProjetarTrajetoriaFixa(); Variaveis.OperacaoEmAndamento.Trajetoria.ProjetarTrajetoriaFixa();
_robotState = new TreinamentoIAModel(); _robotState = new TreinamentoIAModel();
} }

View File

@ -344,7 +344,7 @@ namespace AgroBase.Models
_mapaEnviado = true; _mapaEnviado = true;
} }
public void DefinirRuasSelecionadas(IEnumerable<string> ruas, bool popularTrajetoria = false) public void DefinirRuasSelecionadas(OperacaoModel op, IEnumerable<string> ruas, bool popularTrajetoria = false)
{ {
RuasPercorrer = RuasPercorrer =
ruas == null ruas == null
@ -361,12 +361,12 @@ namespace AgroBase.Models
} }
if (popularTrajetoria) if (popularTrajetoria)
PopularTrajetoriaMapa(); PopularTrajetoriaMapa(op);
} }
public async Task DefinirRuasSelecionadasAsync(IEnumerable<string> ruas, bool popularTrajetoria = false) public async Task DefinirRuasSelecionadasAsync(OperacaoModel op, IEnumerable<string> ruas, bool popularTrajetoria = false)
{ {
DefinirRuasSelecionadas(ruas, popularTrajetoria); DefinirRuasSelecionadas(op, ruas, popularTrajetoria);
if (!_paginaCarregada || browser?.CoreWebView2 == null) if (!_paginaCarregada || browser?.CoreWebView2 == null)
{ {
@ -378,11 +378,11 @@ namespace AgroBase.Models
await browser.CoreWebView2.ExecuteScriptAsync("definirSelecaoRuas(" + ruasJson + ");"); await browser.CoreWebView2.ExecuteScriptAsync("definirSelecaoRuas(" + ruasJson + ");");
} }
public Task DefinirRuasSelecionadasAsync(IEnumerable<int> ruas) public Task DefinirRuasSelecionadasAsync(OperacaoModel op, IEnumerable<int> ruas)
{ {
IEnumerable<string> convertidas = ruas == null ? null : ruas.Select(x => x.ToString()); IEnumerable<string> convertidas = ruas == null ? null : ruas.Select(x => x.ToString());
return DefinirRuasSelecionadasAsync(convertidas); return DefinirRuasSelecionadasAsync(op, convertidas);
} }
private bool PodeSelecionarRuas() private bool PodeSelecionarRuas()
@ -424,7 +424,7 @@ namespace AgroBase.Models
.Distinct() .Distinct()
.ToList(); .ToList();
PopularTrajetoriaMapa(); PopularTrajetoriaMapa(Variaveis.OperacaoEmAndamento);
RuasSelecionadasAlteradas?.Invoke(this, EventArgs.Empty); RuasSelecionadasAlteradas?.Invoke(this, EventArgs.Empty);
} }
catch (Exception ex) catch (Exception ex)
@ -433,7 +433,7 @@ namespace AgroBase.Models
} }
} }
public void PopularTrajetoriaMapa(MapaFeatureCollectionModel dados = null) public void PopularTrajetoriaMapa(OperacaoModel op, MapaFeatureCollectionModel dados = null)
{ {
if (dados != null) if (dados != null)
mapaService.DefinirMapa(dados, mapaService.NomeArquivos ?? "Mapa"); mapaService.DefinirMapa(dados, mapaService.NomeArquivos ?? "Mapa");
@ -512,14 +512,12 @@ namespace AgroBase.Models
ruasMontadas.Add(rua); ruasMontadas.Add(rua);
} }
OperacaoModel op = Variaveis.OperacaoEmAndamento;
if (op == null) if (op == null)
throw new InvalidOperationException("Operação indisponível para instalar a trajetória."); throw new InvalidOperationException("Operação indisponível para instalar a trajetória.");
var trajetoriaAnterior = op.Trajetoria; var trajetoriaAnterior = op.Trajetoria;
var mapaAnterior = TrajetoriaMapa; var mapaAnterior = TrajetoriaMapa;
var novaTrajetoria = new TrajetoriaMapaOperacaoModel(ruasMontadas); var novaTrajetoria = new TrajetoriaMapaOperacaoModel(op, ruasMontadas);
try try
{ {
@ -647,9 +645,9 @@ namespace AgroBase.Models
pnlMapa = null; pnlMapa = null;
} }
public async Task LimparParametrizacaoMapaAsync() public async Task LimparParametrizacaoMapaAsync(OperacaoModel op)
{ {
DefinirRuasSelecionadas(new List<string>(), false); DefinirRuasSelecionadas(op, new List<string>(), false);
if (_paginaCarregada && browser?.CoreWebView2 != null) if (_paginaCarregada && browser?.CoreWebView2 != null)
{ {
await browser.CoreWebView2.ExecuteScriptAsync("definirSelecaoRuas([]);"); await browser.CoreWebView2.ExecuteScriptAsync("definirSelecaoRuas([]);");

View File

@ -476,7 +476,6 @@ namespace AgroBase.Models
Simulando = false, Simulando = false,
GPSTrajetoria = new List<GPSModel>(), GPSTrajetoria = new List<GPSModel>(),
Mapa = new MapasModel(), Mapa = new MapasModel(),
Trajetoria = new TrajetoriaMapaOperacaoModel(new List<List<GPSModel>>()),
ControleAnterior = new OperacaoControleModel(), ControleAnterior = new OperacaoControleModel(),
Parametros = new OperacaoParametrosModel() Parametros = new OperacaoParametrosModel()
{ {
@ -499,10 +498,12 @@ namespace AgroBase.Models
} }
} }
}; };
op.Trajetoria = new TrajetoriaMapaOperacaoModel(op, new List<List<GPSModel>>());
op.Sensoriamento = new OperacaoSensoriamentoConjuntoModel(op) op.Sensoriamento = new OperacaoSensoriamentoConjuntoModel(op)
{ {
Operacao = new OperacaoSensoriamentoLogModel() Operacao = new OperacaoSensoriamentoLogModel()
{ {
Modo = Modo,
OperacaoIniciada = false, OperacaoIniciada = false,
DataInicio = DateTime.MinValue, DataInicio = DateTime.MinValue,
DataFim = DateTime.MinValue DataFim = DateTime.MinValue
@ -559,7 +560,7 @@ namespace AgroBase.Models
AtuPercentualErvasBicoOff = 2, AtuPercentualErvasBicoOff = 2,
AtuPercentualErvasBicoOn = 1, AtuPercentualErvasBicoOn = 1,
AtuPercentualInicioPulverizacao = 85, AtuPercentualInicioPulverizacao = 85,
AtuPressaoLinha = 18, AtuPressaoLinha = 22,
AtuAgitadorModo = ModoAgitadorCalda.SemAgitacao, AtuAgitadorModo = ModoAgitadorCalda.SemAgitacao,
AtuModoControle = ModoControleBomba.PID, AtuModoControle = ModoControleBomba.PID,
AtuModeloCabeca = CabecasModelo.Target, AtuModeloCabeca = CabecasModelo.Target,
@ -666,14 +667,14 @@ namespace AgroBase.Models
DirVelocidadeMovimento = 80, DirVelocidadeMovimento = 80,
DirTipoMovimento = TiposControladorDirecional.MPC, DirTipoMovimento = TiposControladorDirecional.MPC,
DistanciaManobra = 3.0, DistanciaManobra = 2.2,
AtuAlturaAreaPulverizacao = 10, AtuAlturaAreaPulverizacao = 10,
AtuDuracaoAtuacao = 0, AtuDuracaoAtuacao = 0,
AtuPercentualErvasBicoOff = 2, AtuPercentualErvasBicoOff = 2,
AtuPercentualErvasBicoOn = 1, AtuPercentualErvasBicoOn = 1,
AtuPercentualInicioPulverizacao = 85, AtuPercentualInicioPulverizacao = 85,
AtuPressaoLinha = 18, AtuPressaoLinha = 22,
AtuAgitadorModo = ModoAgitadorCalda.Continuo, AtuAgitadorModo = ModoAgitadorCalda.Continuo,
AtuModoControle = ModoControleBomba.PID, AtuModoControle = ModoControleBomba.PID,
AtuModeloCabeca = CabecasModelo.Target, AtuModeloCabeca = CabecasModelo.Target,
@ -1312,9 +1313,9 @@ namespace AgroBase.Models
if (parametros.Mapa != null) if (parametros.Mapa != null)
{ {
novaOp.Mapa = new MapasModel(); novaOp.Mapa = new MapasModel();
novaOp.Mapa.DefinirRuasSelecionadas(ruasSelecionadas); novaOp.Mapa.DefinirRuasSelecionadas(novaOp, ruasSelecionadas);
novaOp.Mapa.TipoMapa = novaOp.Parametros.TipoMapa; novaOp.Mapa.TipoMapa = novaOp.Parametros.TipoMapa;
novaOp.Mapa.PopularTrajetoriaMapa(parametros.Mapa); novaOp.Mapa.PopularTrajetoriaMapa(novaOp, parametros.Mapa);
} }
novaOp?.DispAtu?.Dados?.PrepararAtuadorParaOperacao(); novaOp?.DispAtu?.Dados?.PrepararAtuadorParaOperacao();

View File

@ -16,8 +16,9 @@ namespace AgroBase.Models
{ {
public class TrajetoriaMapaOperacaoModel public class TrajetoriaMapaOperacaoModel
{ {
public TrajetoriaMapaOperacaoModel(List<List<GPSModel>> RuasMapa) public TrajetoriaMapaOperacaoModel(OperacaoModel _op, List<List<GPSModel>> RuasMapa)
{ {
op = _op;
RuasPlantacao = ClonarRuasMapa(RuasMapa); RuasPlantacao = ClonarRuasMapa(RuasMapa);
AutonomiaCorredor = new AutonomiaCorredorModel(); AutonomiaCorredor = new AutonomiaCorredorModel();
} }
@ -47,12 +48,15 @@ namespace AgroBase.Models
private const double ToleranciaCoordenada = 1e-12; private const double ToleranciaCoordenada = 1e-12;
private const double DistanciaMaximaSaltoMapaM = 25.0; private const double DistanciaMaximaSaltoMapaM = 25.0;
[JsonIgnore]
private readonly OperacaoModel op;
#region PARAMETROS #region PARAMETROS
public double AnguloAberturaCurva { get; set; } = 25; // Angulo usado para deslocar o ponto de curva public double AnguloAberturaCurva { get; set; } = 25; // Angulo usado para deslocar o ponto de curva
public double DistanciaProjecaoRua => public double DistanciaProjecaoRua =>
(VariaveisEquipamento.DistanciaEntreEixosCm / 100.0 / 2.0) + (VariaveisEquipamento.DistanciaEntreEixosCm / 100.0 / 2.0) +
(Variaveis.OperacaoEmAndamento?.Parametros?.Controle?.DistanciaManobra ?? 3.0); // Distancia para projetar o primeiro ponto para fora do corredor (op?.Parametros?.Controle?.DistanciaManobra ?? 3.0); // Distancia para projetar o primeiro ponto para fora do corredor
public static double DistanciaEntrePontos { get; set; } = 0.8; // Distancia entre os pontos dentro do corredor public static double DistanciaEntrePontos { get; set; } = 0.8; // Distancia entre os pontos dentro do corredor
public static double DistanciaEntrePontosCurva { get; set; } = 0.25; // Distancia entre os pontos durante a curva entre corredores public static double DistanciaEntrePontosCurva { get; set; } = 0.25; // Distancia entre os pontos durante a curva entre corredores
private double DistanciaManobraEntreRuas { get; set; } = 3.0; // Distancia máxima para gerar a curva de conexão entre os corredores private double DistanciaManobraEntreRuas { get; set; } = 3.0; // Distancia máxima para gerar a curva de conexão entre os corredores
@ -318,7 +322,7 @@ namespace AgroBase.Models
public static double DistanciaMaximaEntreLeituras { get; private set; } public static double DistanciaMaximaEntreLeituras { get; private set; }
public void AtualizarDistanciaMaximaEntreLeituras() public void AtualizarDistanciaMaximaEntreLeituras()
{ {
double percentualVelocidade = Variaveis.OperacaoEmAndamento?.Controle?.PercentualVelocidadeSP ?? 0; double percentualVelocidade = op?.Controle?.PercentualVelocidadeSP ?? 0;
double velocidadeCarroMs = FuncoesMatematicas.CalculaVelocidadeMsPercentual(percentualVelocidade); double velocidadeCarroMs = FuncoesMatematicas.CalculaVelocidadeMsPercentual(percentualVelocidade);
double distanciaMaxima = velocidadeCarroMs / Math.Max(1, GPSService.TaxaAmostragemHz); double distanciaMaxima = velocidadeCarroMs / Math.Max(1, GPSService.TaxaAmostragemHz);
@ -673,7 +677,7 @@ namespace AgroBase.Models
if (CorredorAtual != null) if (CorredorAtual != null)
{ {
bool operacaoEmAndamento = bool operacaoEmAndamento =
Variaveis.OperacaoEmAndamento?.Sensoriamento?.Operacao?.StatusOperacaoAtual == op?.Sensoriamento?.Operacao?.StatusOperacaoAtual ==
StatusOperacao.EmAndamento; StatusOperacao.EmAndamento;
if ((RetornandoBase && CorredorAtual.Dentro) || (!RetornandoBase && operacaoEmAndamento)) if ((RetornandoBase && CorredorAtual.Dentro) || (!RetornandoBase && operacaoEmAndamento))
@ -784,8 +788,6 @@ namespace AgroBase.Models
public string TempoEstimadoRestante { get; private set; } public string TempoEstimadoRestante { get; private set; }
public void AtualizarTempoEstimado(double? velocidadeSemErvasMs = null, double? velocidadeComErvasMs = null) public void AtualizarTempoEstimado(double? velocidadeSemErvasMs = null, double? velocidadeComErvasMs = null)
{ {
var op = Variaveis.OperacaoEmAndamento;
if (op?.Sensoriamento == null || op?.Parametros?.Controle == null) if (op?.Sensoriamento == null || op?.Parametros?.Controle == null)
{ {
TempoEstimadoOperacao = "00:00:00"; TempoEstimadoOperacao = "00:00:00";
@ -871,8 +873,6 @@ namespace AgroBase.Models
if (CorredorAtual.Idx > 0) return; if (CorredorAtual.Idx > 0) return;
if (!CorredorAtual.Dentro) return; if (!CorredorAtual.Dentro) return;
var op = Variaveis.OperacaoEmAndamento;
if (op?.Sensoriamento == null) if (op?.Sensoriamento == null)
return; return;
@ -952,7 +952,7 @@ namespace AgroBase.Models
{ {
MarcarVisitadoAte(_TrajetoriaFixa.Count - 1); MarcarVisitadoAte(_TrajetoriaFixa.Count - 1);
Variaveis.OperacaoEmAndamento.Sensoriamento?.InserirLog( op.Sensoriamento?.InserirLog(
T_Code.Trj, T_Code.Trj,
StatusModulo.Operante, StatusModulo.Operante,
100, 100,
@ -976,7 +976,6 @@ namespace AgroBase.Models
} }
private void AtualizarErroCombinado() private void AtualizarErroCombinado()
{ {
var op = Variaveis.OperacaoEmAndamento;
var gps = GPSPosicaoAtual; var gps = GPSPosicaoAtual;
if (op?.Parametros?.Controle == null || gps == null || PontoAtual?.Posicao == null || ProximoPonto?.Posicao == null) if (op?.Parametros?.Controle == null || gps == null || PontoAtual?.Posicao == null || ProximoPonto?.Posicao == null)
@ -1139,6 +1138,10 @@ namespace AgroBase.Models
for (int i = idxInicial; i <= idxFinal; i++) for (int i = idxInicial; i <= idxFinal; i++)
{ {
if (PontoAtual.idxPonto == _TrajetoriaFixa[i].idxPonto)
{
}
_TrajetoriaFixa[i].AtualizarPropriedades( _TrajetoriaFixa[i].AtualizarPropriedades(
_gpsAtualCiclo, _gpsAtualCiclo,
_gpsAnteriorCiclo, _gpsAnteriorCiclo,
@ -1261,7 +1264,7 @@ namespace AgroBase.Models
if (!confirmou) if (!confirmou)
return; return;
Variaveis.OperacaoEmAndamento.Sensoriamento?.InserirLog( op.Sensoriamento?.InserirLog(
T_Code.Trj, T_Code.Trj,
StatusModulo.Operante, StatusModulo.Operante,
100, 100,
@ -1468,7 +1471,7 @@ namespace AgroBase.Models
private bool VerificaInicioOperacaoMeioRua(bool considerarStatus) private bool VerificaInicioOperacaoMeioRua(bool considerarStatus)
{ {
if (_TrajetoriaFixaDefinida && !VerificacaoInicialMeioRuaConcluida && ((considerarStatus && new List<StatusOperacao>() { StatusOperacao.Aguardando, StatusOperacao.EmAndamento }.Contains(Variaveis.OperacaoEmAndamento.Sensoriamento.Operacao.StatusOperacaoAtual)) || !considerarStatus)) if (_TrajetoriaFixaDefinida && !VerificacaoInicialMeioRuaConcluida && ((considerarStatus && new List<StatusOperacao>() { StatusOperacao.Aguardando, StatusOperacao.EmAndamento }.Contains(op.Sensoriamento.Operacao.StatusOperacaoAtual)) || !considerarStatus))
{ {
var _posicaoAtual = GPSPosicaoAtual; var _posicaoAtual = GPSPosicaoAtual;
if (_posicaoAtual == null) if (_posicaoAtual == null)
@ -1487,7 +1490,7 @@ namespace AgroBase.Models
if (!double.IsNaN(distanciaUltimoPonto) && !double.IsInfinity(distanciaUltimoPonto) && distanciaUltimoPonto < 5.0) if (!double.IsNaN(distanciaUltimoPonto) && !double.IsInfinity(distanciaUltimoPonto) && distanciaUltimoPonto < 5.0)
{ {
msg = $"Início no meio da rua ignorado: robô está a {distanciaUltimoPonto:0.00}m do fim. Nova operação começará do zero."; msg = $"Início no meio da rua ignorado: robô está a {distanciaUltimoPonto:0.00}m do fim. Nova operação começará do zero.";
Variaveis.OperacaoEmAndamento?.Sensoriamento?.InserirLog(T_Code.Trj, StatusModulo.Operante, 100, msg); op?.Sensoriamento?.InserirLog(T_Code.Trj, StatusModulo.Operante, 100, msg);
Variaveis.MostrarLog($"[TRJ] {msg}"); Variaveis.MostrarLog($"[TRJ] {msg}");
return false; return false;
@ -1504,7 +1507,7 @@ namespace AgroBase.Models
) )
{ {
msg = "Inicio no meio da rua rejeitado: geometria e trajetoria discordam sobre o corredor atual."; msg = "Inicio no meio da rua rejeitado: geometria e trajetoria discordam sobre o corredor atual.";
Variaveis.OperacaoEmAndamento?.Sensoriamento?.InserirLog(T_Code.Trj, StatusModulo.Falha, 0, msg); op?.Sensoriamento?.InserirLog(T_Code.Trj, StatusModulo.Falha, 0, msg);
Variaveis.MostrarLog("[TRJ] " + msg); Variaveis.MostrarLog("[TRJ] " + msg);
throw new InvalidOperationException(msg); throw new InvalidOperationException(msg);
} }
@ -1517,14 +1520,14 @@ namespace AgroBase.Models
CorredorAtual?.AtualizarDados(); CorredorAtual?.AtualizarDados();
msg = $"Início no meio da rua confirmado: idx={idxPontoMaisProximo}/{_TrajetoriaFixa.Count - 1}, marcados={idxPontoMaisProximo + 1}/{_TrajetoriaFixa.Count}."; msg = $"Início no meio da rua confirmado: idx={idxPontoMaisProximo}/{_TrajetoriaFixa.Count - 1}, marcados={idxPontoMaisProximo + 1}/{_TrajetoriaFixa.Count}.";
Variaveis.OperacaoEmAndamento?.Sensoriamento?.InserirLog(T_Code.Trj, StatusModulo.Alerta, 100, msg); op?.Sensoriamento?.InserirLog(T_Code.Trj, StatusModulo.Alerta, 100, msg);
Variaveis.MostrarLog($"[TRJ] {msg}"); Variaveis.MostrarLog($"[TRJ] {msg}");
return true; return true;
} }
msg = $"Início no meio da rua ignorado: robô está fora do corredor. Nova operação começará do zero."; msg = $"Início no meio da rua ignorado: robô está fora do corredor. Nova operação começará do zero.";
Variaveis.OperacaoEmAndamento?.Sensoriamento?.InserirLog(T_Code.Trj, StatusModulo.Operante, 100, msg); op?.Sensoriamento?.InserirLog(T_Code.Trj, StatusModulo.Operante, 100, msg);
Variaveis.MostrarLog($"[TRJ] {msg}"); Variaveis.MostrarLog($"[TRJ] {msg}");
} }
@ -1534,7 +1537,6 @@ namespace AgroBase.Models
private void VerificaFimCorredorSegmentacao() private void VerificaFimCorredorSegmentacao()
{ {
var op = Variaveis.OperacaoEmAndamento;
double limiteFim = op?.Parametros?.Controle?.AnteciparManobraCorredorM ?? 0; double limiteFim = op?.Parametros?.Controle?.AnteciparManobraCorredorM ?? 0;
if ( if (
@ -1972,7 +1974,7 @@ namespace AgroBase.Models
catch (Exception ex) catch (Exception ex)
{ {
Variaveis.MostrarLog("[TRJ] Falha no ciclo da trajetoria: " + ex.Message); Variaveis.MostrarLog("[TRJ] Falha no ciclo da trajetoria: " + ex.Message);
Variaveis.OperacaoEmAndamento?.Sensoriamento?.InserirLog( op?.Sensoriamento?.InserirLog(
T_Code.Trj, T_Code.Trj,
StatusModulo.Falha, StatusModulo.Falha,
0, 0,
@ -2005,8 +2007,6 @@ namespace AgroBase.Models
private void LoopAtualizaDadosCore() private void LoopAtualizaDadosCore()
{ {
var op = Variaveis.OperacaoEmAndamento;
if (!(op?.Parametros?.ControleAutomatico ?? false)) if (!(op?.Parametros?.ControleAutomatico ?? false))
return; return;
@ -2023,8 +2023,6 @@ namespace AgroBase.Models
private void AtualizarDadosControleRedis() private void AtualizarDadosControleRedis()
{ {
var op = Variaveis.OperacaoEmAndamento;
if (op == null || !op.Sensoriamento.Operacao.OperacaoIniciada) if (op == null || !op.Sensoriamento.Operacao.OperacaoIniciada)
{ {
return; return;
@ -2270,7 +2268,7 @@ namespace AgroBase.Models
DefinirPontoAtual(); DefinirPontoAtual();
DefinirProximoPonto(); DefinirProximoPonto();
Variaveis.OperacaoEmAndamento.Sensoriamento?.InserirLog( op.Sensoriamento?.InserirLog(
T_Code.Trj, T_Code.Trj,
StatusModulo.Operante, StatusModulo.Operante,
100, 100,
@ -3203,7 +3201,7 @@ namespace AgroBase.Models
List<List<GPSModel>> ruasParaProjetar; List<List<GPSModel>> ruasParaProjetar;
if (Variaveis.OperacaoEmAndamento?.Mapa?.TipoMapa == TipoMapaOperacao.Corredores) if (op?.Mapa?.TipoMapa == TipoMapaOperacao.Corredores)
{ {
throw new NotSupportedException( throw new NotSupportedException(
"O tipo de mapa Corredores ainda nao possui geracao validada para campo. " + "O tipo de mapa Corredores ainda nao possui geracao validada para campo. " +
@ -3679,7 +3677,7 @@ namespace AgroBase.Models
private void ProjetarTrajetoriaFixaCore(GPSModel posicaoRobo) private void ProjetarTrajetoriaFixaCore(GPSModel posicaoRobo)
{ {
if (Variaveis.OperacaoEmAndamento?.Sensoriamento?.Operacao?.Modo != ModoOperacao.MapaGPS) if (op?.Parametros?.Modo != ModoOperacao.MapaGPS)
return; return;
List<PontoTrajetoriaModel> _trajetoriaFixa = new List<PontoTrajetoriaModel>(); List<PontoTrajetoriaModel> _trajetoriaFixa = new List<PontoTrajetoriaModel>();
@ -3693,7 +3691,7 @@ namespace AgroBase.Models
catch (Exception ex) catch (Exception ex)
{ {
Variaveis.MostrarLog($"[TRJ] Trajetória rejeitada: {ex.Message}"); Variaveis.MostrarLog($"[TRJ] Trajetória rejeitada: {ex.Message}");
Variaveis.OperacaoEmAndamento?.Sensoriamento?.InserirLog(T_Code.Trj, StatusModulo.Falha, 0, $"Trajetória rejeitada: {ex.Message}"); op?.Sensoriamento?.InserirLog(T_Code.Trj, StatusModulo.Falha, 0, $"Trajetória rejeitada: {ex.Message}");
throw; throw;
} }
@ -3810,7 +3808,8 @@ namespace AgroBase.Models
Corredores[idx] = CorredorAtual; Corredores[idx] = CorredorAtual;
double anguloProjetar1 = GPSUtils.CalcularOrientacao(CorredorAtual.Skip(1).FirstOrDefault(), CorredorAtual.FirstOrDefault()); double anguloProjetar1 = GPSUtils.CalcularOrientacao(CorredorAtual.Skip(1).FirstOrDefault(), CorredorAtual.FirstOrDefault());
GPSModel PrimeiroPonto = GPSUtils.ProjetarPontoDeslocado(CorredorAtual.FirstOrDefault(), DistanciaProjecaoRua * 0.8, anguloProjetar1); double percentualAcrescimoPontoAberturaCurvaPrimeiroPonto = 0.7;
GPSModel PrimeiroPonto = GPSUtils.ProjetarPontoDeslocado(CorredorAtual.FirstOrDefault(), DistanciaProjecaoRua * percentualAcrescimoPontoAberturaCurvaPrimeiroPonto, anguloProjetar1);
var ultimoPontoTrajetoria = _trajetoriaFixa.LastOrDefault(); var ultimoPontoTrajetoria = _trajetoriaFixa.LastOrDefault();
@ -3823,7 +3822,7 @@ namespace AgroBase.Models
Direcao = direcaoAtual, Direcao = direcaoAtual,
Orientacao = GPSUtils.CalcularOrientacao(ultimoPontoTrajetoria.Posicao, PrimeiroPonto), Orientacao = GPSUtils.CalcularOrientacao(ultimoPontoTrajetoria.Posicao, PrimeiroPonto),
Visitado = false, Visitado = false,
LarguraCorredor = larguraCorredorMenor LarguraCorredor = LarguraCorredorPadrao // larguraCorredorMenor
}; };
_trajetoriaFixa.Add(PontoInicial); _trajetoriaFixa.Add(PontoInicial);
@ -3849,8 +3848,8 @@ namespace AgroBase.Models
_trajetoriaFixa.Add(PontoTrajetoria); _trajetoriaFixa.Add(PontoTrajetoria);
} }
double percentualAcrescimoPontoAberturaCurva = 0.8; double percentualAcrescimoPontoAberturaCurvaUltimoPonto = 0.8;
double distancia_projetar = !ultimoCorredor ? (DistanciaProjecaoRua * percentualAcrescimoPontoAberturaCurva) : DistanciaProjecaoRua; double distancia_projetar = !ultimoCorredor ? (DistanciaProjecaoRua * percentualAcrescimoPontoAberturaCurvaUltimoPonto) : DistanciaProjecaoRua;
GPSModel ultimoPontoCorredorAtual = CorredorAtual.Last(); GPSModel ultimoPontoCorredorAtual = CorredorAtual.Last();
double anguloProjetar2 = GPSUtils.CalcularOrientacao(CorredorAtual.Skip(CorredorAtual.Count() - 2).FirstOrDefault(), ultimoPontoCorredorAtual); double anguloProjetar2 = GPSUtils.CalcularOrientacao(CorredorAtual.Skip(CorredorAtual.Count() - 2).FirstOrDefault(), ultimoPontoCorredorAtual);
@ -3882,7 +3881,7 @@ namespace AgroBase.Models
Direcao = direcaoAtual, Direcao = direcaoAtual,
Orientacao = GPSUtils.CalcularOrientacao(_trajetoriaFixa.Last().Posicao, UltimoPonto), Orientacao = GPSUtils.CalcularOrientacao(_trajetoriaFixa.Last().Posicao, UltimoPonto),
Visitado = false, Visitado = false,
LarguraCorredor = larguraCorredorMenor // ultimoCorredor ? larguraCorredorMenor : (larguraCorredorMenor * 0.8) LarguraCorredor = LarguraCorredorPadrao * 1.1 // larguraCorredorMenor // ultimoCorredor ? larguraCorredorMenor : (larguraCorredorMenor * 0.8)
}; };
_trajetoriaFixa.Add(PontoFinal); _trajetoriaFixa.Add(PontoFinal);
@ -4376,7 +4375,6 @@ namespace AgroBase.Models
double distanciaTotalAnterior = DistanciaTotal; double distanciaTotalAnterior = DistanciaTotal;
double distanciaPercorridaAnterior = DistanciaPercorrida; double distanciaPercorridaAnterior = DistanciaPercorrida;
var autonomiaAnterior = AutonomiaCorredor?.Clone(); var autonomiaAnterior = AutonomiaCorredor?.Clone();
var op = Variaveis.OperacaoEmAndamento;
var parametrosAnteriores = op?.Parametros; var parametrosAnteriores = op?.Parametros;
bool simulandoAnterior = op?.Simulando ?? false; bool simulandoAnterior = op?.Simulando ?? false;
bool? operacaoIniciadaAnterior = op?.Sensoriamento?.Operacao?.OperacaoIniciada; bool? operacaoIniciadaAnterior = op?.Sensoriamento?.Operacao?.OperacaoIniciada;
@ -4407,7 +4405,7 @@ namespace AgroBase.Models
} }
Variaveis.MostrarLog("[RETORNO] Trajetoria rejeitada: " + ex.Message); Variaveis.MostrarLog("[RETORNO] Trajetoria rejeitada: " + ex.Message);
Variaveis.OperacaoEmAndamento?.Sensoriamento?.InserirLog( op?.Sensoriamento?.InserirLog(
T_Code.Trj, T_Code.Trj,
StatusModulo.Falha, StatusModulo.Falha,
0, 0,
@ -4649,7 +4647,7 @@ namespace AgroBase.Models
AtualizarTrajetoriaDinamicaCore(); AtualizarTrajetoriaDinamicaCore();
AtualizarDistanciaRestante(); AtualizarDistanciaRestante();
Variaveis.OperacaoEmAndamento.Sensoriamento?.InserirLog(T_Code.Trj, StatusModulo.Operante, 100, $"[RETORNO] Trajetória aplicada ({motivo}) - Pontos: {traj.Count}"); op.Sensoriamento?.InserirLog(T_Code.Trj, StatusModulo.Operante, 100, $"[RETORNO] Trajetória aplicada ({motivo}) - Pontos: {traj.Count}");
} }
private List<GPSModel> CortarFinalDaRota(List<GPSModel> pontos, double distanciaAntesDoFimM) private List<GPSModel> CortarFinalDaRota(List<GPSModel> pontos, double distanciaAntesDoFimM)
@ -4689,8 +4687,6 @@ namespace AgroBase.Models
private void AtualizarDadosOperacaoParaRetorno(List<GPSModel> waypointsManual) private void AtualizarDadosOperacaoParaRetorno(List<GPSModel> waypointsManual)
{ {
var op = Variaveis.OperacaoEmAndamento;
if (op == null) if (op == null)
throw new InvalidOperationException("Operacao indisponivel ao iniciar retorno."); throw new InvalidOperationException("Operacao indisponivel ao iniciar retorno.");
@ -4784,7 +4780,7 @@ namespace AgroBase.Models
*/ */
if (!ReferenceEquals( if (!ReferenceEquals(
op, op,
Variaveis.OperacaoEmAndamento)) op))
{ {
Variaveis.MostrarLog( Variaveis.MostrarLog(
"[RETORNO/MAPSYNC] Operação mudou durante sincronização." "[RETORNO/MAPSYNC] Operação mudou durante sincronização."
@ -5971,7 +5967,7 @@ namespace AgroBase.Models
*/ */
limiteDistancia = Math.Min(limiteDistancia, 0.80); limiteDistancia = Math.Min(limiteDistancia, 0.80);
if (NaMargem && DistanciaAtual <= limiteDistancia) if (NaMargem) // && DistanciaAtual <= limiteDistancia
{ {
Visitado = true; Visitado = true;
} }

View File

@ -324,7 +324,7 @@ namespace AgroBase.Services.Operadores
{ {
percent_vel_max = pControle.MovVelocidadeSErvasPercent, percent_vel_max = pControle.MovVelocidadeSErvasPercent,
percent_vel_min = pControle.MovVelocidadeCErvasPercent, percent_vel_min = pControle.MovVelocidadeCErvasPercent,
percent_vel_curva = 20.0, percent_vel_curva = 15.0,
rampa_partida_pct = VariaveisEquipamento.PercentualVelMin, rampa_partida_pct = VariaveisEquipamento.PercentualVelMin,
rampa_aceleracao_pct_s = 12.0, rampa_aceleracao_pct_s = 12.0,
rampa_desaceleracao_pct_s = 18.0, rampa_desaceleracao_pct_s = 18.0,
@ -548,7 +548,7 @@ namespace AgroBase.Services.Operadores
$"[MAPSYNC] Tentativa={tentativa} | " + $"[MAPSYNC] Tentativa={tentativa} | " +
$"cmd={commandId} | " + $"cmd={commandId} | " +
$"rev={mapRevision} | " + $"rev={mapRevision} | " +
$"pontos={op.Trajetoria._TrajetoriaFixa.Count}" $"pontos={op.Trajetoria._TrajetoriaFixa?.Count}"
); );
bool atualizado = bool atualizado =

View File

@ -1,6 +1,9 @@
import time import time
from shared.enums import ModoOperacao, StatusModulo, StatusCarroMapa, T_Code from shared.enums import (
ModoOperacao, StatusModulo, StatusCarroMapa, T_Code,
TipoMovimentoDirecional,
)
from shared.contexto_global_redis import ContextoGlobalRedis, CtxKey from shared.contexto_global_redis import ContextoGlobalRedis, CtxKey
from manager_worker.config import mostrar_log from manager_worker.config import mostrar_log
from manager_worker.filtros import FiltroVelocidade from manager_worker.filtros import FiltroVelocidade
@ -67,6 +70,44 @@ def definir_comando(filtro_vel: FiltroVelocidade, erro_orientacao: float = None)
abs(_float(dir_cfg.get("angulo_max", 30.0), 30.0)) abs(_float(dir_cfg.get("angulo_max", 30.0), 30.0))
) )
# V5 - velocidade coerente com a política geométrica do MPC.
# Até ~5° o carro pode manter velocidade de cruzeiro. Entre 5° e 15°
# reduz progressivamente; a partir de 15° (faixa de MovimentoArco) usa
# velocidade de curva. Os parâmetros podem ser publicados em Mov sem
# exigir mudança imediata do contrato C#.
erro_heading_curva_forte_graus = _clamp(
_float(
mov_cfg.get("erro_heading_curva_forte_graus", 15.0),
15.0,
),
3.0,
ang_max,
)
erro_heading_curva_deadband_graus = _clamp(
_float(
mov_cfg.get("erro_heading_curva_deadband_graus", 5.0),
5.0,
),
0.0,
max(0.0, erro_heading_curva_forte_graus - 0.5),
)
# Movimento/ângulo atualmente aplicados. Como o Processador calcula MOV
# antes de DIR, isto funciona como memória segura do ciclo anterior. O
# erro geométrico abaixo continua sendo o gatilho primário do ciclo atual.
tipo_direcional_atual = _enum_or(
TipoMovimentoDirecional,
controle.get(
"tipo_movimento_direcional",
TipoMovimentoDirecional.RodasDianteiras.value,
),
TipoMovimentoDirecional.RodasDianteiras,
)
angulo_direcional_atual = abs(_float(
controle.get("angulo_sp", 0.0),
0.0,
))
pontos_min_fim_corredor = max( pontos_min_fim_corredor = max(
0, 0,
_int(mov_cfg.get("pontos_fim_corredor_reduzir", 3), 3) _int(mov_cfg.get("pontos_fim_corredor_reduzir", 3), 3)
@ -105,6 +146,12 @@ def definir_comando(filtro_vel: FiltroVelocidade, erro_orientacao: float = None)
status_carro = estado["status_carro"] status_carro = estado["status_carro"]
erro_angular = estado["erro_orientacao"] erro_angular = estado["erro_orientacao"]
erro_angular_caminho = float(
estado.get("erro_angular_caminho", erro_angular)
)
erro_angular_combinado = float(
estado.get("erro_angular_combinado", erro_angular)
)
pontos_fim_corredor = estado["pontos_fim_corredor"] pontos_fim_corredor = estado["pontos_fim_corredor"]
estado_valido = estado["valido"] estado_valido = estado["valido"]
@ -130,6 +177,12 @@ def definir_comando(filtro_vel: FiltroVelocidade, erro_orientacao: float = None)
), ),
} }
debug["pontos_fim_corredor"] = pontos_fim_corredor debug["pontos_fim_corredor"] = pontos_fim_corredor
debug["geometria_curva"] = {
"erro_heading_curva_forte_graus": round(erro_heading_curva_forte_graus, 3),
"erro_heading_curva_deadband_graus": round(erro_heading_curva_deadband_graus, 3),
"tipo_direcional_atual": tipo_direcional_atual.name,
"angulo_direcional_atual": round(angulo_direcional_atual, 3),
}
if not estado_valido: if not estado_valido:
motivo = estado.get("motivo", "Estado operacional inválido para movimento") motivo = estado.get("motivo", "Estado operacional inválido para movimento")
@ -156,8 +209,41 @@ def definir_comando(filtro_vel: FiltroVelocidade, erro_orientacao: float = None)
limitar_velocidade_por_weed = False limitar_velocidade_por_weed = False
reduzir_para_fim_corredor = False reduzir_para_fim_corredor = False
reduzir_por_curva = False reduzir_por_curva = False
teto_rigido_curva = False
teto_curva_pct = None
reduzir_por_ipb = False reduzir_por_ipb = False
# Erro geométrico que conversa com o seletor do MPC. Em tracking de
# caminho e no retorno usamos o heading da centerline, evitando que um
# simples offset lateral pareça uma curva. Em Direcionando preservamos
# o erro combinado porque ali a aquisição do alvo ainda é relevante.
if status_carro in [
StatusCarroMapa.CaminhandoRua,
StatusCarroMapa.Direcionando,
StatusCarroMapa.RetornandoBase,
]:
# Mesma referência geométrica do MPC V5: heading da centerline.
# Assim, rover paralelo porém deslocado não é confundido com curva.
erro_curva_graus = abs(erro_angular_caminho)
fonte_curva = "erro_angular_caminho"
else:
erro_curva_graus = abs(erro_angular_combinado)
fonte_curva = "erro_angular_combinado"
arco_ja_aplicado = (
tipo_direcional_atual == TipoMovimentoDirecional.MovimentoArco
)
curva_forte_geometrica = (
erro_curva_graus >= erro_heading_curva_forte_graus
)
debug["geometria_curva"].update({
"erro_usado_graus": round(float(erro_curva_graus), 3),
"fonte_erro": fonte_curva,
"arco_ja_aplicado": bool(arco_ja_aplicado),
"curva_forte_geometrica": bool(curva_forte_geometrica),
})
if status_carro == StatusCarroMapa.Parado: if status_carro == StatusCarroMapa.Parado:
velocidade_sp = 0.0 velocidade_sp = 0.0
hard_stop = True hard_stop = True
@ -172,6 +258,8 @@ def definir_comando(filtro_vel: FiltroVelocidade, erro_orientacao: float = None)
]: ]:
velocidade_sp = vel_curva velocidade_sp = vel_curva
reduzir_por_curva = True reduzir_por_curva = True
teto_rigido_curva = True
teto_curva_pct = vel_curva
debug["motivos"].append("movimento em curva/manobra") debug["motivos"].append("movimento em curva/manobra")
elif status_carro in [ elif status_carro in [
@ -179,16 +267,27 @@ def definir_comando(filtro_vel: FiltroVelocidade, erro_orientacao: float = None)
StatusCarroMapa.RetornandoBase StatusCarroMapa.RetornandoBase
]: ]:
velocidade_sp = calcular_velocidade_relativa( velocidade_sp = calcular_velocidade_relativa(
vel_min=vel_com_ervas, vel_min=vel_curva,
vel_max=vel_sem_ervas, vel_max=vel_sem_ervas,
ang_max=ang_max, ang_max=erro_heading_curva_forte_graus,
erro_orientacao=erro_angular, erro_orientacao=erro_curva_graus,
k=0.85, k=1.0,
curva=0.80, curva=0.80,
dead=10.0 dead=erro_heading_curva_deadband_graus
) )
reduzir_por_curva = bool(
erro_curva_graus > erro_heading_curva_deadband_graus
or arco_ja_aplicado
)
teto_rigido_curva = bool(reduzir_por_curva)
if teto_rigido_curva:
teto_curva_pct = (
vel_curva
if (curva_forte_geometrica or arco_ja_aplicado)
else velocidade_sp
)
debug["motivos"].append( debug["motivos"].append(
"direcionando/retornando: velocidade por erro angular combinado" f"{status_carro.name}: velocidade pela geometria do caminho/alvo"
) )
elif status_carro == StatusCarroMapa.CaminhandoRua: elif status_carro == StatusCarroMapa.CaminhandoRua:
@ -244,15 +343,30 @@ def definir_comando(filtro_vel: FiltroVelocidade, erro_orientacao: float = None)
) )
velocidade_livre = calcular_velocidade_relativa( velocidade_livre = calcular_velocidade_relativa(
vel_min=vel_com_ervas, vel_min=vel_curva,
vel_max=vel_sem_ervas, vel_max=vel_sem_ervas,
ang_max=ang_max, ang_max=erro_heading_curva_forte_graus,
erro_orientacao=erro_angular, erro_orientacao=erro_curva_graus,
k=1.0, k=1.0,
curva=0.70, curva=0.80,
dead=5.0 dead=erro_heading_curva_deadband_graus
) )
# Mesmo que o C# publique CaminhandoRua durante RetornoBase, uma
# curva forte da centerline não pode ser tratada como reta rápida.
# O heading do caminho é a mesma referência usada pelo MPC V5.
reduzir_por_curva = bool(
erro_curva_graus > erro_heading_curva_deadband_graus
or arco_ja_aplicado
)
teto_rigido_curva = bool(reduzir_por_curva)
if teto_rigido_curva:
teto_curva_pct = (
vel_curva
if (curva_forte_geometrica or arco_ja_aplicado)
else velocidade_livre
)
debug["motivos"].append( debug["motivos"].append(
"caminhando rua: velocidade por erro angular do caminho" "caminhando rua: velocidade por erro angular do caminho"
) )
@ -287,6 +401,16 @@ def definir_comando(filtro_vel: FiltroVelocidade, erro_orientacao: float = None)
f"status_carro não tratado com segurança: {status_carro.name}" f"status_carro não tratado com segurança: {status_carro.name}"
) )
debug["geometria_curva"].update({
"reduzir_por_curva": bool(reduzir_por_curva),
"teto_rigido_curva": bool(teto_rigido_curva),
"teto_curva_pct": (
None if teto_curva_pct is None
else round(float(teto_curva_pct), 3)
),
"vel_curva_pct": round(float(vel_curva), 3),
})
# Se já decidiu parar, não faz limitador tentar reviver velocidade. # Se já decidiu parar, não faz limitador tentar reviver velocidade.
if hard_stop: if hard_stop:
_resetar_rampa_velocidade(filtro_vel) _resetar_rampa_velocidade(filtro_vel)
@ -422,6 +546,29 @@ def definir_comando(filtro_vel: FiltroVelocidade, erro_orientacao: float = None)
debug=debug, debug=debug,
) )
# Teto rígido de curva/Arco. A rampa de segurança continua suavizando
# reduções moderadas, mas uma geometria já classificada como curva forte
# não pode passar um ciclo em velocidade de cruzeiro enquanto o MPC
# coloca o rover em MovimentoArco.
if teto_rigido_curva and teto_curva_pct is not None:
teto_curva_pct = _clamp(teto_curva_pct, vel_min_equip, vel_sem_ervas)
velocidade_filtrada = min(velocidade_filtrada, teto_curva_pct)
try:
filtro_vel._ultimo_sp_campo = velocidade_filtrada
filtro_vel._ultimo_ts_campo = time.monotonic()
except Exception:
pass
motivo_curva = (
"curva forte/Arco"
if (curva_forte_geometrica or arco_ja_aplicado)
else "curva moderada"
)
debug["limitadores"].append(
f"{motivo_curva}: teto geométrico em {teto_curva_pct:.1f}%"
)
# Teto rígido do WeedWorker: # Teto rígido do WeedWorker:
# #
# - ervas detectadas: nunca ultrapassa vel_com_ervas; # - ervas detectadas: nunca ultrapassa vel_com_ervas;

View File

@ -430,6 +430,65 @@ class ControladorMPC:
8.0, min_value=1.0, max_value=self._direcionando_arco_entrada_graus, 8.0, min_value=1.0, max_value=self._direcionando_arco_entrada_graus,
) )
# --------------------------------------------------------------
# V5 - Política geométrica universal do path tracking.
#
# A lógica vale tanto para a passada agrícola quanto para a rota geral
# de RetornoBase:
# - heading muito errado -> MovimentoArco;
# - heading moderadamente errado -> RodasDianteiras;
# - heading praticamente alinhado + cross-track -> MovimentoDiagonal.
#
# A histerese 15°/8° evita ficar trocando Arco <-> Dianteira na
# fronteira. Os defaults herdam a política já validada em Direcionando.
# Manobras estruturais reais continuam com Arco obrigatório.
# --------------------------------------------------------------
self._tracking_arco_entrada_graus = _safe_float(
parametros_mpc.get(
"tracking_arco_entrada_graus",
self._direcionando_arco_entrada_graus,
),
self._direcionando_arco_entrada_graus,
min_value=3.0,
max_value=60.0,
)
self._tracking_arco_saida_graus = _safe_float(
parametros_mpc.get(
"tracking_arco_saida_graus",
self._direcionando_arco_saida_graus,
),
self._direcionando_arco_saida_graus,
min_value=1.0,
max_value=self._tracking_arco_entrada_graus,
)
# --------------------------------------------------------------
# V4 - Entrada/saída de rua: Arco + RodasDianteiras competem.
#
# A preferência usa o HEADING DO CAMINHO, e não o bearing até o
# alvo. Assim, quando o corpo já está alinhado com a próxima rua,
# RodasDianteiras ganham vantagem e terminam a aquisição de forma
# suave. Arco continua favorecido quando o heading ainda está muito
# errado.
#
# Entre os dois limiares a preferência varia continuamente; a
# penalidade normal de troca de tipo fornece histerese adicional.
# --------------------------------------------------------------
self._transicao_rua_dianteira_heading_graus = _safe_float(
parametros_mpc.get("transicao_rua_dianteira_heading_graus", 7.0),
7.0, min_value=0.5, max_value=30.0,
)
self._transicao_rua_arco_heading_graus = _safe_float(
parametros_mpc.get("transicao_rua_arco_heading_graus", 20.0),
20.0,
min_value=self._transicao_rua_dianteira_heading_graus + 0.5,
max_value=60.0,
)
self._transicao_rua_penalidade_modo = _safe_float(
parametros_mpc.get("transicao_rua_penalidade_modo", 1.0),
1.0, min_value=0.0, max_value=5.0,
)
# Amortecimento aplicado ao tracking de passada. MovimentoArco em # Amortecimento aplicado ao tracking de passada. MovimentoArco em
# manobra preserva autoridade; RodasDianteiras/Diagonal são limitados. # manobra preserva autoridade; RodasDianteiras/Diagonal são limitados.
# MPC preserva toda a autoridade angular necessaria para a manobra. # MPC preserva toda a autoridade angular necessaria para a manobra.
@ -744,6 +803,46 @@ class ControladorMPC:
return True return True
return False return False
def _segmento_rua_puro_mesmo_corredor(self, idx_base):
"""
True somente quando a referência local está realmente em um trecho
operacional Rua -> Rua do MESMO corredor.
Diferente de _segmento_operacional_mesmo_corredor(), aqui não aceitamos
ponto estrutural isolado. Isso é importante para distinguir:
- recuperação dentro/ao lado de uma passada reta;
- entrada, saída, ligação ou cabeceira real.
Na recuperação de uma passada, MovimentoArco é indesejado: ele dobra a
autoridade de yaw e pode transformar um desvio lateral em uma inversão
brusca de heading.
"""
n = len(self.pontos_info)
if n < 2:
return False
idx = max(0, min(_safe_int(idx_base, 0), n - 1))
pares = []
if idx + 1 < n:
pares.append((idx, idx + 1))
if idx - 1 >= 0:
pares.append((idx - 1, idx))
for a, b in pares:
ca, cb = self._idx_corredor(a), self._idx_corredor(b)
if ca < 0 or cb < 0 or ca != cb:
continue
if self._ponto_estrutural(a) or self._ponto_estrutural(b):
continue
return True
return False
def _limitar_s_ao_corredor(self, s_proj, s_desejado, idx_base): def _limitar_s_ao_corredor(self, s_proj, s_desejado, idx_base):
"""Não deixa o heading look-ahead enxergar a rua seguinte pela cabeceira.""" """Não deixa o heading look-ahead enxergar a rua seguinte pela cabeceira."""
if self._s_nodes is None or len(self._s_nodes) == 0: if self._s_nodes is None or len(self._s_nodes) == 0:
@ -812,42 +911,81 @@ class ControladorMPC:
return float(self._wrap_pi(float(theta_ref) - float(theta_robo))) return float(self._wrap_pi(float(theta_ref) - float(theta_robo)))
def _tracking_reta_permitido(self, contexto, idx_base, e_lat, erro_heading_rad): def _tracking_reta_permitido(self, contexto, idx_base, e_lat, erro_heading_rad):
"""Decide se a política de passada pode assumir o controle. """
Decide se o path tracking geométrico deve assumir o controle.
CaminhandoRua sempre usa path tracking. Entrando/Manobrando só migra Regras V5:
cedo para a política de reta quando a referência ainda pertence ao
mesmo corredor e a pose já está perto o bastante da passada. Isso 1) CaminhandoRua / EntrandoRua / SaindoRua / Direcionando /
evita o caso real em que o rover inicia alinhado mas recebe Arco por RetornandoBase:
burocracia de estado. usam a mesma política geométrica universal. O estado operacional
continua útil para velocidade e semântica, mas não deve aprisionar
a direção em uma geometria específica.
2) Manobrando em trecho Rua -> Rua do MESMO corredor:
é recuperação de trajetória e também pode usar a política universal.
3) Manobra estrutural real:
fica fora deste ramo e preserva MovimentoArco obrigatório, adequado
à curva de cabeceira/180 graus.
""" """
carro = _as_dict(_as_dict(contexto).get("Carro", {})) carro = _as_dict(_as_dict(contexto).get("Carro", {}))
status = carro.get("Status", StatusCarroMapa.Parado.value) status = carro.get("Status", StatusCarroMapa.Parado.value)
if _status_in(status, [StatusCarroMapa.CaminhandoRua]):
if _status_in(
status,
[
StatusCarroMapa.CaminhandoRua,
StatusCarroMapa.EntrandoRua,
StatusCarroMapa.SaindoRua,
StatusCarroMapa.Direcionando,
StatusCarroMapa.RetornandoBase,
],
):
return True return True
if not _status_in(status, [StatusCarroMapa.EntrandoRua, StatusCarroMapa.Manobrando]): if _status_in(status, [StatusCarroMapa.Manobrando]):
return False return self._segmento_rua_puro_mesmo_corredor(idx_base)
if not self._segmento_operacional_mesmo_corredor(idx_base):
return False
return bool( return False
abs(float(e_lat)) <= self._lateral_reta_aquisicao_max_m
and abs(math.degrees(float(erro_heading_rad))) <= self._heading_reta_aquisicao_max_graus
)
def _referencia_direcional_reta(self, e_lat, erro_heading_rad, velocidade, tipo_preferido=None): def _referencia_direcional_reta(self, e_lat, erro_heading_rad, velocidade, tipo_preferido=None):
"""Escolhe modo e centro angular da busca para uma passada. """Escolhe a geometria pelo estado geométrico rover x caminho.
- heading fora do alinhamento -> RodasDianteiras e corrige heading; Política universal V5:
- heading alinhado -> MovimentoDiagonal para remover cross-track sem - heading muito errado -> MovimentoArco para recuperar orientação com
girar o corpo; menor raio;
- histerese estreita impede ficar 5-10 graus torto em diagonal. - heading moderadamente errado -> RodasDianteiras;
- heading praticamente alinhado + cross-track -> MovimentoDiagonal;
- heading alinhado e centralizado -> RodasDianteiras praticamente reta.
A decisão usa heading do CAMINHO, nunca bearing rover->waypoint.
""" """
e_lat = float(e_lat) e_lat = float(e_lat)
v = max(0.0, float(velocidade)) v = max(0.0, float(velocidade))
e_head_deg = abs(math.degrees(float(erro_heading_rad))) erro_heading_rad = float(erro_heading_rad)
e_head_deg = abs(math.degrees(erro_heading_rad))
tipo_prev = _movimento_from_value(tipo_preferido) tipo_prev = _movimento_from_value(tipo_preferido)
# 1) Erro grande de heading tem prioridade absoluta sobre cross-track.
# A histerese mantém Arco até o erro cair bem abaixo do limiar de
# entrada, evitando caça de geometria em torno de 15 graus.
manter_arco = (
tipo_prev == TipoMovimentoDirecional.MovimentoArco
and e_head_deg > self._tracking_arco_saida_graus
)
entrar_arco = e_head_deg >= self._tracking_arco_entrada_graus
if entrar_arco or manter_arco:
delta = self._ganho_heading_reta * erro_heading_rad
lim = math.radians(self.angulo_max_graus)
return (
TipoMovimentoDirecional.MovimentoArco,
float(np.clip(delta, -lim, lim)),
)
# 2) Só usa diagonal quando o corpo já está praticamente paralelo ao
# caminho. Ela corrige deslocamento lateral sem criar yaw desnecessário.
precisa_recentralizar = abs(e_lat) > self._lateral_deadband_m precisa_recentralizar = abs(e_lat) > self._lateral_deadband_m
manter_diag = ( manter_diag = (
tipo_prev == TipoMovimentoDirecional.MovimentoDiagonal tipo_prev == TipoMovimentoDirecional.MovimentoDiagonal
@ -870,21 +1008,15 @@ class ControladorMPC:
lim = math.radians(min(self._angulo_diagonal_max_graus, self.angulo_max_graus)) lim = math.radians(min(self._angulo_diagonal_max_graus, self.angulo_max_graus))
return TipoMovimentoDirecional.MovimentoDiagonal, float(np.clip(delta, -lim, lim)) return TipoMovimentoDirecional.MovimentoDiagonal, float(np.clip(delta, -lim, lim))
# Fora da janela diagonal, prioridade absoluta é voltar a ficar # 3) Faixa intermediária: RodasDianteiras têm UMA responsabilidade,
# paralelo. A parcela lateral perde força conforme o erro de heading # corrigir heading. O erro lateral fica reservado para a diagonal quando
# cresce, para não transformar a correção em perseguição oscilante. # o corpo voltar à janela estreita de alinhamento.
if abs(math.degrees(erro_heading_rad)) <= self._heading_deadband_graus: if e_head_deg <= self._heading_deadband_graus:
e_head_eff = 0.0 e_head_eff = 0.0
else: else:
e_head_eff = float(erro_heading_rad) e_head_eff = erro_heading_rad
fator_lat = 1.0 - min(1.0, e_head_deg / max(3.0, self._diagonal_saida_heading_graus * 3.0)) delta = self._ganho_heading_reta * e_head_eff
lat_eff = 0.0 if abs(e_lat) <= self._lateral_deadband_m else e_lat
delta_lat = math.atan2(
self._ganho_lateral_dianteira * lat_eff,
v + self._velocidade_offset_lateral,
) * fator_lat
delta = self._ganho_heading_reta * e_head_eff + delta_lat
lim = math.radians(self.angulo_max_graus) lim = math.radians(self.angulo_max_graus)
return TipoMovimentoDirecional.RodasDianteiras, float(np.clip(delta, -lim, lim)) return TipoMovimentoDirecional.RodasDianteiras, float(np.clip(delta, -lim, lim))
@ -917,21 +1049,51 @@ class ControladorMPC:
)) ))
dt = _safe_float(dt_controle, 0.5, min_value=0.05, max_value=1.5) dt = _safe_float(dt_controle, 0.5, min_value=0.05, max_value=1.5)
tau = float(self._filtro_angulo_reta_tau_s)
alpha = 1.0 if tau <= 1e-6 else dt / (tau + dt)
filtrado = anterior + alpha * (desejado - anterior)
max_delta = np.radians(self._taxa_angulo_reta_graus_s) * dt
filtrado = anterior + float(np.clip(
filtrado - anterior,
-max_delta,
max_delta,
))
dead = np.radians(self._deadband_angulo_reta_graus) dead = np.radians(self._deadband_angulo_reta_graus)
# Guarda de fase:
#
# Um filtro/rate-limit não pode continuar esterçando para a direita
# quando o MPC já pediu correção significativa para a esquerda, ou
# vice-versa. Nos logs reais isso chegou a persistir por vários ciclos
# e o rover atravessou a centerline antes de o comando cruzar zero.
#
# Na inversão significativa fazemos uma passagem deliberada por zero.
# É mais suave mecanicamente do que saltar direto de +A para -B e,
# principalmente, jamais aplica correção no sentido sabidamente errado.
inversao_significativa = bool(
abs(desejado) > dead
and abs(anterior) > dead
and (desejado * anterior) < 0.0
)
if inversao_significativa:
filtrado = 0.0
else:
tau = float(self._filtro_angulo_reta_tau_s)
alpha = 1.0 if tau <= 1e-6 else dt / (tau + dt)
filtrado = anterior + alpha * (desejado - anterior)
max_delta = np.radians(self._taxa_angulo_reta_graus_s) * dt
filtrado = anterior + float(np.clip(
filtrado - anterior,
-max_delta,
max_delta,
))
if abs(desejado) <= dead and abs(anterior) <= 2.0 * dead: if abs(desejado) <= dead and abs(anterior) <= 2.0 * dead:
filtrado = 0.0 filtrado = 0.0
# Última barreira: mesmo diante de qualquer alteração futura no filtro,
# uma saída não nula nunca pode permanecer no hemisfério oposto ao
# comando desejado.
if (
abs(desejado) > dead
and abs(filtrado) > dead
and (desejado * filtrado) < 0.0
):
filtrado = 0.0
return float(np.clip( return float(np.clip(
filtrado, filtrado,
-np.radians(self.angulo_max_graus), -np.radians(self.angulo_max_graus),
@ -1476,7 +1638,7 @@ class ControladorMPC:
(theta_trecho - theta_robo + math.pi) % (2.0 * math.pi) (theta_trecho - theta_robo + math.pi) % (2.0 * math.pi)
- math.pi - math.pi
) )
erro_maximo = math.radians(35.0 if estrutural else 45.0) erro_maximo = math.radians(40.0 if estrutural else 50.0) # 35.0 e 45.0
ponto_ficou_para_tras = ( ponto_ficou_para_tras = (
math.isfinite(avanco) math.isfinite(avanco)
@ -1495,7 +1657,14 @@ class ControladorMPC:
return self._proximo_nao_visitado(pontos_visitados) return self._proximo_nao_visitado(pontos_visitados)
def _calcular_pesos_movimento(self, contexto, erro_ori_graus, erro_lat_m, tracking_reta=False): def _calcular_pesos_movimento(
self,
contexto,
erro_ori_graus,
erro_lat_m,
tracking_reta=False,
erro_heading_caminho_graus=None,
):
# helpers simples # helpers simples
def _clamp(x, lo, hi): def _clamp(x, lo, hi):
return lo if x < lo else hi if x > hi else x return lo if x < lo else hi if x > hi else x
@ -1564,20 +1733,62 @@ class ControladorMPC:
erro_lat_abs = abs(float(erro_lat_m)) erro_lat_abs = abs(float(erro_lat_m))
if tracking_reta: if tracking_reta:
# Na passada Arco é indesejado. Diagonal é o modo de # V5: os pesos acompanham a mesma política geométrica usada na
# recentralização quando heading está alinhado; dianteira # geração de candidatos. Embora normalmente apenas um tipo seja
# recupera heading quando ele sai da janela estreita. # gerado por ciclo, manter o custo coerente evita preferências
custo_movimento[TipoMovimentoDirecional.MovimentoArco] += 3.0 # contraditórias nas simulações do horizonte.
if erro_ori_abs <= self._diagonal_saida_heading_graus: if erro_heading_caminho_graus is None:
erro_heading_modo = erro_ori_abs
else:
erro_heading_modo = abs(float(erro_heading_caminho_graus))
if erro_heading_modo >= self._tracking_arco_entrada_graus:
custo_movimento[TipoMovimentoDirecional.MovimentoArco] += 0.0
custo_movimento[TipoMovimentoDirecional.RodasDianteiras] += 1.5
custo_movimento[TipoMovimentoDirecional.MovimentoDiagonal] += 3.0
elif erro_heading_modo <= self._diagonal_saida_heading_graus:
custo_movimento[TipoMovimentoDirecional.MovimentoArco] += 3.0
custo_movimento[TipoMovimentoDirecional.MovimentoDiagonal] += 0.0 custo_movimento[TipoMovimentoDirecional.MovimentoDiagonal] += 0.0
custo_movimento[TipoMovimentoDirecional.RodasDianteiras] += 0.25 custo_movimento[TipoMovimentoDirecional.RodasDianteiras] += 0.25
else: else:
custo_movimento[TipoMovimentoDirecional.MovimentoDiagonal] += 3.0 custo_movimento[TipoMovimentoDirecional.MovimentoArco] += 1.5
custo_movimento[TipoMovimentoDirecional.RodasDianteiras] += 0.0 custo_movimento[TipoMovimentoDirecional.RodasDianteiras] += 0.0
elif status in [ StatusCarroMapa.EntrandoRua, StatusCarroMapa.SaindoRua, StatusCarroMapa.Manobrando ]: custo_movimento[TipoMovimentoDirecional.MovimentoDiagonal] += 3.0
elif status == StatusCarroMapa.Manobrando:
# Cabeceira/ligação estrutural real: preserva a autoridade do
# Arco. Na geração de candidatos V4 ele continua sendo o único
# tipo permitido neste estado quando não estamos em recuperação
# Rua -> Rua da V3.
custo_movimento[TipoMovimentoDirecional.MovimentoArco] += 0.0 custo_movimento[TipoMovimentoDirecional.MovimentoArco] += 0.0
custo_movimento[TipoMovimentoDirecional.RodasDianteiras] += 1.0 custo_movimento[TipoMovimentoDirecional.RodasDianteiras] += 1.5
custo_movimento[TipoMovimentoDirecional.MovimentoDiagonal] += 2.0 custo_movimento[TipoMovimentoDirecional.MovimentoDiagonal] += 3.0
elif status in [StatusCarroMapa.EntrandoRua, StatusCarroMapa.SaindoRua]:
# V4: usa o heading do CAMINHO para decidir qual geometria
# merece preferência. O bearing/erro combinado continua nos
# demais custos da manobra, mas não decide sozinho o modo.
if erro_heading_caminho_graus is None:
erro_heading_modo = erro_ori_abs
else:
erro_heading_modo = abs(float(erro_heading_caminho_graus))
e_lo = float(self._transicao_rua_dianteira_heading_graus)
e_hi = float(self._transicao_rua_arco_heading_graus)
t = (erro_heading_modo - e_lo) / max(e_hi - e_lo, 1e-6)
t = _smoothstep01(_clamp(t, 0.0, 1.0))
penal = float(self._transicao_rua_penalidade_modo)
# t=0: alinhado -> dianteira preferida
# t=1: desalinhado -> arco preferido
custo_movimento[TipoMovimentoDirecional.RodasDianteiras] += penal * t
custo_movimento[TipoMovimentoDirecional.MovimentoArco] += penal * (1.0 - t)
# Diagonal não pertence a esta política de transição.
custo_movimento[TipoMovimentoDirecional.MovimentoDiagonal] += 3.0
else: else:
custo_movimento[TipoMovimentoDirecional.MovimentoArco] += 1.5 custo_movimento[TipoMovimentoDirecional.MovimentoArco] += 1.5
custo_movimento[TipoMovimentoDirecional.RodasDianteiras] += 0.0 custo_movimento[TipoMovimentoDirecional.RodasDianteiras] += 0.0
@ -2289,8 +2500,9 @@ class ControladorMPC:
idx_base = _safe_int(ponto_alvo.get("_idx_base", 0), 0) idx_base = _safe_int(ponto_alvo.get("_idx_base", 0), 0)
# -------------------------------------------------------------- # --------------------------------------------------------------
# RETA: heading do CAMINHO + cross-track, sem mirar ponto. # PATH TRACKING: heading do CAMINHO + cross-track, sem mirar ponto.
# MANOBRA: mantém pure-pursuit para contornar cabeceira/ligação. # A geometria é escolhida por erro de heading/cross-track (V5).
# MANOBRA estrutural: mantém pure-pursuit e Arco obrigatório.
# -------------------------------------------------------------- # --------------------------------------------------------------
ref_path = self._referencia_caminho_lookahead( ref_path = self._referencia_caminho_lookahead(
ponto_atual[0], ponto_atual[1], idx_base, velocidade ponto_atual[0], ponto_atual[1], idx_base, velocidade
@ -2324,17 +2536,33 @@ class ControladorMPC:
tipo_preferido_enum = _movimento_from_value(tipo_preferido) tipo_preferido_enum = _movimento_from_value(tipo_preferido)
erro_heading_alvo_deg = abs(float(math.degrees(delta_theta))) erro_heading_alvo_deg = abs(float(math.degrees(delta_theta)))
if _status_in( if _status_in(status, [StatusCarroMapa.Manobrando]):
status, # Manobra estrutural real continua com Arco obrigatório.
[ # A recuperação Rua -> Rua da V3 entra antes em
StatusCarroMapa.EntrandoRua, # tracking_reta=True, portanto não cai neste ramo.
StatusCarroMapa.SaindoRua,
StatusCarroMapa.Manobrando,
],
):
# Manobras estruturais continuam com Arco obrigatório.
tipos_validos = [TipoMovimentoDirecional.MovimentoArco] tipos_validos = [TipoMovimentoDirecional.MovimentoArco]
elif _status_in(
status,
[StatusCarroMapa.EntrandoRua, StatusCarroMapa.SaindoRua],
):
# V4: entrada/saída são estados de TRANSIÇÃO. Arco e
# RodasDianteiras são avaliados pelo mesmo MPC.
#
# Colocar o tipo anterior primeiro só estabiliza a divisão
# do orçamento quando sobra um candidato ímpar. A escolha
# final continua sendo pelo custo acumulado.
if tipo_preferido_enum == TipoMovimentoDirecional.RodasDianteiras:
tipos_validos = [
TipoMovimentoDirecional.RodasDianteiras,
TipoMovimentoDirecional.MovimentoArco,
]
else:
tipos_validos = [
TipoMovimentoDirecional.MovimentoArco,
TipoMovimentoDirecional.RodasDianteiras,
]
elif _status_in(status, [StatusCarroMapa.Direcionando]): elif _status_in(status, [StatusCarroMapa.Direcionando]):
# Fora da rua, um heading muito errado deve ser corrigido # Fora da rua, um heading muito errado deve ser corrigido
# com Arco, que possui raio de giro menor. Quando o rover # com Arco, que possui raio de giro menor. Quando o rover
@ -2354,6 +2582,22 @@ class ControladorMPC:
else: else:
tipos_validos = [TipoMovimentoDirecional.RodasDianteiras] tipos_validos = [TipoMovimentoDirecional.RodasDianteiras]
elif _status_in(status, [StatusCarroMapa.RetornandoBase]):
# Guarda defensiva. Em V5 RetornandoBase deve entrar no
# path tracking acima; se algum contexto incompleto impedir
# isso, ainda assim nunca volta ao antigo fallback Front-only.
manter_arco = (
tipo_preferido_enum == TipoMovimentoDirecional.MovimentoArco
and erro_heading_alvo_deg > self._tracking_arco_saida_graus
)
if (
erro_heading_alvo_deg >= self._tracking_arco_entrada_graus
or manter_arco
):
tipos_validos = [TipoMovimentoDirecional.MovimentoArco]
else:
tipos_validos = [TipoMovimentoDirecional.RodasDianteiras]
else: else:
tipos_validos = [TipoMovimentoDirecional.RodasDianteiras] tipos_validos = [TipoMovimentoDirecional.RodasDianteiras]
@ -3292,7 +3536,13 @@ class ControladorMPC:
) )
pesos = self._calcular_pesos_movimento( pesos = self._calcular_pesos_movimento(
contexto, erro_ori_sim, e_lat, tracking_reta=tracking_reta_sim contexto,
erro_ori_sim,
e_lat,
tracking_reta=tracking_reta_sim,
erro_heading_caminho_graus=abs(
float(np.degrees(erro_heading_sim))
),
) )
(peso_erro_pos, peso_erro_ori, peso_suavidade, (peso_erro_pos, peso_erro_ori, peso_suavidade,
peso_fator_re, peso_ideal, peso_lateral, peso_fator_re, peso_ideal, peso_lateral,

View File

@ -134,7 +134,7 @@ VISUAL_DEFAULT_CONFIG = {
"enabled": True, "enabled": True,
# Imprime resumo periódico no console. # Imprime resumo periódico no console.
"console_performance": True, "console_performance": False,
# Abre o timing da inferência em crop/resize/cor/session/decode. # Abre o timing da inferência em crop/resize/cor/session/decode.
"runner_detailed_timing": True, "runner_detailed_timing": True,

View File

@ -28,7 +28,7 @@ from visual_worker.utils import converter_valores_numpy
from shared.perf_monitor import VisualPerfMonitor from shared.perf_monitor import VisualPerfMonitor
CAMERA_MANAGER_VERSION = "production_v1_2026_09_12_runtime_priority_v3" CAMERA_MANAGER_VERSION = "production_v1_2026_09_15_debug_geometry_sync_v4"
PRODUCT_ASSEMBLY_SCHEMA = "multispec_module_params_assembly_v1" PRODUCT_ASSEMBLY_SCHEMA = "multispec_module_params_assembly_v1"
PRODUCT_MODULE_SCHEMA = "multispec_module_params_v3" PRODUCT_MODULE_SCHEMA = "multispec_module_params_v3"
@ -2044,6 +2044,135 @@ class CameraManager:
return True return True
@staticmethod
def _clamp01_debug(value, default=0.0):
try:
return max(0.0, min(1.0, float(value)))
except Exception:
return max(0.0, min(1.0, float(default)))
def _resolver_geometria_debug_bicos(self, config, analise_debug=None):
"""
Resolve a geometria que o Debug deve desenhar.
Autoridade, nesta ordem:
1) estatísticas produzidas pelo WeedDetector na última decisão real;
2) o próprio helper do WeedDetector, quando disponível;
3) fallback local com o MESMO contrato: 0=topo, 1=pé.
Assim o preview não mantém uma segunda implementação da lógica de atuação.
"""
cfg = config or {}
analise_debug = analise_debug or {}
dados_visuais = analise_debug.get("dados_visuais", {}) or {}
estatisticas = dados_visuais.get("estatisticas", {}) or {}
estat_bicos = estatisticas.get("bicos", {}) or {}
estat_atuacao = estat_bicos.get("atuacao", {}) or {}
ordem_invertida = bool(
estat_bicos.get(
"ordem_bicos_invertida",
cfg.get("inverter_ordem_bicos", False),
)
)
sentido_vertical_invertido = bool(
estat_bicos.get(
"sentido_vertical_invertido",
estat_atuacao.get(
"sentido_vertical_invertido",
cfg.get("inverter_sentido_vertical_bicos", False),
),
)
)
zona_ini = estat_atuacao.get("zona_atuacao_inicio_frac")
zona_fim = estat_atuacao.get("zona_atuacao_fim_frac")
eval_ini = estat_atuacao.get("zona_eval_inicio_frac")
eval_fim = estat_atuacao.get("zona_eval_fim_frac")
# Se ainda não existe uma decisão real (startup, reset etc.), pede ao
# detector a mesma função usada pelo controle. Evita duplicar semântica.
if zona_ini is None or zona_fim is None:
detector = self.weed_detector
helper = getattr(detector, "_calcular_zona_atuacao_frac_from_config", None)
if callable(helper):
try:
zona_ini, zona_fim = helper(cfg)
except Exception:
zona_ini = None
zona_fim = None
# Último fallback, mantido fail-soft para o Debug. Esta é a convenção
# oficial atual do WeedDetector: faixa é medida de cima para baixo.
if zona_ini is None or zona_fim is None:
faixa = self._clamp01_debug(cfg.get("faixa_atuacao_bicos", 0.70), 0.70)
area = self._clamp01_debug(cfg.get("area_atuacao_bicos", 0.10), 0.10)
area = max(0.001, area)
zona_ini = faixa
zona_fim = min(1.0, faixa + area)
if sentido_vertical_invertido:
zona_ini, zona_fim = 1.0 - zona_fim, 1.0 - zona_ini
zona_ini = self._clamp01_debug(zona_ini, 0.0)
zona_fim = self._clamp01_debug(zona_fim, zona_ini)
if zona_fim < zona_ini:
zona_ini, zona_fim = zona_fim, zona_ini
# Sem compensação de latência disponível, a zona avaliada coincide com
# a zona física. Quando o detector informar outra zona, mostramos ambas.
if eval_ini is None or eval_fim is None:
eval_ini, eval_fim = zona_ini, zona_fim
eval_ini = self._clamp01_debug(eval_ini, zona_ini)
eval_fim = self._clamp01_debug(eval_fim, zona_fim)
if eval_fim < eval_ini:
eval_ini, eval_fim = eval_fim, eval_ini
pulverizacao = dados_visuais.get("pulverizacao", {}) or {}
return {
"zona_inicio_frac": float(zona_ini),
"zona_fim_frac": float(zona_fim),
"zona_eval_inicio_frac": float(eval_ini),
"zona_eval_fim_frac": float(eval_fim),
"ordem_bicos_invertida": bool(ordem_invertida),
"sentido_vertical_invertido": bool(sentido_vertical_invertido),
"pulverizacao": pulverizacao,
"fonte_detector": bool(estat_atuacao),
}
@staticmethod
def _frac_vertical_para_pixels(inicio_frac, fim_frac, altura):
if altura <= 1:
return 0, 0
inicio = max(0.0, min(1.0, float(inicio_frac)))
fim = max(0.0, min(1.0, float(fim_frac)))
if fim < inicio:
inicio, fim = fim, inicio
y0 = int(round(inicio * (altura - 1)))
y1 = int(round(fim * (altura - 1)))
y0 = max(0, min(altura - 1, y0))
y1 = max(0, min(altura - 1, y1))
return min(y0, y1), max(y0, y1)
@staticmethod
def _intervalo_visual_bico(indice_bico, qtd_bicos, largura, ordem_invertida):
"""Mapeia índice FÍSICO do bico para a coluna correspondente da imagem."""
qtd = max(1, int(qtd_bicos))
idx = int(indice_bico)
idx_visual = (qtd - 1 - idx) if ordem_invertida else idx
x0 = int(round(idx_visual * largura / float(qtd)))
x1 = int(round((idx_visual + 1) * largura / float(qtd))) - 1
x0 = max(0, min(largura - 1, x0))
x1 = max(x0, min(largura - 1, x1))
return x0, x1
def get_debug_frame(self, mostrar=False, overlay_bgr=None): def get_debug_frame(self, mostrar=False, overlay_bgr=None):
overlay = overlay_bgr if overlay_bgr is not None else self._ultimo_preview_overlay overlay = overlay_bgr if overlay_bgr is not None else self._ultimo_preview_overlay
@ -2057,12 +2186,21 @@ class CameraManager:
"fps_loop": self._ultimo_loop_analise_fps, "fps_loop": self._ultimo_loop_analise_fps,
} }
# A detecção publica controle final + análise sob _vida_lock. Tiramos o
# snapshot sob o mesmo lock para o Debug não misturar estados de ciclos
# diferentes enquanto a thread de detecção está atualizando os caches.
with self._vida_lock:
atuacao_bicos = dict(self._ultimo_controle or {})
analise_debug = dict(self._ultima_analise or {})
config_debug = dict(self.seg_config or {})
# Não publica diretamente no cache: o preview producer faz a # Não publica diretamente no cache: o preview producer faz a
# publicação atômica depois de reduzir/comprimir o bundle inteiro. # publicação atômica depois de reduzir/comprimir o bundle inteiro.
return self._montar_debug_overlay( return self._montar_debug_overlay(
overlay_bgr=overlay, overlay_bgr=overlay,
atuacao_bicos=self._ultimo_controle or {}, atuacao_bicos=atuacao_bicos,
config=self.seg_config or {}, config=config_debug,
analise_debug=analise_debug,
metricas_perf=metricas_perf, metricas_perf=metricas_perf,
mostrar=mostrar, mostrar=mostrar,
) )
@ -2072,6 +2210,7 @@ class CameraManager:
overlay_bgr, overlay_bgr,
atuacao_bicos, atuacao_bicos,
config, config,
analise_debug=None,
metricas_perf=None, metricas_perf=None,
mostrar=False, mostrar=False,
): ):
@ -2080,6 +2219,10 @@ class CameraManager:
return None return None
metricas_perf = metricas_perf or {} metricas_perf = metricas_perf or {}
geometria = self._resolver_geometria_debug_bicos(
config=config,
analise_debug=analise_debug,
)
W, H = self._dbg_shape W, H = self._dbg_shape
@ -2104,25 +2247,68 @@ class CameraManager:
self._dbg_img[...] = overlay_bgr self._dbg_img[...] = overlay_bgr
qtd_bicos = int(config.get("qtd_bicos", self.qtd_bicos or 1) or 1) qtd_bicos = int(config.get("qtd_bicos", self.qtd_bicos or 1) or 1)
zona_inicio = float(config.get("faixa_atuacao_bicos", 0.7)) qtd_bicos = max(1, qtd_bicos)
faixa_atuacao = float(config.get("area_atuacao_bicos", 0.1))
y_inicio = int((1.0 - zona_inicio) * H) y0, y1 = self._frac_vertical_para_pixels(
y_fim = int((1.0 - (zona_inicio + faixa_atuacao)) * H) geometria["zona_inicio_frac"],
y0, y1 = min(y_inicio, y_fim), max(y_inicio, y_fim) geometria["zona_fim_frac"],
H,
)
ey0, ey1 = self._frac_vertical_para_pixels(
geometria["zona_eval_inicio_frac"],
geometria["zona_eval_fim_frac"],
H,
)
# ------------------------------------------------------------
# Zona FÍSICA dos bicos. É a faixa configurada pelo operador.
# ------------------------------------------------------------
self._dbg_layer.fill(0) self._dbg_layer.fill(0)
cv2.rectangle( cv2.rectangle(
self._dbg_layer, self._dbg_layer,
(0, y0), (0, y0),
(W, y1), (W - 1, y1),
(220, 220, 100), (220, 220, 100),
thickness=-1, thickness=-1,
) )
cv2.addWeighted(self._dbg_layer, 0.18, self._dbg_img, 0.82, 0, dst=self._dbg_img) cv2.addWeighted(
cv2.rectangle(self._dbg_img, (0, y0), (W, y1), (180, 180, 80), 2) self._dbg_layer,
0.18,
self._dbg_img,
0.82,
0,
dst=self._dbg_img,
)
cv2.rectangle(
self._dbg_img,
(0, y0),
(W - 1, y1),
(180, 180, 80),
2,
)
# Zona REALMENTE avaliada pelo detector quando há antecipação por
# latência/velocidade. Se coincidir com a zona física, não duplica.
eval_diferente = abs(ey0 - y0) > 1 or abs(ey1 - y1) > 1
if eval_diferente:
cv2.rectangle(
self._dbg_img,
(0, ey0),
(W - 1, ey1),
(255, 255, 0),
2,
)
cv2.putText(
self._dbg_img,
"EVAL",
(max(5, W - 60), max(16, min(H - 5, ey0 + 16))),
cv2.FONT_HERSHEY_SIMPLEX,
0.45,
(255, 255, 0),
1,
cv2.LINE_AA,
)
largura_bico = W / float(max(qtd_bicos, 1))
cores = [ cores = [
(0, 255, 0), (0, 255, 0),
(255, 0, 0), (255, 0, 0),
@ -2134,33 +2320,64 @@ class CameraManager:
(0, 0, 255), (0, 0, 255),
] ]
ordem_invertida = bool(geometria["ordem_bicos_invertida"])
# ------------------------------------------------------------
# Estado ON/OFF FINAL, já depois do gate de pulverização.
# Cada índice é o bico FÍSICO. A posição visual respeita
# inverter_ordem_bicos exatamente como o WeedDetector.
# ------------------------------------------------------------
self._dbg_layer.fill(0) self._dbg_layer.fill(0)
for i in range(qtd_bicos): for i in range(qtd_bicos):
x0 = int(i * largura_bico) x0, x1 = self._intervalo_visual_bico(
x1 = int((i + 1) * largura_bico) i,
qtd_bicos,
W,
ordem_invertida,
)
cor = cores[i % len(cores)] cor = cores[i % len(cores)]
if atuacao_bicos.get(i, False): if bool(atuacao_bicos.get(i, False)):
cv2.rectangle(self._dbg_layer, (x0, y0), (x1, y1), cor, thickness=-1) cv2.rectangle(
self._dbg_layer,
(x0, y0),
(x1, y1),
cor,
thickness=-1,
)
cv2.addWeighted(self._dbg_layer, 0.15, self._dbg_img, 0.85, 0, dst=self._dbg_img) cv2.addWeighted(
self._dbg_layer,
0.15,
self._dbg_img,
0.85,
0,
dst=self._dbg_img,
)
for i in range(qtd_bicos): for i in range(qtd_bicos):
x0 = int(i * largura_bico) x0, x1 = self._intervalo_visual_bico(
x1 = int((i + 1) * largura_bico) i,
qtd_bicos,
W,
ordem_invertida,
)
cor = cores[i % len(cores)] cor = cores[i % len(cores)]
status = "ON" if atuacao_bicos.get(i, False) else "OFF" status = "ON" if bool(atuacao_bicos.get(i, False)) else "OFF"
cv2.rectangle(self._dbg_img, (x0, y0), (x1, y1), cor, 1) cv2.rectangle(self._dbg_img, (x0, y0), (x1, y1), cor, 1)
texto = f"Bico {i} {status}"
text_y = max(18, min(H - 6, y0 + 20))
cv2.putText( cv2.putText(
self._dbg_img, self._dbg_img,
f"Bico {i} {status}", texto,
(x0 + 5, min(H - 5, y1 + 20)), (x0 + 4, text_y),
cv2.FONT_HERSHEY_SIMPLEX, cv2.FONT_HERSHEY_SIMPLEX,
0.55, 0.48,
cor, cor,
2, 1,
cv2.LINE_AA,
) )
fps_infer = float(metricas_perf.get("fps_infer") or 0.0) fps_infer = float(metricas_perf.get("fps_infer") or 0.0)
@ -2188,6 +2405,51 @@ class CameraManager:
2, 2,
) )
zona_txt = (
f"Zona={geometria['zona_inicio_frac'] * 100:.0f}-"
f"{geometria['zona_fim_frac'] * 100:.0f}%"
)
if eval_diferente:
zona_txt += (
f" | Eval={geometria['zona_eval_inicio_frac'] * 100:.0f}-"
f"{geometria['zona_eval_fim_frac'] * 100:.0f}%"
)
zona_txt += (
f" | V={'INV' if geometria['sentido_vertical_invertido'] else 'NORMAL'}"
f" H={'INV' if ordem_invertida else 'NORMAL'}"
)
cv2.putText(
self._dbg_img,
zona_txt,
(10, 88),
cv2.FONT_HERSHEY_SIMPLEX,
0.48,
(230, 230, 230),
1,
cv2.LINE_AA,
)
pulverizacao = geometria.get("pulverizacao", {}) or {}
if pulverizacao:
permitida = bool(pulverizacao.get("permitida", False))
motivo = str(pulverizacao.get("motivo", "") or "")
gate_txt = f"Pulv: {'ON' if permitida else 'OFF'}"
if motivo and motivo != "ok":
gate_txt += f" ({motivo[:48]})"
cv2.putText(
self._dbg_img,
gate_txt,
(10, 108),
cv2.FONT_HERSHEY_SIMPLEX,
0.45,
(200, 255, 200) if permitida else (180, 180, 255),
1,
cv2.LINE_AA,
)
if mostrar: if mostrar:
cv2.imshow("Debug Weed Worker", self._dbg_img) cv2.imshow("Debug Weed Worker", self._dbg_img)
cv2.waitKey(1) cv2.waitKey(1)

View File

@ -170,8 +170,8 @@ WEED_DEFAULT_CONFIG = {
# 1) Observabilidade / salvamento # 1) Observabilidade / salvamento
# ======================================================== # ========================================================
"debug_visual": False, "debug_visual": False,
"debug_perf": True, "debug_perf": False,
"detector_debug_perf": True, "detector_debug_perf": False,
# RAW científico para pós-processamento. # RAW científico para pós-processamento.
"posproc_intervalo_min_s": 5.0, "posproc_intervalo_min_s": 5.0,
@ -256,7 +256,7 @@ WEED_DEFAULT_CONFIG = {
# True no rover atual: # True no rover atual:
# a aproximação física do alvo ocorre do pé para o topo da imagem. # a aproximação física do alvo ocorre do pé para o topo da imagem.
"inverter_sentido_vertical_bicos": True, "inverter_sentido_vertical_bicos": False,
# Zona física de atuação no frame. # Zona física de atuação no frame.
# Ambos podem ser sobrescritos pela configuração da operação. # Ambos podem ser sobrescritos pela configuração da operação.

View File

@ -25,11 +25,14 @@ class WeedDetector:
- Desloca a memória pela distância real percorrida pelo rover. - Desloca a memória pela distância real percorrida pelo rover.
- Atua quando a evidência chega na zona física de atuação. - Atua quando a evidência chega na zona física de atuação.
Convenção espacial: Convenção espacial oficial:
- cell index 0 = região mais próxima da linha dos bicos. - eixo vertical sempre usa coordenada de imagem: 0.0 = topo, 1.0 = pé.
- cell index maior = região mais distante, vista antes pela câmera. - cell index 0 = topo da imagem.
- Conforme o rover anda, a memória se desloca de índices maiores - cell index N-1 = pé da imagem.
para índices menores. - inverter_sentido_vertical_bicos=False (rover atual):
alvo caminha topo -> pé; faixa=0.85, area=0.15 atua em 85%..100%.
- inverter_sentido_vertical_bicos=True:
alvo caminha pé -> topo; a mesma faixa é espelhada para 0%..15%.
Este arquivo NÃO: Este arquivo NÃO:
- gera imagem; - gera imagem;
@ -41,6 +44,7 @@ class WeedDetector:
CONTRATO_OFICIAL = "target_binary" CONTRATO_OFICIAL = "target_binary"
TARGET_ID = 1 TARGET_ID = 1
VERSION = "weed_detector_v2_2026_09_15_vertical_contract_fix"
def __init__(self, config: Optional[dict] = None): def __init__(self, config: Optional[dict] = None):
# O CameraManager já mantém um snapshot de configuração. Quando ele é # O CameraManager já mantém um snapshot de configuração. Quando ele é
@ -76,8 +80,14 @@ class WeedDetector:
self.config.get("inverter_ordem_bicos", False) self.config.get("inverter_ordem_bicos", False)
) )
# Convenção oficial:
# False = fluxo normal topo -> pé (rover atual).
# True = fluxo invertido pé -> topo.
#
# IMPORTANTE: esta flag altera tanto o sentido da memória espacial
# quanto a posição vertical da zona física de atuação.
self.inverter_sentido_vertical_bicos = bool( self.inverter_sentido_vertical_bicos = bool(
self.config.get("inverter_sentido_vertical_bicos", True) self.config.get("inverter_sentido_vertical_bicos", False)
) )
self.detector_debug_perf = bool( self.detector_debug_perf = bool(
@ -168,11 +178,14 @@ class WeedDetector:
0.0 = topo da imagem 0.0 = topo da imagem
1.0 = pé da imagem 1.0 = pé da imagem
Normal: Normal (inverter_sentido_vertical_bicos=False):
faixa=0.90, area=0.10 -> 90% até 100%, embaixo. faixa=0.90, area=0.10 -> 90% até 100%, embaixo.
Com inverter_sentido_vertical_bicos=True: Invertido (inverter_sentido_vertical_bicos=True):
faixa=0.90, area=0.10 -> 0% até 10%, em cima. faixa=0.90, area=0.10 -> 0% até 10%, em cima.
Portanto, para o rover atual, em que o alvo se aproxima do topo para
o pé da imagem, a configuração correta é False.
""" """
faixa = float(cfg.get("faixa_atuacao_bicos", 0.70) or 0.70) faixa = float(cfg.get("faixa_atuacao_bicos", 0.70) or 0.70)
@ -918,11 +931,12 @@ class WeedDetector:
k_shift = max(0.0, k_shift) k_shift = max(0.0, k_shift)
# A zona configurada representa a posição física fixa dos bicos. # A zona configurada representa a posição física fixa dos bicos.
# Com faixa=0,85, área=0,10 e inversão vertical, 85%..95% da imagem # Exemplo no rover atual (sentido normal / False):
# original vira 5%..15% na convenção interna. Essa zona NÃO muda de # faixa=0,85, área=0,15 -> 85%..100%, no pé da imagem.
# parado até a velocidade de referência. Somente a parcela de # No sentido invertido / True a mesma faixa é espelhada para 0%..15%.
# velocidade ACIMA da referência antecipa a avaliação para compensar # Essa zona NÃO muda de parado até a velocidade de referência. Somente
# o tempo de resposta do sistema. # a parcela de velocidade ACIMA da referência antecipa a avaliação para
# compensar o tempo de resposta do sistema.
vel_referencia = max( vel_referencia = max(
0.0, 0.0,
float(cfg.get("velocidade_referencia_atuacao_mps", 0.65) or 0.65), float(cfg.get("velocidade_referencia_atuacao_mps", 0.65) or 0.65),
@ -1282,19 +1296,46 @@ class WeedDetector:
vel_norm: float, vel_norm: float,
cfg: dict, cfg: dict,
): ):
# Convenção legada: """
# zona_inicio e zona_altura são frações medidas a partir da parte inferior da imagem. ROI do fallback legado usando a MESMA convenção do caminho oficial.
y_inicio = int((1.0 - zona_inicio) * h)
y_fim = int((1.0 - (zona_inicio + zona_altura)) * h)
y_top = min(y_inicio, y_fim) Coordenada vertical:
y_bot = max(y_inicio, y_fim) 0.0 = topo
1.0 = pé
False: alvo topo -> pé; 0.85 + 0.15 => faixa inferior.
True : alvo pé -> topo; a mesma faixa é espelhada para o topo.
Os argumentos zona_inicio/zona_altura são mantidos na assinatura por
compatibilidade, mas o cálculo usa os valores de cfg para garantir que
exista uma única fonte de verdade.
"""
_ = zona_inicio, zona_altura
zona_ini_frac, zona_fim_frac = self._calcular_zona_atuacao_frac_from_config(cfg)
y_top = int(np.floor(zona_ini_frac * h))
y_bot = int(np.ceil(zona_fim_frac * h))
y_top = max(0, min(h, y_top))
y_bot = max(y_top, min(h, y_bot))
# Antecipação legada em pixels. A avaliação deve se deslocar para a
# região que o alvo ocupa ANTES de chegar aos bicos:
# fluxo topo -> pé : antecipa para cima (subtrai y)
# fluxo pé -> topo : antecipa para baixo (soma y)
roi_shift_per_v = float(cfg.get("k_roi_shift_px_per_vnorm", 24.0)) roi_shift_per_v = float(cfg.get("k_roi_shift_px_per_vnorm", 24.0))
shift = int(roi_shift_per_v * self._clamp01(vel_norm)) shift = int(max(0.0, roi_shift_per_v) * self._clamp01(vel_norm))
y_top = max(0, y_top - shift) if getattr(self, "inverter_sentido_vertical_bicos", False):
y_bot = max(y_top, min(h, y_bot - shift)) y_top = min(h, y_top + shift)
y_bot = min(h, y_bot + shift)
else:
y_top = max(0, y_top - shift)
y_bot = max(0, y_bot - shift)
if y_bot < y_top:
y_top, y_bot = y_bot, y_top
return int(y_top), int(y_bot), int(shift) return int(y_top), int(y_bot), int(shift)