berechnung Weg und geschwindigkeit
This commit is contained in:
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"updated_at": "2026-05-23 09:21:10",
|
||||||
|
"points": [
|
||||||
|
{
|
||||||
|
"percent": 0,
|
||||||
|
"speed_mps": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"percent": 20,
|
||||||
|
"speed_mps": 0.04629415304846998
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"percent": 30,
|
||||||
|
"speed_mps": 0.1198609612849095
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"percent": 40,
|
||||||
|
"speed_mps": 0.17397355601948503
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"percent": 50,
|
||||||
|
"speed_mps": 0.21978021978021978
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"percent": 60,
|
||||||
|
"speed_mps": 0.22972662531587412
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"percent": 80,
|
||||||
|
"speed_mps": 0.21968365553602814
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"percent": 100,
|
||||||
|
"speed_mps": 0.23523876734885907
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>CUNO // Admin</title>
|
<title>CUNO // Admin</title>
|
||||||
<link rel="stylesheet" href="./app.css?v=20260522104500">
|
<link rel="stylesheet" href="./app.css?v=20260522104500">
|
||||||
|
<script src="./vendor/roslib.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body class="page-admin">
|
<body class="page-admin">
|
||||||
|
|
||||||
@@ -38,6 +39,7 @@
|
|||||||
<div class="admin-shell">
|
<div class="admin-shell">
|
||||||
<div class="admin-section-tabs" role="tablist" aria-label="Admin-Bereiche">
|
<div class="admin-section-tabs" role="tablist" aria-label="Admin-Bereiche">
|
||||||
<button id="admin_tab_system" class="admin-section-tab is_active" role="tab" aria-selected="true" onclick="switchAdminSection('system')">Admin</button>
|
<button id="admin_tab_system" class="admin-section-tab is_active" role="tab" aria-selected="true" onclick="switchAdminSection('system')">Admin</button>
|
||||||
|
<button id="admin_tab_drive" class="admin-section-tab" role="tab" aria-selected="false" onclick="switchAdminSection('drive')">Geschwindigkeit / Weg</button>
|
||||||
<button id="admin_tab_motor" class="admin-section-tab" role="tab" aria-selected="false" onclick="switchAdminSection('motor')">Motorentest</button>
|
<button id="admin_tab_motor" class="admin-section-tab" role="tab" aria-selected="false" onclick="switchAdminSection('motor')">Motorentest</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -169,6 +171,80 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section id="admin_panel_drive" class="admin-section-panel" role="tabpanel">
|
||||||
|
<div class="admin-section-grid">
|
||||||
|
<div class="admin-col">
|
||||||
|
<div class="panel">
|
||||||
|
<div class="p-hdr">
|
||||||
|
<div class="p-dot"></div>
|
||||||
|
<span class="p-lbl">Geschwindigkeit / Weg</span>
|
||||||
|
<span class="p-tag">Estimated Telemetry</span>
|
||||||
|
</div>
|
||||||
|
<div class="copy-text drive-estimator-copy">
|
||||||
|
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.
|
||||||
|
</div>
|
||||||
|
<div class="drive-estimator-toolbar">
|
||||||
|
<label class="drive-estimator-input">
|
||||||
|
<span>Raddurchmesser</span>
|
||||||
|
<input id="drive_estimator_diameter_mm" type="number" min="20" max="300" step="1" value="100">
|
||||||
|
<em>mm</em>
|
||||||
|
</label>
|
||||||
|
<button class="act-btn warm" onclick="resetDriveEstimator()">Nullen</button>
|
||||||
|
</div>
|
||||||
|
<div class="drive-estimator-grid">
|
||||||
|
<article class="drive-estimator-card">
|
||||||
|
<div class="drive-estimator-label">Geschwindigkeit</div>
|
||||||
|
<div id="drive_estimator_speed" class="drive-estimator-value">0,00</div>
|
||||||
|
<div class="drive-estimator-unit">cm/s</div>
|
||||||
|
</article>
|
||||||
|
<article class="drive-estimator-card">
|
||||||
|
<div class="drive-estimator-label">Weg</div>
|
||||||
|
<div id="drive_estimator_distance" class="drive-estimator-value">0,00</div>
|
||||||
|
<div class="drive-estimator-unit">m</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
<div class="ss-hdr"><div class="ss-hdr-line"></div><span class="ss-hdr-txt">Quelle</span><div class="ss-hdr-line"></div></div>
|
||||||
|
<div class="sr">
|
||||||
|
<span class="sr-led led-blue"></span>
|
||||||
|
<span class="sr-key">ROSBridge</span>
|
||||||
|
<strong id="drive_estimator_ros_state" class="sr-val service_state state_unknown">verbinde…</strong>
|
||||||
|
</div>
|
||||||
|
<div class="sr">
|
||||||
|
<span class="sr-led led-ok"></span>
|
||||||
|
<span class="sr-key">Impulsbreite</span>
|
||||||
|
<span id="drive_estimator_pulse" class="sr-val">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="sr">
|
||||||
|
<span class="sr-led led-blue"></span>
|
||||||
|
<span class="sr-key">Servo-Drehzahl</span>
|
||||||
|
<span id="drive_estimator_rpm" class="sr-val">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="sr">
|
||||||
|
<span class="sr-led led-blue"></span>
|
||||||
|
<span class="sr-key">Modus</span>
|
||||||
|
<span id="drive_estimator_mode" class="sr-val">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="sr">
|
||||||
|
<span class="sr-led led-blue"></span>
|
||||||
|
<span class="sr-key">Richtung</span>
|
||||||
|
<span id="drive_estimator_direction" class="sr-val">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="ss-hdr"><div class="ss-hdr-line"></div><span class="ss-hdr-txt">Kalibrierung</span><div class="ss-hdr-line"></div></div>
|
||||||
|
<div class="copy-text drive-estimator-copy">
|
||||||
|
Für jede Stufe drückst du <code>1 m messen</code>. Der Timer startet automatisch, sobald der Rover wirklich anfährt. Wenn genau <code>1 m</code> erreicht ist, drückst du <code>1 m erreicht</code>. Daraus wird die echte Geschwindigkeit in <code>cm/s</code> berechnet.
|
||||||
|
</div>
|
||||||
|
<div class="drive-calibration-toolbar">
|
||||||
|
<span id="drive_calibration_info" class="ctrl-status drive-calibration-info">nicht geladen</span>
|
||||||
|
<button class="act-btn subtle" onclick="loadDriveEstimatorCalibration()">Neu laden</button>
|
||||||
|
<button class="act-btn warm" onclick="fillDriveEstimatorCalibrationDefaults()">Datenblatt</button>
|
||||||
|
<button id="drive_calibration_save_button" class="act-btn" onclick="saveDriveEstimatorCalibration()">Speichern</button>
|
||||||
|
</div>
|
||||||
|
<div id="drive_calibration_grid" class="drive-calibration-grid"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section id="admin_panel_motor" class="admin-section-panel motor-admin-tab" role="tabpanel">
|
<section id="admin_panel_motor" class="admin-section-panel motor-admin-tab" role="tabpanel">
|
||||||
<div class="admin-section-grid">
|
<div class="admin-section-grid">
|
||||||
<div class="admin-col">
|
<div class="admin-col">
|
||||||
@@ -286,6 +362,9 @@
|
|||||||
var adminApiBase = window.location.protocol + '//' + window.location.hostname + ':8082';
|
var adminApiBase = window.location.protocol + '//' + window.location.hostname + ':8082';
|
||||||
var AUTH_TOKEN = 'Y3Vubw==';
|
var AUTH_TOKEN = 'Y3Vubw==';
|
||||||
var activeAdminSection = 'system';
|
var activeAdminSection = 'system';
|
||||||
|
var ros = null;
|
||||||
|
var roverCommandTopic = null;
|
||||||
|
var motorCommandsTopic = null;
|
||||||
|
|
||||||
var isMotorTestRunning = false;
|
var isMotorTestRunning = false;
|
||||||
var isContainerActionRunning = false;
|
var isContainerActionRunning = false;
|
||||||
@@ -303,6 +382,47 @@
|
|||||||
var driveNeutralSavedValues = {};
|
var driveNeutralSavedValues = {};
|
||||||
var STEERING_NEUTRAL_STEP = 5;
|
var STEERING_NEUTRAL_STEP = 5;
|
||||||
var WHEEL_ORDER = ['fl', 'fr', 'cl', 'cr', 'rl', 'rr'];
|
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() {
|
function submitPassword() {
|
||||||
var input = document.getElementById('auth_input');
|
var input = document.getElementById('auth_input');
|
||||||
@@ -328,22 +448,29 @@
|
|||||||
function switchAdminSection(sectionName) {
|
function switchAdminSection(sectionName) {
|
||||||
activeAdminSection = sectionName;
|
activeAdminSection = sectionName;
|
||||||
var systemTab = document.getElementById('admin_tab_system');
|
var systemTab = document.getElementById('admin_tab_system');
|
||||||
|
var driveTab = document.getElementById('admin_tab_drive');
|
||||||
var motorTab = document.getElementById('admin_tab_motor');
|
var motorTab = document.getElementById('admin_tab_motor');
|
||||||
var systemPanel = document.getElementById('admin_panel_system');
|
var systemPanel = document.getElementById('admin_panel_system');
|
||||||
|
var drivePanel = document.getElementById('admin_panel_drive');
|
||||||
var motorPanel = document.getElementById('admin_panel_motor');
|
var motorPanel = document.getElementById('admin_panel_motor');
|
||||||
var isSystem = sectionName === 'system';
|
var isSystem = sectionName === 'system';
|
||||||
|
var isDrive = sectionName === 'drive';
|
||||||
|
var isMotor = sectionName === 'motor';
|
||||||
systemTab.classList.toggle('is_active', isSystem);
|
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');
|
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);
|
systemPanel.classList.toggle('is_active', isSystem);
|
||||||
motorPanel.classList.toggle('is_active', !isSystem);
|
drivePanel.classList.toggle('is_active', isDrive);
|
||||||
if (!isSystem) fetchMotorTestStatus();
|
motorPanel.classList.toggle('is_active', isMotor);
|
||||||
|
if (isMotor) fetchMotorTestStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setText(id, value) {
|
function setText(id, value) {
|
||||||
var node = document.getElementById(id);
|
var node = document.getElementById(id);
|
||||||
if (node) node.textContent = value || '-';
|
if (node) node.textContent = (value === undefined || value === null || value === '') ? '-' : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setServiceState(id, value) {
|
function setServiceState(id, value) {
|
||||||
@@ -381,6 +508,474 @@
|
|||||||
node.classList.add('state_unknown');
|
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(
|
||||||
|
'<article class="drive-calibration-card' + (isDirty ? ' is_dirty' : '') + (isActive ? ' is_active' : '') + '">' +
|
||||||
|
'<div class="drive-calibration-percent">' + point.percent + ' %</div>' +
|
||||||
|
'<div class="drive-calibration-value">' + formatSpeedCms(point.speed_mps, 1) + ' cm/s</div>' +
|
||||||
|
'<div class="drive-calibration-meta">1 m in ' + (point.speed_mps > 0 ? (formatNumber(measuredSeconds, 2) + ' s') : '-') + '</div>' +
|
||||||
|
'<div class="drive-calibration-status">' + statusText + '</div>' +
|
||||||
|
'<div class="drive-calibration-actions">' +
|
||||||
|
'<button class="act-btn subtle drive-calibration-button" onclick="startDriveCalibrationMeasurement(' + index + ')"' + (driveCalibrationMeasurement.activeIndex !== null && !isActive ? ' disabled' : '') + '>' + (isActive ? 'Aktiv' : '1 m messen') + '</button>' +
|
||||||
|
'<button class="act-btn drive-calibration-button" onclick="finishDriveCalibrationMeasurement(' + index + ')"' + (isActive ? '' : ' disabled') + '>1 m erreicht</button>' +
|
||||||
|
'<button class="act-btn warm drive-calibration-button" onclick="cancelDriveCalibrationMeasurement(' + index + ')"' + (isActive ? '' : ' disabled') + '>Abbrechen</button>' +
|
||||||
|
'</div>' +
|
||||||
|
'</article>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
grid.innerHTML = html.join('');
|
||||||
|
var infoNode = document.getElementById('drive_calibration_info');
|
||||||
|
if (infoNode) infoNode.textContent = driveEstimator.calibrationInfo || '-';
|
||||||
|
updateDriveCalibrationSaveButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDriveMeasurementMoving() {
|
||||||
|
return isTranslationMode() && Math.abs(getAverageMotorSpeedPercent()) >= driveCalibrationMeasurement.startThresholdPercent;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDriveMeasurementStopped() {
|
||||||
|
return Math.abs(getAverageMotorSpeedPercent()) <= driveCalibrationMeasurement.stopThresholdPercent;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startDriveCalibrationMeasurement(index) {
|
||||||
|
if (!driveEstimator.calibrationPoints[index]) return;
|
||||||
|
driveCalibrationMeasurement.activeIndex = index;
|
||||||
|
driveCalibrationMeasurement.waitingForMotion = true;
|
||||||
|
driveCalibrationMeasurement.startedAtMs = 0;
|
||||||
|
driveCalibrationMeasurement.elapsedMs = 0;
|
||||||
|
driveCalibrationMeasurement.stopCandidateStartedAtMs = 0;
|
||||||
|
driveEstimator.calibrationInfo = driveEstimator.calibrationPoints[index].percent + ' %: warte auf Anfahrt';
|
||||||
|
renderDriveEstimatorCalibration();
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishDriveCalibrationMeasurement(index, automatic) {
|
||||||
|
if (driveCalibrationMeasurement.activeIndex !== index) return;
|
||||||
|
if (driveCalibrationMeasurement.waitingForMotion || driveCalibrationMeasurement.elapsedMs <= 0) {
|
||||||
|
alert('Die Messung hat noch nicht begonnen. Erst losfahren, dann bei 1 m erneut drücken.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var elapsedSeconds = driveCalibrationMeasurement.elapsedMs / 1000.0;
|
||||||
|
driveEstimator.calibrationPoints[index].speed_mps = driveCalibrationMeasurement.distanceM / elapsedSeconds;
|
||||||
|
driveEstimator.calibrationInfo = driveEstimator.calibrationPoints[index].percent + ' %: 1 m in ' + formatNumber(elapsedSeconds, 2) + ' s' + (automatic ? ' (auto)' : '');
|
||||||
|
driveCalibrationMeasurement.activeIndex = null;
|
||||||
|
driveCalibrationMeasurement.waitingForMotion = false;
|
||||||
|
driveCalibrationMeasurement.startedAtMs = 0;
|
||||||
|
driveCalibrationMeasurement.elapsedMs = 0;
|
||||||
|
driveCalibrationMeasurement.stopCandidateStartedAtMs = 0;
|
||||||
|
renderDriveEstimatorCalibration();
|
||||||
|
renderDriveEstimator();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelDriveCalibrationMeasurement(index) {
|
||||||
|
if (driveCalibrationMeasurement.activeIndex !== index) return;
|
||||||
|
driveCalibrationMeasurement.activeIndex = null;
|
||||||
|
driveCalibrationMeasurement.waitingForMotion = false;
|
||||||
|
driveCalibrationMeasurement.startedAtMs = 0;
|
||||||
|
driveCalibrationMeasurement.elapsedMs = 0;
|
||||||
|
driveCalibrationMeasurement.stopCandidateStartedAtMs = 0;
|
||||||
|
driveEstimator.calibrationInfo = 'Messung abgebrochen';
|
||||||
|
renderDriveEstimatorCalibration();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDriveCalibrationMeasurementState() {
|
||||||
|
if (driveCalibrationMeasurement.activeIndex === null) return;
|
||||||
|
if (driveCalibrationMeasurement.waitingForMotion) {
|
||||||
|
if (isDriveMeasurementMoving()) {
|
||||||
|
driveCalibrationMeasurement.waitingForMotion = false;
|
||||||
|
driveCalibrationMeasurement.startedAtMs = Date.now();
|
||||||
|
driveCalibrationMeasurement.elapsedMs = 0;
|
||||||
|
driveCalibrationMeasurement.stopCandidateStartedAtMs = 0;
|
||||||
|
driveEstimator.calibrationInfo = driveEstimator.calibrationPoints[driveCalibrationMeasurement.activeIndex].percent + ' %: Messung läuft';
|
||||||
|
renderDriveEstimatorCalibration();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (driveCalibrationMeasurement.startedAtMs > 0) {
|
||||||
|
var nowMs = Date.now();
|
||||||
|
var previousTenths = Math.floor(driveCalibrationMeasurement.elapsedMs / 100);
|
||||||
|
driveCalibrationMeasurement.elapsedMs = Math.max(0, nowMs - driveCalibrationMeasurement.startedAtMs);
|
||||||
|
var nextTenths = Math.floor(driveCalibrationMeasurement.elapsedMs / 100);
|
||||||
|
if (nextTenths !== previousTenths) renderDriveEstimatorCalibration();
|
||||||
|
if (!isDriveMeasurementStopped()) {
|
||||||
|
driveCalibrationMeasurement.stopCandidateStartedAtMs = 0;
|
||||||
|
} else if (driveCalibrationMeasurement.stopCandidateStartedAtMs === 0) {
|
||||||
|
driveCalibrationMeasurement.stopCandidateStartedAtMs = nowMs;
|
||||||
|
} else if (nowMs - driveCalibrationMeasurement.stopCandidateStartedAtMs >= driveCalibrationMeasurement.stopDebounceMs) {
|
||||||
|
finishDriveCalibrationMeasurement(driveCalibrationMeasurement.activeIndex, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function interpolateCalibrationSpeed(percentMagnitude) {
|
||||||
|
var points = driveEstimator.calibrationPoints;
|
||||||
|
if (!points || !points.length) return 0;
|
||||||
|
var p = Math.max(0, Math.min(100, Math.abs(percentMagnitude)));
|
||||||
|
if (p <= points[0].percent) return Number(points[0].speed_mps || 0);
|
||||||
|
for (var i = 1; i < points.length; i += 1) {
|
||||||
|
var left = points[i - 1];
|
||||||
|
var right = points[i];
|
||||||
|
if (p <= right.percent) {
|
||||||
|
var span = right.percent - left.percent;
|
||||||
|
if (span <= 0) return Number(right.speed_mps || 0);
|
||||||
|
var ratio = (p - left.percent) / span;
|
||||||
|
return Number(left.speed_mps || 0) + (Number(right.speed_mps || 0) - Number(left.speed_mps || 0)) * ratio;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Number(points[points.length - 1].speed_mps || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillDriveEstimatorCalibrationDefaults() {
|
||||||
|
if (driveCalibrationMeasurement.activeIndex !== null) return;
|
||||||
|
driveEstimator.calibrationPoints = normalizeCalibrationPoints(getDriveEstimatorDefaultCalibration());
|
||||||
|
driveEstimator.calibrationInfo = 'Datenblatt-Kurve geladen';
|
||||||
|
renderDriveEstimatorCalibration();
|
||||||
|
renderDriveEstimator();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDriveEstimatorDiameterM() {
|
||||||
|
var input = document.getElementById('drive_estimator_diameter_mm');
|
||||||
|
if (!input) return driveEstimator.wheelDiameterM;
|
||||||
|
var mm = parseFloat(input.value);
|
||||||
|
if (isNaN(mm)) return driveEstimator.wheelDiameterM;
|
||||||
|
mm = Math.max(20, Math.min(300, mm));
|
||||||
|
input.value = String(mm);
|
||||||
|
driveEstimator.wheelDiameterM = mm / 1000.0;
|
||||||
|
return driveEstimator.wheelDiameterM;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAverageMotorSpeedPercent() {
|
||||||
|
if (!driveEstimator.motorSpeeds || !driveEstimator.motorSpeeds.length) return 0;
|
||||||
|
var sum = 0;
|
||||||
|
var count = 0;
|
||||||
|
driveEstimator.motorSpeeds.forEach(function(value) {
|
||||||
|
if (typeof value === 'number' && !isNaN(value)) {
|
||||||
|
sum += value;
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return count ? (sum / count) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTranslationMode() {
|
||||||
|
return driveEstimator.locomotionMode !== 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDriveEstimatorModel() {
|
||||||
|
updateDriveCalibrationMeasurementState();
|
||||||
|
var nowMs = Date.now();
|
||||||
|
var dt = Math.max(0, (nowMs - driveEstimator.lastTickMs) / 1000.0);
|
||||||
|
driveEstimator.lastTickMs = nowMs;
|
||||||
|
|
||||||
|
var avgSpeedPercent = getAverageMotorSpeedPercent();
|
||||||
|
var direction = avgSpeedPercent > 0 ? 1 : (avgSpeedPercent < 0 ? -1 : 0);
|
||||||
|
var pulsePeriodMs = 1000.0 / driveEstimator.pwmFrequencyHz;
|
||||||
|
var pulseStepMs = pulsePeriodMs / 4096.0;
|
||||||
|
var pwmValue = driveEstimator.drivePwmNeutralAverage + (avgSpeedPercent / 100.0 * driveEstimator.drivePwmRange);
|
||||||
|
var pulseWidthMs = pwmValue * pulseStepMs;
|
||||||
|
var neutralPulseMs = driveEstimator.drivePwmNeutralAverage * pulseStepMs;
|
||||||
|
var deltaMs = pulseWidthMs - neutralPulseMs;
|
||||||
|
var rpm = 0;
|
||||||
|
if (isTranslationMode() && driveEstimator.motorsEnabled) {
|
||||||
|
rpm = (deltaMs / driveEstimator.servoFullScalePulseDeltaMs) * driveEstimator.servoMaxRpm;
|
||||||
|
rpm = Math.max(-driveEstimator.servoMaxRpm, Math.min(driveEstimator.servoMaxRpm, rpm));
|
||||||
|
}
|
||||||
|
|
||||||
|
var speedMagnitude = isTranslationMode() && driveEstimator.motorsEnabled
|
||||||
|
? interpolateCalibrationSpeed(avgSpeedPercent)
|
||||||
|
: 0;
|
||||||
|
var speedMps = direction === 0 ? 0 : speedMagnitude * direction;
|
||||||
|
|
||||||
|
if (Math.abs(speedMps) < 0.0005) speedMps = 0;
|
||||||
|
if (dt > 0 && dt < 2.5) driveEstimator.distanceM += Math.abs(speedMps) * dt;
|
||||||
|
|
||||||
|
driveEstimator.rpm = rpm;
|
||||||
|
driveEstimator.speedMps = speedMps;
|
||||||
|
driveEstimator.pulseWidthMs = pulseWidthMs;
|
||||||
|
driveEstimator.modeLabel = LOCOMOTION_MODE_NAMES[driveEstimator.locomotionMode] || '-';
|
||||||
|
driveEstimator.directionLabel = !driveEstimator.motorsEnabled ? 'Motoren aus'
|
||||||
|
: !isTranslationMode() ? 'Drehen auf der Stelle'
|
||||||
|
: direction > 0 ? 'Vorwärts'
|
||||||
|
: direction < 0 ? 'Rückwärts'
|
||||||
|
: 'Stopp';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDriveEstimator() {
|
||||||
|
updateDriveEstimatorModel();
|
||||||
|
setText('drive_estimator_speed', formatSpeedCms(driveEstimator.speedMps, 1));
|
||||||
|
setText('drive_estimator_distance', formatNumber(driveEstimator.distanceM, 2));
|
||||||
|
setText('drive_estimator_pulse', formatNumber(driveEstimator.pulseWidthMs, 3) + ' ms');
|
||||||
|
setText('drive_estimator_rpm', formatNumber(driveEstimator.rpm, 1) + ' RPM');
|
||||||
|
setText('drive_estimator_mode', driveEstimator.modeLabel);
|
||||||
|
setText('drive_estimator_direction', driveEstimator.directionLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetDriveEstimator() {
|
||||||
|
driveEstimator.distanceM = 0;
|
||||||
|
driveEstimator.lastTickMs = Date.now();
|
||||||
|
renderDriveEstimator();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchDriveEstimatorConfig() {
|
||||||
|
try {
|
||||||
|
var response = await fetch(adminApiBase + '/api/drive-estimator/config');
|
||||||
|
if (!response.ok) throw new Error('status ' + response.status);
|
||||||
|
var data = await response.json();
|
||||||
|
var estimator = data.estimator || {};
|
||||||
|
driveEstimator.drivePwmNeutralAverage = Number(estimator.drive_pwm_neutral_average || driveEstimator.drivePwmNeutralAverage);
|
||||||
|
driveEstimator.drivePwmRange = Number(estimator.drive_pwm_range || driveEstimator.drivePwmRange);
|
||||||
|
driveEstimator.pwmFrequencyHz = Number(estimator.pwm_frequency_hz || driveEstimator.pwmFrequencyHz);
|
||||||
|
driveEstimator.servoMaxRpm = Number(estimator.servo_max_rpm || driveEstimator.servoMaxRpm);
|
||||||
|
driveEstimator.servoFullScalePulseDeltaMs = Number(estimator.servo_full_scale_pulse_delta_ms || driveEstimator.servoFullScalePulseDeltaMs);
|
||||||
|
driveEstimator.wheelDiameterM = Number(estimator.wheel_diameter_m_default || driveEstimator.wheelDiameterM);
|
||||||
|
var input = document.getElementById('drive_estimator_diameter_mm');
|
||||||
|
if (input) input.value = String(Math.round(driveEstimator.wheelDiameterM * 1000));
|
||||||
|
driveEstimator.configLoaded = true;
|
||||||
|
if (!driveEstimator.calibrationLoaded) {
|
||||||
|
driveEstimator.calibrationPoints = normalizeCalibrationPoints(getDriveEstimatorDefaultCalibration());
|
||||||
|
driveEstimator.savedCalibrationPoints = normalizeCalibrationPoints(driveEstimator.calibrationPoints);
|
||||||
|
driveEstimator.calibrationInfo = 'Datenblatt-Kurve';
|
||||||
|
renderDriveEstimatorCalibration();
|
||||||
|
}
|
||||||
|
renderDriveEstimator();
|
||||||
|
} catch (e) {
|
||||||
|
setDriveEstimatorState('Config fehlt', 'state_unknown');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadDriveEstimatorCalibration() {
|
||||||
|
try {
|
||||||
|
var response = await fetch(adminApiBase + '/api/drive-estimator/calibration');
|
||||||
|
if (!response.ok) throw new Error('status ' + response.status);
|
||||||
|
var data = await response.json();
|
||||||
|
var calibration = data.calibration || {};
|
||||||
|
var points = Array.isArray(calibration.points) && calibration.points.length
|
||||||
|
? calibration.points
|
||||||
|
: getDriveEstimatorDefaultCalibration();
|
||||||
|
var useDefaults = calibration.source !== 'saved' && points.every(function(point) {
|
||||||
|
return Math.abs(Number(point.speed_mps || 0)) < 0.0005;
|
||||||
|
});
|
||||||
|
if (useDefaults) points = getDriveEstimatorDefaultCalibration();
|
||||||
|
driveEstimator.calibrationPoints = normalizeCalibrationPoints(points);
|
||||||
|
driveEstimator.savedCalibrationPoints = normalizeCalibrationPoints(points);
|
||||||
|
driveEstimator.calibrationLoaded = true;
|
||||||
|
driveCalibrationMeasurement.activeIndex = null;
|
||||||
|
driveCalibrationMeasurement.waitingForMotion = false;
|
||||||
|
driveCalibrationMeasurement.startedAtMs = 0;
|
||||||
|
driveCalibrationMeasurement.elapsedMs = 0;
|
||||||
|
driveEstimator.calibrationInfo = calibration.source === 'saved'
|
||||||
|
? ('gespeichert: ' + (calibration.updated_at || 'unbekannt'))
|
||||||
|
: 'Datenblatt-Kurve';
|
||||||
|
renderDriveEstimatorCalibration();
|
||||||
|
renderDriveEstimator();
|
||||||
|
} catch (e) {
|
||||||
|
driveEstimator.calibrationPoints = normalizeCalibrationPoints(getDriveEstimatorDefaultCalibration());
|
||||||
|
driveEstimator.savedCalibrationPoints = normalizeCalibrationPoints(driveEstimator.calibrationPoints);
|
||||||
|
driveEstimator.calibrationLoaded = true;
|
||||||
|
driveCalibrationMeasurement.activeIndex = null;
|
||||||
|
driveCalibrationMeasurement.waitingForMotion = false;
|
||||||
|
driveCalibrationMeasurement.startedAtMs = 0;
|
||||||
|
driveCalibrationMeasurement.elapsedMs = 0;
|
||||||
|
driveEstimator.calibrationInfo = 'Fallback: Datenblatt-Kurve';
|
||||||
|
renderDriveEstimatorCalibration();
|
||||||
|
renderDriveEstimator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveDriveEstimatorCalibration() {
|
||||||
|
if (!driveEstimator.calibrationLoaded) return;
|
||||||
|
if (driveCalibrationMeasurement.activeIndex !== null) {
|
||||||
|
alert('Bitte die laufende Messung erst abschließen oder abbrechen.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
var response = await fetch(adminApiBase + '/api/drive-estimator/calibration', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({points: driveEstimator.calibrationPoints})
|
||||||
|
});
|
||||||
|
var data = await response.json();
|
||||||
|
if (!response.ok) throw new Error(data.status || ('status ' + response.status));
|
||||||
|
var calibration = data.calibration || {};
|
||||||
|
var points = Array.isArray(calibration.points) ? calibration.points : driveEstimator.calibrationPoints;
|
||||||
|
driveEstimator.calibrationPoints = normalizeCalibrationPoints(points);
|
||||||
|
driveEstimator.savedCalibrationPoints = normalizeCalibrationPoints(points);
|
||||||
|
driveEstimator.calibrationInfo = calibration.updated_at
|
||||||
|
? ('gespeichert: ' + calibration.updated_at)
|
||||||
|
: 'gespeichert';
|
||||||
|
renderDriveEstimatorCalibration();
|
||||||
|
setAdminStatus(data.status || 'Kalibrierung gespeichert');
|
||||||
|
window.setTimeout(fetchStatus, 1500);
|
||||||
|
} catch (e) {
|
||||||
|
alert(e.message || 'Die Kalibrierung konnte nicht gespeichert werden.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectDriveEstimatorRos() {
|
||||||
|
if (typeof ROSLIB === 'undefined') {
|
||||||
|
setDriveEstimatorState('ROSLIB fehlt', 'state_inactive');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ros) return;
|
||||||
|
|
||||||
|
ros = new ROSLIB.Ros({
|
||||||
|
url: 'ws://' + window.location.hostname + ':9090'
|
||||||
|
});
|
||||||
|
|
||||||
|
ros.on('connection', function() {
|
||||||
|
setDriveEstimatorState('verbunden', 'state_active');
|
||||||
|
});
|
||||||
|
ros.on('close', function() {
|
||||||
|
setDriveEstimatorState('getrennt', 'state_inactive');
|
||||||
|
ros = null;
|
||||||
|
roverCommandTopic = null;
|
||||||
|
motorCommandsTopic = null;
|
||||||
|
window.setTimeout(connectDriveEstimatorRos, 3000);
|
||||||
|
});
|
||||||
|
ros.on('error', function() {
|
||||||
|
setDriveEstimatorState('Fehler', 'state_unknown');
|
||||||
|
});
|
||||||
|
|
||||||
|
roverCommandTopic = new ROSLIB.Topic({
|
||||||
|
ros: ros,
|
||||||
|
name: '/rover_command',
|
||||||
|
messageType: 'exomy/RoverCommand'
|
||||||
|
});
|
||||||
|
roverCommandTopic.subscribe(function(message) {
|
||||||
|
driveEstimator.motorsEnabled = !!message.motors_enabled;
|
||||||
|
driveEstimator.locomotionMode = Number(message.locomotion_mode);
|
||||||
|
driveEstimator.modeLabel = LOCOMOTION_MODE_NAMES[driveEstimator.locomotionMode] || '-';
|
||||||
|
});
|
||||||
|
|
||||||
|
motorCommandsTopic = new ROSLIB.Topic({
|
||||||
|
ros: ros,
|
||||||
|
name: '/motor_commands',
|
||||||
|
messageType: 'exomy/MotorCommands'
|
||||||
|
});
|
||||||
|
motorCommandsTopic.subscribe(function(message) {
|
||||||
|
driveEstimator.motorSpeeds = Array.isArray(message.motor_speeds) ? message.motor_speeds.map(Number) : [0, 0, 0, 0, 0, 0];
|
||||||
|
renderDriveEstimator();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function temperatureToWidth(value) {
|
function temperatureToWidth(value) {
|
||||||
if (!value || typeof value !== 'string') return '0%';
|
if (!value || typeof value !== 'string') return '0%';
|
||||||
var n = parseFloat(value.replace('°C', '').replace('°C', '').replace(',', '.'));
|
var n = parseFloat(value.replace('°C', '').replace('°C', '').replace(',', '.'));
|
||||||
@@ -880,6 +1475,8 @@
|
|||||||
});
|
});
|
||||||
if (window.location.hash === '#motor') {
|
if (window.location.hash === '#motor') {
|
||||||
switchAdminSection('motor');
|
switchAdminSection('motor');
|
||||||
|
} else if (window.location.hash === '#drive') {
|
||||||
|
switchAdminSection('drive');
|
||||||
}
|
}
|
||||||
updateClock();
|
updateClock();
|
||||||
window.setInterval(updateClock, 1000);
|
window.setInterval(updateClock, 1000);
|
||||||
@@ -887,10 +1484,15 @@
|
|||||||
fetchDelay();
|
fetchDelay();
|
||||||
fetchSpeedLimit();
|
fetchSpeedLimit();
|
||||||
fetchMotorTestStatus();
|
fetchMotorTestStatus();
|
||||||
|
fetchDriveEstimatorConfig();
|
||||||
|
loadDriveEstimatorCalibration();
|
||||||
|
connectDriveEstimatorRos();
|
||||||
|
renderDriveEstimator();
|
||||||
window.setInterval(fetchStatus, 10000);
|
window.setInterval(fetchStatus, 10000);
|
||||||
window.setInterval(fetchDelay, 5000);
|
window.setInterval(fetchDelay, 5000);
|
||||||
window.setInterval(fetchSpeedLimit, 5000);
|
window.setInterval(fetchSpeedLimit, 5000);
|
||||||
window.setInterval(fetchMotorTestStatus, 4000);
|
window.setInterval(fetchMotorTestStatus, 4000);
|
||||||
|
window.setInterval(renderDriveEstimator, 250);
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('beforeunload', function(event) {
|
window.addEventListener('beforeunload', function(event) {
|
||||||
|
|||||||
@@ -990,6 +990,10 @@ body.page-admin .motor-admin-tab .steering_neutral_adjust{ padding:4px 10px !imp
|
|||||||
body.page-admin .admin-section-tab{ flex:1; text-align:center; padding:10px 8px; }
|
body.page-admin .admin-section-tab{ flex:1; text-align:center; padding:10px 8px; }
|
||||||
body.page-admin .motor-admin-tab .tab-row{ grid-template-columns:1fr; }
|
body.page-admin .motor-admin-tab .tab-row{ grid-template-columns:1fr; }
|
||||||
body.page-admin .motor-admin-tab .tab-box + .tab-box{ border-left:none; border-top:1px solid rgba(0,255,145,.06); }
|
body.page-admin .motor-admin-tab .tab-box + .tab-box{ border-left:none; border-top:1px solid rgba(0,255,145,.06); }
|
||||||
|
body.page-admin .drive-estimator-toolbar{ flex-direction:column; align-items:stretch; }
|
||||||
|
body.page-admin .drive-estimator-input{ grid-template-columns:1fr; }
|
||||||
|
body.page-admin .drive-estimator-grid{ grid-template-columns:1fr; }
|
||||||
|
body.page-admin .drive-estimator-value{ font-size:28px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── ACTION BUTTONS ─────────────────────────────── */
|
/* ── ACTION BUTTONS ─────────────────────────────── */
|
||||||
@@ -1032,6 +1036,94 @@ body.page-admin .ctrl-select{
|
|||||||
}
|
}
|
||||||
body.page-admin .ctrl-select:focus{ border-color:rgba(0,255,145,.5); }
|
body.page-admin .ctrl-select:focus{ border-color:rgba(0,255,145,.5); }
|
||||||
body.page-admin .ctrl-status{ font-size:9px; letter-spacing:1.2px; color:rgba(225,255,242,.92); white-space:nowrap; min-width:64px; text-align:right; }
|
body.page-admin .ctrl-status{ font-size:9px; letter-spacing:1.2px; color:rgba(225,255,242,.92); white-space:nowrap; min-width:64px; text-align:right; }
|
||||||
|
body.page-admin .drive-estimator-copy{
|
||||||
|
font-size:10px; color:rgba(205,255,235,.86); letter-spacing:.4px; line-height:1.6;
|
||||||
|
padding:8px 12px; border-bottom:1px solid rgba(0,255,145,.05);
|
||||||
|
}
|
||||||
|
body.page-admin .drive-estimator-toolbar{
|
||||||
|
display:flex; align-items:end; gap:10px; padding:12px; border-bottom:1px solid rgba(0,255,145,.05);
|
||||||
|
}
|
||||||
|
body.page-admin .drive-estimator-input{
|
||||||
|
display:grid; grid-template-columns:auto 88px auto; align-items:center; gap:8px; flex:1;
|
||||||
|
}
|
||||||
|
body.page-admin .drive-estimator-input span{
|
||||||
|
font-size:9px; letter-spacing:1.6px; color:rgba(210,255,235,.9); text-transform:uppercase;
|
||||||
|
}
|
||||||
|
body.page-admin .drive-estimator-input input{
|
||||||
|
font-family:var(--mono); font-size:12px; letter-spacing:1px; color:rgba(245,255,250,.96);
|
||||||
|
background:#050A14; border:1px solid rgba(120,255,205,.36); padding:8px 10px; outline:none;
|
||||||
|
}
|
||||||
|
body.page-admin .drive-estimator-input input:focus{ border-color:rgba(0,255,145,.5); }
|
||||||
|
body.page-admin .drive-estimator-input em{
|
||||||
|
font-style:normal; font-size:10px; color:rgba(225,255,242,.92); letter-spacing:1px;
|
||||||
|
}
|
||||||
|
body.page-admin .drive-estimator-grid{
|
||||||
|
display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); gap:10px; padding:12px;
|
||||||
|
}
|
||||||
|
body.page-admin .drive-estimator-card{
|
||||||
|
border:1px solid rgba(0,255,145,.12);
|
||||||
|
background:linear-gradient(180deg, rgba(0,255,145,.05), rgba(0,255,145,.015));
|
||||||
|
padding:14px 12px;
|
||||||
|
box-shadow:inset 0 0 24px rgba(0,255,145,.018);
|
||||||
|
}
|
||||||
|
body.page-admin .drive-estimator-label{
|
||||||
|
font-size:9px; letter-spacing:2px; color:rgba(205,255,235,.86); text-transform:uppercase; margin-bottom:10px;
|
||||||
|
}
|
||||||
|
body.page-admin .drive-estimator-value{
|
||||||
|
font-size:34px; font-weight:700; line-height:1; letter-spacing:2px; color:#fff;
|
||||||
|
text-shadow:0 0 18px rgba(120,255,205,.16);
|
||||||
|
}
|
||||||
|
body.page-admin .drive-estimator-unit{
|
||||||
|
margin-top:8px; font-size:11px; letter-spacing:2px; color:rgba(120,255,205,.9); text-transform:uppercase;
|
||||||
|
}
|
||||||
|
body.page-admin .drive-calibration-toolbar{
|
||||||
|
display:flex; align-items:center; gap:8px; padding:10px 12px; border-top:1px solid rgba(0,255,145,.05); border-bottom:1px solid rgba(0,255,145,.05);
|
||||||
|
}
|
||||||
|
body.page-admin .drive-calibration-info{
|
||||||
|
margin-right:auto; min-width:auto; text-align:left;
|
||||||
|
}
|
||||||
|
body.page-admin .drive-calibration-grid{
|
||||||
|
display:grid; grid-template-columns:repeat(auto-fit, minmax(150px, 1fr)); gap:8px; padding:12px;
|
||||||
|
}
|
||||||
|
body.page-admin .drive-calibration-card{
|
||||||
|
border:1px solid rgba(0,255,145,.12);
|
||||||
|
background:rgba(0,255,145,.025);
|
||||||
|
padding:10px;
|
||||||
|
}
|
||||||
|
body.page-admin .drive-calibration-card.is_active{
|
||||||
|
border-color:rgba(180,255,225,.72);
|
||||||
|
box-shadow:0 0 16px rgba(120,255,205,.14), inset 0 0 16px rgba(180,255,225,.05);
|
||||||
|
}
|
||||||
|
body.page-admin .drive-calibration-card.is_dirty{
|
||||||
|
border-color:rgba(255,140,0,.4);
|
||||||
|
background:rgba(255,140,0,.03);
|
||||||
|
}
|
||||||
|
body.page-admin .drive-calibration-percent{
|
||||||
|
font-size:11px; font-weight:700; letter-spacing:1.6px; color:#fff; margin-bottom:10px;
|
||||||
|
}
|
||||||
|
body.page-admin .drive-calibration-value{
|
||||||
|
font-size:22px; font-weight:700; letter-spacing:1.5px; color:#fff; margin-bottom:6px;
|
||||||
|
}
|
||||||
|
body.page-admin .drive-calibration-meta{
|
||||||
|
font-size:10px; letter-spacing:1px; color:rgba(205,255,235,.86); margin-bottom:8px;
|
||||||
|
}
|
||||||
|
body.page-admin .drive-calibration-status{
|
||||||
|
font-size:9px; letter-spacing:1.4px; color:rgba(120,255,205,.9); text-transform:uppercase; margin-bottom:10px;
|
||||||
|
}
|
||||||
|
body.page-admin .drive-calibration-actions{
|
||||||
|
display:grid; grid-template-columns:1fr; gap:6px;
|
||||||
|
}
|
||||||
|
body.page-admin .drive-calibration-button{
|
||||||
|
width:100%;
|
||||||
|
}
|
||||||
|
@media (max-width:479px){
|
||||||
|
body.page-admin .drive-estimator-toolbar{ flex-direction:column; align-items:stretch; }
|
||||||
|
body.page-admin .drive-estimator-input{ grid-template-columns:1fr; }
|
||||||
|
body.page-admin .drive-estimator-grid{ grid-template-columns:1fr; }
|
||||||
|
body.page-admin .drive-estimator-value{ font-size:28px; }
|
||||||
|
body.page-admin .drive-calibration-toolbar{ flex-direction:column; align-items:stretch; }
|
||||||
|
body.page-admin .drive-calibration-grid{ grid-template-columns:1fr; }
|
||||||
|
}
|
||||||
|
|
||||||
/* ── AUTH OVERLAY ───────────────────────────────── */
|
/* ── AUTH OVERLAY ───────────────────────────────── */
|
||||||
body.page-admin .auth-overlay{
|
body.page-admin .auth-overlay{
|
||||||
|
|||||||
@@ -71,6 +71,18 @@
|
|||||||
HOST: <span id="cam-host">--</span><br>
|
HOST: <span id="cam-host">--</span><br>
|
||||||
STREAM: 8083
|
STREAM: 8083
|
||||||
</div>
|
</div>
|
||||||
|
<div class="c-bm">
|
||||||
|
<div class="c-bm-box">
|
||||||
|
<span class="c-bm-lbl">V</span>
|
||||||
|
<strong id="overlay-speed">0,0</strong>
|
||||||
|
<span class="c-bm-unit">cm/s</span>
|
||||||
|
</div>
|
||||||
|
<div class="c-bm-box">
|
||||||
|
<span class="c-bm-lbl">S</span>
|
||||||
|
<strong id="overlay-distance">0,00</strong>
|
||||||
|
<span class="c-bm-unit">m</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="c-xhair">
|
<div class="c-xhair">
|
||||||
<svg viewBox="-90 -90 180 180" xmlns="http://www.w3.org/2000/svg" width="180" height="180">
|
<svg viewBox="-90 -90 180 180" xmlns="http://www.w3.org/2000/svg" width="180" height="180">
|
||||||
<!-- äußerer rotierender Strichelring -->
|
<!-- äußerer rotierender Strichelring -->
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ PORT = 8082
|
|||||||
EXOMY_CONTAINER = 'exomy_autostart'
|
EXOMY_CONTAINER = 'exomy_autostart'
|
||||||
DELAY_FILE = '/tmp/exomy_delay.txt'
|
DELAY_FILE = '/tmp/exomy_delay.txt'
|
||||||
SPEED_LIMIT_FILE = '/tmp/exomy_speed_limit.txt'
|
SPEED_LIMIT_FILE = '/tmp/exomy_speed_limit.txt'
|
||||||
|
DRIVE_ESTIMATOR_CALIBRATION_FILE = os.path.join(
|
||||||
|
os.path.dirname(__file__), '..', 'config', 'drive_estimator_calibration.json'
|
||||||
|
)
|
||||||
MOTOR_TEST_LOCK = threading.Lock()
|
MOTOR_TEST_LOCK = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
@@ -492,6 +495,136 @@ def get_drive_neutral_status():
|
|||||||
return 500, {'status': 'Fahr-Neutralwerte konnten nicht geladen werden'}
|
return 500, {'status': 'Fahr-Neutralwerte konnten nicht geladen werden'}
|
||||||
|
|
||||||
|
|
||||||
|
def get_drive_estimator_config():
|
||||||
|
from admin_motor_test import WHEELS, _load_config
|
||||||
|
|
||||||
|
try:
|
||||||
|
config_dir = os.path.join(os.path.dirname(__file__), '..', 'config')
|
||||||
|
config_candidates = [
|
||||||
|
os.path.join(config_dir, 'exomy.yaml'),
|
||||||
|
os.path.join(config_dir, 'exomy.remote.yaml'),
|
||||||
|
os.path.join(config_dir, 'exomy.yaml.template'),
|
||||||
|
]
|
||||||
|
config_path = next((path for path in config_candidates if os.path.exists(path)), None)
|
||||||
|
if config_path is None:
|
||||||
|
raise FileNotFoundError('Konfigurationsdatei nicht gefunden')
|
||||||
|
|
||||||
|
config = _load_config(config_path)
|
||||||
|
neutral_values = [
|
||||||
|
int(config[f'drive_pwm_neutral_{wheel}'])
|
||||||
|
for wheel in WHEELS
|
||||||
|
]
|
||||||
|
average_neutral = sum(neutral_values) / len(neutral_values)
|
||||||
|
return 200, {
|
||||||
|
'status': 'Bereit',
|
||||||
|
'estimator': {
|
||||||
|
'config_path': os.path.abspath(config_path),
|
||||||
|
'drive_pwm_neutral_average': average_neutral,
|
||||||
|
'drive_pwm_neutral_values': neutral_values,
|
||||||
|
'drive_pwm_range': int(config['drive_pwm_range']),
|
||||||
|
'pwm_frequency_hz': 50,
|
||||||
|
'servo_max_rpm': 50.0,
|
||||||
|
'servo_full_scale_pulse_delta_ms': 0.2,
|
||||||
|
'wheel_diameter_m_default': 0.1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
except FileNotFoundError:
|
||||||
|
return 500, {'status': 'Motor-Konfiguration fehlt'}
|
||||||
|
except Exception:
|
||||||
|
return 500, {'status': 'Estimator-Konfiguration konnte nicht geladen werden'}
|
||||||
|
|
||||||
|
|
||||||
|
def get_drive_estimator_calibration():
|
||||||
|
default_points = [0, 20, 30, 40, 50, 60, 80, 100]
|
||||||
|
data = {
|
||||||
|
'points': [
|
||||||
|
{'percent': point, 'speed_mps': 0.0}
|
||||||
|
for point in default_points
|
||||||
|
],
|
||||||
|
'updated_at': None,
|
||||||
|
'source': 'default',
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(DRIVE_ESTIMATOR_CALIBRATION_FILE, 'r', encoding='utf-8') as handle:
|
||||||
|
payload = json.load(handle)
|
||||||
|
points = payload.get('points')
|
||||||
|
if isinstance(points, list):
|
||||||
|
cleaned_points = []
|
||||||
|
for item in points:
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
continue
|
||||||
|
percent = int(item.get('percent', 0))
|
||||||
|
speed_mps = float(item.get('speed_mps', 0.0))
|
||||||
|
cleaned_points.append({
|
||||||
|
'percent': max(0, min(100, percent)),
|
||||||
|
'speed_mps': max(0.0, speed_mps),
|
||||||
|
})
|
||||||
|
if cleaned_points:
|
||||||
|
cleaned_points.sort(key=lambda item: item['percent'])
|
||||||
|
data = {
|
||||||
|
'points': cleaned_points,
|
||||||
|
'updated_at': payload.get('updated_at'),
|
||||||
|
'source': 'saved',
|
||||||
|
}
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
except (OSError, ValueError, TypeError, json.JSONDecodeError):
|
||||||
|
return 500, {'status': 'Kalibrierung konnte nicht geladen werden'}
|
||||||
|
|
||||||
|
return 200, {
|
||||||
|
'status': 'Bereit',
|
||||||
|
'calibration': data,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def save_drive_estimator_calibration(points):
|
||||||
|
if not isinstance(points, list) or not points:
|
||||||
|
return 400, {'status': 'Kalibrierpunkte fehlen'}
|
||||||
|
|
||||||
|
cleaned_points = []
|
||||||
|
seen_percents = set()
|
||||||
|
for item in points:
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
return 400, {'status': 'Kalibrierpunkte sind ungültig'}
|
||||||
|
try:
|
||||||
|
percent = int(item.get('percent', 0))
|
||||||
|
speed_mps = float(item.get('speed_mps', 0.0))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return 400, {'status': 'Kalibrierwerte sind ungültig'}
|
||||||
|
percent = max(0, min(100, percent))
|
||||||
|
speed_mps = max(0.0, speed_mps)
|
||||||
|
if percent in seen_percents:
|
||||||
|
return 400, {'status': 'Jede Stufe darf nur einmal vorkommen'}
|
||||||
|
seen_percents.add(percent)
|
||||||
|
cleaned_points.append({
|
||||||
|
'percent': percent,
|
||||||
|
'speed_mps': speed_mps,
|
||||||
|
})
|
||||||
|
|
||||||
|
cleaned_points.sort(key=lambda item: item['percent'])
|
||||||
|
os.makedirs(os.path.dirname(DRIVE_ESTIMATOR_CALIBRATION_FILE), exist_ok=True)
|
||||||
|
payload = {
|
||||||
|
'updated_at': time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
'points': cleaned_points,
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
with open(DRIVE_ESTIMATOR_CALIBRATION_FILE, 'w', encoding='utf-8') as handle:
|
||||||
|
json.dump(payload, handle, ensure_ascii=False, indent=2)
|
||||||
|
handle.write('\n')
|
||||||
|
except OSError:
|
||||||
|
return 500, {'status': 'Kalibrierung konnte nicht gespeichert werden'}
|
||||||
|
|
||||||
|
return 200, {
|
||||||
|
'status': 'Kalibrierung gespeichert',
|
||||||
|
'calibration': {
|
||||||
|
'points': cleaned_points,
|
||||||
|
'updated_at': payload['updated_at'],
|
||||||
|
'source': 'saved',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def preview_drive_neutral_action(values):
|
def preview_drive_neutral_action(values):
|
||||||
from admin_motor_test import preview_drive_neutral_value
|
from admin_motor_test import preview_drive_neutral_value
|
||||||
|
|
||||||
@@ -587,6 +720,14 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
status_code, response = get_drive_neutral_status()
|
status_code, response = get_drive_neutral_status()
|
||||||
self.write_json(status_code, response)
|
self.write_json(status_code, response)
|
||||||
return
|
return
|
||||||
|
if self.path == '/api/drive-estimator/config':
|
||||||
|
status_code, response = get_drive_estimator_config()
|
||||||
|
self.write_json(status_code, response)
|
||||||
|
return
|
||||||
|
if self.path == '/api/drive-estimator/calibration':
|
||||||
|
status_code, response = get_drive_estimator_calibration()
|
||||||
|
self.write_json(status_code, response)
|
||||||
|
return
|
||||||
self.write_json(404, {'status': 'Unbekannt'})
|
self.write_json(404, {'status': 'Unbekannt'})
|
||||||
|
|
||||||
def do_POST(self):
|
def do_POST(self):
|
||||||
@@ -626,6 +767,21 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
self.write_json(500, {'status': str(exc)})
|
self.write_json(500, {'status': str(exc)})
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if self.path == '/api/drive-estimator/calibration':
|
||||||
|
try:
|
||||||
|
content_length = int(self.headers.get('Content-Length', '0'))
|
||||||
|
except ValueError:
|
||||||
|
content_length = 0
|
||||||
|
raw_body = self.rfile.read(content_length) if content_length > 0 else b'{}'
|
||||||
|
try:
|
||||||
|
payload = json.loads(raw_body.decode('utf-8'))
|
||||||
|
except (UnicodeDecodeError, json.JSONDecodeError):
|
||||||
|
self.write_json(400, {'status': 'Ungültige Anfrage'})
|
||||||
|
return
|
||||||
|
status_code, response = save_drive_estimator_calibration(payload.get('points'))
|
||||||
|
self.write_json(status_code, response)
|
||||||
|
return
|
||||||
|
|
||||||
if self.path == '/api/motor-test/drive-neutral/preview':
|
if self.path == '/api/motor-test/drive-neutral/preview':
|
||||||
try:
|
try:
|
||||||
content_length = int(self.headers.get('Content-Length', '0'))
|
content_length = int(self.headers.get('Content-Length', '0'))
|
||||||
|
|||||||
Reference in New Issue
Block a user