diff --git a/AgroBase/AgroBase/Models/Operacoes/OperacaoModel.cs b/AgroBase/AgroBase/Models/Operacoes/OperacaoModel.cs index fbc4f4635..8adb16df5 100644 --- a/AgroBase/AgroBase/Models/Operacoes/OperacaoModel.cs +++ b/AgroBase/AgroBase/Models/Operacoes/OperacaoModel.cs @@ -4061,7 +4061,7 @@ namespace AgroBase.Models saude_geral < 80.0 ? StatusModulo.Alerta : StatusModulo.Operante; - op.SubstituirOperacaoSnapshot(this, operacaoAtualizada); + op.SubstituirOperacaoSnapshot(op.Sensoriamento, operacaoAtualizada); if (statusMudou) { @@ -5308,7 +5308,8 @@ namespace AgroBase.Models Finalizando = origem.Finalizando; DataInicio = origem.DataInicio; DataFim = origem.DataFim; - StatusOperacaoAnterior = origem.StatusOperacaoAnterior; + //StatusOperacaoAtual = origem.StatusOperacaoAtual; + //StatusOperacaoAnterior = origem.StatusOperacaoAnterior; ModsCalibragem = origem.ModsCalibragem? @@ -5350,7 +5351,7 @@ namespace AgroBase.Models ParametrosMandatorios = new List(ParametrosMandatorios ?? new List()), DispositivosMapeados = new List(DispositivosMapeados ?? new List()), ModsCalibragem = new Dictionary>(ModsCalibragem ?? new Dictionary>()), - ModsCalibragemView = new Dictionary>(ModsCalibragemView), + ModsCalibragemView = new Dictionary>(ModsCalibragemView ?? new Dictionary>()), }; } diff --git a/AgroBase/AgroBase/Models/Variaveis.cs b/AgroBase/AgroBase/Models/Variaveis.cs index 5f87ff5c6..92f7ac3e1 100644 --- a/AgroBase/AgroBase/Models/Variaveis.cs +++ b/AgroBase/AgroBase/Models/Variaveis.cs @@ -2269,8 +2269,8 @@ namespace AgroBase.Models { return numbers.GroupBy(n => n) .OrderByDescending(g => g.Count()) - .First() - .Key; + .FirstOrDefault()? + .Key ?? double.NaN; } public static double CalcularTemperatura(double leitura, double offset = 0.0) diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/main.py b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/main.py index 673191eb0..e851a1f12 100644 --- a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/main.py +++ b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/main.py @@ -30,7 +30,7 @@ def main(): T_Code.Sen: ModuloSensoriamento(), T_Code.Atu: ModuloAtuador(), #T_Code.Lra: ModuloLoRa(), - T_Code.Imu: CameraIMU(modulos_imu=[T_Code.Snr]), + T_Code.Imu: CameraIMU(modulos_imu=[T_Code.Snr, T_Code.Cam], max_disagreement_deg=4.0, max_skew_imu_s=0.12), T_Code.Npc: ModuloPC(), T_Code.Lvx: ModuloLivox(), T_Code.Ipb: ModuloIPBribge(), diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/imu.py b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/imu.py index d97ae23aa..efa2fa365 100644 --- a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/imu.py +++ b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/imu.py @@ -61,6 +61,8 @@ class CameraIMU(ModuloDiagnosticoBase): filtro_alpha=0.35, historico_len=100, modulos_imu=None, + + max_skew_imu_s=0.12 ): self.freq = float(freq) self.publish_hz = float(publish_hz) @@ -150,6 +152,8 @@ class CameraIMU(ModuloDiagnosticoBase): self._assinatura_fontes_anterior = None + self.max_skew_imu_s = float(max_skew_imu_s) + self.thread = threading.Thread( target=self.imu_task_loop, name="CameraIMUFinalWorker", @@ -904,6 +908,7 @@ class CameraIMU(ModuloDiagnosticoBase): 1.0, ), "fonte_principal": fonte_principal["mx_id"], + "fonte_principal_t_code": fonte_principal.get("t_code"), "single_source": False, "estrategia_fusao": "weighted_agreement", } @@ -917,11 +922,9 @@ class CameraIMU(ModuloDiagnosticoBase): "roll_rate_dps": fonte_principal["roll_rate_dps"], "pitch_rate_dps": fonte_principal["pitch_rate_dps"], "yaw_rate_dps": fonte_principal["yaw_rate_dps"], - "confidence": min( - float(fonte_principal["confidence"]), - 1.0, - ), + "confidence": min(float(fonte_principal["confidence"]), 1.0), "fonte_principal": fonte_principal["mx_id"], + "fonte_principal_t_code": fonte_principal.get("t_code"), "fonte_roll": fonte_principal["mx_id"], "fonte_pitch": fonte_principal["mx_id"], "fonte_roll_rate": fonte_principal["mx_id"], @@ -1011,6 +1014,39 @@ class CameraIMU(ModuloDiagnosticoBase): """ self._filtro_fusao_inicializado = False + def _filtrar_fontes_dessincronizadas(self, fontes): + if len(fontes) < 2: + return fontes, [] + + mais_recente = min( + fontes, + key=lambda f: f.get("idade_s", 999.0) + ) + + idade_referencia = mais_recente.get("idade_s", 999.0) + + sincronizadas = [] + dessincronizadas = [] + + for fonte in fontes: + skew_s = abs( + fonte.get("idade_s", 999.0) - idade_referencia + ) + + if skew_s <= self.max_skew_imu_s: + sincronizadas.append(fonte) + else: + fonte = dict(fonte) + fonte["confiavel"] = False + fonte["confidence"] = 0.0 + fonte["motivos"] = list(fonte.get("motivos", [])) + fonte["motivos"].append( + f"imu_dessincronizada_{skew_s:.3f}s" + ) + dessincronizadas.append(fonte) + + return sincronizadas, dessincronizadas + # ============================================================ # Risco e ação sugerida # ============================================================ @@ -1454,6 +1490,9 @@ class CameraIMU(ModuloDiagnosticoBase): fontes, ignoradas, desabilitadas = self._ler_fontes_imu() + fontes, dessincronizadas = (self._filtrar_fontes_dessincronizadas(fontes)) + ignoradas.extend(dessincronizadas) + modulos_validos = { f.get("t_code") for f in fontes diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/camera_manager.py b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/camera_manager.py index 6fd671b90..33a170d3b 100644 --- a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/camera_manager.py +++ b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/weed_worker/camera_manager.py @@ -861,28 +861,30 @@ class CameraManager: self._ultima_saude_ts = time.time() try: - # Mantém a referência estável durante leitura/publicação da saúde. - # fechar_camera_manager aguarda esta seção curta terminar. + # O lock protege somente a identidade da sessão da câmera. with self._vida_lock: camera_atual = self.camera + generation = self._camera_generation - if camera_atual is not None: - if pipeline_ia is None: - pipeline_ia = self._avaliar_saude_pipeline_ia() - camera_atual.atualizar_saude(pipeline_ia=pipeline_ia) + if camera_atual is not None: + if pipeline_ia is None: + pipeline_ia = self._avaliar_saude_pipeline_ia() - elif self.mx_id is not None: - from camera_worker.manager import definir_saude_camera - definir_saude_camera( - self.mx_id, - StatusModulo.DESCONECTADO, - 0, - ["desconectado"], - False, - {}, - disp=T_Code.Cam, - conectado=False, - ) + camera_atual.atualizar_saude(pipeline_ia=pipeline_ia) + + elif self.mx_id is not None: + from camera_worker.manager import definir_saude_camera + + definir_saude_camera( + self.mx_id, + StatusModulo.DESCONECTADO, + 0, + ["desconectado"], + False, + {}, + disp=T_Code.Cam, + conectado=False, + ) except Exception as e: self.mostrar_log(f"[saude] erro: {e}") @@ -2914,4 +2916,4 @@ class CameraManager: def _finalizar_salvamento_posprocessamento(self): with self._posproc_save_lock: - self._posproc_salvando = False \ No newline at end of file + self._posproc_salvando = False