diff --git a/ExoMy_Software-master/gui/admin.html b/ExoMy_Software-master/gui/admin.html
index 8ec2f03..ec070ff 100644
--- a/ExoMy_Software-master/gui/admin.html
+++ b/ExoMy_Software-master/gui/admin.html
@@ -40,6 +40,7 @@
+
@@ -330,6 +331,17 @@
}
}
+ async function runRumble() {
+ try {
+ var response = await fetch(adminApiBase + '/api/rumble', { method: 'POST' });
+ var data = await response.json();
+ setAdminStatus(data.status || 'Vibration ausgelöst');
+ window.setTimeout(function() { setAdminStatus('Bereit'); }, 2000);
+ } catch (e) {
+ setAdminStatus('Fehler');
+ }
+ }
+
window.addEventListener('load', function () {
var input = document.getElementById('auth_input');
input.focus();
diff --git a/ExoMy_Software-master/gui/style.css b/ExoMy_Software-master/gui/style.css
index 8c0080f..f7d9179 100644
--- a/ExoMy_Software-master/gui/style.css
+++ b/ExoMy_Software-master/gui/style.css
@@ -632,6 +632,22 @@ h2 {
0 8px 18px rgba(109, 126, 132, 0.08);
}
+.rumble_btn {
+ grid-column: span 2;
+ min-height: auto !important;
+ padding: 0.45rem 0.9rem;
+ font-size: 0.8rem;
+ opacity: 0.75;
+ color: #fff;
+ background: rgba(255,255,255,0.12);
+ border: 1px solid rgba(255,255,255,0.2);
+}
+
+.rumble_btn:hover {
+ opacity: 1;
+ background: rgba(255,255,255,0.2);
+}
+
.delay_control {
display: flex;
flex-direction: column;
diff --git a/ExoMy_Software-master/scripts/exomy_admin_api.py b/ExoMy_Software-master/scripts/exomy_admin_api.py
index bc04b27..5a176b2 100644
--- a/ExoMy_Software-master/scripts/exomy_admin_api.py
+++ b/ExoMy_Software-master/scripts/exomy_admin_api.py
@@ -1,9 +1,12 @@
#!/usr/bin/env python3
+import fcntl
+import glob
import http.server
import json
import os
import shutil
import socketserver
+import struct
import subprocess
import threading
import time
@@ -248,6 +251,62 @@ def set_delay(seconds):
return seconds
+def rumble_controller(duration_ms=400):
+ # Linux FF constants
+ EV_FF = 0x15
+ FF_RUMBLE = 0x50
+ # EVIOCSFF = _IOW('E', 0x80, struct ff_effect) — ff_effect is 48 bytes on ARM64
+ EVIOCSFF = (0x40000000 | (48 << 16) | (ord('E') << 8) | 0x80)
+
+ device_path = None
+ for path in sorted(glob.glob('/dev/input/event*')):
+ try:
+ sys_name = f'/sys/class/input/{os.path.basename(path)}/device/name'
+ with open(sys_name, encoding='utf-8') as f:
+ name = f.read().strip()
+ if any(k in name for k in ('Gamepad', 'F710', 'GAMEPAD')):
+ device_path = path
+ break
+ except OSError:
+ pass
+
+ if not device_path:
+ raise RuntimeError('Kein Gamepad gefunden')
+
+ fd = os.open(device_path, os.O_RDWR)
+ try:
+ # struct ff_effect layout on ARM64 (48 bytes):
+ # offset 0: type (u16)
+ # offset 2: id (s16) — set to -1, kernel assigns
+ # offset 4: direction (u16)
+ # offset 6: trigger.button (u16)
+ # offset 8: trigger.interval (u16)
+ # offset 10: replay.length (u16, ms)
+ # offset 12: replay.delay (u16)
+ # offset 14: padding (2 bytes)
+ # offset 16: union.rumble.strong_magnitude (u16)
+ # offset 18: union.rumble.weak_magnitude (u16)
+ # offset 20–47: zeros
+ buf = bytearray(48)
+ struct.pack_into('