""" 机械臂服务端 - 机械臂端主程序 运行在 10.247.46.165 上,端口 5002 (TCP) + 5003 (视频流) 通过 TCP Socket 接收 AGV 发来的指令,转发给 RoboFlow (630 Socket API) 同时通过 ffmpeg 提供 HTTP 视频流 """ import socket import threading import time import logging import os import sys import subprocess from flask import Flask, Response, jsonify from werkzeug.serving import make_server # 添加当前目录到路径 sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) # 配置日志 logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", handlers=[ logging.StreamHandler(), logging.FileHandler(os.path.expanduser("~/work/arm_server/server.log")) ] ) logger = logging.getLogger("arm_server") # ========== Flask HTTP 服务器 - 视频流 (ffmpeg) ========== arm_video_app = Flask(__name__) ARM_CAMERA_INDEX = 0 # 机械臂端摄像头设备号 _ffmpeg_proc = None @arm_video_app.route("/api/camera/preview") def arm_camera_preview(): """机械臂摄像头 MJPEG 流 (ffmpeg)""" global _ffmpeg_proc def generate(): global _ffmpeg_proc # 启动 ffmpeg 进程(如果尚未运行) if _ffmpeg_proc is None or _ffmpeg_proc.poll() is not None: logger.info(f"启动 ffmpeg 视频流 (Video{ARM_CAMERA_INDEX})") _ffmpeg_proc = subprocess.Popen( [ "ffmpeg", "-f", "v4l2", "-input_format", "mjpeg", "-i", f"/dev/video{ARM_CAMERA_INDEX}", "-vf", "rotate=PI", "-q:v", "8", "-f", "mjpeg", "-" ], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, ) time.sleep(0.5) # 等待 ffmpeg 初始化 try: while True: if _ffmpeg_proc is None or _ffmpeg_proc.poll() is not None: break jpeg = _ffmpeg_proc.stdout.read(65536) if not jpeg: break yield (b"--frame\r\nContent-Type: image/jpeg\r\n\r\n" + jpeg + b"\r\n") except Exception as e: logger.error(f"视频流异常: {e}") finally: logger.info("视频流连接关闭") return Response(generate(), mimetype="multipart/x-mixed-replace; boundary=frame") @arm_video_app.route("/api/camera/status") def arm_camera_status(): """摄像头状态""" global _ffmpeg_proc running = _ffmpeg_proc is not None and _ffmpeg_proc.poll() is None return jsonify({"opened": running}) @arm_video_app.route("/api/camera/restart", methods=["POST"]) def arm_camera_restart(): """重启视频流""" global _ffmpeg_proc if _ffmpeg_proc: _ffmpeg_proc.terminate() _ffmpeg_proc = None return jsonify({"ok": True}) # ========== RoboFlow 630 Socket API 客户端 ========== class RoboFlowClient: """通过 Socket 连接 RoboFlow 630 机械臂控制盒""" def __init__(self, host: str = "127.0.0.1", port: int = 5001, timeout: float = 10): self.host = host self.port = port self.timeout = timeout self._sock: socket.socket = None def connect(self) -> bool: try: self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self._sock.settimeout(self.timeout) self._sock.connect((self.host, self.port)) logger.info(f"已连接到 RoboFlow {self.host}:{self.port}") return True except Exception as e: logger.error(f"连接 RoboFlow 失败: {e}") return False def send_recv(self, cmd: str) -> str: """发送命令并等待响应""" if not self._sock: raise ConnectionError("未连接到 RoboFlow") try: self._sock.sendall((cmd + "\n").encode("utf-8")) resp = self._sock.recv(4096).decode("utf-8").strip() return resp except socket.timeout: return "ERROR: timeout" except Exception as e: return f"ERROR: {e}" def close(self): if self._sock: self._sock.close() self._sock = None def __enter__(self): self.connect() return self def __exit__(self, *args): self.close() # ========== TCP 服务器 - 接收 AGV 指令 ========== class AGVCommandServer: """TCP 服务器,接收 AGV 发来的指令""" def __init__(self, host: str = "0.0.0.0", port: int = 5002): self.host = host self.port = port self._sock: socket.socket = None self._running = False self.roboflow: RoboFlowClient = None self._connect_roboflow() def _connect_roboflow(self): self.roboflow = RoboFlowClient() if self.roboflow.connect(): logger.info("RoboFlow 连接成功") else: logger.warning("RoboFlow 连接失败,服务将以 limited 模式运行") def start(self): self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self._sock.bind((self.host, self.port)) self._sock.listen(5) self._running = True logger.info(f"=" * 50) logger.info(f"机械臂服务端已启动,监听 {self.host}:{self.port}") logger.info(f"等待 AGV 连接...") logger.info(f"=" * 50) while self._running: try: self._sock.settimeout(1.0) try: client_sock, addr = self._sock.accept() logger.info(f"AGV 已连接: {addr}") threading.Thread(target=self._handle_client, args=(client_sock,), daemon=True).start() except socket.timeout: continue except Exception as e: if self._running: logger.error(f"服务器异常: {e}") break def _handle_client(self, client_sock: socket.socket): try: client_sock.settimeout(30) buffer = "" while self._running: try: data = client_sock.recv(4096) if not data: break buffer += data.decode("utf-8") while "\n" in buffer: line, buffer = buffer.split("\n", 1) line = line.strip() if not line: continue response = self._execute_command(line) client_sock.sendall((response + "\n").encode("utf-8")) logger.info(f"CMD: {line} → {response}") except socket.timeout: continue except Exception as e: logger.error(f"客户端处理异常: {e}") finally: client_sock.close() logger.info("AGV 客户端已断开") def _execute_command(self, cmd: str) -> str: if not self.roboflow or not self.roboflow._sock: return f"ERROR: RoboFlow not connected" try: return self.roboflow.send_recv(cmd) except Exception as e: return f"ERROR: {e}" def stop(self): self._running = False if self._sock: try: self._sock.close() except: pass if self.roboflow: self.roboflow.close() logger.info("机械臂服务端已停止") # ========== 入口 ========== def main(): import signal server = AGVCommandServer(port=5002) # 启动 Flask 视频流服务(端口 5003) arm_server_http = make_server("0.0.0.0", 5003, arm_video_app, threaded=True) http_thread = threading.Thread(target=arm_server_http.serve_forever, daemon=True) http_thread.start() logger.info("机械臂视频流服务已启动: http://0.0.0.0:5003") def signal_handler(sig, frame): logger.info("收到停止信号...") global _ffmpeg_proc if _ffmpeg_proc: _ffmpeg_proc.terminate() server.stop() arm_server_http.shutdown() sys.exit(0) signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) server.start() if __name__ == "__main__": main()