487 lines
14 KiB
Python
487 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
import json
|
|
import math
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
|
|
WHEELS = ('fl', 'fr', 'cl', 'cr', 'rl', 'rr')
|
|
|
|
WHEEL_LABELS = {
|
|
'fl': 'Vorne links',
|
|
'fr': 'Vorne rechts',
|
|
'cl': 'Mitte links',
|
|
'cr': 'Mitte rechts',
|
|
'rl': 'Hinten links',
|
|
'rr': 'Hinten rechts',
|
|
}
|
|
|
|
WHEEL_DIRECTIONS = {
|
|
'fl': -1,
|
|
'fr': 1,
|
|
'cl': -1,
|
|
'cr': 1,
|
|
'rl': -1,
|
|
'rr': 1,
|
|
}
|
|
|
|
STEER_PIN_KEYS = {
|
|
'fl': 'pin_steer_fl',
|
|
'fr': 'pin_steer_fr',
|
|
'cl': 'pin_steer_cl',
|
|
'cr': 'pin_steer_cr',
|
|
'rl': 'pin_steer_rl',
|
|
'rr': 'pin_steer_rr',
|
|
}
|
|
|
|
STEER_NEUTRAL_KEYS = {
|
|
'fl': 'steer_pwm_neutral_fl',
|
|
'fr': 'steer_pwm_neutral_fr',
|
|
'cl': 'steer_pwm_neutral_cl',
|
|
'cr': 'steer_pwm_neutral_cr',
|
|
'rl': 'steer_pwm_neutral_rl',
|
|
'rr': 'steer_pwm_neutral_rr',
|
|
}
|
|
|
|
DRIVE_PIN_KEYS = {
|
|
'fl': 'pin_drive_fl',
|
|
'fr': 'pin_drive_fr',
|
|
'cl': 'pin_drive_cl',
|
|
'cr': 'pin_drive_cr',
|
|
'rl': 'pin_drive_rl',
|
|
'rr': 'pin_drive_rr',
|
|
}
|
|
|
|
ACTION_SETTINGS = {
|
|
'steer_left': {
|
|
'kind': 'steer',
|
|
'angle': -35,
|
|
'duration': 0.6,
|
|
'label': 'Lenkung links',
|
|
},
|
|
'steer_right': {
|
|
'kind': 'steer',
|
|
'angle': 35,
|
|
'duration': 0.6,
|
|
'label': 'Lenkung rechts',
|
|
},
|
|
'drive_forward': {
|
|
'kind': 'drive',
|
|
'speed': 35,
|
|
'duration': 0.7,
|
|
'label': 'Vorwärts',
|
|
},
|
|
'drive_backward': {
|
|
'kind': 'drive',
|
|
'speed': -35,
|
|
'duration': 0.7,
|
|
'label': 'Rückwärts',
|
|
},
|
|
}
|
|
|
|
CONTAINER_NAME = 'exomy_autostart'
|
|
CONTAINER_SCRIPT_PATH = '/root/exomy_ws/src/exomy/scripts/admin_motor_test.py'
|
|
|
|
|
|
class PCA9685Direct:
|
|
MODE1 = 0x00
|
|
PRESCALE = 0xFE
|
|
LED0_ON_L = 0x06
|
|
|
|
def __init__(self, busnum=1, address=0x40):
|
|
try:
|
|
import smbus
|
|
except ImportError as exc:
|
|
raise RuntimeError('smbus ist nicht installiert.') from exc
|
|
|
|
self.address = address
|
|
self.bus = smbus.SMBus(busnum)
|
|
self.write8(self.MODE1, 0x00)
|
|
time.sleep(0.005)
|
|
|
|
def write8(self, reg, value):
|
|
self.bus.write_byte_data(self.address, reg, value & 0xFF)
|
|
|
|
def read8(self, reg):
|
|
return self.bus.read_byte_data(self.address, reg)
|
|
|
|
def set_pwm_freq(self, freq_hz):
|
|
prescaleval = 25000000.0
|
|
prescaleval /= 4096.0
|
|
prescaleval /= float(freq_hz)
|
|
prescaleval -= 1.0
|
|
prescale = int(math.floor(prescaleval + 0.5))
|
|
|
|
oldmode = self.read8(self.MODE1)
|
|
sleepmode = (oldmode & 0x7F) | 0x10
|
|
self.write8(self.MODE1, sleepmode)
|
|
self.write8(self.PRESCALE, prescale)
|
|
self.write8(self.MODE1, oldmode)
|
|
time.sleep(0.005)
|
|
self.write8(self.MODE1, oldmode | 0xA1)
|
|
|
|
def set_pwm(self, channel, on, off):
|
|
base = self.LED0_ON_L + 4 * int(channel)
|
|
self.write8(base, on & 0xFF)
|
|
self.write8(base + 1, (on >> 8) & 0xFF)
|
|
self.write8(base + 2, off & 0xFF)
|
|
self.write8(base + 3, (off >> 8) & 0xFF)
|
|
|
|
|
|
def _load_config(config_path):
|
|
config = {}
|
|
with open(config_path, 'r', encoding='utf-8') as handle:
|
|
for raw_line in handle:
|
|
line = raw_line.strip()
|
|
if not line or line.startswith('#') or ':' not in line:
|
|
continue
|
|
key, value = line.split(':', 1)
|
|
config[key.strip()] = value.strip()
|
|
return config
|
|
|
|
|
|
def _update_config_values(config_path, updates):
|
|
output_lines = []
|
|
with open(config_path, 'r', encoding='utf-8') as handle:
|
|
for raw_line in handle:
|
|
line = raw_line
|
|
stripped = raw_line.strip()
|
|
if stripped and not stripped.startswith('#') and ':' in raw_line:
|
|
key = raw_line.split(':', 1)[0].strip()
|
|
if key in updates:
|
|
line = f'{key}: {updates[key]}\n'
|
|
output_lines.append(line)
|
|
|
|
with open(config_path, 'w', encoding='utf-8') as handle:
|
|
handle.writelines(output_lines)
|
|
|
|
|
|
class MotorTester:
|
|
def __init__(self, config_path=None):
|
|
try:
|
|
import Adafruit_PCA9685
|
|
self.pwm = Adafruit_PCA9685.PCA9685()
|
|
except ImportError:
|
|
self.pwm = PCA9685Direct(busnum=1)
|
|
|
|
if config_path is None:
|
|
config_path = os.path.join(os.path.dirname(__file__), '..', 'config', 'exomy.yaml')
|
|
|
|
if not os.path.exists(config_path):
|
|
raise FileNotFoundError(f'Konfigurationsdatei nicht gefunden: {config_path}')
|
|
|
|
self.config_path = os.path.abspath(config_path)
|
|
self.config = _load_config(self.config_path)
|
|
self.pwm.set_pwm_freq(50)
|
|
self.steer_pwm_range = int(self.config['steer_pwm_range'])
|
|
self.drive_pwm_neutral = int(self.config['drive_pwm_neutral'])
|
|
self.drive_pwm_range = int(self.config['drive_pwm_range'])
|
|
|
|
def _steer_pin(self, wheel):
|
|
return int(self.config[STEER_PIN_KEYS[wheel]])
|
|
|
|
def _steer_neutral(self, wheel):
|
|
return int(self.config[STEER_NEUTRAL_KEYS[wheel]])
|
|
|
|
def _drive_pin(self, wheel):
|
|
return int(self.config[DRIVE_PIN_KEYS[wheel]])
|
|
|
|
def set_drive_neutral_pwm(self, pwm_value):
|
|
duty_cycle = int(pwm_value)
|
|
for wheel in WHEELS:
|
|
self.pwm.set_pwm(self._drive_pin(wheel), 0, duty_cycle)
|
|
time.sleep(0.03)
|
|
|
|
def center_all_steering(self):
|
|
for wheel in WHEELS:
|
|
self.pwm.set_pwm(self._steer_pin(wheel), 0, self._steer_neutral(wheel))
|
|
time.sleep(0.03)
|
|
|
|
def stop_all_driving(self):
|
|
for wheel in WHEELS:
|
|
self.pwm.set_pwm(self._drive_pin(wheel), 0, self.drive_pwm_neutral)
|
|
time.sleep(0.03)
|
|
|
|
def set_steering_angle(self, wheel, angle):
|
|
duty_cycle = int(self._steer_neutral(wheel) + angle / 90.0 * self.steer_pwm_range)
|
|
self.pwm.set_pwm(self._steer_pin(wheel), 0, duty_cycle)
|
|
|
|
def set_steering_neutral(self, wheel):
|
|
self.pwm.set_pwm(self._steer_pin(wheel), 0, self._steer_neutral(wheel))
|
|
|
|
def set_steering_neutral_pwm(self, wheel, pwm_value):
|
|
self.pwm.set_pwm(self._steer_pin(wheel), 0, int(pwm_value))
|
|
|
|
def set_steering_percent(self, wheel, signed_percent):
|
|
percent = max(-100.0, min(100.0, float(signed_percent)))
|
|
duty_cycle = int(self._steer_neutral(wheel) + percent / 100.0 * self.steer_pwm_range)
|
|
self.pwm.set_pwm(self._steer_pin(wheel), 0, duty_cycle)
|
|
|
|
def set_all_steering_angle(self, angle):
|
|
for wheel in WHEELS:
|
|
self.set_steering_angle(wheel, angle)
|
|
time.sleep(0.03)
|
|
|
|
def set_all_steering_neutral(self):
|
|
for wheel in WHEELS:
|
|
self.set_steering_neutral(wheel)
|
|
time.sleep(0.03)
|
|
|
|
def test_steering(self, wheel, angle, duration):
|
|
self.set_steering_angle(wheel, angle)
|
|
time.sleep(duration)
|
|
self.set_steering_neutral(wheel)
|
|
|
|
def test_driving(self, wheel, speed, duration):
|
|
self.pwm.set_pwm(self._steer_pin(wheel), 0, self._steer_neutral(wheel))
|
|
duty_cycle = int(
|
|
self.drive_pwm_neutral +
|
|
speed / 100.0 * self.drive_pwm_range * WHEEL_DIRECTIONS[wheel]
|
|
)
|
|
self.pwm.set_pwm(self._drive_pin(wheel), 0, duty_cycle)
|
|
time.sleep(duration)
|
|
self.pwm.set_pwm(self._drive_pin(wheel), 0, self.drive_pwm_neutral)
|
|
|
|
def execute(self, wheel, action):
|
|
if wheel not in WHEELS:
|
|
raise ValueError('Unbekanntes Rad.')
|
|
|
|
if action not in ACTION_SETTINGS:
|
|
raise ValueError('Unbekannte Testaktion.')
|
|
|
|
settings = ACTION_SETTINGS[action]
|
|
|
|
self.stop_all_driving()
|
|
|
|
if settings['kind'] == 'steer':
|
|
self.test_steering(wheel, settings['angle'], settings['duration'])
|
|
else:
|
|
self.test_driving(wheel, settings['speed'], settings['duration'])
|
|
|
|
return {
|
|
'wheel': wheel,
|
|
'wheel_label': WHEEL_LABELS[wheel],
|
|
'action': action,
|
|
'action_label': settings['label'],
|
|
}
|
|
|
|
def hold_steering(self, wheel, angle):
|
|
if wheel not in WHEELS:
|
|
raise ValueError('Unbekanntes Rad.')
|
|
|
|
self.stop_all_driving()
|
|
self.set_steering_angle(wheel, angle)
|
|
return {
|
|
'scope': 'single',
|
|
'wheel': wheel,
|
|
'wheel_label': WHEEL_LABELS[wheel],
|
|
'angle': angle,
|
|
}
|
|
|
|
def hold_steering_percent(self, wheel, percent, direction):
|
|
if wheel not in WHEELS:
|
|
raise ValueError('Unbekanntes Rad.')
|
|
|
|
if direction not in ('left', 'right'):
|
|
raise ValueError('Unbekannte Richtung.')
|
|
|
|
clamped_percent = max(0.0, min(100.0, float(percent)))
|
|
signed_percent = -clamped_percent if direction == 'left' else clamped_percent
|
|
|
|
self.stop_all_driving()
|
|
self.set_steering_percent(wheel, signed_percent)
|
|
return {
|
|
'scope': 'single',
|
|
'wheel': wheel,
|
|
'wheel_label': WHEEL_LABELS[wheel],
|
|
'percent': clamped_percent,
|
|
'direction': direction,
|
|
'signed_percent': signed_percent,
|
|
}
|
|
|
|
def hold_all_steering(self, angle):
|
|
self.stop_all_driving()
|
|
self.set_all_steering_angle(angle)
|
|
return {
|
|
'scope': 'all',
|
|
'wheel': 'all',
|
|
'wheel_label': 'Alle Räder',
|
|
'angle': angle,
|
|
}
|
|
|
|
def neutral_single_steering(self, wheel):
|
|
if wheel not in WHEELS:
|
|
raise ValueError('Unbekanntes Rad.')
|
|
|
|
self.stop_all_driving()
|
|
self.set_steering_neutral(wheel)
|
|
return {
|
|
'scope': 'single',
|
|
'wheel': wheel,
|
|
'wheel_label': WHEEL_LABELS[wheel],
|
|
'angle': 0,
|
|
}
|
|
|
|
def neutral_all_steering(self):
|
|
self.stop_all_driving()
|
|
self.set_all_steering_neutral()
|
|
return {
|
|
'scope': 'all',
|
|
'wheel': 'all',
|
|
'wheel_label': 'Alle Räder',
|
|
'angle': 0,
|
|
}
|
|
|
|
|
|
def run_motor_test(wheel, action, config_path=None):
|
|
tester = MotorTester(config_path=config_path)
|
|
return tester.execute(wheel.lower(), action)
|
|
|
|
|
|
def hold_single_steering(wheel, angle, config_path=None):
|
|
tester = MotorTester(config_path=config_path)
|
|
return tester.hold_steering(wheel.lower(), angle)
|
|
|
|
|
|
def hold_all_steering(angle, config_path=None):
|
|
tester = MotorTester(config_path=config_path)
|
|
return tester.hold_all_steering(angle)
|
|
|
|
|
|
def hold_single_steering_percent(wheel, percent, direction, config_path=None):
|
|
tester = MotorTester(config_path=config_path)
|
|
return tester.hold_steering_percent(wheel.lower(), percent, direction)
|
|
|
|
|
|
def neutral_single_steering(wheel, config_path=None):
|
|
tester = MotorTester(config_path=config_path)
|
|
return tester.neutral_single_steering(wheel.lower())
|
|
|
|
|
|
def neutral_all_steering(config_path=None):
|
|
tester = MotorTester(config_path=config_path)
|
|
return tester.neutral_all_steering()
|
|
|
|
|
|
def get_steering_neutral_values(config_path=None):
|
|
tester = MotorTester(config_path=config_path)
|
|
values = {}
|
|
for wheel in WHEELS:
|
|
key = STEER_NEUTRAL_KEYS[wheel]
|
|
values[wheel] = {
|
|
'wheel': wheel,
|
|
'wheel_label': WHEEL_LABELS[wheel],
|
|
'key': key,
|
|
'value': int(tester.config[key]),
|
|
'pin': tester._steer_pin(wheel),
|
|
}
|
|
return {
|
|
'config_path': tester.config_path,
|
|
'values': values,
|
|
}
|
|
|
|
|
|
def preview_steering_neutral_value(wheel, pwm_value, config_path=None):
|
|
tester = MotorTester(config_path=config_path)
|
|
if wheel not in WHEELS:
|
|
raise ValueError('Unbekanntes Rad.')
|
|
|
|
tester.stop_all_driving()
|
|
tester.set_steering_neutral_pwm(wheel, int(pwm_value))
|
|
return {
|
|
'wheel': wheel,
|
|
'wheel_label': WHEEL_LABELS[wheel],
|
|
'value': int(pwm_value),
|
|
'pin': tester._steer_pin(wheel),
|
|
}
|
|
|
|
|
|
def save_steering_neutral_values(values, config_path=None):
|
|
tester = MotorTester(config_path=config_path)
|
|
updates = {}
|
|
|
|
for wheel in WHEELS:
|
|
key = STEER_NEUTRAL_KEYS[wheel]
|
|
if key not in values:
|
|
raise ValueError(f'Neutralwert fehlt: {key}')
|
|
updates[key] = int(values[key])
|
|
|
|
_update_config_values(tester.config_path, updates)
|
|
tester.config = _load_config(tester.config_path)
|
|
tester.stop_all_driving()
|
|
tester.center_all_steering()
|
|
|
|
return get_steering_neutral_values(config_path=tester.config_path)
|
|
|
|
|
|
def get_drive_neutral_value(config_path=None):
|
|
tester = MotorTester(config_path=config_path)
|
|
return {
|
|
'config_path': tester.config_path,
|
|
'value': int(tester.config['drive_pwm_neutral']),
|
|
'pins': {wheel: tester._drive_pin(wheel) for wheel in WHEELS},
|
|
}
|
|
|
|
|
|
def preview_drive_neutral_value(pwm_value, config_path=None):
|
|
tester = MotorTester(config_path=config_path)
|
|
value = int(pwm_value)
|
|
tester.center_all_steering()
|
|
tester.set_drive_neutral_pwm(value)
|
|
return {
|
|
'value': value,
|
|
'pins': {wheel: tester._drive_pin(wheel) for wheel in WHEELS},
|
|
}
|
|
|
|
|
|
def save_drive_neutral_value(pwm_value, config_path=None):
|
|
tester = MotorTester(config_path=config_path)
|
|
value = int(pwm_value)
|
|
_update_config_values(tester.config_path, {'drive_pwm_neutral': value})
|
|
tester.config = _load_config(tester.config_path)
|
|
tester.drive_pwm_neutral = int(tester.config['drive_pwm_neutral'])
|
|
tester.center_all_steering()
|
|
tester.set_drive_neutral_pwm(tester.drive_pwm_neutral)
|
|
return get_drive_neutral_value(config_path=tester.config_path)
|
|
|
|
|
|
def run_motor_test_in_container(wheel, action):
|
|
command = [
|
|
'docker',
|
|
'exec',
|
|
CONTAINER_NAME,
|
|
'python',
|
|
CONTAINER_SCRIPT_PATH,
|
|
wheel.lower(),
|
|
action,
|
|
]
|
|
result = subprocess.run(command, capture_output=True, text=True, check=False)
|
|
if result.returncode != 0:
|
|
stderr = result.stderr.strip()
|
|
stdout = result.stdout.strip()
|
|
message = stderr or stdout or 'Motortest im Container fehlgeschlagen'
|
|
raise RuntimeError(message)
|
|
|
|
try:
|
|
return json.loads(result.stdout)
|
|
except json.JSONDecodeError as exc:
|
|
raise RuntimeError('Antwort des Container-Motortests war ungültig') from exc
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) != 3:
|
|
print('Aufruf: admin_motor_test.py <fl|fr|cl|cr|rl|rr> <steer_left|steer_right|drive_forward|drive_backward>')
|
|
return 1
|
|
|
|
wheel = sys.argv[1].lower()
|
|
action = sys.argv[2]
|
|
result = run_motor_test(wheel, action, config_path=None)
|
|
print(json.dumps(result))
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
raise SystemExit(main())
|