Turn on Point kann jetzt schon im Admin aufgerufen werden um den Rover in eine Richtug zu drehen
This commit is contained in:
@@ -180,6 +180,24 @@
|
|||||||
<button class="act-btn subtle" style="padding:2px 8px;font-size:11px;" onclick="resetHudColor()">Reset</button>
|
<button class="act-btn subtle" style="padding:2px 8px;font-size:11px;" onclick="resetHudColor()">Reset</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="sr">
|
||||||
|
<span class="sr-led led-off"></span>
|
||||||
|
<span class="sr-key">Ziel-Heading</span>
|
||||||
|
<span class="sr-val" style="display:flex;align-items:center;gap:6px;">
|
||||||
|
<input type="number" id="rotate_target" min="0" max="359" step="1" value="0"
|
||||||
|
style="width:60px;background:#111;color:#eee;border:1px solid #444;padding:2px 4px;font-size:12px;border-radius:3px;">
|
||||||
|
<span style="font-size:11px;color:#888;">° (0=N, 90=O, 180=S, 270=W)</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="act-grid">
|
||||||
|
<button class="act-btn subtle full" id="rotate_start_btn" onclick="startRotate()">Zu Heading drehen</button>
|
||||||
|
<button class="act-btn subtle full" id="rotate_stop_btn" onclick="stopRotate()" style="display:none;">Drehung abbrechen</button>
|
||||||
|
</div>
|
||||||
|
<div class="sr" id="rotate_status_row" style="display:none;">
|
||||||
|
<span class="sr-led led-blue" id="rotate_status_led"></span>
|
||||||
|
<span class="sr-key">Drehung</span>
|
||||||
|
<strong id="rotate_status_val" class="sr-val">-</strong>
|
||||||
|
</div>
|
||||||
<div class="sr">
|
<div class="sr">
|
||||||
<span class="sr-led led-off"></span>
|
<span class="sr-led led-off"></span>
|
||||||
<span class="sr-key">Hinweis</span>
|
<span class="sr-key">Hinweis</span>
|
||||||
@@ -2890,6 +2908,83 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var rotatePoller = null;
|
||||||
|
|
||||||
|
function updateRotateUi(rotate) {
|
||||||
|
if (!rotate) return;
|
||||||
|
var running = rotate.result === 'running';
|
||||||
|
var startBtn = document.getElementById('rotate_start_btn');
|
||||||
|
var stopBtn = document.getElementById('rotate_stop_btn');
|
||||||
|
var row = document.getElementById('rotate_status_row');
|
||||||
|
var val = document.getElementById('rotate_status_val');
|
||||||
|
var led = document.getElementById('rotate_status_led');
|
||||||
|
if (startBtn) startBtn.style.display = running ? 'none' : '';
|
||||||
|
if (stopBtn) stopBtn.style.display = running ? '' : 'none';
|
||||||
|
if (row) row.style.display = rotate.result === 'idle' ? 'none' : '';
|
||||||
|
if (led) {
|
||||||
|
led.className = 'sr-led ' + (
|
||||||
|
rotate.result === 'done' ? 'led-ok' :
|
||||||
|
rotate.result === 'error' ? 'led-warn' :
|
||||||
|
rotate.result === 'aborted' ? 'led-warn' : 'led-blue'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (val) {
|
||||||
|
var txt = rotate.message || '-';
|
||||||
|
if (running && rotate.current_deg !== null && rotate.error_deg !== null) {
|
||||||
|
txt += ' | Ist: ' + rotate.current_deg + '° | Rest: ' + rotate.error_deg + '°';
|
||||||
|
}
|
||||||
|
val.textContent = txt;
|
||||||
|
}
|
||||||
|
if (!running && rotatePoller) {
|
||||||
|
clearInterval(rotatePoller);
|
||||||
|
rotatePoller = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startRotate() {
|
||||||
|
var input = document.getElementById('rotate_target');
|
||||||
|
var target = parseFloat(input ? input.value : 0);
|
||||||
|
if (isNaN(target)) { alert('Ungültiger Winkel'); return; }
|
||||||
|
setAdminStatus('Starte Drehung zu ' + target + '°…');
|
||||||
|
try {
|
||||||
|
var response = await fetch(adminApiBase + '/api/imu/rotate-to', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({target_deg: target})
|
||||||
|
});
|
||||||
|
var data = await response.json();
|
||||||
|
if (!response.ok) throw new Error(data.status || ('status ' + response.status));
|
||||||
|
updateRotateUi(data.rotate);
|
||||||
|
setAdminStatus(data.status || 'Drehung gestartet');
|
||||||
|
if (rotatePoller) clearInterval(rotatePoller);
|
||||||
|
rotatePoller = setInterval(pollRotateStatus, 300);
|
||||||
|
} catch (e) {
|
||||||
|
setAdminStatus('Fehler: ' + (e.message || 'unbekannt'));
|
||||||
|
alert(e.message || 'Drehung konnte nicht gestartet werden.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopRotate() {
|
||||||
|
setAdminStatus('Stoppe Drehung…');
|
||||||
|
try {
|
||||||
|
var response = await fetch(adminApiBase + '/api/imu/rotate-stop', { method: 'POST' });
|
||||||
|
var data = await response.json();
|
||||||
|
updateRotateUi(data.rotate);
|
||||||
|
setAdminStatus(data.status || 'Drehung gestoppt');
|
||||||
|
} catch (e) {
|
||||||
|
setAdminStatus('Fehler beim Stoppen');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pollRotateStatus() {
|
||||||
|
try {
|
||||||
|
var response = await fetch(adminApiBase + '/api/imu/rotate-status');
|
||||||
|
if (!response.ok) return;
|
||||||
|
var data = await response.json();
|
||||||
|
updateRotateUi(data.rotate);
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchDelay() {
|
async function fetchDelay() {
|
||||||
try {
|
try {
|
||||||
var response = await fetch(adminApiBase + '/api/delay');
|
var response = await fetch(adminApiBase + '/api/delay');
|
||||||
|
|||||||
@@ -227,6 +227,7 @@ def _connect_imu_ros():
|
|||||||
global IMU_ROS_CLIENT, IMU_ROS_TOPICS
|
global IMU_ROS_CLIENT, IMU_ROS_TOPICS
|
||||||
|
|
||||||
import roslibpy
|
import roslibpy
|
||||||
|
import imu_rotate
|
||||||
|
|
||||||
client = roslibpy.Ros(host=ROSBRIDGE_HOST, port=ROSBRIDGE_PORT)
|
client = roslibpy.Ros(host=ROSBRIDGE_HOST, port=ROSBRIDGE_PORT)
|
||||||
client.run()
|
client.run()
|
||||||
@@ -239,13 +240,29 @@ def _connect_imu_ros():
|
|||||||
imu_topic = roslibpy.Topic(client, '/imu/data', 'sensor_msgs/Imu')
|
imu_topic = roslibpy.Topic(client, '/imu/data', 'sensor_msgs/Imu')
|
||||||
mag_topic = roslibpy.Topic(client, '/imu/mag', 'sensor_msgs/MagneticField')
|
mag_topic = roslibpy.Topic(client, '/imu/mag', 'sensor_msgs/MagneticField')
|
||||||
status_topic = roslibpy.Topic(client, '/imu/status', 'std_msgs/String')
|
status_topic = roslibpy.Topic(client, '/imu/status', 'std_msgs/String')
|
||||||
|
rover_cmd_topic = roslibpy.Topic(client, '/rover_command', 'exomy/RoverCommand')
|
||||||
imu_topic.subscribe(_on_imu_data_message)
|
imu_topic.subscribe(_on_imu_data_message)
|
||||||
mag_topic.subscribe(_on_imu_mag_message)
|
mag_topic.subscribe(_on_imu_mag_message)
|
||||||
status_topic.subscribe(_on_imu_status_message)
|
status_topic.subscribe(_on_imu_status_message)
|
||||||
|
rover_cmd_topic.advertise()
|
||||||
IMU_ROS_CLIENT = client
|
IMU_ROS_CLIENT = client
|
||||||
IMU_ROS_TOPICS = [imu_topic, mag_topic, status_topic]
|
IMU_ROS_TOPICS = [imu_topic, mag_topic, status_topic, rover_cmd_topic]
|
||||||
_imu_ros_cache_update(status='ROS verbunden', error='')
|
_imu_ros_cache_update(status='ROS verbunden', error='')
|
||||||
|
|
||||||
|
def _publish_rover_cmd(locomotion_mode, vel, steering):
|
||||||
|
try:
|
||||||
|
rover_cmd_topic.publish(roslibpy.Message({
|
||||||
|
'connected': True,
|
||||||
|
'motors_enabled': True,
|
||||||
|
'locomotion_mode': int(locomotion_mode),
|
||||||
|
'vel': int(vel),
|
||||||
|
'steering': int(steering),
|
||||||
|
}))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
imu_rotate.set_publish_fn(_publish_rover_cmd)
|
||||||
|
|
||||||
|
|
||||||
def _imu_ros_worker():
|
def _imu_ros_worker():
|
||||||
global IMU_ROS_CLIENT, IMU_ROS_TOPICS
|
global IMU_ROS_CLIENT, IMU_ROS_TOPICS
|
||||||
@@ -1957,6 +1974,10 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
self.end_headers()
|
self.end_headers()
|
||||||
|
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
|
if self.path == '/api/imu/rotate-status':
|
||||||
|
import imu_rotate
|
||||||
|
self.write_json(200, {'rotate': imu_rotate.get_rotate_status()})
|
||||||
|
return
|
||||||
if self.path == '/api/delay':
|
if self.path == '/api/delay':
|
||||||
self.write_json(200, {'delay_seconds': get_delay()})
|
self.write_json(200, {'delay_seconds': get_delay()})
|
||||||
return
|
return
|
||||||
@@ -1999,7 +2020,8 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
def do_POST(self):
|
def do_POST(self):
|
||||||
raw_body = b'{}'
|
raw_body = b'{}'
|
||||||
if self.path in ('/api/routes/save', '/api/routes/delete', '/api/imu/heading-offset'):
|
if self.path in ('/api/routes/save', '/api/routes/delete', '/api/imu/heading-offset',
|
||||||
|
'/api/imu/rotate-to'):
|
||||||
try:
|
try:
|
||||||
content_length = int(self.headers.get('Content-Length', '0'))
|
content_length = int(self.headers.get('Content-Length', '0'))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@@ -2290,6 +2312,39 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
self.write_json(status_code, response)
|
self.write_json(status_code, response)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if self.path == '/api/imu/rotate-to':
|
||||||
|
try:
|
||||||
|
payload = json.loads(raw_body.decode('utf-8'))
|
||||||
|
target_deg = float(payload.get('target_deg', 0))
|
||||||
|
except (UnicodeDecodeError, json.JSONDecodeError, TypeError, ValueError):
|
||||||
|
self.write_json(400, {'status': 'Ungültige Anfrage'})
|
||||||
|
return
|
||||||
|
with IMU_ROS_LOCK:
|
||||||
|
heading = IMU_ROS_CACHE.get('heading_deg')
|
||||||
|
if heading is None:
|
||||||
|
self.write_json(409, {'status': 'Noch keine frischen IMU-Daten'})
|
||||||
|
return
|
||||||
|
import imu_rotate
|
||||||
|
heading_offset = read_imu_heading_calibration()['offset_deg']
|
||||||
|
|
||||||
|
def _get_corrected_heading():
|
||||||
|
with IMU_ROS_LOCK:
|
||||||
|
raw = IMU_ROS_CACHE.get('heading_deg')
|
||||||
|
return get_corrected_heading_deg(raw, heading_offset) if raw is not None else None
|
||||||
|
|
||||||
|
status = imu_rotate.start_rotate(
|
||||||
|
target_deg=target_deg,
|
||||||
|
get_heading_fn=_get_corrected_heading,
|
||||||
|
)
|
||||||
|
self.write_json(200, {'status': 'Drehung gestartet', 'rotate': status})
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.path == '/api/imu/rotate-stop':
|
||||||
|
import imu_rotate
|
||||||
|
imu_rotate.stop_rotate()
|
||||||
|
self.write_json(200, {'status': 'Drehung gestoppt', 'rotate': imu_rotate.get_rotate_status()})
|
||||||
|
return
|
||||||
|
|
||||||
actions = {
|
actions = {
|
||||||
'/api/cold-start-gps': {
|
'/api/cold-start-gps': {
|
||||||
'handler': cold_start_gps_receiver,
|
'handler': cold_start_gps_receiver,
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
"""
|
||||||
|
Dreht den Rover auf der Stelle zu einem Ziel-Heading.
|
||||||
|
|
||||||
|
Öffentliche API:
|
||||||
|
set_publish_fn(fn) wird von exomy_admin_api nach ROS-Connect gesetzt
|
||||||
|
start_rotate(target_deg, get_heading_fn) -> dict
|
||||||
|
stop_rotate()
|
||||||
|
get_rotate_status() -> dict
|
||||||
|
"""
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
_lock = threading.Lock()
|
||||||
|
_thread = None
|
||||||
|
_stop_event = threading.Event()
|
||||||
|
_publish_fn = 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:
|
||||||
|
_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)
|
||||||
|
|
||||||
|
|
||||||
|
def start_rotate(target_deg, get_heading_fn):
|
||||||
|
global _thread, _status
|
||||||
|
|
||||||
|
target_deg = float(target_deg) % 360
|
||||||
|
stop_rotate()
|
||||||
|
|
||||||
|
_stop_event.clear()
|
||||||
|
with _lock:
|
||||||
|
_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),
|
||||||
|
}
|
||||||
|
|
||||||
|
_thread = threading.Thread(
|
||||||
|
target=_control_loop,
|
||||||
|
args=(target_deg, get_heading_fn),
|
||||||
|
daemon=True,
|
||||||
|
)
|
||||||
|
_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
|
||||||
|
|
||||||
|
|
||||||
|
def get_rotate_status():
|
||||||
|
with _lock:
|
||||||
|
return dict(_status)
|
||||||
Reference in New Issue
Block a user