537 lines
20 KiB
Python
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() |