diff --git a/.gitignore b/.gitignore index 9654f6c..672266e 100755 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,8 @@ yarn-debug.log* yarn-error.log* pnpm-debug.log* +public/snapshots + # Builds & Coverage coverage/ dist/ diff --git a/public/calculateActions.js b/public/calculateActions.js index 79c1e69..4b2c63a 100755 --- a/public/calculateActions.js +++ b/public/calculateActions.js @@ -374,6 +374,7 @@ async function calculate() { const row242 = getRow(242, 3); const row200 = getRow(200, 3); const row204 = getRow(204, 3); + const row222 = getRow(222, 3); const angleYCandidates = []; @@ -516,10 +517,14 @@ async function calculate() { } // Ellbow / Zusatzschätzung - if (row226 || row229 || row198 || row243 || row242 || row200 || row204) { + if (row222 || row226 || row229 || row198 || row243 || row242 || row200 || row204) { let x226 = 0; let xCount = 0; + if(row222){ + x226 += row222.x_mm * 5; + xCount += 5; + } if (row226) { x226 += row226.x_mm * 5; xCount += 5; diff --git a/public/index.html b/public/index.html index c89c1d8..b7d76d1 100755 --- a/public/index.html +++ b/public/index.html @@ -4,132 +4,85 @@ appRobotHoming - - + -
-

appRobotHoming

-
Status:
-
-
-
- - - - - - + + - - -
+
-
- - -
+ +
+

Aktionen

-
- - -
+
+ + + + + -
- -
-
- - -
-
- -
-
+ + + + +
-
+
-
- + +
+

Ausgabe

+ +
+ + +
+

Analysis & Reasoning

+ +
+ + +
+

Result – Raw JSON

+ +
+ + +
+
+ + +
+

Result – Tree View

+ + +
+ +
+
+
+ + +
+

Neuester Snapshot

-
-
+ - + + \ No newline at end of file diff --git a/public/o/calculateActions.js b/public/o/calculateActions.js index dfd38eb..4b2c63a 100755 --- a/public/o/calculateActions.js +++ b/public/o/calculateActions.js @@ -1,262 +1,655 @@ // calculateActions.js -// Funktionen zum Berechnen von Vorschlägen basierend auf den neuesten CSV-Daten +// Berechnung + nachvollziehbare Result-Struktur + Live-Logs in analysis-log -const analysisLogEl = document.getElementById('analysis-log'); +function getAnalysisLogEl() { + if (typeof document === "undefined") return null; + return document.getElementById("analysis-log"); +} function appendToAnalysis(line) { + const el = getAnalysisLogEl(); + if (!el) return; + const now = new Date().toISOString(); - analysisLogEl.value += `[${now}] ${line}\n`; - analysisLogEl.scrollTop = analysisLogEl.scrollHeight; + el.value += `[${now}] ${line}\n`; + el.scrollTop = el.scrollHeight; +} + +function createAnalysisResult(meta = {}) { + return { + meta: { + timestamp: new Date().toISOString(), + ...meta + }, + inputs: { + source: null, + headers: [], + rowCount: 0 + }, + observations: [], + calculations: [], + features: {}, + commands: [], + logs: [], + errors: [], + summary: { + observationCount: 0, + calculationCount: 0, + featureCount: 0, + commandCount: 0 + }, + status: "ok" + }; +} + +function rowSnapshot(row) { + return { + id: row?.id, + seen_by: row?.seen_by, + x_mm: row?.x_mm, + y_mm: row?.y_mm, + z_mm: row?.z_mm, + roll_deg: row?.roll_deg, + pitch_deg: row?.pitch_deg + }; +} + +function addLog(result, message, level = "info") { + const entry = { + timestamp: new Date().toISOString(), + level, + message + }; + result.logs.push(entry); + appendToAnalysis(message); +} + +function addError(result, message, error = null) { + result.status = "error"; + result.errors.push({ + timestamp: new Date().toISOString(), + message, + details: error ? String(error) : null + }); + addLog(result, `Fehler: ${message}`, "error"); +} + +function addObservation(result, key, row, confidence = 1.0, notes = []) { + result.observations.push({ + key, + source: { + rowId: row?.id, + seenBy: row?.seen_by + }, + values: rowSnapshot(row), + confidence, + notes + }); +} + +function addCalculation(result, calc) { + result.calculations.push({ + timestamp: new Date().toISOString(), + ...calc + }); +} + +function addFeature(result, key, feature) { + result.features[key] = feature; +} + +function addCommand(result, id, command, basedOn = [], confidence = 1.0, format = "gcode") { + result.commands.push({ + id, + command, + basedOn, + confidence, + format + }); +} + +function weightedAverage(candidates) { + const weightSum = candidates.reduce((sum, c) => sum + c.weight, 0); + if (!weightSum) return null; + + const valueRad = candidates.reduce((sum, c) => sum + (c.valueRad * c.weight), 0) / weightSum; + const spreadRad = candidates.length > 1 + ? Math.sqrt( + candidates.reduce( + (sum, c) => sum + c.weight * Math.pow(c.valueRad - valueRad, 2), + 0 + ) / weightSum + ) + : 0; + + const confidence = Math.max( + 0.2, + Math.min( + 0.98, + 0.55 + + Math.min(candidates.length * 0.1, 0.25) - + Math.min(spreadRad / 1.0, 0.2) + ) + ); + + return { valueRad, weightSum, spreadRad, confidence }; } async function fetchCSV() { - console.log('Lade und verarbeite CSV-Daten...'); - const res = await fetch('/api/latest-snapshot'); - if (!res.ok) throw new Error('Fehler beim Laden des Snapshots'); + const res = await fetch("/api/latest-snapshot"); + if (!res.ok) throw new Error("Fehler beim Laden des Snapshots"); - let data; - if (res.headers.get('content-type')?.includes('application/json')) { - data = await res.json(); - } else { - const csvData = await res.text(); - data = { filename: 'latest.csv', mtime: new Date().toISOString(), content: csvData }; - } + let data; + if (res.headers.get("content-type")?.includes("application/json")) { + data = await res.json(); + } else { + const csvData = await res.text(); + data = { + filename: "latest.csv", + mtime: new Date().toISOString(), + content: csvData + }; + } - // CSV parsen - const lines = data.content.trim().split('\n'); - if (lines.length < 2) { - throw new Error('Keine oder unvollständige Daten'); - } + const lines = data.content.trim().split(/\r?\n/).filter(Boolean); + if (lines.length < 2) { + throw new Error("Keine oder unvollständige Daten"); + } - const headers = lines[0].split(',').map(h => h.trim()); - const rows = lines.slice(1).map(line => { - const cells = line.split(','); - let obj = {}; - headers.forEach((h, i) => { - const val = cells[i]?.trim(); - obj[h] = isNaN(val) ? val : parseFloat(val); - }); - return obj; + const headers = lines[0].split(",").map(h => h.trim()); + + const rows = lines.slice(1).map(line => { + const cells = line.split(","); + const obj = {}; + headers.forEach((h, i) => { + const raw = (cells[i] ?? "").trim(); + const numeric = Number(raw); + obj[h] = raw !== "" && Number.isFinite(numeric) ? numeric : raw; }); - appendToAnalysis(`CSV-Daten geladen: ${rows.length} Zeilen, ${headers.length} Spalten.`); - return { data, headers, rows }; + return obj; + }); + + return { data, headers, rows }; } -async function readValues( data, headers, rows ){ - console.log('Geladene Daten:', data); - console.log('Headers:', headers); - console.log('Parsed rows:', rows); +function calculateAngleFromPosition(result, row, axisY, axisZ, deltaYangle0, deltaZangle0 = 0, label = "angle") { + const y = parseFloat(row.y_mm); + const z = parseFloat(row.z_mm); - -} + const dy = -(y - axisY); + const dz = z - axisZ; -function calculateAngleFromPosition(row, axisY, axisZ, deltaYangle0, deltaZangle0 = 0) { - let y = parseFloat(row.y_mm); - let z = parseFloat(row.z_mm); + let angle0Rad = 0; + if (deltaZangle0 !== 0 && deltaYangle0 !== 0) { + angle0Rad = Math.atan(deltaZangle0 / deltaYangle0); + } - let dy = -(y - axisY); - let dz = z - axisZ; + const angleRad = Math.atan2(dz, dy) - angle0Rad; + const angleDeg = angleRad * 180 / Math.PI; - let angle0Rad = 0; - if(deltaZangle0 !== 0){ - angle0Rad = Math.atan(deltaZangle0/deltaYangle0); + const message = `(${label} = ${angleDeg.toFixed(2)}° = ${angleRad.toFixed(4)} rad) aus Position von ID = ${row.id}`; + addLog(result, message); + + addCalculation(result, { + type: "angleFromPosition", + label, + source: { + rowId: row.id, + seenBy: row.seen_by + }, + input: { + axisY, + axisZ, + deltaYangle0, + deltaZangle0, + y_mm: y, + z_mm: z, + dy, + dz, + angle0Rad + }, + output: { + angleRad, + angleDeg } + }); - angleRad = Math.atan(dz/dy) - angle0Rad; - angleDeg = angleRad * (180 / Math.PI); - - appendToAnalysis(`(yMotor = ${angleDeg.toFixed(2)}° = ${angleRad.toFixed(4)} rad ) aus Position von ID = ${row.id}`); - return angleRad; + return angleRad; } -function calculateAngleFromRollColumn(row, roll0 = 0, pitch0 = 0, yaw0 = 0, strMotor = "yMotor") { - let roll = -parseFloat(row.roll_deg) + roll0; - appendToAnalysis(`(${strMotor} = ${roll.toFixed(2)}° = ${(roll * Math.PI / 180).toFixed(4)} rad) aus roll_deg von ID = ${row.id}`); - return roll*Math.PI / 180; +function calculateAngleFromRollColumn(result, row, roll0 = 0, pitch0 = 0, yaw0 = 0, strMotor = "motor") { + const rollDeg = -parseFloat(row.roll_deg) + roll0; + const rollRad = rollDeg * Math.PI / 180; + + const message = `(${strMotor} = ${rollDeg.toFixed(2)}° = ${rollRad.toFixed(4)} rad) aus roll_deg von ID = ${row.id}`; + addLog(result, message); + + addCalculation(result, { + type: "angleFromRollColumn", + label: strMotor, + source: { + rowId: row.id, + seenBy: row.seen_by + }, + input: { + roll0, + pitch0, + yaw0, + roll_deg: row.roll_deg + }, + output: { + angleRad: rollRad, + angleDeg: rollDeg + } + }); + + return rollRad; } -function calculateAngleFromRelativePosition(row1, row2, strMotor = "zMotor") { - let y1 = parseFloat(row1.y_mm); - let z1 = parseFloat(row1.z_mm); - let y2 = parseFloat(row2.y_mm); - let z2 = parseFloat(row2.z_mm); - let dy = y1 - y2; - let dz = z1 - z2; - let angleRad = -Math.atan(dz/dy); - let angleDeg = angleRad * (180 / Math.PI); - appendToAnalysis(`(${strMotor} = ${angleDeg.toFixed(2)}° = ${angleRad.toFixed(4)} rad) aus relativer Position von ID = ${row1.id} und ID = ${row2.id}`); - return angleRad; +function calculateAngleFromRelativePosition(result, row1, row2, strMotor = "motor") { + const y1 = parseFloat(row1.y_mm); + const z1 = parseFloat(row1.z_mm); + const y2 = parseFloat(row2.y_mm); + const z2 = parseFloat(row2.z_mm); + + const dy = y1 - y2; + const dz = z1 - z2; + + const angleRad = -Math.atan2(dz, dy); + const angleDeg = angleRad * 180 / Math.PI; + + const message = `(${strMotor} = ${angleDeg.toFixed(2)}° = ${angleRad.toFixed(4)} rad) aus relativer Position von ID = ${row1.id} und ID = ${row2.id}`; + addLog(result, message); + + addCalculation(result, { + type: "angleFromRelativePosition", + label: strMotor, + source: { + rowIds: [row1.id, row2.id], + seenBy: [row1.seen_by, row2.seen_by] + }, + input: { + y1_mm: y1, + z1_mm: z1, + y2_mm: y2, + z2_mm: z2, + dy, + dz + }, + output: { + angleRad, + angleDeg + } + }); + + return angleRad; +} + +function buildFeatureFromCandidates(result, key, title, method, candidates) { + const summary = weightedAverage(candidates); + if (!summary) return null; + + const feature = { + title, + method, + valueRad: summary.valueRad, + valueDeg: summary.valueRad * 180 / Math.PI, + confidence: summary.confidence, + spreadRad: summary.spreadRad, + weightSum: summary.weightSum, + evidence: candidates.map(c => c.source), + parts: candidates + }; + + addFeature(result, key, feature); + addCalculation(result, { + type: "featureSummary", + key, + title, + method, + input: candidates, + output: { + valueRad: feature.valueRad, + valueDeg: feature.valueDeg, + confidence: feature.confidence, + spreadRad: feature.spreadRad, + weightSum: feature.weightSum + } + }); + + return feature; } async function calculate() { - - let shoulderAxisY = 115; - let shoulderAxisZ = 61; + const result = createAnalysisResult({ + sourceScript: "calculateActions.js" + }); - let rows = null; - let headers = null; + try { + addLog(result, "Starte Berechnung..."); - try { - appendToAnalysis('Starte Berechnung...'); - const result = await fetchCSV(); - rows = result.rows; - headers = result.headers; - const data = result.data; - await readValues( data, headers, rows ); - } catch (err) { - appendToAnalysis('Fehler in calculate: ' + err.message); - } + const { data, headers, rows } = await fetchCSV(); - - // Oberarm: - var angleY = 0; - var angleZ = 0; - var angleYcount = 0; - var angleZcount = 0; - - // 243 damit 35mm weiter außen (250+35) und 0mm höher als Schulterachse - const row243 = rows.find(r => r.id == 243 && r.seen_by == 3) - if(row243){ - angleY += await calculateAngleFromPosition(row243, shoulderAxisY, shoulderAxisZ, 250+35, 0); - angleY += await calculateAngleFromRollColumn(row243, 90, 0, 0); - angleYcount+=2; - } - const row229 = rows.find(r => r.id == 229 && r.seen_by == 3) - if(row229){ - angleY += await calculateAngleFromPosition(row229, shoulderAxisY, shoulderAxisZ, 250, 35); - //angleY +=calculateAngleFromRollColumn(row229, 0, 0, 0); // Roll ist extrem unzuverlässig - angleYcount+=1; - } - console.log(angleY, angleYcount); - const row198 = rows.find(r => r.id == 198 && r.seen_by == 3) - if(row198){ - angleY += await calculateAngleFromPosition(row198, shoulderAxisY, shoulderAxisZ, 165, 35); - //angleY +=calculateAngleFromRollColumn(row198, 180, 0, 0); // ist ungenau - angleYcount++; - } - - if(row198 && row229){ - angleY += 3*(calculateAngleFromRelativePosition(row198, row229, "yMotor")); - angleYcount += 3; - } - - const row197 = rows.find(r => r.id == 197) - if(row197){ - var angleDeg = 90 - row197.pitch_deg; - var angleRad = angleDeg * Math.PI / 180; - appendToAnalysis(`(yMotor = ${angleDeg.toFixed(2)}° = ${angleRad.toFixed(4)} rad ) aus Pitch von ${row197.id}`); - } - - // Unterarm: - - // 218 und 219, wenn die sichtbar sind, ist auch die Schulter eindeutig definiert - const row218 = rows.find(r => r.id == 218 && r.seen_by == 3) - const row219 = rows.find(r => r.id == 219 && r.seen_by == 3) - if(row218 && row219){ - const lowerArmAngle = calculateAngleFromRelativePosition(row218, row219, "zMotor"); - angleZ += lowerArmAngle; - angleZcount++; - } - console.log("z", angleZ); - - const row226 = rows.find(r => r.id == 226 && r.seen_by == 3) - if(row226){ - angleZ += calculateAngleFromRollColumn(row226, 0, 0, 0, "zMotor"); - angleZcount++; - } - - console.log("z", angleZ); - - if(angleYcount > 0 && angleYcount > 0){ - strActionOptionA = `G92 y${(angleY*180/(angleYcount*Math.PI)).toFixed(1)} z${(angleZ*180/(Math.PI*angleZcount)).toFixed(1)} (Set Coord. Angles in deg)`; - strActionOptionB = `M92 y${(angleY/(angleYcount)).toFixed(3)} z${(angleZ/(angleZcount)).toFixed(3)} (Set Coord. Angles in rad)`; - - appendToAnalysis(`Suggestion: ${strActionOptionA}`); - appendToAnalysis(`Suggestion: ${strActionOptionB}`); - - // ToDo: Change - } - - // Ellbow - const row242 = rows.find(r => r.id == 242 && r.seen_by == 3) - const row200 = rows.find(r => r.id == 200 && r.seen_by == 3) - const row204 = rows.find(r => r.id == 204 && r.seen_by == 3) - // Ellenbogen-Rotation aus x-Position 218 219 - // dazu brauche ich genaue x-Position von 226 - if(row226 || row229 || row198 || row243 || row242 || row200 || row200){ - var x226 = 0; - var xCount = 0; - if(row226){ - x226 += row226.x_mm * 5; - xCount += 5; - } - if(row229){ - x226 += row229.x_mm + 90; - xCount += 1; - } - if(row198){ - x226 += row198.x_mm + 90; - xCount += 1; - } - if(row243){ - x226 += row243.x_mm + 90; - xCount += 1; - } - if(row242){ - x226 += row242.x_mm + 90; - xCount += 1; - } - if(row200){ - x226 += row200.x_mm + 154; - xCount += 1; - } - if(row204){ - x226 += row204.x_mm + 160; - xCount += 1; - } - x226 = x226 / xCount; - appendToAnalysis(`Ellebogen x226=${x226}`) - - // Wenn 218 und/oder 219 sicher gesehen wird, kann ich daraus x-pos bestimmen - if(row218 || row219){ - var x219 = 0; - xCount = 0; - if(row218){ - x219 += row218.x_mm; - xCount += 1; - } - if(row219){ - x219 += row219.x_mm; - xCount += 1; - } - x219 = x219 / xCount; - appendToAnalysis(`Ellebogen x219=${x219}`) - var xDelta = x219 - x226; - if(Math.abs(xDelta) < 35){ - - appendToAnalysis(`Ellebogen xDelta / 35=${xDelta / 35}`) - var angleRad = Math.asin(xDelta / 35) - var angleDeg = 90 -angleRad*180/Math.PI; - appendToAnalysis(`(xEllbow = ${angleDeg.toFixed(2)}° = ${angleRad.toFixed(4)} rad ) aus x von 218 bzw. 219`); - - } - } - } -} - - - - - -if (typeof module !== 'undefined') { - module.exports = { - calculateAngleFromPosition, - calculateAngleFromRollColumn, - calculateAngleFromRelativePosition, - calculate + result.inputs = { + source: { + filename: data.filename, + mtime: data.mtime + }, + headers, + rowCount: rows.length }; + + addLog(result, `CSV-Daten geladen: ${rows.length} Zeilen, ${headers.length} Spalten.`); + + const getRow = (id, seenBy = null) => { + const row = rows.find(r => r.id == id && (seenBy === null || r.seen_by == seenBy)); + if (row) { + addObservation( + result, + `row-${id}${seenBy !== null ? `-seenBy-${seenBy}` : ""}`, + row, + seenBy === 3 ? 0.95 : 0.75, + [ + `id=${id}`, + seenBy !== null ? `seen_by=${seenBy}` : "seen_by=any" + ] + ); + } else { + addLog(result, `Zeile nicht gefunden: id=${id}${seenBy !== null ? `, seen_by=${seenBy}` : ""}`, "warn"); + } + return row; + }; + + const shoulderAxisY = 115; + const shoulderAxisZ = 61; + + // Oberarm / shoulder + const row243 = getRow(243, 3); + const row229 = getRow(229, 3); + const row198 = getRow(198, 3); + const row197 = getRow(197); + const row218 = getRow(218, 3); + const row219 = getRow(219, 3); + const row226 = getRow(226, 3); + const row242 = getRow(242, 3); + const row200 = getRow(200, 3); + const row204 = getRow(204, 3); + const row222 = getRow(222, 3); + + const angleYCandidates = []; + + if (row243) { + const a1 = calculateAngleFromPosition(result, row243, shoulderAxisY, shoulderAxisZ, 285, 0, "yMotor"); + const a2 = calculateAngleFromRollColumn(result, row243, 90, 0, 0, "yMotor"); + + angleYCandidates.push({ + source: "row243.position", + valueRad: a1, + weight: 1 + }); + + angleYCandidates.push({ + source: "row243.roll_deg", + valueRad: a2, + weight: 1 + }); + } + + if (row229) { + const a = calculateAngleFromPosition(result, row229, shoulderAxisY, shoulderAxisZ, 250, 35, "yMotor"); + angleYCandidates.push({ + source: "row229.position", + valueRad: a, + weight: 1 + }); + } + + if (row198) { + const a = calculateAngleFromPosition(result, row198, shoulderAxisY, shoulderAxisZ, 165, 35, "yMotor"); + angleYCandidates.push({ + source: "row198.position", + valueRad: a, + weight: 1 + }); + } + + if (row198 && row229) { + const a = calculateAngleFromRelativePosition(result, row198, row229, "yMotor"); + angleYCandidates.push({ + source: "row198-row229.relative", + valueRad: a, + weight: 3 + }); + } + + if (row197) { + const angleDeg = 90 - parseFloat(row197.pitch_deg); + const angleRad = angleDeg * Math.PI / 180; + + addLog(result, `(yMotor = ${angleDeg.toFixed(2)}° = ${angleRad.toFixed(4)} rad ) aus Pitch von ${row197.id}`); + + addCalculation(result, { + type: "angleFromPitch", + label: "yMotor", + source: { + rowId: row197.id, + seenBy: row197.seen_by + }, + input: { + pitch_deg: row197.pitch_deg + }, + output: { + angleRad, + angleDeg + } + }); + + angleYCandidates.push({ + source: "row197.pitch_deg", + valueRad: angleRad, + weight: 1 + }); + } + + if (angleYCandidates.length > 0) { + buildFeatureFromCandidates( + result, + "shoulder.angleY", + "Schulter Y", + "weighted-average", + angleYCandidates + ); + } + + // Unterarm / z + const angleZCandidates = []; + + if (row218 && row219) { + const lowerArmAngle = calculateAngleFromRelativePosition(result, row218, row219, "zMotor"); + angleZCandidates.push({ + source: "row218-row219.relative", + valueRad: lowerArmAngle, + weight: 200 // Hohes gewicht, da ich sicher bin, dass es korrekt ist + }); + } + + if (row226) { + const a = calculateAngleFromRollColumn(result, row226, 0, 0, 0, "zMotor"); + angleZCandidates.push({ + source: "row226.roll_deg", + valueRad: a, + weight: 1 + }); + } + + if (angleZCandidates.length > 0) { + buildFeatureFromCandidates( + result, + "forearm.angleZ", + "Unterarm Z", + "combined", + angleZCandidates + ); + } + + // Commands + const angleY = result.features["shoulder.angleY"]; + const angleZ = result.features["forearm.angleZ"]; + + if (angleY && angleZ) { + const cmdConfidence = Math.min(angleY.confidence, angleZ.confidence); + + addCommand( + result, + "set-coord-angles-deg", + `G92 y${angleY.valueDeg.toFixed(1)} z${angleZ.valueDeg.toFixed(1)} (Set Coord. Angles in deg)`, + ["shoulder.angleY", "forearm.angleZ"], + cmdConfidence + ); + + addCommand( + result, + "set-coord-angles-rad", + `M92 y${angleY.valueRad.toFixed(3)} z${angleZ.valueRad.toFixed(3)} (Set Coord. Angles in rad)`, + ["shoulder.angleY", "forearm.angleZ"], + cmdConfidence + ); + } + + // Ellbow / Zusatzschätzung + if (row222 || row226 || row229 || row198 || row243 || row242 || row200 || row204) { + let x226 = 0; + let xCount = 0; + + if(row222){ + x226 += row222.x_mm * 5; + xCount += 5; + } + if (row226) { + x226 += row226.x_mm * 5; + xCount += 5; + } + if (row229) { + x226 += row229.x_mm + 90; + xCount += 1; + } + if (row198) { + x226 += row198.x_mm + 90; + xCount += 1; + } + if (row243) { + x226 += row243.x_mm + 90; + xCount += 1; + } + if (row242) { + x226 += row242.x_mm + 90; + xCount += 1; + } + if (row200) { + x226 += row200.x_mm + 154; + xCount += 1; + } + if (row204) { + x226 += row204.x_mm + 160; + xCount += 1; + } + + if (xCount > 0) { + x226 = x226 / xCount; + addLog(result, `Ellbogen x226=${x226}`); + + addCalculation(result, { + type: "elbowXEstimate", + source: { + rowIds: [row226, row229, row198, row243, row242, row200, row204].filter(Boolean).map(r => r.id) + }, + output: { + x226 + } + }); + } + + if (row218 || row219) { + let x219 = 0; + let x219Count = 0; + + if (row218) { + x219 += row218.x_mm; + x219Count += 1; + } + if (row219) { + x219 += row219.x_mm; + x219Count += 1; + } + + if (x219Count > 0) { + x219 = x219 / x219Count; + addLog(result, `Ellbogen x219=${x219}`); + + const xDelta = x219 - x226; + + addCalculation(result, { + type: "elbowXComparison", + input: { + x226, + x219, + xDelta + } + }); + + if (Math.abs(xDelta) < 35) { + addLog(result, `Ellbogen xDelta / 35=${xDelta / 35}`); + const angleRad = Math.asin(xDelta / 35); + const angleDeg = 90 - angleRad * 180 / Math.PI; + addLog(result, `(xEllbow = ${angleDeg.toFixed(2)}° = ${angleRad.toFixed(4)} rad ) aus x von 218 bzw. 219`); + + addCalculation(result, { + type: "elbowAngleFromX", + input: { + x226, + x219, + xDelta + }, + output: { + angleRad, + angleDeg + } + }); + } + } + } + } + + result.summary = { + observationCount: result.observations.length, + calculationCount: result.calculations.length, + featureCount: Object.keys(result.features).length, + commandCount: result.commands.length + }; + + addLog( + result, + `Berechnung fertig: ${result.summary.featureCount} Features, ${result.summary.commandCount} Commands.` + ); + + return result; + } catch (err) { + addError(result, err.message, err); + return result; + } } +if (typeof window !== "undefined") { + window.calculate = calculate; + window.createAnalysisResult = createAnalysisResult; +} -// Export für Module, falls benötigt -// export { fetchCSV, calculate }; \ No newline at end of file +if (typeof module !== "undefined") { + module.exports = { + calculate, + createAnalysisResult, + calculateAngleFromPosition, + calculateAngleFromRollColumn, + calculateAngleFromRelativePosition + }; +} \ No newline at end of file diff --git a/public/o/client.js b/public/o/client.js index 6a8e7d7..39fb8da 100755 --- a/public/o/client.js +++ b/public/o/client.js @@ -1,136 +1,135 @@ -(function(){ - const logEl = document.getElementById('log'); - const connEl = document.getElementById('conn'); +// client.js +// UI: Buttons, Anzeige von Result als JSON + Baum, Fallback für Commands - function append(line){ - const now = new Date().toISOString(); - logEl.value += `[${now}] ${line} -`; - logEl.scrollTop = logEl.scrollHeight; +function appendLog(line) { + const el = document.getElementById("log"); + if (!el) return; + + const now = new Date().toISOString(); + el.value += `[${now}] ${line}\n`; + el.scrollTop = el.scrollHeight; +} + +function clearTextarea(id) { + const el = document.getElementById(id); + if (el) el.value = ""; +} + +function clearElement(id) { + const el = document.getElementById(id); + if (el) el.innerHTML = ""; +} + +function formatScalar(value) { + if (value === null) return "null"; + if (value === undefined) return "undefined"; + if (typeof value === "string") return JSON.stringify(value); + if (typeof value === "number" && Number.isFinite(value)) return String(value); + if (typeof value === "boolean") return String(value); + return String(value); +} + +function renderTree(container, value, key = "result", open = true) { + if (!container) return; + + container.innerHTML = ""; + container.appendChild(renderNode(key, value, open)); +} + +function renderNode(key, value, open = false) { + const isObject = value !== null && typeof value === "object"; + + if (!isObject) { + const leaf = document.createElement("div"); + leaf.className = "tree-leaf"; + leaf.textContent = `${key}: ${formatScalar(value)}`; + return leaf; } - async function refreshStatus(){ - try{ - const res = await fetch('/api/status'); - const st = await res.json(); - if (st.connected){ connEl.textContent = 'verbunden'; connEl.className = 'badge ok'; } - else if (st.lastError){ connEl.textContent = 'fehler'; connEl.className = 'badge err'; } - else { connEl.textContent = 'getrennt'; connEl.className = 'badge warn'; } - }catch(e){ connEl.textContent = 'unbekannt'; connEl.className = 'badge'; } - } + const details = document.createElement("details"); + details.open = open; - function processDataShortenPosition(data){ - if(data?.text){ - try{ - let obj = JSON.parse(data.text); - if(obj?.position){ - obj.position.x = parseFloat(obj.position.x.toFixed(3)); - obj.position.y = parseFloat(obj.position.y.toFixed(3)); - obj.position.z = parseFloat(obj.position.z.toFixed(3)); - obj.position.a = parseFloat(obj.position.a.toFixed(3)); - obj.position.b = parseFloat(obj.position.b.toFixed(3)); - obj.position.c = parseFloat(obj.position.c.toFixed(3)); - } - if(obj?.motorCounts){ - obj.motorCounts.x = parseFloat(obj.motorCounts.x.toFixed(3)); - obj.motorCounts.y = parseFloat(obj.motorCounts.y.toFixed(3)); - obj.motorCounts.z = parseFloat(obj.motorCounts.z.toFixed(3)); - obj.motorCounts.a = parseFloat(obj.motorCounts.a.toFixed(3)); - obj.motorCounts.b = parseFloat(obj.motorCounts.b.toFixed(3)); - obj.motorCounts.c = parseFloat(obj.motorCounts.c.toFixed(3)); - if(obj.motorCounts.e !== undefined) obj.motorCounts.e = parseFloat(obj.motorCounts.e.toFixed(3)); - } - return "text: " + JSON.stringify(obj); - }catch(e){ - return "text: " + data.text; - } - } - return ""; - } + const summary = document.createElement("summary"); + summary.textContent = Array.isArray(value) + ? `${key} [${value.length}]` + : key; - function connectSSE(){ - const es = new EventSource('/api/events'); - es.onmessage = (ev)=>{ - try{ - const p = JSON.parse(ev.data); - if (p.level === 'msg' && p.data?.text !== 'Ping') append(`WSS → ${processDataShortenPosition(p.data)}`); - //if (p.level === 'msg') append(`WSS → ${processDataShortenPosition(p.data)}`); - else if (p.level === 'tx') append(`TX → ${JSON.stringify(p.data)}`); - else append(`${p.level?.toUpperCase?.()}: ${p.message}`); - }catch{ append(ev.data); } - }; - es.onerror = ()=>{ - append('SSE Fehler/unterbrochen. Versuche neu zu verbinden…'); - setTimeout(connectSSE, 2000); - }; - } + details.appendChild(summary); - function bindButtons(){ - document.querySelectorAll('button[data-cmd]').forEach(btn =>{ - btn.addEventListener('click', async () =>{ - const cmd = btn.getAttribute('data-cmd'); + const body = document.createElement("div"); + body.style.marginLeft = "16px"; - let payload = null; - const payloadSelector = btn.getAttribute('data-payload'); - if (payloadSelector) { - const field = document.querySelector(payloadSelector); - if (field) payload = field.value; - } - - try{ - const res = await fetch('/api/send', { - method:'POST', headers:{ 'Content-Type':'application/json' }, - body: JSON.stringify({ cmd, payload }) - }); - const data = await res.json(); - if(!res.ok){ append(`FEHLER ${res.status}: ${data.error || 'Unbekannt'}`); } - else { append(`Sende: ${cmd}`); } - }catch(err){ append('FEHLER: ' + (err?.message || err)); } - }); + if (Array.isArray(value)) { + value.forEach((item, idx) => { + body.appendChild(renderNode(String(idx), item, false)); + }); + } else { + Object.entries(value).forEach(([childKey, childVal]) => { + body.appendChild(renderNode(childKey, childVal, false)); }); } - async function loadLatestSnapshot() { - try { - const res = await fetch('/api/latest-snapshot'); - if (!res.ok) throw new Error('Fehler beim Laden des Snapshots'); - let data; - if (res.headers.get('content-type')?.includes('application/json')) { - data = await res.json(); - } else { - const csvData = await res.text(); - // Fallback: filename aus dem Pfad oder unbekannt, mtime jetzt - data = { filename: 'latest.csv', mtime: new Date().toISOString(), content: csvData }; - } - const infoEl = document.getElementById('snapshot-info'); - const tableEl = document.getElementById('snapshot-table'); - - // Info anzeigen - const mtime = new Date(data.mtime).toLocaleString(); - infoEl.textContent = `Datei: ${data.filename} | Erstellt: ${mtime}`; - - // CSV parsen und Tabelle bauen - const lines = data.content.trim().split('\n'); - if (lines.length === 0) { - tableEl.innerHTML = 'Keine Daten'; - return; - } - const headers = lines[0].split(','); - let html = '' + headers.map(h => `${h.trim()}`).join('') + ''; - for (let i = 1; i < lines.length; i++) { - const cells = lines[i].split(','); - html += '' + cells.map(c => `${c.trim()}`).join('') + ''; - } - html += ''; - tableEl.innerHTML = html; - } catch (err) { - document.getElementById('snapshot-info').textContent = 'Fehler: ' + err.message; - document.getElementById('snapshot-table').innerHTML = ''; - } + details.appendChild(body); + return details; +} + +function renderResult(result) { + const jsonEl = document.getElementById("result-json"); + const treeEl = document.getElementById("result-tree"); + + if (jsonEl) { + jsonEl.value = JSON.stringify(result, null, 2); } - bindButtons(); - connectSSE(); - refreshStatus(); - loadLatestSnapshot(); -})(); + renderTree(treeEl, result, "result", true); +} + +async function onCalculateClick() { + clearTextarea("analysis-log"); + clearTextarea("result-json"); + clearElement("result-tree"); + + appendLog("Starte Berechnung..."); + + try { + const result = await window.calculate(); + renderResult(result); + appendLog("Result angezeigt."); + } catch (err) { + appendLog(`Fehler: ${err.message}`); + } +} + +async function onCommandClick(btn) { + const cmd = btn.dataset.cmd; + const payloadSelector = btn.dataset.payload; + const payload = payloadSelector + ? document.querySelector(payloadSelector)?.value ?? "" + : ""; + + if (typeof window.sendCommand === "function") { + try { + await window.sendCommand(cmd, payload); + appendLog(`Command gesendet: ${cmd}${payload ? " " + payload : ""}`); + } catch (err) { + appendLog(`Command-Fehler: ${err.message}`); + } + return; + } + + appendLog(`Command (kein Transport definiert): ${cmd}${payload ? " " + payload : ""}`); +} + +function setupUi() { + const calculateBtn = document.getElementById("btn-calculate"); + if (calculateBtn) { + calculateBtn.addEventListener("click", onCalculateClick); + } + + document.querySelectorAll("button[data-cmd]").forEach(btn => { + if (btn.id === "btn-calculate") return; + btn.addEventListener("click", () => onCommandClick(btn)); + }); +} + +window.addEventListener("DOMContentLoaded", setupUi); \ No newline at end of file diff --git a/public/o/index.html b/public/o/index.html index b46205f..c89c1d8 100755 --- a/public/o/index.html +++ b/public/o/index.html @@ -5,6 +5,70 @@ appRobotHoming +
@@ -19,7 +83,8 @@ - + + -
@@ -36,10 +100,24 @@
- +
+
+ +
+
+ + +
+
+ +
+
+
+
+
@@ -51,7 +129,7 @@ HTTPS + WSS Relay • © - + - + \ No newline at end of file diff --git a/public/o/styles.css b/public/o/styles.css new file mode 100755 index 0000000..96177bf --- /dev/null +++ b/public/o/styles.css @@ -0,0 +1,23 @@ +:root{ --bg:#0f172a; --fg:#e2e8f0; --muted:#94a3b8; --accent:#38bdf8; --ok:#22c55e; --warn:#f59e0b; --err:#ef4444; } +*{ box-sizing:border-box; } +body{ margin:0; font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif; background:var(--bg); color:var(--fg); } +header{ display:flex; justify-content:space-between; align-items:center; padding:16px 24px; border-bottom:1px solid #1f2937; } +h1{ margin:0; font-size:20px; } +.badge{ padding:2px 8px; border-radius:999px; background:#334155; } +.badge.ok{ background: #064e3b; color:#a7f3d0; } +.badge.warn{ background:#3f1b00; color:#fdba74; } +.badge.err{ background:#3f0d0d; color:#fecaca; } +main{ display:grid; grid-template-columns:1fr; gap:16px; padding:16px; max-width:1400px; margin:0 auto; } +.controls{ display:flex; gap:12px; flex-wrap:wrap; } +.controls button{ background:#1e293b; color:var(--fg); border:1px solid #334155; padding:10px 16px; border-radius:8px; cursor:pointer; } +.controls button:hover{ border-color: var(--accent); } +.log{ display:flex; flex-direction:column; gap:8px; } +#log{ width:100%; height:360px; background:#0b1220; color:var(--fg); border:1px solid #1f2937; border-radius:8px; padding:8px; font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size:12px; } +.analysis{ display:flex; flex-direction:column; gap:8px; } +#analysis-log{ width:100%; height:200px; background:#0b1220; color:var(--fg); border:1px solid #1f2937; border-radius:8px; padding:8px; font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size:12px; } +.snapshot{ display:flex; flex-direction:column; gap:8px; } +#snapshot-info{ font-size:14px; color:var(--muted); } +#snapshot-table{ width:100%; border-collapse:collapse; background:#0b1220; color:var(--fg); border:1px solid #1f2937; border-radius:8px; overflow:hidden; } +#snapshot-table th, #snapshot-table td{ padding:4px 8px; border:1px solid #334155; text-align:left; font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size:12px; } +#snapshot-table th{ background:#1e293b; } +footer{ padding:12px 24px; border-top:1px solid #1f2937; color:var(--muted); } diff --git a/public/styles.css b/public/styles.css index 96177bf..4a29131 100755 --- a/public/styles.css +++ b/public/styles.css @@ -1,23 +1,178 @@ -:root{ --bg:#0f172a; --fg:#e2e8f0; --muted:#94a3b8; --accent:#38bdf8; --ok:#22c55e; --warn:#f59e0b; --err:#ef4444; } -*{ box-sizing:border-box; } -body{ margin:0; font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif; background:var(--bg); color:var(--fg); } -header{ display:flex; justify-content:space-between; align-items:center; padding:16px 24px; border-bottom:1px solid #1f2937; } -h1{ margin:0; font-size:20px; } -.badge{ padding:2px 8px; border-radius:999px; background:#334155; } -.badge.ok{ background: #064e3b; color:#a7f3d0; } -.badge.warn{ background:#3f1b00; color:#fdba74; } -.badge.err{ background:#3f0d0d; color:#fecaca; } -main{ display:grid; grid-template-columns:1fr; gap:16px; padding:16px; max-width:1400px; margin:0 auto; } -.controls{ display:flex; gap:12px; flex-wrap:wrap; } -.controls button{ background:#1e293b; color:var(--fg); border:1px solid #334155; padding:10px 16px; border-radius:8px; cursor:pointer; } -.controls button:hover{ border-color: var(--accent); } -.log{ display:flex; flex-direction:column; gap:8px; } -#log{ width:100%; height:360px; background:#0b1220; color:var(--fg); border:1px solid #1f2937; border-radius:8px; padding:8px; font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size:12px; } -.analysis{ display:flex; flex-direction:column; gap:8px; } -#analysis-log{ width:100%; height:200px; background:#0b1220; color:var(--fg); border:1px solid #1f2937; border-radius:8px; padding:8px; font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size:12px; } -.snapshot{ display:flex; flex-direction:column; gap:8px; } -#snapshot-info{ font-size:14px; color:var(--muted); } -#snapshot-table{ width:100%; border-collapse:collapse; background:#0b1220; color:var(--fg); border:1px solid #1f2937; border-radius:8px; overflow:hidden; } -#snapshot-table th, #snapshot-table td{ padding:4px 8px; border:1px solid #334155; text-align:left; font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size:12px; } -#snapshot-table th{ background:#1e293b; } -footer{ padding:12px 24px; border-top:1px solid #1f2937; color:var(--muted); } +:root { + --bg: #0b1220; + --panel: #132c44; + --border: #0e1822; + --text: #e0e6ed; + --muted: #9aa6b2; + --accent: #a4bbd4; +} + +* { + box-sizing: border-box; +} + +html, body { + min-height: 100%; +} + +body { + font-family: Arial, sans-serif; + margin: 16px; + background: linear-gradient( + to bottom, + #dddddd -20%, + var(--bg) 130% + ); + color: var(--text); + font-size: 14px; +} + + +/* ===== Titel ===== */ + +.app-title { + margin: 0 0 12px; + font-size: 16px; + font-weight: 500; + color: var(--accent); +} + +/* ===== GRID ===== */ + +.sections { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); + gap: 16px; +} + +.section { + grid-column: span 2; +} + +.section.half { + grid-column: span 1; +} + +.section.full { + grid-column: span 2; +} + +/* ===== SECTION CARD ===== */ + +.section { + background: var(--panel); + border: 1px solid var(--border); + border-radius: 8px; + padding: 22px 20px; +} + +.section h2 { + margin: 0; + font-size: 15px; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + color: var(--accent); +} + +/* Collapse Pfeil */ + +.section h2::after { + content: "▼"; + font-size: 12px; + transition: transform 0.2s ease; +} + +.section.collapsed h2::after { + transform: rotate(-90deg); +} + +.section.collapsed > :not(h2) { + display: none; +} + +/* ===== ACTIONS ===== */ + +.controls { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-top: 12px; +} + +.controls button { + background: #1e293b; + color: var(--text); + border: 1px solid #334155; + padding: 10px 16px; + border-radius: 8px; + cursor: pointer; +} + +.controls button:hover { + border-color: var(--accent); +} + +.controls input { + padding: 10px; + border-radius: 8px; + border: 1px solid #334155; + background: #0b1220; + color: var(--text); + width: 220px; +} + +/* ===== TEXTAREAS ===== */ + +textarea { + width: 100%; + min-height: 200px; + margin-top: 12px; + background: #0b1220; + color: var(--text); + border: 1px solid #1f2937; + border-radius: 8px; + padding: 10px; + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 12px; +} + +/* ===== PANEL (alte Struktur wiederhergestellt) ===== */ + +.panel { + background: #0b1220; + border: 1px solid #1f2937; + border-radius: 8px; + padding: 12px; + margin-top: 12px; +} + +.panel label { + display: block; + font-size: 13px; + color: var(--muted); + margin-bottom: 6px; +} + +/* ===== TREE VIEW (JS-kompatibel) ===== */ + +#result-tree { + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 12px; +} + +#result-tree details { + margin-left: 14px; +} + +#result-tree summary { + cursor: pointer; + user-select: none; + color: #93c5fd; +} + +#result-tree .tree-leaf { + margin-left: 18px; + white-space: pre-wrap; +} \ No newline at end of file