+ Die Werte sind eine Schätzung auf Basis der Servo-Pulsbreite, des Parallax-Datenblatts und des eingestellten Radmaßes. Punkt-Drehen zählt nicht als Fahrweg.
+
+
+
+
+
+
+
+
Geschwindigkeit
+
0,00
+
cm/s
+
+
+
Weg
+
0,00
+
m
+
+
+
Quelle
+
+
+ ROSBridge
+ verbinde…
+
+
+
+ Impulsbreite
+ -
+
+
+
+ Servo-Drehzahl
+ -
+
+
+
+ Modus
+ -
+
+
+
+ Richtung
+ -
+
+
Kalibrierung
+
+ Für jede Stufe drückst du 1 m messen. Der Timer startet automatisch, sobald der Rover wirklich anfährt. Wenn genau 1 m erreicht ist, drückst du 1 m erreicht. Daraus wird die echte Geschwindigkeit in cm/s berechnet.
+
+
+ nicht geladen
+
+
+
+
+
+
+
+
+
+
@@ -286,6 +362,9 @@
var adminApiBase = window.location.protocol + '//' + window.location.hostname + ':8082';
var AUTH_TOKEN = 'Y3Vubw==';
var activeAdminSection = 'system';
+ var ros = null;
+ var roverCommandTopic = null;
+ var motorCommandsTopic = null;
var isMotorTestRunning = false;
var isContainerActionRunning = false;
@@ -303,6 +382,47 @@
var driveNeutralSavedValues = {};
var STEERING_NEUTRAL_STEP = 5;
var WHEEL_ORDER = ['fl', 'fr', 'cl', 'cr', 'rl', 'rr'];
+ var REQUIRED_CALIBRATION_POINTS = [0, 20, 30, 40, 50, 60, 80, 100];
+ var LOCOMOTION_MODE_NAMES = {
+ 0: 'Fake Ackermann',
+ 1: 'Ackermann',
+ 2: 'Punkt-Drehen',
+ 3: 'Crabbing'
+ };
+ var driveEstimator = {
+ configLoaded: false,
+ calibrationLoaded: false,
+ drivePwmNeutralAverage: 307,
+ drivePwmRange: 100,
+ pwmFrequencyHz: 50,
+ servoMaxRpm: 50,
+ servoFullScalePulseDeltaMs: 0.2,
+ wheelDiameterM: 0.1,
+ lastTickMs: Date.now(),
+ distanceM: 0,
+ speedMps: 0,
+ rpm: 0,
+ pulseWidthMs: 0,
+ directionLabel: 'Stopp',
+ modeLabel: '-',
+ calibrationInfo: '',
+ motorsEnabled: false,
+ locomotionMode: null,
+ motorSpeeds: [0, 0, 0, 0, 0, 0],
+ calibrationPoints: [],
+ savedCalibrationPoints: []
+ };
+ var driveCalibrationMeasurement = {
+ distanceM: 1.0,
+ activeIndex: null,
+ waitingForMotion: false,
+ startedAtMs: 0,
+ elapsedMs: 0,
+ stopCandidateStartedAtMs: 0,
+ stopDebounceMs: 400,
+ startThresholdPercent: 2,
+ stopThresholdPercent: 1
+ };
function submitPassword() {
var input = document.getElementById('auth_input');
@@ -328,22 +448,29 @@
function switchAdminSection(sectionName) {
activeAdminSection = sectionName;
var systemTab = document.getElementById('admin_tab_system');
+ var driveTab = document.getElementById('admin_tab_drive');
var motorTab = document.getElementById('admin_tab_motor');
var systemPanel = document.getElementById('admin_panel_system');
+ var drivePanel = document.getElementById('admin_panel_drive');
var motorPanel = document.getElementById('admin_panel_motor');
var isSystem = sectionName === 'system';
+ var isDrive = sectionName === 'drive';
+ var isMotor = sectionName === 'motor';
systemTab.classList.toggle('is_active', isSystem);
- motorTab.classList.toggle('is_active', !isSystem);
+ driveTab.classList.toggle('is_active', isDrive);
+ motorTab.classList.toggle('is_active', isMotor);
systemTab.setAttribute('aria-selected', isSystem ? 'true' : 'false');
- motorTab.setAttribute('aria-selected', isSystem ? 'false' : 'true');
+ driveTab.setAttribute('aria-selected', isDrive ? 'true' : 'false');
+ motorTab.setAttribute('aria-selected', isMotor ? 'true' : 'false');
systemPanel.classList.toggle('is_active', isSystem);
- motorPanel.classList.toggle('is_active', !isSystem);
- if (!isSystem) fetchMotorTestStatus();
+ drivePanel.classList.toggle('is_active', isDrive);
+ motorPanel.classList.toggle('is_active', isMotor);
+ if (isMotor) fetchMotorTestStatus();
}
function setText(id, value) {
var node = document.getElementById(id);
- if (node) node.textContent = value || '-';
+ if (node) node.textContent = (value === undefined || value === null || value === '') ? '-' : value;
}
function setServiceState(id, value) {
@@ -381,6 +508,474 @@
node.classList.add('state_unknown');
}
+ function formatNumber(value, digits) {
+ return Number(value || 0).toLocaleString('de-DE', {
+ minimumFractionDigits: digits,
+ maximumFractionDigits: digits
+ });
+ }
+
+ function formatSpeedCms(speedMps, digits) {
+ return formatNumber(Number(speedMps || 0) * 100, digits);
+ }
+
+ function setDriveEstimatorState(text, stateClass) {
+ var node = document.getElementById('drive_estimator_ros_state');
+ if (!node) return;
+ node.textContent = text || '-';
+ node.classList.remove('state_active', 'state_inactive', 'state_unknown');
+ node.classList.add(stateClass || 'state_unknown');
+ }
+
+ function cloneCalibrationPoints(points) {
+ return (points || []).map(function(item) {
+ return {
+ percent: Number(item.percent),
+ speed_mps: Number(item.speed_mps)
+ };
+ });
+ }
+
+ function normalizeCalibrationPoints(points) {
+ var source = cloneCalibrationPoints(points).sort(function(a, b) {
+ return a.percent - b.percent;
+ });
+ if (!source.length) source = [{percent: 0, speed_mps: 0}];
+
+ return REQUIRED_CALIBRATION_POINTS.map(function(targetPercent) {
+ var exact = source.find(function(point) {
+ return Number(point.percent) === Number(targetPercent);
+ });
+ if (exact) {
+ return {
+ percent: targetPercent,
+ speed_mps: Number(exact.speed_mps || 0)
+ };
+ }
+
+ var lower = null;
+ var upper = null;
+ source.forEach(function(point) {
+ if (point.percent < targetPercent) lower = point;
+ if (!upper && point.percent > targetPercent) upper = point;
+ });
+
+ if (!lower && upper) {
+ return {
+ percent: targetPercent,
+ speed_mps: Number(upper.speed_mps || 0)
+ };
+ }
+ if (lower && !upper) {
+ return {
+ percent: targetPercent,
+ speed_mps: Number(lower.speed_mps || 0)
+ };
+ }
+ if (lower && upper) {
+ var span = Number(upper.percent) - Number(lower.percent);
+ var ratio = span <= 0 ? 0 : (targetPercent - Number(lower.percent)) / span;
+ return {
+ percent: targetPercent,
+ speed_mps: Number(lower.speed_mps || 0) + (Number(upper.speed_mps || 0) - Number(lower.speed_mps || 0)) * ratio
+ };
+ }
+
+ return {
+ percent: targetPercent,
+ speed_mps: 0
+ };
+ });
+ }
+
+ function getDriveEstimatorDefaultCalibration() {
+ var wheelDiameterM = getDriveEstimatorDiameterM();
+ return REQUIRED_CALIBRATION_POINTS.map(function(percent) {
+ var rpm = driveEstimator.servoMaxRpm * (percent / 100.0);
+ return {
+ percent: percent,
+ speed_mps: (rpm / 60.0) * Math.PI * wheelDiameterM
+ };
+ });
+ }
+
+ function hasUnsavedDriveCalibrationChanges() {
+ if (driveEstimator.calibrationPoints.length !== driveEstimator.savedCalibrationPoints.length) return true;
+ for (var i = 0; i < driveEstimator.calibrationPoints.length; i += 1) {
+ var current = driveEstimator.calibrationPoints[i];
+ var saved = driveEstimator.savedCalibrationPoints[i];
+ if (!saved) return true;
+ if (Number(current.percent) !== Number(saved.percent)) return true;
+ if (Math.abs(Number(current.speed_mps) - Number(saved.speed_mps)) > 0.0005) return true;
+ }
+ return false;
+ }
+
+ function updateDriveCalibrationSaveButton() {
+ var button = document.getElementById('drive_calibration_save_button');
+ if (button) button.disabled = !driveEstimator.calibrationLoaded || !hasUnsavedDriveCalibrationChanges();
+ }
+
+ function renderDriveEstimatorCalibration() {
+ var grid = document.getElementById('drive_calibration_grid');
+ if (!grid) return;
+ var html = [];
+ driveEstimator.calibrationPoints.forEach(function(point, index) {
+ var saved = driveEstimator.savedCalibrationPoints[index];
+ var isDirty = !saved || Math.abs(Number(point.speed_mps) - Number(saved.speed_mps)) > 0.0005;
+ var isActive = driveCalibrationMeasurement.activeIndex === index;
+ var measuredSeconds = point.speed_mps > 0 ? (driveCalibrationMeasurement.distanceM / point.speed_mps) : 0;
+ var statusText = 'Bereit';
+ if (isActive && driveCalibrationMeasurement.waitingForMotion) statusText = 'Warte auf Anfahrt';
+ else if (isActive) statusText = 'Misst: ' + formatNumber(driveCalibrationMeasurement.elapsedMs / 1000.0, 2) + ' s';
+ html.push(
+ '' +
+ '
' + point.percent + ' %
' +
+ '
' + formatSpeedCms(point.speed_mps, 1) + ' cm/s
' +
+ '
1 m in ' + (point.speed_mps > 0 ? (formatNumber(measuredSeconds, 2) + ' s') : '-') + '