ajustes nas cameras, imu, implementado heading e ajustes no mapa

This commit is contained in:
Diego Freitas 2026-03-12 17:03:24 -03:00
parent 7f39aaecad
commit a3590bd224
25 changed files with 690 additions and 942 deletions

View File

@ -2359,13 +2359,14 @@ namespace AgroBase.Models
//var sIMU = LivoxManagerProcess.DadosLeitura.imu; //var sIMU = LivoxManagerProcess.DadosLeitura.imu;
string imu = RedisService.Get(RedisService.ModKey(T_Code.Imu)) ?? ""; string imu = RedisService.Get(RedisService.ModKey(T_Code.Imu)) ?? "";
var sIMU = JsonConvert.DeserializeObject<OAKImuModel>(imu); var sIMU = JsonConvert.DeserializeObject<OAKImuModel>(imu);
bool imu_iniciado = sIMU.timestamp > 0 && HealthWorkerService.ModulosSaude.FirstOrDefault(x => x.modulo == T_Code.Imu)?.status != StatusModulo.Desconectado;
var dadosIMU = sIMU != null ? new OperacaoSensoriamentoLogImuModel() var dadosIMU = sIMU != null ? new OperacaoSensoriamentoLogImuModel()
{ {
Iniciado = sIMU.timestamp > 0 && HealthWorkerService.ModulosSaude.FirstOrDefault(x => x.modulo == T_Code.Imu)?.status != StatusModulo.Desconectado, Iniciado = imu_iniciado,
InclinacaoLateral = sIMU.roll, InclinacaoLateral = imu_iniciado ? sIMU.pitch : 0,
InclinacaoFrontal = sIMU.pitch, InclinacaoFrontal = imu_iniciado ? sIMU.roll : 0,
Rotacao = sIMU.yaw, Rotacao = imu_iniciado ? sIMU.yaw : 0,
RotacaoCorrigida = sIMU.yaw_fixed, RotacaoCorrigida = imu_iniciado ? sIMU.yaw_fixed : 0,
} : new OperacaoSensoriamentoLogImuModel(); } : new OperacaoSensoriamentoLogImuModel();
var dadosAtuador = _DispAtu != null ? new OperacaoSensoriamentoLogAtuModel() var dadosAtuador = _DispAtu != null ? new OperacaoSensoriamentoLogAtuModel()

View File

@ -768,6 +768,7 @@ namespace AgroBase.Models.Operacoes
}; };
PerformanceNcp = sen?.DadosPerformance?.Clone(); PerformanceNcp = sen?.DadosPerformance?.Clone();
LivoxLidar = sen?.LivoxLidar?.Clone(); LivoxLidar = sen?.LivoxLidar?.Clone();
CameraWorkerService.GetListaCameras();
Cameras = CameraWorkerService.ListaCameras; Cameras = CameraWorkerService.ListaCameras;
OperadorVisual = VisualWorkerService.DadosLeitura?.Resumo?.UpdateClone(); OperadorVisual = VisualWorkerService.DadosLeitura?.Resumo?.UpdateClone();
var l = sen?.Logs; var l = sen?.Logs;

View File

@ -47,7 +47,8 @@ namespace AgroBase.Models.Operadores
public double fps { get; set; } public double fps { get; set; }
public int stream_w { get; set; } public int stream_w { get; set; }
public int stream_h { get; set; } public int stream_h { get; set; }
public int stream_kbps { get; set; } public double stream_kbps { get; set; }
public double stream_last_frame { get; set; }
public SaudeWorkerModel saude { get; set; } public SaudeWorkerModel saude { get; set; }
public CameraWorkerItemPerformanceModel performance { get; set; } public CameraWorkerItemPerformanceModel performance { get; set; }

View File

@ -2409,7 +2409,7 @@ namespace AgroBase.Models
public double DistanciaPercorridaTotal { get; private set; } = 0; public double DistanciaPercorridaTotal { get; private set; } = 0;
public void AtualizarDistancias(double distancia) public void AtualizarDistancias(double distancia)
{ {
DistanciaTotal += distancia; DistanciaPercorridaTotal += distancia;
if (Dentro) if (Dentro)
{ {
DistanciaPercorridaCorredor += distancia; DistanciaPercorridaCorredor += distancia;

View File

@ -732,6 +732,9 @@ namespace AgroBase.Models
double dx = velocidadeMs * tempoDelta * Math.Sin(heading); // Leste(+) double dx = velocidadeMs * tempoDelta * Math.Sin(heading); // Leste(+)
double dy = velocidadeMs * tempoDelta * Math.Cos(heading); // Norte(+) double dy = velocidadeMs * tempoDelta * Math.Cos(heading); // Norte(+)
GPSModel ultimaPosicao = GPSService.historicoPosicao.Peek();
if (pos == null) return ultimaPosicao;
// geo // geo
double R_earth = GPSUtils.RaioDaTerra; double R_earth = GPSUtils.RaioDaTerra;
double dLat = (dy / R_earth) * 180.0 / Math.PI; double dLat = (dy / R_earth) * 180.0 / Math.PI;
@ -740,7 +743,6 @@ namespace AgroBase.Models
double latitude = pos.Latitude + dLat; double latitude = pos.Latitude + dLat;
double longitude = pos.Longitude + dLon; double longitude = pos.Longitude + dLon;
GPSModel ultimaPosicao = GPSService.historicoPosicao.Peek();
GPSModel novaPosicao = new GPSModel GPSModel novaPosicao = new GPSModel
{ {
Momento = DateTime.Now, Momento = DateTime.Now,

View File

@ -1,29 +1,17 @@
import numpy as np import numpy as np
import time import time
import socket
import struct
from camera_worker.tcp_streamer import CameraTcpStreamer
from camera_worker.raw_segformer_service import make_bgr_preview_from_raw from camera_worker.raw_segformer_service import make_bgr_preview_from_raw
from shared.utils import resize_frame
from shared.enums import StatusModulo, T_Code from shared.enums import StatusModulo, T_Code
from shared.contexto_global_redis import ContextoGlobalRedis from shared.contexto_global_redis import ContextoGlobalRedis
from camera_worker.gal_service import GalService from camera_worker.gal_service import GalService
import subprocess
import cv2
GST_LAUNCH = r"C:\Program Files\gstreamer\1.0\msvc_x86_64\bin\gst-launch-1.0.exe"
class CameraGal: class CameraGal:
def __init__(self, mostrar_log, mx_id, raw_w=None, raw_h=None): def __init__(self, mostrar_log, mx_id, raw_w=None, raw_h=None):
self.mx_id = mx_id self.mx_id = mx_id
self.mostrar_log = mostrar_log self.mostrar_log = mostrar_log
self.stream = None
self.gst_proc = None
self.gst_FPS = 5
self.gst_WIDTH = 480
self.gst_HEIGHT = 270
self.gst_BIT_RATE = 300
self._ultimo_envio_gst = 0.0
self.dispositivo = T_Code.Vzo self.dispositivo = T_Code.Vzo
self.modelo = "Desconhecido" self.modelo = "Desconhecido"
@ -56,58 +44,19 @@ class CameraGal:
self.modelo = info.get("model") self.modelo = info.get("model")
self.versao = info.get("version_word") self.versao = info.get("version_word")
_camera = ContextoGlobalRedis.get_camera(self.mx_id) or {} ContextoGlobalRedis.atualizar_ctx_dict(
_camera["mx_id"] = self.mx_id ContextoGlobalRedis.CamKey(self.mx_id),
_camera["versao"] = self.versao mx_id=self.mx_id,
_camera["iniciando"] = True versao=self.versao,
_camera["iniciado"] = False iniciando=True,
_camera["rodando"] = False iniciado=False,
_camera["parametros"] = {} rodando=False,
parametros={},
# Criar processo para transmissao de video modelo=self.modelo,
try: dispositivo=self.dispositivo.value,
self._sock = None tem_depth=False,
self._sock_conectado = False tem_imu=False,
self._sock_ultima_tentativa_conexao = 0.0 )
self._sock_intervalo_reconexao = 5.0 # segundos entre tentativas
self._op_mode = 3
self._op_fps = 5
self._last_frame_sent_ts = 0.0
self._last_mode_change_ts = 0.0
if False and _ip is not None and _porta is not None:
self.mostrar_log(f"Iniciando GStreamer para {_ip}:{_porta}...")
gst_cmd = [
GST_LAUNCH,
"fdsrc", "fd=0",
#"!", "queue", "leaky=2", "max-size-buffers=1", # dropa frames antigos
"!", "videoparse",
f"width={self.gst_WIDTH}", f"height={self.gst_HEIGHT}",
"format=bgr",
f"framerate={self.gst_FPS}/1",
"!", "videoconvert",
"!", "videoscale",
"!", f"video/x-raw,width={self.gst_WIDTH},height={self.gst_HEIGHT}",
"!", "x264enc", "tune=zerolatency", "speed-preset=ultrafast",
f"bitrate={self.gst_BIT_RATE}", "key-int-max=20",
#"byte-stream=true", "key-int-max=5", "bframes=0", "aud=true", f"bitrate={self.gst_BIT_RATE}",
"!", "rtph264pay", "config-interval=-1", "pt=96",
#"!", "h264parse", "!", "mpegtsmux",
"!", "udpsink",
f"host={_ip}", f"port={_porta}",
"sync=false", "async=false",
]
self.gst_proc = subprocess.Popen(
gst_cmd,
stdin=subprocess.PIPE,
)
except Exception as e:
self.gst_proc = None
self.mostrar_log(f"Erro ao criar script de transmissao de video: {e}")
_camera["modelo"] = self.modelo
_camera["dispositivo"] = self.dispositivo.value
_camera["tem_depht"] = False
_camera["tem_imu"] = False
# Fase 2: criar pipeline e instanciar normalmente # Fase 2: criar pipeline e instanciar normalmente
try: try:
@ -134,127 +83,23 @@ class CameraGal:
"cm_por_px_y": cm_por_px_y, "cm_por_px_y": cm_por_px_y,
} }
_camera["parametros"] = self.parametros
self.ultima_saude["timestamp"] = time.time() self.ultima_saude["timestamp"] = time.time()
self.ultima_saude["status"] = StatusModulo.OPERANTE.value self.ultima_saude["status"] = StatusModulo.OPERANTE.value
self.iniciado = True self.iniciado = True
_camera["iniciado"] = True _porta = (ContextoGlobalRedis.get_equipamento() or {}).get("base_porta_ervas")
_camera["iniciado_em"] = time.time() self.stream = CameraTcpStreamer(porta=_porta, mx_id=self.mx_id, mostrar_log=self.mostrar_log)
except Exception as e: except Exception as e:
self.mostrar_log(f"Erro ao iniciar camera: {e}") self.mostrar_log(f"Erro ao iniciar camera: {e}")
pass pass
_camera["iniciando"] = False ContextoGlobalRedis.atualizar_ctx_dict(
ContextoGlobalRedis.set(ContextoGlobalRedis.CamKey(self.mx_id), _camera) ContextoGlobalRedis.CamKey(self.mx_id),
parametros=self.parametros,
def requisitar_frame_raw(self, force: bool = False, max_age_s: float = None): iniciado=self.iniciado,
""" iniciando=False,
Se force=False, reaproveita o último frame se ainda for "novo". iniciado_em=time.time() if self.iniciado else None
Se force=True, sempre busca um frame novo na câmera. )
"""
try:
agora = time.time()
if max_age_s is None:
max_age_s = self._raw_cache_max_age
# Se NÃO for forçado e tem frame recente em cache, reaproveita
if (not force
and self.ultimo_frame_raw is not None
and self.timestamp_ultimo_frame_raw is not None
and (agora - self.timestamp_ultimo_frame_raw) < max_age_s):
return self.ultimo_frame_raw
# Caso contrário, busca frame novo
raw4_base, dbg = self.cam.grab_raw4(
out_h=self.parametros["raw_h"],
out_w=self.parametros["raw_w"],
timeout_ms=500
)
self.ultimo_frame_raw = raw4_base
self.timestamp_ultimo_frame_raw = agora
return raw4_base
except Exception as e:
self.mostrar_log(f"Erro ao requisitar frame raw: {e}")
return None
def requisitar_frame_rgb(self, force: bool = False, max_age_s: float = None):
"""
- Se force=False:
* Se existir RGB recente em cache, devolve ele (barato).
* Se estiver "velho" demais, recalcula a partir do raw (sem necessariamente ir na câmera).
- Se force=True:
* Recalcula RGB agora, usando o raw mais novo possível.
"""
try:
agora = time.time()
if max_age_s is None:
max_age_s = self._rgb_cache_max_age
# 1) Se NÃO for forçado e tenho frame RGB "fresco", só devolvo ele
if (not force
and self.ultimo_frame_rgb is not None
and self.timestamp_ultimo_frame_rgb is not None
and (agora - self.timestamp_ultimo_frame_rgb) < max_age_s):
dur = 0.0 # basicamente só acesso memória
resultado = {
"erro": None,
"duracao": dur,
"frame_valido": True
}
return self.ultimo_frame_rgb, resultado
# 2) Se preciso atualizar o RGB agora
start = time.time()
# Aqui a sacada: NÃO precisa obrigatoriamente ir na câmera.
# Usa o raw em cache (que a segmentação acabou de usar) se possível.
raw4_base = self.requisitar_frame_raw(force=False)
if raw4_base is None:
raise RuntimeError("raw4_base veio None em requisitar_frame_rgb")
frame = make_bgr_preview_from_raw(raw4_base, rgirb=True, preview_fast=True)
self.ultimo_frame_rgb = frame
self.timestamp_ultimo_frame_rgb = time.time()
dur = self.timestamp_ultimo_frame_rgb - start
self.enviar_frame_stream(frame)
resultado = {
"erro": None,
"duracao": dur,
"frame_valido": frame is not None and frame.size > 0
}
return frame, resultado
except Exception as e:
self.mostrar_log(f"Erro ao requisitar frame RGB: {e}")
return None, {
"erro": str(e),
"duracao": 0,
"frame_valido": False
}
def enviar_frame_stream(self, frame):
_stream_on = (ContextoGlobalRedis.get_camera(self.mx_id) or {}).get("streaming", False)
if self.gst_proc is not None and _stream_on:
self._ultimo_envio_gst = time.time()
if frame is not None and frame.size > 0:
if frame.shape[1] != self.gst_WIDTH or frame.shape[0] != self.gst_HEIGHT:
frame_stream = cv2.resize(frame, (self.gst_WIDTH, self.gst_HEIGHT), interpolation=cv2.INTER_AREA)
else:
frame_stream = frame
try:
self.gst_proc.stdin.write(frame_stream.tobytes())
except BrokenPipeError:
self.mostrar_log("GStreamer fechou o pipe. Encerrando.")
def atualizar_saude(self): def atualizar_saude(self):
# 1) Ver se “existe” no contexto e se o handle da GAL está aberto # 1) Ver se “existe” no contexto e se o handle da GAL está aberto
@ -377,151 +222,97 @@ class CameraGal:
from camera_worker.manager import definir_saude_camera from camera_worker.manager import definir_saude_camera
definir_saude_camera(self.mx_id, status, saude, motivos, self.rodando, performance, conectado, agora, self.dispositivo) definir_saude_camera(self.mx_id, status, saude, motivos, self.rodando, performance, conectado, agora, self.dispositivo)
def _sock_tentar_conectar(self):
_cfg = ContextoGlobalRedis.get_equipamento() or {}
_ip = _cfg.get("base_ip")
_porta = _cfg.get("base_porta_ervas")
# Sem IP/porta configurado, nem tenta
if not _ip or not _porta:
return
agora = time.time()
if (agora - self._sock_ultima_tentativa_conexao) < self._sock_intervalo_reconexao:
# ainda não deu o tempo mínimo, evita flood
return
self._sock_ultima_tentativa_conexao = agora
# Se já tem socket antigo, fecha
if self._sock is not None:
try:
self._sock.close()
except:
pass
self._sock = None
try:
#self.mostrar_log(f"Tentando conectar em {_ip}:{_porta}...")
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
s.settimeout(1.0) # timeout curto pra não travar
s.connect((_ip, _porta))
s.settimeout(None) # depois de conectado, volta p/ blocking normal
self._sock = s
self._sock_conectado = True
self.mostrar_log("Socket de vídeo conectado.")
except Exception as e:
self._sock_conectado = False
self._sock = None
# log mais leve pra não spammar
#self.mostrar_log(f"Falha ao conectar socket de vídeo: {e}")
def enviar_frame_tcp(self, frame_bgr): def enviar_frame_tcp(self, frame_bgr):
_camera = ContextoGlobalRedis.get_camera(self.mx_id) or {} self.stream.enviar_frame_tcp(frame_bgr)
_stream_on = _camera.get("streaming", False)
if not _stream_on:
return
# 1) Atualiza modo (não custa caro)
self._maybe_update_mode()
params = self._mode_params(self._op_mode)
self._op_fps = params["fps"]
# 2) Throttle por FPS (só tenta enviar quando "bate o relógio")
now = time.time()
min_dt = 1.0 / max(0.1, self._op_fps)
if (now - self._last_frame_sent_ts) < min_dt:
return
prev_sent_ts = self._last_frame_sent_ts
self._last_frame_sent_ts = now
# 4) Conexão socket (igual você já faz)
if not self._sock_conectado or self._sock is None:
self._sock_tentar_conectar()
if not self._sock_conectado or self._sock is None:
return
# 5) Resize adaptativo
frame_bgr = resize_frame(frame_bgr, max_width=params["w"], max_height=params["h"])
# 6) JPEG quality adaptativa
encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), int(params["q"])]
ok, buf = cv2.imencode(".jpg", frame_bgr, encode_param)
if not ok:
return
data = buf.tobytes()
size = len(data)
header = struct.pack("!I", size)
# 6) Calcula bitrate real
dt_real = max(0.001, now - prev_sent_ts)
kbps_inst = (size * 8) / dt_real / 1000.0
kbps_ant = float(_camera.get("stream_kbps", 0.0) or 0.0)
kbps_suave = kbps_inst if kbps_ant <= 0 else (kbps_ant * 0.8 + kbps_inst * 0.2)
def requisitar_frame_raw(self, force: bool = False, max_age_s: float = None):
"""
Se force=False, reaproveita o último frame se ainda for "novo".
Se force=True, sempre busca um frame novo na câmera.
"""
try: try:
self._sock.sendall(header + data) agora = time.time()
except (BrokenPipeError, ConnectionResetError, OSError) as e: if max_age_s is None:
self._sock_conectado = False max_age_s = self._raw_cache_max_age
kbps_suave = 0.0
try: # Se NÃO for forçado e tem frame recente em cache, reaproveita
self._sock.close() if (not force
except: and self.ultimo_frame_raw is not None
pass and self.timestamp_ultimo_frame_raw is not None
self._sock = None and (agora - self.timestamp_ultimo_frame_raw) < max_age_s):
finally: return self.ultimo_frame_raw
ContextoGlobalRedis.atualizar_ctx_dict(
ContextoGlobalRedis.CamKey(self.mx_id), # Caso contrário, busca frame novo
stream_kbps=round(kbps_suave, 1), raw4_base, dbg = self.cam.grab_raw4(
fps=params["fps"], out_h=self.parametros["raw_h"],
stream_w=params["w"], out_w=self.parametros["raw_w"],
stream_h=params["h"], timeout_ms=500
) )
def _get_ipb_bw_util_pct(self) -> float: self.ultimo_frame_raw = raw4_base
ipb = ContextoGlobalRedis.get_modulo(T_Code.Ipb) or {} # ajuste se sua key for diferente self.timestamp_ultimo_frame_raw = agora
saude = (ipb.get("saude") or {})
detalhes = (saude.get("detalhes") or {})
return float(detalhes.get("bw_pressure_pct") or detalhes.get("bw_util_pct") or 0.0)
def _mode_params(self, mode: int): return raw4_base
# mode 0..3
table = {
3: dict(fps=5.0, q=55, w=640, h=360),
2: dict(fps=3.0, q=45, w=640, h=360),
1: dict(fps=2.0, q=40, w=480, h=270),
0: dict(fps=1.0, q=35, w=320, h=180),
}
return table.get(int(mode), table[1])
def _maybe_update_mode(self): except Exception as e:
self.mostrar_log(f"Erro ao requisitar frame raw: {e}")
return None
def requisitar_frame_rgb(self, force: bool = False, max_age_s: float = None):
""" """
Decide modo baseado no uso de banda. Histerese com cooldown pra não oscilar. - Se force=False:
* Se existir RGB recente em cache, devolve ele (barato).
* Se estiver "velho" demais, recalcula a partir do raw (sem necessariamente ir na câmera).
- Se force=True:
* Recalcula RGB agora, usando o raw mais novo possível.
""" """
util = self._get_ipb_bw_util_pct() # 0..150 (pode passar 100) try:
now = time.time() agora = time.time()
if max_age_s is None:
max_age_s = self._rgb_cache_max_age
# cooldown mínimo entre trocas # 1) Se NÃO for forçado e tenho frame RGB "fresco", só devolvo ele
if (now - self._last_mode_change_ts) < 1.5: if (not force
return and self.ultimo_frame_rgb is not None
and self.timestamp_ultimo_frame_rgb is not None
and (agora - self.timestamp_ultimo_frame_rgb) < max_age_s):
dur = 0.0 # basicamente só acesso memória
resultado = {
"erro": None,
"duracao": dur,
"frame_valido": True
}
return self.ultimo_frame_rgb, resultado
# thresholds (ajuste fino depois) # 2) Se preciso atualizar o RGB agora
# sobe só se estiver bem folgado start = time.time()
if util < 55 and self._op_mode < 3:
self._op_mode += 1
self._last_mode_change_ts = now
# desce se está pesado
elif util > 85 and self._op_mode > 0:
self._op_mode -= 1
self._last_mode_change_ts = now
# emergência: muito alto, cai mais
if util > 90 and self._op_mode > 0:
self._op_mode = max(0, self._op_mode - 1)
self._last_mode_change_ts = now
# Aqui a sacada: NÃO precisa obrigatoriamente ir na câmera.
# Usa o raw em cache (que a segmentação acabou de usar) se possível.
raw4_base = self.requisitar_frame_raw(force=False)
if raw4_base is None:
raise RuntimeError("raw4_base veio None em requisitar_frame_rgb")
frame = make_bgr_preview_from_raw(raw4_base, rgirb=True, preview_fast=True)
self.ultimo_frame_rgb = frame
self.timestamp_ultimo_frame_rgb = time.time()
dur = self.timestamp_ultimo_frame_rgb - start
resultado = {
"erro": None,
"duracao": dur,
"frame_valido": frame is not None and frame.size > 0
}
return frame, resultado
except Exception as e:
self.mostrar_log(f"Erro ao requisitar frame RGB: {e}")
return None, {
"erro": str(e),
"duracao": 0,
"frame_valido": False
}

View File

@ -1,30 +1,18 @@
import numpy as np import numpy as np
import depthai as dai import depthai as dai
import time import time
import socket
import struct
from camera_worker.tcp_streamer import CameraTcpStreamer
from health_worker.modulos.imu import IMUCamera from health_worker.modulos.imu import IMUCamera
from shared.utils import resize_frame
from shared.enums import StatusModulo, T_Code from shared.enums import StatusModulo, T_Code
from shared.contexto_global_redis import ContextoGlobalRedis from shared.contexto_global_redis import ContextoGlobalRedis
import subprocess
import cv2
GST_LAUNCH = r"C:\Program Files\gstreamer\1.0\msvc_x86_64\bin\gst-launch-1.0.exe"
class CameraOak: class CameraOak:
def __init__(self, mostrar_log, mx_id, modelo_ia_seg=None, modelo_ia_det=None, iniciar_imu=False): def __init__(self, mostrar_log, mx_id, modelo_ia_seg=None, modelo_ia_det=None, iniciar_imu=False):
self.mostrar_log = mostrar_log self.mostrar_log = mostrar_log
self.modelo_ia_seg = modelo_ia_seg self.modelo_ia_seg = modelo_ia_seg
self.modelo_ia_det = modelo_ia_det self.modelo_ia_det = modelo_ia_det
self.stream = None
self.gst_proc = None
self.gst_FPS = 5
self.gst_WIDTH = 480
self.gst_HEIGHT = 270
self.gst_BIT_RATE = 300
self._ultimo_envio_gst = 0.0
self.dispositivo = T_Code.Vzo self.dispositivo = T_Code.Vzo
self.modelo = "Desconhecido" self.modelo = "Desconhecido"
@ -52,13 +40,15 @@ class CameraOak:
self.mx_id = self.dev_info.getMxId() self.mx_id = self.dev_info.getMxId()
self.versao = self.dev_info.name self.versao = self.dev_info.name
_camera = ContextoGlobalRedis.get_camera(self.mx_id) or {} ContextoGlobalRedis.atualizar_ctx_dict(
_camera["mx_id"] = self.mx_id ContextoGlobalRedis.CamKey(self.mx_id),
_camera["versao"] = self.versao mx_id=self.mx_id,
_camera["iniciando"] = True versao=self.versao,
_camera["iniciado"] = False iniciando=True,
_camera["rodando"] = False iniciado=False,
_camera["parametros"] = {} rodando=False,
parametros={}
)
# Fase 1: detectar sensores sem pipeline # Fase 1: detectar sensores sem pipeline
try: try:
@ -81,49 +71,13 @@ class CameraOak:
pass pass
self.mostrar_log(f"Falha ao detectar sensores: {e}") self.mostrar_log(f"Falha ao detectar sensores: {e}")
_camera["modelo"] = self.modelo ContextoGlobalRedis.atualizar_ctx_dict(
_camera["dispositivo"] = self.dispositivo.value ContextoGlobalRedis.CamKey(self.mx_id),
_camera["tem_depht"] = self.tem_depth modelo=self.modelo,
_camera["tem_imu"] = self.tem_imu dispositivo=self.dispositivo.value,
tem_depth=self.tem_depth,
# Criar processo para transmissao de video tem_imu=self.tem_imu
try: )
self._sock = None
self._sock_conectado = False
self._sock_ultima_tentativa_conexao = 0.0
self._sock_intervalo_reconexao = 5.0 # segundos entre tentativas
self._op_mode = 3
self._op_fps = 5
self._last_frame_sent_ts = 0.0
self._last_mode_change_ts = 0.0
if False and _ip is not None and _porta is not None:
self.mostrar_log(f"Iniciando GStreamer para {_ip}:{_porta}...")
gst_cmd = [
GST_LAUNCH,
"fdsrc", "fd=0",
"!", "videoparse",
f"width={self.gst_WIDTH}", f"height={self.gst_HEIGHT}",
"format=bgr",
f"framerate={self.gst_FPS}/1",
"!", "videoconvert",
"!", "videoscale",
"!", f"video/x-raw,width={self.gst_WIDTH},height={self.gst_HEIGHT}",
"!", "x264enc", "tune=zerolatency", "speed-preset=ultrafast",
f"bitrate={self.gst_BIT_RATE}", "key-int-max=20",
#"byte-stream=true", "key-int-max=5", "bframes=0", "aud=true", f"bitrate={self.gst_BIT_RATE}",
"!", "rtph264pay", "config-interval=-1", "pt=96",
#"!", "h264parse", "!", "mpegtsmux",
"!", "udpsink",
f"host={_ip}", f"port={_porta}",
"sync=false", "async=false",
]
self.gst_proc = subprocess.Popen(
gst_cmd,
stdin=subprocess.PIPE,
)
except Exception as e:
self.gst_proc = None
self.mostrar_log(f"Erro ao criar script de transmissao de video: {e}")
# Fase 2: criar pipeline e instanciar normalmente # Fase 2: criar pipeline e instanciar normalmente
try: try:
@ -161,7 +115,6 @@ class CameraOak:
"percentual_altura_solo": dadosSnr.get("percentual_altura_solo", 30), "percentual_altura_solo": dadosSnr.get("percentual_altura_solo", 30),
} }
self._set_calib() self._set_calib()
_camera["parametros"] = self.parametros
elif self.dispositivo == T_Code.Cam: elif self.dispositivo == T_Code.Cam:
rgb_width = 1920 rgb_width = 1920
rgb_height = 1080 rgb_height = 1080
@ -183,19 +136,148 @@ class CameraOak:
"cm_por_px_x": cm_por_px_x, "cm_por_px_x": cm_por_px_x,
"cm_por_px_y": cm_por_px_y, "cm_por_px_y": cm_por_px_y,
} }
_camera["parametros"] = self.parametros
self.ultima_saude["timestamp"] = time.time() self.ultima_saude["timestamp"] = time.time()
self.iniciado = True self.iniciado = True
_camera["iniciado"] = True _porta = (ContextoGlobalRedis.get_equipamento() or {}).get("base_porta_caminho")
_camera["iniciado_em"] = time.time() self.stream = CameraTcpStreamer(porta=_porta, mx_id=self.mx_id, mostrar_log=self.mostrar_log)
except Exception as e: except Exception as e:
self.mostrar_log(f"Erro ao iniciar camera: {e}") self.mostrar_log(f"Erro ao iniciar camera: {e}")
pass pass
_camera["iniciando"] = False ContextoGlobalRedis.atualizar_ctx_dict(
ContextoGlobalRedis.set(ContextoGlobalRedis.CamKey(self.mx_id), _camera) ContextoGlobalRedis.CamKey(self.mx_id),
parametros=self.parametros,
iniciado=self.iniciado,
iniciando=False,
iniciado_em=time.time() if self.iniciado else None
)
def atualizar_saude(self):
#self.mostrar_log(f"[{self.mx_id}] Atualizando saude {self.dispositivo.name}...")
if self.imu is not None:
self.imu.atualizar_saude()
conectado = ContextoGlobalRedis.get_cameras().get(self.mx_id) is not None
saude = 50
motivos = []
performance = {
"temperatura": 0,
"memoria_usada": 0,
"executando": False,
"velocidade": ""
}
if conectado:
try:
dev = self.device
temp = dev.getChipTemperature().average
ddr = dev.getDdrMemoryUsage().used / 1024.0 / 1024.0
running = dev.isPipelineRunning()
speed = dev.getUsbSpeed().name
performance = {
"temperatura": temp,
"memoria_usada": ddr,
"executando": running,
"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
# 🔸 Memória DDR (> 300 MB começa a penalizar)
if ddr >= 500:
motivos.append(f"Memória DDR crítica: {ddr:.1f} MB")
saude -= 30
elif ddr >= 400:
motivos.append(f"Memória DDR elevada: {ddr:.1f} MB")
saude -= 15
elif ddr >= 300:
motivos.append(f"Memória DDR acima do ideal: {ddr:.1f} MB")
saude -= 5
# 🔸 Pipeline inativa
if not running:
motivos.append("Pipeline parada")
saude -= 20
# 🔸 USB Speed
if speed.lower() not in ["super", "superplus"]: # USB 3.x
motivos.append(f"USB lenta: {speed}")
saude -= 15
frame, resultado = self.requisitar_frame_rgb()
except Exception as e:
self.mostrar_log(f"Erro ao requisitar dados da camera para atualizar saude: {e}")
resultado = {
"erro": str(e),
"frame_valido": False,
"duracao": 0
}
if "Communication exception" in str(e) or "X_LINK_ERROR" in str(e):
conectado = False
if not conectado:
motivos.append("desconectado")
saude = 0
elif resultado["erro"]:
motivos.append(resultado["erro"])
saude = 0
if "Communication exception" in resultado["erro"] or "X_LINK_ERROR" in resultado["erro"]:
conectado = False
elif not resultado["frame_valido"]:
motivos.append("Frame inválido ou vazio")
saude = 0
else:
saude += 50
if resultado["duracao"] > 1.0:
saude -= 20
motivos.append(f"Tempo elevado para captura: {resultado['duracao']:.2f}s")
saude = min(max(saude, 0), 100)
status = StatusModulo.OPERANTE
if not conectado:
status = StatusModulo.DESCONECTADO
elif saude <= 0:
status = StatusModulo.FALHA
elif saude < 80:
status = StatusModulo.ALERTA
agora = time.time()
saude_geral = {
"timestamp": agora,
"conectado": conectado,
"status": status.value,
"saude": saude,
"motivos": motivos,
"saude_idividual": []
}
self.ultima_saude = saude_geral
timeout = 2.0
ts_depth = self.timestamp_ultimo_frame_depth or 0
ts_rgb = self.timestamp_ultimo_frame_rgb or 0
self.rodando = ((agora - ts_depth) <= timeout or (agora - ts_rgb) <= timeout)
from camera_worker.manager import definir_saude_camera
definir_saude_camera(self.mx_id, status, saude, motivos, self.rodando, performance, conectado, agora, self.dispositivo)
def enviar_frame_tcp(self, frame_bgr):
self.stream.enviar_frame_tcp(frame_bgr)
def _criar_pipeline(self): def _criar_pipeline(self):
pipeline = dai.Pipeline() pipeline = dai.Pipeline()
@ -401,8 +483,6 @@ class CameraOak:
#self.timestamp_ultimo_frame_rgb = time.time() #self.timestamp_ultimo_frame_rgb = time.time()
dur = time.time() - start dur = time.time() - start
self.enviar_frame_stream(frame)
resultado = { resultado = {
"erro": None, "erro": None,
"duracao": dur, "duracao": dur,
@ -419,20 +499,6 @@ class CameraOak:
"frame_valido": False "frame_valido": False
} }
def enviar_frame_stream(self, frame):
_stream_on = (ContextoGlobalRedis.get_camera(self.mx_id) or {}).get("streaming", False)
if self.gst_proc is not None and _stream_on:
self._ultimo_envio_gst = time.time()
if frame is not None and frame.size > 0:
if frame.shape[1] != self.gst_WIDTH or frame.shape[0] != self.gst_HEIGHT:
frame_stream = cv2.resize(frame, (self.gst_WIDTH, self.gst_HEIGHT), interpolation=cv2.INTER_AREA)
else:
frame_stream = frame
try:
self.gst_proc.stdin.write(frame_stream.tobytes())
except BrokenPipeError:
self.mostrar_log("GStreamer fechou o pipe. Encerrando.")
def requisitar_frame_depth(self): def requisitar_frame_depth(self):
if not self.tem_depth: if not self.tem_depth:
return None, { return None, {
@ -583,271 +649,4 @@ class CameraOak:
dur = time.time() - start dur = time.time() - start
return [], {"erro": str(e), "duracao": dur, "frame_valido": False} return [], {"erro": str(e), "duracao": dur, "frame_valido": False}
def atualizar_saude(self):
#self.mostrar_log(f"[{self.mx_id}] Atualizando saude {self.dispositivo.name}...")
if self.imu is not None:
self.imu.atualizar_saude()
conectado = ContextoGlobalRedis.get_cameras().get(self.mx_id) is not None
saude = 50
motivos = []
performance = {
"temperatura": 0,
"memoria_usada": 0,
"executando": False,
"velocidade": ""
}
if conectado:
try:
dev = self.device
temp = dev.getChipTemperature().average
ddr = dev.getDdrMemoryUsage().used / 1024.0 / 1024.0
running = dev.isPipelineRunning()
speed = dev.getUsbSpeed().name
performance = {
"temperatura": temp,
"memoria_usada": ddr,
"executando": running,
"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
# 🔸 Memória DDR (> 300 MB começa a penalizar)
if ddr >= 500:
motivos.append(f"Memória DDR crítica: {ddr:.1f} MB")
saude -= 30
elif ddr >= 400:
motivos.append(f"Memória DDR elevada: {ddr:.1f} MB")
saude -= 15
elif ddr >= 300:
motivos.append(f"Memória DDR acima do ideal: {ddr:.1f} MB")
saude -= 5
# 🔸 Pipeline inativa
if not running:
motivos.append("Pipeline parada")
saude -= 20
# 🔸 USB Speed
if speed.lower() not in ["super", "superplus"]: # USB 3.x
motivos.append(f"USB lenta: {speed}")
saude -= 15
frame, resultado = self.requisitar_frame_rgb()
except Exception as e:
self.mostrar_log(f"Erro ao requisitar dados da camera para atualizar saude: {e}")
resultado = {
"erro": str(e),
"frame_valido": False,
"duracao": 0
}
if "Communication exception" in str(e) or "X_LINK_ERROR" in str(e):
conectado = False
if not conectado:
motivos.append("desconectado")
saude = 0
elif resultado["erro"]:
motivos.append(resultado["erro"])
saude = 0
if "Communication exception" in resultado["erro"] or "X_LINK_ERROR" in resultado["erro"]:
conectado = False
elif not resultado["frame_valido"]:
motivos.append("Frame inválido ou vazio")
saude = 0
else:
saude += 50
if resultado["duracao"] > 1.0:
saude -= 20
motivos.append(f"Tempo elevado para captura: {resultado['duracao']:.2f}s")
saude = min(max(saude, 0), 100)
status = StatusModulo.OPERANTE
if not conectado:
status = StatusModulo.DESCONECTADO
elif saude <= 0:
status = StatusModulo.FALHA
elif saude < 80:
status = StatusModulo.ALERTA
agora = time.time()
saude_geral = {
"timestamp": agora,
"conectado": conectado,
"status": status.value,
"saude": saude,
"motivos": motivos,
"saude_idividual": []
}
self.ultima_saude = saude_geral
timeout = 2.0
ts_depth = self.timestamp_ultimo_frame_depth or 0
ts_rgb = self.timestamp_ultimo_frame_rgb or 0
self.rodando = ((agora - ts_depth) <= timeout or (agora - ts_rgb) <= timeout)
from camera_worker.manager import definir_saude_camera
definir_saude_camera(self.mx_id, status, saude, motivos, self.rodando, performance, conectado, agora, self.dispositivo)
def _sock_tentar_conectar(self):
_cfg = ContextoGlobalRedis.get_equipamento() or {}
_ip = _cfg.get("base_ip")
_porta = _cfg.get("base_porta_caminho")
# Sem IP/porta configurado, nem tenta
if not _ip or not _porta:
return
agora = time.time()
if (agora - self._sock_ultima_tentativa_conexao) < self._sock_intervalo_reconexao:
# ainda não deu o tempo mínimo, evita flood
return
self._sock_ultima_tentativa_conexao = agora
# Se já tem socket antigo, fecha
if self._sock is not None:
try:
self._sock.close()
except:
pass
self._sock = None
try:
#self.mostrar_log(f"Tentando conectar em {_ip}:{_porta}...")
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
s.settimeout(1.0) # timeout curto pra não travar
s.connect((_ip, _porta))
s.settimeout(None) # depois de conectado, volta p/ blocking normal
self._sock = s
self._sock_conectado = True
self.mostrar_log("Socket de vídeo conectado.")
except Exception as e:
self._sock_conectado = False
self._sock = None
# log mais leve pra não spammar
#self.mostrar_log(f"Falha ao conectar socket de vídeo: {e}")
def enviar_frame_tcp(self, frame_bgr):
_camera = ContextoGlobalRedis.get_camera(self.mx_id) or {}
_stream_on = _camera.get("streaming", False)
if not _stream_on:
return
# 1) Atualiza modo (não custa caro)
self._maybe_update_mode()
params = self._mode_params(self._op_mode)
self._op_fps = params["fps"]
# 2) Throttle por FPS (só tenta enviar quando "bate o relógio")
now = time.time()
min_dt = 1.0 / max(0.1, self._op_fps)
if (now - self._last_frame_sent_ts) < min_dt:
return
prev_sent_ts = self._last_frame_sent_ts
self._last_frame_sent_ts = now
# 4) Conexão socket (igual você já faz)
if not self._sock_conectado or self._sock is None:
self._sock_tentar_conectar()
if not self._sock_conectado or self._sock is None:
return
# 5) Resize adaptativo
frame_bgr = resize_frame(frame_bgr, max_width=params["w"], max_height=params["h"])
# 6) JPEG quality adaptativa
encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), int(params["q"])]
ok, buf = cv2.imencode(".jpg", frame_bgr, encode_param)
if not ok:
return
data = buf.tobytes()
size = len(data)
header = struct.pack("!I", size)
# 6) Calcula bitrate real
dt_real = max(0.001, now - prev_sent_ts)
kbps_inst = (size * 8) / dt_real / 1000.0
kbps_ant = float(_camera.get("stream_kbps", 0.0) or 0.0)
kbps_suave = kbps_inst if kbps_ant <= 0 else (kbps_ant * 0.8 + kbps_inst * 0.2)
try:
self._sock.sendall(header + data)
except (BrokenPipeError, ConnectionResetError, OSError) as e:
self._sock_conectado = False
kbps_suave = 0.0
try:
self._sock.close()
except:
pass
self._sock = None
finally:
ContextoGlobalRedis.atualizar_ctx_dict(
ContextoGlobalRedis.CamKey(self.mx_id),
stream_kbps=round(kbps_suave, 1),
fps=params["fps"],
stream_w=params["w"],
stream_h=params["h"],
)
def _get_ipb_bw_util_pct(self) -> float:
ipb = ContextoGlobalRedis.get_modulo(T_Code.Ipb) or {} # ajuste se sua key for diferente
saude = (ipb.get("saude") or {})
detalhes = (saude.get("detalhes") or {})
return float(detalhes.get("bw_pressure_pct") or detalhes.get("bw_util_pct") or 0.0)
def _mode_params(self, mode: int):
# mode 0..3
table = {
3: dict(fps=5.0, q=55, w=640, h=360),
2: dict(fps=3.0, q=45, w=640, h=360),
1: dict(fps=2.0, q=40, w=480, h=270),
0: dict(fps=1.0, q=35, w=320, h=180),
}
return table.get(int(mode), table[1])
def _maybe_update_mode(self):
"""
Decide modo baseado no uso de banda. Histerese com cooldown pra não oscilar.
"""
util = self._get_ipb_bw_util_pct() # 0..150 (pode passar 100)
now = time.time()
# cooldown mínimo entre trocas
if (now - self._last_mode_change_ts) < 1.5:
return
# thresholds (ajuste fino depois)
# sobe só se estiver bem folgado
if util < 55 and self._op_mode < 3:
self._op_mode += 1
self._last_mode_change_ts = now
# desce se está pesado
elif util > 75 and self._op_mode > 0:
self._op_mode -= 1
self._last_mode_change_ts = now
# emergência: muito alto, cai mais
if util > 85 and self._op_mode > 0:
self._op_mode = max(0, self._op_mode - 1)
self._last_mode_change_ts = now

View File

@ -0,0 +1,155 @@
import socket
import struct
import time
import cv2
import numpy as np
from shared.enums import T_Code
from shared.utils import resize_frame
from shared.contexto_global_redis import ContextoGlobalRedis
class CameraTcpStreamer:
def __init__(self, porta: int, mx_id: str, mostrar_log=None):
self.mx_id = mx_id
self.mostrar_log = mostrar_log or (lambda msg: None)
self._sock = None
self.sock_port = porta
self._sock_conectado = False
self._sock_ultima_tentativa_conexao = 0.0
self._sock_intervalo_reconexao = 1.0
self._last_frame_sent_ts = 0.0
self._last_mode_change_ts = 0.0
self._op_mode = 1
self._op_fps = 2.0
def fechar(self):
self._sock_conectado = False
if self._sock is not None:
try:
self._sock.close()
except:
pass
self._sock = None
def _sock_tentar_conectar(self):
_cfg = ContextoGlobalRedis.get_equipamento() or {}
_ip = _cfg.get("base_ip")
if not _ip or not self.sock_port:
return
agora = time.time()
if (agora - self._sock_ultima_tentativa_conexao) < self._sock_intervalo_reconexao:
return
self._sock_ultima_tentativa_conexao = agora
self.fechar()
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
s.settimeout(1.0)
s.connect((_ip, self.sock_port))
s.settimeout(None)
self._sock = s
self._sock_conectado = True
self.mostrar_log(f"[{self.mx_id}] Socket de vídeo conectado.")
except Exception:
self._sock_conectado = False
self._sock = None
def _get_ipb_bw_util_pct(self) -> float:
ipb = ContextoGlobalRedis.get_modulo(T_Code.Ipb) or {}
saude = (ipb.get("saude") or {})
detalhes = (saude.get("detalhes") or {})
return float(detalhes.get("bw_pressure_pct") or detalhes.get("bw_util_pct") or 0.0)
def _mode_params(self, mode: int):
table = {
3: dict(fps=5.0, q=55, w=640, h=360),
2: dict(fps=3.0, q=45, w=640, h=360),
1: dict(fps=2.0, q=40, w=480, h=270),
0: dict(fps=1.0, q=35, w=320, h=180),
}
return table.get(int(mode), table[1])
def _maybe_update_mode(self):
util = self._get_ipb_bw_util_pct()
now = time.time()
if (now - self._last_mode_change_ts) < 1.5:
return
if util < 55 and self._op_mode < 3:
self._op_mode += 1
self._last_mode_change_ts = now
elif util > 75 and self._op_mode > 0:
self._op_mode -= 1
self._last_mode_change_ts = now
if util > 85 and self._op_mode > 0:
self._op_mode = max(0, self._op_mode - 1)
self._last_mode_change_ts = now
def enviar_frame_tcp(self, frame_bgr):
_camera = ContextoGlobalRedis.get_camera(self.mx_id) or {}
_stream_on = _camera.get("streaming", False)
if not _stream_on:
return
self._maybe_update_mode()
params = self._mode_params(self._op_mode)
self._op_fps = params["fps"]
now = time.time()
min_dt = 1.0 / max(0.1, self._op_fps)
if (now - self._last_frame_sent_ts) < min_dt:
return
prev_sent_ts = self._last_frame_sent_ts
self._last_frame_sent_ts = now
if not self._sock_conectado or self._sock is None:
self._sock_tentar_conectar()
if not self._sock_conectado or self._sock is None:
return
if frame_bgr is None:
frame_bgr = np.zeros((params["h"], params["w"], 3), dtype=np.uint8)
else:
frame_bgr = resize_frame(frame_bgr, max_width=params["w"], max_height=params["h"])
encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), int(params["q"])]
ok, buf = cv2.imencode(".jpg", frame_bgr, encode_param)
if not ok:
return
data = buf.tobytes()
size = len(data)
header = struct.pack("!I", size)
dt_real = max(0.001, now - prev_sent_ts)
fps_real = 1.0 / dt_real
kbps_inst = (size * 8) / dt_real / 1000.0
kbps_ant = float(_camera.get("stream_kbps", 0.0) or 0.0)
kbps_suave = kbps_inst if kbps_ant <= 0 else (kbps_ant * 0.8 + kbps_inst * 0.2)
try:
self._sock.sendall(header + data)
except (BrokenPipeError, ConnectionResetError, OSError):
self._sock_conectado = False
kbps_suave = 0.0
self.fechar()
finally:
ContextoGlobalRedis.atualizar_ctx_dict(
ContextoGlobalRedis.CamKey(self.mx_id),
stream_last_frame=time.time(),
stream_kbps=round(kbps_suave, 1),
fps=round(fps_real, 1),
stream_w=params["w"],
stream_h=params["h"],
)

View File

@ -124,8 +124,8 @@ class IMUCamera(ModuloDiagnosticoBase):
f_exec = 1.0 / (latencia + 1e-9) f_exec = 1.0 / (latencia + 1e-9)
ContextoGlobalRedis.atualizar_ctx_dict( ContextoGlobalRedis.atualizar_ctx_dict(
ContextoGlobalRedis.ModKey(T_Code.Imu), ContextoGlobalRedis.ModKey(T_Code.Imu),
roll=round(roll, 2), # lateral roll=round(-roll, 2), # frontal
pitch=round(-pitch, 2), # frontal pitch=round(pitch, 2), # lateral
yaw=round(yaw, 2), yaw=round(yaw, 2),
vel_mps=round(velocidade_mps, 3), vel_mps=round(velocidade_mps, 3),
timestamp=(t1 * 1000), timestamp=(t1 * 1000),

View File

@ -3,14 +3,14 @@ from shared.enums import StatusModulo, StatusOperacao, T_Code
from shared.contexto_global_redis import ContextoGlobalRedis, CtxKey from shared.contexto_global_redis import ContextoGlobalRedis, CtxKey
from manager_worker.config import mostrar_log from manager_worker.config import mostrar_log
def verifica_controle_liberado(): def verifica_controle_liberado(ignorar_parada=False):
try: try:
motivos = [] motivos = []
_operacao = ContextoGlobalRedis.get_operacao() _operacao = ContextoGlobalRedis.get_operacao()
status_operacao = StatusOperacao(_operacao.get("status", StatusOperacao.NaoIniciado.value)) status_operacao = StatusOperacao(_operacao.get("status", StatusOperacao.NaoIniciado.value))
finalizando = _operacao.get("finalizando", False) finalizando = _operacao.get("finalizando", False)
if (status_operacao != StatusOperacao.EmAndamento) or finalizando: if not ignorar_parada and ((status_operacao != StatusOperacao.EmAndamento) or finalizando):
motivos.append(f"Operação não está em andamento ou finalizando: {status_operacao.name}") motivos.append(f"Operação não está em andamento ou finalizando: {status_operacao.name}")
_controle = ContextoGlobalRedis.get_controle() _controle = ContextoGlobalRedis.get_controle()
@ -20,7 +20,7 @@ def verifica_controle_liberado():
_dados_vw = ContextoGlobalRedis.get(CtxKey.DadosVisualWorker, {}) _dados_vw = ContextoGlobalRedis.get(CtxKey.DadosVisualWorker, {})
_snr = ContextoGlobalRedis.get_modulo(T_Code.Snr) _snr = ContextoGlobalRedis.get_modulo(T_Code.Snr)
_snr_saude = StatusModulo(_snr.get("saude", {}).get("status", StatusModulo.DESCONECTADO.value)) _snr_saude = StatusModulo(_snr.get("saude", {}).get("status", StatusModulo.DESCONECTADO.value))
visual_worker_operante = _snr_saude == StatusModulo.OPERANTE visual_worker_operante = _snr_saude in [StatusModulo.OPERANTE, StatusModulo.ALERTA]
analise_deteccao_atualizada = (time.perf_counter() - _dados_vw.get("matriz_confianca", {}).get("ts", 10.0) <= 1.0) analise_deteccao_atualizada = (time.perf_counter() - _dados_vw.get("matriz_confianca", {}).get("ts", 10.0) <= 1.0)
obstaculo_detectado = _dados_vw.get("matriz_confianca", {}).get("block", {}).get("decision", {}).get("parar", False) if analise_deteccao_atualizada else False obstaculo_detectado = _dados_vw.get("matriz_confianca", {}).get("block", {}).get("decision", {}).get("parar", False) if analise_deteccao_atualizada else False
@ -33,7 +33,7 @@ def verifica_controle_liberado():
if imu_parada_por_inclinacao: if imu_parada_por_inclinacao:
_imu = ContextoGlobalRedis.get_modulo(T_Code.Imu) _imu = ContextoGlobalRedis.get_modulo(T_Code.Imu)
_imu_saude = StatusModulo(_imu.get("saude", {}).get("status", StatusModulo.DESCONECTADO.value)) _imu_saude = StatusModulo(_imu.get("saude", {}).get("status", StatusModulo.DESCONECTADO.value))
imu_operante = _imu_saude == StatusModulo.OPERANTE imu_operante = _imu_saude in [StatusModulo.OPERANTE, StatusModulo.ALERTA]
_equipamento = ContextoGlobalRedis.get_equipamento() _equipamento = ContextoGlobalRedis.get_equipamento()
ang_roll_max = _equipamento.get("angulo_roll_max", 15.0) ang_roll_max = _equipamento.get("angulo_roll_max", 15.0)
ang_pitch_max = _equipamento.get("angulo_pitch_max", 30.0) ang_pitch_max = _equipamento.get("angulo_pitch_max", 30.0)

View File

@ -44,7 +44,8 @@ class ProcessadorEmAndamento(ProcessadorBase):
erro=False, erro=False,
latencia=0, latencia=0,
erro_lateral=0, erro_lateral=0,
debug_custo={} debug_custo={},
motivos=motivos_parada
) )
_controle = ContextoGlobalRedis.get_controle() _controle = ContextoGlobalRedis.get_controle()
@ -71,7 +72,8 @@ class ProcessadorEmAndamento(ProcessadorBase):
erro=comando_dir.get("erro", False), erro=comando_dir.get("erro", False),
latencia=comando_dir.get("latencia", 0.0), latencia=comando_dir.get("latencia", 0.0),
erro_lateral=comando_dir.get("erro_lateral", 0.0), erro_lateral=comando_dir.get("erro_lateral", 0.0),
debug_custo=comando_dir.get("debug_custo", {}) debug_custo=comando_dir.get("debug_custo", {}),
motivos=[]
) )
except Exception as e: except Exception as e:
from manager_worker.config import mostrar_log from manager_worker.config import mostrar_log

View File

@ -1,10 +1,12 @@
from .base import ProcessadorBase from .base import ProcessadorBase
from manager_worker.processadores.padroes import comando_parado from manager_worker.processadores.padroes import comando_parado
from manager_worker.modulos.regras_taticas import verifica_controle_liberado
class ProcessadorParado(ProcessadorBase): class ProcessadorParado(ProcessadorBase):
def processar(self): def processar(self):
try: try:
return comando_parado() motivos_parada = verifica_controle_liberado(ignorar_parada=True)
return comando_parado(motivos=motivos_parada)
except Exception as e: except Exception as e:
from manager_worker.config import mostrar_log from manager_worker.config import mostrar_log
mostrar_log(f"Erro no processador Parado: {e}") mostrar_log(f"Erro no processador Parado: {e}")

View File

@ -3,7 +3,7 @@ from shared.enums import ModoOperacao, StatusOperacao
from manager_worker.config import mostrar_log from manager_worker.config import mostrar_log
from shared.contexto_global_redis import ContextoGlobalRedis, CtxKey from shared.contexto_global_redis import ContextoGlobalRedis, CtxKey
def comando_controle(percentual_velocidade: float, frear: bool, angulo: float, tipo_movimento: int, simulacao = [], erro: bool = False, latencia: float = 0.0, erro_lateral: float = 0.0, debug_custo: dict = {}): def comando_controle(percentual_velocidade: float, frear: bool, angulo: float, tipo_movimento: int, simulacao = [], erro: bool = False, latencia: float = 0.0, erro_lateral: float = 0.0, debug_custo: dict = {}, motivos = None):
_controle = ContextoGlobalRedis.get_controle() _controle = ContextoGlobalRedis.get_controle()
hb_atual = _controle.get("heartbeat", 0) hb_atual = _controle.get("heartbeat", 0)
hb_novo = hb_atual hb_novo = hb_atual
@ -14,6 +14,10 @@ def comando_controle(percentual_velocidade: float, frear: bool, angulo: float, t
hb_novo = 0 hb_novo = 0
hb_novo = (hb_novo + 1) % 10 hb_novo = (hb_novo + 1) % 10
_motivos_anterior = _controle.get("motivos", [])
if motivos is None:
motivos = _motivos_anterior
ContextoGlobalRedis.atualizar_ctx_dict( ContextoGlobalRedis.atualizar_ctx_dict(
CtxKey.DadosControle, CtxKey.DadosControle,
angulo_sp=angulo, angulo_sp=angulo,
@ -24,7 +28,8 @@ def comando_controle(percentual_velocidade: float, frear: bool, angulo: float, t
heartbeat=hb_novo, heartbeat=hb_novo,
latencia=latencia, latencia=latencia,
erro_lateral=erro_lateral, erro_lateral=erro_lateral,
debug_custo=debug_custo debug_custo=debug_custo,
motivos=motivos
) )
return { return {
"velocidade_sp": percentual_velocidade, "velocidade_sp": percentual_velocidade,
@ -38,8 +43,8 @@ def comando_controle(percentual_velocidade: float, frear: bool, angulo: float, t
"debug_custo": debug_custo "debug_custo": debug_custo
} }
def comando_parado(): def comando_parado(motivos=None):
return comando_controle(0, False, 0, TipoMovimentoDirecional.RodasDianteiras) return comando_controle(0, False, 0, TipoMovimentoDirecional.RodasDianteiras, motivos=motivos)
def log_status_operacao(): def log_status_operacao():
modo = ModoOperacao(ContextoGlobalRedis.get_operacao().get("modo")) modo = ModoOperacao(ContextoGlobalRedis.get_operacao().get("modo"))

View File

@ -386,7 +386,13 @@ class ContextoGlobalRedis:
if _motivo_traj: if _motivo_traj:
motivos_dos_modulos_mandatorios.append(_motivo_traj) motivos_dos_modulos_mandatorios.append(_motivo_traj)
liberou = ((base_ok and parametros_ok and emergencia_ok and pausa_ok) or debug_mode) and trajetoria_ok controle_ok = True
_motivos_controle = cls.get_controle().get("motivos", [])
if len(_motivos_controle) > 0:
controle_ok = False
motivos_dos_modulos_mandatorios.extend(_motivos_controle)
liberou = ((base_ok and parametros_ok) or debug_mode) and emergencia_ok and pausa_ok and trajetoria_ok and controle_ok
cls.atualizar_ctx_dict( cls.atualizar_ctx_dict(
CtxKey.DadosOperacao, CtxKey.DadosOperacao,

View File

@ -115,7 +115,7 @@ class CameraManager:
self._analisando_deteccao = False self._analisando_deteccao = False
self._iniciar_loop_analise_continua(15.0) self._iniciar_loop_analise_continua(15.0)
self._iniciar_loop_frame_stream(self.camera._op_fps) self._iniciar_loop_frame_stream(self.camera.stream._op_fps)
self.iniciando = False self.iniciando = False
self.atualizar_saude_camera() self.atualizar_saude_camera()
@ -302,8 +302,7 @@ class CameraManager:
if (_stream_on): if (_stream_on):
_frame_type = TipoFrameCamera(_camera.get("frame_type", TipoFrameCamera.Rgb.value)) _frame_type = TipoFrameCamera(_camera.get("frame_type", TipoFrameCamera.Rgb.value))
_frame = self.get_selected_frame(_frame_type) _frame = self.get_selected_frame(_frame_type)
if _frame is not None: self.camera.enviar_frame_tcp(_frame)
self.camera.enviar_frame_tcp(_frame)
from visual_worker.config import load_seg_config, load_det_config from visual_worker.config import load_seg_config, load_det_config
seg_config = load_seg_config() seg_config = load_seg_config()
@ -318,7 +317,7 @@ class CameraManager:
self.mostrar_log(f"Erro no loop de stream: {e}") self.mostrar_log(f"Erro no loop de stream: {e}")
finally: finally:
latencia = time.time() - t0 latencia = time.time() - t0
freq = self.camera._op_fps freq = self.camera.stream._op_fps
time.sleep(max(0, (1.0 / freq) - latencia)) time.sleep(max(0, (1.0 / freq) - latencia))
threading.Thread(target=loop, daemon=True).start() threading.Thread(target=loop, daemon=True).start()

View File

@ -79,7 +79,7 @@ class CameraManager:
self.weed_detector = WeedDetector(color_map=colormap_rgb, classes=classes) self.weed_detector = WeedDetector(color_map=colormap_rgb, classes=classes)
self._iniciar_loop_analise_continua(20.0) self._iniciar_loop_analise_continua(20.0)
self._iniciar_loop_frame_stream(self.camera._op_fps) self._iniciar_loop_frame_stream(self.camera.stream._op_fps)
self.iniciando = False self.iniciando = False
self.atualizar_saude_camera() self.atualizar_saude_camera()
@ -202,8 +202,7 @@ class CameraManager:
if (_stream_on): if (_stream_on):
_frame_type = TipoFrameCamera(_camera.get("frame_type", TipoFrameCamera.Rgb.value)) _frame_type = TipoFrameCamera(_camera.get("frame_type", TipoFrameCamera.Rgb.value))
_frame = self.get_selected_frame(_frame_type) _frame = self.get_selected_frame(_frame_type)
if _frame is not None: self.camera.enviar_frame_tcp(_frame)
self.camera.enviar_frame_tcp(_frame)
from weed_worker.config import load_seg_config from weed_worker.config import load_seg_config
config = load_seg_config() config = load_seg_config()
@ -215,7 +214,7 @@ class CameraManager:
self.mostrar_log(f"Erro no loop de stream: {e}") self.mostrar_log(f"Erro no loop de stream: {e}")
finally: finally:
latencia = time.time() - t0 latencia = time.time() - t0
freq = self.camera._op_fps freq = self.camera.stream._op_fps
time.sleep(max(0, (1.0 / freq) - latencia)) time.sleep(max(0, (1.0 / freq) - latencia))
threading.Thread(target=loop, daemon=True).start() threading.Thread(target=loop, daemon=True).start()

View File

@ -496,9 +496,9 @@ namespace OperationControl.Controls
Mapa.Map = new Mapsui.Map(); Mapa.Map = new Mapsui.Map();
Mapa.Map.BackColor = Color.FromString("#111111"); Mapa.Map.BackColor = Color.FromString("#111111");
} }
Mapa.Map.Widgets.Clear();
InitBaseMapOptions(); InitBaseMapOptions();
EnsureBaseLayer(); EnsureBaseLayer();
NavigateToView(); NavigateToView();
} }

View File

@ -1,4 +1,5 @@
using System.Collections.ObjectModel; using AgroBase.Models.Operadores;
using System.Collections.ObjectModel;
using System.ComponentModel; using System.ComponentModel;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using static AgroBase.Models.Enums; using static AgroBase.Models.Enums;
@ -115,12 +116,22 @@ namespace OperationControl.ViewModels.Views.Operacao.Monitoramento
CameraErvasFrameTipoSelecionado = CameraErvasFrameTipos.FirstOrDefault(); CameraErvasFrameTipoSelecionado = CameraErvasFrameTipos.FirstOrDefault();
} }
public void AtualizarDadosCameras(double? cf_fps, double? cf_kbps, double? ct_fps, double? ct_kbps, StatusCarroMapa? status_carro, double? pct_ervas) public void AtualizarDadosCameras(CameraWorkerItemModel? snr, CameraWorkerItemModel? cam, StatusCarroMapa? status_carro, double? pct_ervas)
{ {
CameraFrontalStatus = $"{cf_fps ?? 0} fps • {cf_kbps ?? 0} kbps • {status_carro ?? StatusCarroMapa.Indefinido}"; var snr_dt = DateTimeOffset.FromUnixTimeMilliseconds((long)((snr?.stream_last_frame ?? 0) * 1000)).UtcDateTime;
CameraErvasStatus = $"{ct_fps ?? 0} fps • {ct_kbps ?? 0} kbps • {pct_ervas ?? 0:F2}% ervas"; bool snr_sem_sinal = (DateTime.UtcNow - snr_dt).TotalSeconds > 10;
CameraFrontalSemSinal = (cf_kbps ?? 0) == 0; CameraFrontalSemSinal = snr_sem_sinal;
CameraErvasSemSinal = (ct_kbps ?? 0) == 0; double snr_fps = snr_sem_sinal ? 0 : snr?.fps ?? 0;
double snr_kbps = snr_sem_sinal ? 0 : snr?.stream_kbps ?? 0;
CameraFrontalStatus = $"{snr_fps:F1} fps • {snr_kbps:F1} kbps • {status_carro ?? StatusCarroMapa.Indefinido}";
var cam_dt = DateTimeOffset.FromUnixTimeMilliseconds((long)((cam?.stream_last_frame ?? 0) * 1000)).UtcDateTime;
bool cam_sem_sinal = (DateTime.UtcNow - cam_dt).TotalSeconds > 10;
CameraErvasSemSinal = cam_sem_sinal;
double cam_fps = cam_sem_sinal ? 0 : cam?.fps ?? 0;
double cam_kbps = cam_sem_sinal ? 0 : cam?.stream_kbps ?? 0;
CameraErvasStatus = $"{cam_fps:F1} fps • {cam_kbps:F1} kbps • {pct_ervas ?? 0:F2}% ervas";
} }
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;

View File

@ -2,135 +2,136 @@
using System.Windows.Media; using System.Windows.Media;
using Brush = System.Windows.Media.Brush; using Brush = System.Windows.Media.Brush;
using Brushes = System.Windows.Media.Brushes; using Brushes = System.Windows.Media.Brushes;
using Color = System.Windows.Media.Color;
using ColorConverter = System.Windows.Media.ColorConverter; using ColorConverter = System.Windows.Media.ColorConverter;
using Color = System.Windows.Media.Color;
namespace OperationControl.ViewModels.Views.Operacao namespace OperationControl.ViewModels.Views.Operacao
{ {
public class OperacaoTopViewModel : INotifyPropertyChanged public class OperacaoTopViewModel : INotifyPropertyChanged
{ {
private string _contextoSelecionado = "BASE"; private string _contextoSelecionado = "ROVER 01";
public string ContextoSelecionado public string ContextoSelecionado
{ {
get => _contextoSelecionado; get => _contextoSelecionado;
set set { _contextoSelecionado = value; OnPropertyChanged(nameof(ContextoSelecionado)); }
{
if (_contextoSelecionado != value)
{
_contextoSelecionado = value;
OnPropertyChanged(nameof(ContextoSelecionado));
}
}
} }
private string _mensagemStatus = "Sistema inicializado."; private string _tituloStatus = "ROVER OPERANDO NORMALMENTE";
public string MensagemStatus public string TituloStatus
{ {
get => _mensagemStatus; get => _tituloStatus;
set set { _tituloStatus = value; OnPropertyChanged(nameof(TituloStatus)); }
{
if (_mensagemStatus != value)
{
_mensagemStatus = value;
OnPropertyChanged(nameof(MensagemStatus));
}
}
} }
private string _badgeTexto = "PRONTO"; private string _motivoPrincipal = "Todos os módulos obrigatórios e regras de controle estão OK.";
public string MotivoPrincipal
{
get => _motivoPrincipal;
set { _motivoPrincipal = value; OnPropertyChanged(nameof(MotivoPrincipal)); }
}
private string _badgeTexto = "NORMAL";
public string BadgeTexto public string BadgeTexto
{ {
get => _badgeTexto; get => _badgeTexto;
set set { _badgeTexto = value; OnPropertyChanged(nameof(BadgeTexto)); }
{
if (_badgeTexto != value)
{
_badgeTexto = value;
OnPropertyChanged(nameof(BadgeTexto));
}
}
} }
private Brush _corFundo = Brushes.DimGray; private string _resumoCurto = "Sistema saudável";
public string ResumoCurto
{
get => _resumoCurto;
set { _resumoCurto = value; OnPropertyChanged(nameof(ResumoCurto)); }
}
private Brush _corFundo = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#153D2C"));
public Brush CorFundo public Brush CorFundo
{ {
get => _corFundo; get => _corFundo;
set set { _corFundo = value; OnPropertyChanged(nameof(CorFundo)); }
{
if (_corFundo != value)
{
_corFundo = value;
OnPropertyChanged(nameof(CorFundo));
}
}
} }
private Brush _corBadge = Brushes.SteelBlue; private Brush _corBorda = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#2C7A57"));
public Brush CorBorda
{
get => _corBorda;
set { _corBorda = value; OnPropertyChanged(nameof(CorBorda)); }
}
private Brush _corAccent = Brushes.LightGreen;
public Brush CorAccent
{
get => _corAccent;
set { _corAccent = value; OnPropertyChanged(nameof(CorAccent)); }
}
private Brush _corBadge = Brushes.LightGreen;
public Brush CorBadge public Brush CorBadge
{ {
get => _corBadge; get => _corBadge;
set set { _corBadge = value; OnPropertyChanged(nameof(CorBadge)); }
{
if (_corBadge != value)
{
_corBadge = value;
OnPropertyChanged(nameof(CorBadge));
}
}
} }
private Brush _corTexto = Brushes.White; private Brush _corTexto = Brushes.White;
public Brush CorTexto public Brush CorTexto
{ {
get => _corTexto; get => _corTexto;
set set { _corTexto = value; OnPropertyChanged(nameof(CorTexto)); }
{
if (_corTexto != value)
{
_corTexto = value;
OnPropertyChanged(nameof(CorTexto));
}
}
} }
public void DefinirStatusNormal(string contexto, string mensagem) private Brush _corTextoBadge = Brushes.Black;
public Brush CorTextoBadge
{ {
ContextoSelecionado = contexto; get => _corTextoBadge;
MensagemStatus = mensagem; set { _corTextoBadge = value; OnPropertyChanged(nameof(CorTextoBadge)); }
}
public void MockarNormal()
{
ContextoSelecionado = "ROVER 01";
TituloStatus = "ROVER OPERANDO NORMALMENTE";
MotivoPrincipal = "Todos os módulos obrigatórios e regras de controle estão OK.";
BadgeTexto = "NORMAL"; BadgeTexto = "NORMAL";
CorFundo = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#1F3A2D")); ResumoCurto = "Sistema saudável";
CorFundo = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#153D2C"));
CorBorda = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#2C7A57"));
CorAccent = Brushes.LightGreen;
CorBadge = Brushes.LightGreen; CorBadge = Brushes.LightGreen;
CorTexto = Brushes.White; CorTexto = Brushes.White;
CorTextoBadge = Brushes.Black;
} }
public void DefinirStatusManual(string contexto, string mensagem) public void MockarParadoPorInclinacao()
{ {
ContextoSelecionado = contexto; ContextoSelecionado = "ROVER 01";
MensagemStatus = mensagem; TituloStatus = "ROVER PARADO POR SEGURANÇA";
BadgeTexto = "MANUAL"; MotivoPrincipal = "Inclinação lateral acima do limite permitido.";
CorFundo = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#2D3440")); BadgeTexto = "PARADO";
CorBadge = Brushes.Gold; ResumoCurto = "Verificar IMU";
CorTexto = Brushes.White;
}
public void DefinirStatusAtencao(string contexto, string mensagem)
{
ContextoSelecionado = contexto;
MensagemStatus = mensagem;
BadgeTexto = "ATENÇÃO";
CorFundo = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4A3315"));
CorBadge = Brushes.Orange;
CorTexto = Brushes.White;
}
public void DefinirStatusCritico(string contexto, string mensagem)
{
ContextoSelecionado = contexto;
MensagemStatus = mensagem;
BadgeTexto = "CRÍTICO";
CorFundo = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4A1F1F")); CorFundo = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4A1F1F"));
CorBorda = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#9B3A3A"));
CorAccent = Brushes.OrangeRed;
CorBadge = Brushes.OrangeRed; CorBadge = Brushes.OrangeRed;
CorTexto = Brushes.White; CorTexto = Brushes.White;
CorTextoBadge = Brushes.Black;
}
public void MockarObstaculo()
{
ContextoSelecionado = "ROVER 01";
TituloStatus = "MOVIMENTO BLOQUEADO PREVENTIVAMENTE";
MotivoPrincipal = "Obstáculo detectado no corredor à frente.";
BadgeTexto = "ATENÇÃO";
ResumoCurto = "Inspecionar caminho";
CorFundo = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4A3315"));
CorBorda = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#B7791F"));
CorAccent = Brushes.Orange;
CorBadge = Brushes.Orange;
CorTexto = Brushes.White;
CorTextoBadge = Brushes.Black;
} }
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
@ -139,4 +140,4 @@ namespace OperationControl.ViewModels.Views.Operacao
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nome)); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nome));
} }
} }
} }

View File

@ -540,9 +540,9 @@ namespace OperationControl.ViewModels
// ESQUERDA // ESQUERDA
var conexao = obj.ModulosSaude?.FirstOrDefault(x => x.modulo == AgroBase.Models.Enums.T_Code.Ipb); var conexao = obj.ModulosSaude?.FirstOrDefault(x => x.modulo == AgroBase.Models.Enums.T_Code.Ipb);
double c = conexao?.saude ?? 0; double c = conexao?.saude ?? 0;
double conexao_latencia = conexao?.detalhes?["avg_rtt"]?.Value<double>() ?? 0.0; double conexao_latencia = conexao?.detalhes?["avg_rtt"]?.Value<double?>() ?? 0.0;
double conexao_perda = conexao?.detalhes?["loss_pct"]?.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; double bwTot = conexao?.detalhes?["bw_total_mbps"]?.Value<double?>() ?? 0.0;
string comunicacao = $"{conexao_latencia:0.00} ms {bwTot:0.00} Mbps"; string comunicacao = $"{conexao_latencia:0.00} ms {bwTot:0.00} Mbps";
string comunicacao_extended = $"{conexao_latencia:0.00} ms {conexao_perda:F2}% {bwTot:0.00} Mbps"; string comunicacao_extended = $"{conexao_latencia:0.00} ms {conexao_perda:F2}% {bwTot:0.00} Mbps";
@ -565,8 +565,8 @@ namespace OperationControl.ViewModels
r.Modo = obj.Operacao == null ? "-" : obj.Operacao.Modo.ToString(); r.Modo = obj.Operacao == null ? "-" : obj.Operacao.Modo.ToString();
r.Status = obj.Operacao == null ? "-" : $"{obj.Operacao.Status}" + (obj.Operacao.Status == AgroBase.Models.Enums.StatusOperacao.Aguardando ? $" ({obj.Operacao.TempoAguardandoSegs:0}s)" : ""); r.Status = obj.Operacao == null ? "-" : $"{obj.Operacao.Status}" + (obj.Operacao.Status == AgroBase.Models.Enums.StatusOperacao.Aguardando ? $" ({obj.Operacao.TempoAguardandoSegs:0}s)" : "");
r.Carro = obj.Trajetoria == null ? "-" : obj.Trajetoria.StatusCarro.ToString(); r.Carro = obj.Trajetoria == null ? "-" : obj.Trajetoria.StatusCarro.ToString();
r.Temperatura = $"{(obj.PerformanceNcp?.CPU_temp ?? 0):0.00} °C"; r.Temperatura = $"{(obj.Refrigeracao?.Temperatura ?? 0):F2} °C";
r.RuaAtual = obj.Trajetoria == null ? "-" : $"{obj.Trajetoria.CorredorAtualDistanciaPercorrida:0.00} m / " + $"{obj.Trajetoria.CorredorAtualDistanciaTotal:0.00} m " + $"({obj.Trajetoria.CorredorAtualIdx + 1})"; r.RuaAtual = obj.Trajetoria == null ? "-" : $"{obj.Trajetoria.CorredorAtualDistanciaPercorrida:F2} m / " + $"{obj.Trajetoria.CorredorAtualDistanciaTotal:0.00} m " + $"({obj.Trajetoria.CorredorAtualIdx + 1})";
r.AreaTotal = obj.Trajetoria == null ? "-" : $"{obj.Trajetoria.DistanciaPercorrida:0.00} m / " + $"{obj.Trajetoria.DistanciaTotal:0.00} m"; r.AreaTotal = obj.Trajetoria == null ? "-" : $"{obj.Trajetoria.DistanciaPercorrida:0.00} m / " + $"{obj.Trajetoria.DistanciaTotal:0.00} m";
r.Bateria = obj.Bateria == null ? "-" : $"{obj.Bateria.TensaoInstantanea:0.00} V " + $"({TimeSpan.FromMinutes(obj.Bateria.TempoEstimadoRestanteMinutos):dd\\.hh\\:mm\\:ss} " + $"{obj.Bateria.DistanciaEstimadaRestanteMetros:0.00} m)"; r.Bateria = obj.Bateria == null ? "-" : $"{obj.Bateria.TensaoInstantanea:0.00} V " + $"({TimeSpan.FromMinutes(obj.Bateria.TempoEstimadoRestanteMinutos):dd\\.hh\\:mm\\:ss} " + $"{obj.Bateria.DistanciaEstimadaRestanteMetros:0.00} m)";
r.Herbicida = obj.Atuador == null ? "-" : $"{obj.Atuador.VolumeReservatorioL:0.00} L " + $"({TimeSpan.FromMinutes(obj.Atuador.TempoEstimadoRestanteMinutos):hh\\:mm\\:ss} " + $"{obj.Atuador.DistanciaEstimadaRestanteMetros:0.00} m)"; r.Herbicida = obj.Atuador == null ? "-" : $"{obj.Atuador.VolumeReservatorioL:0.00} L " + $"({TimeSpan.FromMinutes(obj.Atuador.TempoEstimadoRestanteMinutos):hh\\:mm\\:ss} " + $"{obj.Atuador.DistanciaEstimadaRestanteMetros:0.00} m)";
@ -620,45 +620,30 @@ namespace OperationControl.ViewModels
bico.TempoAtuado = _bc.TempoAtuado / 1000.0; bico.TempoAtuado = _bc.TempoAtuado / 1000.0;
} }
// HEADING
var HDG = _viewOperacaoCenter.Monitoramento.Heading;
HDG.Heading = obj?.Gnss?.OrientacaoReal ?? double.NaN;
HDG.CourseHeading = obj?.Trajetoria?.AnguloCaminho ?? double.NaN;
// ATTITUDE
var IMU = _viewOperacaoCenter.Monitoramento.Attitude;
if (obj?.Imu?.InclinacaoFrontal != null) IMU.PitchDeg = (double)obj.Imu.InclinacaoFrontal;
if (obj?.Imu?.InclinacaoLateral != null) IMU.RollDeg = (double)obj.Imu.InclinacaoLateral;
IMU.LateralError = erroLateral;
// CAMERAS // CAMERAS
var camera_snr = obj?.Cameras?.FirstOrDefault(x => x.dispositivo == AgroBase.Models.Enums.T_Code.Snr); var snr = obj?.Cameras?.FirstOrDefault(x => x.dispositivo == AgroBase.Models.Enums.T_Code.Snr);
var camera_cam = obj?.Cameras?.FirstOrDefault(x => x.dispositivo == AgroBase.Models.Enums.T_Code.Cam); var cam = obj?.Cameras?.FirstOrDefault(x => x.dispositivo == AgroBase.Models.Enums.T_Code.Cam);
_viewOperacaoCenter?.Monitoramento?._vm?.AtualizarDadosCameras(camera_snr?.fps, camera_snr?.stream_kbps, camera_cam?.fps, camera_cam?.stream_kbps, obj?.OperadorVisual?.StatusCarro, obj?.Atuador?.PercentualErvasNoRadar);
_viewOperacaoCenter?.Monitoramento?._vm?.AtualizarDadosCameras(snr, cam, obj?.OperadorVisual?.StatusCarro, obj?.Atuador?.PercentualErvasNoRadar);
} }
public void AtualizarTopOperacaoBase()
{
System.Windows.Application.Current.Dispatcher.BeginInvoke(new Action(() =>
{
_viewOperacaoTop?._vm.DefinirStatusNormal("BASE", "Base selecionada. Sistema pronto.");
}));
}
public void AtualizarTopOperacaoRover(string nomeRover, bool modoManual) public void AtualizarTopOperacaoRover(string nomeRover, bool modoManual)
{ {
System.Windows.Application.Current.Dispatcher.BeginInvoke(new Action(() => System.Windows.Application.Current.Dispatcher.BeginInvoke(new Action(() =>
{ {
if (modoManual) _viewOperacaoTop?._vm.MockarNormal();
_viewOperacaoTop?._vm.DefinirStatusManual($"{nomeRover}", "Rover em modo manual. Comandos locais liberados.");
else
_viewOperacaoTop?._vm.DefinirStatusNormal($"{nomeRover}", "Rover selecionado. Operação pronta.");
}));
}
public void AtualizarTopOperacaoAlerta(string contexto, string mensagem)
{
System.Windows.Application.Current.Dispatcher.BeginInvoke(new Action(() =>
{
_viewOperacaoTop?._vm.DefinirStatusAtencao(contexto, mensagem);
}));
}
public void AtualizarTopOperacaoCritico(string contexto, string mensagem)
{
System.Windows.Application.Current.Dispatcher.BeginInvoke(new Action(() =>
{
_viewOperacaoTop?._vm.DefinirStatusCritico(contexto, mensagem);
})); }));
} }

View File

@ -15,8 +15,8 @@
<Grid Margin="12"> <Grid Margin="12">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="2.2*"/> <RowDefinition Height="*"/>
<RowDefinition Height="1.3*"/> <RowDefinition Height="*"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
@ -42,9 +42,11 @@
</DockPanel> </DockPanel>
<Grid Grid.Row="1" Background="Black"> <Grid Grid.Row="1" Background="Black">
<TextBlock Text="SEM SINAL" Foreground="Gray" FontSize="18" HorizontalAlignment="Center" VerticalAlignment="Center" Visibility="{Binding CameraFrontalSemSinal, Converter={StaticResource BoolToVisibilityConverter}}"/>
<Image x:Name="imgVideoFront" Stretch="Uniform" RenderOptions.BitmapScalingMode="LowQuality"/> <Image x:Name="imgVideoFront" Stretch="Uniform" RenderOptions.BitmapScalingMode="LowQuality"/>
<Border Background="Black" Visibility="{Binding CameraFrontalSemSinal, Converter={StaticResource BoolToVisibilityConverter}}">
<TextBlock Text="SEM SINAL" Foreground="Gray" FontSize="18" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</Grid> </Grid>
</Grid> </Grid>
</Border> </Border>
@ -67,9 +69,11 @@
</DockPanel> </DockPanel>
<Grid Grid.Row="1" Background="Black"> <Grid Grid.Row="1" Background="Black">
<TextBlock Text="SEM SINAL" Foreground="Gray" FontSize="18" HorizontalAlignment="Center" VerticalAlignment="Center" Visibility="{Binding CameraErvasSemSinal, Converter={StaticResource BoolToVisibilityConverter}}"/>
<Image x:Name="imgVideoWeed" Stretch="Uniform" RenderOptions.BitmapScalingMode="LowQuality"/> <Image x:Name="imgVideoWeed" Stretch="Uniform" RenderOptions.BitmapScalingMode="LowQuality"/>
<Border Background="Black" Visibility="{Binding CameraErvasSemSinal, Converter={StaticResource BoolToVisibilityConverter}}">
<TextBlock Text="SEM SINAL" Foreground="Gray" FontSize="18" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</Grid> </Grid>
</Grid> </Grid>
</Border> </Border>
@ -85,15 +89,22 @@
<TextBlock Grid.Row="0" Text="Mapa e posição do rover" Margin="0,0,0,8" FontSize="12" FontWeight="SemiBold" Foreground="#E6E6E6"/> <TextBlock Grid.Row="0" Text="Mapa e posição do rover" Margin="0,0,0,8" FontSize="12" FontWeight="SemiBold" Foreground="#E6E6E6"/>
<controls:MapViewControl x:Name="MapaMonitoramento" MarkerClicked="MAP_MarkerClicked" StreetMapClicked="MAP_StreetMapClicked" Grid.Row="1" Latitude="-22.1726572492617" Longitude="-47.3952163870556" Scale="5000" /> <!-- Área do mapa com overlays -->
<Grid Grid.Row="1">
<controls:MapViewControl x:Name="MapaMonitoramento" MarkerClicked="MAP_MarkerClicked" StreetMapClicked="MAP_StreetMapClicked" Latitude="-22.1726572492617" Longitude="-47.3952163870556" Scale="5000" />
<controls:HeadingIndicator x:Name="areaHeading" Width="130" Height="130" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="2"/>
<controls:AttitudeIndicator x:Name="areaAttitude" Width="160" Height="180" CriticalLimit="20" WarningLimit="10" HorizontalAlignment="Left" VerticalAlignment="Bottom" Margin="2"/>
</Grid>
</Grid> </Grid>
</Border> </Border>
<!-- ÁREA DIREITA INFERIOR --> <!-- ÁREA DIREITA INFERIOR -->
<Grid Grid.Row="1" Grid.Column="1" Margin="6,6,0,0"> <Grid Grid.Row="1" Grid.Column="1" Margin="6,6,0,0">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="1.45*"/> <RowDefinition Height="2*"/>
<RowDefinition Height="0.55*"/> <RowDefinition Height="1*"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<!-- PULVERIZADOR --> <!-- PULVERIZADOR -->

View File

@ -14,6 +14,8 @@ namespace OperationControl.Views.Operacao
private TcpVideoReceiver videoFront; private TcpVideoReceiver videoFront;
private TcpVideoReceiver videoWeed; private TcpVideoReceiver videoWeed;
public PulverizadorIndicator Pulverizador => areaPulverizador; public PulverizadorIndicator Pulverizador => areaPulverizador;
public HeadingIndicator Heading => areaHeading;
public AttitudeIndicator Attitude => areaAttitude;
public MonitoramentoView() public MonitoramentoView()
{ {
@ -58,7 +60,6 @@ namespace OperationControl.Views.Operacao
videoFront.Iniciar(); videoFront.Iniciar();
var tipo = _vm.CameraFrontalFrameTipoSelecionado?.Valor ?? AgroBase.Models.Enums.TipoFrameCamera.Rgb; var tipo = _vm.CameraFrontalFrameTipoSelecionado?.Valor ?? AgroBase.Models.Enums.TipoFrameCamera.Rgb;
VariaveisControleOperacao.EnviarComandoTransmissaoVideo(AgroBase.Models.Enums.T_Code.Snr, true, tipo); VariaveisControleOperacao.EnviarComandoTransmissaoVideo(AgroBase.Models.Enums.T_Code.Snr, true, tipo);
//_vm.CameraFrontalSemSinal = false;
} }
public void PararStreamCameraFrontal(bool finalizar = false) public void PararStreamCameraFrontal(bool finalizar = false)
@ -66,7 +67,6 @@ namespace OperationControl.Views.Operacao
videoFront.Parar(); videoFront.Parar();
var tipo = _vm.CameraFrontalFrameTipoSelecionado?.Valor ?? AgroBase.Models.Enums.TipoFrameCamera.Rgb; var tipo = _vm.CameraFrontalFrameTipoSelecionado?.Valor ?? AgroBase.Models.Enums.TipoFrameCamera.Rgb;
VariaveisControleOperacao.EnviarComandoTransmissaoVideo(AgroBase.Models.Enums.T_Code.Snr, false, tipo); VariaveisControleOperacao.EnviarComandoTransmissaoVideo(AgroBase.Models.Enums.T_Code.Snr, false, tipo);
//_vm.CameraFrontalSemSinal = true;
} }
#endregion #endregion
@ -87,7 +87,6 @@ namespace OperationControl.Views.Operacao
videoWeed.Iniciar(); videoWeed.Iniciar();
var tipo = _vm.CameraErvasFrameTipoSelecionado?.Valor ?? AgroBase.Models.Enums.TipoFrameCamera.Rgb; var tipo = _vm.CameraErvasFrameTipoSelecionado?.Valor ?? AgroBase.Models.Enums.TipoFrameCamera.Rgb;
VariaveisControleOperacao.EnviarComandoTransmissaoVideo(AgroBase.Models.Enums.T_Code.Cam, true, tipo); VariaveisControleOperacao.EnviarComandoTransmissaoVideo(AgroBase.Models.Enums.T_Code.Cam, true, tipo);
//_vm.CameraErvasSemSinal = false;
} }
public void PararStreamCameraErvas(bool finalizar = false) public void PararStreamCameraErvas(bool finalizar = false)
@ -95,7 +94,6 @@ namespace OperationControl.Views.Operacao
videoWeed.Parar(); videoWeed.Parar();
var tipo = _vm.CameraErvasFrameTipoSelecionado?.Valor ?? AgroBase.Models.Enums.TipoFrameCamera.Rgb; var tipo = _vm.CameraErvasFrameTipoSelecionado?.Valor ?? AgroBase.Models.Enums.TipoFrameCamera.Rgb;
VariaveisControleOperacao.EnviarComandoTransmissaoVideo(AgroBase.Models.Enums.T_Code.Cam, false, tipo); VariaveisControleOperacao.EnviarComandoTransmissaoVideo(AgroBase.Models.Enums.T_Code.Cam, false, tipo);
//_vm.CameraErvasSemSinal = true;
} }
#endregion #endregion

View File

@ -1,73 +1,82 @@
<UserControl x:Class="OperationControl.Views.Operacao.OperacaoTopView" <UserControl x:Class="OperationControl.Views.Operacao.OperacaoTopView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:OperationControl.Views.Operacao" mc:Ignorable="d"
mc:Ignorable="d" d:DesignWidth="1400"
d:DesignHeight="70" d:DesignWidth="1200"> d:DesignHeight="92">
<Border Background="{Binding CorFundo}"
CornerRadius="10"
Padding="14,10"
Margin="10,8,10,6">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="260"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="160"/>
</Grid.ColumnDefinitions>
<!-- Contexto --> <Grid Height="92" Margin="10,0,10,0">
<Border Grid.Column="0" <Border Background="{Binding CorFundo}" BorderBrush="{Binding CorBorda}" BorderThickness="1" CornerRadius="12" Padding="12,10">
Background="#22000000"
CornerRadius="8"
Padding="12,8"
Margin="0,0,12,0">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="Selecionado: "
Foreground="#D8D8D8"
FontSize="14"
VerticalAlignment="Center"/>
<TextBlock Text="{Binding ContextoSelecionado}"
Foreground="{Binding CorTexto}"
FontSize="16"
FontWeight="Bold"
VerticalAlignment="Center"/>
</StackPanel>
</Border>
<!-- Mensagem principal --> <Grid>
<Border Grid.Column="1" <Grid.ColumnDefinitions>
Background="#16000000" <ColumnDefinition Width="240"/>
CornerRadius="8" <ColumnDefinition Width="10"/>
Padding="14,8" <ColumnDefinition Width="*"/>
Margin="0,0,12,0"> <ColumnDefinition Width="180"/>
<TextBlock Text="{Binding MensagemStatus}" </Grid.ColumnDefinitions>
Foreground="{Binding CorTexto}"
FontSize="17"
FontWeight="SemiBold"
TextWrapping="Wrap"
VerticalAlignment="Center"
HorizontalAlignment="Center"
TextAlignment="Center"/>
</Border>
<!-- Badge --> <!-- BLOCO ESQUERDO -->
<Border Grid.Column="2" <Border Grid.Column="0" Background="#1AFFFFFF" CornerRadius="10" Padding="12,8">
Background="{Binding CorBadge}" <Grid>
CornerRadius="18" <Grid.RowDefinitions>
Padding="12,8" <RowDefinition Height="18"/>
HorizontalAlignment="Right" <RowDefinition Height="28"/>
VerticalAlignment="Center" </Grid.RowDefinitions>
MinWidth="120">
<TextBlock Text="{Binding BadgeTexto}" <TextBlock Grid.Row="0" Text="ROVER / CONTEXTO" Foreground="#D8D8D8" FontSize="11" FontWeight="SemiBold" VerticalAlignment="Center"/>
Foreground="Black"
FontWeight="Bold" <Viewbox Grid.Row="1" Stretch="Uniform" StretchDirection="DownOnly" HorizontalAlignment="Left" VerticalAlignment="Center" Height="24">
FontSize="14" <TextBlock Text="{Binding ContextoSelecionado}" Foreground="{Binding CorTexto}" FontSize="18" FontWeight="Bold" TextWrapping="NoWrap"/>
HorizontalAlignment="Center" </Viewbox>
VerticalAlignment="Center" </Grid>
TextAlignment="Center"/> </Border>
</Border>
</Grid> <!-- BARRA VERTICAL -->
</Border> <Border Grid.Column="1" Width="4" Margin="0,4" Background="{Binding CorAccent}" CornerRadius="2"/>
</UserControl>
<!-- BLOCO CENTRAL -->
<Border Grid.Column="2" Margin="10,0,10,0" Background="#14000000" CornerRadius="10" Padding="14,8">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition Height="24"/>
</Grid.RowDefinitions>
<!-- STATUS PRINCIPAL -->
<Viewbox Grid.Row="0" Stretch="Uniform" StretchDirection="DownOnly" HorizontalAlignment="Left" VerticalAlignment="Center" Height="26">
<TextBlock Text="{Binding TituloStatus}" Foreground="{Binding CorTexto}" FontSize="24" FontWeight="Bold" TextWrapping="NoWrap"/>
</Viewbox>
<!-- MOTIVO -->
<Viewbox Grid.Row="1" Stretch="Uniform" StretchDirection="DownOnly" HorizontalAlignment="Left" VerticalAlignment="Center" Height="18">
<TextBlock Text="{Binding MotivoPrincipal}" Foreground="#F2F2F2" FontSize="14" FontWeight="SemiBold" TextWrapping="NoWrap"/>
</Viewbox>
</Grid>
</Border>
<!-- BLOCO DIREITO -->
<Border Grid.Column="3" Background="#1AFFFFFF" CornerRadius="10" Padding="10,8">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="28"/>
<RowDefinition Height="24"/>
</Grid.RowDefinitions>
<Border Grid.Row="0" Background="{Binding CorBadge}" CornerRadius="14" Padding="10,4" HorizontalAlignment="Stretch" VerticalAlignment="Center">
<Viewbox Stretch="Uniform" StretchDirection="DownOnly" Height="18">
<TextBlock Text="{Binding BadgeTexto}" Foreground="{Binding CorTextoBadge}" FontWeight="Bold" FontSize="13" TextWrapping="NoWrap" HorizontalAlignment="Center"/>
</Viewbox>
</Border>
<Viewbox Grid.Row="1" Stretch="Uniform" StretchDirection="DownOnly" Height="16" VerticalAlignment="Bottom">
<TextBlock Text="{Binding ResumoCurto}" Foreground="{Binding CorTexto}" FontSize="11" FontWeight="SemiBold" TextWrapping="NoWrap" HorizontalAlignment="Center"/>
</Viewbox>
</Grid>
</Border>
</Grid>
</Border>
</Grid>
</UserControl>

View File

@ -1,24 +1,7 @@
using OperationControl.ViewModels.Views.Operacao; using OperationControl.ViewModels.Views.Operacao;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace OperationControl.Views.Operacao namespace OperationControl.Views.Operacao
{ {
/// <summary>
/// Interaction logic for OperacaoTopView.xaml
/// </summary>
public partial class OperacaoTopView : System.Windows.Controls.UserControl public partial class OperacaoTopView : System.Windows.Controls.UserControl
{ {
public OperacaoTopViewModel _vm; public OperacaoTopViewModel _vm;
@ -28,8 +11,6 @@ namespace OperationControl.Views.Operacao
InitializeComponent(); InitializeComponent();
_vm = new OperacaoTopViewModel(); _vm = new OperacaoTopViewModel();
DataContext = _vm; DataContext = _vm;
_vm.DefinirStatusNormal("BASE", "Sistema pronto para operação.");
} }
} }
} }

View File

@ -14,11 +14,11 @@
<Grid> <Grid>
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="100"/> <RowDefinition Height="110"/>
<!-- Topo --> <!-- Topo -->
<RowDefinition Height="*"/> <RowDefinition Height="*"/>
<!-- Centro --> <!-- Centro -->
<RowDefinition Height="120"/> <RowDefinition Height="90"/>
<!-- Rodapé --> <!-- Rodapé -->
</Grid.RowDefinitions> </Grid.RowDefinitions>
@ -32,24 +32,17 @@
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<!-- Topo (span em 3 colunas) --> <!-- Topo (span em 3 colunas) -->
<Border Grid.Row="0" Grid.ColumnSpan="3" <Border Grid.Row="0" Grid.ColumnSpan="3" Background="#222">
Background="#222"
Padding="8">
<ContentControl Content="{Binding TopContent}"/> <ContentControl Content="{Binding TopContent}"/>
</Border> </Border>
<!-- Área central principal --> <!-- Área central principal -->
<Border Grid.Row="1" Grid.Column="1" <Border Grid.Row="1" Grid.Column="1" Margin="8" BorderThickness="1" BorderBrush="Gray">
Margin="8"
BorderThickness="1"
BorderBrush="Gray">
<ContentControl Content="{Binding CenterContent}"/> <ContentControl Content="{Binding CenterContent}"/>
</Border> </Border>
<!-- Rodapé (span 3 colunas) --> <!-- Rodapé (span 3 colunas) -->
<Border Grid.Row="2" Grid.ColumnSpan="3" <Border Grid.Row="2" Grid.ColumnSpan="3" Background="#181818" Padding="5">
Background="#181818"
Padding="8">
<ContentControl Content="{Binding BottomContent}"/> <ContentControl Content="{Binding BottomContent}"/>
</Border> </Border>
@ -85,11 +78,7 @@
</Grid> </Grid>
<!-- Lateral direita --> <!-- Lateral direita -->
<Border Grid.Row="1" Grid.Column="2" <Border Grid.Row="1" Grid.Column="2" Margin="8" BorderThickness="1" BorderBrush="#404040" Background="#1C1C1C">
Margin="8"
BorderThickness="1"
BorderBrush="#404040"
Background="#1C1C1C">
<ContentControl Content="{Binding RightContent}"/> <ContentControl Content="{Binding RightContent}"/>
</Border> </Border>
</Grid> </Grid>