From 933135d00a5635e7218c6a42e52a0c570f29ae0f Mon Sep 17 00:00:00 2001 From: Diego Freitas Date: Fri, 18 Sep 2026 09:02:10 -0300 Subject: [PATCH] ajustado mapsync e parada ao detectar fim dos pontos no MPC --- .gitignore | 1 + .../.vs/AgroBase.csproj.dtbcache.json | 1 - .../Models/Operacoes/OperacaoModel.cs | 32 +- .../Operadores/HealthWorkerService.cs | 339 ++++++++++++++---- .../Scripts/workers/manager_worker/main.py | 15 + .../manager_worker/modulos/direcional.py | 27 +- .../workers/manager_worker/modulos/mpc.py | 125 ++++++- .../processadores/_4_em_andamento.py | 115 +++++- .../workers/shared/contexto_global_redis.py | 226 +++++++++++- 9 files changed, 786 insertions(+), 95 deletions(-) delete mode 100644 AgroBase/AgroBase/.vs/AgroBase.csproj.dtbcache.json diff --git a/.gitignore b/.gitignore index eccb91e00..d73dd5b94 100644 --- a/.gitignore +++ b/.gitignore @@ -97,3 +97,4 @@ 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 +/AgroBase/AgroBase/.vs/AgroBase.csproj.dtbcache.json diff --git a/AgroBase/AgroBase/.vs/AgroBase.csproj.dtbcache.json b/AgroBase/AgroBase/.vs/AgroBase.csproj.dtbcache.json deleted file mode 100644 index b0dbc8f49..000000000 --- a/AgroBase/AgroBase/.vs/AgroBase.csproj.dtbcache.json +++ /dev/null @@ -1 +0,0 @@ -{"RootPath":"C:\\ZendionInc\\agrobot_base\\AgroBase\\AgroBase","ProjectFileName":"AgroBase.csproj","Configuration":"Debug|x64","FrameworkPath":"","Sources":[{"SourceFile":"Forms\\Atuador\\frmAtuCalibragem.cs"},{"SourceFile":"Forms\\Atuador\\frmAtuCalibragem.Designer.cs"},{"SourceFile":"Forms\\Atuador\\frmAtuConfig.cs"},{"SourceFile":"Forms\\Atuador\\frmAtuConfig.Designer.cs"},{"SourceFile":"Forms\\Direcional\\frmDirConfig.cs"},{"SourceFile":"Forms\\Direcional\\frmDirConfig.Designer.cs"},{"SourceFile":"Forms\\Direcional\\frmDirPlot.cs"},{"SourceFile":"Forms\\Direcional\\frmDirPlot.Designer.cs"},{"SourceFile":"Forms\\frmA05Config.cs"},{"SourceFile":"Forms\\frmA05Config.Designer.cs"},{"SourceFile":"Forms\\frmAcompanhamento.cs"},{"SourceFile":"Forms\\frmAcompanhamento.Designer.cs"},{"SourceFile":"Forms\\Atuador\\frmAtuCamera.cs"},{"SourceFile":"Forms\\Atuador\\frmAtuCamera.Designer.cs"},{"SourceFile":"Forms\\frmBLD.cs"},{"SourceFile":"Forms\\frmBLD.Designer.cs"},{"SourceFile":"Forms\\frmEthernet.cs"},{"SourceFile":"Forms\\frmEthernet.Designer.cs"},{"SourceFile":"Forms\\frmGPS.cs"},{"SourceFile":"Forms\\frmGPS.Designer.cs"},{"SourceFile":"Forms\\frmKinect.cs"},{"SourceFile":"Forms\\frmKinect.Designer.cs"},{"SourceFile":"Forms\\frmLoraConfig.cs"},{"SourceFile":"Forms\\frmLoraConfig.Designer.cs"},{"SourceFile":"Forms\\frmMapa.cs"},{"SourceFile":"Forms\\frmMapa.Designer.cs"},{"SourceFile":"Forms\\frmMKS.cs"},{"SourceFile":"Forms\\frmMKS.Designer.cs"},{"SourceFile":"Forms\\frmMvdConfig.cs"},{"SourceFile":"Forms\\frmMvdConfig.Designer.cs"},{"SourceFile":"Forms\\frmOID.cs"},{"SourceFile":"Forms\\frmOID.Designer.cs"},{"SourceFile":"Forms\\frmOperarioSonar.cs"},{"SourceFile":"Forms\\frmOperarioSonar.Designer.cs"},{"SourceFile":"Forms\\frmPinout.cs"},{"SourceFile":"Forms\\frmPinout.Designer.cs"},{"SourceFile":"Forms\\frmSimulacaoMapaGPS.cs"},{"SourceFile":"Forms\\frmSimulacaoMapaGPS.Designer.cs"},{"SourceFile":"Forms\\frmTreinamentoIA.cs"},{"SourceFile":"Forms\\frmTreinamentoIA.Designer.cs"},{"SourceFile":"Forms\\frmWT901C.cs"},{"SourceFile":"Forms\\frmWT901C.Designer.cs"},{"SourceFile":"Forms\\IHM\\frmAjustes.cs"},{"SourceFile":"Forms\\IHM\\frmAjustes.Designer.cs"},{"SourceFile":"Forms\\IHM\\frmDiagnosticos.cs"},{"SourceFile":"Forms\\IHM\\frmDiagnosticos.Designer.cs"},{"SourceFile":"Forms\\IHM\\frmDialogoMapa.cs"},{"SourceFile":"Forms\\IHM\\frmDialogoMapa.Designer.cs"},{"SourceFile":"Forms\\IHM\\frmIHM.cs"},{"SourceFile":"Forms\\IHM\\frmIHM.Designer.cs"},{"SourceFile":"Forms\\Movimentacao\\frmMovCamera.cs"},{"SourceFile":"Forms\\Movimentacao\\frmMovCamera.Designer.cs"},{"SourceFile":"Forms\\Movimentacao\\frmMovDiagnosticos.cs"},{"SourceFile":"Forms\\Movimentacao\\frmMovDiagnosticos.Designer.cs"},{"SourceFile":"Forms\\frmInstancial.cs"},{"SourceFile":"Forms\\frmInstancial.Designer.cs"},{"SourceFile":"Forms\\frmJoystick.cs"},{"SourceFile":"Forms\\frmJoystick.Designer.cs"},{"SourceFile":"Forms\\Movimentacao\\frmMovConfig.cs"},{"SourceFile":"Forms\\Movimentacao\\frmMovConfig.Designer.cs"},{"SourceFile":"Forms\\Movimentacao\\frmMovPlot.cs"},{"SourceFile":"Forms\\Movimentacao\\frmMovPlot.Designer.cs"},{"SourceFile":"Forms\\IHM\\frmInicioOperacao.cs"},{"SourceFile":"Forms\\IHM\\frmInicioOperacao.Designer.cs"},{"SourceFile":"Forms\\Operacoes\\frmOperacaoManual.cs"},{"SourceFile":"Forms\\Operacoes\\frmOperacaoManual.Designer.cs"},{"SourceFile":"Forms\\Operacoes\\frmOperacaoMapaGPS.cs"},{"SourceFile":"Forms\\Operacoes\\frmOperacaoSeguidorLinha.Designer.cs"},{"SourceFile":"Forms\\Operacoes\\frmParametrizacaoOperacao.cs"},{"SourceFile":"Forms\\Operacoes\\frmParametrizacaoOperacao.Designer.cs"},{"SourceFile":"Forms\\Operacoes\\frmResultadosOperacao.cs"},{"SourceFile":"Forms\\Operacoes\\frmResultadosOperacao.Designer.cs"},{"SourceFile":"Forms\\Sensoriamento\\frmSenCamera.cs"},{"SourceFile":"Forms\\Sensoriamento\\frmSenCamera.Designer.cs"},{"SourceFile":"Forms\\Sensoriamento\\frmSenConfig.cs"},{"SourceFile":"Forms\\Sensoriamento\\frmSenConfig.Designer.cs"},{"SourceFile":"Forms\\Sensoriamento\\frmSonar.cs"},{"SourceFile":"Forms\\Sensoriamento\\frmSonar.Designer.cs"},{"SourceFile":"Models\\AlarmeModel.cs"},{"SourceFile":"Models\\Components\\AsyncTaskTimerModel.cs"},{"SourceFile":"Models\\BLD300RModel.cs"},{"SourceFile":"Models\\CameraCaminhoModel.cs"},{"SourceFile":"Models\\CameraModel.cs"},{"SourceFile":"Models\\CameraSoloModel.cs"},{"SourceFile":"Models\\Components\\CustomDialog.cs"},{"SourceFile":"Models\\DispositivosModel.cs"},{"SourceFile":"Models\\GPSModel.cs"},{"SourceFile":"Models\\GraficoInterativoModel.cs"},{"SourceFile":"Models\\KalmanFilter.cs"},{"SourceFile":"Models\\KinectModel.cs"},{"SourceFile":"Models\\LoRaModel.cs"},{"SourceFile":"Models\\MapaDinamicoModel.cs"},{"SourceFile":"Models\\MapasModel.cs"},{"SourceFile":"Models\\MensagemFilaModel.cs"},{"SourceFile":"Models\\MKS057DModel.cs"},{"SourceFile":"Models\\ModbusModel.cs"},{"SourceFile":"Models\\Modules\\AtuadorModel.cs"},{"SourceFile":"Models\\Modules\\DirecionalModel.cs"},{"SourceFile":"Models\\Modules\\DispositivoBaseModel.cs"},{"SourceFile":"Models\\Enums.cs"},{"SourceFile":"Models\\GeneralJoystick.cs"},{"SourceFile":"Models\\Modules\\MovimentacaoModel.cs"},{"SourceFile":"Models\\Modules\\MovimentacaoUnificadoModel.cs"},{"SourceFile":"Models\\Modules\\PinoutModel.cs"},{"SourceFile":"Models\\Modules\\SensoriamentoModel.cs"},{"SourceFile":"Models\\MotorComum.cs"},{"SourceFile":"Models\\MPCController.cs"},{"SourceFile":"Models\\MPCControllerAprimorado.cs"},{"SourceFile":"Models\\MPCControllerSimple.cs"},{"SourceFile":"Models\\OAKCameraModel.cs"},{"SourceFile":"Models\\OIDModel.cs"},{"SourceFile":"Models\\Operacoes\\OperacaoMapaGPSModel.cs"},{"SourceFile":"Models\\Operacoes\\OperacaoModel.cs"},{"SourceFile":"Models\\Operacoes\\OperacaoParametrosModel.cs"},{"SourceFile":"Models\\Operadores\\HealthWorkerModel.cs"},{"SourceFile":"Models\\Operadores\\OperadoresModels.cs"},{"SourceFile":"Models\\Operadores\\VisualWorkerModel.cs"},{"SourceFile":"Models\\PerformanceModel.cs"},{"SourceFile":"Models\\PIDModel.cs"},{"SourceFile":"Models\\PZEMModel.cs"},{"SourceFile":"Models\\SonarCamModel.cs"},{"SourceFile":"Models\\SonarModel.cs"},{"SourceFile":"Models\\SyncDataModel.cs"},{"SourceFile":"Models\\TrajetoriaMapaOperacaoModel.cs"},{"SourceFile":"Models\\TreinamentoIAModel.cs"},{"SourceFile":"Models\\UltrasonicA05Model.cs"},{"SourceFile":"Models\\Variaveis.cs"},{"SourceFile":"Models\\VariaveisMonitoramento.cs"},{"SourceFile":"Models\\VersionamentoModel.cs"},{"SourceFile":"Models\\Visualizador3DModel.cs"},{"SourceFile":"Models\\WT901CModel.cs"},{"SourceFile":"Services\\APIService.cs"},{"SourceFile":"Services\\BLD300RService.cs"},{"SourceFile":"Services\\CameraService.cs"},{"SourceFile":"Services\\CANService.cs"},{"SourceFile":"Services\\CoulombCounterService.cs"},{"SourceFile":"Services\\DispositivosService.cs"},{"SourceFile":"Forms\\frmPrincipal.cs"},{"SourceFile":"Forms\\frmPrincipal.Designer.cs"},{"SourceFile":"Program.cs"},{"SourceFile":"Properties\\AssemblyInfo.cs"},{"SourceFile":"Services\\DispositivosServiceFactory.cs"},{"SourceFile":"Services\\EthernetService.cs"},{"SourceFile":"Services\\FilaService.cs"},{"SourceFile":"Services\\GPSService.cs"},{"SourceFile":"Services\\IDispositivosService.cs"},{"SourceFile":"Services\\IFirmwareSensorBase.cs"},{"SourceFile":"Services\\KinectService.cs"},{"SourceFile":"Services\\LoRaEspService.cs"},{"SourceFile":"Services\\LoRaSerializer.cs"},{"SourceFile":"Services\\LoRaService.cs"},{"SourceFile":"Services\\MapasService.cs"},{"SourceFile":"Services\\MKS057DCanService.cs"},{"SourceFile":"Services\\MKS057DService.cs"},{"SourceFile":"Services\\ModbusService.cs"},{"SourceFile":"Services\\MqttService.cs"},{"SourceFile":"Services\\OAKCameraService.cs"},{"SourceFile":"Services\\OIDCanService.cs"},{"SourceFile":"Services\\OIDService.cs"},{"SourceFile":"Services\\Operadores\\HealthWorkerService.cs"},{"SourceFile":"Services\\Operadores\\ManagerWorkerService.cs"},{"SourceFile":"Services\\Operadores\\VisualWorkerService.cs"},{"SourceFile":"Services\\PythonService.cs"},{"SourceFile":"Services\\PZEMService.cs"},{"SourceFile":"Services\\SerialService.cs"},{"SourceFile":"Services\\SonarService.cs"},{"SourceFile":"Services\\SyncDataService.cs"},{"SourceFile":"Services\\UltrasonicA05Service.cs"},{"SourceFile":"Services\\UsrDR302Service.cs"},{"SourceFile":"Services\\VersionamentoService.cs"},{"SourceFile":"Services\\Visualizador3DService.cs"},{"SourceFile":"Services\\WaveshareCanFDService.cs"},{"SourceFile":"Services\\WifiService.cs"},{"SourceFile":"Services\\WT901CService.cs"},{"SourceFile":"Properties\\Resources.Designer.cs"},{"SourceFile":"Properties\\Settings.Designer.cs"},{"SourceFile":"obj\\x64\\Debug\\.NETFramework,Version=v4.8.AssemblyAttributes.cs"}],"References":[{"Reference":"C:\\ZendionInc\\agrobot_base\\AgroBase\\packages\\AForge.2.2.5\\lib\\AForge.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\ZendionInc\\agrobot_base\\AgroBase\\packages\\AForge.Video.DirectShow.2.2.5\\lib\\AForge.Video.DirectShow.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\ZendionInc\\agrobot_base\\AgroBase\\packages\\AForge.Video.2.2.5\\lib\\AForge.Video.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\ZendionInc\\agrobot_base\\AgroBase\\packages\\AssimpNet.4.1.0\\lib\\net40\\AssimpNet.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\ZendionInc\\agrobot_base\\AgroBase\\packages\\Emgu.CV.Bitmap.4.4.0.4099\\lib\\netstandard2.0\\Emgu.CV.Bitmap.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\ZendionInc\\agrobot_base\\AgroBase\\packages\\Emgu.CV.4.4.0.4099\\lib\\netstandard2.0\\Emgu.CV.Platform.NetStandard.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\ZendionInc\\agrobot_base\\AgroBase\\packages\\MathNet.Numerics.5.0.0\\lib\\net461\\MathNet.Numerics.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\ZendionInc\\agrobot_base\\AgroBase\\packages\\Microsoft.Bcl.AsyncInterfaces.8.0.0\\lib\\net462\\Microsoft.Bcl.AsyncInterfaces.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\Program Files (x86)\\Reference Assemblies\\Microsoft\\Framework\\.NETFramework\\v4.8\\Microsoft.CSharp.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\ZendionInc\\agrobot_base\\AgroBase\\AgroBase\\Microsoft.DirectX.DirectInput.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\ZendionInc\\agrobot_base\\AgroBase\\AgroBase\\Microsoft.DirectX.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\Program Files\\Microsoft SDKs\\Kinect\\v1.8\\Assemblies\\Microsoft.Kinect.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\ZendionInc\\agrobot_base\\AgroBase\\packages\\Microsoft.Web.WebView2.1.0.3240.44\\lib\\net462\\Microsoft.Web.WebView2.Core.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\ZendionInc\\agrobot_base\\AgroBase\\packages\\Microsoft.Web.WebView2.1.0.3240.44\\lib\\net462\\Microsoft.Web.WebView2.WinForms.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\ZendionInc\\agrobot_base\\AgroBase\\packages\\Microsoft.Web.WebView2.1.0.3240.44\\lib\\net462\\Microsoft.Web.WebView2.Wpf.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\ZendionInc\\agrobot_base\\AgroBase\\packages\\MQTTnet.3.1.1\\lib\\net461\\MQTTnet.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\Program Files (x86)\\Reference Assemblies\\Microsoft\\Framework\\.NETFramework\\v4.8\\mscorlib.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\ZendionInc\\agrobot_base\\AgroBase\\packages\\Newtonsoft.Json.13.0.3\\lib\\net45\\Newtonsoft.Json.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\ZendionInc\\agrobot_base\\AgroBase\\packages\\OpenCvSharp4.4.10.0.20240616\\lib\\netstandard2.0\\OpenCvSharp.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\ZendionInc\\agrobot_base\\AgroBase\\packages\\OpenCvSharp4.Extensions.4.10.0.20240616\\lib\\netstandard2.0\\OpenCvSharp.Extensions.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\ZendionInc\\agrobot_base\\AgroBase\\AgroBase\\OpenHardwareMonitorLib.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\ZendionInc\\agrobot_base\\AgroBase\\packages\\OpenTK.1.1.2225.0\\lib\\net20\\OpenTK.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\ZendionInc\\agrobot_base\\AgroBase\\packages\\OpenTK.GLControl.1.1.2225.0\\lib\\net20\\OpenTK.GLControl.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\ZendionInc\\agrobot_base\\AgroBase\\packages\\SharpDX.DirectInput.4.2.0\\lib\\net45\\SharpDX.DirectInput.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\ZendionInc\\agrobot_base\\AgroBase\\packages\\SharpDX.4.2.0\\lib\\net45\\SharpDX.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\ZendionInc\\agrobot_base\\AgroBase\\packages\\System.Buffers.4.5.1\\lib\\net461\\System.Buffers.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\ZendionInc\\agrobot_base\\AgroBase\\packages\\System.CodeDom.8.0.0\\lib\\net462\\System.CodeDom.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\Program Files (x86)\\Reference Assemblies\\Microsoft\\Framework\\.NETFramework\\v4.8\\System.ComponentModel.Composition.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\Program Files (x86)\\Reference Assemblies\\Microsoft\\Framework\\.NETFramework\\v4.8\\System.Core.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\Program Files (x86)\\Reference Assemblies\\Microsoft\\Framework\\.NETFramework\\v4.8\\System.Data.DataSetExtensions.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\Program Files (x86)\\Reference Assemblies\\Microsoft\\Framework\\.NETFramework\\v4.8\\System.Data.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\Program Files (x86)\\Reference Assemblies\\Microsoft\\Framework\\.NETFramework\\v4.8\\System.Deployment.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\Program Files (x86)\\Reference Assemblies\\Microsoft\\Framework\\.NETFramework\\v4.8\\System.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\ZendionInc\\agrobot_base\\AgroBase\\packages\\System.Drawing.Common.8.0.1\\lib\\net462\\System.Drawing.Common.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\Program Files (x86)\\Reference Assemblies\\Microsoft\\Framework\\.NETFramework\\v4.8\\System.Drawing.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\Program Files (x86)\\Reference Assemblies\\Microsoft\\Framework\\.NETFramework\\v4.8\\Facades\\System.Drawing.Primitives.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\Program Files (x86)\\Reference Assemblies\\Microsoft\\Framework\\.NETFramework\\v4.8\\System.IO.Compression.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\Program Files (x86)\\Reference Assemblies\\Microsoft\\Framework\\.NETFramework\\v4.8\\System.Management.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\ZendionInc\\agrobot_base\\AgroBase\\packages\\System.Memory.4.5.5\\lib\\net461\\System.Memory.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\Program Files (x86)\\Reference Assemblies\\Microsoft\\Framework\\.NETFramework\\v4.8\\System.Net.Http.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\Program Files (x86)\\Reference Assemblies\\Microsoft\\Framework\\.NETFramework\\v4.8\\System.Numerics.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\ZendionInc\\agrobot_base\\AgroBase\\packages\\System.Numerics.Vectors.4.5.0\\lib\\net46\\System.Numerics.Vectors.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\ZendionInc\\agrobot_base\\AgroBase\\packages\\System.Runtime.CompilerServices.Unsafe.6.0.0\\lib\\net461\\System.Runtime.CompilerServices.Unsafe.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\Program Files (x86)\\Reference Assemblies\\Microsoft\\Framework\\.NETFramework\\v4.8\\Facades\\System.Runtime.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\Program Files (x86)\\Reference Assemblies\\Microsoft\\Framework\\.NETFramework\\v4.8\\Facades\\System.Runtime.InteropServices.RuntimeInformation.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\ZendionInc\\agrobot_base\\AgroBase\\packages\\System.Text.Encodings.Web.8.0.0\\lib\\net462\\System.Text.Encodings.Web.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\ZendionInc\\agrobot_base\\AgroBase\\packages\\System.Text.Json.8.0.1\\lib\\net462\\System.Text.Json.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\ZendionInc\\agrobot_base\\AgroBase\\packages\\System.Threading.Tasks.Extensions.4.5.4\\lib\\net461\\System.Threading.Tasks.Extensions.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\ZendionInc\\agrobot_base\\AgroBase\\packages\\System.ValueTuple.4.5.0\\lib\\net47\\System.ValueTuple.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\Program Files (x86)\\Reference Assemblies\\Microsoft\\Framework\\.NETFramework\\v4.8\\System.Windows.Forms.DataVisualization.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\Program Files (x86)\\Reference Assemblies\\Microsoft\\Framework\\.NETFramework\\v4.8\\System.Windows.Forms.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\Program Files (x86)\\Reference Assemblies\\Microsoft\\Framework\\.NETFramework\\v4.8\\System.Xml.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\Program Files (x86)\\Reference Assemblies\\Microsoft\\Framework\\.NETFramework\\v4.8\\System.Xml.Linq.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\WINDOWS\\System32\\WinMetadata\\Windows.Gaming.winmd","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""},{"Reference":"C:\\ZendionInc\\agrobot_base\\AgroBase\\packages\\ZedGraph.5.1.5\\lib\\ZedGraph.dll","ResolvedFrom":"","OriginalItemSpec":"","Name":"","EmbedInteropTypes":false,"CopyLocal":false,"IsProjectReference":false,"ProjectPath":""}],"Analyzers":[],"Outputs":[{"OutputItemFullPath":"C:\\ZendionInc\\agrobot_base\\AgroBase\\AgroBase\\bin\\x64\\Debug\\AgroBase.exe","OutputItemRelativePath":"AgroBase.exe"},{"OutputItemFullPath":"C:\\ZendionInc\\agrobot_base\\AgroBase\\AgroBase\\bin\\x64\\Debug\\AgroBase.pdb","OutputItemRelativePath":"AgroBase.pdb"}],"CopyToOutputEntries":[]} \ No newline at end of file diff --git a/AgroBase/AgroBase/Models/Operacoes/OperacaoModel.cs b/AgroBase/AgroBase/Models/Operacoes/OperacaoModel.cs index f7a2bdbec..1eb779f2d 100644 --- a/AgroBase/AgroBase/Models/Operacoes/OperacaoModel.cs +++ b/AgroBase/AgroBase/Models/Operacoes/OperacaoModel.cs @@ -1414,7 +1414,21 @@ namespace AgroBase.Models op.IniciarTimerLogsOperacao(); - RedisService.Publish(CmdKey.ManagerWorkerRx, JsonConvert.SerializeObject(new { cmd = ManagerWorkerCommandType.IniciarMPC })); + // Barreira crítica: a operação só pode seguir para a fase de + // execução depois que o Manager/Python confirmar exatamente a + // trajetória completa projetada neste OperacaoModel. O método + // faz retry enquanto esta operação continuar sendo a atual. + bool mapaConfirmado = await HealthWorkerService + .SincronizarMapaCriticoAsync(op) + .ConfigureAwait(false); + + if (!mapaConfirmado) + { + Variaveis.MostrarLog( + "[OperacaoModel.IniciarOperacao] Mapa MPC não confirmado. Operação permanece bloqueada." + ); + return; + } await op.RealizarCalibragemInicialAsync(true); } @@ -1533,7 +1547,21 @@ namespace AgroBase.Models op.Trajetoria?.ProjetarTrajetoriaFixa(); op.Trajetoria.VerificacaoInicialMeioRuaConcluida = false; - RedisService.Publish(CmdKey.ManagerWorkerRx, JsonConvert.SerializeObject(new { cmd = ManagerWorkerCommandType.IniciarMPC })); + + // Simulação usa a mesma barreira transacional do campo. Isso é + // importante porque foi justamente no simulador que uma revisão + // parcial (um único corredor) conseguiu iniciar o MPC. + bool mapaConfirmado = await HealthWorkerService + .SincronizarMapaCriticoAsync(op) + .ConfigureAwait(false); + + if (!mapaConfirmado) + { + Variaveis.MostrarLog( + "[OperacaoModel.IniciarSimulacao] Mapa MPC não confirmado. Simulação permanece bloqueada." + ); + return; + } } public void ReiniciarSimulacao() diff --git a/AgroBase/AgroBase/Services/Operadores/HealthWorkerService.cs b/AgroBase/AgroBase/Services/Operadores/HealthWorkerService.cs index b5da57be5..480b35931 100644 --- a/AgroBase/AgroBase/Services/Operadores/HealthWorkerService.cs +++ b/AgroBase/AgroBase/Services/Operadores/HealthWorkerService.cs @@ -309,10 +309,24 @@ namespace AgroBase.Services.Operadores var pControle = _Parametros.Controle; var _Controle = op.Controle; + bool exigeConfirmacaoMapaMpc = + (op.Parametros?.ControleAutomatico ?? false) && + pControle != null && + pControle.DirTipoMovimento == TiposControladorDirecional.MPC && + (op.Trajetoria?._TrajetoriaFixa?.Any() ?? false); + + // Sempre que esta chamada solicita sincronizacao de um mapa MPC, + // a operacao fica estruturalmente NAO configurada ate o ACK exato + // da mesma revisao voltar do Manager/Python. Isto elimina a janela + // em que uma trajetoria nova ja foi publicada, mas o MPC ainda + // executa a revisao anterior. + bool configuradoPublicado = + configurado && !(sincronizarMapa && exigeConfirmacaoMapaMpc); + RedisService.AtualizarCampos( CtxKey.DadosOperacao, ("id", op.ID ?? ""), - ("configurado", configurado), + ("configurado", configuradoPublicado), ("modo", op.Parametros?.Modo ?? ModoOperacao.NaoDefinido), ("tempo_aguardar_inicio_operacao", op.TempoIniciarOperacao), ("tempo_aguardar_retomada", op.TempoAguardarRetomada), @@ -414,7 +428,19 @@ namespace AgroBase.Services.Operadores if (!sucesso) { Variaveis.MostrarLog( - "[MAPSYNC] Sincronização não crítica não foi confirmada." + "[MAPSYNC] Sincronização não crítica não foi confirmada. Operação permanece bloqueada." + ); + } + else if ( + configurado && + ReferenceEquals(op, Variaveis.OperacaoEmAndamento) + ) + { + // Só publica configurado=true DEPOIS que o mapa foi + // confirmado pelo Manager/MPC. + RedisService.AtualizarCampos( + CtxKey.DadosOperacao, + ("configurado", true) ); } } @@ -438,13 +464,98 @@ namespace AgroBase.Services.Operadores } private static readonly SemaphoreSlim _mapaSync = new SemaphoreSlim(1, 1); + private static long _mapaRevisionSeq = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + private static long ProximaRevisaoMapa() + { + return Interlocked.Increment(ref _mapaRevisionSeq); + } + + private static string ObterIdOperacaoMapa(OperacaoModel op) + { + if (op == null) + return ""; + + if (!string.IsNullOrWhiteSpace(op.ID)) + return op.ID; + + long ticks = op.Sensoriamento?.Operacao?.DataInicio.Ticks ?? 0L; + return $"local-{ticks}-{op.GetHashCode()}"; + } public static async Task SincronizarMapaCriticoAsync(OperacaoModel op) { - return await SolicitarAtualizacaoMapaAsync( - op, - aguardarGate: true - ).ConfigureAwait(false); + int ciclo = 0; + + // Intertravamento publicado ANTES de esperar o gate. Mesmo que uma + // sincronizacao antiga termine nesse intervalo, ela nao pode liberar + // a operacao enquanto esta barreira estiver ativa. + RedisService.AtualizarCampos( + CtxKey.DadosOperacao, + ("mapa_barreira_critica", true), + ("mapa_confirmado", false), + ("configurado", false) + ); + + /* + * Barreira critica de partida: + * enquanto esta mesma operacao existir, nao desistimos de sincronizar. + * A operacao permanece configurado=false ate o ACK exato voltar. + */ + while (ReferenceEquals(op, Variaveis.OperacaoEmAndamento)) + { + ciclo++; + + // Publica no Redis o snapshot mais recente da trajetoria, sem + // disparar outra sincronizacao em paralelo e sem liberar operacao. + AtualizarDadosOperacao( + forcar: true, + sincronizarMapa: false, + configurado: false + ); + + bool sucesso = await SolicitarAtualizacaoMapaAsync( + op, + aguardarGate: true + ).ConfigureAwait(false); + + if (sucesso) + { + // Somente agora a configuracao pode ser considerada pronta. + AtualizarDadosOperacao( + forcar: true, + sincronizarMapa: false, + configurado: true + ); + + // A barreira e a ultima coisa a cair. Nesse instante a + // revisao, a quantidade e o ACK do Manager ja foram validados. + RedisService.AtualizarCampos( + CtxKey.DadosOperacao, + ("mapa_barreira_critica", false) + ); + + Variaveis.MostrarLog( + $"[MAPSYNC/CRITICO] Mapa confirmado. ciclo={ciclo}. Operacao liberavel." + ); + + return true; + } + + if (!ReferenceEquals(op, Variaveis.OperacaoEmAndamento)) + return false; + + int atrasoMs = Math.Min(2000, 250 + ciclo * 250); + + Variaveis.MostrarLog( + $"[MAPSYNC/CRITICO] Mapa ainda nao confirmado. " + + $"retry em {atrasoMs}ms | ciclo={ciclo}." + ); + + await Task.Delay(atrasoMs).ConfigureAwait(false); + } + + return false; } private static async Task SolicitarAtualizacaoMapaAsync(OperacaoModel op, bool aguardarGate = false) @@ -505,39 +616,81 @@ namespace AgroBase.Services.Operadores return false; } + var pontosSnapshot = op.Trajetoria?._TrajetoriaFixa?.ToList(); + int quantidadeEsperada = pontosSnapshot?.Count ?? 0; + + if (quantidadeEsperada <= 0) + { + Variaveis.MostrarLog( + "[MAPSYNC] Snapshot vazio. Operacao continua bloqueada." + ); + return false; + } + + int primeiroIdxEsperado = pontosSnapshot.First().idxPonto; + int ultimoIdxEsperado = pontosSnapshot.Last().idxPonto; + /* - * A REVISÃO identifica o MAPA. - * - * Ela permanece a mesma durante todos os retries. + * A REVISAO identifica o snapshot logico do mapa. + * commandId identifica apenas cada tentativa de transporte. */ - long mapRevision = - DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - - string operationId = op.ID ?? ""; + long mapRevision = ProximaRevisaoMapa(); + string operationId = ObterIdOperacaoMapa(op); + // Invalida qualquer confirmacao anterior ANTES de publicar a + // nova tentativa. Assim uma revisao velha jamais libera a atual. RedisService.AtualizarCampos( CtxKey.DadosOperacao, + ("configurado", false), ("mapa_revisao_solicitada", mapRevision), ("mapa_operacao_id", operationId), + ("mapa_quantidade_pontos_esperada", quantidadeEsperada), + ("mapa_primeiro_idx_esperado", primeiroIdxEsperado), + ("mapa_ultimo_idx_esperado", ultimoIdxEsperado), + ("mapa_revisao_processada", 0L), + ("mapa_quantidade_pontos_processada", 0), + ("mapa_revisao_mpc_aplicada", 0L), + ("mapa_quantidade_pontos_mpc_aplicada", 0), + ("mapa_command_id_mpc_aplicado", ""), + ("mapa_status_mpc", "pendente"), + ("mapa_confirmado", false), ("mapa_status", "pendente") ); for (int tentativa = 1; tentativa <= 3; tentativa++) { - if (!ReferenceEquals( - op, - Variaveis.OperacaoEmAndamento)) + if (!ReferenceEquals(op, Variaveis.OperacaoEmAndamento)) + return false; + + var atual = op.Trajetoria?._TrajetoriaFixa; + int qtdAtual = atual?.Count ?? 0; + int primeiroAtual = qtdAtual > 0 ? atual.First().idxPonto : int.MinValue; + int ultimoAtual = qtdAtual > 0 ? atual.Last().idxPonto : int.MinValue; + + // Se a trajetoria mudou enquanto esta transacao estava viva, + // nao tentamos "encaixar" a revisao antiga no mapa novo. + if ( + qtdAtual != quantidadeEsperada || + primeiroAtual != primeiroIdxEsperado || + ultimoAtual != ultimoIdxEsperado + ) { + Variaveis.MostrarLog( + $"[MAPSYNC] Trajetoria mudou durante sincronizacao | " + + $"esperado={quantidadeEsperada}({primeiroIdxEsperado}->{ultimoIdxEsperado}) | " + + $"atual={qtdAtual}({primeiroAtual}->{ultimoAtual}). Nova revisao sera criada." + ); + + RedisService.AtualizarCampos( + CtxKey.DadosOperacao, + ("mapa_status", "alterado_durante_sync"), + ("mapa_confirmado", false) + ); + return false; } - /* - * commandId identifica a TENTATIVA de transporte. - * - * A revisão continua a mesma. - */ - string commandId = - Guid.NewGuid().ToString("N"); + string commandId = Guid.NewGuid().ToString("N"); RedisService.AtualizarCampos( CtxKey.DadosOperacao, @@ -548,27 +701,32 @@ namespace AgroBase.Services.Operadores $"[MAPSYNC] Tentativa={tentativa} | " + $"cmd={commandId} | " + $"rev={mapRevision} | " + - $"pontos={op.Trajetoria._TrajetoriaFixa?.Count}" + $"pontos={quantidadeEsperada} | " + + $"idx={primeiroIdxEsperado}->{ultimoIdxEsperado}" ); - bool atualizado = - await AtualizarPontosMapa( - op, - commandId, - operationId, - mapRevision - ).ConfigureAwait(false); + bool atualizado = await AtualizarPontosMapa( + op, + commandId, + operationId, + mapRevision, + quantidadeEsperada + ).ConfigureAwait(false); if (atualizado) { RedisService.AtualizarCampos( CtxKey.DadosOperacao, - ("mapa_status", "aplicado") + ("mapa_status", "aplicado"), + ("mapa_confirmado", true), + ("mapa_quantidade_pontos_confirmada", quantidadeEsperada), + ("mapa_revisao_confirmada", mapRevision) ); Variaveis.MostrarLog( - $"[MAPSYNC] SUCESSO | " + + $"[MAPSYNC] SUCESSO EXATO | " + $"rev={mapRevision} | " + + $"pontos={quantidadeEsperada} | " + $"tentativa={tentativa}" ); @@ -579,19 +737,18 @@ namespace AgroBase.Services.Operadores $"[MAPSYNC] Falha/timeout | " + $"tentativa={tentativa} | " + $"cmd={commandId} | " + - $"rev={mapRevision}" + $"rev={mapRevision} | " + + $"pontos={quantidadeEsperada}" ); if (tentativa < 3) - { - await Task.Delay(250) - .ConfigureAwait(false); - } + await Task.Delay(250).ConfigureAwait(false); } RedisService.AtualizarCampos( CtxKey.DadosOperacao, - ("mapa_status", "erro") + ("mapa_status", "erro"), + ("mapa_confirmado", false) ); return false; @@ -603,7 +760,13 @@ namespace AgroBase.Services.Operadores } } - private static async Task AtualizarPontosMapa(OperacaoModel op, string commandId, string operationId, long mapRevision) + private static async Task AtualizarPontosMapa( + OperacaoModel op, + string commandId, + string operationId, + long mapRevision, + int quantidadeEsperada + ) { if ( !(op?.Parametros?.ControleAutomatico ?? false) || @@ -618,7 +781,8 @@ namespace AgroBase.Services.Operadores cmd = HealthWorkerCommandType.AtualizarPontosMapa, command_id = commandId, operation_id = operationId, - map_revision = mapRevision + map_revision = mapRevision, + expected_point_count = quantidadeEsperada }; long assinantes = RedisService.Publish( @@ -630,13 +794,9 @@ namespace AgroBase.Services.Operadores $"[MAPSYNC/TX] cmd={commandId} | " + $"rev={mapRevision} | " + $"subs={assinantes} | " + - $"pontos={op.Trajetoria._TrajetoriaFixa.Count}" + $"pontos={quantidadeEsperada}" ); - /* - * Se ninguém ouviu o Publish, não existe motivo para - * esperar quatro segundos por um ACK impossível. - */ if (assinantes <= 0) { Variaveis.MostrarLog( @@ -647,10 +807,6 @@ namespace AgroBase.Services.Operadores return false; } - /* - * O Python agora espera o ACK do Manager (~2,5 s). - * Portanto 3 s ficou apertado demais. - */ bool sucesso = await AguardarMapaAplicadoAsync( commandId, mapRevision, @@ -659,19 +815,17 @@ namespace AgroBase.Services.Operadores if (!sucesso) { - string status = - RedisService.GetField( - CtxKey.DadosHealthWorker, - "mapa_status", - "" - ); + string status = RedisService.GetField( + CtxKey.DadosHealthWorker, + "mapa_status", + "" + ); - string erro = - RedisService.GetField( - CtxKey.DadosHealthWorker, - "mapa_erro", - "" - ); + string erro = RedisService.GetField( + CtxKey.DadosHealthWorker, + "mapa_erro", + "" + ); Variaveis.MostrarLog( $"[MAPSYNC] ACK não confirmado | " + @@ -680,9 +834,62 @@ namespace AgroBase.Services.Operadores $"status={status} | " + $"erro={erro}" ); + + return false; } - return sucesso; + // Segunda confirmacao, diretamente no estado transacional usado + // pelo Manager. O ACK so vale se a MESMA revisao e a MESMA + // quantidade de pontos realmente chegaram ao MPC. + long revisaoProcessada = RedisService.GetField( + CtxKey.DadosOperacao, + "mapa_revisao_processada", + 0L + ); + + int qtdProcessada = RedisService.GetField( + CtxKey.DadosOperacao, + "mapa_quantidade_pontos_processada", + 0 + ); + + long revisaoMpc = RedisService.GetField( + CtxKey.DadosOperacao, + "mapa_revisao_mpc_aplicada", + 0L + ); + + int qtdMpc = RedisService.GetField( + CtxKey.DadosOperacao, + "mapa_quantidade_pontos_mpc_aplicada", + 0 + ); + + string statusMpc = RedisService.GetField( + CtxKey.DadosOperacao, + "mapa_status_mpc", + "" + ); + + bool confirmacaoExata = + revisaoProcessada == mapRevision && + qtdProcessada == quantidadeEsperada && + revisaoMpc == mapRevision && + qtdMpc == quantidadeEsperada && + statusMpc == "aplicado"; + + if (!confirmacaoExata) + { + Variaveis.MostrarLog( + $"[MAPSYNC] ACK rejeitado por divergencia de snapshot | " + + $"revEsperada={mapRevision} | revProcessada={revisaoProcessada} | revMpc={revisaoMpc} | " + + $"qtdEsperada={quantidadeEsperada} | qtdProcessada={qtdProcessada} | qtdMpc={qtdMpc} | " + + $"statusMpc={statusMpc}" + ); + return false; + } + + return true; } private static async Task AguardarMapaAplicadoAsync(string commandId, long revision, TimeSpan timeout) @@ -696,16 +903,12 @@ namespace AgroBase.Services.Operadores string status = RedisService.GetField(CtxKey.DadosHealthWorker, "mapa_status", ""); if (ultimoCommandId == commandId && revisaoAplicada == revision && status == "aplicado") - { return true; - } if (ultimoCommandId == commandId && status == "erro") - { return false; - } - await Task.Delay(100); + await Task.Delay(100).ConfigureAwait(false); } return false; diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/manager_worker/main.py b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/manager_worker/main.py index 8478c0718..c02a6c35b 100644 --- a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/manager_worker/main.py +++ b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/manager_worker/main.py @@ -95,6 +95,19 @@ def main(): "map_revision" ) + expected_point_count = _p.get( + "expected_point_count" + ) + + if expected_point_count is not None: + expected_point_count = int(expected_point_count) + quantidade_recebida = len(mapa or []) + if quantidade_recebida != expected_point_count: + raise ValueError( + f"Snapshot de mapa divergente no Manager: " + f"esperado={expected_point_count}, recebido={quantidade_recebida}" + ) + mostrar_log( f"[MAPSYNC/MANAGER] aplicando mapa | " f"cmd={command_id} | " @@ -122,6 +135,7 @@ def main(): CtxKey.DadosOperacao, mapa_command_id_mpc_aplicado=str(command_id), mapa_revisao_mpc_aplicada=map_revision, + mapa_quantidade_pontos_mpc_aplicada=len(mapa or []), mapa_status_mpc="aplicado", mapa_erro_mpc="", mapa_mpc_aplicado_em=time.time(), @@ -150,6 +164,7 @@ def main(): CtxKey.DadosOperacao, mapa_command_id_mpc_aplicado=str(command_id), mapa_revisao_mpc_aplicada=map_revision, + mapa_quantidade_pontos_mpc_aplicada=0, mapa_status_mpc="erro", mapa_erro_mpc=str(e), ) diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/manager_worker/modulos/direcional.py b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/manager_worker/modulos/direcional.py index 055897519..fbdda1442 100644 --- a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/manager_worker/modulos/direcional.py +++ b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/manager_worker/modulos/direcional.py @@ -250,6 +250,12 @@ def _normalizar_comando_mpc(comando: dict, contexto: dict): comando.get("tipo", TipoMovimentoDirecional.RodasDianteiras.value) ) + parada_necessaria = _bool(comando.get("parada_necessaria", False)) + if parada_necessaria: + # A direção deve ficar neutra no mesmo ciclo em que o MPC pede stop. + angulo = 0.0 + tipo = TipoMovimentoDirecional.RodasDianteiras.value + debug_custo = comando.get("debug_custo", {}) if not isinstance(debug_custo, dict): debug_custo = {} @@ -261,7 +267,7 @@ def _normalizar_comando_mpc(comando: dict, contexto: dict): return { "comando_definido": True, "enviar_comando": _bool(comando.get("enviar_comando", True)), - "parada_necessaria": _bool(comando.get("parada_necessaria", False)), + "parada_necessaria": parada_necessaria, "erro": _bool(comando.get("erro", False)), "latencia": _float(comando.get("latencia", 0.0), 0.0), "angulo": round(angulo, 2), @@ -1083,6 +1089,25 @@ def _montar_comando_retorno(comando, latencia=-1.0): comando["erro_lateral"] = _float(comando.get("erro_lateral", 0.0), 0.0) comando["erro_orientacao"] = _float(comando.get("erro_orientacao", 0.0), 0.0) + # V17 SAFETY: + # "parada_necessaria" é um contrato forte. Nenhum chamador pode receber + # esse flag junto com um esterçamento residual e continuar aplicando-o. + if comando["parada_necessaria"]: + comando["enviar_comando"] = True + comando["angulo"] = 0.0 + comando["tipo"] = TipoMovimentoDirecional.RodasDianteiras.value + comando["simulacao"] = [] + + debug_stop = comando.get("debug_custo", {}) + if not isinstance(debug_stop, dict): + debug_stop = {} + + debug_stop["parada_necessaria"] = { + "ativo": True, + "acao_direcional": "angulo_zero_rodas_dianteiras", + } + comando["debug_custo"] = debug_stop + if not isinstance(comando.get("simulacao", []), list): comando["simulacao"] = [] diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/manager_worker/modulos/mpc.py b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/manager_worker/modulos/mpc.py index 2ae1143e3..58c9d20b1 100644 --- a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/manager_worker/modulos/mpc.py +++ b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/manager_worker/modulos/mpc.py @@ -892,6 +892,11 @@ class ControladorMPC: # Índices globais são opcionais para manter compatibilidade. Quando # presentes, eliminam a ambiguidade entre o índice da trajetória C# # e o índice local de uma lista dinâmica enviada ao MPC. + self._idx_global_explicito = bool(self.pontos_info) and all( + isinstance(p, dict) + and ("idxPonto" in p or "IdxPonto" in p) + for p in self.pontos_info + ) self._idx_global_pontos = np.array([ _safe_int( p.get("idxPonto", p.get("IdxPonto", i)), @@ -3616,7 +3621,13 @@ class ControladorMPC: try: if not self.pontos_info: - return None + motivo = "MPC sem pontos carregados" + _log(f"[MPC/V17 SAFETY] HARD STOP | {motivo}") + return self._comando_parada_segura_mpc( + motivo=motivo, + latencia=0.0, + erro=True, + ) # -------------------- Estado atual (real) -------------------- GPS = contexto.get("GPS", {}) @@ -3645,6 +3656,83 @@ class ControladorMPC: idx_ponto_alvo = contexto.get("Carro", {}).get("IdxPontoAlvo", 0) Nmax = 30 if status_carro == StatusCarroMapa.CaminhandoRua.value else 60 + # ============================================================ + # V17 SAFETY - o índice autoritativo do C# NÃO pode ultrapassar + # a fotografia de mapa carregada neste MPC. + # + # Esta barreira vem ANTES de: + # - sincronização de visitados; + # - construção do alvo; + # - Pure Pursuit Dubins; + # - árvore MPC. + # + # Assim, se o C# pedir um ponto que não existe localmente, o + # controlador nunca faz clamp silencioso para o último ponto e + # nunca transforma "fim de mapa local" em um comando de reta. + # ============================================================ + idx_proximo_ponto_real = _safe_int(idx_proximo_ponto_real, 0) + qtd_pontos_mpc = len(self.pontos_info) + + if idx_proximo_ponto_real < 0: + motivo = ( + f"Índice autoritativo inválido: " + f"idx_csharp={idx_proximo_ponto_real} | " + f"pontos_mpc={qtd_pontos_mpc}" + ) + _log(f"[MPC/V17 SAFETY] HARD STOP | {motivo}") + return self._comando_parada_segura_mpc( + motivo=motivo, + latencia=pos_latencia, + erro=True, + ) + + identidade_global = bool( + getattr(self, "_idx_global_explicito", False) + ) + + if identidade_global: + # Com identidade global explícita não usamos o tamanho do array + # para validar um índice ainda existente. Isso mantém o código + # correto mesmo se no futuro houver snapshots com ids globais + # não iniciados em zero. + if idx_proximo_ponto_real not in self._idx_global_para_local: + max_idx_global = ( + int(np.max(self._idx_global_pontos)) + if len(self._idx_global_pontos) + else -1 + ) + fim_normal_global = ( + idx_proximo_ponto_real == max_idx_global + 1 + ) + motivo = ( + f"Mapa MPC sem ponto futuro autoritativo: " + f"idx_csharp={idx_proximo_ponto_real} | " + f"idx_max_mpc={max_idx_global} | " + f"pontos_mpc={qtd_pontos_mpc} | " + f"status={_enum_name(status_carro, StatusCarroMapa)}" + ) + _log(f"[MPC/V17 SAFETY] HARD STOP | {motivo}") + return self._comando_parada_segura_mpc( + motivo=motivo, + latencia=pos_latencia, + erro=not fim_normal_global, + ) + elif idx_proximo_ponto_real >= qtd_pontos_mpc: + # Contrato legado: índice local == posição no array. + excedeu = idx_proximo_ponto_real > qtd_pontos_mpc + motivo = ( + f"Mapa MPC sem ponto futuro autoritativo: " + f"idx_csharp={idx_proximo_ponto_real} | " + f"pontos_mpc={qtd_pontos_mpc} | " + f"status={_enum_name(status_carro, StatusCarroMapa)}" + ) + _log(f"[MPC/V17 SAFETY] HARD STOP | {motivo}") + return self._comando_parada_segura_mpc( + motivo=motivo, + latencia=pos_latencia, + erro=bool(excedeu), + ) + self.tempo_execucao_local = contexto.get("Carro", {}).get("TempoEntreComandos", 0.5) #self.passos_horizonte_local = max(1, math.floor(1 / self.tempo_execucao_local)) @@ -4123,26 +4211,41 @@ class ControladorMPC: mostrar_log(f"🚨 Tempo maximo de execucao excedido: {delta}/{limite}, processo: {processo}") return hot_stop - def _comando_fallback_hot_stop(self, comando_anterior, motivos, latencia=0): - comando_anterior = _as_dict(comando_anterior) - tipo_seguro = _movimento_from_value( - comando_anterior.get("tipo", TipoMovimentoDirecional.RodasDianteiras.value) - ).value + def _comando_parada_segura_mpc(self, motivo, latencia=0.0, erro=False): + """Comando fail-stop canônico do MPC. + + Uma parada necessária deve SEMPRE zerar o direcional na origem. + O ProcessadorEmAndamento converte este contrato também em MOV=0. + """ + motivos = _safe_motivos(motivo) return { "enviar_comando": True, "parada_necessaria": True, - "erro": True, + "erro": bool(erro), "latencia": _safe_float(latencia, 0.0, min_value=0.0), - "angulo": _safe_float(comando_anterior.get("angulo", 0.0), 0.0), - "tipo": tipo_seguro, + "angulo": 0.0, + "tipo": TipoMovimentoDirecional.RodasDianteiras.value, "simulacao": [], "erro_lateral": 0.0, "erro_orientacao": 0.0, - "debug_custo": {}, + "debug_custo": { + "safety_stop": { + "ativo": True, + "origem": "MPC", + "motivo": " | ".join(map(str, motivos)), + } + }, "candidatos_testados": 0, - "motivos": _safe_motivos(motivos), + "motivos": motivos, } + def _comando_fallback_hot_stop(self, comando_anterior, motivos, latencia=0): + return self._comando_parada_segura_mpc( + motivo=motivos, + latencia=latencia, + erro=True, + ) + def _gerar_angulos_candidatos_receding( self, ponto_alvo, diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/manager_worker/processadores/_4_em_andamento.py b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/manager_worker/processadores/_4_em_andamento.py index ce5c72422..ae58c710f 100644 --- a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/manager_worker/processadores/_4_em_andamento.py +++ b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/manager_worker/processadores/_4_em_andamento.py @@ -256,8 +256,121 @@ class ProcessadorEmAndamento(ProcessadorBase): erro_dir = bool( comando_dir.get("erro", False) ) + parada_dir = bool( + comando_dir.get("parada_necessaria", False) + ) - if erro_dir: + # ======================================================== + # V17 SAFETY - parada necessária do direcional é GLOBAL. + # + # Antes desta versão o flag chegava corretamente do MPC, + # porém só `erro=True` zerava o MOV. Isso permitia o caso: + # MPC sem pontos -> parada_necessaria=True, erro=False + # DIR ia para 0°, mas MOV continuava com a velocidade + # calculada alguns microssegundos antes. + # + # Agora qualquer parada_necessaria força no MESMO envelope: + # MOV = 0 + # freio = true + # DIR = 0° + # tipo = RodasDianteiras + # ======================================================== + if parada_dir: + motivos_stop = ( + comando_dir.get("motivos", []) or [] + ) + if isinstance(motivos_stop, str): + motivos_stop = [motivos_stop] + elif not isinstance(motivos_stop, list): + try: + motivos_stop = list(motivos_stop) + except Exception: + motivos_stop = [str(motivos_stop)] + + if not motivos_stop: + motivos_stop = [ + "Controle direcional solicitou parada necessária" + ] + + motivos.extend(motivos_stop) + self._logar_bloqueio(motivos_stop) + + erro_geral = bool(erro_geral or erro_dir) + + if movimento_automatico: + velocidade = 0.0 + frear = True + atualizar_movimento = ( + self._movimento_mudou( + velocidade, + frear + ) + or self._venceu_intervalo_movimento(agora) + ) + + angulo = 0.0 + tipo_movimento = ( + TipoMovimentoDirecional + .RodasDianteiras + .value + ) + + simulacao = [] + + latencia = self._to_float( + comando_dir.get( + "latencia", + 0.0 + ), + 0.0 + ) + + erro_lateral = self._to_float( + comando_dir.get( + "erro_lateral", + 0.0 + ), + 0.0 + ) + + debug_custo = ( + comando_dir.get( + "debug_custo", + {} + ) + or {} + ) + if not isinstance(debug_custo, dict): + debug_custo = {} + + debug_custo["parada_global"] = { + "ativa": True, + "origem": "direcional.parada_necessaria", + "movimento_percentual": 0.0, + "frear": True, + "angulo": 0.0, + "tipo": int( + TipoMovimentoDirecional + .RodasDianteiras + .value + ), + } + + atualizar_direcional = ( + self._direcional_mudou( + angulo, + tipo_movimento + ) + or self._venceu_intervalo_direcional(agora) + or bool( + comando_dir.get( + "enviar_comando", + True + ) + ) + ) + + elif erro_dir: erro_geral = True motivos.extend( diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/shared/contexto_global_redis.py b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/shared/contexto_global_redis.py index 09cc72d7b..4f1da5091 100644 --- a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/shared/contexto_global_redis.py +++ b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/shared/contexto_global_redis.py @@ -433,6 +433,19 @@ class ContextoGlobalRedis: modulos_opcionais_configurados=[], status_anterior_parado=StatusOperacao.Parado.value, status_iniciado_anterior=False, + mapa_status="nao_sincronizado", + mapa_barreira_critica=False, + mapa_confirmado=False, + mapa_revisao_solicitada=0, + mapa_revisao_processada=0, + mapa_revisao_mpc_aplicada=0, + mapa_quantidade_pontos_esperada=0, + mapa_quantidade_pontos_processada=0, + mapa_quantidade_pontos_mpc_aplicada=0, + mapa_primeiro_idx_esperado=None, + mapa_ultimo_idx_esperado=None, + mapa_status_mpc="", + mapa_command_id_mpc_aplicado="", ) # ============================================================ @@ -614,6 +627,110 @@ class ContextoGlobalRedis: detalhes_regras["trajetoria"] = trj_debug + # ------------------------------------------------------------ + # 4.1) Confirmação transacional do mapa no MPC + # ------------------------------------------------------------ + # Para qualquer operação realmente controlada por MPC, não basta o + # mapa existir no Redis. A MESMA revisão precisa ter sido convertida + # pelo HealthWorker e aplicada pelo Manager com a MESMA quantidade + # de pontos. Este gate não é ignorado nem em debug_mode. + dados_dir = cls._dict(operacao.get("Dir", {})) + tipo_controle_dir = cls._int( + dados_dir.get("tipo_controle", TiposControladorDirecional.Manual.value), + TiposControladorDirecional.Manual.value, + ) + dados_mpc = cls._dict(dados_dir.get("mpc", {})) + pontos_publicados = dados_mpc.get("pontos", []) or [] + + requer_confirmacao_mapa_mpc = bool( + tipo_controle_dir == TiposControladorDirecional.MPC.value + ) + + revisao_solicitada = cls._int(operacao.get("mapa_revisao_solicitada", 0), 0) + revisao_processada = cls._int(operacao.get("mapa_revisao_processada", 0), 0) + revisao_mpc = cls._int(operacao.get("mapa_revisao_mpc_aplicada", 0), 0) + qtd_esperada = cls._int(operacao.get("mapa_quantidade_pontos_esperada", 0), 0) + qtd_processada = cls._int(operacao.get("mapa_quantidade_pontos_processada", 0), 0) + qtd_mpc = cls._int(operacao.get("mapa_quantidade_pontos_mpc_aplicada", 0), 0) + qtd_publicada = len(pontos_publicados) + status_mapa = str(operacao.get("mapa_status", "") or "") + status_mpc = str(operacao.get("mapa_status_mpc", "") or "") + mapa_confirmado_flag = cls._bool(operacao.get("mapa_confirmado", False)) + mapa_barreira_critica = cls._bool(operacao.get("mapa_barreira_critica", False)) + + primeiro_esperado = operacao.get("mapa_primeiro_idx_esperado", None) + ultimo_esperado = operacao.get("mapa_ultimo_idx_esperado", None) + primeiro_publicado = None + ultimo_publicado = None + try: + if pontos_publicados: + primeiro_publicado = cls._int( + cls._dict(pontos_publicados[0]).get("idxPonto", -1), -1 + ) + ultimo_publicado = cls._int( + cls._dict(pontos_publicados[-1]).get("idxPonto", -1), -1 + ) + except Exception: + primeiro_publicado = None + ultimo_publicado = None + + indices_extremos_ok = True + if requer_confirmacao_mapa_mpc: + if primeiro_esperado is not None: + indices_extremos_ok = indices_extremos_ok and ( + primeiro_publicado == cls._int(primeiro_esperado, -999999) + ) + if ultimo_esperado is not None: + indices_extremos_ok = indices_extremos_ok and ( + ultimo_publicado == cls._int(ultimo_esperado, -999999) + ) + + mapa_mpc_ok = ( + not requer_confirmacao_mapa_mpc + or ( + not mapa_barreira_critica + and mapa_confirmado_flag + and status_mapa == "aplicado" + and status_mpc == "aplicado" + and revisao_solicitada > 0 + and revisao_solicitada == revisao_processada == revisao_mpc + and qtd_esperada > 0 + and qtd_publicada == qtd_esperada + and qtd_processada == qtd_esperada + and qtd_mpc == qtd_esperada + and indices_extremos_ok + ) + ) + + if not mapa_mpc_ok: + motivos.append( + "Mapa/MPC ainda não confirmado: " + f"rev solicitada/processada/mpc={revisao_solicitada}/{revisao_processada}/{revisao_mpc}; " + f"pontos publicados/esperados/processados/mpc={qtd_publicada}/{qtd_esperada}/{qtd_processada}/{qtd_mpc}; " + f"status={status_mapa}/{status_mpc}" + ) + + detalhes_regras["mapa_mpc"] = { + "requer_confirmacao": requer_confirmacao_mapa_mpc, + "ok": mapa_mpc_ok, + "confirmado": mapa_confirmado_flag, + "barreira_critica": mapa_barreira_critica, + "status_mapa": status_mapa, + "status_mpc": status_mpc, + "revisao_solicitada": revisao_solicitada, + "revisao_processada": revisao_processada, + "revisao_mpc": revisao_mpc, + "pontos_publicados": qtd_publicada, + "pontos_esperados": qtd_esperada, + "pontos_processados": qtd_processada, + "pontos_mpc": qtd_mpc, + "primeiro_idx_publicado": primeiro_publicado, + "ultimo_idx_publicado": ultimo_publicado, + "primeiro_idx_esperado": primeiro_esperado, + "ultimo_idx_esperado": ultimo_esperado, + "indices_extremos_ok": indices_extremos_ok, + } + # ------------------------------------------------------------ # 5) Motivos runtime do controle # ------------------------------------------------------------ @@ -719,7 +836,7 @@ class ContextoGlobalRedis: # ------------------------------------------------------------ estrutural_ok = bool(modulos_ok and parametros_ok) - base_ok = bool(emergencia_ok and pausa_ok and trajetoria_ok) + base_ok = bool(emergencia_ok and pausa_ok and trajetoria_ok and mapa_mpc_ok) if debug_mode: # Debug ignora módulos/parâmetros, mas NÃO ignora emergência, pausa ou trajetória. @@ -1399,6 +1516,7 @@ class ContextoGlobalRedis: mapa=None, map_revision=None, command_id=None, + expected_point_count=None, ): try: # Snapshot único. @@ -1469,6 +1587,7 @@ class ContextoGlobalRedis: # Identidade da transação "map_revision": map_revision, "command_id": command_id, + "expected_point_count": expected_point_count, }, }, ) @@ -1492,7 +1611,6 @@ class ContextoGlobalRedis: # ========================================================== # 1. Snapshot único da operação # ========================================================== - operacao = cls.get_operacao() operacao_atual = str( @@ -1512,7 +1630,6 @@ class ContextoGlobalRedis: # ========================================================== # 2. Garantir que o comando pertence à revisão atual # ========================================================== - revisao_atual = operacao.get( "mapa_revisao_solicitada", None @@ -1532,10 +1649,72 @@ class ContextoGlobalRedis: ), } + # ========================================================== + # 2.1. Validar o snapshot BRUTO antes da conversão + # ========================================================== + dados_dir = cls._dict(operacao.get("Dir", {})) + dados_mpc = cls._dict(dados_dir.get("mpc", {})) + pontos_brutos = dados_mpc.get("pontos", []) or [] + + qtd_esperada = cls._int( + operacao.get("mapa_quantidade_pontos_esperada", 0), + 0, + ) + primeiro_esperado = operacao.get("mapa_primeiro_idx_esperado", None) + ultimo_esperado = operacao.get("mapa_ultimo_idx_esperado", None) + + if qtd_esperada <= 0: + return { + "sucesso": False, + "erro": "Quantidade esperada de pontos ausente/inválida", + } + + if len(pontos_brutos) != qtd_esperada: + return { + "sucesso": False, + "erro": ( + f"Snapshot bruto divergente: " + f"esperado={qtd_esperada}, recebido={len(pontos_brutos)}" + ), + } + + if pontos_brutos: + primeiro_bruto = cls._int( + cls._dict(pontos_brutos[0]).get("idxPonto", -1), + -1, + ) + ultimo_bruto = cls._int( + cls._dict(pontos_brutos[-1]).get("idxPonto", -1), + -1, + ) + + if ( + primeiro_esperado is not None + and primeiro_bruto != cls._int(primeiro_esperado, -999999) + ): + return { + "sucesso": False, + "erro": ( + f"Primeiro idxPonto divergente: " + f"esperado={primeiro_esperado}, recebido={primeiro_bruto}" + ), + } + + if ( + ultimo_esperado is not None + and ultimo_bruto != cls._int(ultimo_esperado, -999999) + ): + return { + "sucesso": False, + "erro": ( + f"Último idxPonto divergente: " + f"esperado={ultimo_esperado}, recebido={ultimo_bruto}" + ), + } + # ========================================================== # 3. Converter exatamente este snapshot # ========================================================== - pontos_info = cls._converter_pontos_mapa( operacao ) @@ -1546,6 +1725,17 @@ class ContextoGlobalRedis: "erro": "Nenhum ponto válido foi produzido", } + # Não aceitamos conversão parcial. Se um único ponto falhou, + # a transação inteira é rejeitada e será reenviada. + if len(pontos_info) != qtd_esperada: + return { + "sucesso": False, + "erro": ( + f"Conversão parcial rejeitada: " + f"esperado={qtd_esperada}, convertido={len(pontos_info)}" + ), + } + mostrar_log( f"[MAPSYNC] mapa convertido | " f"cmd={command_id} | " @@ -1556,11 +1746,11 @@ class ContextoGlobalRedis: # ========================================================== # 4. Gravar pontos convertidos # ========================================================== - atualizado = cls.atualizar_ctx_dict( CtxKey.DadosOperacao, pontos_mapa=pontos_info, mapa_status_mpc="pendente", + mapa_quantidade_pontos_processada=0, ) if not atualizado: @@ -1572,10 +1762,10 @@ class ContextoGlobalRedis: # ========================================================== # 5. Enviar EXATAMENTE este mapa ao Manager # ========================================================== - mostrar_log( f"[MAPSYNC] enviando mapa ao Manager | " - f"cmd={command_id} | rev={map_revision}" + f"cmd={command_id} | rev={map_revision} | " + f"pontos={qtd_esperada}" ) mpc_enviado = cls._atualizar_mpc( @@ -1585,6 +1775,7 @@ class ContextoGlobalRedis: mapa=pontos_info, map_revision=map_revision, command_id=command_id, + expected_point_count=qtd_esperada, ) if not mpc_enviado: @@ -1596,10 +1787,10 @@ class ContextoGlobalRedis: # ========================================================== # 6. Esperar Manager confirmar que realmente carregou # ========================================================== - aplicado_mpc = cls._aguardar_mapa_mpc_aplicado( command_id=command_id, map_revision=map_revision, + expected_point_count=qtd_esperada, timeout_s=2.5, ) @@ -1607,18 +1798,19 @@ class ContextoGlobalRedis: return { "sucesso": False, "erro": ( - f"Manager não confirmou aplicação do mapa | " - f"cmd={command_id} | rev={map_revision}" + f"Manager não confirmou aplicação exata do mapa | " + f"cmd={command_id} | rev={map_revision} | " + f"pontos={qtd_esperada}" ), } # ========================================================== # 7. Só agora considerar revisão processada # ========================================================== - cls.atualizar_ctx_dict( CtxKey.DadosOperacao, mapa_revisao_processada=map_revision, + mapa_quantidade_pontos_processada=len(pontos_info), ) return { @@ -1725,6 +1917,7 @@ class ContextoGlobalRedis: cls, command_id, map_revision, + expected_point_count=None, timeout_s=2.5, ): limite = time.time() + max(0.1, float(timeout_s)) @@ -1744,6 +1937,11 @@ class ContextoGlobalRedis: None ) + qtd_aplicada = cls._int( + operacao.get("mapa_quantidade_pontos_mpc_aplicada", 0), + 0, + ) + status = str( operacao.get( "mapa_status_mpc", @@ -1751,11 +1949,17 @@ class ContextoGlobalRedis: ) ) + quantidade_ok = ( + expected_point_count is None + or qtd_aplicada == int(expected_point_count) + ) + if ( cmd_aplicado == str(command_id) and revisao_aplicada is not None and int(revisao_aplicada) == int(map_revision) and status == "aplicado" + and quantidade_ok ): return True