Info-Tree
This commit is contained in:
@@ -1,262 +1,650 @@
|
|||||||
// calculateActions.js
|
// 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) {
|
function appendToAnalysis(line) {
|
||||||
|
const el = getAnalysisLogEl();
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
analysisLogEl.value += `[${now}] ${line}\n`;
|
el.value += `[${now}] ${line}\n`;
|
||||||
analysisLogEl.scrollTop = analysisLogEl.scrollHeight;
|
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() {
|
async function fetchCSV() {
|
||||||
console.log('Lade und verarbeite CSV-Daten...');
|
const res = await fetch("/api/latest-snapshot");
|
||||||
const res = await fetch('/api/latest-snapshot');
|
if (!res.ok) throw new Error("Fehler beim Laden des Snapshots");
|
||||||
if (!res.ok) throw new Error('Fehler beim Laden des Snapshots');
|
|
||||||
|
|
||||||
let data;
|
let data;
|
||||||
if (res.headers.get('content-type')?.includes('application/json')) {
|
if (res.headers.get("content-type")?.includes("application/json")) {
|
||||||
data = await res.json();
|
data = await res.json();
|
||||||
} else {
|
} else {
|
||||||
const csvData = await res.text();
|
const csvData = await res.text();
|
||||||
data = { filename: 'latest.csv', mtime: new Date().toISOString(), content: csvData };
|
data = {
|
||||||
}
|
filename: "latest.csv",
|
||||||
|
mtime: new Date().toISOString(),
|
||||||
|
content: csvData
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// CSV parsen
|
const lines = data.content.trim().split(/\r?\n/).filter(Boolean);
|
||||||
const lines = data.content.trim().split('\n');
|
if (lines.length < 2) {
|
||||||
if (lines.length < 2) {
|
throw new Error("Keine oder unvollständige Daten");
|
||||||
throw new Error('Keine oder unvollständige Daten');
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const headers = lines[0].split(',').map(h => h.trim());
|
const headers = lines[0].split(",").map(h => h.trim());
|
||||||
const rows = lines.slice(1).map(line => {
|
|
||||||
const cells = line.split(',');
|
const rows = lines.slice(1).map(line => {
|
||||||
let obj = {};
|
const cells = line.split(",");
|
||||||
headers.forEach((h, i) => {
|
const obj = {};
|
||||||
const val = cells[i]?.trim();
|
headers.forEach((h, i) => {
|
||||||
obj[h] = isNaN(val) ? val : parseFloat(val);
|
const raw = (cells[i] ?? "").trim();
|
||||||
});
|
const numeric = Number(raw);
|
||||||
return obj;
|
obj[h] = raw !== "" && Number.isFinite(numeric) ? numeric : raw;
|
||||||
});
|
});
|
||||||
appendToAnalysis(`CSV-Daten geladen: ${rows.length} Zeilen, ${headers.length} Spalten.`);
|
return obj;
|
||||||
return { data, headers, rows };
|
});
|
||||||
|
|
||||||
|
return { data, headers, rows };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readValues( data, headers, rows ){
|
function calculateAngleFromPosition(result, row, axisY, axisZ, deltaYangle0, deltaZangle0 = 0, label = "angle") {
|
||||||
console.log('Geladene Daten:', data);
|
const y = parseFloat(row.y_mm);
|
||||||
console.log('Headers:', headers);
|
const z = parseFloat(row.z_mm);
|
||||||
console.log('Parsed rows:', rows);
|
|
||||||
|
|
||||||
|
const dy = -(y - axisY);
|
||||||
}
|
const dz = z - axisZ;
|
||||||
|
|
||||||
function calculateAngleFromPosition(row, axisY, axisZ, deltaYangle0, deltaZangle0 = 0) {
|
let angle0Rad = 0;
|
||||||
let y = parseFloat(row.y_mm);
|
if (deltaZangle0 !== 0 && deltaYangle0 !== 0) {
|
||||||
let z = parseFloat(row.z_mm);
|
angle0Rad = Math.atan(deltaZangle0 / deltaYangle0);
|
||||||
|
}
|
||||||
|
|
||||||
let dy = -(y - axisY);
|
const angleRad = Math.atan2(dz, dy) - angle0Rad;
|
||||||
let dz = z - axisZ;
|
const angleDeg = angleRad * 180 / Math.PI;
|
||||||
|
|
||||||
let angle0Rad = 0;
|
const message = `(${label} = ${angleDeg.toFixed(2)}° = ${angleRad.toFixed(4)} rad) aus Position von ID = ${row.id}`;
|
||||||
if(deltaZangle0 !== 0){
|
addLog(result, message);
|
||||||
angle0Rad = Math.atan(deltaZangle0/deltaYangle0);
|
|
||||||
|
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;
|
return angleRad;
|
||||||
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") {
|
function calculateAngleFromRollColumn(result, row, roll0 = 0, pitch0 = 0, yaw0 = 0, strMotor = "motor") {
|
||||||
let roll = -parseFloat(row.roll_deg) + roll0;
|
const rollDeg = -parseFloat(row.roll_deg) + roll0;
|
||||||
appendToAnalysis(`(${strMotor} = ${roll.toFixed(2)}° = ${(roll * Math.PI / 180).toFixed(4)} rad) aus roll_deg von ID = ${row.id}`);
|
const rollRad = rollDeg * Math.PI / 180;
|
||||||
return roll*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") {
|
function calculateAngleFromRelativePosition(result, row1, row2, strMotor = "motor") {
|
||||||
let y1 = parseFloat(row1.y_mm);
|
const y1 = parseFloat(row1.y_mm);
|
||||||
let z1 = parseFloat(row1.z_mm);
|
const z1 = parseFloat(row1.z_mm);
|
||||||
let y2 = parseFloat(row2.y_mm);
|
const y2 = parseFloat(row2.y_mm);
|
||||||
let z2 = parseFloat(row2.z_mm);
|
const z2 = parseFloat(row2.z_mm);
|
||||||
let dy = y1 - y2;
|
|
||||||
let dz = z1 - z2;
|
const dy = y1 - y2;
|
||||||
let angleRad = -Math.atan(dz/dy);
|
const dz = z1 - z2;
|
||||||
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}`);
|
const angleRad = -Math.atan2(dz, dy);
|
||||||
return angleRad;
|
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() {
|
async function calculate() {
|
||||||
|
const result = createAnalysisResult({
|
||||||
let shoulderAxisY = 115;
|
sourceScript: "calculateActions.js"
|
||||||
let shoulderAxisZ = 61;
|
});
|
||||||
|
|
||||||
let rows = null;
|
try {
|
||||||
let headers = null;
|
addLog(result, "Starte Berechnung...");
|
||||||
|
|
||||||
try {
|
const { data, headers, rows } = await fetchCSV();
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
result.inputs = {
|
||||||
// Oberarm:
|
source: {
|
||||||
var angleY = 0;
|
filename: data.filename,
|
||||||
var angleZ = 0;
|
mtime: data.mtime
|
||||||
var angleYcount = 0;
|
},
|
||||||
var angleZcount = 0;
|
headers,
|
||||||
|
rowCount: rows.length
|
||||||
// 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
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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
|
if (typeof module !== "undefined") {
|
||||||
// export { fetchCSV, calculate };
|
module.exports = {
|
||||||
|
calculate,
|
||||||
|
createAnalysisResult,
|
||||||
|
calculateAngleFromPosition,
|
||||||
|
calculateAngleFromRollColumn,
|
||||||
|
calculateAngleFromRelativePosition
|
||||||
|
};
|
||||||
|
}
|
||||||
247
public/client.js
247
public/client.js
@@ -1,136 +1,135 @@
|
|||||||
(function(){
|
// client.js
|
||||||
const logEl = document.getElementById('log');
|
// UI: Buttons, Anzeige von Result als JSON + Baum, Fallback für Commands
|
||||||
const connEl = document.getElementById('conn');
|
|
||||||
|
|
||||||
function append(line){
|
function appendLog(line) {
|
||||||
const now = new Date().toISOString();
|
const el = document.getElementById("log");
|
||||||
logEl.value += `[${now}] ${line}
|
if (!el) return;
|
||||||
`;
|
|
||||||
logEl.scrollTop = logEl.scrollHeight;
|
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(){
|
const details = document.createElement("details");
|
||||||
try{
|
details.open = open;
|
||||||
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){
|
const summary = document.createElement("summary");
|
||||||
if(data?.text){
|
summary.textContent = Array.isArray(value)
|
||||||
try{
|
? `${key} [${value.length}]`
|
||||||
let obj = JSON.parse(data.text);
|
: key;
|
||||||
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(){
|
details.appendChild(summary);
|
||||||
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(){
|
const body = document.createElement("div");
|
||||||
document.querySelectorAll('button[data-cmd]').forEach(btn =>{
|
body.style.marginLeft = "16px";
|
||||||
btn.addEventListener('click', async () =>{
|
|
||||||
const cmd = btn.getAttribute('data-cmd');
|
|
||||||
|
|
||||||
let payload = null;
|
if (Array.isArray(value)) {
|
||||||
const payloadSelector = btn.getAttribute('data-payload');
|
value.forEach((item, idx) => {
|
||||||
if (payloadSelector) {
|
body.appendChild(renderNode(String(idx), item, false));
|
||||||
const field = document.querySelector(payloadSelector);
|
});
|
||||||
if (field) payload = field.value;
|
} else {
|
||||||
}
|
Object.entries(value).forEach(([childKey, childVal]) => {
|
||||||
|
body.appendChild(renderNode(childKey, childVal, false));
|
||||||
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() {
|
details.appendChild(body);
|
||||||
try {
|
return details;
|
||||||
const res = await fetch('/api/latest-snapshot');
|
}
|
||||||
if (!res.ok) throw new Error('Fehler beim Laden des Snapshots');
|
|
||||||
let data;
|
function renderResult(result) {
|
||||||
if (res.headers.get('content-type')?.includes('application/json')) {
|
const jsonEl = document.getElementById("result-json");
|
||||||
data = await res.json();
|
const treeEl = document.getElementById("result-tree");
|
||||||
} else {
|
|
||||||
const csvData = await res.text();
|
if (jsonEl) {
|
||||||
// Fallback: filename aus dem Pfad oder unbekannt, mtime jetzt
|
jsonEl.value = JSON.stringify(result, null, 2);
|
||||||
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 = '<tr><td>Keine Daten</td></tr>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const headers = lines[0].split(',');
|
|
||||||
let html = '<thead><tr>' + headers.map(h => `<th>${h.trim()}</th>`).join('') + '</tr></thead><tbody>';
|
|
||||||
for (let i = 1; i < lines.length; i++) {
|
|
||||||
const cells = lines[i].split(',');
|
|
||||||
html += '<tr>' + cells.map(c => `<td>${c.trim()}</td>`).join('') + '</tr>';
|
|
||||||
}
|
|
||||||
html += '</tbody>';
|
|
||||||
tableEl.innerHTML = html;
|
|
||||||
} catch (err) {
|
|
||||||
document.getElementById('snapshot-info').textContent = 'Fehler: ' + err.message;
|
|
||||||
document.getElementById('snapshot-table').innerHTML = '';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bindButtons();
|
renderTree(treeEl, result, "result", true);
|
||||||
connectSSE();
|
}
|
||||||
refreshStatus();
|
|
||||||
loadLatestSnapshot();
|
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);
|
||||||
@@ -5,6 +5,70 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>appRobotHoming</title>
|
<title>appRobotHoming</title>
|
||||||
<link rel="stylesheet" href="/styles.css" />
|
<link rel="stylesheet" href="/styles.css" />
|
||||||
|
<style>
|
||||||
|
.result-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel textarea,
|
||||||
|
.panel pre {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 240px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
background: #0b1220;
|
||||||
|
color: #e2e8f0;
|
||||||
|
padding: 12px;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#result-tree {
|
||||||
|
min-height: 240px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
background: #0b1220;
|
||||||
|
color: #e2e8f0;
|
||||||
|
padding: 12px;
|
||||||
|
overflow: auto;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
|
||||||
|
#result-tree .tree-kv {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
.result-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
@@ -19,7 +83,8 @@
|
|||||||
<button data-cmd="STATUS">STATUS</button>
|
<button data-cmd="STATUS">STATUS</button>
|
||||||
<button data-cmd="RESET">RESET</button>
|
<button data-cmd="RESET">RESET</button>
|
||||||
<button data-cmd="PING">PING</button>
|
<button data-cmd="PING">PING</button>
|
||||||
<button onclick="calculate()">Calculate Actions</button>
|
<button id="btn-calculate">Calculate Actions</button>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
id="gcodePayload"
|
id="gcodePayload"
|
||||||
type="text"
|
type="text"
|
||||||
@@ -27,7 +92,6 @@
|
|||||||
style="width: 220px; padding: 10px; border-radius: 8px; border: 1px solid #334155; background: #0b1220; color: #e2e8f0;"
|
style="width: 220px; padding: 10px; border-radius: 8px; border: 1px solid #334155; background: #0b1220; color: #e2e8f0;"
|
||||||
/>
|
/>
|
||||||
<button data-cmd="GCODEMOTOR" data-payload="#gcodePayload">GCodeMotor</button>
|
<button data-cmd="GCODEMOTOR" data-payload="#gcodePayload">GCodeMotor</button>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="log">
|
<section class="log">
|
||||||
@@ -36,10 +100,24 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="analysis">
|
<section class="analysis">
|
||||||
<label for="analysis">Analysis & Reasoning</label>
|
<label for="analysis-log">Analysis & Reasoning</label>
|
||||||
<textarea id="analysis-log" readonly></textarea>
|
<textarea id="analysis-log" readonly></textarea>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="result">
|
||||||
|
<label>Berechnetes Result</label>
|
||||||
|
<div class="result-layout">
|
||||||
|
<div class="panel">
|
||||||
|
<label for="result-json">Raw JSON</label>
|
||||||
|
<textarea id="result-json" readonly></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="panel">
|
||||||
|
<label>Tree View</label>
|
||||||
|
<div id="result-tree"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="snapshot">
|
<section class="snapshot">
|
||||||
<label for="snapshot-content">Neuester Snapshot</label>
|
<label for="snapshot-content">Neuester Snapshot</label>
|
||||||
<div id="snapshot-info"></div>
|
<div id="snapshot-info"></div>
|
||||||
@@ -51,7 +129,7 @@
|
|||||||
<small>HTTPS + WSS Relay • ©</small>
|
<small>HTTPS + WSS Relay • ©</small>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="/client.js"></script>
|
|
||||||
<script src="/calculateActions.js"></script>
|
<script src="/calculateActions.js"></script>
|
||||||
|
<script src="/client.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
262
public/o/calculateActions.js
Executable file
262
public/o/calculateActions.js
Executable file
@@ -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 };
|
||||||
136
public/o/client.js
Executable file
136
public/o/client.js
Executable file
@@ -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 = '<tr><td>Keine Daten</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const headers = lines[0].split(',');
|
||||||
|
let html = '<thead><tr>' + headers.map(h => `<th>${h.trim()}</th>`).join('') + '</tr></thead><tbody>';
|
||||||
|
for (let i = 1; i < lines.length; i++) {
|
||||||
|
const cells = lines[i].split(',');
|
||||||
|
html += '<tr>' + cells.map(c => `<td>${c.trim()}</td>`).join('') + '</tr>';
|
||||||
|
}
|
||||||
|
html += '</tbody>';
|
||||||
|
tableEl.innerHTML = html;
|
||||||
|
} catch (err) {
|
||||||
|
document.getElementById('snapshot-info').textContent = 'Fehler: ' + err.message;
|
||||||
|
document.getElementById('snapshot-table').innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bindButtons();
|
||||||
|
connectSSE();
|
||||||
|
refreshStatus();
|
||||||
|
loadLatestSnapshot();
|
||||||
|
})();
|
||||||
57
public/o/index.html
Executable file
57
public/o/index.html
Executable file
@@ -0,0 +1,57 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>appRobotHoming</title>
|
||||||
|
<link rel="stylesheet" href="/styles.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>appRobotHoming</h1>
|
||||||
|
<div id="status">Status: <span class="badge" id="conn">…</span></div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="controls">
|
||||||
|
<button data-cmd="HOME">HOME</button>
|
||||||
|
<button data-cmd="STOP">STOP</button>
|
||||||
|
<button data-cmd="STATUS">STATUS</button>
|
||||||
|
<button data-cmd="RESET">RESET</button>
|
||||||
|
<button data-cmd="PING">PING</button>
|
||||||
|
<button onclick="calculate()">Calculate Actions</button>
|
||||||
|
<input
|
||||||
|
id="gcodePayload"
|
||||||
|
type="text"
|
||||||
|
placeholder="G-Code / Motorbefehl"
|
||||||
|
style="width: 220px; padding: 10px; border-radius: 8px; border: 1px solid #334155; background: #0b1220; color: #e2e8f0;"
|
||||||
|
/>
|
||||||
|
<button data-cmd="GCODEMOTOR" data-payload="#gcodePayload">GCodeMotor</button>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="log">
|
||||||
|
<label for="log">Ausgabe</label>
|
||||||
|
<textarea id="log" readonly></textarea>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="analysis">
|
||||||
|
<label for="analysis">Analysis & Reasoning</label>
|
||||||
|
<textarea id="analysis-log" readonly></textarea>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="snapshot">
|
||||||
|
<label for="snapshot-content">Neuester Snapshot</label>
|
||||||
|
<div id="snapshot-info"></div>
|
||||||
|
<table id="snapshot-table"></table>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<small>HTTPS + WSS Relay • ©</small>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="/client.js"></script>
|
||||||
|
<script src="/calculateActions.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user