agrobot_base/Python/raspi/cam_2/pi/server.py

537 lines
20 KiB
Python

import socket
import traceback
import threading
from protocol import decode_message, encode_message
from state import ModuleState
def parse_bool(value):
if isinstance(value, bool):
return value
if isinstance(value, (int, float)):
return bool(value)
if isinstance(value, str):
s = value.strip().lower()
if s in ("1", "true", "yes", "on"):
return True
if s in ("0", "false", "no", "off"):
return False
raise ValueError("Valor booleano inválido")
def parse_frame_type(value):
valid = {"RAW_BRUTO", "RGB", "RGBNIR"}
if value not in valid:
raise ValueError(f"frame_type inválido: {value}")
return value
def parse_output_dtype(value):
valid = {"uint8", "uint16", "float32"}
if value not in valid:
raise ValueError(f"output_dtype inválido: {value}")
return value
def parse_capture_mode(value):
valid = {"AUTO", "SINGLE", "DUAL"}
if value not in valid:
raise ValueError(f"capture_mode inválido: {value}")
return value
def parse_bayer_pattern(value):
valid = {"GBRG", "GRBG", "RGGB", "BGGR"}
if value not in valid:
raise ValueError(f"bayer_pattern inválido: {value}")
return value
class ModuleServer:
def __init__(self, host="0.0.0.0", port=5000):
self.host = host
self.port = port
self.state = ModuleState()
self.running = False
self.lock = threading.RLock()
from camera_manager import CameraManager
from trigger_manager import TriggerManager
from frame_service import FrameService
from stream_sender import StreamSender
self.camera = CameraManager(self.state)
self.trigger = TriggerManager(
pin=self.state.trigger_pin,
active_high=self.state.trigger_active_high,
pulse_ms=self.state.trigger_pulse_ms
)
self.frame_service = FrameService(self.state, self.trigger, self.camera)
self.stream_sender = StreamSender(self.state, self.frame_service)
def _mark_reconfigure_needed(self):
if hasattr(self.camera, "mark_reconfigure_needed"):
self.camera.mark_reconfigure_needed()
def _get_active_cameras(self):
return [cam for cam in self.state.cameras if cam.connected and cam.enabled]
def _get_primary_camera(self):
active = self._get_active_cameras()
if active:
return active[0]
if self.state.cameras:
return self.state.cameras[0]
return None
def _payload_dict(self):
payload = self.state.payload
return {
"payload_format_version": self.state.payload_format_version,
"frame_type": self.state.frame_type,
"output_dtype": self.state.output_dtype,
"output_layout": payload.layout,
"output_channels": payload.channels,
"output_channel_names": payload.channel_names,
"output_width": payload.width,
"output_height": payload.height,
"payload_sources": payload.sources,
}
def _source_dict(self):
cam = self._get_primary_camera()
if cam is None:
return {
"source_camera": None,
"source_width": 0,
"source_height": 0,
"source_bayer_pattern": None,
"source_bit_depth": None,
}
return {
"source_camera": cam.id,
"source_width": cam.width,
"source_height": cam.height,
"source_bayer_pattern": cam.bayer_pattern,
"source_bit_depth": cam.bit_depth,
}
def _build_config_response(self):
return {
"ok": True,
"module": self.state.module_name,
"version": self.state.version,
"status": self.state.status,
"initialized": self.state.initialized,
"streaming": self.state.streaming,
"fps": self.state.fps,
"jpeg_quality": self.state.jpeg_quality,
"capture_mode": self.state.capture_mode,
"detected_mode": self.state.detected_mode,
"camera_count_detected": self.state.camera_count_detected,
"camera_count_active": self.state.camera_count_active,
"active_camera_ids": self.state.active_camera_ids,
"cameras": [cam.to_dict() for cam in self.state.cameras],
**self._payload_dict(),
**self._source_dict(),
}
def handle_command(self, msg: dict) -> dict:
with self.lock:
cmd = msg.get("cmd")
self.state.last_command = cmd
try:
if cmd == "ping":
return {"ok": True, "reply": "pong"}
if cmd == "get_status":
return {"ok": True, **self.state.to_dict()}
if cmd == "get_config":
return self._build_config_response()
if cmd == "begin":
frame_type = parse_frame_type(msg.get("frame_type", self.state.frame_type))
output_dtype = parse_output_dtype(msg.get("output_dtype", self.state.output_dtype))
capture_mode = parse_capture_mode(msg.get("capture_mode", self.state.capture_mode))
self.state.frame_type = frame_type
self.state.output_dtype = output_dtype
self.state.capture_mode = capture_mode
self.state.update_payload_spec()
if self.state.initialized:
return {
"ok": True,
"status": self.state.status,
"initialized": True,
**self._payload_dict(),
**self._source_dict(),
}
self.state.status = "initializing"
self.state.status_detail = None
self.trigger.begin()
self.camera.begin()
self.state.initialized = True
self.state.status = "ready"
self.state.last_error = None
return {
"ok": True,
"status": self.state.status,
"initialized": True,
**self._payload_dict(),
**self._source_dict(),
}
if cmd == "stop":
self.state.status = "stopping"
if getattr(self.stream_sender, "is_running", False):
self.stream_sender.stop()
self.camera.stop()
self.trigger.stop()
self.state.initialized = False
self.state.streaming = False
self.state.status = "idle"
self.state.status_detail = None
return {"ok": True, "status": self.state.status}
if cmd == "capture_frame":
data = self.frame_service.capture_frame_base64()
return {"ok": True, **data}
if cmd == "set_fps":
value = int(msg.get("value"))
if value <= 0 or value > 120:
return {"ok": False, "error": "fps inválido"}
self.state.fps = value
self._mark_reconfigure_needed()
return {"ok": True, "fps": self.state.fps}
if cmd == "set_jpeg_quality":
value = int(msg.get("value"))
if value < 1 or value > 100:
return {"ok": False, "error": "jpeg_quality inválido"}
self.state.jpeg_quality = value
return {"ok": True, "jpeg_quality": self.state.jpeg_quality}
if cmd == "set_frame_type":
frame_type = parse_frame_type(msg.get("value"))
self.state.frame_type = frame_type
self.state.update_payload_spec()
self._mark_reconfigure_needed()
return {
"ok": True,
"frame_type": self.state.frame_type,
**self._payload_dict(),
}
if cmd == "set_output_dtype":
output_dtype = parse_output_dtype(msg.get("value"))
self.state.output_dtype = output_dtype
self.state.update_payload_spec()
self._mark_reconfigure_needed()
return {
"ok": True,
"output_dtype": self.state.output_dtype,
**self._payload_dict(),
}
if cmd == "set_capture_mode":
capture_mode = parse_capture_mode(msg.get("value"))
self.state.capture_mode = capture_mode
self.state.update_payload_spec()
self._mark_reconfigure_needed()
return {
"ok": True,
"capture_mode": self.state.capture_mode,
"detected_mode": self.state.detected_mode,
**self._payload_dict(),
}
if cmd == "set_camera_enabled":
index = int(msg.get("index"))
enabled = parse_bool(msg.get("enabled"))
self.state.set_camera_enabled(index, enabled)
self._mark_reconfigure_needed()
cam = self.state.get_camera_by_index(index)
return {
"ok": True,
"camera": cam.to_dict(),
"camera_count_active": self.state.camera_count_active,
"active_camera_ids": self.state.active_camera_ids,
**self._payload_dict(),
}
if cmd == "set_camera_bayer":
index = int(msg.get("index"))
pattern = parse_bayer_pattern(msg.get("pattern"))
cam = self.state.get_camera_by_index(index)
if cam is None:
return {"ok": False, "error": f"Câmera de índice {index} não existe"}
cam.bayer_pattern = pattern
self.state.update_payload_spec()
self._mark_reconfigure_needed()
return {
"ok": True,
"camera": cam.to_dict(),
**self._source_dict(),
}
if cmd == "set_camera_resolution":
index = int(msg.get("index"))
width = int(msg.get("width"))
height = int(msg.get("height"))
if width <= 0 or height <= 0:
return {"ok": False, "error": "resolução inválida"}
cam = self.state.get_camera_by_index(index)
if cam is None:
return {"ok": False, "error": f"Câmera de índice {index} não existe"}
cam.width = width
cam.height = height
self.state.update_payload_spec()
self._mark_reconfigure_needed()
return {
"ok": True,
"camera": cam.to_dict(),
**self._payload_dict(),
**self._source_dict(),
}
if cmd == "start_stream":
host = msg.get("host")
port = int(msg.get("port"))
fps = float(msg.get("fps", self.state.fps))
if not host:
return {"ok": False, "error": "host obrigatório"}
if port <= 0 or port > 65535:
return {"ok": False, "error": "porta inválida"}
if fps <= 0:
return {"ok": False, "error": "fps inválido"}
if getattr(self.stream_sender, "is_running", False):
return {
"ok": True,
"streaming": True,
"host": self.state.stream_host,
"port": self.state.stream_port,
"fps": self.state.stream_fps,
**self._payload_dict(),
**self._source_dict(),
}
self.stream_sender.start(host, port, fps)
self.state.streaming = True
self.state.stream_host = host
self.state.stream_port = port
self.state.stream_fps = fps
self.state.status = "streaming"
return {
"ok": True,
"streaming": True,
"host": host,
"port": port,
"fps": fps,
**self._payload_dict(),
**self._source_dict(),
}
if cmd == "stop_stream":
if getattr(self.stream_sender, "is_running", False):
self.stream_sender.stop()
self.state.streaming = False
self.state.stream_host = None
self.state.stream_port = None
self.state.stream_fps = None
self.state.status = "ready" if self.state.initialized else "idle"
return {
"ok": True,
"streaming": False,
"status": self.state.status
}
if cmd == "set_ae_enable":
self.state.ae_enable = parse_bool(msg.get("value"))
if self.camera.initialized:
self.camera.apply_controls()
return {"ok": True, "ae_enable": self.state.ae_enable}
if cmd == "set_awb_enable":
self.state.awb_enable = parse_bool(msg.get("value"))
if self.camera.initialized:
self.camera.apply_controls()
return {"ok": True, "awb_enable": self.state.awb_enable}
if cmd == "set_exposure_time":
value = msg.get("value")
if value is not None:
value = int(value)
if value <= 0:
return {"ok": False, "error": "ExposureTime inválido"}
self.state.exposure_time_us = value
if self.camera.initialized:
self.camera.apply_controls()
return {"ok": True, "exposure_time_us": self.state.exposure_time_us}
if cmd == "set_analogue_gain":
value = msg.get("value")
if value is not None:
value = float(value)
if value <= 0:
return {"ok": False, "error": "AnalogueGain inválido"}
self.state.analogue_gain = value
if self.camera.initialized:
self.camera.apply_controls()
return {"ok": True, "analogue_gain": self.state.analogue_gain}
if cmd == "set_colour_gains":
r_gain = msg.get("r_gain")
b_gain = msg.get("b_gain")
if r_gain is None or b_gain is None:
return {"ok": False, "error": "r_gain e b_gain são obrigatórios"}
r_gain = float(r_gain)
b_gain = float(b_gain)
if r_gain <= 0 or b_gain <= 0:
return {"ok": False, "error": "ColourGains inválidos"}
self.state.colour_gains = [r_gain, b_gain]
if self.camera.initialized:
self.camera.apply_controls()
return {"ok": True, "colour_gains": self.state.colour_gains}
if cmd == "clear_exposure_time":
self.state.exposure_time_us = None
if self.camera.initialized:
self.camera.apply_controls()
return {"ok": True, "exposure_time_us": None}
if cmd == "clear_analogue_gain":
self.state.analogue_gain = None
if self.camera.initialized:
self.camera.apply_controls()
return {"ok": True, "analogue_gain": None}
if cmd == "clear_colour_gains":
self.state.colour_gains = None
if self.camera.initialized:
self.camera.apply_controls()
return {"ok": True, "colour_gains": None}
if cmd == "get_camera_controls":
return {
"ok": True,
"ae_enable": self.state.ae_enable,
"awb_enable": self.state.awb_enable,
"exposure_time_us": self.state.exposure_time_us,
"analogue_gain": self.state.analogue_gain,
"colour_gains": self.state.colour_gains,
"fps": self.state.fps,
}
if cmd == "get_sensor_modes":
modes = self.camera.get_sensor_modes()
return {"ok": True, "sensor_modes": modes}
return {"ok": False, "error": f"Comando desconhecido: {cmd}"}
except Exception as e:
self.state.set_error(str(e))
return {"ok": False, "error": str(e)}
def client_thread(self, conn, addr):
print(f"[INFO] Cliente conectado: {addr}")
buffer = b""
try:
conn.settimeout(1.0)
while self.running:
try:
chunk = conn.recv(4096)
except socket.timeout:
continue
if not chunk:
break
buffer += chunk
while b"\n" in buffer:
line, buffer = buffer.split(b"\n", 1)
if not line.strip():
continue
try:
msg = decode_message(line.decode("utf-8"))
response = self.handle_command(msg)
except Exception as e:
response = {"ok": False, "error": str(e)}
conn.sendall(encode_message(response))
except Exception as e:
print(f"[ERRO] Cliente {addr}: {e}")
traceback.print_exc()
finally:
try:
conn.close()
except Exception:
pass
print(f"[INFO] Cliente desconectado: {addr}")
def start(self):
self.running = True
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind((self.host, self.port))
server_socket.listen(5)
server_socket.settimeout(1.0)
print(f"[INFO] Servidor ouvindo em {self.host}:{self.port}")
while self.running:
try:
conn, addr = server_socket.accept()
except socket.timeout:
continue
t = threading.Thread(
target=self.client_thread,
args=(conn, addr),
daemon=True
)
t.start()