""" Startet auto_rotate_ros.py via docker exec im ExoMy-Container. Das Script läuft dort nativ mit rospy — kein WebSocket-Overhead. Öffentliche API: start_rotate(target_deg, heading_offset, restore_mode) -> dict stop_rotate() get_rotate_status() -> dict """ import subprocess import threading import time _CONTAINER = 'exomy_autostart' _SCRIPT_PATH = '/root/exomy_ws/src/exomy/scripts/auto_rotate_ros.py' _ROS_SETUP = 'source /opt/ros/melodic/setup.bash && source /root/exomy_ws/devel/setup.bash' _lock = threading.Lock() _proc = None # subprocess.Popen (docker exec) _monitor_thread = None _status = { 'running': False, 'target_deg': None, 'result': 'idle', 'message': '', } def _monitor(proc, target_deg, restore_mode): """Wartet auf Prozessende und aktualisiert Status.""" global _proc returncode = proc.wait() with _lock: if _proc is proc: # nicht überschrieben durch neuen Start _proc = None _status['running'] = False if returncode == 0: _status['result'] = 'done' _status['message'] = 'Ziel erreicht' elif returncode == -15 or returncode == 143: _status['result'] = 'aborted' _status['message'] = 'Abgebrochen' else: _status['result'] = 'error' _status['message'] = 'Fehler (exit {})'.format(returncode) def start_rotate(target_deg, heading_offset, restore_mode=1, done_deg=1.0, vel=None): global _proc, _monitor_thread, _status stop_rotate() target_deg = float(target_deg) % 360 if vel is None: # Fest 20% Geschwindigkeit, unabhängig vom Admin-Speed-Limit # Entspricht dem Joystick-Expo: 100 * (20/100)^2 = 4 vel = 4 cmd = [ 'docker', 'exec', _CONTAINER, 'bash', '-c', '{setup} && python {script} {target} {mode} {offset} {done} {vel}'.format( setup=_ROS_SETUP, script=_SCRIPT_PATH, target=target_deg, mode=int(restore_mode), offset=float(heading_offset), done=float(done_deg), vel=int(vel), ) ] proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) with _lock: _proc = proc _status = { 'running': True, 'target_deg': round(target_deg, 1), 'result': 'running', 'message': 'Drehe zu {:.0f}°'.format(target_deg), } _monitor_thread = threading.Thread( target=_monitor, args=(proc, target_deg, restore_mode), daemon=True, ) _monitor_thread.start() return get_rotate_status() def stop_rotate(): global _proc with _lock: proc = _proc _proc = None if proc is not None and proc.poll() is None: # SIGTERM an den docker-exec-Prozess → Container-Prozess beendet sich sauber proc.terminate() try: proc.wait(timeout=3.0) except subprocess.TimeoutExpired: proc.kill() def get_rotate_status(): with _lock: return dict(_status)