agrobot_base/Python/raspi/cam_2/multispectral_service.py

332 lines
11 KiB
Python

import socket
import json
import numpy as np
import base64
import time
from typing import Optional
class MultiSpectralService:
def __init__(self, host="192.168.105.6", port=5000, timeout=5):
self.host = host
self.port = port
self.timeout = timeout
self.sock = None
self.file = None
def __enter__(self):
self.connect()
return self
def __exit__(self, exc_type, exc, tb):
self.disconnect()
def connect(self):
if self.sock is not None:
return
self.sock = socket.create_connection((self.host, self.port), timeout=self.timeout)
self.sock.settimeout(self.timeout)
self.file = self.sock.makefile("r", encoding="utf-8")
def disconnect(self):
try:
if self.file:
self.file.close()
except Exception:
pass
try:
if self.sock:
self.sock.close()
except Exception:
pass
self.file = None
self.sock = None
def check_connection(self, timeout: float = None) -> bool:
try:
self.connect()
resp = self._send_command({"cmd": "ping"})
return resp.get("ok") and resp.get("reply") == "pong"
except Exception:
return False
def is_alive(self) -> bool:
try:
self.connect()
resp = self._send_command({"cmd": "ping"})
return resp.get("ok") and resp.get("reply") == "pong"
except Exception:
return False
def ensure_alive(self):
try:
self.connect()
except Exception as e:
raise RuntimeError(
f"Não foi possível conectar ao módulo em {self.host}:{self.port}. "
f"Verifique rede, IP e se o Pi está ligado. Erro: {e}"
) from e
try:
resp = self._send_command({"cmd": "ping"})
except Exception as e:
raise RuntimeError(
f"Conectou ao endereço {self.host}:{self.port}, mas o módulo não respondeu ao ping. "
f"Verifique se o serviço está rodando no Pi. Erro: {e}"
) from e
if not resp.get("ok") or resp.get("reply") != "pong":
raise RuntimeError(
f"Resposta inválida do módulo ao ping: {resp}"
)
def _send_command(self, payload: dict) -> dict:
if self.sock is None:
self.connect()
data = (json.dumps(payload) + "\n").encode("utf-8")
self.sock.sendall(data)
line = self.file.readline()
if not line:
self.disconnect()
raise RuntimeError("Conexão encerrada pelo servidor")
return json.loads(line.strip())
def _numpy_dtype_from_string(self, dtype_str: str):
mapping = {
"uint8": np.uint8,
"float32": np.float32,
"uint16": np.uint16,
}
if dtype_str not in mapping:
raise RuntimeError(f"dtype não suportado recebido do Pi: {dtype_str}")
return mapping[dtype_str]
def _reshape_array(self, raw_bytes: bytes, dtype_str: str, layout: str, width: int, height: int, channels: int):
np_dtype = self._numpy_dtype_from_string(dtype_str)
arr = np.frombuffer(raw_bytes, dtype=np_dtype)
if layout == "HW":
return arr.reshape(height, width)
if layout == "CHW":
return arr.reshape(channels, height, width)
if layout == "HWC":
return arr.reshape(height, width, channels)
raise RuntimeError(f"Layout não suportado recebido do Pi: {layout}")
def ping(self):
return self._send_command({"cmd": "ping"})
def get_status(self):
return self._send_command({"cmd": "get_status"})
def get_config(self):
return self._send_command({"cmd": "get_config"})
def begin(self, frame_type: str = "RAW_BRUTO", output_dtype: str = "uint8", capture_mode: str = "AUTO"):
return self._send_command({
"cmd": "begin",
"frame_type": frame_type,
"output_dtype": output_dtype,
"capture_mode": capture_mode,
})
def stop(self):
return self._send_command({"cmd": "stop"})
def set_fps(self, fps: int):
return self._send_command({"cmd": "set_fps", "value": fps})
def set_jpeg_quality(self, quality: int):
return self._send_command({"cmd": "set_jpeg_quality", "value": quality})
def set_frame_type(self, frame_type: str):
return self._send_command({"cmd": "set_frame_type", "value": frame_type})
def set_output_dtype(self, output_dtype: str):
return self._send_command({"cmd": "set_output_dtype", "value": output_dtype})
def set_capture_mode(self, capture_mode: str):
return self._send_command({"cmd": "set_capture_mode", "value": capture_mode})
def set_camera_enabled(self, index: int, enabled: bool):
return self._send_command({
"cmd": "set_camera_enabled",
"index": index,
"enabled": bool(enabled)
})
def set_camera_bayer(self, index: int, bayer_pattern: str):
return self._send_command({
"cmd": "set_camera_bayer",
"index": index,
"pattern": bayer_pattern
})
def set_camera_resolution(self, index: int, width: int, height: int):
return self._send_command({
"cmd": "set_camera_resolution",
"index": index,
"width": width,
"height": height
})
def capture_frame(self):
t0 = time.perf_counter()
resp = self._send_command({"cmd": "capture_frame"})
if not resp.get("ok"):
raise RuntimeError(resp.get("error", "Falha ao capturar frame"))
encoding = resp.get("encoding", "base64")
output_layout = resp.get("output_layout", "HW")
dtype_str = resp.get("dtype") or resp.get("output_dtype") or "uint8"
meta = {
"frame_type": resp.get("frame_type"),
"payload_format_version": resp.get("payload_format_version"),
"output_dtype": resp.get("output_dtype"),
"dtype": dtype_str,
"output_layout": output_layout,
"output_channel_names": resp.get("output_channel_names"),
"payload_sources": resp.get("payload_sources"),
"source_camera": resp.get("source_camera"),
"source_cameras": resp.get("source_cameras"),
"camera_frames": resp.get("camera_frames"),
"multi_payload": resp.get("multi_payload", False),
"payload_parts": resp.get("payload_parts"),
"packed_width": resp.get("packed_width"),
"packed_height": resp.get("packed_height"),
"source_width": resp.get("source_width"),
"source_height": resp.get("source_height"),
"source_bayer_pattern": resp.get("source_bayer_pattern"),
"source_bit_depth": resp.get("source_bit_depth"),
"size": resp.get("size"),
"ts_pi": resp.get("ts_pi"),
"ts_pi_monotonic": resp.get("ts_pi_monotonic"),
"dt_trigger": resp.get("dt_trigger"),
"dt_settle": resp.get("dt_settle"),
"dt_capture": resp.get("dt_capture"),
"dt_process": resp.get("dt_process"),
"dt_total_pi": resp.get("dt_total_pi"),
}
if encoding == "base64":
raw = base64.b64decode(resp["data"])
width = int(resp.get("output_width", resp.get("width")))
height = int(resp.get("output_height", resp.get("height")))
channels = int(resp.get("output_channels", resp.get("channels", 1)))
arr = self._reshape_array(
raw_bytes=raw,
dtype_str=dtype_str,
layout=output_layout,
width=width,
height=height,
channels=channels,
)
meta.update({
"width": width,
"height": height,
"channels": channels,
"dt_total_pc": time.perf_counter() - t0,
})
return arr, meta
if encoding == "base64-multi":
frames_resp = resp.get("frames", {})
frames = {}
payload_parts = resp.get("payload_parts", [])
parts_by_cam = {p.get("camera_id"): p for p in payload_parts if p.get("camera_id")}
camera_frames = resp.get("camera_frames", {})
for cam_id, item in frames_resp.items():
raw = base64.b64decode(item["data"])
cam_meta = camera_frames.get(cam_id, {})
part_meta = parts_by_cam.get(cam_id, {})
width = int(cam_meta.get("width"))
height = int(cam_meta.get("height"))
channels = int(cam_meta.get("channels", 1))
# Para RAW bruto multi, cada parte tende a ser HW
arr = self._reshape_array(
raw_bytes=raw,
dtype_str=dtype_str if dtype_str != "multi" else "uint16",
layout="HW",
width=width,
height=height,
channels=channels,
)
frames[cam_id] = arr
meta.update({
"frames_meta": camera_frames,
"dt_total_pc": time.perf_counter() - t0,
})
return frames, meta
raise RuntimeError(f"encoding não suportado recebido do Pi: {encoding}")
def capture_frame_array(self):
return self.capture_frame()
def start_stream(self, host: str, port: int, fps: float):
return self._send_command({
"cmd": "start_stream",
"host": host,
"port": port,
"fps": fps
})
def stop_stream(self):
return self._send_command({"cmd": "stop_stream"})
def get_camera_controls(self):
return self._send_command({"cmd": "get_camera_controls"})
def set_ae_enable(self, value: bool):
return self._send_command({"cmd": "set_ae_enable", "value": bool(value)})
def set_awb_enable(self, value: bool):
return self._send_command({"cmd": "set_awb_enable", "value": bool(value)})
def set_exposure_time(self, exposure_time_us: Optional[int] = None):
return self._send_command({"cmd": "set_exposure_time", "value": exposure_time_us})
def clear_exposure_time(self):
return self._send_command({"cmd": "clear_exposure_time"})
def set_analogue_gain(self, gain: float | None):
return self._send_command({"cmd": "set_analogue_gain", "value": gain})
def clear_analogue_gain(self):
return self._send_command({"cmd": "clear_analogue_gain"})
def set_colour_gains(self, r_gain: float, b_gain: float):
return self._send_command({
"cmd": "set_colour_gains",
"r_gain": r_gain,
"b_gain": b_gain
})
def clear_colour_gains(self):
return self._send_command({"cmd": "clear_colour_gains"})
def get_sensor_modes(self):
return self._send_command({"cmd": "get_sensor_modes"})