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">
|
||||
<title>CUNO // Admin</title>
|
||||
<link rel="stylesheet" href="./app.css?v=20260522104500">
|
||||
<script src="./vendor/roslib.min.js"></script>
|
||||
</head>
|
||||
<body class="page-admin">
|
||||
|
||||
@@ -38,6 +39,7 @@
|
||||
<div class="admin-shell">
|
||||
<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_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>
|
||||
</div>
|
||||
|
||||
@@ -169,6 +171,80 @@
|
||||
</div>
|
||||
</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">
|
||||
<div class="admin-section-grid">
|
||||
<div class="admin-col">
|
||||
@@ -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(
|
||||
'<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) {
|
||||
if (!value || typeof value !== 'string') return '0%';
|
||||
var n = parseFloat(value.replace('°C', '').replace('°C', '').replace(',', '.'));
|
||||
@@ -880,6 +1475,8 @@
|
||||
});
|
||||
if (window.location.hash === '#motor') {
|
||||
switchAdminSection('motor');
|
||||
} else if (window.location.hash === '#drive') {
|
||||
switchAdminSection('drive');
|
||||
}
|
||||
updateClock();
|
||||
window.setInterval(updateClock, 1000);
|
||||
@@ -887,10 +1484,15 @@
|
||||
fetchDelay();
|
||||
fetchSpeedLimit();
|
||||
fetchMotorTestStatus();
|
||||
fetchDriveEstimatorConfig();
|
||||
loadDriveEstimatorCalibration();
|
||||
connectDriveEstimatorRos();
|
||||
renderDriveEstimator();
|
||||
window.setInterval(fetchStatus, 10000);
|
||||
window.setInterval(fetchDelay, 5000);
|
||||
window.setInterval(fetchSpeedLimit, 5000);
|
||||
window.setInterval(fetchMotorTestStatus, 4000);
|
||||
window.setInterval(renderDriveEstimator, 250);
|
||||
});
|
||||
|
||||
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 .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 .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 ─────────────────────────────── */
|
||||
@@ -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-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 ───────────────────────────────── */
|
||||
body.page-admin .auth-overlay{
|
||||
|
||||
@@ -71,6 +71,18 @@
|
||||
HOST: <span id="cam-host">--</span><br>
|
||||
STREAM: 8083
|
||||
</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">
|
||||
<svg viewBox="-90 -90 180 180" xmlns="http://www.w3.org/2000/svg" width="180" height="180">
|
||||
<!-- äußerer rotierender Strichelring -->
|
||||
|
||||
@@ -15,6 +15,9 @@ PORT = 8082
|
||||
EXOMY_CONTAINER = 'exomy_autostart'
|
||||
DELAY_FILE = '/tmp/exomy_delay.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()
|
||||
|
||||
|
||||
@@ -492,6 +495,136 @@ def get_drive_neutral_status():
|
||||
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):
|
||||
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()
|
||||
self.write_json(status_code, response)
|
||||
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'})
|
||||
|
||||
def do_POST(self):
|
||||
@@ -626,6 +767,21 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
self.write_json(500, {'status': str(exc)})
|
||||
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':
|
||||
try:
|
||||
content_length = int(self.headers.get('Content-Length', '0'))
|
||||
|
||||
Reference in New Issue
Block a user