agrobot_base/Python/raspi/pi/server.py

489 lines
19 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", "float32"}
if value not in valid:
raise ValueError(f"output_dtype 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 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 {
"ok": True,
"fps": self.state.fps,
"jpeg_quality": self.state.jpeg_quality,
"width": self.state.width,
"height": self.state.height,
"camera_id": self.state.camera_id,
"payload_format_version": self.state.payload_format_version,
"source_bayer_pattern": self.state.source_bayer_pattern,
"source_bit_depth": self.state.source_bit_depth,
"frame_type": self.state.frame_type,
"output_dtype": self.state.output_dtype,
"output_layout": self.state.output_layout,
"output_channels": self.state.output_channels,
"output_channel_names": self.state.output_channel_names,
"output_width": self.state.output_width,
"output_height": self.state.output_height,
}
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))
# Atualiza a especificação de saída ANTES de inicializar
self.state.frame_type = frame_type
self.state.output_dtype = output_dtype
self.state.update_output_spec()
if self.state.initialized:
return {
"ok": True,
"status": self.state.status,
"initialized": True,
"payload_format_version": self.state.payload_format_version,
"frame_type": self.state.frame_type,
"output_dtype": self.state.output_dtype,
"output_layout": self.state.output_layout,
"output_channels": self.state.output_channels,
"output_channel_names": self.state.output_channel_names,
"output_width": self.state.output_width,
"output_height": self.state.output_height,
"source_bayer_pattern": self.state.source_bayer_pattern,
"source_bit_depth": self.state.source_bit_depth,
}
self.state.status = "initializing"
self.state.status_detail = None
self.trigger.begin()
self.camera.begin()
self.state.initialized = True
self.state.camera_connected = True
self.state.status = "ready"
self.state.last_error = None
return {
"ok": True,
"status": self.state.status,
"initialized": True,
"payload_format_version": self.state.payload_format_version,
"frame_type": self.state.frame_type,
"output_dtype": self.state.output_dtype,
"output_layout": self.state.output_layout,
"output_channels": self.state.output_channels,
"output_channel_names": self.state.output_channel_names,
"output_width": self.state.output_width,
"output_height": self.state.output_height,
"source_bayer_pattern": self.state.source_bayer_pattern,
"source_bit_depth": self.state.source_bit_depth,
}
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.camera_connected = 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_bayer":
pattern = parse_bayer_pattern(msg.get("pattern", self.state.source_bayer_pattern))
self.state.source_bayer_pattern = pattern
return {
"ok": True,
"bayer_pattern": self.state.source_bayer_pattern
}
if cmd == "set_resolution":
width = int(msg.get("width"))
height = int(msg.get("height"))
if width <= 0 or height <= 0:
return {"ok": False, "error": "resolução inválida"}
self.state.width = width
self.state.height = height
self.state.update_output_spec()
self._mark_reconfigure_needed()
return {
"ok": True,
"width": self.state.width,
"height": self.state.height,
"output_width": self.state.output_width,
"output_height": self.state.output_height,
"frame_type": self.state.frame_type,
"output_layout": self.state.output_layout,
"output_channels": self.state.output_channels,
"output_channel_names": self.state.output_channel_names,
}
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,
"payload_format_version": self.state.payload_format_version,
"frame_type": self.state.frame_type,
"output_dtype": self.state.output_dtype,
"output_layout": self.state.output_layout,
"output_channels": self.state.output_channels,
"output_channel_names": self.state.output_channel_names,
"output_width": self.state.output_width,
"output_height": self.state.output_height,
"source_bayer_pattern": self.state.source_bayer_pattern,
"source_bit_depth": self.state.source_bit_depth,
}
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,
"payload_format_version": self.state.payload_format_version,
"frame_type": self.state.frame_type,
"output_dtype": self.state.output_dtype,
"output_layout": self.state.output_layout,
"output_channels": self.state.output_channels,
"output_channel_names": self.state.output_channel_names,
"output_width": self.state.output_width,
"output_height": self.state.output_height,
"source_bayer_pattern": self.state.source_bayer_pattern,
"source_bit_depth": self.state.source_bit_depth,
}
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
if self.state.initialized:
self.state.status = "ready"
else:
self.state.status = "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()