Ajustes em IMU, IPB, Alertas, Top Bar, Left Bar, Controle, Redis

This commit is contained in:
Diego Freitas 2026-03-19 07:55:56 -03:00
parent 3f55ed4fca
commit a7ac637032
15 changed files with 1233 additions and 886 deletions

View File

@ -170,10 +170,17 @@ namespace AgroBase.Models
{
public double timestamp { get; set; }
public double roll { get; set; }
public double roll_seg { get; set; }
public double pitch { get; set; }
public double pitch_seg { get; set; }
public double yaw { get; set; }
public double yaw_fixed { get; set; }
public double vel_mps { get; set; }
public bool em_movimento { get; set; }
public double movimento_idx { get; set; }
public double rugosidade_idx { get; set; }
public double impacto_idx { get; set; }
public double estabilidade_idx { get; set; }
public double latencia { get; set; }
public double frequencia { get; set; }
}

View File

@ -2407,6 +2407,11 @@ namespace AgroBase.Models
InclinacaoFrontal = imu_iniciado ? sIMU.roll : 0,
Rotacao = imu_iniciado ? sIMU.yaw : 0,
RotacaoCorrigida = imu_iniciado ? sIMU.yaw_fixed : 0,
EmMovimento = imu_iniciado ? sIMU.em_movimento : false,
IndiceMovimento = imu_iniciado ? sIMU.movimento_idx : 0,
IndiceRugosidade = imu_iniciado ? sIMU.rugosidade_idx : 0,
IndiceEstabilidade = imu_iniciado ? sIMU.estabilidade_idx : 0,
IndiceImpacto = imu_iniciado ? sIMU.impacto_idx : 0,
} : new OperacaoSensoriamentoLogImuModel();
var dadosAtuador = _DispAtu != null ? new OperacaoSensoriamentoLogAtuModel()
@ -2693,6 +2698,11 @@ namespace AgroBase.Models
public double InclinacaoFrontal { get; set; }
public double Rotacao { get; set; }
public double RotacaoCorrigida { get; set; }
public bool EmMovimento { get; set; }
public double IndiceMovimento { get; set; }
public double IndiceRugosidade { get; set; }
public double IndiceEstabilidade { get; set; }
public double IndiceImpacto { get; set; }
public OperacaoSensoriamentoLogImuModel Clone()
{
@ -2703,6 +2713,11 @@ namespace AgroBase.Models
InclinacaoFrontal = InclinacaoFrontal,
Rotacao = Rotacao,
RotacaoCorrigida = RotacaoCorrigida,
EmMovimento = EmMovimento,
IndiceMovimento = IndiceMovimento,
IndiceImpacto = IndiceImpacto,
IndiceEstabilidade = IndiceEstabilidade,
IndiceRugosidade = IndiceRugosidade,
};
}
}

View File

@ -402,6 +402,11 @@ namespace AgroBase.Models
public static void MostrarLog(string Mensagem)
{
Console.WriteLine($"[AGROBASE] {Mensagem}");
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,7 @@
import numpy as np
import time
import threading
from collections import deque
from ahrs.filters import Madgwick
from scipy.spatial.transform import Rotation as R
from shared.contexto_global_redis import ContextoGlobalRedis
@ -10,27 +11,54 @@ from health_worker.modulos.base import ModuloDiagnosticoBase
class IMUCamera(ModuloDiagnosticoBase):
def __init__(self, queue=None, freq=100, angulo_inicial=26.3):
self.freq = freq
if queue is None: return
if queue is None:
return
self.imu_queue = queue
self.filtro_imu = Madgwick(beta=0.8, frequency=freq)
self.roll_inicial = 90 - angulo_inicial # graus
# beta bem mais conservador
self.filtro_imu = Madgwick(beta=0.05, frequency=freq)
self.roll_inicial = 90 - angulo_inicial
self.pitch_inicial = 0.0
self.yaw_inicial = 0.0
r = R.from_euler('xyz', [self.roll_inicial, self.pitch_inicial, self.yaw_inicial], degrees=True)
q = r.as_quat() # formato: [x, y, z, w]
self.q = np.array([q[3], q[0], q[1], q[2]]) # reordenando para [w, x, y, z] como espera o filtro
q = r.as_quat()
self.q = np.array([q[3], q[0], q[1], q[2]], dtype=np.float64)
self.imu_em_falha = False
self.tempo_erro = 0
self.v_world = np.zeros(3, dtype=np.float32)
self.last_imu_ts = None
self.v_world = np.zeros(3, dtype=np.float64)
self.g = 9.80665
self.last_loop_ts = None
# filtros de segurança
self.roll_seg = 0.0
self.pitch_seg = 0.0
self.alpha_seg = 0.12
# mediana curta para matar espinhos
self.roll_hist = deque(maxlen=5)
self.pitch_hist = deque(maxlen=5)
self.acc_norm_hist = deque(maxlen=100) # ~1s
self.acc_z_hist = deque(maxlen=100) # ~1s
self.gyro_norm_hist = deque(maxlen=60) # ~0.6s
self.roll_seg_hist = deque(maxlen=60) # ~0.6s
self.pitch_seg_hist = deque(maxlen=60) # ~0.6s
self.impacto_hist = deque(maxlen=20) # ~0.2s para pico recente
self.em_movimento = False
self.movimento_idx = 0.0
self.rugosidade_idx = 0.0
self.impacto_idx = 0.0
self.estabilidade_idx = 100.0
self.last_redis_ts = 0.0
self.redis_period = 0.10 # 100 ms
self.ativo = True
self.imu_thread = threading.Thread(target=self.imu_task_loop, daemon=True)
self.imu_thread.start()
@ -42,48 +70,185 @@ class IMUCamera(ModuloDiagnosticoBase):
self.ativo = False
self.mostrar_log("Task parada")
def _mediana_curta(self, hist, valor):
hist.append(valor)
return float(np.median(hist))
def clamp(self, valor, vmin, vmax):
return max(vmin, min(vmax, valor))
def norm_to_100(self, valor, faixa_min, faixa_max):
if faixa_max <= faixa_min:
return 0.0
x = (valor - faixa_min) / (faixa_max - faixa_min)
return self.clamp(x * 100.0, 0.0, 100.0)
def rms(self, valores):
if not valores:
return 0.0
arr = np.asarray(valores, dtype=np.float64)
return float(np.sqrt(np.mean(arr ** 2)))
def stddev(self, valores):
if not valores:
return 0.0
arr = np.asarray(valores, dtype=np.float64)
return float(np.std(arr))
def _atualizar_indices(self, velocidade_mps, gx, gy, gz, a_lin):
acc_lin_norm = float(np.linalg.norm(a_lin))
gyro_norm_deg = float(np.rad2deg(np.linalg.norm([gx, gy, gz])))
# Históricos
self.acc_norm_hist.append(acc_lin_norm)
self.acc_z_hist.append(float(a_lin[2]))
self.gyro_norm_hist.append(gyro_norm_deg)
self.roll_seg_hist.append(float(self.roll_seg))
self.pitch_seg_hist.append(float(self.pitch_seg))
self.impacto_hist.append(acc_lin_norm)
# ============================
# 1) Flag em movimento
# ============================
vel_ok = velocidade_mps > 0.05
gyro_ok = gyro_norm_deg > 3.0
acc_ok = acc_lin_norm > 0.20
self.em_movimento = bool(vel_ok or gyro_ok or acc_ok)
# ============================
# 2) Movimento idx
# ============================
vel_score = self.norm_to_100(velocidade_mps, 0.0, 1.8)
gyro_score = self.norm_to_100(gyro_norm_deg, 0.0, 40.0)
acc_score = self.norm_to_100(acc_lin_norm, 0.0, 1.5)
movimento_idx = (
0.50 * vel_score +
0.25 * gyro_score +
0.25 * acc_score
)
self.movimento_idx = round(self.clamp(movimento_idx, 0.0, 100.0), 1)
# ============================
# 3) Rugosidade idx
# ============================
rug_z = self.rms(self.acc_z_hist)
rug_total = self.stddev(self.acc_norm_hist)
rug_z_score = self.norm_to_100(rug_z, 0.02, 0.80)
rug_total_score = self.norm_to_100(rug_total, 0.01, 0.60)
rugosidade_idx = (
0.65 * rug_z_score +
0.35 * rug_total_score
)
self.rugosidade_idx = round(self.clamp(rugosidade_idx, 0.0, 100.0), 1)
# ============================
# 4) Impacto idx
# ============================
pico_impacto = max(self.impacto_hist) if self.impacto_hist else 0.0
impacto_bruto = self.norm_to_100(pico_impacto, 0.4, 4.0)
# decaimento suave
self.impacto_idx = max(impacto_bruto, self.impacto_idx * 0.85)
self.impacto_idx = round(self.clamp(self.impacto_idx, 0.0, 100.0), 1)
# ============================
# 5) Estabilidade idx
# ============================
roll_std = self.stddev(self.roll_seg_hist)
pitch_std = self.stddev(self.pitch_seg_hist)
osc_ang = float(np.sqrt(roll_std**2 + pitch_std**2))
osc_score = self.norm_to_100(osc_ang, 0.2, 8.0)
instabilidade = (
0.60 * osc_score +
0.40 * self.rugosidade_idx
)
estabilidade_idx = 100.0 - instabilidade
self.estabilidade_idx = round(self.clamp(estabilidade_idx, 0.0, 100.0), 1)
def imu_task_loop(self):
t0 = time.time()
t0 = time.perf_counter()
while self.ativo:
latencia = 0
latencia = 0.0
try:
agora = time.time()
agora = time.perf_counter()
t_tick = agora - t0
t0 = agora
f_tick = 1.0 / (t_tick + 1e-9)
f_tick = 1.0 / max(t_tick, 1e-6)
imuData = self.imu_queue.tryGet()
if imuData is not None and self.imu_em_falha:
self.mostrar_log("Reconectado com sucesso, voltando ao modo normal")
self.imu_em_falha = False
if imuData is not None:
# dt estimado por pacote
now = time.time()
if self.last_imu_ts is None:
dt = 1.0 / max(1e-3, f_tick) # fallback
if imuData is not None and len(imuData.packets) > 0:
now = time.perf_counter()
if self.last_loop_ts is None:
dt_total = 1.0 / self.freq
else:
dt = max(0.0, now - self.last_imu_ts)
self.last_imu_ts = now
dt = min(dt, 0.05) # clamp anti-bursts (<=50 ms)
self.filtro_imu.Dt = float(dt)
dt_total = max(1e-4, now - self.last_loop_ts)
self.last_loop_ts = now
dt_total = min(dt_total, 0.05)
num_packets = max(1, len(imuData.packets))
dt_packet = max(1e-4, dt_total / num_packets)
for packet in imuData.packets:
accel = packet.acceleroMeter
gyro = packet.gyroscope
ax = accel.x
ay = accel.y
az = accel.z
ax = float(accel.x)
ay = float(accel.y)
az = float(accel.z)
gx = np.deg2rad(gyro.x)
gy = np.deg2rad(gyro.y)
gz = np.deg2rad(gyro.z)
gx = float(np.deg2rad(gyro.x))
gy = float(np.deg2rad(gyro.y))
gz = float(np.deg2rad(gyro.z))
a_body = np.array([ax, ay, az], dtype=np.float64)
acc_norm = np.linalg.norm(a_body)
self.filtro_imu.Dt = float(dt_packet)
# Confia no acelerômetro só quando ele parece representar gravidade
usa_acc = abs(acc_norm - self.g) < 0.20
if usa_acc:
self.q = self.filtro_imu.updateIMU(
q=self.q,
gyr=np.array([gx, gy, gz], dtype=np.float64),
acc=a_body
)
else:
# fallback: mantém última orientação integrando com gyro "na mão"
# caso sua lib não aceite acc=None
omega = np.array([gx, gy, gz], dtype=np.float64)
omega_norm = np.linalg.norm(omega)
if omega_norm > 1e-9:
theta = omega_norm * dt_packet
axis = omega / omega_norm
dq_xyz = axis * np.sin(theta / 2.0)
dq_w = np.cos(theta / 2.0)
dq = np.array([dq_w, dq_xyz[0], dq_xyz[1], dq_xyz[2]], dtype=np.float64)
w1, x1, y1, z1 = self.q
w2, x2, y2, z2 = dq
self.q = np.array([
w1*w2 - x1*x2 - y1*y2 - z1*z2,
w1*x2 + x1*w2 + y1*z2 - z1*y2,
w1*y2 - x1*z2 + y1*w2 + z1*x2,
w1*z2 + x1*y2 - y1*x2 + z1*w2
], dtype=np.float64)
self.q /= max(np.linalg.norm(self.q), 1e-9)
self.q = self.filtro_imu.updateIMU(
q=self.q,
gyr=np.array([gx, gy, gz]),
acc=np.array([ax, ay, az])
)
r = R.from_quat([self.q[1], self.q[2], self.q[3], self.q[0]])
roll, pitch, yaw = r.as_euler('xyz', degrees=True)
@ -91,48 +256,67 @@ class IMUCamera(ModuloDiagnosticoBase):
pitch -= self.pitch_inicial
yaw -= self.yaw_inicial
# mata picos curtíssimos
roll = self._mediana_curta(self.roll_hist, roll)
pitch = self._mediana_curta(self.pitch_hist, pitch)
# filtro leve para segurança
self.roll_seg = (1.0 - self.alpha_seg) * self.roll_seg + self.alpha_seg * roll
self.pitch_seg = (1.0 - self.alpha_seg) * self.pitch_seg + self.alpha_seg * pitch
# velocidade linear
Rwb = r.as_matrix()
a_world = Rwb @ a_body
a_lin = a_world - np.array([0.0, 0.0, self.g], dtype=np.float64)
# 1) aceleração no mundo
a_body = np.array([ax, ay, az], dtype=np.float64)
a_world = Rwb @ a_body
# 2) remove gravidade
a_lin = a_world - np.array([0.0, 0.0, self.g], dtype=np.float64)
# 3) filtra ruído muito baixo (deadzone)
dead = 0.05 # m/s²
dead = 0.08
a_lin[np.abs(a_lin) < dead] = 0.0
# 4) ZUPT (Zero-velocity update) se parado:
# - giros muito baixos e módulo da aceleração ~ g
if (np.linalg.norm([gx, gy, gz]) < np.deg2rad(2.0)
and abs(np.linalg.norm(a_body) - self.g) < 0.12):
# parado: puxe para zero agressivo
self.v_world *= 0.2
else:
# 5) integra velocidade
self.v_world += (a_lin * dt)
parado = (
np.linalg.norm([gx, gy, gz]) < np.deg2rad(1.5) and
abs(np.linalg.norm(a_body) - self.g) < 0.10
)
if parado:
self.v_world *= 0.1
else:
self.v_world += (a_lin * dt_packet)
# 6) projeta no plano do chão (z=0) e computa velocidade escalar
v_planar = self.v_world.copy()
v_planar[2] = 0.0
velocidade_mps = float(np.linalg.norm(v_planar))
t1 = time.time()
latencia = t1 - t0
f_exec = 1.0 / (latencia + 1e-9)
self._atualizar_indices(
velocidade_mps=velocidade_mps,
gx=gx, gy=gy, gz=gz,
a_lin=a_lin
)
t1 = time.perf_counter()
latencia = t1 - agora
t_pub = time.perf_counter()
if (t_pub - self.last_redis_ts) >= self.redis_period:
self.last_redis_ts = t_pub
ContextoGlobalRedis.atualizar_ctx_dict(
ContextoGlobalRedis.ModKey(T_Code.Imu),
roll=round(-roll, 2), # frontal
pitch=round(pitch, 2), # lateral
ContextoGlobalRedis.ModKey(T_Code.Imu),
roll=round(-roll, 2),
pitch=round(pitch, 2),
yaw=round(yaw, 2),
roll_seg=round(-self.roll_seg, 2),
pitch_seg=round(self.pitch_seg, 2),
vel_mps=round(velocidade_mps, 3),
timestamp=(t1 * 1000),
em_movimento=self.em_movimento,
movimento_idx=round(self.movimento_idx, 1),
rugosidade_idx=round(self.rugosidade_idx, 1),
impacto_idx=round(self.impacto_idx, 1),
estabilidade_idx=round(self.estabilidade_idx, 1),
timestamp=(time.time() * 1000),
latencia=latencia,
frequencia=f_tick
)
#self.mostrar_log(f"IMU | Roll: {roll:.2f} | Pitch: {pitch:.2f} | Yaw: {yaw:.2f} | Freq: {f_tick:.2f} Hz | Latência: {latencia:.3f} s")
except Exception as e:
self.mostrar_log(f"Erro no loop: {e}")
if not self.imu_em_falha:
@ -143,14 +327,12 @@ class IMUCamera(ModuloDiagnosticoBase):
self.ativo = False
finally:
if self.imu_em_falha:
# Espera mais tempo pra evitar flood de erro
time.sleep(5.0)
else:
delay_corrigido = max(0, (1.0 / self.freq) - latencia)
delay_corrigido = max(0.0, (1.0 / self.freq) - latencia)
time.sleep(delay_corrigido)
def atualizar_saude(self):
#self.mostrar_log("Atualizando saude")
try:
modulo = ContextoGlobalRedis.get_modulo(T_Code.Imu)
FREQ_BASE = self.freq
@ -178,7 +360,6 @@ class IMUCamera(ModuloDiagnosticoBase):
if p > 0:
motivos.append(f"Frequência abaixo do ideal: {freq_hz:.2f} Hz (penalidade {p}%)")
# Penaliza se latência está alta (> 0.5s)
if latencia > 0.5:
p = int(min(latencia * 100, 30))
saude -= p
@ -210,4 +391,4 @@ class IMUCamera(ModuloDiagnosticoBase):
self.mostrar_log(f"Erro ao atualizar saude: {e}")
def mostrar_log(self, mensagem):
print(f"{time.time()} - [IMU] {mensagem}")
print(f"{time.time()} - [IMU] {mensagem}")

View File

@ -4,6 +4,7 @@ import time
import threading
import statistics
import socket
import numpy as np
import psutil
import subprocess
from concurrent.futures import ThreadPoolExecutor
@ -82,6 +83,8 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
self._ping_timeout_ms = 400 # recomendado: 300500
self._ping_count = 2 # recomendado: 1 (janela já suaviza)
self._ping_min_interval = 0.6 # não precisa pingar a cada 100ms
self._hb_last_rx_monotonic = 0.0
self._last_ping_sample_applied_ts = 0.0
self.bw_max_mbps = 10.0 # capacidade aproximada do link (ajuste por teste)
self.bw_safe_mbps = 05.0 # alvo saudável (até aqui não penaliza)
@ -366,6 +369,7 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
now = time.time()
with self._mqtt_lock:
self.last_heartbeat_ts = now
self._hb_last_rx_monotonic = time.perf_counter()
self._heartbeat_ok = True
self._mqtt_mostra_log(f"[client={cid}] heartbeat recebido | t={now:.3f}")
@ -449,45 +453,6 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
self.last_bw_counters = counters
self.last_bw_ts = now
def _ping_once_old(self, pings=5, timeout=500):
"""
Retorna RTT em ms se ok, ou None se timeout.
Aqui exemplo pra Windows usando 'ping -n 1'.
Ajuste pro teu ambiente se precisar.
"""
try:
base_ip = self.get_base_ip()
if base_ip is None:
return None, None
# timeout de 1000 ms
proc = subprocess.run(
["ping", "-n", f"{pings}", "-w", f"{timeout}", base_ip],
capture_output=True, text=True
)
if proc.returncode != 0:
print("timeout")
return None, 100
print(proc.stdout)
perda = None
media_tempo = None
for line in proc.stdout.splitlines():
line = line.lower()
if ("%" in line and perda is None):
perda = line.split('(')[1].split('%')[0]
perda = float(perda) if perda.isdigit() else None
elif (" = " in line and "ms" in line and media_tempo is None):
media_tempo = line.split(' = ')[3].split('ms')[0]
media_tempo = float(media_tempo) if media_tempo.isdigit() else None
if perda is not None and media_tempo is not None:
break
return media_tempo, perda
except Exception:
print("erro timeout")
return None, 100
def _ping_once(self, base_ip: str, pings: int, timeout_ms: int):
"""
Retorna (ok: bool, avg_rtt_ms: float|None, loss_pct: float)
@ -771,24 +736,19 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
# 3) usa snapshot pra alimentar suas janelas rtts/loses/timeouts
ok, rtt, loss, ping_ts = self._get_ping_snapshot()
#print(f"ok: {ok}, rtt: {rtt}, loss: {loss}, ping_ts: {ping_ts}")
self.timeouts_f.append(loss == 100)
self.timeouts_s.append(loss == 100)
if ping_ts > self._last_ping_sample_applied_ts:
self._last_ping_sample_applied_ts = ping_ts
if ok and rtt is not None:
self.rtts_f.append(rtt)
self.rtts_s.append(rtt)
self.timeouts_f.append(loss == 100)
self.timeouts_s.append(loss == 100)
if loss is not None:
self.loses_f.append(loss)
self.loses_s.append(loss)
if ok and rtt is not None:
self.rtts_f.append(rtt)
self.rtts_s.append(rtt)
#(rtt, loss) = self._ping_once_old(pings=3, timeout=200)
#print(f"rtt: {rtt}, loss: {loss}")
#self.timeouts.append(loss == 100)
#if rtt is not None:
# self.rtts.append(rtt)
#if loss is not None:
# self.loses.append(loss)
if loss is not None:
self.loses_f.append(loss)
self.loses_s.append(loss)
self._update_nic_errors()
self._update_nic_bandwidth()
@ -1118,21 +1078,21 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
risk_score = max(0.0, min(100.0, risk_score))
degradado_agora = (
loss_pct_s >= 5 or
tmout_pct_s >= 4 or
atraso_s >= 4.0 or
(loss_pct_f >= 8 and loss_pct_s >= 3) or
(tmout_pct_f >= 10 and tmout_pct_s >= 3) or
(jitter_s >= 35 and loss_pct_s >= 3)
loss_pct_s >= 7 or
tmout_pct_s >= 6 or
atraso_s >= 5.0 or
(loss_pct_f >= 12 and loss_pct_s >= 4) or
(tmout_pct_f >= 15 and tmout_pct_s >= 4) or
(jitter_s >= 45 and loss_pct_s >= 4)
)
critico_agora = (
loss_pct_s >= 10 or
tmout_pct_s >= 8 or
atraso_s >= 6.0 or
(loss_pct_f >= 15 and loss_pct_s >= 5) or
(tmout_pct_f >= 20 and tmout_pct_s >= 5) or
((loss_pct_s >= 6) and (atraso_s >= 5.0))
loss_pct_s >= 15 or
tmout_pct_s >= 12 or
atraso_s >= 8.0 or
(loss_pct_f >= 20 and loss_pct_s >= 6) or
(tmout_pct_f >= 25 and tmout_pct_s >= 6) or
((loss_pct_s >= 8) and (atraso_s >= 6.0))
)
colapso_agora = (
@ -1216,11 +1176,12 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
# Condição 2: Cerca invisível de rede violada
dist_violando_cerca = (
self.link_state == "DEGRADED" and
self._cycles_degraded >= 15 and
risk_score >= 70 and
self._distancia_inicio_degradado is not None and
distancia_base is not None and
distancia_base >= 0 and
self._max_distancia_desde_degradado > self._distancia_inicio_degradado + 2.0 and
distancia_base > self._distancia_inicio_degradado + 1.0
distancia_base > self._distancia_inicio_degradado + 5.0
)
if dist_violando_cerca:
condicoes.append({
@ -1338,3 +1299,5 @@ class ModuloIPBribge(ModuloDiagnosticoBase):
)
except Exception as e:
print(f"Erro ao atualizar saude do modulo {self.t_code.name}: {e}")

View File

@ -37,7 +37,7 @@ def verifica_controle_liberado(ignorar_parada=False):
_equipamento = ContextoGlobalRedis.get_equipamento()
ang_roll_max = _equipamento.get("angulo_roll_max", 15.0)
ang_pitch_max = _equipamento.get("angulo_pitch_max", 30.0)
inclinacao_perigosa = abs(_imu.get("pitch", 0.0)) > ang_pitch_max or abs(_imu.get("roll", 0.0)) > ang_roll_max
inclinacao_perigosa = abs(_imu.get("pitch_seg", 0.0)) > ang_pitch_max or abs(_imu.get("roll_seg", 0.0)) > ang_roll_max
if not imu_operante:
motivos.append(f"IMU mandatório não operante: {_imu_saude.name}")

View File

@ -129,6 +129,27 @@ namespace OperationControl.Models
Angulo = 0,
PercentualVelocidadeSPKmh = 0,
TipoMovimento = TipoMovimentoDirecional.RodasDianteiras
},
Gnss = new AgroBase.Models.Operacoes.OperacaoParametrosDadosGNSSModel()
{
Latitude = -22.17254631649402,
Longitude = -47.395203906184044,
OrientacaoReal = 187
},
ModulosSaude = new List<AgroBase.Models.Operadores.ManagerWorkerMessageResponseModulosPendentesModel>()
{
new AgroBase.Models.Operadores.ManagerWorkerMessageResponseModulosPendentesModel()
{
modulo = T_Code.Gps,
saude = 0,
status = StatusModulo.Desconectado,
},
new AgroBase.Models.Operadores.ManagerWorkerMessageResponseModulosPendentesModel()
{
modulo = T_Code.Can,
saude = 70,
status = StatusModulo.Alerta,
},
}
};
r.Controle = new AgroBase.Models.Operacoes.OperacaoParametrosControleModel()

View File

@ -366,7 +366,7 @@ namespace OperationControl.Models
return Variaveis.UdpChannel.SendBurstAsync(0x01, msg.ToBytes(), count: 6, intervalMs: 25, requestAck: false);
}
public static void EnviarComandoReferenciamento(string mod_id = null)
public static void EnviarComandoReferenciamento(string? mod_id = null)
{
EnviarDadosControle(SelectedRoverId, new OperacaoComandoBaseModel()
{

View File

@ -54,6 +54,10 @@ namespace OperationControl.ViewModels.Views.Operacao.Monitoramento
LimparTeclas();
EnviarNeutros();
}
else
{
VariaveisControleOperacao.EnviarComandoIniciarUDP(VariaveisControleOperacao.RoverEmFoco.IP, true);
}
}
}
}

View File

@ -750,8 +750,7 @@ namespace OperationControl.ViewModels.Views.Operacao.Monitoramento.Diagnostico
AddOrAppendSerie("Jitter", "ms", conexao?.detalhes?["jitter"]?.Value<double?>());
AddOrAppendSerie("Timeout", "%", conexao?.detalhes?["tmout_pct"]?.Value<double?>());
AddOrAppendSerie("Perda", "%", conexao?.detalhes?["loss_pct"]?.Value<double?>());
AddOrAppendSerie("Banda", "Mbps", conexao?.detalhes?["bw_total_mbps"]?.Value<double?>());
AddOrAppendSerie("Banda %", "%", conexao?.detalhes?["bw_util_pct"]?.Value<double?>());
AddOrAppendSerie("Banda", "%", conexao?.detalhes?["bw_util_pct"]?.Value<double?>());
break;
case T_Code.Gps:
@ -768,6 +767,14 @@ namespace OperationControl.ViewModels.Views.Operacao.Monitoramento.Diagnostico
AddOrAppendSerie("Corrente", "A", bat?.CorrenteInstantanea);
AddOrAppendSerie("Temperatura", "°C", bat?.Temperatura);
break;
case T_Code.Imu:
var imu = rover.Imu;
AddOrAppendSerie("Movimento", "", imu?.IndiceMovimento);
AddOrAppendSerie("Estabilidade", "", imu?.IndiceEstabilidade);
AddOrAppendSerie("Rugosidade", "", imu?.IndiceRugosidade);
AddOrAppendSerie("Impacto", "", imu?.IndiceImpacto);
break;
}
}

View File

@ -104,6 +104,7 @@ namespace OperationControl.ViewModels.Views.Operacao.Monitoramento
public event Action<TipoFrameCamera>? CameraErvasFrameTipoAlterado;
public ICommand RetornoBaseCommand { get; }
public ICommand ReferenciamentoCommand { get; }
public MonitoramentoViewModel()
{
@ -123,6 +124,7 @@ namespace OperationControl.ViewModels.Views.Operacao.Monitoramento
CameraErvasFrameTipoSelecionado = CameraErvasFrameTipos.FirstOrDefault();
RetornoBaseCommand = new RelayCommand(_ => ExecutarRetornoBase(), _ => PodeExecutarRetornoBase());
ReferenciamentoCommand = new RelayCommand(_ => ExecutarReferenciamento(), _ => PodeExecutarReferenciamento());
}
public void AtualizarDadosCameras(CameraWorkerItemModel? snr, CameraWorkerItemModel? cam, StatusCarroMapa? status_carro, double? pct_ervas)
@ -165,6 +167,19 @@ namespace OperationControl.ViewModels.Views.Operacao.Monitoramento
return true;
}
private void ExecutarReferenciamento()
{
if (System.Windows.MessageBox.Show("Deseja executar o referenciamento do equipamento?", "Referenciamento", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
{
VariaveisControleOperacao.EnviarComandoReferenciamento();
}
}
private bool PodeExecutarReferenciamento()
{
return true;
}
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)

View File

@ -209,15 +209,20 @@ namespace OperationControl.ViewModels.Views.Operacao
if (ind.status == StatusModulo.Operante)
continue;
alertasEsperados.Add(new AlertaModel
bool mandatorio = ind.em_uso;
if (mandatorio)
{
Rover_ID = rover.RoverId,
Modulo = mod.modulo,
Mod_ID = ind.label,
Severidade = DeParaStatus(ind.status, ind.em_uso),
Mensagem = string.Join(", ", ind.motivos ?? new List<string>())
// NÃO define Timestamp aqui ainda
});
alertasEsperados.Add(new AlertaModel
{
Rover_ID = rover.RoverId,
Modulo = mod.modulo,
Mod_ID = ind.label,
Severidade = DeParaStatus(ind.status, mandatorio),
Mensagem = string.Join(", ", ind.motivos ?? new List<string>())
// NÃO define Timestamp aqui ainda
});
}
}
}
else
@ -226,17 +231,20 @@ namespace OperationControl.ViewModels.Views.Operacao
continue;
bool mandatorio = rover.ModulosMandatorios?.Any(x =>
x.Dispositivo == mod.modulo && x.Mandatorio) ?? false;
x.Dispositivo == mod.modulo && x.Utilizar) ?? false;
alertasEsperados.Add(new AlertaModel
if (mandatorio)
{
Rover_ID = rover.RoverId,
Modulo = mod.modulo,
Mod_ID = null,
Severidade = DeParaStatus(mod.status, mandatorio),
Mensagem = string.Join(", ", mod.motivos ?? new List<string>())
// NÃO define Timestamp aqui ainda
});
alertasEsperados.Add(new AlertaModel
{
Rover_ID = rover.RoverId,
Modulo = mod.modulo,
Mod_ID = null,
Severidade = DeParaStatus(mod.status, mandatorio),
Mensagem = string.Join(", ", mod.motivos ?? new List<string>() { "Motivo desconhecido" })
// NÃO define Timestamp aqui ainda
});
}
}
}
}

View File

@ -8,6 +8,7 @@ using Brush = System.Windows.Media.Brush;
using Brushes = System.Windows.Media.Brushes;
using SolidColorBrush = System.Windows.Media.SolidColorBrush;
using Color = System.Windows.Media.Color;
using AgroBase.Models.Operacoes;
namespace OperationControl.ViewModels.Views.Operacao
{
@ -191,6 +192,21 @@ namespace OperationControl.ViewModels.Views.Operacao
}
}
private string _distanciaBase = "--";
public string DistanciaBase
{
get => DistanciaOuTracos(_distanciaBase);
set
{
if (_distanciaBase != value)
{
_distanciaBase = value;
OnPropertyChanged(nameof(DistanciaBase));
}
}
}
public Brush CorTemperatura =>
!RoverSelecionado ? Brushes.Gray :
_temperaturaNumerica >= 55 ? Brushes.OrangeRed :
@ -217,9 +233,19 @@ namespace OperationControl.ViewModels.Views.Operacao
!RoverSelecionado ? Brushes.Gray :
CorPorTexto(_saudeGnss);
public Brush CorDistBase =>
!RoverSelecionado ? Brushes.Gray :
_distanciaBaseNumerico <= 50 ? Brushes.LimeGreen :
_distanciaBaseNumerico <= 150 ? Brushes.Green :
_distanciaBaseNumerico <= 400 ? Brushes.YellowGreen :
_distanciaBaseNumerico <= 800 ? Brushes.Gold :
_distanciaBaseNumerico <= 1200 ? Brushes.OrangeRed :
Brushes.DarkRed;
private double _temperaturaNumerica;
private double _bateriaNumerica;
private double _reservatorioNumerico;
private double _distanciaBaseNumerico;
public OperacaoLeftViewModel()
{
@ -237,6 +263,7 @@ namespace OperationControl.ViewModels.Views.Operacao
_temperaturaNumerica = 0;
_bateriaNumerica = 0;
_reservatorioNumerico = 0;
_distanciaBaseNumerico = 0;
Velocidade = "--";
AnguloDirecional = "--";
@ -246,31 +273,35 @@ namespace OperationControl.ViewModels.Views.Operacao
Reservatorio = "--";
SaudeRede = "--";
SaudeGnss = "--";
DistanciaBase = "--";
OnPropertyChanged(nameof(CorTemperatura));
OnPropertyChanged(nameof(CorBateria));
OnPropertyChanged(nameof(CorReservatorio));
OnPropertyChanged(nameof(CorRede));
OnPropertyChanged(nameof(CorGnss));
OnPropertyChanged(nameof(CorDistBase));
}
public void AtualizarRover(string nomeRover, double velocidadeKmh, double anguloDirecionalGraus, TipoMovimentoDirecional tipoMovimento, double temperaturaC, double bateriaPct, double reservatorioPct, string saudeRede, string saudeGnss, bool op_iniciada)
public void AtualizarRover(string descricao, OperacaoParametrosDadosControleModel? controle, double temperaturaC, double bateriaPct, double reservatorioPct, string saudeRede, string saudeGnss, double distanciaBase, bool op_iniciada)
{
RoverSelecionado = true;
NomeRover = nomeRover;
NomeRover = descricao;
_temperaturaNumerica = temperaturaC;
_bateriaNumerica = bateriaPct;
_reservatorioNumerico = reservatorioPct;
_distanciaBaseNumerico = distanciaBase;
Velocidade = velocidadeKmh.ToString("0.0");
AnguloDirecional = anguloDirecionalGraus.ToString("0.0");
TipoMovimento = tipoMovimento;
Velocidade = $"{controle?.PercentualVelocidadeSPKmh ?? 0:F2}";
AnguloDirecional = $"{controle?.Angulo ?? 0:F1}";
TipoMovimento = controle?.TipoMovimento ?? TipoMovimentoDirecional.RodasDianteiras;
Temperatura = temperaturaC.ToString("0.0");
Bateria = bateriaPct.ToString("0");
Reservatorio = reservatorioPct.ToString("0");
SaudeRede = saudeRede;
SaudeGnss = saudeGnss;
DistanciaBase = $"{distanciaBase:F2}";
OperacaoEmExecucao = op_iniciada;
@ -279,6 +310,7 @@ namespace OperationControl.ViewModels.Views.Operacao
OnPropertyChanged(nameof(CorReservatorio));
OnPropertyChanged(nameof(CorRede));
OnPropertyChanged(nameof(CorGnss));
OnPropertyChanged(nameof(CorDistBase));
OnPropertyChanged(nameof(OperacaoEmExecucao));
OnPropertyChanged(nameof(TextoBotaoOperacao));
OnPropertyChanged(nameof(CorBotaoOperacao));
@ -331,6 +363,7 @@ namespace OperationControl.ViewModels.Views.Operacao
private string VelocidadeValor(string valor) => RoverSelecionado ? $"{valor} km/h" : "--";
private string TemperaturaValor(string valor) => RoverSelecionado ? $"{valor} °C" : "--";
private string PercentualOuTracos(string valor) => RoverSelecionado ? $"{valor}%" : "--";
private string DistanciaOuTracos(string valor) => RoverSelecionado ? $"{valor} m" : "--";
private Brush CorPorTexto(string valor)
{

View File

@ -372,7 +372,7 @@ namespace OperationControl.ViewModels
var eventos = new List<MotivoTopBarModel>();
double tempoSemResposta = (DateTime.Now - rover.UltimoContato).TotalSeconds;
bool semComunicacao = tempoSemResposta >= VariaveisControleOperacao.TempoRoverVivo;
bool semComunicacao = !AppShell.Mock && tempoSemResposta >= VariaveisControleOperacao.TempoRoverVivo;
if (semComunicacao)
{
@ -394,7 +394,7 @@ namespace OperationControl.ViewModels
eventos.Add(new MotivoTopBarModel
{
Prioridade = 1100,
Fonte = "Núcleo Central de Processamento",
Fonte = "NCP",
Titulo = "NÚCLEO CENTRAL DE PROCESSAMENTO DESCONECTADO",
Descricao = "O núcleo central de processamento foi desconectado, tentando reconectar...",
StatusVisual = StatusModulo.Falha
@ -475,29 +475,68 @@ namespace OperationControl.ViewModels
// 5) Módulos com condições operacionais críticas
// ============================
var modulosCriticos = obj.ModulosSaude?
.Where(mod => mod?.condicoes_operacionais?.Any(cond => cond?.severidade > 90) ?? false)
.Where(mod =>
(mod?.condicoes_operacionais?.Any(cond => cond?.severidade > 90) ?? false) ||
(
!new List<StatusModulo>() { StatusModulo.Operante, StatusModulo.Alerta }.Contains(mod?.status ?? StatusModulo.Desconectado) &&
(rover?.ModulosMandatorios?.Any(x =>
(x?.Dispositivo ?? T_Code.Vzo) == (mod?.modulo ?? T_Code.Vzo) &&
(x?.Utilizar ?? false) &&
(x?.Mandatorio ?? false)
) ?? false)
)
)
.ToList() ?? new List<AgroBase.Models.Operadores.ManagerWorkerMessageResponseModulosPendentesModel>();
foreach (var modulo in modulosCriticos)
{
var nomeModulo = (modulo?.modulo ?? T_Code.Vzo).ToString();
var descricoesCriticas = modulo?.condicoes_operacionais?
.Where(cond => cond?.severidade > 90)
.Select(cond => cond?.descricao ?? "")
.Where(desc => !string.IsNullOrWhiteSpace(desc))
.Distinct()
.ToList() ?? new List<string>();
bool temCondicaoSevera = modulo?.condicoes_operacionais?.Any(cond => cond?.severidade > 90) ?? false;
bool moduloMandatorioNaoOperante =
!new List<StatusModulo>() { StatusModulo.Operante, StatusModulo.Alerta }.Contains(modulo?.status ?? StatusModulo.Desconectado) &&
(rover?.ModulosMandatorios?.Any(x =>
(x?.Dispositivo ?? T_Code.Vzo) == (modulo?.modulo ?? T_Code.Vzo) &&
(x?.Utilizar ?? false) &&
(x?.Mandatorio ?? false)
) ?? false);
foreach (var desc in descricoesCriticas)
// Caso 1: condições operacionais severas
if (temCondicaoSevera)
{
var descricoesCriticas = modulo?.condicoes_operacionais?
.Where(cond => cond?.severidade > 90)
.Select(cond => cond?.descricao ?? "")
.Where(desc => !string.IsNullOrWhiteSpace(desc))
.Distinct()
.ToList() ?? new List<string>();
foreach (var desc in descricoesCriticas)
{
eventos.Add(new MotivoTopBarModel
{
Prioridade = 600,
Fonte = "ModuloCritico",
Titulo = "CONDIÇÕES OPERACIONAIS CRÍTICAS",
Descricao = $"{nomeModulo}: {desc}",
StatusVisual = StatusModulo.Alerta,
Modulo = modulo?.modulo
});
}
}
// Caso 2: módulo mandatório não operante
if (moduloMandatorioNaoOperante)
{
var statusTexto = (modulo?.status ?? StatusModulo.Desconectado).ToString();
eventos.Add(new MotivoTopBarModel
{
Prioridade = 600,
Prioridade = 610,
Fonte = "ModuloCritico",
Titulo = "CONDIÇÕES OPERACIONAIS CRÍTICAS",
Descricao = $"{nomeModulo}: {desc}",
StatusVisual = StatusModulo.Alerta,
Titulo = "MÓDULO MANDATÓRIO NÃO OPERACIONAL",
Descricao = $"{nomeModulo}: módulo mandatório em estado '{statusTexto}'",
StatusVisual = StatusModulo.Falha,
Modulo = modulo?.modulo
});
}
@ -507,9 +546,11 @@ namespace OperationControl.ViewModels
{
{ T_Code.Ipb, 1 },
{ T_Code.Npc, 2 },
{ T_Code.Bat, 3 },
{ T_Code.Atu, 4 },
{ T_Code.Sen, 5 }
{ T_Code.Can, 3 },
{ T_Code.Bat, 4 },
{ T_Code.Gps, 5 },
{ T_Code.Atu, 6 },
{ T_Code.Sen, 7 }
};
// ============================
@ -590,7 +631,7 @@ namespace OperationControl.ViewModels
if (string.IsNullOrWhiteSpace(linha2))
linha2 = "Sem detalhes adicionais.";
Variaveis.MostrarLog($"LINHA 1: {linha1}");
Models.Variaveis.MostrarLog($"LINHA 1: {linha1}");
// ============================
// 8) Atualiza UI
@ -619,7 +660,7 @@ namespace OperationControl.ViewModels
public void AtualizarDadosGnss(AgroBase.Models.GPSModel dados)
{
var bf = Variaveis.GpsService.BaseFix;
var bf = Models.Variaveis.GpsService.BaseFix;
if (bf.CorrecaoEmAndamento)
{
AtualizarProgressoFixacaoBase(bf.Progresso, bf.ProgressoStr);
@ -805,7 +846,7 @@ namespace OperationControl.ViewModels
// ESQUERDA
var conexao = obj.ModulosSaude?.FirstOrDefault(x => x.modulo == T_Code.Ipb);
double c = conexao?.saude ?? 0;
double saude_rede = conexao?.saude ?? 0;
double conexao_latencia = conexao?.detalhes?["avg_rtt"]?.Value<double?>() ?? 0.0;
double conexao_perda = conexao?.detalhes?["loss_pct"]?.Value<double?>() ?? 0.0;
double bwTot = conexao?.detalhes?["bw_total_mbps"]?.Value<double?>() ?? 0.0;
@ -814,14 +855,13 @@ namespace OperationControl.ViewModels
_viewOperacaoLeft?._vm?.AtualizarRover(
rover?.Descricao ?? "",
obj.Controle?.PercentualVelocidadeSPKmh ?? 0,
obj.Controle?.Angulo ?? 0,
obj.Controle?.TipoMovimento ?? TipoMovimentoDirecional.Diagnostico,
obj.Controle,
obj.Refrigeracao?.Temperatura ?? 0,
obj.Bateria?.PercentualBateria ?? 0,
obj.Atuador?.PercentualReservatorio ?? 0,
$"{c}%",
$"{saude_rede}%",
$"{obj.Gnss?.QualidadeFix ?? TiposCorrecaoGPS.SemCorrecao} {obj.Gnss?.PrecisaoCm:F2} cm",
AgroBase.Models.GPSUtils.DistanciaEntrePontos(new AgroBase.Models.GPSModel() { Latitude = obj.Gnss?.Latitude ?? 0, Longitude = obj.Gnss?.Longitude ?? 0 }, Models.Variaveis.GpsService.UltimaLeitura),
obj.Operacao?.Iniciada ?? false
);