ajustes no imu, filtro temperatura motor, alarmes npc, offset temperatura sensoriamento, metodo que abre curva ao final do corredor

This commit is contained in:
Diego Freitas 2026-08-05 15:14:23 -03:00
parent 6242d8b3dc
commit 742aac3f5d
8 changed files with 841 additions and 172 deletions

View File

@ -232,7 +232,8 @@ namespace AgroBase.Models.Modules
maximo = 80,
minimo = 5,
nominal = 25,
unidade_medida = "ºC"
unidade_medida = "ºC",
offset = -39.9,
},
new SensorValoresLeituraModel()
{
@ -288,7 +289,8 @@ namespace AgroBase.Models.Modules
maximo = 80,
minimo = 5,
nominal = 25,
unidade_medida = "ºC"
unidade_medida = "ºC",
offset = -0.5,
},
new SensorValoresLeituraModel()
{
@ -344,7 +346,8 @@ namespace AgroBase.Models.Modules
maximo = 80,
minimo = 5,
nominal = 25,
unidade_medida = "ºC"
unidade_medida = "ºC",
offset = -15.9,
},
new SensorValoresLeituraModel()
{
@ -400,7 +403,8 @@ namespace AgroBase.Models.Modules
maximo = 80,
minimo = 5,
nominal = 25,
unidade_medida = "ºC"
unidade_medida = "ºC",
offset = -3.6,
},
new SensorValoresLeituraModel()
{

View File

@ -47,7 +47,7 @@ namespace AgroBase.Models
public AutonomiaCorredorModel AutonomiaCorredor { get; set; }
[JsonIgnore]
private GPSModel GPSPosicaoAtual => Variaveis.OperacaoEmAndamento.Sensoriamento?.Gps?.Clone();
private GPSModel GPSPosicaoAtual => GPSService.UltimaLeitura?.Clone();
[JsonProperty]
public double TempoEntreLeituras { get; private set; }
@ -2155,8 +2155,6 @@ namespace AgroBase.Models
int idxEsq = 0;
int idxDir = 0;
double anguloAcrescentar = direcaoAtual == DirecaoCarroRua.Ida ? AnguloAberturaCurva : -AnguloAberturaCurva;
for (int idx = 0; idx < Corredores.Count(); idx++)
{
double _FatorLarguraCorredor = CorredoresLarguras[idx] / LarguraCorredorPadrao;
@ -2282,15 +2280,29 @@ namespace AgroBase.Models
_trajetoriaFixa.Add(PontoTrajetoria);
}
double anguloProjetar2 = GPSUtils.CalcularOrientacao(CorredorAtual.Skip(CorredorAtual.Count() - 2).First(), CorredorAtual.Last());
double percentualAcrescimoPontoAberturaCurva = 0.8;
double distancia_projetar = !ultimoCorredor ? (DistanciaProjecaoRua * percentualAcrescimoPontoAberturaCurva) : (DistanciaProjecaoRua * percentualAcrescimoPontoAberturaCurva);
GPSModel ultimoPontoCorredorAtual = CorredorAtual.Last();
double anguloProjetar2 = GPSUtils.CalcularOrientacao(CorredorAtual.Skip(CorredorAtual.Count() - 2).First(), ultimoPontoCorredorAtual);
GPSModel UltimoPonto = GPSUtils.ProjetarPontoDeslocado(ultimoPontoCorredorAtual, distancia_projetar, anguloProjetar2);
// Realiza um acréscimo no angulo à projetar, permitindo que o equipamento faça uma curva mais aberta entre corredores
if (!ultimoCorredor && !_FatorLarguraElevado)
{
anguloProjetar2 += anguloAcrescentar;
anguloAcrescentar *= -1;
GPSModel up_pos = GPSUtils.ProjetarPontoDeslocado(ultimoPontoCorredorAtual, distancia_projetar, anguloProjetar2 + AnguloAberturaCurva);
GPSModel up_neg = GPSUtils.ProjetarPontoDeslocado(ultimoPontoCorredorAtual, distancia_projetar, anguloProjetar2 - AnguloAberturaCurva);
GPSModel pp_proximoCorredor = direcaoAtual == DirecaoCarroRua.Volta ? Corredores[idx + 1].FirstOrDefault() : Corredores[idx + 1].LastOrDefault();
double d_pos = GPSUtils.DistanciaEntrePontos(pp_proximoCorredor, up_pos);
double d_neg = GPSUtils.DistanciaEntrePontos(pp_proximoCorredor, up_neg);
if (d_pos > d_neg)
UltimoPonto = GPSUtils.ProjetarPontoDeslocado(ultimoPontoCorredorAtual, distancia_projetar, anguloProjetar2 + AnguloAberturaCurva);
else
UltimoPonto = GPSUtils.ProjetarPontoDeslocado(ultimoPontoCorredorAtual, distancia_projetar, anguloProjetar2 - AnguloAberturaCurva);
}
double percentualAcrescimoPontoAberturaCurva = 0.8;
GPSModel UltimoPonto = GPSUtils.ProjetarPontoDeslocado(CorredorAtual.Last(), !ultimoCorredor ? (DistanciaProjecaoRua * percentualAcrescimoPontoAberturaCurva) : (DistanciaProjecaoRua * percentualAcrescimoPontoAberturaCurva), anguloProjetar2);
PontoTrajetoriaModel PontoFinal = new PontoTrajetoriaModel(TipoPontoRua.LigacaoSaida)
{

View File

@ -86,8 +86,13 @@ class IMUCamera(ModuloDiagnosticoBase):
self.calibrado = False
self.calibrando = False
self.calib_inicio_ts = 0.0
self.calib_primeira_amostra_quat_ts = None
self.calib_min_amostras_quat = max(30, int(self.freq * 0.50))
self.calib_max_std_deg = 1.50
self.calib_quats_xyzw = []
self.q_ref_inv = None
self.rotation_accuracy_rad = None
self.rotation_accuracy_max_rad = math.radians(20.0)
# Estado angular
self.roll_deg = 0.0
@ -176,7 +181,9 @@ class IMUCamera(ModuloDiagnosticoBase):
self.calibrado = False
self.calibrando = True
self.calib_inicio_ts = time.perf_counter()
# A janela útil começa somente quando chegar a primeira amostra válida.
self.calib_inicio_ts = 0.0
self.calib_primeira_amostra_quat_ts = None
self.calib_quats_xyzw.clear()
self._calib_roll_hist.clear()
@ -580,7 +587,12 @@ class IMUCamera(ModuloDiagnosticoBase):
if not self.calibrando:
return
self.calib_quats_xyzw.append(q_xyzw)
agora = time.perf_counter()
if self.calib_primeira_amostra_quat_ts is None:
self.calib_primeira_amostra_quat_ts = agora
self.calib_inicio_ts = agora
self.calib_quats_xyzw.append(np.asarray(q_xyzw, dtype=np.float64))
try:
roll_abs, pitch_abs, _ = R.from_quat(q_xyzw).as_euler("xyz", degrees=True)
@ -589,9 +601,28 @@ class IMUCamera(ModuloDiagnosticoBase):
except Exception:
pass
elapsed = time.perf_counter() - self.calib_inicio_ts
elapsed = agora - self.calib_primeira_amostra_quat_ts
if elapsed < self.calibracao_segundos:
if (
elapsed < self.calibracao_segundos
or len(self.calib_quats_xyzw) < self.calib_min_amostras_quat
):
return
roll_std = float(np.std(self._calib_roll_hist)) if self._calib_roll_hist else 999.0
pitch_std = float(np.std(self._calib_pitch_hist)) if self._calib_pitch_hist else 999.0
# Não aceita como zero uma janela em que o rover/câmera se moveu.
if roll_std > self.calib_max_std_deg or pitch_std > self.calib_max_std_deg:
self.mostrar_log(
f"Calibração rejeitada por movimento | std_roll={roll_std:.3f}° | "
f"std_pitch={pitch_std:.3f}°. Reiniciando janela."
)
self.calib_quats_xyzw.clear()
self._calib_roll_hist.clear()
self._calib_pitch_hist.clear()
self.calib_primeira_amostra_quat_ts = None
self.calib_inicio_ts = 0.0
return
q_ref = self._media_quaternion_xyzw(self.calib_quats_xyzw)
@ -607,9 +638,6 @@ class IMUCamera(ModuloDiagnosticoBase):
self.calibrando = False
self.calibrado = True
roll_std = float(np.std(self._calib_roll_hist)) if self._calib_roll_hist else 0.0
pitch_std = float(np.std(self._calib_pitch_hist)) if self._calib_pitch_hist else 0.0
self.mostrar_log(
f"Calibração concluída | amostras={len(self.calib_quats_xyzw)} | "
f"std_roll={roll_std:.3f}° | std_pitch={pitch_std:.3f}°"
@ -762,7 +790,6 @@ class IMUCamera(ModuloDiagnosticoBase):
if q_xyzw is None:
return False
agora = time.perf_counter()
agora_wall = time.time()
self.last_packet_ts = agora_wall
@ -770,6 +797,8 @@ class IMUCamera(ModuloDiagnosticoBase):
self._processar_calibracao(q_xyzw)
self.rotation_accuracy_rad = accuracy_rad
roll, pitch, yaw = self._quat_relativo_para_euler(q_xyzw)
self.roll_deg = roll
@ -825,6 +854,11 @@ class IMUCamera(ModuloDiagnosticoBase):
# Metadados do rotation vector.
"rotation_accuracy_rad": accuracy_rad,
"rotation_accuracy_ok": bool(
accuracy_rad is not None
and math.isfinite(float(accuracy_rad))
and 0.0 <= float(accuracy_rad) <= self.rotation_accuracy_max_rad
),
# Saúde operacional básica.
"timestamp": agora_wall * 1000.0,

View File

@ -808,18 +808,26 @@ class CameraMultispectral:
)
performance["device_metrics"] = metrics
temp = float(metrics.get("temperature_average_c", 0.0) or 0.0)
performance["temperatura"] = temp
if temp >= 80:
motivos.append(f"Temperatura crítica: {temp:.1f} °C")
saude -= 30
elif temp >= 70:
motivos.append(f"Temperatura elevada: {temp:.1f} °C")
saude -= 15
elif temp >= 60:
motivos.append(f"Temperatura acima do ideal: {temp:.1f} °C")
saude -= 5
temp_raw = metrics.get("temperature_average_c")
if temp_raw is None:
performance["temperatura"] = None
motivos.append("Temperatura interna indisponível")
saude -= 10
else:
temp = float(temp_raw)
performance["temperatura"] = temp
if temp >= 100:
motivos.append(f"Temperatura de emergência: {temp:.1f} °C")
saude = min(saude, 10)
elif temp >= 95:
motivos.append(f"Temperatura crítica: {temp:.1f} °C")
saude = min(saude, 35)
elif temp >= 85:
motivos.append(f"Temperatura elevada: {temp:.1f} °C")
saude -= 15
elif temp >= 75:
motivos.append(f"Aquecimento preventivo: {temp:.1f} °C")
saude -= 5
ddr = float(metrics.get("ddr_used_bytes", 0) or 0) / 1024.0 / 1024.0
performance["memoria_usada"] = ddr

View File

@ -138,7 +138,7 @@ class CameraOak:
nome_sensor=f"{self.modelo}_imu",
modo=self.imu_modo,
gyro_unidade="rad_s",
referencial_robo="r,p,y",
referencial_robo="y,r,p",
)
if self.modelo_ia_seg is not None:
@ -634,15 +634,24 @@ class CameraOak:
"velocidade": speed
}
# 🔸 Temperatura (> 60 °C começa a penalizar)
if temp >= 80:
motivos.append(f"Temperatura crítica: {temp:.1f} °C")
saude -= 30
elif temp >= 70:
motivos.append(f"Temperatura elevada: {temp:.1f} °C")
saude -= 15
elif temp >= 60:
motivos.append(f"Temperatura acima do ideal: {temp:.1f} °C")
saude -= 5
if temp is None:
performance["temperatura"] = None
motivos.append("Temperatura interna indisponível")
saude -= 10
else:
temp = float(temp)
if temp >= 100:
motivos.append(f"Temperatura de emergência: {temp:.1f} °C")
saude = min(saude, 10)
elif temp >= 95:
motivos.append(f"Temperatura crítica: {temp:.1f} °C")
saude = min(saude, 35)
elif temp >= 85:
motivos.append(f"Temperatura elevada: {temp:.1f} °C")
saude -= 15
elif temp >= 75:
motivos.append(f"Aquecimento preventivo: {temp:.1f} °C")
saude -= 5
# 🔸 Memória DDR (> 300 MB começa a penalizar)
if ddr >= 500:

View File

@ -35,6 +35,7 @@ class CameraIMU(ModuloDiagnosticoBase):
timeout_imu_s=0.40,
timeout_camera_s=2.0,
max_disagreement_deg=4.0,
rotation_accuracy_max_rad=0.35,
# Limites principais vindos do equipamento.
# No rover:
@ -68,6 +69,7 @@ class CameraIMU(ModuloDiagnosticoBase):
self.timeout_imu_s = float(timeout_imu_s)
self.timeout_camera_s = float(timeout_camera_s)
self.max_disagreement_deg = float(max_disagreement_deg)
self.rotation_accuracy_max_rad = float(rotation_accuracy_max_rad)
# ============================================================
# Limites angulares do equipamento
@ -429,14 +431,13 @@ class CameraIMU(ModuloDiagnosticoBase):
# timestamp pode vir em ms.
ts_s = ts_ms / 1000.0 if ts_ms > 100000000000.0 else ts_ms
candidatos_ts = [
ts_s if ts_s > 0 else 0.0,
last_packet_ts if last_packet_ts > 0 else 0.0,
last_publish_ts if last_publish_ts > 0 else 0.0,
]
ts_ref = max(candidatos_ts)
idade_s = (agora - ts_ref) if ts_ref > 0 else 999.0
# Todos os campos acima devem estar no relógio de parede (Unix epoch).
# Não usa max() cegamente: um timestamp monotônico misturado ao epoch
# pode esconder uma fonte velha ou produzir idade negativa.
ts_ref = ts_s if ts_s > 0 else last_packet_ts
if ts_ref <= 0:
ts_ref = last_publish_ts
idade_s = max(0.0, agora - ts_ref) if ts_ref > 0 else 999.0
roll = self._safe_float(imu, "roll", 0.0)
pitch = self._safe_float(imu, "pitch", 0.0)
@ -450,6 +451,9 @@ class CameraIMU(ModuloDiagnosticoBase):
pitch_rate = self._safe_float(imu, "pitch_rate_dps", 0.0)
yaw_rate = self._safe_float(imu, "yaw_rate_dps", 0.0)
modo = str(imu.get("modo", "") or "").lower()
accuracy_rad = self._safe_float(imu, "rotation_accuracy_rad", float("nan"))
imu_saude = None
try:
saude_obj = imu.get("saude")
@ -491,6 +495,29 @@ class CameraIMU(ModuloDiagnosticoBase):
confiavel = False
motivos.append(f"peso_base_baixo")
valores = (roll_filtrado, pitch_filtrado, yaw_filtrado,
roll_rate, pitch_rate, yaw_rate)
if not all(math.isfinite(v) for v in valores):
confiavel = False
motivos.append("imu_valor_nao_finito")
accuracy_score = 1.0
if modo in ("rotation_vector", "game_rotation_vector"):
if not math.isfinite(accuracy_rad) or accuracy_rad < 0.0:
confiavel = False
accuracy_score = 0.0
motivos.append("rotation_accuracy_indisponivel")
elif accuracy_rad > self.rotation_accuracy_max_rad:
confiavel = False
accuracy_score = 0.0
motivos.append(f"rotation_accuracy_ruim_{accuracy_rad:.3f}rad")
else:
accuracy_score = self.clamp(
1.0 - accuracy_rad / self.rotation_accuracy_max_rad,
0.25,
1.0,
)
freshness = 1.0 - self.norm_to_100(idade_s, 0.0, self.timeout_imu_s) / 100.0
freshness = self.clamp(freshness, 0.0, 1.0)
@ -501,7 +528,10 @@ class CameraIMU(ModuloDiagnosticoBase):
if imu_saude is not None:
saude_score = self.clamp(imu_saude / 100.0, 0.0, 1.0)
confidence = peso_base * freshness * calib_score * valido_score * saude_score
confidence = (
peso_base * freshness * calib_score * valido_score
* saude_score * accuracy_score
)
if not confiavel:
confidence = 0.0
@ -525,9 +555,11 @@ class CameraIMU(ModuloDiagnosticoBase):
"confidence": float(confidence),
"peso_base": float(peso_base),
"roll": float(roll),
"pitch": float(pitch),
"yaw": float(yaw),
# O módulo final consome uma atitude coerente: os três ângulos
# filtrados pertencem à mesma fonte e ao mesmo instante publicado.
"roll": float(roll_filtrado),
"pitch": float(pitch_filtrado),
"yaw": float(yaw_filtrado),
"roll_filtrado": float(roll_filtrado),
"pitch_filtrado": float(pitch_filtrado),
@ -536,6 +568,7 @@ class CameraIMU(ModuloDiagnosticoBase):
"roll_rate_dps": float(roll_rate),
"pitch_rate_dps": float(pitch_rate),
"yaw_rate_dps": float(yaw_rate),
"rotation_accuracy_rad": accuracy_rad if math.isfinite(accuracy_rad) else None,
"timestamp": ts_ms,
"last_packet_ts": last_packet_ts,
@ -790,11 +823,18 @@ class CameraIMU(ModuloDiagnosticoBase):
"divergentes": [],
}
def maior_distancia_circular(valores):
maior = 0.0
for i, a in enumerate(valores):
for b in valores[i + 1:]:
maior = max(maior, abs(self._angle_delta_deg(a, b)))
return float(maior)
rolls = [float(f["roll"]) for f in fontes]
pitchs = [float(f["pitch"]) for f in fontes]
roll_dis = float(max(rolls) - min(rolls))
pitch_dis = float(max(pitchs) - min(pitchs))
roll_dis = maior_distancia_circular(rolls)
pitch_dis = maior_distancia_circular(pitchs)
max_dis = max(roll_dis, pitch_dis)
sensores_agree = max_dis <= self.max_disagreement_deg
@ -817,17 +857,6 @@ class CameraIMU(ModuloDiagnosticoBase):
"divergentes": divergentes,
}
def _valor_mais_conservador(self, fontes, key):
fonte = max(
fontes,
key=lambda f: abs(float(f.get(key, 0.0))),
)
return (
float(fonte.get(key, 0.0)),
fonte["mx_id"],
)
def _fundir_fontes(self, fontes, sensores_agree=True):
if not fontes:
return None
@ -879,41 +908,26 @@ class CameraIMU(ModuloDiagnosticoBase):
"estrategia_fusao": "weighted_agreement",
}
roll, fonte_roll = self._valor_mais_conservador(
fontes,
"roll",
)
pitch, fonte_pitch = self._valor_mais_conservador(
fontes,
"pitch",
)
roll_rate, fonte_roll_rate = self._valor_mais_conservador(
fontes,
"roll_rate_dps",
)
pitch_rate, fonte_pitch_rate = self._valor_mais_conservador(
fontes,
"pitch_rate_dps",
)
# Em divergência, não cria uma orientação artificial misturando eixos.
# Isola as fontes secundárias e usa o pacote completo da mais confiável.
return {
"roll": roll,
"pitch": pitch,
"roll": fonte_principal["roll"],
"pitch": fonte_principal["pitch"],
"yaw": fonte_principal["yaw"],
"roll_rate_dps": roll_rate,
"pitch_rate_dps": pitch_rate,
"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,
),
"fonte_principal": fonte_principal["mx_id"],
"fonte_roll": fonte_roll,
"fonte_pitch": fonte_pitch,
"fonte_roll_rate": fonte_roll_rate,
"fonte_pitch_rate": fonte_pitch_rate,
"fonte_roll": fonte_principal["mx_id"],
"fonte_pitch": fonte_principal["mx_id"],
"fonte_roll_rate": fonte_principal["mx_id"],
"fonte_pitch_rate": fonte_principal["mx_id"],
"single_source": False,
"estrategia_fusao": "conservative_disagreement",
"estrategia_fusao": "primary_source_on_disagreement",
}
def _atualizar_filtro_fusao(self, fusao):

View File

@ -47,6 +47,33 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
PENALIDADE_FREQ_MAX_PADRAO = 4
PENALIDADE_FREQ_MAX_CRITICA = 8
# Temperatura MOV
TEMP_ALERTA_C = 70.0
TEMP_FALHA_C = 85.0
TEMP_EMERGENCIA_C = 100.0
TEMP_RECUPERA_ALERTA_C = 67.0
TEMP_RECUPERA_FALHA_C = 80.0
TEMP_RECUPERA_EMERGENCIA_C = 90.0
TEMP_ALERTA_INTERMEDIARIO_S = 3.0
TEMP_ALERTA_CONFIRMADO_S = 10.0
TEMP_FALHA_INTERMEDIARIO_S = 2.0
TEMP_FALHA_CONFIRMADO_S = 5.0
TEMP_EMERGENCIA_CONFIRMADO_S = 1.0
TEMP_RECUPERACAO_S = 10.0
# Elétricos
TENSAO_FALHA_PERSISTENCIA_S = 2.0
CORRENTE_MOTOR_FALHA_PERSISTENCIA_S = 1.0
CORRENTE_BARRAMENTO_FALHA_PERSISTENCIA_S = 1.0
# Freio/comando
FREIO_RPM_FALHA_PERSISTENCIA_S = 1.0
ALARME_COMANDO_PERSISTENCIA_S = 2.0
def __init__(self):
self.t_code = T_Code.Mov
self.nome = "Movimentacao"
@ -89,6 +116,8 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
19: ("Sinal Z não encontrado", 25, False),
}
self._estados_persistencia = {}
def atualizar_saude(self):
try:
dados_mov = ContextoGlobalRedis.get_modulo(self.t_code) or {}
@ -543,7 +572,7 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
# Faixas absolutas. Ajustar depois com logs reais.
if tensao > 0:
if tensao < 24 or tensao > 50:
if tensao < 25 or tensao > 45:
motivos.append(f"Tensão MOV em faixa perigosa: {tensao:.1f} V")
condicoes.append({
"label": "Tensão MOV",
@ -554,7 +583,7 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
})
score -= 70
risco_eletrico_termico = True
elif tensao < 28 or tensao > 45:
elif tensao < 30 or tensao > 43:
motivos.append(f"Tensão MOV fora da faixa esperada: {tensao:.1f} V")
condicoes.append({
"label": "Tensão MOV",
@ -579,7 +608,7 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
})
score -= 75
risco_eletrico_termico = True
elif abs(corrente_motor) > 45:
elif abs(corrente_motor) > 50:
motivos.append(f"Corrente do motor elevada: {corrente_motor:.1f} A")
condicoes.append({
"label": "Corrente motor",
@ -615,36 +644,37 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
})
score -= 20
temp_max = max(temperatura_motor, temperatura_driver)
# Temperatura do driver
_penalidade_d, _motivos_d, _condicoes_d, _risco_d = (
self._avaliar_temperatura(
endereco=endereco,
temperatura=temperatura_driver,
regra_desc="driver",
)
)
motivos.extend(_motivos_d)
condicoes.extend(_condicoes_d)
# Temperatura do motor
_penalidade_m, _motivos_m, _condicoes_m, _risco_m = (
self._avaliar_temperatura(
endereco=endereco,
temperatura=temperatura_motor,
regra_desc="motor",
)
)
motivos.extend(_motivos_m)
condicoes.extend(_condicoes_m)
# Aplica somente a maior penalidade térmica.
score -= max(_penalidade_d, _penalidade_m)
risco_eletrico_termico = (
risco_eletrico_termico
or _risco_d
or _risco_m
)
if temp_max > 85:
motivos.append(f"Temperatura MOV perigosa: {temp_max:.1f} °C")
condicoes.append({
"label": "Temperatura MOV",
"valor": temp_max,
"severidade": 95,
"descricao": f"Temperatura em faixa perigosa ({temp_max:.1f} °C)",
"acoes": [
"Reduzir carga dos motores.",
"Verificar ventilação do compartimento.",
"Pausar operação se persistir.",
],
})
score -= 80
risco_eletrico_termico = True
elif temp_max > 70:
motivos.append(f"Temperatura MOV elevada: {temp_max:.1f} °C")
condicoes.append({
"label": "Temperatura MOV",
"valor": temp_max,
"severidade": 60,
"descricao": f"Temperatura elevada ({temp_max:.1f} °C)",
"acoes": [
"Reduzir carga dos motores.",
"Verificar ventilação do compartimento dos drivers.",
],
})
score -= 25
# Padrões dinâmicos: úteis para diagnóstico, mas não travam operação.
self._avaliar_padroes_dinamicos(
@ -752,6 +782,399 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
"parametros": parametros_debug,
}
def _temperatura_valida(self, temperatura):
try:
valor = float(temperatura)
return 0.0 < valor < 200.0
except (TypeError, ValueError):
return False
def _avaliar_temperatura(
self,
endereco,
temperatura,
regra_desc,
):
motivos = []
condicoes = []
penalidade = 0
risco_eletrico_termico = False
nome_origem = (
"driver de movimento"
if regra_desc == "driver"
else "motor de tração"
)
label = f"Temperatura do {nome_origem}"
if not self._temperatura_valida(temperatura):
motivos.append(
f"Temperatura do {nome_origem} inválida: "
f"{temperatura} °C"
)
condicoes.append({
"label": label,
"valor": temperatura,
"severidade": 50,
"descricao": (
"Leitura de temperatura inválida"
),
"acoes": [
"Verificar sensor de temperatura.",
],
})
return (
penalidade,
motivos,
condicoes,
risco_eletrico_termico,
)
estado_temp_emergencia = self._estado_limite_alto(
driver=endereco,
regra=f"temperatura_emergencia_{regra_desc}",
valor=temperatura,
limite_entrada=self.TEMP_EMERGENCIA_C,
limite_saida=self.TEMP_RECUPERA_EMERGENCIA_C,
tempo_intermediario_s=0.0,
tempo_confirmacao_s=self.TEMP_EMERGENCIA_CONFIRMADO_S,
tempo_recuperacao_s=self.TEMP_RECUPERACAO_S,
)
estado_temp_falha = self._estado_limite_alto(
driver=endereco,
regra=f"temperatura_falha_{regra_desc}",
valor=temperatura,
limite_entrada=self.TEMP_FALHA_C,
limite_saida=self.TEMP_RECUPERA_FALHA_C,
tempo_intermediario_s=self.TEMP_FALHA_INTERMEDIARIO_S,
tempo_confirmacao_s=self.TEMP_FALHA_CONFIRMADO_S,
tempo_recuperacao_s=self.TEMP_RECUPERACAO_S,
)
estado_temp_alerta = self._estado_limite_alto(
driver=endereco,
regra=f"temperatura_alerta_{regra_desc}",
valor=temperatura,
limite_entrada=self.TEMP_ALERTA_C,
limite_saida=self.TEMP_RECUPERA_ALERTA_C,
tempo_intermediario_s=self.TEMP_ALERTA_INTERMEDIARIO_S,
tempo_confirmacao_s=self.TEMP_ALERTA_CONFIRMADO_S,
tempo_recuperacao_s=self.TEMP_RECUPERACAO_S,
)
# ============================================================
# EMERGÊNCIA CONFIRMADA
# ============================================================
if estado_temp_emergencia["nivel"] == "confirmado":
motivos.append(
f"Temperatura do {nome_origem} em nível de emergência "
f"confirmado: {temperatura:.1f} °C"
)
condicoes.append({
"label": label,
"valor": temperatura,
"severidade": 98,
"descricao": (
"Temperatura de emergência confirmada após "
f"{estado_temp_emergencia['duracao_s']:.1f} s"
),
"acoes": [
"Interromper imediatamente o esforço dos motores.",
"Verificar ventilação, motor, driver e sensor térmico.",
],
})
penalidade = 90
risco_eletrico_termico = True
# ============================================================
# RECUPERANDO DE EMERGÊNCIA
# ============================================================
elif estado_temp_emergencia["nivel"] == "recuperando":
motivos.append(
f"Temperatura do {nome_origem} normalizando após emergência: "
f"{temperatura:.1f} °C. Aguardando estabilidade para liberação."
)
condicoes.append({
"label": label,
"valor": temperatura,
"severidade": 98,
"descricao": (
"Temperatura caiu abaixo do limite de recuperação da "
"emergência, mas ainda aguarda estabilidade por "
f"{self.TEMP_RECUPERACAO_S:.1f} s "
f"({estado_temp_emergencia['duracao_s']:.1f} s decorridos)"
),
"acoes": [
"Manter o equipamento sem esforço.",
"Aguardar a conclusão da recuperação térmica.",
"Verificar se a temperatura continua diminuindo.",
],
})
# Mantém exatamente a proteção da emergência enquanto recupera.
penalidade = 90
risco_eletrico_termico = True
# ============================================================
# EMERGÊNCIA DETECTADA, AINDA NÃO CONFIRMADA
# ============================================================
elif estado_temp_emergencia["ativa"]:
motivos.append(
f"Temperatura de emergência do {nome_origem} detectada: "
f"{temperatura:.1f} °C "
f"({estado_temp_emergencia['duracao_s']:.1f} s)"
)
condicoes.append({
"label": label,
"valor": temperatura,
"severidade": 89,
"descricao": (
"Temperatura em nível de emergência, aguardando "
"confirmação da leitura"
),
"acoes": [
"Acompanhar imediatamente as próximas leituras.",
"Preparar a interrupção do esforço dos motores.",
],
})
# Ainda não bloqueia, pois não passou pela confirmação.
penalidade = 45
# ============================================================
# FALHA TÉRMICA CONFIRMADA
# ============================================================
elif estado_temp_falha["nivel"] == "confirmado":
motivos.append(
f"Temperatura perigosa do {nome_origem} confirmada: "
f"{temperatura:.1f} °C"
)
condicoes.append({
"label": label,
"valor": temperatura,
"severidade": 95,
"descricao": (
"Temperatura perigosa persistiu por "
f"{estado_temp_falha['duracao_s']:.1f} s"
),
"acoes": [
"Interromper ou reduzir o esforço dos motores.",
"Verificar a ventilação do compartimento.",
"Inspecionar motor, driver e sensor térmico.",
],
})
penalidade = 80
risco_eletrico_termico = True
# ============================================================
# RECUPERANDO DE FALHA TÉRMICA
# ============================================================
elif estado_temp_falha["nivel"] == "recuperando":
motivos.append(
f"Temperatura do {nome_origem} normalizando após condição "
f"perigosa: {temperatura:.1f} °C. "
"Aguardando estabilidade para liberação."
)
condicoes.append({
"label": label,
"valor": temperatura,
"severidade": 95,
"descricao": (
"Temperatura caiu abaixo do limite de recuperação da "
"falha, mas ainda aguarda estabilidade por "
f"{self.TEMP_RECUPERACAO_S:.1f} s "
f"({estado_temp_falha['duracao_s']:.1f} s decorridos)"
),
"acoes": [
"Manter a carga reduzida.",
"Aguardar a conclusão da recuperação térmica.",
"Verificar se a temperatura permanece estável.",
],
})
# Continua bloqueante até terminar a recuperação.
penalidade = 80
risco_eletrico_termico = True
# ============================================================
# FALHA TÉRMICA PERSISTINDO
# ============================================================
elif estado_temp_falha["nivel"] == "persistente":
motivos.append(
f"Temperatura perigosa do {nome_origem} persistindo: "
f"{temperatura:.1f} °C "
f"({estado_temp_falha['duracao_s']:.1f} s)"
)
condicoes.append({
"label": label,
"valor": temperatura,
"severidade": 85,
"descricao": (
"Temperatura perigosa persistindo, aguardando "
"confirmação de segurança"
),
"acoes": [
"Acompanhar as próximas leituras.",
"Reduzir a carga se a temperatura continuar aumentando.",
],
})
penalidade = 35
# ============================================================
# PRIMEIRA DETECÇÃO DE FALHA TÉRMICA
# ============================================================
elif estado_temp_falha["nivel"] == "detectado":
motivos.append(
f"Pico de temperatura perigosa do {nome_origem} detectado: "
f"{temperatura:.1f} °C"
)
condicoes.append({
"label": label,
"valor": temperatura,
"severidade": 70,
"descricao": (
"Temperatura perigosa detectada, ainda sem persistência"
),
"acoes": [
"Acompanhar a confirmação da leitura térmica.",
],
})
penalidade = 10
# ============================================================
# ALERTA TÉRMICO CONFIRMADO
# ============================================================
elif estado_temp_alerta["nivel"] == "confirmado":
motivos.append(
f"Temperatura elevada do {nome_origem} confirmada: "
f"{temperatura:.1f} °C"
)
condicoes.append({
"label": label,
"valor": temperatura,
"severidade": 60,
"descricao": (
"Temperatura elevada persistiu por "
f"{estado_temp_alerta['duracao_s']:.1f} s"
),
"acoes": [
"Reduzir a carga dos motores.",
"Verificar a ventilação.",
],
})
penalidade = 25
# ============================================================
# RECUPERANDO DE ALERTA TÉRMICO
# ============================================================
elif estado_temp_alerta["nivel"] == "recuperando":
motivos.append(
f"Temperatura do {nome_origem} normalizando após alerta: "
f"{temperatura:.1f} °C"
)
condicoes.append({
"label": label,
"valor": temperatura,
"severidade": 60,
"descricao": (
"Temperatura caiu abaixo do limite de recuperação do "
"alerta, aguardando estabilidade por "
f"{self.TEMP_RECUPERACAO_S:.1f} s "
f"({estado_temp_alerta['duracao_s']:.1f} s decorridos)"
),
"acoes": [
"Acompanhar a estabilização da temperatura.",
"Manter a carga moderada durante a recuperação.",
],
})
# Mantém a penalidade do alerta até concluir a recuperação,
# mas não caracteriza risco elétrico/térmico bloqueante.
penalidade = 25
# ============================================================
# ALERTA PERSISTINDO
# ============================================================
elif estado_temp_alerta["nivel"] == "persistente":
motivos.append(
f"Aquecimento do {nome_origem} persistindo: "
f"{temperatura:.1f} °C "
f"({estado_temp_alerta['duracao_s']:.1f} s)"
)
condicoes.append({
"label": label,
"valor": temperatura,
"severidade": 50,
"descricao": (
"Temperatura elevada persistindo, aguardando confirmação"
),
"acoes": [
"Acompanhar a evolução da temperatura.",
"Verificar a ventilação se continuar aumentando.",
],
})
penalidade = 15
# ============================================================
# PRIMEIRA DETECÇÃO DE ALERTA
# ============================================================
elif estado_temp_alerta["nivel"] == "detectado":
motivos.append(
f"Aquecimento do {nome_origem} detectado: "
f"{temperatura:.1f} °C"
)
condicoes.append({
"label": label,
"valor": temperatura,
"severidade": 40,
"descricao": (
"Temperatura acima da faixa ideal, aguardando persistência"
),
"acoes": [
"Acompanhar a evolução da temperatura.",
],
})
penalidade = 5
return (
penalidade,
motivos,
condicoes,
risco_eletrico_termico,
)
# ============================================================
# Frequência / contrato
# ============================================================
@ -835,16 +1258,13 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
@staticmethod
def _penalizar_frequencia(freq, base, minimo, max_penalidade):
try:
return float(penalizar_frequencia(freq, base, minimo, max_penalidade) or 0)
except Exception:
if base <= 0:
return 0.0
if freq >= minimo:
return 0.0
if freq <= 0:
return float(max_penalidade)
return float(max_penalidade) * (1.0 - max(0.0, min(1.0, freq / max(minimo, 0.001))))
if base <= 0:
return 0.0
if freq >= minimo:
return 0.0
if freq <= 0:
return float(max_penalidade)
return float(max_penalidade) * (1.0 - max(0.0, min(1.0, freq / max(minimo, 0.001))))
# ============================================================
# Padrões dinâmicos
@ -903,6 +1323,117 @@ class ModuloMovimentacao(ModuloDiagnosticoBase):
except Exception:
pass
def _estado_persistencia(
self,
driver,
regra,
ativa,
tempo_intermediario_s,
tempo_confirmacao_s,
tempo_recuperacao_s=0.0,
):
agora = time.monotonic()
chave = (str(driver), str(regra))
estado = self._estados_persistencia.setdefault(chave, {
"ativa_desde": None,
"normal_desde": None,
"confirmada": False,
})
if ativa:
estado["normal_desde"] = None
if estado["ativa_desde"] is None:
estado["ativa_desde"] = agora
duracao_s = max(0.0, agora - estado["ativa_desde"])
if duracao_s >= tempo_confirmacao_s:
estado["confirmada"] = True
if estado["confirmada"]:
nivel = "confirmado"
elif duracao_s >= tempo_intermediario_s:
nivel = "persistente"
else:
nivel = "detectado"
return {
"ativa": True,
"nivel": nivel,
"duracao_s": duracao_s,
"confirmada": estado["confirmada"],
}
estado["ativa_desde"] = None
if not estado["confirmada"]:
estado["normal_desde"] = None
return {
"ativa": False,
"nivel": "normal",
"duracao_s": 0.0,
"confirmada": False,
}
if estado["normal_desde"] is None:
estado["normal_desde"] = agora
normal_ha_s = max(0.0, agora - estado["normal_desde"])
if normal_ha_s >= tempo_recuperacao_s:
estado["confirmada"] = False
estado["normal_desde"] = None
return {
"ativa": False,
"nivel": "normal",
"duracao_s": 0.0,
"confirmada": False,
}
return {
"ativa": True,
"nivel": "recuperando",
"duracao_s": normal_ha_s,
"confirmada": True,
}
def _estado_limite_alto(
self,
driver,
regra,
valor,
limite_entrada,
limite_saida,
tempo_intermediario_s,
tempo_confirmacao_s,
tempo_recuperacao_s,
):
chave = (str(driver), str(regra))
persistencia = self._estados_persistencia.get(chave, {})
confirmada = bool(persistencia.get("confirmada", False))
# Histerese:
# ainda não confirmou -> entra no limite_entrada
# já confirmou -> permanece até cair abaixo do limite_saida
ativa = (
valor >= limite_saida
if confirmada
else valor >= limite_entrada
)
return self._estado_persistencia(
driver=driver,
regra=regra,
ativa=ativa,
tempo_intermediario_s=tempo_intermediario_s,
tempo_confirmacao_s=tempo_confirmacao_s,
tempo_recuperacao_s=tempo_recuperacao_s,
)
# ============================================================
# Status/publicação
# ============================================================

View File

@ -148,7 +148,7 @@ class ModuloPC(ModuloDiagnosticoBase):
# Processo C#
"process_private_mem_warn_mb": 6000.0,
"process_private_mem_fail_mb": 10000.0,
"process_threads_warn": 120,
"process_threads_warn": 160,
"process_threads_fail": 220,
# ThreadPool
@ -615,19 +615,6 @@ class ModuloPC(ModuloDiagnosticoBase):
],
))
if st_gpu_mem_load != StatusModulo.OPERANTE:
gpu_cond.append(self._cond(
"Carga da Memória do GPU",
raw["gpu_memory_load_percent"],
80,
"Controlador/memória da GPU sob carga elevada.",
[
"Verificar tamanho dos tensores",
"Reduzir cópias CPU/GPU",
"Avaliar TensorRT/ONNX",
],
))
if gpu_power is not None and gpu_power >= C["gpu_power_warn_w"]:
gpu_cond.append(self._cond(
"Potência do GPU",
@ -654,6 +641,23 @@ class ModuloPC(ModuloDiagnosticoBase):
else StatusModulo.OPERANTE
)
if st_gpu_memory_block != StatusModulo.OPERANTE:
gpu_memory_value = max(
x for x in [gpu_mem_load, gpu_fb_load]
if x is not None
) if gpu_mem_load is not None or gpu_fb_load is not None else None
gpu_cond.append(self._cond(
"Carga da Memória do GPU",
gpu_memory_value,
80,
"Controlador/memória da GPU sob carga elevada.",
[
"Verificar tamanho dos tensores",
"Reduzir cópias CPU/GPU",
"Avaliar TensorRT/ONNX",
],
))
saude_individual.append(self._item(
"gpu_temp",
"Temperatura do GPU",
@ -692,7 +696,7 @@ class ModuloPC(ModuloDiagnosticoBase):
"Carga de Memória/Framebuffer GPU",
st_gpu_memory_block,
sc_gpu_memory_block,
self._motivo("gpu_memory_load", st_gpu_memory_block, min(
self._motivo("gpu_memory_load", st_gpu_memory_block, max(
x for x in [gpu_mem_load, gpu_fb_load] if x is not None
) if gpu_mem_load is not None or gpu_fb_load is not None else None, "%"),
[c for c in gpu_cond if c["label"] == "Carga da Memória do GPU"],
@ -877,29 +881,78 @@ class ModuloPC(ModuloDiagnosticoBase):
)
sc_process = min(sc_proc_mem, sc_proc_threads, sc_tp)
st_process = (
StatusModulo.FALHA
if StatusModulo.FALHA in [st_proc_mem, st_proc_threads, st_tp]
else StatusModulo.ALERTA
if StatusModulo.ALERTA in [st_proc_mem, st_proc_threads, st_tp]
else StatusModulo.OPERANTE
)
status_processos = [st_proc_mem, st_proc_threads, st_tp]
if StatusModulo.FALHA in status_processos:
st_process = StatusModulo.FALHA
elif StatusModulo.ALERTA in status_processos:
st_process = StatusModulo.ALERTA
elif StatusModulo.DESCONECTADO in status_processos:
st_process = StatusModulo.DESCONECTADO
else:
st_process = StatusModulo.OPERANTE
process_cond = []
if st_process != StatusModulo.OPERANTE:
process_motivos = []
if st_proc_mem != StatusModulo.OPERANTE:
process_motivos += self._motivo(
"process_memory",
st_proc_mem,
proc_private,
"MB",
)
process_cond.append(self._cond(
"Processo C#",
"Memória do Processo",
raw["process_private_memory_mb"],
100 if st_process == StatusModulo.FALHA else 70,
"Processo principal com uso elevado de memória, threads ou ThreadPool.",
100 if st_proc_mem == StatusModulo.FALHA else 70,
"Uso elevado de memória privada pelo processo principal.",
[
"Verificar vazamento de memória",
"Revisar criação de threads",
"Verificar tasks longas no C#",
"Verificar crescimento contínuo da memória",
"Verificar retenção de imagens e buffers",
"Verificar objetos gráficos não descartados",
],
))
motivos += self._motivo("process", st_process, proc_private, "MB")
if st_proc_threads != StatusModulo.OPERANTE:
process_motivos += self._motivo(
"process_threads",
st_proc_threads,
proc_threads,
"",
)
process_cond.append(self._cond(
"Threads do Processo",
raw["process_thread_count"],
100 if st_proc_threads == StatusModulo.FALHA else 70,
"Quantidade elevada de threads no processo principal.",
[
"Verificar criação recorrente de threads e timers",
"Verificar ciclos de reconexão de dispositivos",
"Verificar threads que não estão sendo finalizadas",
],
))
if st_tp != StatusModulo.OPERANTE:
process_motivos += self._motivo(
"threadpool_worker",
st_tp,
tp_worker,
"%",
)
process_cond.append(self._cond(
"Uso do ThreadPool",
raw["threadpool_used_worker_percent"],
100 if st_tp == StatusModulo.FALHA else 70,
"Ocupação elevada das threads worker do ThreadPool.",
[
"Verificar tarefas bloqueantes",
"Verificar operações síncronas demoradas",
"Verificar tasks de longa duração",
],
))
motivos += process_motivos
condicoes += process_cond
saude_individual.append(self._item(
@ -907,10 +960,10 @@ class ModuloPC(ModuloDiagnosticoBase):
"Processo Principal",
st_process,
sc_process,
self._motivo("process", st_process, proc_private, "MB"),
process_motivos,
process_cond,
valor=raw["process_private_memory_mb"],
unidade="MB",
valor=None,
unidade="",
))
# ======================================================
@ -979,17 +1032,21 @@ class ModuloPC(ModuloDiagnosticoBase):
st_ram == StatusModulo.FALHA,
st_vram == StatusModulo.FALHA,
st_disk == StatusModulo.FALHA,
st_process == StatusModulo.FALHA,
])
any_alert = any(
item.get("status") == StatusModulo.ALERTA.value
any_problem = any(
item.get("status") in (
StatusModulo.ALERTA.value,
StatusModulo.FALHA.value,
)
for item in saude_individual
)
if hard_fail:
status = StatusModulo.FALHA
score_total = min(score_total, 35.0)
elif score_total < 80.0 or any_alert:
elif score_total < 80.0 or any_problem:
status = StatusModulo.ALERTA
else:
status = StatusModulo.OPERANTE