Compare commits

...

2 Commits

Author SHA1 Message Date
chk
cce53cda54 Roadmap Homing 2026-06-14 16:29:01 +02:00
chk
275ab083fa Roadmap planen 2026-06-14 16:23:18 +02:00
13 changed files with 277 additions and 3338 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,595 +0,0 @@
{
"coordinateSystem": {
"handedness": "right",
"x": "right",
"y": "backward",
"z": "up"
},
"units": {
"length": "mm",
"rotation": "degree"
},
"vision_config": {
"MarkerType": "DICT_4X4_250",
"MarkerSize": 0.025
},
"renderingInfo": {
"width": 1280,
"height": 720,
"cameraPosition__1": [-10, -800, 500],
"cameraPosition__2": [-500, 300, 1200],
"cameraPosition__3": [-200, -900, 200],
"cameraPosition__4": [1200, 200, 300],
"cameraPosition_a":[-300, -800,500],
"cameraPosition":[300, -500,1450],
"cameraPosition_c":[600, -500,600],
"cameraTarget": [210, -200, 180],
"cameraUpVector": [0, 0, 1],
"lightPosition": [-500, -500, 500],
"lightTarget": [0, 0, 0],
"lightUpVector": [0, 0, 1],
"metric": "mm",
"showSkeleton": true,
"showMarkers": true,
"backgroundColor": [0.70, 0.85, 1.0],
"backgroundStrength": 0.20,
"sunEnergy": 0.35,
"areaEnergy": 120,
"exposure": -1.5,
"lensDirt": true,
"lensDirtStrength": 0.08,
"dofEnabled": true,
"dofFStop": 11,
"arucoDust": false,
"arucoDustStrength": 0.00005,
"localizedBlur": false,
"localizedBlurStrength": 0.15,
"vignette": true,
"vignetteStrength": 0.08,
"sensorNoise": true,
"sensorNoiseStrength": 0.01,
"lensDistortion": true,
"lensDistortionStrength": 0.002,
"materials": {
"wood": {
"baseColor": [0.72, 0.52, 0.33],
"roughness": 0.85,
"metallic": 0.0
},
"plaWhite": {
"baseColor": [0.95, 0.95, 0.95],
"roughness": 0.45,
"metallic": 0.0
},
"steel": {
"baseColor": [0.72, 0.72, 0.75],
"roughness": 0.25,
"metallic": 1.0
},
"powderCoatBlue": {
"baseColor": [0.15, 0.25, 0.7],
"roughness": 0.55,
"metallic": 0.0
},
"defaultPlastic": {
"baseColor": [0.95, 0.95, 0.95],
"roughness": 0.4,
"metallic": 0.0
},
"skeletonRed": {
"baseColor": [0.85, 0.20, 0.20],
"roughness": 0.35,
"metallic": 0.0
},
"markerBlack": {
"baseColor": [0.04, 0.04, 0.04],
"roughness": 0.80,
"metallic": 0.0
}
},
"skeletonDefaults": {
"radius": 4,
"color": [0.85, 0.20, 0.20]
},
"markerDefaults": {
"size": 25,
"thickness": 1,
"color": [0.04, 0.04, 0.04]
}
},
"defaultPosition__": {
"x": 10,
"y": 4,
"z": 20,
"a": 10,
"b": 2,
"c": 9,
"e": 1
},
"defaultPosition": {
"x": 180,
"y": 86,
"z": -120,
"a": -60,
"b": 22,
"c": 91,
"e": 10
},
"recognized": {
"x": null,
"y": null,
"z": null,
"a": null,
"b": null,
"c": null,
"e": null
},
"constraint_rules": {
"rigid_distance": {
"enabled": true,
"mode": "mst",
"weight": 1.0
},
"joint_axis_projection": {
"enabled": true,
"max_pairs": 2,
"weight": 0.35
},
"chain_axis_projection": {
"enabled": false,
"max_depth": 3,
"max_pairs": 2,
"weight": 0.15
},
"axis_alignment_threshold": 0.95
},
"observation_weighting": {
"enabled": true,
"distance_weight": true,
"marker_size_weight": true,
"view_angle_weight": true
},
"multiview_calculation": {
"combine_mode": "mean",
"size_ref_px": 50.0,
"border_ref_px": 120.0,
"center_ref_norm": 0.01,
"sharpness_ref": 2500.0,
"homography_ref": 0.18,
"size_factor": 0.3,
"aspect_factor": 0.3,
"border_factor": 0.01,
"center_factor": 0.01,
"sharpness_factor": 0.5,
"homography_factor": 0.2,
"normal_visibility_factor": 0.01,
"spin_factor": 0.3,
"weight_floor": 0.3
},
"robot_test_poses": {
"sim04": {"x": 70, "y": 50,"z": -70,"a": 120,"b": 50,"c": 30,"e": 20},
"sim05": {"x": 180,"y": 86,"z": -120,"a": -60,"b": 22,"c": 91,"e": 10},
"sim06": {"x": 80, "y": 20, "z": 80, "a": -120, "b": 23, "c": 9, "e": 3},
"sim07": {"x": 30, "y": -2, "z": 95, "a": 20, "b": 23, "c": 9, "e": 9},
"sim08": {"x": 50, "y": -2, "z": 95, "a": 20, "b": 60, "c": 9, "e": 3},
"sim09": {"x": 60, "y": -2, "z": 95, "a": 200, "b": 60, "c": 9, "e": 8},
"sim09a": {"x": 60, "y": -2, "z": 95, "a": 200, "b": 60, "c": 9, "e": 8},
"sim09b": {"x": 60, "y": -2, "z": 95, "a": 200, "b": 60, "c": 9, "e": 8},
"sim010": {"x": 120, "y": 60, "z": -110, "a": 20, "b": 30, "c": 180, "e": 4},
"sim011": {"x": 50, "y": 4, "z": 176, "a": 20, "b": 60, "c": 9, "e": 5},
"sim012": {"x": 50, "y": 0, "z": 178, "a": 210, "b": 80, "c": 90, "e": 6}
},
"movements": {
"x": null,
"y": null,
"z": null,
"a": null,
"b": null,
"c": null,
"e": null
},
"state_pose_params":{
"numbers_of_Elements_to_consider_start": 3,
"numbers_of_Elements_to_consider_final": 4,
"solver_in_between_geometrical": false,
"solver_after_geometrical": false,
"geometric_passes_per_stage": 2,
"revolute_search_coarse_deg": 5.0,
"revolute_search_fine_deg": 1.0,
"root_pose_min_markers": 3,
"use_marker_normals_flip_tiebreak": true,
"normal_flip_weight": 0.05
},
"links": {
"Board": {
"parent": null,
"size": [1000, 200, 25],
"mountPosition": [0, 0, 0],
"mountRotation": [0, 0, 0],
"skeleton": {
"from": [0, 0, 16],
"to": [1000, 0, 16],
"radius": 4,
"color": [0.85, 0.20, 0.20]
},
"markers":[
{"id":210,"position":[20, -20, 0.3], "normal":[0,0,1]},
{"id":211,"position":[250, -10, 0.3], "normal":[0,0,1]},
{"id":215,"position":[250, -90, 0.3], "normal":[0,0,1]},
{"id":214,"position":[350, -10, 0.3], "normal":[0,0,1]},
{"id":208,"position":[350, -90, 0.3], "normal":[0,0,1]},
{"id":206,"position":[650, -10, 0.3], "normal":[0,0,1]},
{"id":205,"position":[750, -90, 0.3], "normal":[0,0,1]},
{"id":207,"position":[750, -10, 0.3], "normal":[0,0,1]},
{"id":217,"position":[650, -90, 0.3], "normal":[0,0,1]},
{"id": 46, "position": [536.71, 185.44, -27.3], "normal": [0, 0, 1], "spin": 90, "info": "is placed on a white paper, A0_60Arucos_25mm_Seet223.pdf, with the following marker placements:"},
{"id": 47, "position": [344.23, -286.54, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 48, "position": [688.69, -320.72, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 49, "position": [1006.0, 158.33, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 50, "position": [573.41, 211.86, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 51, "position": [167.8, -172.08, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 52, "position": [94.68, 208.66, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 53, "position": [486.25, 212.24, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 54, "position": [342.27, -330.59, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 55, "position": [283.72, -262.58, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 56, "position": [498.68, 168.67, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 57, "position": [602.86, -364.05, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 58, "position": [50.09, -218.11, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 59, "position": [626.21, -278.75, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 60, "position": [434.36, 283.81, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 61, "position": [-22.42, 335.83, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 62, "position": [404.7, -175.1, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 63, "position": [777.4, -236.15, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 64, "position": [-21.27, -188.23, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 65, "position": [803.39, -297.37, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 66, "position": [209.75, -363.23, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 67, "position": [523.07, 267.04, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 68, "position": [573.73, 170.64, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 69, "position": [7.61, -281.21, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 70, "position": [601.87, 300.33, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 71, "position": [749.75, -284.01, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 72, "position": [440.99, 194.32, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 73, "position": [221.73, 333.11, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 74, "position": [93.78, 144.5, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 75, "position": [-25.7, 194.58, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 76, "position": [685.21, 166.8, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 77, "position": [18.19, 191.57, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 78, "position": [823.11, -344.38, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 79, "position": [312.3, -159.11, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 80, "position": [863.59, -335.92, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 81, "position": [132.14, 169.03, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 82, "position": [219.16, 297.24, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 83, "position": [44.16, 339.22, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 84, "position": [407.49, 258.42, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 85, "position": [504.58, -312.75, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 86, "position": [362.89, 292.01, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 87, "position": [943.63, -245.76, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 88, "position": [765.87, 316.04, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 89, "position": [988.02, -369.14, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 90, "position": [643.17, 316.43, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 91, "position": [723.35, 328.05, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 92, "position": [645.09, -184.84, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 93, "position": [934.88, 143.6, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 94, "position": [875.7, 173.65, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 95, "position": [186.04, -274.07, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 96, "position": [369.77, -186.49, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 97, "position": [304.35, -359.67, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 98, "position": [575.27, 315.06, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 99, "position": [959.16, -321.55, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 100, "position": [803.25, 172.36, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 101, "position": [117.7, 298.66, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 102, "position": [649.69, -223.0, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 103, "position": [105.71, -187.71, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 104, "position": [826.71, 239.16, -27.3], "normal": [0, 0, 1], "spin": 90},
{"id": 105, "position": [524.84, -266.25, -27.3], "normal": [0, 0, 1], "spin": 90}
],
"model": [
{
"stlFile": "surfaces/Board.stl",
"originOfModel": [0, 0, 0],
"rotationOfModelDegree": [0, 0, -90],
"material": "wood"
},
{
"stlFile": "surfaces/BoardRail.stl",
"originOfModel": [0, 0, 0],
"rotationOfModelDegree": [0, 0, -90],
"material": "steel"
}
]
},
"Base": {
"parent": "Board",
"size": [150, 200, 150],
"mountPosition": [0, 0, 0],
"mountRotation": [0, 0, 0],
"jointToParent": {
"name": "Slider",
"type": "linear",
"axis": [1, 0, 0],
"origin": [0, 0, 16],
"rotation": [0, 0, 0],
"variable": "x"
},
"skeleton": {
"from": [0, 108, 45],
"to": [110, 108, 45],
"radius": 4,
"color": [0.20, 0.80, 0.20]
},
"markers": [
],
"model": [
{
"stlFile": "surfaces/Base.stl",
"originOfModel": [-30, 0, -35],
"rotationOfModelDegree": [0, 0, 0],
"material": "plaWhite"
}
]
},
"Arm1": {
"parent": "Base",
"size": [70, 250, 70],
"mountPosition": [0, 0, 0],
"mountRotation": [0, 0, 0],
"jointToParent": {
"name": "Joint1",
"type": "revolute",
"axis": [-1, 0, 0],
"origin": [110, 108, 45],
"rotation": [0, 0, 0],
"variable": "y"
},
"skeleton": {
"from": [0, 0, 0],
"to": [0, -250, 0],
"radius": 4,
"color": [0.20, 0.20, 0.90]
},
"markers": [
{
"id": 198,
"name": "aruco_198",
"position": [0, -160, 35],
"normal": [0, 0, 1],
"size": 25,
"spin": 0
},
{
"id": 229,
"name": "aruco_229",
"position": [0, -250, 35],
"normal": [0, 0, 1],
"size": 25,
"spin": 0
},
{
"id": 242,
"name": "aruco_242",
"position": [0, -250, -35],
"normal": [0, 0, -1],
"size": 25,
"spin": 0
},
{
"id": 243,
"name": "aruco_243",
"position": [0, -285, 0],
"normal": [0, -1, 0],
"size": 25,
"spin": 0
}
],
"model": [
{
"stlFile": "surfaces/Holm.stl",
"originOfModel__": [-25,29,-28.5],
"originOfModel": [-29,25,28.5],
"rotationOfModelDegree__": [0, 0, 0],
"rotationOfModelDegree": [180, 0, -90],
"material": "powderCoatBlue"
}
]
},
"Ellbow": {
"parent": "Arm1",
"mountPosition": [0, 0, 0],
"mountRotation": [0, 0, 0],
"jointToParent": {
"name": "Joint2",
"type": "revolute",
"axis": [-1, 0, 0],
"origin": [0, -250, 0],
"rotation": [0, 0, 0],
"variable": "z"
},
"skeleton": {
"from": [0, 0, 0],
"to": [90, 0, 0],
"radius": 4,
"color": [0.90, 0.20, 0.20]
},
"model": [
{
"stlFile": "surfaces/Ellebogen.stl",
"originOfModel": [90,0,0],
"rotationOfModelDegree": [0,-90,-90],
"material": "defaultPlastic"
}
],
"markers": [
{"id": 244, "name": "aruco_244", "position": [125, 0, 0], "normal": [1, 0, 0], "size": 25, "spin": 0},
{"id": 245, "name": "aruco_245", "position": [90, 0, -35], "normal": [0, 0, -1], "size": 25, "spin": 0},
{"id": 246, "name": "aruco_246", "position": [90, 0, 35], "normal": [0, 0, 1], "size": 25},
{"id": 247, "name": "aruco_247", "position": [52.5, 0, 35], "normal": [0, 0, 1], "size": 25},
{"id": 248, "name": "aruco_248", "position": [52.5, 0, -35], "normal": [0, 0, -1], "size": 25},
{"id": 232, "name": "aruco_232", "position": [90, 24.75, -24.75], "normal": [0, 1, -1], "size": 25},
{"id": 231, "name": "aruco_231", "position": [90, 24.75, 24.75], "normal": [0, 1, 1], "size": 25}
]
},
"Arm2": {
"parent": "Ellbow",
"mountPosition": [0, 0, 0],
"mountRotation": [0, 0, 0],
"jointToParent": {
"name": "Joint3",
"type": "revolute",
"axis": [0, -1, 0],
"origin": [90, 0, 0],
"rotation": [0, 0, 0],
"variable": "a"
},
"skeleton": {
"from": [0, 0, 0],
"to": [0, -250, 0],
"radius": 4,
"color": [0.95, 0.85, 0.20]
},
"model": [
{
"stlFile": "surfaces/Unterarm.stl",
"originOfModel": [0,-250,0],
"rotationOfModelDegree": [180, 0, -90],
"material": "defaultPlastic"
}
],
"markers":[
{"id":120, "position":[24.75, -112, -24.75],"normal":[1,0,-1]},
{"id":122, "name": "aruco_122", "position":[-35,-112,0], "normal":[-1,0,0]},
{"id":218, "name": "aruco_218", "position":[35,-112,0], "normal":[1,0,0]},
{"id":113, "name": "aruco_113", "position":[0, -182, 30],"normal":[0,0,1]},
{"id":114, "name": "aruco_114", "position":[ 24.75, -182, -24.75],"normal":[ 1,0,-1]},
{"id":115, "name": "aruco_115", "position":[-24.75, -182, -24.75],"normal":[-1,0,-1]},
{"id":124, "name": "aruco_124", "position":[-35,-219,0], "normal":[-1,0,0]},
{"id":219, "name": "aruco_219", "position":[35,-219,0], "normal":[1,0,0]}
]
},
"Hand": {
"parent": "Arm2",
"mountPosition": [0, 0, 0],
"mountRotation": [0, 0, 0],
"jointToParent": {
"name": "Joint4",
"type": "revolute",
"axis": [1, 0, 0],
"origin": [0, -250, 0],
"rotation": [0, 0, 0],
"variable": "b"
},
"skeleton": {
"from": [0, 0, 0],
"to": [0, -35, 0],
"radius": 4,
"color": [0.95, 0.55, 0.15]
}
},
"Palm": {
"parent": "Hand",
"mountPosition": [0, 0, 0],
"mountRotation": [0, 0, 0],
"jointToParent": {
"name": "Joint3",
"type": "revolute",
"axis": [0, -1, 0],
"origin": [0, 0, 0],
"rotation": [0, 0, 0],
"variable": "c"
},
"skeleton": {
"from": [-50, -35, 0],
"to": [50, -35, 0],
"radius": 7,
"color": [0.95, 0.20, 0.20]
}
},
"FingerA": {
"parent": "Palm",
"size": [80, 60, 20],
"mountPosition": [0, 0, 0],
"mountRotation": [0, 0, 0],
"jointToParent": {
"name": "Slider",
"type": "linear",
"axis": [1, 0, 0],
"origin": [4, -35,0],
"rotation": [0, 0, 0],
"variable": "e"
},
"skeleton": {
"from": [0, 0,0],
"to": [0, -60, 0],
"radius": 4,
"color": [0.20, 0.80, 0.20]
},
"markers":[
{"id":40,"position":[12,-24,-17.1],"normal":[-10.98,0,-23.56]},
{"id":41,"position":[1.5,-2.2,25.8],"normal":[0,-25.6,9.5]},
{"id":42,"position":[13.9,-40,0],"normal":[1,-0.35,0.40], "spin": 27}
],
"model": [
{
"stlFile": "surfaces/Finger.stl",
"originOfModel": [24,0,-9.1],
"rotationOfModelDegree": [90, -90,0],
"material": "defaultPlastic"
}
]
},
"FingerB": {
"parent": "Palm",
"size": [80, 60, 20],
"mountPosition": [0, 0, 0],
"mountRotation": [0, 0, 0],
"jointToParent": {
"name": "Slider",
"type": "linear",
"axis": [-1, 0, 0],
"origin": [-4, -35,0],
"rotation": [0, 0, 0],
"variable": "e"
},
"skeleton": {
"from": [0, 0,0],
"to": [0, -60, 0],
"radius": 4,
"color": [0.20, 0.80, 0.20]
},
"markers":[
{"id":43,"position":[-12,-24,17.1],"normal":[10.98,0,23.56], "spin":90 },
{"id":44,"position":[-1.5,-2.2,-25.8],"normal":[0,-25.6,-9.5], "spin":90},
{"id":45,"position":[-13.9,-40,0],"normal":[-1,-0.35,-0.40], "spin": -27}
],
"model": [
{
"stlFile": "surfaces/Finger.stl",
"originOfModel": [-24,0,9.1],
"rotationOfModelDegree": [90, 90,0],
"material": "defaultPlastic"
}
]
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 MiB

View File

@@ -8,374 +8,349 @@
## Was ist Homing? ## Was ist Homing?
Homing = der Roboter weiss **nicht**, wo er ist. Homing = der Roboter weiss **nicht**, wo er ist.
Die Kameras schauen auf das Board + die ArUco-Marker am Roboter und berechnen Die Kameras schauen auf das Board + die ArUco-Marker am Roboter und berechnen
daraus die vollständige Pose aller Gelenke — ohne mechanische Endschalter. daraus die vollständige Pose aller Gelenke — ohne mechanische Endschalter.
Homing läuft bei **jedem** Einschalten ab, also muss es schnell und robust sein. Homing läuft bei **jedem** Einschalten ab: schnell, robust, vollautomatisch.
Kalibrierung hingegen läuft nur nach mechanischen Änderungen (≈ einmalig). Kalibrierung hingegen läuft nur nach mechanischen Änderungen (≈ einmalig).
--- ---
## Voraussetzungen (Kalibrierung muss abgeschlossen sein) ## Kinematik-Kette (aus `robot.json → links`)
| Was | Wo gespeichert | Status | ```
|-----|----------------|--------| Board (ROOT, fest) ← Referenz aller Kameras
| Kamera-Intrinsik (NPZ) | `data/calibration/camX_calibration.npz` | ✅ |
| Board-Marker-Positionen | `robot.json → links.Board.markers[]` | ✅ | ├── Base linear x axis=[1,0,0] ← Slider-Position
| X-Achsen-Richtung | `robot.json → links.*.position` (rotiert) | ✅ | ├── Arm1 revolute y axis=[-1,0,0] ← Schultergelenk
| **Arm1-Gelenkursprung Y/Z** | `robot.json → links.Arm1.jointToParent.origin[1,2]` | 🔶 in Arbeit | ├── Ellbow revolute z axis=[-1,0,0] ← Ellbogen
| Arm-Marker-Zuordnung | `robot.json → links.Arm1/Ellbow/Arm2/Hand.markers[]` | ❌ offen | ├── Arm2 revolute a axis=[0,-1,0] ← Unterarm-Drehung
├── Hand revolute b axis=[1,0,0] ← Handgelenk
├── Palm revolute c axis=[0,-1,0] ← Handfläche
└── FingerA/B linear e axis=±[1,0,0] ← Greifer (symmetrisch)
```
> **Schlüssel-Erkenntnis:** Sobald `Arm1.jointToParent.origin` korrekt gesetzt ist (aus **Resultat-State:** `{ x_mm, y_deg, z_deg, a_deg, b_deg, c_deg, e_mm }`
> der Y-Achsen-Kalibrierung), ist die gesamte Kinematikkette in `robot.json` geometrisch
> definiert. Dann kann Homing starten.
--- ---
## Kinematik-Kette (aus `robot.json`) ## Voraussetzungen (Kalibrierung)
``` | Was | Mechanismus | Status |
Board (ROOT, fest) |-----|-------------|--------|
| Kamera-Intrinsik (NPZ) | `calibration.html` → Tab Camera NPZ | ✅ fertig |
├── Base linear variable=x axis=[1,0,0] origin=[0,0,16] | Board-Marker-Positionen | `calibration.html` → Tab Board | ✅ fertig |
│ → Slider-Position entlang Board-X | X-Achsen-Richtung | `calibration.html` → Tab Robot X-Axis | ✅ fertig |
| **Arm1 Joint-Origin Y/Z** | `calibration.html` → Tab Arm1 → Button „Joint-Origin Y/Z übernehmen" | ✅ **Button vorhanden, ausführbar** |
├── Arm1 revolute variable=y axis=[-1,0,0] origin=[110, 108*, 45*] | Arm-Marker in robot.json | Manuell eintragen (links.Arm1/Ellbow/Arm2/Hand.markers) | 🔶 Nutzer trägt ein |
│ → Schultergelenk (heben/senken) *aus Y-Achsen-Kalib.
├── Ellbow revolute variable=z axis=[-1,0,0] origin=[0,-250,0]
│ → Ellbogen (relativ zu Arm1-Ende)
├── Arm2 revolute variable=a axis=[0,-1,0] origin=[90,0,0]
│ → Unterarm-Drehung
├── Hand revolute variable=b axis=[1,0,0] origin=[0,-250,0]
│ → Handgelenk
├── Palm revolute variable=c axis=[0,-1,0] origin=[0,0,0]
│ → Handflächen-Rotation
└── FingerA/B linear variable=e axis=±[1,0,0] origin=[±4,-35,0]
→ Greifer (symmetrisch)
```
**Gelenkvariablen:** `x` (mm), `y z a b c` (Grad), `e` (mm) > **Kalibrierung gilt als abgeschlossen** sobald der Arm1-Button geklickt und
> die Arm-Marker eingetragen sind.
--- ---
## Homing-Ablauf (Schritt für Schritt) ## robot.json Ladestrategie
### Schritt 1 — Snapshot aufnehmen ### Aktuell: lokale Datei
```
**Was:** Alle Kameras gleichzeitig ein Foto. ROBOT_JSON = process.env.ROBOT_JSON || 'scripts/robot_1781069752019.json'
**Script:** — (direkt via Webcam-API)
**UI-Aktion:** Button `[Foto aufnehmen]`
**Ausgabe:**
- `cam0.jpg`, `cam1.jpg`, `cam2.jpg` im aktuellen Run-Verzeichnis
- **Snapshots-Sektion:** Kamerabilder erscheinen
---
### Schritt 2 — ArUco-Marker erkennen
**Was:** Pixel-Koordinaten aller sichtbaren Marker in jedem Kamerabild.
**Script:** `1_detect_aruco_observations.py`
```bash
python 1_detect_aruco_observations.py \
-i cam0.jpg cam1.jpg cam2.jpg \
-robot robot.json \
-outDir data/homing/TIMESTAMP/
``` ```
**Output-Dateien:** ### Geplant: vom Driver per API
- `cam0_aruco_detection.json` — Marker-IDs + Pixel-Ecken + Kamera-Pose-Kandidaten Der Driver-Service (ROBOT_URL) kennt die aktuelle Roboter-Konfiguration.
Lademechanismus (bereits implementiertes Muster aus `ROBOT_URL`/`BODYTRACKER_URL`):
**Snapshot CSV:** Tabelle: MarkerID | Kamera | px-Koordinaten | confidence ```
GET ROBOT_URL/api/robot/config → robot.json Inhalt
**Fehlerfälle:**
- Marker verdeckt → weniger Marker in CSV sichtbar
- Schlechte Beleuchtung → confidence niedrig
---
### Schritt 3 — Kamera-Posen schätzen
**Was:** Aus den Board-Markern (fix im Raum) die extrinsische Lage jeder Kamera
berechnen.
**Script:** `2_estimate_camera_from_observations.py`
```bash
python 2_estimate_camera_from_observations.py \
-i data/homing/TIMESTAMP/ \
-robot robot.json \
-outDir data/homing/TIMESTAMP/
``` ```
**Output-Dateien:** **Implementierung (Backend `server.js`):**
- `cam0_camera_pose.json` — 4×4 Transformationsmatrix Kamera→Welt ```javascript
async function loadRobotConfig() {
**Analysis & Reasoning:** Reprojektions-Fehler pro Kamera (sollte < 3 px) if (ROBOT_URL) {
// Vom Driver holen
**Fehlerfälle:** const res = await fetch(new URL('/api/robot/config', ROBOT_URL));
- Zu wenig Board-Marker sichtbar (< 3) → Kamera-Pose nicht berechenbar return res.json();
- Hoher Reprojektions-Fehler → Kalibrierung möglicherweise veraltet }
// Fallback: lokale Datei
--- return JSON.parse(await fs.readFile(ROBOT_JSON, 'utf8'));
### Schritt 3b — 3D-Marker-Positionen triangulieren
**Was:** Aus den Kamera-Posen und den Pixel-Koordinaten die echten 3D-Positionen
aller Marker berechnen (inkl. Arm-Marker).
**Script:** `3b_corner_marker_poses.py`
```bash
python 3b_corner_marker_poses.py \
-i data/homing/TIMESTAMP/ \
-robot robot.json \
--outDir data/homing/TIMESTAMP/
```
**Output-Dateien:**
- `aruco_marker_poses.json` — für jeden Marker: `position_mm`, `normal`, `corners_m`, `link`
**Snapshot CSV:** Vollständige Marker-Tabelle mit triangulierten 3D-Positionen
**Fehlerfälle:**
- Marker nur in 1 Kamera → keine Triangulation möglich (braucht ≥ 2)
---
### Schritt 4 — Gelenkwinkel bestimmen
Zwei Methoden — **4b** ist sequenziell und robuster:
#### Methode A — Vollständige State-Schätzung (schnell)
**Script:** `4_robotState_estimation_v6.py`
```bash
python 4_robotState_estimation_v6.py \
aruco_marker_poses.json \
--robot robot.json \
--output homing_state.json
```
Schätzt alle Gelenke in einem Durchlauf mit geometrischen Gleichungen
(Kabsch-Fit, Projektions-Residuen, 2D-Winkel).
#### Methode B — Sequenziell Gelenk für Gelenk (robuster)
**Script:** `4b_revolute_angle.py` — einmal pro Gelenk, von root nach tip:
```bash
# Slider-Position (x) aus Board-Markern bekannt → manuell oder aus 4a
python 4b_revolute_angle.py --robot robot.json --aruco aruco_marker_poses.json \
--link Arm1 --x-mm 180 --output state_arm1.json
python 4b_revolute_angle.py --robot robot.json --aruco aruco_marker_poses.json \
--link Ellbow --from-state state_arm1.json --output state_ellbow.json
python 4b_revolute_angle.py --robot robot.json --aruco aruco_marker_poses.json \
--link Arm2 --from-state state_ellbow.json --output state_arm2.json
python 4b_revolute_angle.py --robot robot.json --aruco aruco_marker_poses.json \
--link Hand --from-state state_arm2.json --output state_hand.json
```
Jedes `--from-state` enthält den akkumulierten Gelenk-State aus allen
vorherigen Schritten.
**Warum sequenziell?** Arm2-Winkel kann nur korrekt berechnet werden wenn
Arm1 und Ellbow bereits bekannt sind (der Weltachsenvektor des Arm2-Joints
ändert sich mit den vorherigen Gelenkwinkeln).
**Output-JSON-Struktur (pro Schritt):**
```json
{
"link": "Arm1",
"joint": "y",
"mean_angle_deg": 23.4,
"circular_std_deg": 0.8,
"num_pairs": 6,
"joint_origin_world_mm": [290, 108, 45],
"joint_axis_world": [-1, 0, 0],
"accumulated_state": { "x": 180, "y": 23.4, "z": null, "a": null, ... }
} }
``` ```
**Analysis & Reasoning:** Gelenk-für-Gelenk-Ergebnis als JSON-Baum **Konsequenz für Homing:** Das Homing-Script bekommt robot.json als
temporäre Datei (bereits vorhandenes Muster: `ROBOT_JSON` als Pfad an Python).
Falls ROBOT_URL konfiguriert: zuerst fetch → temp-Datei schreiben → Script aufrufen.
**Fehlerfälle:** **Priorität:** Kann nach dem restlichen Homing implementiert werden.
- `circular_std_deg` > 5° → Marker-Konfiguration fragwürdig, Messung wiederholen Solange ROBOT_URL nicht konfiguriert, läuft alles mit der lokalen Datei.
- `num_pairs` < 2 → Arm-Marker nicht genug sichtbar
--- ---
### Schritt 5 (optional) — Kamera-Z verfeinern ## X-Position (Slider) Bestimmung
**Was:** Kamera-Höhe (Z) ist aus Board-Markern schlecht bestimmt (Board ist flach). Die Slider-Position `x` wird **nicht** manuell eingegeben, sondern aus den
Wenn Ellbow-Winkel bekannt → FK-Vorhersage als Z-Referenz nutzen. triangulierten Marker-Positionen berechnet (nach Schritt 3b).
**Script:** `5_camera_z_refine.py` **Ansatz:** Die absolute X-Position eines bekannten Arm-Markers im
Board-Koordinatensystem enthält direkt die Slider-Information —
alle anderen Gelenke sind rotatorisch und verschieben den Marker nicht
entlang der X-Achse des Boards.
```bash Alternativ: Schwerpunkt der Board-nahen A0-Marker projiziert auf die X-Achse
python 5_camera_z_refine.py \ (robust, braucht keine Arm-Marker).
--angle state_ellbow.json \
--robot robot.json \ **In `4b_revolute_angle.py`:** `--x-mm` wird aus `aruco_marker_poses.json`
--aruco aruco_marker_poses.json \ berechnet und als erstes Argument übergeben. Alle weiteren 4b-Aufrufe
-pose cam0_camera_pose.json \ nutzen `--from-state` des vorherigen Schritts.
-pose cam1_camera_pose.json \
--outDir data/homing/TIMESTAMP/ ---
## Homing-Ablauf (Script-Kette)
```
[Foto] → [1_detect] → [2_camera] → [3b_poses]
[4b Arm1]
[4b Ellbow]
[4b Arm2]
[4b Hand]
State-JSON
{x,y,z,a,b,c,e}
POST ROBOT_URL/api/state
``` ```
**Output:** Korrigierte `*_camera_pose_v8.json` + `z_correction.json` **Strategie:** `4b_revolute_angle.py` sequenziell, Link für Link von root nach tip.
X-Position (`--x-mm`) wird aus den triangulierten Board-Marker-Positionen bestimmt.
**Wann nötig:** Wenn Residuen in Schritt 4 systematisch in Z-Richtung driften.
--- ---
### Schritt 6 — Ergebnis senden ## Implementierungsplan: Homing-UI
**Was:** Vollständigen Joint-State an Roboter-Controller übermitteln. ### ⚠ Wichtig: Schritte 13b existieren bereits
**UI-Aktion:** Button `[An Roboter senden]` Die Kette **Foto → 1_detect → 2_camera → 3b_poses** ist bereits vollständig
implementiert und produktiv in `POST /api/board/run` (`server/server.js`,
ca. Zeile 520606). Sie erzeugt `<runDir>/aruco_marker_poses.json`.
**Body:** `{ x, y, z, a, b, c, e }` **Die echten, funktionierenden Aufrufe (NICHT neu erfinden):**
`POST ROBOT_URL/api/state` (oder Motion-Controller-Protokoll)
**Result Raw JSON:** ```javascript
```json // Pro Kamera geloopt (jede Kamera hat eigene NPZ):
{ runScript([SCRIPT_1, '-i', imgPath, '-npz', npzPath, '-robot', ROBOT_JSON,
"status": "ok", '-cameraId', camId, '-outDir', runDir, '--saveDebugImage']);
"timestamp": "2026-06-13T19:00:00",
"state": { "x": 180.0, "y": 23.4, "z": -12.1, "a": 5.0, "b": 0.0, "c": 0.0, "e": 0.0 }, runScript([SCRIPT_2, '-i', detJson, '-robot', ROBOT_JSON, '-outDir', runDir]);
"confidence": { "y_std_deg": 0.8, "z_std_deg": 1.2 }
// Einmal, nach allen Kameras (braucht ≥2 _camera_pose.json):
runScript([SCRIPT_3B, '--evalDir', runDir, '--robot', ROBOT_JSON]);
// → schreibt <runDir>/aruco_marker_poses.json
```
**Empfehlung:** Die Snapshot+1+2+3b-Logik aus `/api/board/run` in eine
gemeinsame Funktion `runBoardPipeline(runDir, send)` auslagern. Homing ruft
sie auf und hängt nur den 4b-Teil an. So gibt es keine Duplikate und keine
abweichenden Argumente.
---
### Phase 1 — Backend-Route `POST /api/homing/run`
**Datei:** `server/server.js` (neue Route) + `server/homingOrchestrator.js` (neue Datei)
**Ablauf als SSE-Stream:**
```javascript
// server/homingOrchestrator.js
export async function runHoming({ robotJsonPath, send }) {
// 13b: bestehende Board-Pipeline wiederverwenden
// (Foto + 1_detect + 2_camera + 3b_poses, pro Kamera geloopt)
send({ type: 'step', step: 1, text: 'Snapshot + Marker-Triangulation …' });
const runDir = await runBoardPipeline(robotJsonPath, send); // aus server.js ausgelagert
const arucoJson = path.join(runDir, 'aruco_marker_poses.json');
// 4. X-Position aus triangulierten Markern bestimmen
const xMm = estimateXFromMarkers(arucoJson); // siehe Abschnitt „X-Position"
// 4b. Gelenkwinkel sequenziell, Link für Link
const links = ['Arm1', 'Ellbow', 'Arm2', 'Hand'];
let fromState = null;
for (const link of links) {
send({ type: 'step', text: `Gelenkwinkel ${link}` });
const args = [SCRIPT_4B,
'--robot', robotJsonPath, '--aruco', arucoJson,
'--link', link,
'--output', path.join(runDir, `state_${link}.json`),
];
if (fromState) args.push('--from-state', fromState);
else args.push('--x-mm', String(xMm));
const exit = await runScript(args, send);
if (exit !== 0) { send({ type: 'error', text: `4b ${link} Exit ${exit}` }); return; }
fromState = path.join(runDir, `state_${link}.json`);
}
// Ergebnis: letztes state_*.json enthält den vollständigen accumulated_state
const finalState = JSON.parse(fs.readFileSync(fromState, 'utf8'));
send({ type: 'done', state: finalState.accumulated_state, runDir });
} }
``` ```
**Result Tree View:** Gelenk-Werte als lesbare Tabelle > **Hinweis:** `runScript()` gibt den Exit-Code zurück (nicht den Pfad).
> Der State-Pfad wird separat gemerkt (`fromState`).
--- **Route:**
```javascript
app.post('/api/homing/run', async (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
const send = (data) => res.write(`data: ${JSON.stringify(data)}\n\n`);
try {
await runHoming({ robotJsonPath: ROBOT_JSON, send });
} catch (err) {
send({ type: 'error', text: String(err) });
}
res.end();
});
## UI-Seitenstruktur (analog index.html) app.post('/api/homing/send-state', async (req, res) => {
// Sendet { x, y, z, a, b, c, e } an ROBOT_URL/api/state
``` });
┌─────────────────────────────────────────┐
│ AKTIONEN │
│ [Foto aufnehmen] [Homing berechnen] │
│ [An Roboter senden] │
├─────────────────────────────────────────┤
│ AUSGABE / LOG │
│ Schritt-für-Schritt Log aller Scripts │
├─────────────────────────────────────────┤
│ ANALYSIS & REASONING │
│ Kamera-Posen, Reprojektionsfehler, │
│ Gelenk-Schätzungen je Schritt als JSON │
├──────────────────┬──────────────────────┤
│ RESULT RAW JSON │ RESULT TREE │
│ Vollst. State-JSON │ Gelenk-Tabelle │
├─────────────────────────────────────────┤
│ SNAPSHOT CSV │
│ Tabelle aller Marker: ID, Position, │
│ Link, Residual, num_cameras │
├─────────────────────────────────────────┤
│ SNAPSHOTS (Bilder) │
│ Annotierte Kamerabilder mit Markern │
└─────────────────────────────────────────┘
``` ```
--- ---
## Offene Punkte (Voraussetzungen für Homing) ### Phase 2 — Frontend `public/homing.html`
### 🔶 A — Arm-Marker in robot.json eintragen Neue Seite (zugänglich von `index.html` via Link-Button, wie `calibration.html`).
Die Arm-Links haben noch **keine Marker** in `robot.json` (links.Arm1/Ellbow/Arm2/Hand.markers). **Sektionen (identisches Muster wie `index.html`):**
Mögliche Quellen: ```
- Aus der Y-Achsen-Kalibrierung: Marker 197, 218, 219 wurden als rotierend erkannt ┌──────────────────────────────────────────────────────┐
→ diese können mit relativen Positionen in den jeweiligen Links eingetragen werden AKTIONEN │
- Mit `4b_revolute_angle.py` lässt sich pro Link prüfen, welche Marker │ [📷 Foto & Homing berechnen] │
"zu diesem Link gehören" (paarweise Baseline-Methode) │ [✅ An Roboter senden] (disabled bis Ergebnis) │
│ Status-Badge: ○ Warte ● Läuft ✓ Fertig ✗ Fehler │
├──────────────────────────────────────────────────────┤
│ AUSGABE / LOG │
│ Schritt-für-Schritt Log aller Scripts (SSE-Stream) │
│ Fortschritt: ──── Schritt 3/6 ──── │
├──────────────────────────────────────────────────────┤
│ ANALYSIS & REASONING │
│ Zwischenergebnisse je Script als JSON │
│ { camera_reprojection_px, arm1_std_deg, … } │
├──────────────────┬───────────────────────────────────┤
│ RESULT RAW JSON │ RESULT TREE VIEW │
│ { │ x (Slider): 180.0 mm │
│ "x": 180.0, │ y (Arm1): +23.4° │
│ "y": 23.4, │ z (Ellbow): -12.1° │
│ ... │ a (Arm2): +5.0° │
│ } │ b (Hand): 0.0° │
│ │ c (Palm): 0.0° │
│ │ e (Greifer): 0.0 mm │
├──────────────────┴───────────────────────────────────┤
│ SNAPSHOT CSV (Marker-Tabelle) │
│ ID │ Link │ x mm │ y mm │ z mm │ Residual │ │
│ 218 │ Arm2 │ 229.1 │ 118.5 │ 48.3 │ 2.1 mm │ │
├──────────────────────────────────────────────────────┤
│ SNAPSHOTS (annotierte Kamerabilder) │
│ [cam0] [cam1] [cam2] │
└──────────────────────────────────────────────────────┘
```
**Aktion:** Für jedes Arm-Segment mind. 2 Marker bestimmen und in robot.json eintragen. **Schlüssel-Implementierungsdetails:**
### 🔶 B — Arm1.jointToParent.origin[Y, Z] finalisieren ```javascript
// homing.js (client)
Aus der Arm1-Y-Achsen-Kalibrierung (calibration_arm.html): // SSE-Stream vom Backend empfangen
- Berechneter Wert: Y ≈ 115.6 mm, Z ≈ 50.3 mm async function runHoming() {
- Aktueller Wert in robot.json: origin = [110, 108, 45] const response = await fetch('/api/homing/run', { method: 'POST' });
- **Aktion:** Button „Joint-Origin Y/Z übernehmen" klicken → schreibt in robot.json await readSseStream(response, appendLog, (evt) => {
if (evt.type === 'step') { updateProgress(evt); }
if (evt.type === 'analysis') { showAnalysis(evt.data); }
if (evt.type === 'done') {
showResult(evt.state);
enableSendButton(evt.state);
}
});
}
### 🔶 C — X-Position (Slider) im Homing // Ergebnis an Roboter senden
async function sendToRobot(state) {
Die Slider-Position `x` ist für `4b_revolute_angle.py --x-mm` nötig. await fetch('/api/homing/send-state', {
Optionen: method: 'POST',
1. Mechanischer Anschlag / Encoder → immer bekannt body: JSON.stringify({ state }),
2. Aus Board-Markern schätzen (funktioniert wenn A0-Marker sichtbar) });
3. Manuell eingeben (Fallback) }
```
### 🔶 D — Homing-Seite implementieren
Eine neue `homing.html`-Seite (oder Tab in `calibration.html`) die:
- Die Script-Kette 1 → 2 → 3b → 4b(je Link) → (5) als SSE-Stream orchestriert
- Alle Ausgabe-Sektionen wie in index.html befüllt
- „An Roboter senden" Button mit finalem State-JSON verdrahtet
--- ---
## Script-Abhängigkeitsgraph ### Phase 3 — robot.json via Driver-API
**Voraussetzung:** ROBOT_URL ist konfiguriert und der Driver hat `GET /api/robot/config`.
**Implementierung in `server.js`:**
```javascript
// Beim Start oder on-demand: robot.json vom Driver laden
async function fetchRobotConfig() {
if (!ROBOT_URL) return; // lokale Datei reicht
const res = await fetch(new URL('/api/robot/config', ROBOT_URL));
if (!res.ok) return; // Fallback auf lokale Datei
const data = await res.json();
// Temporär in data/robot/robot_live.json cachen
await fs.writeFile(ROBOT_JSON_LIVE, JSON.stringify(data, null, 2));
}
```
**Auswirkung:** Nur `ROBOT_JSON` Variable ändern — alle Scripts bekommen
automatisch die aktuelle Konfiguration.
---
## Reihenfolge der Implementierung
``` ```
Kamera-Snapshot [Jetzt] Arm-Marker eintragen (Nutzer)
→ Arm1 Joint-Origin Button klicken (bereits ausführbar)
[1] detect_aruco → cam*_aruco_detection.json [0] Refactor: Snapshot+1+2+3b aus /api/board/run
in runBoardPipeline(runDir, send) auslagern
→ wird von Board-Run UND Homing genutzt
[2] estimate_camera → cam*_camera_pose.json
│ (aus Board-Markern) [1] POST /api/homing/run + homingOrchestrator.js
→ runBoardPipeline() + 4b-Schleife als SSE
[3b] corner_marker_poses → aruco_marker_poses.json → SCRIPT_4B-Konstante in server.js ergänzen
│ (alle Marker trianguliert) → Minimale UI: nur Log + Raw JSON
├──────────────────────────────────────┐
▼ ▼ [2] public/homing.html
[4b] revolute Arm1 [4] state_v6 (alternativ) → Vollständige UI mit allen Sektionen
→ Link von index.html
[4b] revolute Ellbow ──► [5] camera_z_refine (optional, nutzt Ellbow)
[3] POST /api/homing/send-state
[4b] revolute Arm2 → ROBOT_URL/api/state aufrufen (analog zu robotActions.js)
[4b] revolute Hand [4] robot.json via Driver-API (wenn ROBOT_URL verfügbar)
→ Nur wenn Driver den Endpunkt implementiert
State-JSON {x, y, z, a, b, c, e}
→ Roboter-Controller
``` ```
--- ---
## Status-Tabelle ## Status-Tabelle
| Schritt | Script | Status | Blockiert durch | | Schritt | Was | Status |
|---------|--------|--------|-----------------| |---------|-----|--------|
| 1 Snapshot | Webcam-API | ✅ vorhanden | — | | Scripts 1, 2, 3b, 4b | Homing-Scripts | ✅ vorhanden |
| 2 Marker erkennen | `1_detect_aruco.py` | ✅ vorhanden | — | | Kalibrierung | Kamera, Board, X-Achse | ✅ fertig |
| 3 Kamera-Pose | `2_estimate_camera.py` | ✅ vorhanden | — | | Arm1 Joint-Origin | Button in calibration_arm.html | ✅ **ausführbar** |
| 3b Triangulation | `3b_corner_marker_poses.py` | ✅ vorhanden | — | | Arm-Marker | robot.json links.Arm1/… .markers | 🔶 Nutzer trägt ein |
| 4 State-Schätzung | `4_robotState_estimation_v6.py` | ✅ vorhanden | Arm-Marker fehlen | | `/api/homing/run` | Backend-Orchestrierung | ❌ zu implementieren |
| 4b Sequenziell | `4b_revolute_angle.py` | ✅ vorhanden | Arm-Marker fehlen | | `homing.html` | Frontend-UI | ❌ zu implementieren |
| 5 Z-Verfeinern | `5_camera_z_refine.py` | ✅ vorhanden | Ellbow-Marker fehlen | | `/api/homing/send-state` | State an Roboter | ❌ zu implementieren |
| **Arm1-Origin** | calibration_arm.html | 🔶 bereit | Joint-Origin Y/Z übernehmen | | robot.json via API | Driver-Integration | ⏳ nach allem anderen |
| **Arm-Marker** | robot.json | ❌ fehlen | manuelle Zuordnung |
| **Homing-UI** | homing.html | ❌ fehlt | erst nach Markern sinnvoll |