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()