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", "MULTISPEC"} 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", "DOUBLE", "TRIPLE"} 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) # ========================================================= # Helpers internos # ========================================================= def _mark_reconfigure_needed(self): if hasattr(self.camera, "mark_reconfigure_needed"): self.camera.mark_reconfigure_needed() def _is_reconfigure_needed(self): return bool(getattr(self.camera, "_reconfigure_needed", False)) def _restart_module_if_needed(self): """ Reinicia trigger + câmeras se o módulo já estiver inicializado e alguma configuração estrutural tiver mudado. """ if not self.state.initialized: return if not self._is_reconfigure_needed(): return if getattr(self.stream_sender, "is_running", False): self.stream_sender.stop() self.camera.stop() self.trigger.stop() self.trigger.begin() ok = self.camera.begin() if not ok: raise RuntimeError("Falha ao reinicializar câmeras após reconfiguração") self.state.initialized = True self.state.streaming = False self.state.status = "ready" self.state.status_detail = None self.state.last_error = None if hasattr(self.camera, "_reconfigure_needed"): self.camera._reconfigure_needed = False def _get_active_cameras(self): return [cam for cam in self.state.cameras if cam.connected and cam.enabled] def _get_primary_camera(self): """ Preferência de câmera 'primária': 1. RGB 2. RE 3. NIR 4. primeira ativa 5. primeira cadastrada """ for role in ("rgb", "re", "nir"): cam = self.state.get_active_camera_by_role(role) if cam is not None: return cam 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, "payload_complete": payload.complete, } def _source_dict(self): primary = self._get_primary_camera() if primary is None: return { "source_camera": None, "source_width": 0, "source_height": 0, "source_bayer_pattern": None, "source_bit_depth": None, "source_cameras": [], } source_cameras = [] for cam_id in self.state.payload.sources: cam = self.state.get_camera(cam_id) if cam is None: continue source_cameras.append({ "id": cam.id, "index": cam.index, "role": cam.role, "interface": cam.interface, "width": cam.width, "height": cam.height, "bayer_pattern": cam.bayer_pattern, "bit_depth": cam.bit_depth, "connected": cam.connected, "enabled": cam.enabled, "available": cam.available, "model": cam.model, "serial": cam.serial, }) return { "source_camera": primary.id, "source_width": primary.width, "source_height": primary.height, "source_bayer_pattern": primary.bayer_pattern, "source_bit_depth": primary.bit_depth, "source_cameras": source_cameras, } 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, "has_re": self.state.has_re, "has_nir": self.state.has_nir, "has_rgb": self.state.has_rgb, "has_multispec_pair": self.state.has_multispec_pair, "has_full_multispec": self.state.has_full_multispec, "re_camera_id": self.state.re_camera_id, "nir_camera_id": self.state.nir_camera_id, "rgb_camera_id": self.state.rgb_camera_id, "cameras": [cam.to_dict() for cam in self.state.cameras], **self._payload_dict(), **self._source_dict(), } # ========================================================= # Comandos # ========================================================= 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)) old_frame_type = self.state.frame_type old_output_dtype = self.state.output_dtype old_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: if ( old_frame_type != self.state.frame_type or old_output_dtype != self.state.output_dtype or old_capture_mode != self.state.capture_mode ): self._mark_reconfigure_needed() self._restart_module_if_needed() 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() ok = self.camera.begin() if not ok: raise RuntimeError("Falha ao inicializar câmeras") self.state.initialized = True self.state.streaming = False 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": if not self.state.initialized: return {"ok": False, "error": "Módulo não inicializado"} self._restart_module_if_needed() 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 not self.state.initialized: return {"ok": False, "error": "Módulo não inicializado"} self._restart_module_if_needed() 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)} # ========================================================= # Rede TCP # ========================================================= 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() def stop(self): self.running = False