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('