diff --git a/public/calculateActions.js b/public/calculateActions.js index dfd38eb..8fe4f6f 100755 --- a/public/calculateActions.js +++ b/public/calculateActions.js @@ -1,262 +1,650 @@ // 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 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: 1 + }); + } + + 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 (row226 || row229 || row198 || row243 || row242 || row200 || row204) { + let x226 = 0; + let 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; + } + + 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/client.js b/public/client.js index 6a8e7d7..39fb8da 100755 --- a/public/client.js +++ b/public/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/index.html b/public/index.html index b46205f..c89c1d8 100755 --- a/public/index.html +++ b/public/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/calculateActions.js b/public/o/calculateActions.js new file mode 100755 index 0000000..dfd38eb --- /dev/null +++ b/public/o/calculateActions.js @@ -0,0 +1,262 @@ +// calculateActions.js +// Funktionen zum Berechnen von Vorschlägen basierend auf den neuesten CSV-Daten + +const analysisLogEl = document.getElementById('analysis-log'); + +function appendToAnalysis(line) { + const now = new Date().toISOString(); + analysisLogEl.value += `[${now}] ${line}\n`; + analysisLogEl.scrollTop = analysisLogEl.scrollHeight; +} + +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'); + + 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 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; + }); + appendToAnalysis(`CSV-Daten geladen: ${rows.length} Zeilen, ${headers.length} Spalten.`); + 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(row, axisY, axisZ, deltaYangle0, deltaZangle0 = 0) { + let y = parseFloat(row.y_mm); + let z = parseFloat(row.z_mm); + + let dy = -(y - axisY); + let dz = z - axisZ; + + let angle0Rad = 0; + if(deltaZangle0 !== 0){ + angle0Rad = Math.atan(deltaZangle0/deltaYangle0); + } + + 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; +} + +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 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; +} + +async function calculate() { + + let shoulderAxisY = 115; + let shoulderAxisZ = 61; + + let rows = null; + let headers = null; + + 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); + } + + + // 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 + }; +} + + +// Export für Module, falls benötigt +// export { fetchCSV, calculate }; \ No newline at end of file diff --git a/public/o/client.js b/public/o/client.js new file mode 100755 index 0000000..6a8e7d7 --- /dev/null +++ b/public/o/client.js @@ -0,0 +1,136 @@ +(function(){ + const logEl = document.getElementById('log'); + const connEl = document.getElementById('conn'); + + function append(line){ + const now = new Date().toISOString(); + logEl.value += `[${now}] ${line} +`; + logEl.scrollTop = logEl.scrollHeight; + } + + 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'; } + } + + 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 ""; + } + + 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); + }; + } + + function bindButtons(){ + document.querySelectorAll('button[data-cmd]').forEach(btn =>{ + btn.addEventListener('click', async () =>{ + const cmd = btn.getAttribute('data-cmd'); + + 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)); } + }); + }); + } + + 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 = ''; + } + } + + bindButtons(); + connectSSE(); + refreshStatus(); + loadLatestSnapshot(); +})(); diff --git a/public/o/index.html b/public/o/index.html new file mode 100755 index 0000000..b46205f --- /dev/null +++ b/public/o/index.html @@ -0,0 +1,57 @@ + + + + + + appRobotHoming + + + +
+

appRobotHoming

+
Status:
+
+ +
+
+ + + + + + + + + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+
+
+ +
+ HTTPS + WSS Relay • © +
+ + + + +