HUD-Farbe konfigurierbar, Neigungskalibrierung, Auto-Rotate verbessert
- HUD-Farbe per Farbwähler im Admin einstellbar (localStorage) - Schwarze Kontur auf HUD-Elementen via CSS drop-shadow und canvas shadowBlur - Himmelsrichtungen auf Deutsch (O statt E, etc.) - Roll/Pitch-Neigung kann im Admin genullt werden (imu_tilt_calibration.json) - Neigungsoffset wird im Kamera-Overlay berücksichtigt - Auto-Rotate läuft jetzt nativ als rospy-Script im Docker-Container - Auto-Rotate erkennt aktuellen Locomotion-Mode und stellt ihn danach wieder her - Auto-Rotate pausiert Web-GUI-Joystick via localStorage-Flag - Feste 20%-Geschwindigkeit für Auto-Rotate (vel=4, Expo-äquivalent) - Vignette entfernt Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+71
-92
@@ -1,131 +1,110 @@
|
||||
"""
|
||||
Dreht den Rover auf der Stelle zu einem Ziel-Heading.
|
||||
Startet auto_rotate_ros.py via docker exec im ExoMy-Container.
|
||||
Das Script läuft dort nativ mit rospy — kein WebSocket-Overhead.
|
||||
|
||||
Öffentliche API:
|
||||
set_publish_fn(fn) wird von exomy_admin_api nach ROS-Connect gesetzt
|
||||
start_rotate(target_deg, get_heading_fn) -> dict
|
||||
start_rotate(target_deg, heading_offset, restore_mode) -> dict
|
||||
stop_rotate()
|
||||
get_rotate_status() -> dict
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
|
||||
_POINT_TURN = 2 # LocomotionMode.POINT_TURN
|
||||
_ACKERMANN = 1 # LocomotionMode.ACKERMANN (Standard-Rückkehrmodus)
|
||||
_DONE_DEG = 3.0 # Toleranz in Grad
|
||||
_VEL = 20 # konstante Drehgeschwindigkeit (0–100), direkte Motoransteuerung
|
||||
_INTERVAL = 0.02 # Sekunden zwischen Regelschritten (50 Hz)
|
||||
_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()
|
||||
_thread = None
|
||||
_stop_event = threading.Event()
|
||||
_publish_fn = None
|
||||
_lock = threading.Lock()
|
||||
_proc = None # subprocess.Popen (docker exec)
|
||||
_monitor_thread = None
|
||||
|
||||
_status = {
|
||||
'running': False,
|
||||
'target_deg': None,
|
||||
'current_deg': None,
|
||||
'error_deg': None,
|
||||
'result': 'idle',
|
||||
'message': '',
|
||||
}
|
||||
|
||||
|
||||
def set_publish_fn(fn):
|
||||
global _publish_fn
|
||||
_publish_fn = fn
|
||||
|
||||
|
||||
def _publish(locomotion_mode, vel, steering):
|
||||
fn = _publish_fn
|
||||
if fn is not None:
|
||||
fn(locomotion_mode=locomotion_mode, vel=vel, steering=steering)
|
||||
|
||||
|
||||
def _shortest_error(current, target):
|
||||
"""Vorzeichen: positiv = rechtsdrehung nötig, negativ = linksdrehung."""
|
||||
return (target - current + 540) % 360 - 180
|
||||
|
||||
|
||||
def _restore(restore_mode):
|
||||
_publish(restore_mode, vel=0, steering=0)
|
||||
|
||||
|
||||
def _control_loop(target_deg, get_heading_fn, restore_mode):
|
||||
global _status
|
||||
|
||||
try:
|
||||
while not _stop_event.is_set():
|
||||
heading = get_heading_fn()
|
||||
if heading is None:
|
||||
time.sleep(0.05)
|
||||
continue
|
||||
|
||||
error = _shortest_error(heading, target_deg)
|
||||
|
||||
with _lock:
|
||||
_status['current_deg'] = round(heading, 1)
|
||||
_status['error_deg'] = round(error, 1)
|
||||
|
||||
if abs(error) <= _DONE_DEG:
|
||||
_restore(restore_mode)
|
||||
with _lock:
|
||||
_status['running'] = False
|
||||
_status['result'] = 'done'
|
||||
_status['message'] = 'Ziel erreicht'
|
||||
return
|
||||
|
||||
vel = _VEL if error > 0 else -_VEL
|
||||
_publish(_POINT_TURN, vel=vel, steering=0)
|
||||
time.sleep(_INTERVAL)
|
||||
|
||||
_restore(restore_mode)
|
||||
with _lock:
|
||||
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
|
||||
_status['result'] = 'aborted'
|
||||
_status['message'] = 'Abgebrochen'
|
||||
|
||||
except Exception as exc:
|
||||
_restore(restore_mode)
|
||||
with _lock:
|
||||
_status['running'] = False
|
||||
_status['result'] = 'error'
|
||||
_status['message'] = str(exc)
|
||||
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, get_heading_fn):
|
||||
global _thread, _status
|
||||
def start_rotate(target_deg, heading_offset, restore_mode=1, done_deg=1.0, vel=None):
|
||||
global _proc, _monitor_thread, _status
|
||||
|
||||
target_deg = float(target_deg) % 360
|
||||
stop_rotate()
|
||||
|
||||
_stop_event.clear()
|
||||
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),
|
||||
'current_deg': None,
|
||||
'error_deg': None,
|
||||
'result': 'running',
|
||||
'message': 'Drehe zu {0:.0f}°'.format(target_deg),
|
||||
'running': True,
|
||||
'target_deg': round(target_deg, 1),
|
||||
'result': 'running',
|
||||
'message': 'Drehe zu {:.0f}°'.format(target_deg),
|
||||
}
|
||||
|
||||
_thread = threading.Thread(
|
||||
target=_control_loop,
|
||||
args=(target_deg, get_heading_fn),
|
||||
_monitor_thread = threading.Thread(
|
||||
target=_monitor,
|
||||
args=(proc, target_deg, restore_mode),
|
||||
daemon=True,
|
||||
)
|
||||
_thread.start()
|
||||
_monitor_thread.start()
|
||||
return get_rotate_status()
|
||||
|
||||
|
||||
def stop_rotate():
|
||||
global _thread
|
||||
if _thread is not None and _thread.is_alive():
|
||||
_stop_event.set()
|
||||
_thread.join(timeout=2.0)
|
||||
_thread = None
|
||||
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():
|
||||
|
||||
Reference in New Issue
Block a user