Make machine configuration authoritative and restore native checkpoints exactly
@@ -1,5 +1,7 @@
|
||||
# LinuxCNC Web/WASM
|
||||
|
||||
当前配置驱动改造的重启接续入口:[`nextTask-hardcoding.md`](nextTask-hardcoding.md)。
|
||||
|
||||
五轴机床 INI 参数化设计(XYZAC / XYZBC)的当前工作入口:
|
||||
[`项目文档、任务矩阵与验收证据`](docs/trt-parametric/README.md)。本专项按该索引接续。
|
||||
|
||||
|
||||
18
coverage/configuration/browser.report.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"report_id": "configurable-ui-browser-v1",
|
||||
"status": "verified",
|
||||
"default_machine": "xyzbc-trt",
|
||||
"entrypoint": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/configured-machine.ini",
|
||||
"program": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/configured-preview.ngc",
|
||||
"increments": [
|
||||
"0.25",
|
||||
"2.5"
|
||||
],
|
||||
"feed_max": 1.3,
|
||||
"colors": {
|
||||
"purple": 143449,
|
||||
"green": 85
|
||||
},
|
||||
"configured_icons": 24,
|
||||
"browser_errors": []
|
||||
}
|
||||
BIN
coverage/configuration/configured-ui.png
Normal file
|
After Width: | Height: | Size: 240 KiB |
76
coverage/configuration/input-audit.report.json
Normal file
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"report_id": "configuration-input-audit-v1",
|
||||
"status": "verified",
|
||||
"scope": "configurable inputs; not a zero-literals claim",
|
||||
"checks": {
|
||||
"machine_catalog_parses": true,
|
||||
"resource_catalog_parses": true,
|
||||
"settings_parse": true,
|
||||
"worker_reparses_ini": true,
|
||||
"axis_limits_from_ini": true,
|
||||
"joint_limits_from_ini": true,
|
||||
"no_kinematics_example_limits": true,
|
||||
"no_loader_fixed_timing": true,
|
||||
"no_loader_axis3_override": true,
|
||||
"native_axis_dynamics_input": true,
|
||||
"no_native_xyzac_speed_override": true,
|
||||
"native_acceleration_upper_bound": true,
|
||||
"model_uses_visual_configuration": true,
|
||||
"machine_path_color_configured": true
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"path": "web/workspace/machine-catalog.json",
|
||||
"sha256": "b71a4cf6f5517710c8cc7ed496b3d5d8d7850c07ab4dc5096548407f93dd9130"
|
||||
},
|
||||
{
|
||||
"path": "web/workspace/simulation-resources.json",
|
||||
"sha256": "872a0e52dc4f8725d623ad47f3cbb446b6cb257793ce78ca1f8f0bcf1ee0966b"
|
||||
},
|
||||
{
|
||||
"path": "web/workspace/web-settings.json",
|
||||
"sha256": "c9f7da5ef8ec0d17ee2e9e9f55e71acc051eb4afe75b85a961a18599f28c8a82"
|
||||
},
|
||||
{
|
||||
"path": "web/workspace/trt-configuration.mjs",
|
||||
"sha256": "5ede37c1066545da1825b98ccedd82c22dad7f8f311c8ea0112595a916e6b432"
|
||||
},
|
||||
{
|
||||
"path": "web/worker/sim-session.mjs",
|
||||
"sha256": "07c40bb0e8603291cdea245be03c8afc7d9b6582452433ed4f5eac413add988f"
|
||||
},
|
||||
{
|
||||
"path": "web/worker/wasm-linuxcnc-tp.mjs",
|
||||
"sha256": "50616c4d75e3693f06e0095c9df76671f7af9d417e351a68796bdba92dcbe5f7"
|
||||
},
|
||||
{
|
||||
"path": "wasm-core/src/sim_linuxcnc_tp.c",
|
||||
"sha256": "2db45da43cd7951a172d118c3dd346a2d5182eae29f0bbe2abd9b9d5a645da26"
|
||||
},
|
||||
{
|
||||
"path": "web/worker/wasm-xyzac-kinematics.mjs",
|
||||
"sha256": "7453c58a220b31a1c74ff2e2f9f3d6b7d16cc84973b9ea5be17d06fa179af37f"
|
||||
},
|
||||
{
|
||||
"path": "web/workspace/sim-case-loader.mjs",
|
||||
"sha256": "d6399f2fcb053fc625933d72118b82e7f6eff73df7dc02a606fe45f238b06207"
|
||||
},
|
||||
{
|
||||
"path": "web/react/src/xyzac-machine-model.ts",
|
||||
"sha256": "1d22af1384e4e26adad8bed122fabd17ecfb046d0320c920ca1c7a385c40f5b6"
|
||||
},
|
||||
{
|
||||
"path": "web/react/src/XyzacMachineView.tsx",
|
||||
"sha256": "d0f42c020a8da8708323fc778d206460fa1ea02d114d5df2a3c2eaf85f138f3a"
|
||||
}
|
||||
],
|
||||
"wasm_sha256": "213b55e64b45c14df5c40e59827fa8bb4903135cd788664bb08263f08a49738f",
|
||||
"retained": [
|
||||
"mathematical constants and tolerances",
|
||||
"ABI layouts, enums, and capacities",
|
||||
"source model mesh topology",
|
||||
"CSS/icon assets",
|
||||
"test fixtures and historical evidence",
|
||||
"upstream LinuxCNC and dependency locks"
|
||||
]
|
||||
}
|
||||
1679
coverage/configuration/parametric-regression.report.json
Normal file
657
coverage/configuration/rtcp-jog-browser.report.json
Normal file
@@ -0,0 +1,657 @@
|
||||
{
|
||||
"report_id": "trt-rtcp-jog-browser-v1",
|
||||
"status": "verified",
|
||||
"machines": [
|
||||
{
|
||||
"coordinates": "XYZAC",
|
||||
"rows": [
|
||||
{
|
||||
"axis": "a",
|
||||
"before": {
|
||||
"joints": [
|
||||
5.066649999999981,
|
||||
4.066650000000003,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": 0,
|
||||
"y_offset": 20,
|
||||
"z_offset": 10,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 64,
|
||||
"rendered_tick": 64,
|
||||
"tool_tip": [
|
||||
0,
|
||||
2,
|
||||
0
|
||||
]
|
||||
},
|
||||
"moving": {
|
||||
"joints": [
|
||||
5.066649999999981,
|
||||
4.841821345591999,
|
||||
-1.1400746004481288,
|
||||
4.199999999999992,
|
||||
0
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": 0,
|
||||
"y_offset": 20,
|
||||
"z_offset": 10,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 88,
|
||||
"rendered_tick": 88,
|
||||
"tool_tip": [
|
||||
0,
|
||||
2,
|
||||
0
|
||||
],
|
||||
"tcp_error": 1.3877787807814457e-17
|
||||
},
|
||||
"stopped": {
|
||||
"joints": [
|
||||
5.066649999999981,
|
||||
5.19921919111046,
|
||||
-1.6107075438884007,
|
||||
6.000000000000007,
|
||||
0
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": 0,
|
||||
"y_offset": 20,
|
||||
"z_offset": 10,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 99,
|
||||
"rendered_tick": 99,
|
||||
"tool_tip": [
|
||||
0,
|
||||
2,
|
||||
0
|
||||
],
|
||||
"tcp_error": 2.2247786310271853e-16
|
||||
}
|
||||
},
|
||||
{
|
||||
"axis": "c",
|
||||
"before": {
|
||||
"joints": [
|
||||
5.066649999999981,
|
||||
5.19921919111046,
|
||||
-1.6107075438884007,
|
||||
6.000000000000007,
|
||||
0
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": 0,
|
||||
"y_offset": 20,
|
||||
"z_offset": 10,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 99,
|
||||
"rendered_tick": 99,
|
||||
"tool_tip": [
|
||||
0,
|
||||
2,
|
||||
0
|
||||
]
|
||||
},
|
||||
"moving": {
|
||||
"joints": [
|
||||
4.266433017896709,
|
||||
6.027611642587745,
|
||||
-1.5236399887760221,
|
||||
6.000000000000007,
|
||||
10.199999999999976
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": 0,
|
||||
"y_offset": 20,
|
||||
"z_offset": 10,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 143,
|
||||
"rendered_tick": 136,
|
||||
"tool_tip": [
|
||||
0,
|
||||
2,
|
||||
0
|
||||
],
|
||||
"tcp_error": 2.2473876566261383e-16
|
||||
},
|
||||
"stopped": {
|
||||
"joints": [
|
||||
4.110427463793882,
|
||||
6.158484995689042,
|
||||
-1.5098846450816552,
|
||||
6.000000000000007,
|
||||
11.999999999999938
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": 0,
|
||||
"y_offset": 20,
|
||||
"z_offset": 10,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 154,
|
||||
"rendered_tick": 154,
|
||||
"tool_tip": [
|
||||
0,
|
||||
2,
|
||||
0
|
||||
],
|
||||
"tcp_error": 6.938893903907228e-18
|
||||
}
|
||||
},
|
||||
{
|
||||
"axis": "a",
|
||||
"before": {
|
||||
"joints": [
|
||||
4.110427463793882,
|
||||
6.158484995689042,
|
||||
-1.5098846450816552,
|
||||
6.000000000000007,
|
||||
11.999999999999938
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": 0,
|
||||
"y_offset": 20,
|
||||
"z_offset": 10,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 154,
|
||||
"rendered_tick": 154,
|
||||
"tool_tip": [
|
||||
0,
|
||||
2,
|
||||
0
|
||||
]
|
||||
},
|
||||
"moving": {
|
||||
"joints": [
|
||||
4.110427463793882,
|
||||
9.152485181578204,
|
||||
-4.3665028598393185,
|
||||
19.19999999999992,
|
||||
11.999999999999938
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": 0,
|
||||
"y_offset": 20,
|
||||
"z_offset": 10,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 208,
|
||||
"rendered_tick": 202,
|
||||
"tool_tip": [
|
||||
0,
|
||||
2,
|
||||
0
|
||||
],
|
||||
"tcp_error": 1.3877787807814457e-17
|
||||
},
|
||||
"stopped": {
|
||||
"joints": [
|
||||
4.110427463793882,
|
||||
9.532273556710663,
|
||||
-4.645534652791378,
|
||||
20.69999999999989,
|
||||
11.999999999999938
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": 0,
|
||||
"y_offset": 20,
|
||||
"z_offset": 10,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 218,
|
||||
"rendered_tick": 218,
|
||||
"tool_tip": [
|
||||
0,
|
||||
2,
|
||||
0
|
||||
],
|
||||
"tcp_error": 4.445768331151997e-16
|
||||
}
|
||||
},
|
||||
{
|
||||
"axis": "c",
|
||||
"before": {
|
||||
"joints": [
|
||||
4.110427463793882,
|
||||
9.532273556710663,
|
||||
-4.645534652791378,
|
||||
20.69999999999989,
|
||||
11.999999999999938
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": 0,
|
||||
"y_offset": 20,
|
||||
"z_offset": 10,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 218,
|
||||
"rendered_tick": 218,
|
||||
"tool_tip": [
|
||||
0,
|
||||
2,
|
||||
0
|
||||
]
|
||||
},
|
||||
"moving": {
|
||||
"joints": [
|
||||
3.8414816180013283,
|
||||
9.727059259543312,
|
||||
-4.571931269146798,
|
||||
20.69999999999989,
|
||||
14.999999999999936
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": 0,
|
||||
"y_offset": 20,
|
||||
"z_offset": 10,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 238,
|
||||
"rendered_tick": 238,
|
||||
"tool_tip": [
|
||||
0,
|
||||
2,
|
||||
0
|
||||
],
|
||||
"tcp_error": 1.3877787807814457e-17
|
||||
},
|
||||
"stopped": {
|
||||
"joints": [
|
||||
3.6750116751266004,
|
||||
9.837515099025248,
|
||||
-4.53019348546481,
|
||||
20.69999999999989,
|
||||
16.799999999999933
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": 0,
|
||||
"y_offset": 20,
|
||||
"z_offset": 10,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 249,
|
||||
"rendered_tick": 249,
|
||||
"tool_tip": [
|
||||
0,
|
||||
2,
|
||||
0
|
||||
],
|
||||
"tcp_error": 4.443601777255714e-16
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"coordinates": "XYZBC",
|
||||
"rows": [
|
||||
{
|
||||
"axis": "b",
|
||||
"before": {
|
||||
"joints": [
|
||||
5.0666499999999814,
|
||||
3.666650000000003,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": -20,
|
||||
"y_offset": 0,
|
||||
"z_offset": -15,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 311,
|
||||
"rendered_tick": 311,
|
||||
"tool_tip": [
|
||||
0,
|
||||
1.5,
|
||||
0
|
||||
]
|
||||
},
|
||||
"moving": {
|
||||
"joints": [
|
||||
6.166264319539167,
|
||||
3.666650000000003,
|
||||
-2.0129466859294602,
|
||||
4.499999999999995,
|
||||
0
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": -20,
|
||||
"y_offset": 0,
|
||||
"z_offset": -15,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 336,
|
||||
"rendered_tick": 330,
|
||||
"tool_tip": [
|
||||
0,
|
||||
1.4879917702595602,
|
||||
0
|
||||
],
|
||||
"tcp_error": 2.7755575615628914e-17
|
||||
},
|
||||
"stopped": {
|
||||
"joints": [
|
||||
6.56128605042624,
|
||||
3.666650000000003,
|
||||
-2.8412572372826563,
|
||||
6.30000000000001,
|
||||
0
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": -20,
|
||||
"y_offset": 0,
|
||||
"z_offset": -15,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 347,
|
||||
"rendered_tick": 347,
|
||||
"tool_tip": [
|
||||
0,
|
||||
1.4715874276271734,
|
||||
0
|
||||
],
|
||||
"tcp_error": 5.551115123125783e-17
|
||||
}
|
||||
},
|
||||
{
|
||||
"axis": "c",
|
||||
"before": {
|
||||
"joints": [
|
||||
6.56128605042624,
|
||||
3.666650000000003,
|
||||
-2.8412572372826563,
|
||||
6.30000000000001,
|
||||
0
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": -20,
|
||||
"y_offset": 0,
|
||||
"z_offset": -15,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 347,
|
||||
"rendered_tick": 347,
|
||||
"tool_tip": [
|
||||
0,
|
||||
1.4715874276271734,
|
||||
0
|
||||
]
|
||||
},
|
||||
"moving": {
|
||||
"joints": [
|
||||
6.259816863284065,
|
||||
4.05287171910206,
|
||||
-2.807974729167359,
|
||||
6.30000000000001,
|
||||
4.499999999999995
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": -20,
|
||||
"y_offset": 0,
|
||||
"z_offset": -15,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 372,
|
||||
"rendered_tick": 372,
|
||||
"tool_tip": [
|
||||
0,
|
||||
1.4719202527083264,
|
||||
0
|
||||
],
|
||||
"tcp_error": 6.938893903907228e-18
|
||||
},
|
||||
"stopped": {
|
||||
"joints": [
|
||||
6.130945648374414,
|
||||
4.200492284609181,
|
||||
-2.793747214585462,
|
||||
6.30000000000001,
|
||||
6.30000000000001
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": -20,
|
||||
"y_offset": 0,
|
||||
"z_offset": -15,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 383,
|
||||
"rendered_tick": 383,
|
||||
"tool_tip": [
|
||||
0,
|
||||
1.4720625278541453,
|
||||
0
|
||||
],
|
||||
"tcp_error": 2.2887833992611187e-16
|
||||
}
|
||||
},
|
||||
{
|
||||
"axis": "b",
|
||||
"before": {
|
||||
"joints": [
|
||||
6.130945648374414,
|
||||
4.200492284609181,
|
||||
-2.793747214585462,
|
||||
6.30000000000001,
|
||||
6.30000000000001
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": -20,
|
||||
"y_offset": 0,
|
||||
"z_offset": -15,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 383,
|
||||
"rendered_tick": 383,
|
||||
"tool_tip": [
|
||||
0,
|
||||
1.4720625278541453,
|
||||
0
|
||||
]
|
||||
},
|
||||
"moving": {
|
||||
"joints": [
|
||||
7.840448318636497,
|
||||
4.200492284609181,
|
||||
-7.46817651846061,
|
||||
16.199999999999992,
|
||||
6.30000000000001
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": -20,
|
||||
"y_offset": 0,
|
||||
"z_offset": -15,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 426,
|
||||
"rendered_tick": 420,
|
||||
"tool_tip": [
|
||||
0,
|
||||
1.4340259659607903,
|
||||
0
|
||||
],
|
||||
"tcp_error": 0
|
||||
},
|
||||
"stopped": {
|
||||
"joints": [
|
||||
8.063291030797899,
|
||||
4.200492284609181,
|
||||
-8.346382633448467,
|
||||
17.999999999999954,
|
||||
6.30000000000001
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": -20,
|
||||
"y_offset": 0,
|
||||
"z_offset": -15,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 437,
|
||||
"rendered_tick": 437,
|
||||
"tool_tip": [
|
||||
0,
|
||||
1.4165361736655155,
|
||||
0
|
||||
],
|
||||
"tcp_error": 2.2887833992611187e-16
|
||||
}
|
||||
},
|
||||
{
|
||||
"axis": "c",
|
||||
"before": {
|
||||
"joints": [
|
||||
8.063291030797899,
|
||||
4.200492284609181,
|
||||
-8.346382633448467,
|
||||
17.999999999999954,
|
||||
6.30000000000001
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": -20,
|
||||
"y_offset": 0,
|
||||
"z_offset": -15,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 437,
|
||||
"rendered_tick": 437,
|
||||
"tool_tip": [
|
||||
0,
|
||||
1.4165361736655155,
|
||||
0
|
||||
]
|
||||
},
|
||||
"moving": {
|
||||
"joints": [
|
||||
7.5982993210492396,
|
||||
4.6836006491845,
|
||||
-8.195297668366113,
|
||||
17.999999999999954,
|
||||
12.60000000000002
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": -20,
|
||||
"y_offset": 0,
|
||||
"z_offset": -15,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 468,
|
||||
"rendered_tick": 461,
|
||||
"tool_tip": [
|
||||
0,
|
||||
1.41752527642095,
|
||||
0
|
||||
],
|
||||
"tcp_error": 2.2247786310271853e-16
|
||||
},
|
||||
"stopped": {
|
||||
"joints": [
|
||||
7.456439115506413,
|
||||
4.811480064556107,
|
||||
-8.1492044934736,
|
||||
17.999999999999954,
|
||||
14.40000000000003
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": -20,
|
||||
"y_offset": 0,
|
||||
"z_offset": -15,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 479,
|
||||
"rendered_tick": 474,
|
||||
"tool_tip": [
|
||||
0,
|
||||
1.418507955065264,
|
||||
0
|
||||
],
|
||||
"tcp_error": 1.5515838457795457e-17
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"browser_errors": []
|
||||
}
|
||||
3589
coverage/configuration/rtcp-jog-regression.report.json
Normal file
BIN
coverage/configuration/rtcp-jog-xyzac.png
Normal file
|
After Width: | Height: | Size: 314 KiB |
BIN
coverage/configuration/rtcp-jog-xyzbc.png
Normal file
|
After Width: | Height: | Size: 308 KiB |
129
coverage/configuration/subpath.report.json
Normal file
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"report_id": "subpath-deployment-v1",
|
||||
"status": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/boat-xyzac.ngc",
|
||||
"url": "http://127.0.0.1:5175/dist/",
|
||||
"machine": "xyzac-trt",
|
||||
"icons": [
|
||||
{
|
||||
"src": "http://127.0.0.1:5175/dist/linuxcnc/share/axis/images/axis-24x24.png",
|
||||
"loaded": true
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5175/dist/linuxcnc/share/axis/images/tool_estop.gif",
|
||||
"loaded": true
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5175/dist/linuxcnc/share/axis/images/tool_power.gif",
|
||||
"loaded": true
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5175/dist/linuxcnc/share/axis/images/tool_open.gif",
|
||||
"loaded": true
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5175/dist/linuxcnc/share/axis/images/tool_verify.gif",
|
||||
"loaded": true
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5175/dist/linuxcnc/share/axis/images/tool_reload.gif",
|
||||
"loaded": true
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5175/dist/linuxcnc/share/axis/images/tool_run.gif",
|
||||
"loaded": true
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5175/dist/linuxcnc/share/axis/images/tool_step.gif",
|
||||
"loaded": true
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5175/dist/linuxcnc/share/axis/images/tool_pause.gif",
|
||||
"loaded": true
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5175/dist/linuxcnc/share/axis/images/tool_stop.gif",
|
||||
"loaded": true
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5175/dist/linuxcnc/share/axis/images/tool_blockdelete.gif",
|
||||
"loaded": true
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5175/dist/linuxcnc/share/axis/images/tool_optpause.gif",
|
||||
"loaded": true
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5175/dist/linuxcnc/share/axis/images/tool_zoomin.gif",
|
||||
"loaded": true
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5175/dist/linuxcnc/share/axis/images/tool_zoomout.gif",
|
||||
"loaded": true
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5175/dist/linuxcnc/share/axis/images/tool_axis_z.gif",
|
||||
"loaded": true
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5175/dist/linuxcnc/share/axis/images/tool_axis_z2.gif",
|
||||
"loaded": true
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5175/dist/linuxcnc/share/axis/images/tool_axis_x.gif",
|
||||
"loaded": true
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5175/dist/linuxcnc/share/axis/images/tool_axis_y.gif",
|
||||
"loaded": true
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5175/dist/linuxcnc/share/axis/images/tool_axis_y2.png",
|
||||
"loaded": true
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5175/dist/linuxcnc/share/axis/images/tool_axis_p.gif",
|
||||
"loaded": true
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5175/dist/linuxcnc/share/axis/images/tool_rotate.gif",
|
||||
"loaded": true
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5175/dist/linuxcnc/share/axis/images/tool_clear.gif",
|
||||
"loaded": true
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5175/dist/linuxcnc/share/axis/images/spindle_ccw.gif",
|
||||
"loaded": true
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5175/dist/linuxcnc/share/axis/images/spindle_cw.gif",
|
||||
"loaded": true
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5175/dist/linuxcnc/share/axis/images/tool_zoomin.gif",
|
||||
"loaded": true
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5175/dist/linuxcnc/share/axis/images/tool_zoomout.gif",
|
||||
"loaded": true
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5175/dist/linuxcnc/share/axis/images/tool_axis_z.gif",
|
||||
"loaded": true
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5175/dist/linuxcnc/share/axis/images/tool_verify.gif",
|
||||
"loaded": true
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5175/dist/linuxcnc/share/axis/images/tool_reload.gif",
|
||||
"loaded": true
|
||||
},
|
||||
{
|
||||
"src": "http://127.0.0.1:5175/dist/linuxcnc/share/axis/images/tool_open.gif",
|
||||
"loaded": true
|
||||
}
|
||||
],
|
||||
"errors": []
|
||||
}
|
||||
1781
coverage/configuration/trt-runtime.report.json
Normal file
65
coverage/recovery/acceptance.report.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"report_id": "cfg-r01-trt-r01-acceptance-v1",
|
||||
"status": "verified",
|
||||
"tasks": [
|
||||
"CFG-R01",
|
||||
"TRT-R01"
|
||||
],
|
||||
"native_build_id": "fff6a3970ae13c6a575d90373332f25c90034318d4758616dee3bedd227cfc89",
|
||||
"wasm_sha256": "aa0f5d551e42859eef3b37aee9079f09ae04e668819da4e62aee2b74522e8bc6",
|
||||
"checkpoint_sizes": {
|
||||
"sim_core": 24,
|
||||
"sim_protocol": 12,
|
||||
"sim_event": 786448,
|
||||
"sim_program": 49496,
|
||||
"sim_linuxcnc_tp": 47304,
|
||||
"sim_hal": 49544
|
||||
},
|
||||
"continuation_samples": 120,
|
||||
"first_divergent_tick": null,
|
||||
"boundaries": [
|
||||
"matching native build and machine configuration required",
|
||||
"old snapshots lacking native state are rejected",
|
||||
"TRT checkpoint event tails require explicit subsequent commands",
|
||||
"backward public recovery requires a fresh transport session",
|
||||
"OPFS retains its existing quota; no physical crash or cross-browser parity claim"
|
||||
],
|
||||
"evidence": [
|
||||
{
|
||||
"path": "coverage/recovery/axis3-selected-case.report.json",
|
||||
"sha256": "24a017e622073950726652399da8959fb695097b1f26e9a88c3357269f09b96e"
|
||||
},
|
||||
{
|
||||
"path": "coverage/recovery/axis-gates.report.json",
|
||||
"sha256": "e92a7ee1b9545cffbabe8a4933eca73330703746e1169627cef1fd838d19fdd4"
|
||||
},
|
||||
{
|
||||
"path": "coverage/recovery/axis3-checkpoint.report.json",
|
||||
"sha256": "638b13148f0e82b3b78a9ff9b8a91ead5c144ff0c3ebec4cf0ce9df913c2989b"
|
||||
},
|
||||
{
|
||||
"path": "coverage/recovery/checkpoint-state.report.json",
|
||||
"sha256": "12b7792cd312e327154d4b66a13d869906a4c53a9143dd0d04493f687452da3b"
|
||||
},
|
||||
{
|
||||
"path": "coverage/recovery/exact-checkpoint.report.json",
|
||||
"sha256": "bda234634bcfbaef24dfa7291cf8650c67d33805a3ceefe96e251fdb2a64059e"
|
||||
},
|
||||
{
|
||||
"path": "coverage/recovery/power-off.report.json",
|
||||
"sha256": "d0f18fbe4749d2a89fac242e99596c125ad8aa056067d18b59589519d83c6b46"
|
||||
},
|
||||
{
|
||||
"path": "coverage/recovery/storage-bridge.report.json",
|
||||
"sha256": "e9489615ac04e8f6186f340c7eecf78d5aca32db715b8a0b73ef7bef693d0dc6"
|
||||
},
|
||||
{
|
||||
"path": "coverage/recovery/browser-reload.report.json",
|
||||
"sha256": "108b1da696ac1cc4666301c8b5b1d478d3b6853372640428192211a03771e993"
|
||||
},
|
||||
{
|
||||
"path": "coverage/recovery/browser-reload-xyzbc-trt.report.json",
|
||||
"sha256": "d136b7985b0c7f0d0215cb04f9b4a35aa239e50534ebd65f80e57af278cf1f9f"
|
||||
}
|
||||
]
|
||||
}
|
||||
1
coverage/recovery/axis-gates.report.json
Normal file
@@ -0,0 +1 @@
|
||||
{"schema":"axis-contract-sample.v1","status":"verified","commands":25,"matrix":25,"blocked_reasons":["axis_mdi_queue_full","axis_estop_active"]}
|
||||
1
coverage/recovery/axis3-checkpoint.report.json
Normal file
@@ -0,0 +1 @@
|
||||
{"schema":"wasm-motion-checkpoint-t3.v1","status":"verified","case":"linuxcnc/configs/sim/axis/vismach/3axis-tutorial/3axis-tutorial.ini","checkpoint_hash":"fnv1a64:0fec51679b74f35c","continuous_hash":"fnv1a64:6ad7c87921fa2384","recovered_hash":"fnv1a64:6ad7c87921fa2384","endpoint_equal":true,"trace_import":"verified","checkpoint_queue_depth":1,"blocked_damaged_checkpoint":"blocked"}
|
||||
1
coverage/recovery/axis3-selected-case.report.json
Normal file
@@ -0,0 +1 @@
|
||||
{"schema":"axis3-selected-case-parity.v1","status":"verified","case":"linuxcnc/configs/sim/axis/vismach/3axis-tutorial/3axis-tutorial.ini","machine":{"type":"cartesian_vismach","axes":["X","Y","Z"],"joints":3,"kinematics":"trivkins","model":"3axis-tutorial"},"exact_files":["linuxcnc/configs/sim/axis/vismach/3axis-tutorial/3axis-tutorial.ini","linuxcnc/configs/sim/axis/vismach/3axis-tutorial/3axis-tutorial.hal","linuxcnc/configs/sim/axis/vismach/3axis-tutorial/3axis-tutorial","linuxcnc/configs/sim/axis/vismach/3axis-tutorial/3axis-tutorial_mm.var","linuxcnc/configs/nc_files/3dtest.ngc","linuxcnc/configs/sim/axis/vismach/3axis-tutorial/tool.tbl","linuxcnc/linuxcnc.gif"],"loader":{"fetched":7,"missing":0,"state_hash":"fnv1a64:94e21a5c7a684e63"},"startup":{"state":"TASK_READY","hal_blocked":0},"operation_sequence":["HOME","JOG_START","SIM_STEP","JOG_STOP","SET_OVERRIDE","PROGRAM_LOAD","PROGRAM_RUN","CHECKPOINT","SPINDLE_COMMAND","COOLANT_COMMAND","TOOL_PREPARE","TOOL_CHANGE","ESTOP","IO_RECOVER","RECOVER"],"observed":{"jog_x":0.19999999999999998,"program_status":"completed","checkpoint_hash":"fnv1a64:9d53b245b2779038","recovery":"recovered","final_hash":"fnv1a64:47a5f0e6ae76a4af"}}
|
||||
BIN
coverage/recovery/browser-reload-failure.png
Normal file
|
After Width: | Height: | Size: 235 KiB |
BIN
coverage/recovery/browser-reload-xyzbc-trt.png
Normal file
|
After Width: | Height: | Size: 236 KiB |
39
coverage/recovery/browser-reload-xyzbc-trt.report.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"report_id": "checkpoint-browser-reload-v1",
|
||||
"machine": "xyzbc-trt",
|
||||
"status": "verified",
|
||||
"before": [
|
||||
2.4666500000000022,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"saved": {
|
||||
"schema": "linuxcnc-ui-temp-json.v1",
|
||||
"status": "verified",
|
||||
"storage": "opfs",
|
||||
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
|
||||
"key": "xyzbc-trt-power-off",
|
||||
"generation": 1,
|
||||
"slot": "state-b.json",
|
||||
"state_hash": "fnv1a64:9dc45f4891e8b147",
|
||||
"bytes": 102567
|
||||
},
|
||||
"after": [
|
||||
2.4666500000000022,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"preview_segments": 1,
|
||||
"continued": [
|
||||
2.5666500000000023,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"browser_errors": []
|
||||
}
|
||||
BIN
coverage/recovery/browser-reload.png
Normal file
|
After Width: | Height: | Size: 235 KiB |
39
coverage/recovery/browser-reload.report.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"report_id": "checkpoint-browser-reload-v1",
|
||||
"machine": "xyzac-trt",
|
||||
"status": "verified",
|
||||
"before": [
|
||||
2.266650000000002,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"saved": {
|
||||
"schema": "linuxcnc-ui-temp-json.v1",
|
||||
"status": "verified",
|
||||
"storage": "opfs",
|
||||
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
|
||||
"key": "xyzac-trt-power-off",
|
||||
"generation": 1,
|
||||
"slot": "state-b.json",
|
||||
"state_hash": "fnv1a64:bc9feb6862938390",
|
||||
"bytes": 103146
|
||||
},
|
||||
"after": [
|
||||
2.266650000000002,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"preview_segments": 1,
|
||||
"continued": [
|
||||
2.366650000000002,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"browser_errors": []
|
||||
}
|
||||
168
coverage/recovery/build.log
Normal file
@@ -0,0 +1,168 @@
|
||||
configure: cmake -S /home/mes006/linuxcncWebWASM/wasm-core -B /home/mes006/linuxcncWebWASM/build-wasm -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=/usr/share/emscripten/cmake/Modules/Platform/Emscripten.cmake -DCMAKE_CROSSCOMPILING_EMULATOR=/usr/bin/node;--experimental-wasm-threads
|
||||
-- Configuring done (0.5s)
|
||||
-- Generating done (0.0s)
|
||||
-- Build files have been written to: /home/mes006/linuxcncWebWASM/build-wasm
|
||||
[0/2] Re-checking globbed directories...
|
||||
[1/55] Building C object CMakeFiles/sim_core.dir/src/sim_api.c.o
|
||||
[2/55] Building C object CMakeFiles/sim_core.dir/src/sim_scheduler.c.o
|
||||
[3/55] Building C object CMakeFiles/sim_core.dir/src/sim_protocol.c.o
|
||||
[4/55] Building C object CMakeFiles/sim_core.dir/src/sim_event.c.o
|
||||
[5/55] Building C object CMakeFiles/sim_core.dir/src/sim_task.c.o
|
||||
[6/55] Building C object CMakeFiles/sim_core.dir/src/sim_posemath.c.o
|
||||
[7/55] Building C object CMakeFiles/sim_core.dir/src/sim_device_io.c.o
|
||||
[8/55] Building C object CMakeFiles/sim_core.dir/src/sim_motion_queue.c.o
|
||||
[9/55] Building C object CMakeFiles/sim_core.dir/src/sim_switchkins.c.o
|
||||
[10/55] Building C object CMakeFiles/sim_core.dir/src/sim_linuxcnc_tp.c.o
|
||||
[11/55] Building C object CMakeFiles/sim_core.dir/src/sim_userkins.c.o
|
||||
[12/55] Building C object CMakeFiles/sim_core.dir/src/sim_interpreter.c.o
|
||||
[13/55] Building C object CMakeFiles/sim_core.dir/src/sim_hal.c.o
|
||||
[14/55] Building C object CMakeFiles/sim_core.dir/src/sim_kinematics_registry.c.o
|
||||
[15/55] Building C object CMakeFiles/sim_core.dir/src/sim_kinematics_motion.c.o
|
||||
[16/55] Building C object CMakeFiles/sim_core.dir/src/sim_program.c.o
|
||||
[17/55] Building C object CMakeFiles/sim_core.dir/src/sim_kinematics_feedback.c.o
|
||||
[18/55] Building C object CMakeFiles/sim_core_module.dir/src/sim_api.c.o
|
||||
[19/55] Building C object CMakeFiles/sim_core.dir/src/sim_kinematics_home_limit.c.o
|
||||
[20/55] Building C object CMakeFiles/sim_core_module.dir/src/sim_protocol.c.o
|
||||
[21/55] Building C object CMakeFiles/sim_core_module.dir/src/sim_event.c.o
|
||||
[22/55] Building C object CMakeFiles/sim_core_module.dir/src/sim_task.c.o
|
||||
[23/55] Building C object CMakeFiles/sim_core_module.dir/src/sim_scheduler.c.o
|
||||
[24/55] Building C object CMakeFiles/sim_core.dir/src/sim_io.c.o
|
||||
[25/55] Building C object CMakeFiles/sim_core_module.dir/src/sim_posemath.c.o
|
||||
[26/55] Building C object CMakeFiles/sim_core.dir/src/sim_kinematics.c.o
|
||||
[27/55] Building C object CMakeFiles/sim_core_module.dir/src/sim_device_io.c.o
|
||||
[28/55] Linking CXX static library libsim_core.a
|
||||
[29/55] Building C object CMakeFiles/sim_core_module.dir/src/sim_motion_queue.c.o
|
||||
[30/55] Building C object CMakeFiles/sim_core_module.dir/src/sim_hal.c.o
|
||||
[31/55] Building C object CMakeFiles/sim_core_module.dir/src/sim_switchkins.c.o
|
||||
[32/55] Building C object CMakeFiles/sim_core_module.dir/src/sim_linuxcnc_tp.c.o
|
||||
[33/55] Building C object CMakeFiles/sim_core_module.dir/src/sim_program.c.o
|
||||
[34/55] Building C object CMakeFiles/sim_core_module.dir/src/sim_interpreter.c.o
|
||||
[35/55] Building C object CMakeFiles/sim_core_module.dir/src/sim_kinematics.c.o
|
||||
[36/55] Linking CXX executable sim_kinematics_registry_wasm_test.js
|
||||
[37/55] Linking CXX executable sim_hal_wasm_test.js
|
||||
[38/55] Linking CXX executable sim_kinematics_wasm_test.js
|
||||
[39/55] Linking CXX executable sim_switchkins_wasm_test.js
|
||||
[40/55] Linking CXX executable sim_kinematics_motion_wasm_test.js
|
||||
[41/55] Linking CXX executable sim_userkins_wasm_test.js
|
||||
[42/55] Linking CXX executable sim_kinematics_feedback_wasm_test.js
|
||||
[43/55] Building C object CMakeFiles/sim_core_module.dir/src/sim_userkins.c.o
|
||||
[44/55] Building C object CMakeFiles/sim_core_module.dir/src/sim_kinematics_registry.c.o
|
||||
[45/55] Building C object CMakeFiles/sim_core_module.dir/src/sim_kinematics_motion.c.o
|
||||
[46/55] Building C object CMakeFiles/sim_core_module.dir/src/sim_kinematics_feedback.c.o
|
||||
[47/55] Building C object CMakeFiles/sim_core_module.dir/src/sim_kinematics_home_limit.c.o
|
||||
[48/55] Linking CXX executable sim_kinematics_home_limit_wasm_test.js
|
||||
[49/55] Building C object CMakeFiles/sim_core_module.dir/src/sim_io.c.o
|
||||
[50/55] Linking CXX executable sim_io_wasm_test.js
|
||||
[51/55] Linking CXX executable sim_device_io_wasm_test.js
|
||||
[52/55] Linking CXX executable sim_motion_queue_wasm_test.js
|
||||
[53/55] Linking CXX executable sim_source_unit_adapter_wasm_test.js
|
||||
[54/55] Linking CXX executable sim_xyzac_trace_wasm_test.js
|
||||
[55/55] Linking CXX executable linuxcnc-sim-core.js
|
||||
{"schema":"kinematics-coupled-samples.v1","corexy":{"pose":[4.000000000,2.000000000,3.000000000],"inverse":[6.000000000,2.000000000,3.000000000],"state_hash":15100884384408177781},"rose":{"pose":[-1.969615506,-0.347296355,7.000000000],"inverse":[2.000000000,7.000000000,190.000000000],"branch":1,"state_hash":2438572543122669123},"maxkins":{"pose":[-4.000000000,3.000000000,5.000000000],"inverse":[3.000000000,4.000000000,5.000000000,0.000000000,30.000000000,90.000000000],"state_hash":9619955171041365392},"rotatekins":{"pose":[4.000000000,-3.000000000,5.000000000],"inverse":[3.000000000,4.000000000,5.000000000,0.000000000,0.000000000,90.000000000],"state_hash":4875770340631299468},"fiveaxiskins":{"pose":[118.967490359,111.434580971,76.850293016],"inverse":[10.000000000,20.000000000,30.000000000,35.000000000,40.000000000,-2.000000000,20.000000000],"state_hash":7748142909372720337},"xyzac_trt":{"pose":[142.826706376,9.689621386,163.102540378],"inverse":[100.000000000,50.000000000,200.000000000,30.000000000,45.000000000],"state_hash":13756563895642799233},"xyzbc_trt":{"pose":[44.740352549,25.970325570,227.671759760],"inverse":[100.000000000,50.000000000,200.000000000,30.000000000,45.000000000],"state_hash":16201786080257949760},"scara":{"pose":[590.460310444,130.945742721,465.000000000],"inverse":[-3.644300703,40.000000000,25.000000000,-31.355699297,5.000000000,-3.000000000],"state_hash":16144366080292311245},"puma":{"pose":[450.112320486,207.114833215,-612.180622409],"inverse":[10.000000000,20.000000000,-30.000000000,40.000000000,25.000000000,-15.000000000],"state_hash":3358632253473456540},"scorbot":{"pose":[366.786994761,171.035584569,191.173940759],"inverse":[25.000000000,35.000000000,-20.000000000,10.000000000,-5.000000000],"state_hash":7814353589808450675},"genser":{"pose":[619.051467462,30.411237800,-349.855537721],"inverse":[0.000000000,10.000000000,-20.000000000,29.999999999,15.000000000,-9.999999999],"state_hash":7456282805679296496,"iterations":0,"residual":0.000000000000},"lineardelta":{"pose":[0.000000000,0.000000000,64.636584194],"inverse":[300.000000000,300.000000000,300.000000000],"state_hash":5975346607623231017},"rotarydelta":{"pose":[0.000000000,0.000000000,-3.793360169],"inverse":[10.000000000,10.000000000,10.000000000],"state_hash":2370894419412951220},"tripod":{"pose":[2.000000000,3.000000000,4.000000000],"inverse":[5.385164807,9.433981132,6.447715952],"state_hash":15608486964708188096},"genhex":{"pose":[10.000000000,-5.000000000,60.000000000,4.999999999,-3.000000001,10.000000000],"inverse":[68.274402001,63.007928550,63.783820692,65.475127133,60.456723205,68.231500501],"state_hash":7575268797291078131,"iterations":14,"residual":0.000000000857},"pentapod":{"pose":[0.000000000,500.000000000,300.000000000,0.010000000,-0.010000000],"inverse":[625.895566556,649.261562588,754.767615986,338.490149644,341.118326622],"state_hash":13978136356142463125,"iterations":12,"residual":0.000000001000},"delta_edges":{"lineardelta":{"alternate_status":0,"alternate_branch":1,"alternate_pose":[-0.000000000,-0.000000000,535.363415806],"no_solution":-702,"limit_error":-702,"geometry_error":-703,"branch":0,"iterations":0},"rotarydelta":{"alternate_status":0,"alternate_branch":1,"alternate_pose":[-0.000000000,-0.000000000,0.320396615],"singular":-704,"geometry_error":-703,"branch":0,"iterations":0},"tripod":{"alternate_status":0,"alternate_branch":1,"alternate_pose":[2.000000000,3.000000000,-4.000000000],"no_solution":-702,"singular":-704,"limit_error":-702,"geometry_error":-703,"branch":0,"iterations":0},"genhex":{"alternate_status":0,"alternate_branch":1,"alternate_pose":[10.000000000,-5.000000000,-60.000000000],"nonpositive":-702,"iteration_limit":-702,"max_error":-702,"singular":-704,"limit_error":-702,"branch":0,"iterations":14,"residual":0.000000000857},"pentapod":{"alternate_status":-702,"alternate_branch":1,"alternate_pose":[0.000000000,0.000000000,0.000000000],"alternate_iterations":1,"nonpositive":-702,"nonpositive_iterations":0,"iteration_limit":-702,"iteration_limit_iterations":1,"max_error":-702,"max_error_iterations":1,"singular":-704,"singular_iterations":0,"limit_error":-702,"branch":0,"iterations":12,"residual":0.000000001000}}}
|
||||
{"schema":"hal-corexy-sample.v1","command":{"j0":6.000,"j1":2.000,"alpha":4.000,"beta":2.000},"feedback":{"alpha":4.000,"beta":1.000,"j0":5.000,"j1":3.000},"tick":1,"function_calls":1,"component_batch":{"gate":1,"constant":3.500,"rollback_tick":1}}
|
||||
{"schema":"linuxcnc-source-unit-adapter.v1","cases":[{"id":"source-unit:interp/movement-linear","name":"Movement commands / Linear Feed","source":"linuxcnc/unit_tests/interp/test_interp_block.cc","result_hash":16718210192490359486,"status":"PASSED"},{"id":"source-unit:interp/movement-rapid","name":"Movement commands / Rapid","source":"linuxcnc/unit_tests/interp/test_interp_block.cc","result_hash":10600861468726077694,"status":"PASSED"},{"id":"source-unit:interp/movement-arc","name":"Movement commands / Clockwise circular","source":"linuxcnc/unit_tests/interp/test_interp_block.cc","result_hash":3344452133792667065,"status":"PASSED"},{"id":"source-unit:interp/gcode-to-string","name":"convert G-codes to string","source":"linuxcnc/unit_tests/interp/test_string_conversion.cc","result_hash":18096020171334179193,"status":"PASSED"},{"id":"source-unit:interp/g10-coordinate-offset","name":"G10 L2 / G54 coordinate offset","source":"linuxcnc/unit_tests/interp/test_interp_basics.cc","result_hash":10503294382485028241,"status":"PASSED"},{"id":"source-unit:interp/parameter-expression","name":"parameter expression assignment","source":"linuxcnc/unit_tests/interp/test_interp_basics.cc","result_hash":12191524797282674768,"status":"PASSED"},{"id":"source-unit:interp/modal-units","name":"G20 length/feed conversion","source":"linuxcnc/unit_tests/interp/test_interp_basics.cc","result_hash":17337372050960250323,"status":"PASSED"},{"id":"source-unit:tp/pmCartCartParallel_numerical","name":"pmCartCartParallel_numerical","source":"linuxcnc/unit_tests/tp/test_blendmath.c","result_hash":8528287329722895704,"status":"PASSED"},{"id":"source-unit:tp/pmCartCartAntiParallel_numerical","name":"pmCartCartAntiParallel_numerical","source":"linuxcnc/unit_tests/tp/test_blendmath.c","result_hash":6154898365857503476,"status":"PASSED"}],"counts":{"FAILED":0,"PASSED":9},"aggregate_hash":12570001048447364769,"pending":[{"source_id":"source-test:linuxcnc/tests/abort/g64/test.sh","adapter":"WORKER_TASK_MOTION_PENDING","reason":"G64 blend/tolerance and abort endpoint require Worker Task plus motion queue"},{"source_id":"source-test:linuxcnc/tests/abort/feed-rate/test.sh","adapter":"WORKER_TASK_MOTION_PENDING","reason":"abort/restart timing is owned by Worker Task and fixed-tick motion"}]}
|
||||
-- Configuring done (0.0s)
|
||||
-- Generating done (0.0s)
|
||||
-- Build files have been written to: /home/mes006/linuxcncWebWASM/build-native
|
||||
[0/2] Re-checking globbed directories...
|
||||
[1/43] Building C object CMakeFiles/sim_core.dir/src/sim_api.c.o
|
||||
[2/43] Building C object CMakeFiles/sim_core.dir/src/sim_scheduler.c.o
|
||||
[3/43] Building C object CMakeFiles/sim_core.dir/src/sim_protocol.c.o
|
||||
[4/43] Building C object CMakeFiles/sim_core.dir/src/sim_event.c.o
|
||||
[5/43] Building C object CMakeFiles/sim_core.dir/src/sim_posemath.c.o
|
||||
[6/43] Building C object CMakeFiles/sim_core.dir/src/sim_device_io.c.o
|
||||
[7/43] Building C object CMakeFiles/sim_core.dir/src/sim_switchkins.c.o
|
||||
[8/43] Building C object CMakeFiles/sim_core.dir/src/sim_task.c.o
|
||||
[9/43] Building C object CMakeFiles/sim_core.dir/src/sim_program.c.o
|
||||
[10/43] Building C object CMakeFiles/sim_core.dir/src/sim_userkins.c.o
|
||||
[11/43] Building C object CMakeFiles/sim_core.dir/src/sim_kinematics_registry.c.o
|
||||
[12/43] Building C object CMakeFiles/sim_core.dir/src/sim_hal.c.o
|
||||
[13/43] Building C object CMakeFiles/sim_core.dir/src/sim_kinematics_feedback.c.o
|
||||
[14/43] Building C object CMakeFiles/sim_core.dir/src/sim_kinematics_motion.c.o
|
||||
[15/43] Building C object CMakeFiles/sim_core.dir/src/sim_linuxcnc_tp.c.o
|
||||
[16/43] Building C object CMakeFiles/sim_core.dir/src/sim_kinematics_home_limit.c.o
|
||||
[17/43] Building C object CMakeFiles/sim_core.dir/src/sim_motion_queue.c.o
|
||||
[18/43] Building C object CMakeFiles/sim_core.dir/src/sim_io.c.o
|
||||
[19/43] Building C object CMakeFiles/sim_core.dir/src/sim_interpreter.c.o
|
||||
[20/43] Building C object CMakeFiles/sim_core.dir/src/sim_kinematics.c.o
|
||||
[21/43] Linking CXX static library libsim_core.a
|
||||
[22/43] Linking CXX executable sim_task_test
|
||||
[23/43] Linking CXX executable sim_protocol_test
|
||||
[24/43] Linking CXX executable sim_native_homing_test
|
||||
[25/43] Linking CXX executable sim_event_test
|
||||
[26/43] Linking CXX executable sim_api_test
|
||||
[27/43] Linking CXX executable sim_contract_test
|
||||
[28/43] Linking CXX executable sim_scheduler_test
|
||||
[29/43] Linking CXX executable sim_hal_test
|
||||
[30/43] Linking CXX executable sim_posemath_test
|
||||
[31/43] Linking CXX executable sim_interpreter_test
|
||||
[32/43] Linking CXX executable sim_switchkins_test
|
||||
[33/43] Linking CXX executable sim_kinematics_registry_test
|
||||
[34/43] Linking CXX executable sim_source_unit_adapter_test
|
||||
[35/43] Linking CXX executable sim_userkins_test
|
||||
[36/43] Linking CXX executable sim_kinematics_motion_test
|
||||
[37/43] Linking CXX executable sim_motion_queue_test
|
||||
[38/43] Linking CXX executable sim_kinematics_test
|
||||
[39/43] Linking CXX executable sim_kinematics_feedback_test
|
||||
[40/43] Linking CXX executable sim_kinematics_home_limit_test
|
||||
[41/43] Linking CXX executable sim_xyzac_trace_test
|
||||
[42/43] Linking CXX executable sim_io_test
|
||||
[43/43] Linking CXX executable sim_device_io_test
|
||||
Internal ctest changing into directory: /home/mes006/linuxcncWebWASM/build-native
|
||||
Test project /home/mes006/linuxcncWebWASM/build-native
|
||||
Start 1: sim_native_homing_test
|
||||
1/23 Test #1: sim_native_homing_test ........... Passed 0.00 sec
|
||||
Start 2: sim_api_test
|
||||
2/23 Test #2: sim_api_test ..................... Passed 0.00 sec
|
||||
Start 3: sim_protocol_test
|
||||
3/23 Test #3: sim_protocol_test ................ Passed 0.00 sec
|
||||
Start 4: sim_event_test
|
||||
4/23 Test #4: sim_event_test ................... Passed 0.00 sec
|
||||
Start 5: sim_contract_test
|
||||
5/23 Test #5: sim_contract_test ................ Passed 0.00 sec
|
||||
Start 6: sim_task_test
|
||||
6/23 Test #6: sim_task_test .................... Passed 0.00 sec
|
||||
Start 7: sim_scheduler_test
|
||||
7/23 Test #7: sim_scheduler_test ............... Passed 0.00 sec
|
||||
Start 8: sim_hal_test
|
||||
8/23 Test #8: sim_hal_test ..................... Passed 0.00 sec
|
||||
Start 9: sim_posemath_test
|
||||
9/23 Test #9: sim_posemath_test ................ Passed 0.00 sec
|
||||
Start 10: sim_interpreter_test
|
||||
10/23 Test #10: sim_interpreter_test ............. Passed 0.00 sec
|
||||
Start 11: sim_source_unit_adapter_test
|
||||
11/23 Test #11: sim_source_unit_adapter_test ..... Passed 0.00 sec
|
||||
Start 12: sim_motion_queue_test
|
||||
12/23 Test #12: sim_motion_queue_test ............ Passed 0.00 sec
|
||||
Start 13: sim_kinematics_test
|
||||
13/23 Test #13: sim_kinematics_test .............. Passed 0.00 sec
|
||||
Start 14: sim_switchkins_test
|
||||
14/23 Test #14: sim_switchkins_test .............. Passed 0.00 sec
|
||||
Start 15: sim_userkins_test
|
||||
15/23 Test #15: sim_userkins_test ................ Passed 0.00 sec
|
||||
Start 16: sim_kinematics_registry_test
|
||||
16/23 Test #16: sim_kinematics_registry_test ..... Passed 0.00 sec
|
||||
Start 17: sim_kinematics_motion_test
|
||||
17/23 Test #17: sim_kinematics_motion_test ....... Passed 0.00 sec
|
||||
Start 18: sim_kinematics_feedback_test
|
||||
18/23 Test #18: sim_kinematics_feedback_test ..... Passed 0.00 sec
|
||||
Start 19: sim_kinematics_home_limit_test
|
||||
19/23 Test #19: sim_kinematics_home_limit_test ... Passed 0.00 sec
|
||||
Start 20: sim_io_test
|
||||
20/23 Test #20: sim_io_test ...................... Passed 0.00 sec
|
||||
Start 21: sim_device_io_test
|
||||
21/23 Test #21: sim_device_io_test ............... Passed 0.00 sec
|
||||
Start 22: sim_xyzac_trace_test
|
||||
22/23 Test #22: sim_xyzac_trace_test ............. Passed 0.00 sec
|
||||
Start 23: sim_linuxcnc_tp_test
|
||||
23/23 Test #23: sim_linuxcnc_tp_test ............. Passed 0.00 sec
|
||||
|
||||
100% tests passed, 0 tests failed out of 23
|
||||
|
||||
Total Test time (real) = 0.03 sec
|
||||
wasm_build=/home/mes006/linuxcncWebWASM/build-wasm/linuxcnc-sim-core.js
|
||||
native_test=/home/mes006/linuxcncWebWASM/build-native/sim_api_test
|
||||
native_protocol_test=/home/mes006/linuxcncWebWASM/build-native/sim_protocol_test
|
||||
wasm_kinematics_test=/home/mes006/linuxcncWebWASM/build-wasm/sim_kinematics_wasm_test.js
|
||||
wasm_hal_test=/home/mes006/linuxcncWebWASM/build-wasm/sim_hal_wasm_test.js
|
||||
wasm_source_unit_test=/home/mes006/linuxcncWebWASM/build-wasm/sim_source_unit_adapter_wasm_test.js
|
||||
1
coverage/recovery/checkpoint-state.report.json
Normal file
@@ -0,0 +1 @@
|
||||
{"schema":"xyzac-recovery-rtcp-11.v1","status":"verified","case":"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini","checkpoint":{"required_fields":["context_id","simulation_tick","event_cursor","state_hash","task","program","kinematics","motion","wasm_motion","ledger"],"active_type":1,"state_hash":"fnv1a64:00e5c37554124a5d"},"continuous_endpoint_hash":"fnv1a64:6c0c36b47a65d571","restored_endpoint_hash":"fnv1a64:6c0c36b47a65d571","checkpoint_tick":3,"unreplayed_tail_endpoint_hash":"fnv1a64:40beb05449a569c8","endpoint_at_checkpoint_equal":true,"no_duplicate_m428":true,"replayed_operation":true,"corrupt_checkpoint":"blocked","schema_mismatch":"blocked","journal":{"status":"recovered","corrupt_status":"blocked","schema_status":"blocked"},"opfs":{"unavailable_status":"blocked","probe_status":"blocked","physical_crash":"REAL-EXCLUDED"},"first_divergent_tick":null}
|
||||
131
coverage/recovery/exact-checkpoint.report.json
Normal file
@@ -0,0 +1,131 @@
|
||||
{
|
||||
"report_id": "trt-checkpoint-exact-v1",
|
||||
"status": "verified",
|
||||
"safeguards": {
|
||||
"corrupt_native_rejected": true,
|
||||
"foreign_build_rejected": true,
|
||||
"frozen_subroutines": true,
|
||||
"heap_relocation_checked": true,
|
||||
"receipts_nonrecursive": true,
|
||||
"backward_transport_restore_rejected": true
|
||||
},
|
||||
"rows": [
|
||||
{
|
||||
"machine": "xyzac-trt",
|
||||
"phase": "subroutine",
|
||||
"checkpoint_tick": 6,
|
||||
"continuation_tick": 18,
|
||||
"state_hash": "fnv1a64:113cda82a07e692a",
|
||||
"samples": 12,
|
||||
"first_divergent_tick": null
|
||||
},
|
||||
{
|
||||
"machine": "xyzac-trt",
|
||||
"phase": "paused-resumed",
|
||||
"checkpoint_tick": 18,
|
||||
"continuation_tick": 30,
|
||||
"state_hash": "fnv1a64:e380c733d2e27194",
|
||||
"samples": 12,
|
||||
"new_transport_recovery": true,
|
||||
"first_divergent_tick": null
|
||||
},
|
||||
{
|
||||
"machine": "xyzac-trt",
|
||||
"phase": "line",
|
||||
"checkpoint_tick": 35,
|
||||
"continuation_tick": 47,
|
||||
"state_hash": "fnv1a64:8e128e04aca13b45",
|
||||
"samples": 12,
|
||||
"first_divergent_tick": null
|
||||
},
|
||||
{
|
||||
"machine": "xyzac-trt",
|
||||
"phase": "arc",
|
||||
"checkpoint_tick": 72,
|
||||
"continuation_tick": 84,
|
||||
"state_hash": "fnv1a64:16fef8e7095d1cc7",
|
||||
"samples": 12,
|
||||
"first_divergent_tick": null
|
||||
},
|
||||
{
|
||||
"machine": "xyzac-trt",
|
||||
"phase": "completed",
|
||||
"checkpoint_tick": 684,
|
||||
"continuation_tick": 696,
|
||||
"state_hash": "fnv1a64:571fff3045139a9b",
|
||||
"samples": 12,
|
||||
"first_divergent_tick": null
|
||||
},
|
||||
{
|
||||
"machine": "xyzac-trt",
|
||||
"phase": "trace-and-transport",
|
||||
"state_hash": "fnv1a64:50c207f5409d5062",
|
||||
"first_divergent_tick": null
|
||||
},
|
||||
{
|
||||
"machine": "xyzac-trt",
|
||||
"phase": "post-recovery-jog",
|
||||
"samples": 6,
|
||||
"first_divergent_tick": null
|
||||
},
|
||||
{
|
||||
"machine": "xyzbc-trt",
|
||||
"phase": "subroutine",
|
||||
"checkpoint_tick": 6,
|
||||
"continuation_tick": 18,
|
||||
"state_hash": "fnv1a64:94587fdc5ce1e3b1",
|
||||
"samples": 12,
|
||||
"first_divergent_tick": null
|
||||
},
|
||||
{
|
||||
"machine": "xyzbc-trt",
|
||||
"phase": "paused-resumed",
|
||||
"checkpoint_tick": 18,
|
||||
"continuation_tick": 30,
|
||||
"state_hash": "fnv1a64:5f0adc4aa9f2a037",
|
||||
"samples": 12,
|
||||
"new_transport_recovery": true,
|
||||
"first_divergent_tick": null
|
||||
},
|
||||
{
|
||||
"machine": "xyzbc-trt",
|
||||
"phase": "line",
|
||||
"checkpoint_tick": 35,
|
||||
"continuation_tick": 47,
|
||||
"state_hash": "fnv1a64:74b83ca0c127a6c1",
|
||||
"samples": 12,
|
||||
"first_divergent_tick": null
|
||||
},
|
||||
{
|
||||
"machine": "xyzbc-trt",
|
||||
"phase": "arc",
|
||||
"checkpoint_tick": 72,
|
||||
"continuation_tick": 84,
|
||||
"state_hash": "fnv1a64:89a768be32ed9eda",
|
||||
"samples": 12,
|
||||
"first_divergent_tick": null
|
||||
},
|
||||
{
|
||||
"machine": "xyzbc-trt",
|
||||
"phase": "completed",
|
||||
"checkpoint_tick": 704,
|
||||
"continuation_tick": 716,
|
||||
"state_hash": "fnv1a64:d0cf609cae7c894b",
|
||||
"samples": 12,
|
||||
"first_divergent_tick": null
|
||||
},
|
||||
{
|
||||
"machine": "xyzbc-trt",
|
||||
"phase": "trace-and-transport",
|
||||
"state_hash": "fnv1a64:4e6307e65d801db6",
|
||||
"first_divergent_tick": null
|
||||
},
|
||||
{
|
||||
"machine": "xyzbc-trt",
|
||||
"phase": "post-recovery-jog",
|
||||
"samples": 6,
|
||||
"first_divergent_tick": null
|
||||
}
|
||||
],
|
||||
"state_hash": "fnv1a64:d7f8b093cb3e4510"
|
||||
}
|
||||
1
coverage/recovery/power-off.report.json
Normal file
@@ -0,0 +1 @@
|
||||
{"schema":"xyzac-runtime-followups.v1","status":"verified","motion_delta":{"compact_bytes":7224,"full_bytes":23215,"reduction_ratio":0.689},"errors":{"code":"axis_gate","reason":"xyzac_machine_off","message_key":"xyzac_machine_off"},"power_off_persistence":{"schema":"xyzac-power-off-checkpoint.v1","bytes":101157,"restored":"verified"}}
|
||||
657
coverage/recovery/rtcp-jog-browser.report.json
Normal file
@@ -0,0 +1,657 @@
|
||||
{
|
||||
"report_id": "trt-rtcp-jog-browser-v1",
|
||||
"status": "verified",
|
||||
"machines": [
|
||||
{
|
||||
"coordinates": "XYZAC",
|
||||
"rows": [
|
||||
{
|
||||
"axis": "a",
|
||||
"before": {
|
||||
"joints": [
|
||||
4.266649999999998,
|
||||
3.8666500000000035,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": 0,
|
||||
"y_offset": 20,
|
||||
"z_offset": 10,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 59,
|
||||
"rendered_tick": 59,
|
||||
"tool_tip": [
|
||||
0,
|
||||
2,
|
||||
0
|
||||
]
|
||||
},
|
||||
"moving": {
|
||||
"joints": [
|
||||
4.266649999999998,
|
||||
5.061423130217634,
|
||||
-1.709991602392515,
|
||||
6.300000000000009,
|
||||
0
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": 0,
|
||||
"y_offset": 20,
|
||||
"z_offset": 10,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 90,
|
||||
"rendered_tick": 84,
|
||||
"tool_tip": [
|
||||
0,
|
||||
2,
|
||||
0
|
||||
],
|
||||
"tcp_error": 1.3877787807814457e-17
|
||||
},
|
||||
"stopped": {
|
||||
"joints": [
|
||||
4.266649999999998,
|
||||
5.436614141154411,
|
||||
-2.173445467445781,
|
||||
8.100000000000021,
|
||||
0
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": 0,
|
||||
"y_offset": 20,
|
||||
"z_offset": 10,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 101,
|
||||
"rendered_tick": 101,
|
||||
"tool_tip": [
|
||||
0,
|
||||
2,
|
||||
0
|
||||
],
|
||||
"tcp_error": 4.441434166503682e-16
|
||||
}
|
||||
},
|
||||
{
|
||||
"axis": "c",
|
||||
"before": {
|
||||
"joints": [
|
||||
4.266649999999998,
|
||||
5.436614141154411,
|
||||
-2.173445467445781,
|
||||
8.100000000000021,
|
||||
0
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": 0,
|
||||
"y_offset": 20,
|
||||
"z_offset": 10,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 101,
|
||||
"rendered_tick": 101,
|
||||
"tool_tip": [
|
||||
0,
|
||||
2,
|
||||
0
|
||||
]
|
||||
},
|
||||
"moving": {
|
||||
"joints": [
|
||||
3.816579336612649,
|
||||
5.877023221778387,
|
||||
-2.1107659733420316,
|
||||
8.100000000000021,
|
||||
6.30000000000001
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": 0,
|
||||
"y_offset": 20,
|
||||
"z_offset": 10,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 132,
|
||||
"rendered_tick": 126,
|
||||
"tool_tip": [
|
||||
0,
|
||||
2,
|
||||
0
|
||||
],
|
||||
"tcp_error": 4.441976168356516e-16
|
||||
},
|
||||
"stopped": {
|
||||
"joints": [
|
||||
3.6792686907248924,
|
||||
5.993602656197197,
|
||||
-2.0941742628307054,
|
||||
8.100000000000021,
|
||||
8.100000000000021
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": 0,
|
||||
"y_offset": 20,
|
||||
"z_offset": 10,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 143,
|
||||
"rendered_tick": 143,
|
||||
"tool_tip": [
|
||||
0,
|
||||
2,
|
||||
0
|
||||
],
|
||||
"tcp_error": 1.962615573354719e-17
|
||||
}
|
||||
},
|
||||
{
|
||||
"axis": "a",
|
||||
"before": {
|
||||
"joints": [
|
||||
3.6792686907248924,
|
||||
5.993602656197197,
|
||||
-2.0941742628307054,
|
||||
8.100000000000021,
|
||||
8.100000000000021
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": 0,
|
||||
"y_offset": 20,
|
||||
"z_offset": 10,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 143,
|
||||
"rendered_tick": 143,
|
||||
"tool_tip": [
|
||||
0,
|
||||
2,
|
||||
0
|
||||
]
|
||||
},
|
||||
"moving": {
|
||||
"joints": [
|
||||
3.6792686907248924,
|
||||
6.985677681046019,
|
||||
-3.15582122980466,
|
||||
12.600000000000016,
|
||||
8.100000000000021
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": 0,
|
||||
"y_offset": 20,
|
||||
"z_offset": 10,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 168,
|
||||
"rendered_tick": 162,
|
||||
"tool_tip": [
|
||||
0,
|
||||
2,
|
||||
0
|
||||
],
|
||||
"tcp_error": 2.5018537765542006e-17
|
||||
},
|
||||
"stopped": {
|
||||
"joints": [
|
||||
3.6792686907248924,
|
||||
7.405333794615657,
|
||||
-3.558119369114298,
|
||||
14.40000000000003,
|
||||
8.100000000000021
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": 0,
|
||||
"y_offset": 20,
|
||||
"z_offset": 10,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 179,
|
||||
"rendered_tick": 179,
|
||||
"tool_tip": [
|
||||
0,
|
||||
2,
|
||||
0
|
||||
],
|
||||
"tcp_error": 3.7367087251761353e-17
|
||||
}
|
||||
},
|
||||
{
|
||||
"axis": "c",
|
||||
"before": {
|
||||
"joints": [
|
||||
3.6792686907248924,
|
||||
7.405333794615657,
|
||||
-3.558119369114298,
|
||||
14.40000000000003,
|
||||
8.100000000000021
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": 0,
|
||||
"y_offset": 20,
|
||||
"z_offset": 10,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 179,
|
||||
"rendered_tick": 179,
|
||||
"tool_tip": [
|
||||
0,
|
||||
2,
|
||||
0
|
||||
]
|
||||
},
|
||||
"moving": {
|
||||
"joints": [
|
||||
3.044109145431041,
|
||||
7.8492876810852135,
|
||||
-3.444131385053268,
|
||||
14.40000000000003,
|
||||
15.900000000000045
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": 0,
|
||||
"y_offset": 20,
|
||||
"z_offset": 10,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 215,
|
||||
"rendered_tick": 209,
|
||||
"tool_tip": [
|
||||
0,
|
||||
2,
|
||||
0
|
||||
],
|
||||
"tcp_error": 2.2247786310271853e-16
|
||||
},
|
||||
"stopped": {
|
||||
"joints": [
|
||||
2.8890836739630625,
|
||||
7.9395654828013065,
|
||||
-3.4209519852626453,
|
||||
14.40000000000003,
|
||||
17.70000000000001
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": 0,
|
||||
"y_offset": 20,
|
||||
"z_offset": 10,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 226,
|
||||
"rendered_tick": 226,
|
||||
"tool_tip": [
|
||||
0,
|
||||
2,
|
||||
0
|
||||
],
|
||||
"tcp_error": 3.622208959480119e-17
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"coordinates": "XYZBC",
|
||||
"rows": [
|
||||
{
|
||||
"axis": "b",
|
||||
"before": {
|
||||
"joints": [
|
||||
4.466649999999994,
|
||||
3.466650000000003,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": -20,
|
||||
"y_offset": 0,
|
||||
"z_offset": -15,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 284,
|
||||
"rendered_tick": 284,
|
||||
"tool_tip": [
|
||||
0,
|
||||
1.5,
|
||||
0
|
||||
]
|
||||
},
|
||||
"moving": {
|
||||
"joints": [
|
||||
7.000111209846697,
|
||||
3.466650000000003,
|
||||
-5.273183620607902,
|
||||
11.699999999999944,
|
||||
0
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": -20,
|
||||
"y_offset": 0,
|
||||
"z_offset": -15,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 333,
|
||||
"rendered_tick": 333,
|
||||
"tool_tip": [
|
||||
0,
|
||||
1.4472681637939209,
|
||||
0
|
||||
],
|
||||
"tcp_error": 2.237726045655905e-16
|
||||
},
|
||||
"stopped": {
|
||||
"joints": [
|
||||
7.292314970736374,
|
||||
3.466650000000003,
|
||||
-6.126077205619893,
|
||||
13.499999999999906,
|
||||
0
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": -20,
|
||||
"y_offset": 0,
|
||||
"z_offset": -15,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 344,
|
||||
"rendered_tick": 344,
|
||||
"tool_tip": [
|
||||
0,
|
||||
1.4387392279438012,
|
||||
0
|
||||
],
|
||||
"tcp_error": 2.7755575615628914e-17
|
||||
}
|
||||
},
|
||||
{
|
||||
"axis": "c",
|
||||
"before": {
|
||||
"joints": [
|
||||
7.292314970736374,
|
||||
3.466650000000003,
|
||||
-6.126077205619893,
|
||||
13.499999999999906,
|
||||
0
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": -20,
|
||||
"y_offset": 0,
|
||||
"z_offset": -15,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 344,
|
||||
"rendered_tick": 344,
|
||||
"tool_tip": [
|
||||
0,
|
||||
1.4387392279438012,
|
||||
0
|
||||
]
|
||||
},
|
||||
"moving": {
|
||||
"joints": [
|
||||
6.753295838822215,
|
||||
4.081962692594443,
|
||||
-5.996670161309499,
|
||||
13.499999999999906,
|
||||
8.400000000000015
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": -20,
|
||||
"y_offset": 0,
|
||||
"z_offset": -15,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 382,
|
||||
"rendered_tick": 376,
|
||||
"tool_tip": [
|
||||
0,
|
||||
1.4397384871103407,
|
||||
0
|
||||
],
|
||||
"tcp_error": 2.2247786310271853e-16
|
||||
},
|
||||
"stopped": {
|
||||
"joints": [
|
||||
6.626743801709193,
|
||||
4.202837259715039,
|
||||
-5.966287705280346,
|
||||
13.499999999999906,
|
||||
10.199999999999976
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": -20,
|
||||
"y_offset": 0,
|
||||
"z_offset": -15,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 393,
|
||||
"rendered_tick": 393,
|
||||
"tool_tip": [
|
||||
0,
|
||||
1.4403371229471966,
|
||||
0
|
||||
],
|
||||
"tcp_error": 6.938893903907228e-18
|
||||
}
|
||||
},
|
||||
{
|
||||
"axis": "b",
|
||||
"before": {
|
||||
"joints": [
|
||||
6.626743801709193,
|
||||
4.202837259715039,
|
||||
-5.966287705280346,
|
||||
13.499999999999906,
|
||||
10.199999999999976
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": -20,
|
||||
"y_offset": 0,
|
||||
"z_offset": -15,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 393,
|
||||
"rendered_tick": 393,
|
||||
"tool_tip": [
|
||||
0,
|
||||
1.4403371229471966,
|
||||
0
|
||||
]
|
||||
},
|
||||
"moving": {
|
||||
"joints": [
|
||||
7.783411671211734,
|
||||
4.202837259715039,
|
||||
-10.678717873870822,
|
||||
23.39999999999989,
|
||||
10.199999999999976
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": -20,
|
||||
"y_offset": 0,
|
||||
"z_offset": -15,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 436,
|
||||
"rendered_tick": 429,
|
||||
"tool_tip": [
|
||||
0,
|
||||
1.4033646717032329,
|
||||
0
|
||||
],
|
||||
"tcp_error": 2.2473876566261383e-16
|
||||
},
|
||||
"stopped": {
|
||||
"joints": [
|
||||
7.783411671211734,
|
||||
4.202837259715039,
|
||||
-10.678717873870822,
|
||||
23.39999999999989,
|
||||
10.199999999999976
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": -20,
|
||||
"y_offset": 0,
|
||||
"z_offset": -15,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 441,
|
||||
"rendered_tick": 441,
|
||||
"tool_tip": [
|
||||
0,
|
||||
1.3932128212612918,
|
||||
0
|
||||
],
|
||||
"tcp_error": 2.2473876566261383e-16
|
||||
}
|
||||
},
|
||||
{
|
||||
"axis": "c",
|
||||
"before": {
|
||||
"joints": [
|
||||
7.783411671211734,
|
||||
4.202837259715039,
|
||||
-10.678717873870822,
|
||||
23.39999999999989,
|
||||
10.199999999999976
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": -20,
|
||||
"y_offset": 0,
|
||||
"z_offset": -15,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 441,
|
||||
"rendered_tick": 441,
|
||||
"tool_tip": [
|
||||
0,
|
||||
1.3932128212612918,
|
||||
0
|
||||
]
|
||||
},
|
||||
"moving": {
|
||||
"joints": [
|
||||
7.317075360390216,
|
||||
4.609695511941566,
|
||||
-10.476916131895246,
|
||||
23.39999999999989,
|
||||
16.79999999999999
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": -20,
|
||||
"y_offset": 0,
|
||||
"z_offset": -15,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 473,
|
||||
"rendered_tick": 473,
|
||||
"tool_tip": [
|
||||
0,
|
||||
1.3952308386810475,
|
||||
0
|
||||
],
|
||||
"tcp_error": 2.238801616996866e-16
|
||||
},
|
||||
"stopped": {
|
||||
"joints": [
|
||||
7.205302306697099,
|
||||
4.693820235514115,
|
||||
-10.428547612400239,
|
||||
23.39999999999989,
|
||||
18.299999999999997
|
||||
],
|
||||
"offsets": {
|
||||
"x_rot_point": 0,
|
||||
"y_rot_point": 0,
|
||||
"z_rot_point": 0,
|
||||
"x_offset": -20,
|
||||
"y_offset": 0,
|
||||
"z_offset": -15,
|
||||
"tool_offset": 0
|
||||
},
|
||||
"tick": 483,
|
||||
"rendered_tick": 483,
|
||||
"tool_tip": [
|
||||
0,
|
||||
1.3957145238759976,
|
||||
0
|
||||
],
|
||||
"tcp_error": 2.234765555274009e-16
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"browser_errors": []
|
||||
}
|
||||
BIN
coverage/recovery/rtcp-jog-xyzac.png
Normal file
|
After Width: | Height: | Size: 314 KiB |
BIN
coverage/recovery/rtcp-jog-xyzbc.png
Normal file
|
After Width: | Height: | Size: 308 KiB |
1
coverage/recovery/storage-bridge.report.json
Normal file
@@ -0,0 +1 @@
|
||||
{"schema":"opfs-worker-bridge.v1","status":"verified","ui_commands_do_not_consume_wasm_sequence":true,"poweroff_checkpoint_roundtrip":true}
|
||||
@@ -1,5 +1,40 @@
|
||||
# 推进台账
|
||||
|
||||
## R08 / 2026-09-18 / 仓库保存
|
||||
|
||||
- 用户授权:将 R06 配置驱动改造、R07 三轴门控与五轴精确恢复完整保存到指定 Gitea 仓库的 `main` 分支。
|
||||
- 保存范围:自有源码、配置、测试脚本、项目文档与接续入口、`coverage/configuration/` 和 `coverage/recovery/` 验收证据。
|
||||
- 检查:远程 main 与提交前 HEAD 一致;提交前执行差异格式检查。功能验证沿用 E07/E08/E09/E10,本轮仅更新保存记录。
|
||||
- 原有大型 tick trace、linuxcnc 子模块本地改动、临时脚本和用户图片保留在本地,不纳入专项提交。
|
||||
- 推送完成以远程 main 与本地 HEAD 的提交 ID 一致为准;实际提交 ID 以 Git 记录为准。专项矩阵无待办任务,后续按新需求推进。
|
||||
|
||||
## R07 / 2026-09-18 / 三轴门控与五轴精确恢复
|
||||
|
||||
- 完成 CFG-R01:为直接工具准备/换刀补齐开机、空闲、操作者模式及运动队列检查;三轴综合测试先验证运行中拒绝,再等待实际队列完成。没有删除原断言或放宽急停/故障约束。
|
||||
- 完成 TRT-R01:TP 保存完整原生段状态、混合/速度/进度及队列;程序保存解释器参数、调用栈和子程序源;HAL 保存 tick、引脚/参数和组件内部状态;同时保存调度交接、核心及协议/事件状态。恢复不再根据剩余几何重新建队列或累加 HAL tick。
|
||||
- 指针与兼容性:TP 队列、程序缓冲区和解释器 owner 在恢复时重新绑定;CMake 生成源码/编译环境指纹;不同构建、不同配置、损坏块及旧的不完整记录明确拒绝。配置 hash 增加完整解析 INI 的 hash,包含回零参数变更。
|
||||
- 幂等性:恢复操作回执,重复 M428 不再执行;回执不递归携带旧 checkpoint 和 UI 快照,避免多次保存时内存增长。
|
||||
- 浏览器修复:OPFS 按机型隔离,仅冷启动自动恢复匹配记录;不兼容缓存保留并提示重新加载。关机记录省去可重建的预览,恢复后重新生成并校验。App 保留恢复出来的程序,不再覆盖为船体示例。
|
||||
- 额外实测修复:identity Jog 的 motion_delta 曾使用上一采样的关节值,导致增量 Jog 结束后模型落后约一个采样。现直接投影当前 WASM 规划器关节值。
|
||||
- 改动文件组:`wasm-core/src` 及对应头文件/CMake/单测;`web/worker/native-checkpoint.mjs`、TP/程序/IO 适配器、SimSession、OPFS 桥;`web/app/axis-contract.mjs`、React App;新增精确恢复和浏览器重启测试、验收汇总脚本。
|
||||
- 验证:E08/E09/E10。native 23/23;两机型各五个阶段、合计 120 个连续任务 tick 的状态 hash 完全一致;另有 12 个恢复后 Jog 样本。覆盖暂停、子程序、不同堆地址、trace 导入、新传输会话、损坏/不同构建拒绝及真实 OPFS 重启。
|
||||
- 本轮证据统一在 `coverage/recovery/`,旧 `coverage/trt-parametric/` 报告保持原版本;前轮配置证据保留。
|
||||
- 当前专项矩阵已无待办任务。代码和证据尚未提交/推送;后续按用户保存指令或新需求推进,不重复已通过整轮测试。
|
||||
|
||||
## R06 / 2026-09-18 / 配置驱动改造
|
||||
|
||||
- 用户确认范围:机床参数、轴配置、路径、可调界面设置改为配置驱动;保留数学/协议常量及 LinuxCNC 上游代码。
|
||||
- 新配置:`machine-catalog.json`、`simulation-resources.json`、`web-settings.json`;新增校验/解析模块 `web-settings.mjs`、`trt-configuration.mjs`。具体键和入口见 07。
|
||||
- 运行链路:`sim-case-loader`、`sim-session`、`wasm-linuxcnc-tp`、`wasm-xyzac-kinematics`、`wasm-native-homing`、IO/device-IO、`xyzac-session`;取消固定几何、限位、速度和周期覆盖。TRT 的 IO 时间基准跟随实际 task tick。旧兼容仿真的可调数值归入配置。
|
||||
- 核心:自有 `sim_linuxcnc_tp.c/.h` 增加逐轴动力学、倍率、缩放下限、轨迹混合配置入口和边界测试;修正加速度上限取值方向。重建 WASM/native。
|
||||
- 界面:App、模型和两个预览、资源路径及类型声明采用配置;支持不同默认机型、INI/程序文件名、图标目录、相机/材质/颜色、Jog 增量、刷新设置。物理计算仍走 WASM。
|
||||
- 修复:规划器初始化不再使用上一台机床的快速速度;默认 XYZBC 的会话来源信息同步;通用加载器不再把三轴 Z 上限写成 100;旧队列进给倍率投影使用 WASM 已确认的 controls 值。
|
||||
- 恢复:checkpoint 保存运行配置 hash,参数变化或旧记录缺少该 hash 时拒绝恢复,不把旧速度/限位状态带入新配置。
|
||||
- 验证:E07,native 23/23、专项 WASM/Worker、改配置的浏览器实际渲染、生产构建、子路径部署和 RTCP 回归通过。旧三轴综合脚本的操作门控问题登记 CFG-R01,不放宽 gate。
|
||||
- 证据:统一归档 `coverage/configuration/`,本轮重跑产生的旧测试报告已归档到该目录;`coverage/trt-parametric/` 保留原已提交版本,不破坏 E02/E06 的历史哈希。
|
||||
- 接续文件:用户要求后创建 `nextTask-hardcoding.md`;随后用户明确继续,现已更新为收口状态。
|
||||
- 未提交/推送;下一开发任务为 CFG-R01,随后 TRT-R01。原有大型 trace、linuxcnc 子模块及临时文件未纳入本轮。
|
||||
|
||||
## R05 / 2026-09-18 / 仓库保存
|
||||
|
||||
- 用户授权:保存到 `https://gitea.meswork.com/wangdequan/linuxcncWebWASM.git` 的 `main` 分支。
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
| TRT-P04 | 参数化结构投影 | P02 | 已验收 | 旋转中心及偏置驱动两模型;转台路径层级一致 | E02 |
|
||||
| TRT-P05 | 参数编辑及文件流程 | P03,P04 | 已验收 | 分组布局、应用/重新读取/导入/保存、保留程序、错误反馈 | E03 |
|
||||
| TRT-P06 | 回归及证据收口 | P03,P04,P05 | 已验收 | 专项、相关回归、构建、桌面/移动交互截图和像素检查通过;既有恢复偏差独立登记 | E02,E03,E04 |
|
||||
| TRT-R01 | 既有 checkpoint 恢复状态精度 | P06 | 待办 | 定位 HAL tick 与 TP queue/progress 恢复差异;将恢复测试的相等性变为有效断言,再验证固定 tick 连续与恢复轨迹 | E04:遗留项 |
|
||||
| TRT-R01 | 既有 checkpoint 恢复状态精度 | P06 | 已验收 | HAL/TP/解释器原生状态恢复;相等性强断言;固定 tick 续跑、暂停、子程序、trace、新会话及浏览器重启通过 | E09,E10 |
|
||||
| TRT-UI01 | 机床预览路径改为黄色 | P04 | 已验收 | 工作台普通刀具预览路径使用黄色材质 | E05 |
|
||||
| TRT-JOG01 | M428 旋转轴 Jog 刀尖跟随 | P03,P04 | 已验收 | A/B/C 连续和增量 Jog 使用 RTCP 补偿;松开、重复 Jog 不丢失模式;WASM 与机床模型刀尖位置一致;M429 对照 | E06 |
|
||||
| CFG-01 | 机床运行参数由 INI 驱动 | P06 | 已验收 | 实际限位、逐轴速度/加速度、周期随文件改变;无固定机型值覆盖 | E07 |
|
||||
| CFG-02 | 机型资源及界面配置入口 | CFG-01 | 已验收 | 资源路径与可调界面设置独立配置;子路径部署、非默认设置可用 | E07 |
|
||||
| CFG-03 | 配置审计与回归 | CFG-01,CFG-02 | 已验收 | 本专项 native/WASM/Worker/UI 验证通过,保留项与范围在 07 记录;旧三轴综合测试单列 | E07 |
|
||||
| CFG-R01 | 旧三轴综合验收流程与 tool gate | 无 | 已验收 | 运行中拒绝工具命令,空闲且队列完成后允许;急停/关机/故障拒绝;完整三轴流程通过 | E08 |
|
||||
|
||||
@@ -75,3 +75,55 @@ PLAYWRIGHT_MODULE=/home/mes006/.npm/_npx/e41f203b7505f1fb/node_modules/playwrigh
|
||||
- 回归通过:`node web/react/src/test_trt_parametric.mjs`、`node web/worker/test_xyzac_trt_case.mjs`、`node web/worker/test_xyzac_program_replace.mjs`、`node web/worker/test_xyzac_runtime_followups.mjs`、`node web/worker/test_boat_trt_programs.mjs`、`npm --prefix web/react run build`。构建保留既有大 chunk 提示。
|
||||
- `runtime_followups` 初次失败因未等待 HOME 完成便启动程序;现等待 WASM homed,并断言 PROGRAM_LOAD/RUN 成功后再验收。
|
||||
- job_id / PDF:不适用;使用原有 WASM,无核心 ABI 变更。本轮不声称修复 TRT-R01。
|
||||
|
||||
## E07 配置驱动改造 / 2026-09-18
|
||||
|
||||
本轮新增兼容的 WASM 配置 setter,核心已重建。WASM SHA-256:`213b55e64b45c14df5c40e59827fa8bb4903135cd788664bb08263f08a49738f`。
|
||||
|
||||
| 命令 / 场景 | 实测结果 | 证据 |
|
||||
| --- | --- | --- |
|
||||
| `bash scripts/build-wasm.sh` | WASM 测试通过,native 23/23 通过;随后扩展的 TP 配置边界单测也通过 | `/tmp/trt-config-build.log`,native `sim_linuxcnc_tp_test` |
|
||||
| `node web/worker/test_trt_configuration.mjs` | 两机型 X 实际速度均为 4,实际加速度约 4 且不超过配置 8;周期 .002;axis X 上限 15、joint X 上限 17;非法值、倍率越界拒绝;改速度后拒绝旧 checkpoint | [运行配置报告](../../coverage/configuration/trt-runtime.report.json) |
|
||||
| `PLAYWRIGHT_MODULE=... node scripts/test_configurable_ui_browser.mjs` | 配置为默认 XYZBC、自定义 INI/程序名、Jog 0.25/2.5、进给上限130%、自定义图标路径;紫背景143449像素、绿色路径85像素,零 pageerror | [浏览器报告](../../coverage/configuration/browser.report.json)、[截图](../../coverage/configuration/configured-ui.png) |
|
||||
| `npm --prefix web/react run build` | 类型检查及生产构建通过;保留大 chunk 提示 | 构建输出 |
|
||||
| 生产页面 `http://127.0.0.1:5175/dist/` | 子路径部署加载机床/程序成功,30个图标加载成功,零 pageerror | [子路径报告](../../coverage/configuration/subpath.report.json) |
|
||||
| `python3 scripts/audit_configuration_inputs.py` | 14 项迁移输入检查通过,记录源码/配置/WASM 哈希及保留类型 | [审计报告](../../coverage/configuration/input-audit.report.json) |
|
||||
| 真实 WASM 参数化与 RTCP Jog 回归 | 参数链路、两机型各77检查点、零角/单轴/复合姿态、重复停止通过 | [参数回归](../../coverage/configuration/parametric-regression.report.json)、[Jog 回归](../../coverage/configuration/rtcp-jog-regression.report.json) |
|
||||
| 浏览器实际 A-C-A-C、B-C-B-C 按住/松开 | 两机型各4轮通过,TCP模型误差小于1e-7场景单位,零 pageerror | [报告](../../coverage/configuration/rtcp-jog-browser.report.json)、[XYZAC](../../coverage/configuration/rtcp-jog-xyzac.png)、[XYZBC](../../coverage/configuration/rtcp-jog-xyzbc.png) |
|
||||
|
||||
相关回归命令通过:`node web/worker/test_boat_trt_programs.mjs`(两机型 completed)、`node web/worker/test_xyzac_runtime_followups.mjs`、`node web/worker/test_xyzac_trt_case.mjs`、`node web/worker/test_authoritative_kinematics_s6.mjs`、`node web/worker/test_motion_state.mjs`、`node web/workspace/test_sim_case_loader.mjs`、`node web/react/src/test_xyzac_threejs.mjs`、`node web/react/src/test_xyzac_tool_preview.mjs`。
|
||||
|
||||
旧 `node web/worker/test_axis3_selected_case.mjs` 未通过:脚本在程序/运动尚未空闲时发主轴命令,被 `axis_gate` 拒绝。诊断期间临时增加等待后发现 TOOL_PREPARE 的 `tool` action 缺少旧兼容 gate 分支;临时测试调整已撤回,正式登记 CFG-R01。本轮不声称全仓历史测试全通过,也不以此删除/放宽控制门控。
|
||||
|
||||
审计范围是已识别可配置输入,静态检查不是对任意数值字面量的完整证明。旧报告保留在原目录,本轮报告单独归档。截图已经查看;job_id/PDF 不适用。
|
||||
|
||||
## E08 三轴控制门控收口 / 2026-09-18
|
||||
|
||||
- `node web/app/test_axis_contract.mjs`:工具准备/换刀在运行、暂停、急停、关机、故障、非空运动队列和 AUTO 操作者模式下拒绝;空闲 MANUAL/MDI 允许交由 IO 状态机检查。来源为 `emctaskmain.cc::emcTaskCheckPreconditions` 的 PREPARE 等待 IO、LOAD 等待 motion+IO。
|
||||
- `node web/worker/test_axis3_selected_case.mjs`:完整流程通过,含运行中拒绝、等待真实队列结束、主轴/冷却、工具握手、急停和恢复。
|
||||
- `node web/worker/test_wasm_motion_checkpoint.mjs`:三轴原有恢复及 trace 导入回归通过,连续/恢复 hash 相同。
|
||||
- 报告:[门控](../../coverage/recovery/axis-gates.report.json)、[三轴流程](../../coverage/recovery/axis3-selected-case.report.json)、[三轴恢复](../../coverage/recovery/axis3-checkpoint.report.json)。E07 的 CFG-R01 遗留已在本轮关闭。
|
||||
|
||||
## E09 五轴原生恢复精度 / 2026-09-18
|
||||
|
||||
- `bash scripts/build-wasm.sh`:WASM 测试与 native 23/23 通过,日志 `/tmp/trt-recovery-build.log`。native TP 单测保存后继续 100 个伺服步,再恢复逐步对比;HAL 单测验证 tick、函数调用计数和完整状态字节一致。
|
||||
- `node web/worker/test_xyzac_recovery.mjs`:恢复点前后完整 hash 相同;旧的仅打印差异已改成硬断言;实际重发 M428 验证 switch_count 不增加。报告中 baseline/restored 比较相同 checkpoint tick,未重放尾部另列。
|
||||
- `node web/worker/test_trt_checkpoint_exact.mjs`:XYZAC/XYZBC 在子程序、暂停恢复、直线、圆弧和程序完成阶段分别比较 12 个连续任务 tick,共 120 个全状态比较,没有 divergent tick;另有 12 个恢复后 Jog 的当前关节采样验证。
|
||||
- 同时验证空程序、不同堆布局、外部子程序文件改变时使用快照中冻结的源、trace 导入、新传输会话 RECOVER/RESUME、损坏 native 块和构建不匹配在修改状态前拒绝。
|
||||
- `node web/worker/test_xyzac_runtime_followups.mjs`:关机记录重启恢复通过。
|
||||
- 报告:[恢复点](../../coverage/recovery/checkpoint-state.report.json)、[连续轨迹](../../coverage/recovery/exact-checkpoint.report.json)、[关机恢复](../../coverage/recovery/power-off.report.json);最终源码构建指纹、WASM SHA 和报告 SHA 见[验收汇总](../../coverage/recovery/acceptance.report.json)。
|
||||
|
||||
## E10 浏览器真实 OPFS 重启 / 2026-09-18
|
||||
|
||||
```bash
|
||||
PLAYWRIGHT_MODULE=/home/mes006/.npm/_npx/e41f203b7505f1fb/node_modules/playwright/index.mjs node scripts/test_checkpoint_reload_browser.mjs
|
||||
RECOVERY_MACHINE=xyzbc-trt PLAYWRIGHT_MODULE=/home/mes006/.npm/_npx/e41f203b7505f1fb/node_modules/playwright/index.mjs node scripts/test_checkpoint_reload_browser.mjs
|
||||
```
|
||||
|
||||
- 页面 `http://127.0.0.1:5174/`;上传真实测试程序、开机/回零/Jog、关机写入 OPFS、页面重载、恢复程序和预览、再次开机增量 Jog。
|
||||
- 两机型关机前/重启后关节数组严格相同,保留 `recovery-browser.ngc`,预览重建非空;继续 Jog 精确增加 0.1 mm;无 pageerror。记录约 103 KB,低于已有 1 MiB OPFS 上限。
|
||||
- [XYZAC 报告](../../coverage/recovery/browser-reload.report.json)、[截图](../../coverage/recovery/browser-reload.png);[XYZBC 报告](../../coverage/recovery/browser-reload-xyzbc-trt.report.json)、[截图](../../coverage/recovery/browser-reload-xyzbc-trt.png)。
|
||||
- `node web/worker/test_opfs_ui_state.mjs`、`node web/worker/test_opfs_worker_bridge.mjs`:双槽位校验、机型隔离、配置重载不注入旧缓存、不兼容记录提示通过,见[桥接报告](../../coverage/recovery/storage-bridge.report.json)。
|
||||
- `npm --prefix web/react run build`:类型检查及构建通过,仍有既有大 chunk 提示。
|
||||
- 中间浏览器测试发现 identity Jog 最后关节采样滞后,已修复当前 WASM 样本投影;`browser-reload-failure.png` 保留中间失败截图,最终结论以 verified 报告为准。
|
||||
- 边界:只允许匹配构建/配置的原生记录;旧不完整记录不近似恢复。TRT checkpoint 的后续事件须作为显式命令执行,不静默丢弃尾部;公开恢复向更早逻辑 tick 回退需要新传输会话,避免改写已有事件历史。OPFS 真实断电崩溃与所有浏览器矩阵不在本次结论内。job_id/PDF 不适用。
|
||||
|
||||
@@ -37,3 +37,35 @@ XYZAC 不支持 x-offset,XYZBC 不支持 y-offset。六项都可见,但不
|
||||
JOG_STOP/abort 停止运动不能隐式执行 M429。保留停止姿态对应的运动学模式,只清理待执行段和历史 ID 映射;重新初始化使用原有 reset 路径。Jog 的 native tag 是段 ID,与运动学模式不同,必须关联本段入队时的当前模式,不能沿用同编号的历史程序行。
|
||||
|
||||
RTCP 的判据为:旋转 Jog 时刀尖在工件坐标中的 XYZ 不变,机器 XYZ 按 WASM 逆解联动;Three.js 用相同关节投影。若刀尖恰好在 C 旋转轴上,单独转 C 可没有 XYZ 位移,这属于正确几何行为;验收特意先移动 X/Y 离开旋转中心。
|
||||
|
||||
## D10 / 2026-09-18 / 硬编码清理的确认范围
|
||||
|
||||
用户确认:项目自有代码中的机床参数、轴配置、路径和可调界面设置使用配置;保留数学常量、协议定义和上游 LinuxCNC。代码资产的固定网格拓扑、CSS/图标、测试输入和生成来源索引不通过机械替换数字来“清零”。配置改动必须能改变真实行为,不能只迁移到另一个常量表。
|
||||
|
||||
INI 是机床物理参数来源;JSON 是 Web 行为、资源注册和外观配置来源。图形外观参数不参与逆解。必需 INI 参数缺失即报错;可选原生规划策略/仿真 IO 参数使用有名称的配置默认值。
|
||||
|
||||
## D11 / 2026-09-18 / 完整运行配置和恢复
|
||||
|
||||
区分 AXIS 和 JOINT 限位,逐轴配置动力学;全局加速度上限以及轴上限均取约束较小者。新 C setter 是兼容增量导出;公开旧 ABI 函数签名保持,WASM 与前端应同时重新构建。
|
||||
|
||||
TASK/伺服周期必须为整数倍,IO/device-IO 按实际调度的 task 周期推进,避免模拟时钟分叉。恢复除几何外,还比较运行配置 hash;旧无 hash 的存档明确拒绝,不猜测它对应哪套限位和速度。
|
||||
|
||||
## D12 / 2026-09-18 / 验收与遗留项
|
||||
|
||||
配置专项的通过与全仓历史验收区分。三轴综合脚本的状态流程/工具门控列 CFG-R01;TP 恢复精度仍是 TRT-R01。保留真实失败信息。本轮新回归证据归档到 coverage/configuration,保留旧目录的原报告,以维持历史哈希可核验。
|
||||
|
||||
## D13 / 2026-09-18 / 原生恢复不重建剩余轨迹
|
||||
|
||||
只保存段端点/最终位置不能恢复 TP 的积分、混合、优化、队列进度。采用原生 TP/TC 快照,并保存解释器 side table、调用栈、HAL 内部状态及 Worker 的交接信息。TP 队列和程序缓冲区指针去除后重新绑定,用不同堆布局验证;源码/编译环境指纹限制跨构建读取。旧的缺失原生状态记录明确拒绝。
|
||||
|
||||
保存主程序及子程序输入闭包,恢复先在临时程序实例中验证后替换;不会因工作区子程序后来修改而改变已保存任务。机床配置 hash 包含解析 INI 的完整 hash,回零参数等变化同样使旧记录失效。
|
||||
|
||||
## D14 / 2026-09-18 / 恢复的传输与幂等边界
|
||||
|
||||
精确恢复和后续命令执行分开:TRT 不再静默忽略 checkpoint 后事件。新会话可重设传输序号并续跑;既有会话不能以公开 RECOVER 改写更晚的历史 tick,需新建传输会话。恢复操作回执用于阻止重复副作用,去掉回执中递归嵌套的 checkpoint/UI 大对象;重复操作投影当前机床状态。
|
||||
|
||||
## D15 / 2026-09-18 / 浏览器自动恢复
|
||||
|
||||
按机型分开保存,只在冷启动自动使用匹配配置和构建的记录。显式配置重载、机型切换以本次输入为准。不兼容缓存不删除,提示按当前配置初始化。
|
||||
|
||||
关机记录省去可由 WASM 重建的预览,并校验重建 hash;前端保持恢复出的程序文本和路径,不再次用默认示例覆盖。存储容量边界仍保留,过大记录须报告 quota,不声明已保存。运动增量投影直接使用当前规划器关节样本,避免 identity 模式下的最后一个采样滞后。
|
||||
|
||||
33
docs/trt-parametric/07-配置驱动改造.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# 配置驱动改造
|
||||
|
||||
2026-09-18 用户确认范围:项目自有代码中的机床参数、轴配置、路径和可调界面设置改为配置驱动;保留算法数学常量、协议定义和 LinuxCNC 上游源码。
|
||||
|
||||
## 配置入口
|
||||
|
||||
| 内容 | 输入来源 | 生效方式 |
|
||||
| --- | --- | --- |
|
||||
| TRT 几何、关节/轴限位、逐轴速度与加速度、周期、进给倍率上限 | 当前机床 INI:HAL、KINS、TRAJ、JOINT_n、AXIS_n、EMCMOT、TASK、DISPLAY | 关机空闲后重新读取 INI,Worker 再解析,再创建 WASM 实例 |
|
||||
| 圆弧混合策略 | INI `[TRAJ] ARC_BLEND_*`;缺项使用 web-settings.json 的 runtime.planner.blend | INI 重载 |
|
||||
| 机型入口、默认机型、程序与模型资源路径 | `web/workspace/machine-catalog.json` | 开发服务刷新;发布构建重新打包 |
|
||||
| 案例适配器、HAL 资源映射、预加载组件 | `web/workspace/simulation-resources.json` | 同上 |
|
||||
| Web 刷新节奏、逻辑采样批量、Jog 增量、初始图层、缩放范围 | `web/workspace/web-settings.json` 的 ui | 同上;这不是改变伺服周期的入口 |
|
||||
| 预览颜色、相机、材质、显示单位比例、模型基准高度 | web-settings.json 的 viewer | 同上;只影响图形表现,不参与数控逆解 |
|
||||
| 仿真 IO 容量与响应参数 | INI 可选 `[WEB_RUNTIME]`;缺项使用 web-settings.json 的 runtime.io | INI 重载;配置中的键名用大写,例如 SPINDLE_COUNT |
|
||||
| 旧三轴/Delta/Tripod 兼容仿真默认参数 | web-settings.json 的 runtime.compatibility | 旧兼容入口;不用于 TRT WASM 计算 |
|
||||
|
||||
## 约束与验收
|
||||
|
||||
- 同一值只保留一个输入来源。Worker 不信任调用者另外传来的 limits、周期和速度副本,重新从文件解析。
|
||||
- 无效、缺失、非有限值和不支持的配置拒绝加载,不能回退到 XYZAC 示例值。
|
||||
- AXIS 范围用于编程坐标 Jog,JOINT 范围用于运动学输出/回零检查,二者不能混用。不同轴有独立速度及加速度。
|
||||
- 当前固定 tick 执行器要求 TASK/CYCLE_TIME 为 SERVO_PERIOD 的整数倍,不再静默四舍五入。
|
||||
- IO/device-IO 的 cycle_time 跟随 task 周期;`SPINDLE_TOLERANCE` 是主轴到速容差,允许为 0,不是换刀延时。
|
||||
- 机型可通过 catalog 的 runtime_fixture 指定资源适配配置,自定义入口文件名不必修改加载器;相关资源仍须完整提供并通过 preflight。
|
||||
- JSON 修改在开发服务中刷新生效;静态发布包需重新构建。界面配置没有额外的磁盘写入 API。
|
||||
- LinuxCNC 上游源码未改;自有 C 适配层新增逐轴动力学和倍率上限 setter,原有 ABI 参数和协议编号不变,需要重建 WASM。
|
||||
- 数学常数、单位换算、位掩码、ABI 字段布局、状态枚举、误差阈值、模型原始网格拓扑、图标/CSS 资源定义及固定测试输入不是可调机床参数,不机械替换成无意义变量。
|
||||
- 历史报告、生成源索引、版本锁和第三方依赖锁不当作运行配置编辑。
|
||||
|
||||
验收必须包含修改 INI 后的实际运动速度、加速度约束、限位、周期,以及前端修改设置后的渲染变化;不能仅检查读取到的 JSON。
|
||||
|
||||
CFG-01/02/03 专项验收见 E07;接续的 CFG-R01、TRT-R01 也已按 E08/E09/E10 收口。恢复兼容边界见 D13/D14/D15;本专项通过不等于全仓全功能验收完成。
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
## 读取顺序
|
||||
|
||||
当前配置改造及接续任务 CFG-R01、TRT-R01 均已专项验收。重启后先读 [`nextTask-hardcoding.md`](../../nextTask-hardcoding.md);配置入口见 [07](07-配置驱动改造.md),最新验收见 E08/E09/E10。
|
||||
|
||||
1. 本索引 → [04-任务矩阵](04-任务矩阵.md):选择首个依赖满足且未验收的任务。
|
||||
2. [03-推进台账](03-推进台账.md):只读最新一轮和下一步。
|
||||
3. 按任务读取 [01-项目功能内容](01-项目功能内容.md)、[02-项目程序开发详细步骤](02-项目程序开发详细步骤.md) 和直接相关代码。
|
||||
@@ -11,7 +13,7 @@
|
||||
|
||||
每轮必须记录实际改动、命令、结果和下一任务。未运行的检查不能记为通过;历史通过任务仅在相关代码改变时回归。此目录是本专项接续入口,历史仿真/发布轨道仍保留原有指针,不据此扩大发布范围。
|
||||
|
||||
当前状态:`TRT-P01` 至 `TRT-P06`、`TRT-UI01`、`TRT-JOG01` 已验收。最新修复为 M428 下停止 Jog 后保留 RTCP,避免随后 A/B/C Jog 丢失 XYZ 补偿。下一项为矩阵中的 `TRT-R01`,属于既有恢复精度问题,不影响本专项参数应用流程。实现不依赖聊天记录。用户文字提及 Image #1,但本轮未收到可读取的图片资源,布局按现有结构预览实现。
|
||||
当前状态:`TRT-P01` 至 `TRT-P06`、`TRT-UI01`、`TRT-JOG01` 已验收。最新修复为 M428 下停止 Jog 后保留 RTCP,避免随后 A/B/C Jog 丢失 XYZ 补偿。配置改造 CFG-01/02/03 也已专项验收;CFG-R01 三轴门控和 TRT-R01 恢复精度也已收口,本专项暂无待办项。实现不依赖聊天记录。用户文字提及 Image #1,但本轮未收到可读取的图片资源,布局按现有结构预览实现。
|
||||
|
||||
本地体验地址:`http://127.0.0.1:5174/`。启动命令:`npm --prefix web/react run dev -- --port 5174 --strictPort`;该端口被占用时选择其他端口。
|
||||
|
||||
|
||||
48
nextTask-hardcoding.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# 配置驱动与恢复改造接续入口
|
||||
|
||||
更新时间:2026-09-18。用户已确认清理自有代码的机床参数、轴配置、路径、可调界面设置硬编码,保留数学/协议常量和上游 LinuxCNC;之后要求接续 CFG-R01、TRT-R01。以上专项均已收口。
|
||||
|
||||
## 当前状态
|
||||
|
||||
- 任务矩阵:`docs/trt-parametric/04-任务矩阵.md`;CFG-01/02/03、CFG-R01、TRT-R01 均为已验收。
|
||||
- 最新台账 R08(仓库保存)、证据 E08/E09/E10、决策 D13/D14/D15;R06 配置改造的历史证据仍保留。
|
||||
- 用户已于 2026-09-18 授权将 **R06 配置改造与 R07 精确恢复的完整依赖** 保存到指定仓库 main;本轮为该保存提交,实际提交 ID 以 Git 记录为准。
|
||||
- 验收汇总:`coverage/recovery/acceptance.report.json`;包含真实 WASM SHA、native 构建指纹和证据文件 SHA。
|
||||
- 本专项暂无未完成任务。后续按新需求推进;不要重跑已通过整轮测试,也不要自动认领其他历史发布轨道的阻塞任务。
|
||||
|
||||
## 实现与验证落点
|
||||
|
||||
1. 配置:`web/workspace/machine-catalog.json`、`simulation-resources.json`、`web-settings.json` 和 `trt-configuration.mjs`;完整说明见 `07-配置驱动改造.md`。INI 输入包含运行参数及回零信息的完整 hash;Worker 从文件重新解析。
|
||||
2. 三轴:`web/app/axis-contract.mjs` 和 `test_axis3_selected_case.mjs`。工具准备/换刀在开机、空闲、操作者模式、队列为空时交由 IO 状态机处理;运行、急停、故障等拒绝,未删除旧断言。
|
||||
3. 恢复:自有 C 适配层新增 TP/TC、解释器/程序、HAL、核心及协议/事件快照 API;`web/worker/native-checkpoint.mjs` 做带构建指纹、长度/hash 校验的稀疏块编解码;数学计算仍来自 WASM。
|
||||
4. 程序恢复:保存参数、调用栈及冻结子程序源,验证候选程序后替换;TP 原生队列/进度/混合状态直接恢复。`sim-session.mjs` 保存待处理运动/切换等交接信息;HAL 不再重复 step。
|
||||
5. 幂等/内存:checkpoint 回执剔除递归旧快照,保留操作身份;恢复后重复 M428 不重新执行,返回当前机床投影。
|
||||
6. 浏览器:OPFS 按机型隔离,只有冷启动自动采用兼容缓存;配置重载不覆盖当前输入。省去可重建预览,重建后验证 hash;App 保留恢复出来的程序而非重新加载船体示例。
|
||||
7. 新发现并修复:identity Jog 的增量响应使用当前 WASM 规划器关节,避免最后采样落后。
|
||||
|
||||
## 已完成验证
|
||||
|
||||
- `bash scripts/build-wasm.sh`:WASM 测试通过,native 23/23;native TP 保存后继续 100 个伺服步逐步对比,HAL tick/函数计数和字节恢复验证。日志 `coverage/recovery/build.log`。
|
||||
- `node web/worker/test_trt_checkpoint_exact.mjs`:两机型五阶段共120个连续任务tick全状态hash一致,另12个 Jog 样本;覆盖不同堆地址、暂停/子程序/圆弧/结束、空程序、trace、新传输会话、冻结子程序、损坏块和不同构建拒绝。
|
||||
- `node web/worker/test_xyzac_recovery.mjs`:同一恢复点完整hash相同,实际重发 M428 不重复切换。
|
||||
- `node web/worker/test_axis3_selected_case.mjs`、`node web/app/test_axis_contract.mjs`、`node web/worker/test_wasm_motion_checkpoint.mjs`:通过。
|
||||
- `node web/worker/test_xyzac_runtime_followups.mjs`、`node web/worker/test_opfs_ui_state.mjs`、`node web/worker/test_opfs_worker_bridge.mjs`、`node web/worker/test_trt_configuration.mjs`:通过。
|
||||
- `scripts/test_checkpoint_reload_browser.mjs`:真实 OPFS 关机保存/页面重载恢复,两机型位置严格相同,原程序/预览保留,之后 Jog 正常。XYZBC 使用 `RECOVERY_MACHINE=xyzbc-trt`。
|
||||
- `scripts/test_trt_rtcp_jog_browser.mjs`:两机型 A/B/C Jog 回归通过;产物已归档 `coverage/recovery/`。
|
||||
- `npm --prefix web/react run build`:通过,已有大 chunk 提示保留。
|
||||
- Playwright 环境:`PLAYWRIGHT_MODULE=/home/mes006/.npm/_npx/e41f203b7505f1fb/node_modules/playwright/index.mjs`;Chrome `/usr/bin/google-chrome`。
|
||||
|
||||
## 已确定的恢复边界
|
||||
|
||||
- 原生记录只允许匹配构建和配置。旧缺失原生状态的记录不能近似恢复;自动缓存不兼容时提示并按当前配置启动,原缓存不删除。
|
||||
- TRT checkpoint 后的事件不能静默忽略:须恢复后显式执行后续命令。公开 RECOVER 向更早逻辑tick回退需要新传输会话,避免改写历史;原生状态可在新会话重设传输序号后继续执行。
|
||||
- OPFS 原有1MiB限制仍在,超限须报告quota;不宣称物理断电崩溃和全浏览器矩阵完成。
|
||||
- 本次是专项验收,不等于全 LinuxCNC parity,不更改历史 release gate/context lock。
|
||||
|
||||
## 工作区与保存
|
||||
|
||||
- 开发地址 `http://127.0.0.1:5174/`;不存在时运行 `npm --prefix web/react run dev -- --port 5174 --strictPort`。
|
||||
- 最新代码必须与重建的 WASM 配套;构建产物在 Git ignore 中,保存源码后其他环境需运行 build-wasm.sh。
|
||||
- `coverage/configuration/`、`coverage/recovery/` 是本轮证据;`coverage/trt-parametric/` 保持先前已提交历史版本,不破坏历史哈希。
|
||||
- 两份大型 tick trace、linuxcnc 子模块原有本地改动、`scripts/.tmp_test_nc_files_xyzac.mjs`、用户 Copy.png 不属于本轮,保留原样,不混入提交。
|
||||
- 指定远程为 `https://gitea.meswork.com/wangdequan/linuxcncWebWASM.git`。本轮保存已获授权,推送后核验远端 main 与本地 HEAD 一致;账号口令不写入仓库。
|
||||
59
scripts/audit_configuration_inputs.py
Normal file
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Audit the configuration input migration, without classifying every literal as an error."""
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def main():
|
||||
sources = {
|
||||
"machine_catalog": "web/workspace/machine-catalog.json",
|
||||
"resource_catalog": "web/workspace/simulation-resources.json",
|
||||
"web_settings": "web/workspace/web-settings.json",
|
||||
"ini_reader": "web/workspace/trt-configuration.mjs",
|
||||
"worker": "web/worker/sim-session.mjs",
|
||||
"tp_adapter": "web/worker/wasm-linuxcnc-tp.mjs",
|
||||
"tp_core": "wasm-core/src/sim_linuxcnc_tp.c",
|
||||
"kinematics_adapter": "web/worker/wasm-xyzac-kinematics.mjs",
|
||||
"case_loader": "web/workspace/sim-case-loader.mjs",
|
||||
"model": "web/react/src/xyzac-machine-model.ts",
|
||||
"machine_view": "web/react/src/XyzacMachineView.tsx",
|
||||
}
|
||||
content = {key: (ROOT / path).read_text() for key, path in sources.items()}
|
||||
checks = {
|
||||
"machine_catalog_parses": bool(json.loads(content["machine_catalog"])["machines"]),
|
||||
"resource_catalog_parses": bool(json.loads(content["resource_catalog"])["runtime_fixtures"]),
|
||||
"settings_parse": bool(json.loads(content["web_settings"])["viewer"]),
|
||||
"worker_reparses_ini": "...readTrtConfiguration(configuration)" in content["worker"],
|
||||
"axis_limits_from_ini": 'limits(`AXIS_${axis.toUpperCase()}`)' in content["ini_reader"],
|
||||
"joint_limits_from_ini": 'limits(`JOINT_${joint}`)' in content["ini_reader"],
|
||||
"no_kinematics_example_limits": "DEFAULT_LIMITS" not in content["kinematics_adapter"],
|
||||
"no_loader_fixed_timing": "servo_cycle_time: 0.001" not in content["case_loader"],
|
||||
"no_loader_axis3_override": "[-240, 100]" not in content["case_loader"],
|
||||
"native_axis_dynamics_input": "sim_linuxcnc_tp_set_axis_dynamics" in content["tp_core"] and "sim_linuxcnc_tp_set_axis_dynamics" in content["tp_adapter"],
|
||||
"no_native_xyzac_speed_override": "fmin(20.0, velocity_limit)" not in content["tp_core"] and "fmin(30.0, velocity_limit)" not in content["tp_core"],
|
||||
"native_acceleration_upper_bound": "*acceleration = fmin" in content["tp_core"] and "*acceleration = fmax" not in content["tp_core"],
|
||||
"model_uses_visual_configuration": "WEB_SETTINGS.viewer.models" in content["model"] and "WEB_SETTINGS.viewer.materials" in content["model"],
|
||||
"machine_path_color_configured": "VIEWER.colors.path" in content["machine_view"],
|
||||
}
|
||||
report = {
|
||||
"report_id": "configuration-input-audit-v1",
|
||||
"status": "verified" if all(checks.values()) else "failed",
|
||||
"scope": "configurable inputs; not a zero-literals claim",
|
||||
"checks": checks,
|
||||
"sources": [{"path": path, "sha256": hashlib.sha256((ROOT / path).read_bytes()).hexdigest()} for path in sources.values()],
|
||||
"wasm_sha256": hashlib.sha256((ROOT / "build-wasm/linuxcnc-sim-core.wasm").read_bytes()).hexdigest(),
|
||||
"retained": ["mathematical constants and tolerances", "ABI layouts, enums, and capacities", "source model mesh topology", "CSS/icon assets", "test fixtures and historical evidence", "upstream LinuxCNC and dependency locks"],
|
||||
}
|
||||
output = ROOT / "coverage/configuration/input-audit.report.json"
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
output.write_text(json.dumps(report, indent=2) + "\n")
|
||||
print(json.dumps({"report_id": report["report_id"], "status": report["status"], "checks": len(checks)}))
|
||||
if report["status"] != "verified":
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
34
scripts/generate_recovery_acceptance.mjs
Normal file
@@ -0,0 +1,34 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import createModule from "../build-wasm/linuxcnc-sim-core.js";
|
||||
|
||||
const root = new URL("../", import.meta.url);
|
||||
const names = ["axis3-selected-case.report.json", "axis-gates.report.json", "axis3-checkpoint.report.json", "checkpoint-state.report.json", "exact-checkpoint.report.json", "power-off.report.json", "storage-bridge.report.json", "browser-reload.report.json", "browser-reload-xyzbc-trt.report.json"];
|
||||
const evidence = [];
|
||||
for (const name of names) {
|
||||
const path = `coverage/recovery/${name}`;
|
||||
const bytes = await readFile(new URL(path, root));
|
||||
const report = JSON.parse(bytes);
|
||||
assert.equal(report.status, "verified", path);
|
||||
evidence.push({ path, sha256: createHash("sha256").update(bytes).digest("hex") });
|
||||
}
|
||||
const wasmBinary = await readFile(new URL("build-wasm/linuxcnc-sim-core.wasm", root));
|
||||
const core = await createModule({ wasmBinary });
|
||||
const pointer = core._sim_checkpoint_build_id();
|
||||
const buildId = new TextDecoder().decode(core.HEAPU8.subarray(pointer, pointer + 64));
|
||||
assert.match(buildId, /^[a-f0-9]{64}$/);
|
||||
const exact = JSON.parse(await readFile(new URL("coverage/recovery/exact-checkpoint.report.json", root)));
|
||||
const report = {
|
||||
report_id: "cfg-r01-trt-r01-acceptance-v1", status: "verified",
|
||||
tasks: ["CFG-R01", "TRT-R01"],
|
||||
native_build_id: buildId,
|
||||
wasm_sha256: createHash("sha256").update(wasmBinary).digest("hex"),
|
||||
checkpoint_sizes: Object.fromEntries(["sim_core", "sim_protocol", "sim_event", "sim_program", "sim_linuxcnc_tp", "sim_hal"].map((name) => [name, core[`_${name}_checkpoint_size`]() ])),
|
||||
continuation_samples: exact.rows.filter((row) => row.phase !== "post-recovery-jog").reduce((sum, row) => sum + (row.samples ?? 0), 0),
|
||||
first_divergent_tick: null,
|
||||
boundaries: ["matching native build and machine configuration required", "old snapshots lacking native state are rejected", "TRT checkpoint event tails require explicit subsequent commands", "backward public recovery requires a fresh transport session", "OPFS retains its existing quota; no physical crash or cross-browser parity claim"],
|
||||
evidence,
|
||||
};
|
||||
await writeFile(new URL("coverage/recovery/acceptance.report.json", root), JSON.stringify(report, null, 2) + "\n");
|
||||
console.log(JSON.stringify({ report_id: report.report_id, status: report.status, continuation_samples: report.continuation_samples, evidence: evidence.length, native_build_id: buildId }));
|
||||
75
scripts/test_checkpoint_reload_browser.mjs
Normal file
@@ -0,0 +1,75 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { pathToFileURL } from "node:url";
|
||||
const { chromium } = await import(pathToFileURL(process.env.PLAYWRIGHT_MODULE).href);
|
||||
const output = new URL("../coverage/recovery/", import.meta.url);
|
||||
await mkdir(output, { recursive: true });
|
||||
const browser = await chromium.launch({ executablePath: process.env.BROWSER_EXECUTABLE_PATH ?? "/usr/bin/google-chrome", headless: true, args: ["--no-sandbox", "--use-angle=swiftshader", "--enable-unsafe-swiftshader"] });
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
|
||||
const selectedMachine = process.env.RECOVERY_MACHINE ?? "xyzac-trt";
|
||||
const suffix = selectedMachine === "xyzac-trt" ? "" : `-${selectedMachine}`;
|
||||
if (selectedMachine !== "xyzac-trt") {
|
||||
const catalog = JSON.parse(await readFile(new URL("../web/workspace/machine-catalog.json", import.meta.url), "utf8"));
|
||||
catalog.default_machine = selectedMachine;
|
||||
await page.route(/machine-catalog\.json\?import/, (route) => route.fulfill({ contentType: "text/javascript", body: `export default ${JSON.stringify(catalog)};` }));
|
||||
}
|
||||
const errors = []; page.on("pageerror", (error) => errors.push(error.message));
|
||||
await page.addInitScript(() => {
|
||||
window.recoveryMessages = [];
|
||||
const NativeWorker = window.Worker;
|
||||
window.Worker = class extends NativeWorker {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.addEventListener("message", ({ data }) => {
|
||||
if (!data.payload?.command) return;
|
||||
window.recoveryMessages.push({ command: data.payload.command, kind: data.kind, status: data.payload.status, persistence: data.payload.recovery_persistence, recovery: data.payload.power_off_recovery?.status });
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
const control = (id) => page.locator(`[data-axis-control="${id}"]`);
|
||||
const ready = () => page.waitForFunction(() => document.querySelector("main")?.dataset.axisProgramLoading === "false");
|
||||
const joints = () => page.locator(".xyzac-machine-view").evaluate((node) => JSON.parse(node.dataset.xyzacJoints));
|
||||
const report = { report_id: "checkpoint-browser-reload-v1", machine: selectedMachine, status: "running" };
|
||||
try {
|
||||
await page.goto(process.env.XYZAC_PANEL_URL ?? "http://127.0.0.1:5174/"); await ready();
|
||||
await control("program-file-input").setInputFiles({ name: "recovery-browser.ngc", mimeType: "text/plain", buffer: Buffer.from("G21 G90\nG1 X8 Y3 Z2 F600\nM2\n") });
|
||||
await page.waitForFunction(() => document.querySelector("main")?.dataset.axisProgramPath?.endsWith("recovery-browser.ngc") && document.querySelector("main")?.dataset.axisProgramLoading === "false");
|
||||
await control("toolbar-power").click(); await control("manual-home-all").click();
|
||||
await page.waitForFunction(() => !document.querySelector('[data-axis-control="manual-axis-x"]').disabled);
|
||||
await control("manual-axis-x").check();
|
||||
const button = await control("manual-jog-positive").boundingBox();
|
||||
await page.mouse.move(button.x + button.width / 2, button.y + button.height / 2); await page.mouse.down();
|
||||
await page.waitForFunction(() => JSON.parse(document.querySelector(".xyzac-machine-view").dataset.xyzacJoints)[0] > 2);
|
||||
await page.mouse.up();
|
||||
await page.waitForFunction(() => window.recoveryMessages.some((row) => row.command === "JOG_STOP"));
|
||||
report.before = await joints();
|
||||
await control("toolbar-power").click();
|
||||
await page.waitForFunction(() => window.recoveryMessages.some((row) => row.command === "POWER" && row.persistence?.status === "verified" && row.persistence?.bytes > 0));
|
||||
report.saved = await page.evaluate(() => window.recoveryMessages.filter((row) => row.command === "POWER" && row.persistence?.bytes > 0).at(-1).persistence);
|
||||
await page.reload(); await ready();
|
||||
await page.waitForFunction(() => window.recoveryMessages.some((row) => row.command === "LOAD_WORKSPACE" && row.recovery === "verified"));
|
||||
report.after = await joints();
|
||||
report.preview_segments = Number(await page.locator(".xyzac-tool-preview").getAttribute("data-xyzac-toolpath-count"));
|
||||
assert.ok(report.preview_segments > 0, "restore must publish the regenerated preview");
|
||||
assert.deepEqual(report.after, report.before);
|
||||
assert.match(await page.locator("main").getAttribute("data-axis-program-path"), /recovery-browser\.ngc$/);
|
||||
assert.equal(await page.evaluate(() => window.recoveryMessages.filter((row) => row.command === "PROGRAM_LOAD").length), 0, "startup must not overwrite the recovered program with the packaged demo");
|
||||
await control("toolbar-power").click();
|
||||
await control("manual-jog-increment").selectOption("0.1000");
|
||||
await control("manual-axis-x").check(); await control("manual-jog-positive").click();
|
||||
await page.waitForFunction((start) => JSON.parse(document.querySelector(".xyzac-machine-view").dataset.xyzacJoints)[0] >= start + .0999, report.after[0]);
|
||||
report.continued = await joints();
|
||||
assert.deepEqual(errors, []); report.status = "verified";
|
||||
await page.screenshot({ path: new URL(`browser-reload${suffix}.png`, output).pathname, fullPage: true });
|
||||
} catch (error) {
|
||||
report.status = "failed"; report.error = String(error.stack ?? error);
|
||||
report.messages = await page.evaluate(() => window.recoveryMessages);
|
||||
await page.screenshot({ path: new URL(`browser-reload-failure${suffix}.png`, output).pathname, fullPage: true });
|
||||
throw error;
|
||||
} finally {
|
||||
report.browser_errors = errors;
|
||||
await writeFile(new URL(`browser-reload${suffix}.report.json`, output), JSON.stringify(report, null, 2) + "\n");
|
||||
await browser.close();
|
||||
}
|
||||
console.log(JSON.stringify(report));
|
||||
65
scripts/test_configurable_ui_browser.mjs
Normal file
@@ -0,0 +1,65 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
||||
import { pathToFileURL } from "node:url";
|
||||
const { chromium } = await import(pathToFileURL(process.env.PLAYWRIGHT_MODULE).href);
|
||||
const root = new URL("../", import.meta.url);
|
||||
const settings = JSON.parse(await readFile(new URL("web/workspace/web-settings.json", root), "utf8"));
|
||||
const catalog = JSON.parse(await readFile(new URL("web/workspace/machine-catalog.json", root), "utf8"));
|
||||
settings.ui.jog_increments = ["0.25", "2.5"];
|
||||
settings.viewer.colors.background = "#550055";
|
||||
settings.viewer.colors.path = "#00ff00";
|
||||
settings.assets.axis_images = "configured-icons";
|
||||
catalog.default_machine = "xyzbc-trt";
|
||||
const sourceIni = catalog.machines[1].entrypoint;
|
||||
catalog.machines[1].entrypoint = sourceIni.replace(/[^/]+$/, "configured-machine.ini");
|
||||
catalog.machines[1].program = catalog.machines[1].program.replace(/[^/]+$/, "configured-preview.ngc");
|
||||
const browser = await chromium.launch({ executablePath: process.env.BROWSER_EXECUTABLE_PATH ?? "/usr/bin/google-chrome", headless: true, args: ["--no-sandbox", "--use-angle=swiftshader", "--enable-unsafe-swiftshader"] });
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
|
||||
const errors = []; page.on("pageerror", (error) => errors.push(error.message));
|
||||
const configRequests = [], iconRequests = [];
|
||||
await page.route(/web-settings\.json\?import|machine-catalog\.json\?import/, async (route) => {
|
||||
configRequests.push(route.request().url());
|
||||
const value = route.request().url().includes("web-settings.json") ? settings : catalog;
|
||||
await route.fulfill({ contentType: "text/javascript", body: `export default ${JSON.stringify(value)};` });
|
||||
});
|
||||
await page.route("**/configured-icons/*", async (route) => {
|
||||
iconRequests.push(route.request().url());
|
||||
const name = new URL(route.request().url()).pathname.split("/").at(-1);
|
||||
const response = await route.fetch({ url: new URL(`/linuxcnc/share/axis/images/${name}`, route.request().url()).href });
|
||||
await route.fulfill({ response });
|
||||
});
|
||||
await page.route("**/configured-machine.ini", async (route) => {
|
||||
const response = await route.fetch({ url: new URL(`/${sourceIni}`, route.request().url()).href });
|
||||
const content = (await response.text()).replace(/MAX_FEED_OVERRIDE\s*=\s*[^\r\n]+/, "MAX_FEED_OVERRIDE = 1.3");
|
||||
await route.fulfill({ response, body: content });
|
||||
});
|
||||
await page.route("**/configured-preview.ngc", (route) => route.fulfill({ contentType: "text/plain", body: "G21 G90\nG0 X-30 Y-30 Z20\nG1 X30 Y-30 F600\nY30\nX-30\nY-30\nM2\n" }));
|
||||
try {
|
||||
await page.goto(process.env.XYZAC_PANEL_URL ?? "http://127.0.0.1:5174/");
|
||||
await page.waitForFunction(() => document.querySelector("main")?.dataset.axisProgramLoading === "false");
|
||||
assert.equal(await page.locator(".xyzac-machine-view").getAttribute("data-xyzac-model-id"), "xyzbc-trt");
|
||||
assert.equal(await page.locator("main").getAttribute("data-axis-program-path"), catalog.machines[1].program);
|
||||
assert.deepEqual(await page.locator('[data-axis-control="manual-jog-increment"] option').allTextContents(), ["Continuous", "0.25", "2.5"]);
|
||||
assert.equal(await page.locator('[data-axis-control="override-feed"]').getAttribute("max"), "130");
|
||||
assert.ok(configRequests.length >= 2); assert.ok(iconRequests.length > 10);
|
||||
const colors = await page.locator('.xyzac-machine-view canvas[data-engine]').evaluate(async (canvas) => {
|
||||
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
||||
const gl = canvas.getContext("webgl2"); const data = new Uint8Array(canvas.width * canvas.height * 4);
|
||||
gl.readPixels(0, 0, canvas.width, canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, data);
|
||||
let purple = 0, green = 0;
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
if (data[i] > 30 && data[i + 2] > 30 && data[i + 1] < 10) purple++;
|
||||
if (data[i + 1] > 150 && data[i + 1] > data[i] + 30 && data[i + 1] > data[i + 2] + 30) green++;
|
||||
}
|
||||
return { purple, green };
|
||||
});
|
||||
assert.ok(colors.purple > 1000, JSON.stringify(colors));
|
||||
await page.screenshot({ path: new URL("coverage/configuration/configured-ui.png", root).pathname, fullPage: true });
|
||||
assert.ok(colors.green > 0, JSON.stringify(colors));
|
||||
assert.deepEqual(errors, []);
|
||||
const output = new URL("coverage/configuration/", root); await mkdir(output, { recursive: true });
|
||||
await page.screenshot({ path: new URL("configured-ui.png", output).pathname, fullPage: true });
|
||||
const report = { report_id: "configurable-ui-browser-v1", status: "verified", default_machine: catalog.default_machine, entrypoint: catalog.machines[1].entrypoint, program: catalog.machines[1].program, increments: settings.ui.jog_increments, feed_max: 1.3, colors, configured_icons: iconRequests.length, browser_errors: errors };
|
||||
await writeFile(new URL("browser.report.json", output), JSON.stringify(report, null, 2) + "\n");
|
||||
console.log(JSON.stringify(report));
|
||||
} finally { await browser.close(); }
|
||||
@@ -25,6 +25,10 @@ uint32_t sim_read_snapshot(uint8_t *out, uint32_t capacity);
|
||||
uint32_t sim_read_event(uint8_t *out, uint32_t capacity);
|
||||
int32_t sim_export_state(uint8_t *out, uint32_t capacity);
|
||||
int32_t sim_import_state(const uint8_t *data, uint32_t len);
|
||||
uint32_t sim_core_checkpoint_size(void);
|
||||
const char *sim_checkpoint_build_id(void);
|
||||
int32_t sim_core_checkpoint_export(uint8_t *output, uint32_t capacity);
|
||||
int32_t sim_core_checkpoint_restore(const uint8_t *input, uint32_t length);
|
||||
void sim_destroy(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -27,6 +27,9 @@ typedef struct sim_event_record {
|
||||
} sim_event_record;
|
||||
|
||||
void sim_event_reset(void);
|
||||
uint32_t sim_event_checkpoint_size(void);
|
||||
int32_t sim_event_checkpoint_export(uint8_t *output, uint32_t capacity);
|
||||
int32_t sim_event_checkpoint_restore(const uint8_t *input, uint32_t length);
|
||||
int32_t sim_event_append(uint32_t seq, uint32_t tick, const char *kind,
|
||||
const uint8_t *payload, uint32_t payload_len);
|
||||
uint32_t sim_event_count(void);
|
||||
|
||||
@@ -14,6 +14,10 @@ extern "C" {
|
||||
#define SIM_HAL_COMPONENT_CAPACITY 64U
|
||||
#define SIM_HAL_PARAM_CAPACITY 256U
|
||||
|
||||
uint32_t sim_hal_checkpoint_size(void);
|
||||
int32_t sim_hal_checkpoint_export(uint8_t *output, uint32_t capacity);
|
||||
int32_t sim_hal_checkpoint_restore(const uint8_t *input, uint32_t length);
|
||||
|
||||
enum sim_hal_result {
|
||||
SIM_HAL_OK = 0,
|
||||
SIM_HAL_ERR_ARGUMENT = -300,
|
||||
|
||||
@@ -74,6 +74,9 @@ int sim_interpreter_set_coordinates(sim_interpreter *interpreter, const sim_coor
|
||||
int sim_interpreter_sync_position(sim_interpreter *interpreter, const sim_posemath_pose *position);
|
||||
|
||||
int sim_interpreter_init(sim_interpreter *interpreter);
|
||||
uint32_t sim_interpreter_checkpoint_size(void);
|
||||
int32_t sim_interpreter_checkpoint_export(const sim_interpreter *interpreter, uint8_t *output, uint32_t capacity);
|
||||
int32_t sim_interpreter_checkpoint_restore(sim_interpreter *interpreter, const uint8_t *input, uint32_t length);
|
||||
int sim_interpreter_reset(sim_interpreter *interpreter);
|
||||
int sim_interpreter_execute(sim_interpreter *interpreter, const char *block, uint32_t length);
|
||||
uint32_t sim_interpreter_size(void);
|
||||
|
||||
@@ -18,6 +18,9 @@ typedef struct sim_linuxcnc_tp_segment {
|
||||
} sim_linuxcnc_tp_segment;
|
||||
|
||||
uint32_t sim_linuxcnc_tp_size(void);
|
||||
uint32_t sim_linuxcnc_tp_checkpoint_size(void);
|
||||
int32_t sim_linuxcnc_tp_checkpoint_export(const sim_linuxcnc_tp *planner, uint8_t *output, uint32_t capacity);
|
||||
int32_t sim_linuxcnc_tp_checkpoint_restore(sim_linuxcnc_tp *planner, const uint8_t *input, uint32_t length);
|
||||
uint32_t sim_linuxcnc_tp_queue_capacity(const sim_linuxcnc_tp *planner);
|
||||
int32_t sim_linuxcnc_tp_init(sim_linuxcnc_tp *planner, double cycle_time,
|
||||
double velocity_limit, double acceleration_limit);
|
||||
@@ -27,6 +30,10 @@ int32_t sim_linuxcnc_tp_set_feed_override(sim_linuxcnc_tp *planner, double scale
|
||||
int32_t sim_linuxcnc_tp_set_velocity_scale(sim_linuxcnc_tp *planner, double scale);
|
||||
int32_t sim_linuxcnc_tp_set_jog_scale(sim_linuxcnc_tp *planner, double scale);
|
||||
int32_t sim_linuxcnc_tp_set_axis_limit(sim_linuxcnc_tp *planner, int axis, double minimum, double maximum);
|
||||
int32_t sim_linuxcnc_tp_set_axis_dynamics(sim_linuxcnc_tp *planner, int axis, double velocity, double acceleration);
|
||||
int32_t sim_linuxcnc_tp_set_feed_limit(sim_linuxcnc_tp *planner, double maximum);
|
||||
int32_t sim_linuxcnc_tp_set_scale_minimum(sim_linuxcnc_tp *planner, double minimum);
|
||||
int32_t sim_linuxcnc_tp_set_blend(sim_linuxcnc_tp *planner, int enable, int fallback, int depth, int gap, double ramp, double kink);
|
||||
int32_t sim_linuxcnc_tp_jog(sim_linuxcnc_tp *planner, int axis, int direction, double increment, uint32_t tag);
|
||||
double sim_linuxcnc_tp_velocity_scale(const sim_linuxcnc_tp *planner);
|
||||
double sim_linuxcnc_tp_velocity_limit(const sim_linuxcnc_tp *planner);
|
||||
|
||||
@@ -83,6 +83,9 @@ typedef struct sim_program {
|
||||
} sim_program;
|
||||
|
||||
uint32_t sim_program_size(void);
|
||||
uint32_t sim_program_checkpoint_size(void);
|
||||
int32_t sim_program_checkpoint_export(const sim_program *program, uint8_t *output, uint32_t capacity);
|
||||
int32_t sim_program_checkpoint_restore(sim_program *program, const uint8_t *input, uint32_t length);
|
||||
int32_t sim_program_requires_subroutines(const char *source, uint32_t length);
|
||||
int32_t sim_program_init(sim_program *program);
|
||||
int32_t sim_program_reset(sim_program *program);
|
||||
|
||||
@@ -37,6 +37,9 @@ int32_t sim_protocol_resume(void);
|
||||
int32_t sim_protocol_state_value(void);
|
||||
uint32_t sim_protocol_last_seq(void);
|
||||
uint32_t sim_protocol_tick(void);
|
||||
uint32_t sim_protocol_checkpoint_size(void);
|
||||
int32_t sim_protocol_checkpoint_export(uint8_t *output, uint32_t capacity);
|
||||
int32_t sim_protocol_checkpoint_restore(const uint8_t *input, uint32_t length);
|
||||
int32_t sim_protocol_set_tick(uint32_t simulation_tick);
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -11,6 +11,23 @@ typedef struct {
|
||||
|
||||
static sim_state state;
|
||||
|
||||
#ifndef SIM_CHECKPOINT_BUILD_ID
|
||||
#define SIM_CHECKPOINT_BUILD_ID "unversioned"
|
||||
#endif
|
||||
const char *sim_checkpoint_build_id(void) { return SIM_CHECKPOINT_BUILD_ID; }
|
||||
|
||||
uint32_t sim_core_checkpoint_size(void) { return sizeof(state); }
|
||||
int32_t sim_core_checkpoint_export(uint8_t *output, uint32_t capacity) {
|
||||
if (output == NULL || capacity != sizeof(state)) return SIM_ERR_INVALID_ARGUMENT;
|
||||
memcpy(output, &state, sizeof(state)); return SIM_OK;
|
||||
}
|
||||
int32_t sim_core_checkpoint_restore(const uint8_t *input, uint32_t length) {
|
||||
if (input == NULL || length != sizeof(state)) return SIM_ERR_INVALID_ARGUMENT;
|
||||
const sim_state *saved = (const sim_state *)input;
|
||||
if (saved->created > 1 || saved->loaded > 1 || saved->pending_event > 1) return SIM_ERR_INVALID_ARGUMENT;
|
||||
memcpy(&state, saved, sizeof(state)); return SIM_OK;
|
||||
}
|
||||
|
||||
static int valid_buffer(const uint8_t *data, uint32_t len) {
|
||||
return len == 0 || data != NULL;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,14 @@ typedef struct sim_event_state {
|
||||
|
||||
static sim_event_state state;
|
||||
|
||||
uint32_t sim_event_checkpoint_size(void) { return sizeof(state); }
|
||||
int32_t sim_event_checkpoint_export(uint8_t *output, uint32_t capacity) {
|
||||
return sim_event_export(output, capacity) == sizeof(state) ? SIM_EVENT_OK : SIM_EVENT_ERR_BUFFER;
|
||||
}
|
||||
int32_t sim_event_checkpoint_restore(const uint8_t *input, uint32_t length) {
|
||||
return sim_event_import(input, length);
|
||||
}
|
||||
|
||||
static uint64_t hash_payload(const uint8_t *payload, uint32_t payload_len) {
|
||||
uint64_t hash = 1469598103934665603ULL;
|
||||
uint32_t index;
|
||||
|
||||
@@ -87,6 +87,24 @@ typedef struct {
|
||||
|
||||
static hal_state state;
|
||||
|
||||
uint32_t sim_hal_checkpoint_size(void) { return (uint32_t)sizeof(hal_state); }
|
||||
|
||||
int32_t sim_hal_checkpoint_export(uint8_t *output, uint32_t capacity) {
|
||||
if (output == NULL || capacity != sizeof(state)) return SIM_HAL_ERR_ARGUMENT;
|
||||
memcpy(output, &state, sizeof(state));
|
||||
return SIM_HAL_OK;
|
||||
}
|
||||
|
||||
int32_t sim_hal_checkpoint_restore(const uint8_t *input, uint32_t length) {
|
||||
if (input == NULL || length != sizeof(state)) return SIM_HAL_ERR_ARGUMENT;
|
||||
const hal_state *saved = (const hal_state *)input;
|
||||
for (uint32_t i = 0; i < SIM_HAL_PIN_CAPACITY; ++i) if (saved->pins[i].used && (memchr(saved->pins[i].name, '\0', SIM_HAL_NAME_CAPACITY) == NULL || saved->pins[i].value.type < SIM_HAL_BIT || saved->pins[i].value.type > SIM_HAL_FLOAT)) return SIM_HAL_ERR_ARGUMENT;
|
||||
for (uint32_t i = 0; i < SIM_HAL_FUNCTION_CAPACITY; ++i) if (saved->functions[i].used && (saved->functions[i].thread >= SIM_HAL_THREAD_CAPACITY || (saved->functions[i].component_index != UINT32_MAX && saved->functions[i].component_index >= SIM_HAL_COMPONENT_CAPACITY))) return SIM_HAL_ERR_ARGUMENT;
|
||||
for (uint32_t i = 0; i < sizeof(saved->links) / sizeof(saved->links[0]); ++i) if (saved->links[i].used && (saved->links[i].source >= SIM_HAL_PIN_CAPACITY || saved->links[i].target >= SIM_HAL_PIN_CAPACITY)) return SIM_HAL_ERR_ARGUMENT;
|
||||
memcpy(&state, saved, sizeof(state));
|
||||
return SIM_HAL_OK;
|
||||
}
|
||||
|
||||
static int valid_name(const char *name) {
|
||||
return name != NULL && name[0] != '\0' && strlen(name) < SIM_HAL_NAME_CAPACITY;
|
||||
}
|
||||
|
||||
@@ -85,6 +85,28 @@ static int code_is(double value, int integer_code) {
|
||||
return isfinite(value) && fabs(value - (double)integer_code) <= 1e-12;
|
||||
}
|
||||
|
||||
typedef struct { uint32_t magic, size; sim_interpreter interpreter; interpreter_meta meta; } interpreter_checkpoint;
|
||||
uint32_t sim_interpreter_checkpoint_size(void) { return sizeof(interpreter_checkpoint); }
|
||||
int32_t sim_interpreter_checkpoint_export(const sim_interpreter *interpreter, uint8_t *output, uint32_t capacity) {
|
||||
const interpreter_meta *meta = meta_for(interpreter, 0);
|
||||
if (meta == NULL || output == NULL || capacity != sizeof(interpreter_checkpoint)) return SIM_INTERP_ERR_ARGUMENT;
|
||||
interpreter_checkpoint *saved = (interpreter_checkpoint *)output;
|
||||
memset(saved, 0, sizeof(*saved));
|
||||
saved->magic = UINT32_C(0x49504331); saved->size = sizeof(*saved);
|
||||
saved->interpreter = *interpreter; saved->meta = *meta; saved->meta.owner = NULL;
|
||||
return SIM_INTERP_OK;
|
||||
}
|
||||
int32_t sim_interpreter_checkpoint_restore(sim_interpreter *interpreter, const uint8_t *input, uint32_t length) {
|
||||
if (interpreter == NULL || input == NULL || length != sizeof(interpreter_checkpoint)) return SIM_INTERP_ERR_ARGUMENT;
|
||||
const interpreter_checkpoint *saved = (const interpreter_checkpoint *)input;
|
||||
if (saved->magic != UINT32_C(0x49504331) || saved->size != sizeof(*saved) || saved->meta.owner != NULL || !finite_state(&saved->interpreter.state) || saved->interpreter.canonical_length >= SIM_INTERPRETER_CANONICAL_CAPACITY || saved->meta.coordinate_system < 0 || saved->meta.coordinate_system > 9) return SIM_INTERP_ERR_ARGUMENT;
|
||||
for (unsigned i = 0; i < NAMED_PARAMETER_CAPACITY; ++i) if (saved->meta.named[i].used && memchr(saved->meta.named[i].name, '\0', NAMED_PARAMETER_LENGTH) == NULL) return SIM_INTERP_ERR_ARGUMENT;
|
||||
interpreter_meta *meta = meta_for(interpreter, 1);
|
||||
if (meta == NULL) return SIM_INTERP_ERR_ARGUMENT;
|
||||
*interpreter = saved->interpreter; *meta = saved->meta; meta->owner = interpreter;
|
||||
return SIM_INTERP_OK;
|
||||
}
|
||||
|
||||
static int code_is_value(double value, double expected) {
|
||||
return isfinite(value) && fabs(value - expected) <= 1e-12;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ struct sim_linuxcnc_tp {
|
||||
double axis_max[9];
|
||||
double velocity_scale;
|
||||
double jog_scale;
|
||||
double scale_minimum;
|
||||
uint32_t tick;
|
||||
uint32_t last_active_id;
|
||||
int just_completed;
|
||||
@@ -31,6 +32,45 @@ struct sim_linuxcnc_tp {
|
||||
|
||||
static sim_linuxcnc_tp *active_planner;
|
||||
|
||||
/* Same-build checkpoint: TP/TC contain the integration and blend state which
|
||||
cannot be reconstructed by re-enqueuing the remaining geometric path.
|
||||
The queue pointer is removed on export and rebound on restore. All other
|
||||
members of these source structs are values, not process addresses. */
|
||||
typedef struct {
|
||||
uint32_t magic, planner_size, tc_size, capacity;
|
||||
sim_linuxcnc_tp planner;
|
||||
TC_STRUCT queue[TP_DEFAULT_QUEUE_SIZE];
|
||||
} tp_checkpoint;
|
||||
|
||||
uint32_t sim_linuxcnc_tp_checkpoint_size(void) { return (uint32_t)sizeof(tp_checkpoint); }
|
||||
|
||||
int32_t sim_linuxcnc_tp_checkpoint_export(const sim_linuxcnc_tp *planner, uint8_t *output, uint32_t capacity) {
|
||||
if (planner == NULL || !planner->initialized || output == NULL || capacity != sizeof(tp_checkpoint) || planner->tp.queue.size != TP_DEFAULT_QUEUE_SIZE) return -1228;
|
||||
tp_checkpoint *saved = (tp_checkpoint *)output;
|
||||
memset(saved, 0, sizeof(*saved));
|
||||
saved->magic = UINT32_C(0x54504331);
|
||||
saved->planner_size = sizeof(sim_linuxcnc_tp); saved->tc_size = sizeof(TC_STRUCT); saved->capacity = TP_DEFAULT_QUEUE_SIZE;
|
||||
saved->planner = *planner;
|
||||
saved->planner.tp.queue.queue = NULL;
|
||||
memcpy(saved->queue, planner->tp.queue.queue, sizeof(saved->queue));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int32_t sim_linuxcnc_tp_checkpoint_restore(sim_linuxcnc_tp *planner, const uint8_t *input, uint32_t length) {
|
||||
if (planner == NULL || !planner->initialized || input == NULL || length != sizeof(tp_checkpoint)) return -1228;
|
||||
const tp_checkpoint *saved = (const tp_checkpoint *)input;
|
||||
const TC_QUEUE_STRUCT *queue = &saved->planner.tp.queue;
|
||||
if (saved->magic != UINT32_C(0x54504331) || saved->planner_size != sizeof(sim_linuxcnc_tp) || saved->tc_size != sizeof(TC_STRUCT) || saved->capacity != TP_DEFAULT_QUEUE_SIZE || saved->planner.initialized != 1 || queue->queue != NULL || queue->size != TP_DEFAULT_QUEUE_SIZE || queue->start < 0 || queue->start >= queue->size || queue->end < 0 || queue->end >= queue->size || queue->_len < 0 || queue->_rlen < 0 || queue->_len > queue->size || queue->_rlen > queue->size - queue->_len) return -1228;
|
||||
if (!isfinite(saved->planner.tp.cycleTime) || saved->planner.tp.cycleTime != planner->tp.cycleTime || memcmp(&saved->planner.config, &planner->config, sizeof(planner->config)) != 0 || memcmp(saved->planner.axis_min, planner->axis_min, sizeof(planner->axis_min)) != 0 || memcmp(saved->planner.axis_max, planner->axis_max, sizeof(planner->axis_max)) != 0 || memcmp(saved->planner.axis_velocity, planner->axis_velocity, sizeof(planner->axis_velocity)) != 0 || memcmp(saved->planner.axis_acceleration, planner->axis_acceleration, sizeof(planner->axis_acceleration)) != 0) return -1229;
|
||||
TC_STRUCT *storage = planner->tp.queue.queue;
|
||||
*planner = saved->planner;
|
||||
planner->tp.queue.queue = storage;
|
||||
memcpy(storage, saved->queue, sizeof(saved->queue));
|
||||
active_planner = planner;
|
||||
tpMotData(&planner->status, &planner->config);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void noop_dio(int index, char value) { (void)index; (void)value; }
|
||||
static void noop_aio(int index, double value) { (void)index; (void)value; }
|
||||
static void noop_rotary_unlock(int index, int value) { (void)index; (void)value; }
|
||||
@@ -60,7 +100,7 @@ static void xyzac_line_limits(const sim_linuxcnc_tp *planner, const EmcPose *sta
|
||||
const double distance = has_linear ? sqrt(linear_length) : has_angular ? sqrt(angular_length) : 0.0;
|
||||
if (!(distance > 0.0)) return;
|
||||
if (max_velocity_time > 0.0) *velocity = fmin(*velocity, distance / max_velocity_time);
|
||||
if (max_acceleration_time > 0.0) *acceleration = fmax(*acceleration, distance / max_acceleration_time);
|
||||
if (max_acceleration_time > 0.0) *acceleration = fmin(*acceleration, distance / max_acceleration_time);
|
||||
}
|
||||
|
||||
static void xyzac_circle_limits(const sim_linuxcnc_tp *planner, const EmcPose *start, const EmcPose *end, const PmCartesian *center, const PmCartesian *normal, int turn, double *velocity, double *initial_velocity, double *acceleration) {
|
||||
@@ -95,7 +135,7 @@ static void xyzac_circle_limits(const sim_linuxcnc_tp *planner, const EmcPose *s
|
||||
const double max_acceleration = total_xyz_length / fmax(time_motion_acceleration, time_spiral_acceleration);
|
||||
*velocity = fmin(*velocity, max_velocity);
|
||||
*initial_velocity = max_velocity;
|
||||
*acceleration = fmax(*acceleration, max_acceleration);
|
||||
*acceleration = fmin(*acceleration, max_acceleration);
|
||||
}
|
||||
|
||||
static EmcPose to_emc_pose(const sim_posemath_pose *value) {
|
||||
@@ -143,18 +183,17 @@ int32_t sim_linuxcnc_tp_init(sim_linuxcnc_tp *planner, double cycle_time, double
|
||||
planner->config.arcBlendEnable = 1; planner->config.arcBlendFallbackEnable = 0;
|
||||
planner->config.arcBlendOptDepth = 50; planner->config.arcBlendGapCycles = 4;
|
||||
planner->config.arcBlendRampFreq = 100.0; planner->config.arcBlendTangentKinkRatio = 0.1;
|
||||
// Match xyzac-trt.ini joint limits: the trajectory cap is 35 mm/s, but
|
||||
// XYZ joints are limited to 20 and rotary A/C joints to 30 deg/s. The
|
||||
// upstream TP uses these per-axis callbacks when calculating tangent
|
||||
// velocity and acceleration at a corner.
|
||||
// The caller configures each axis from INI before motion is queued.
|
||||
// Standalone planners use their supplied trajectory limits.
|
||||
for (int axis = 0; axis < 9; ++axis) {
|
||||
planner->axis_velocity[axis] = axis < 3 ? fmin(20.0, velocity_limit) : (axis == 3 || axis == 5) ? fmin(30.0, velocity_limit) : velocity_limit;
|
||||
planner->axis_velocity[axis] = velocity_limit;
|
||||
planner->axis_acceleration[axis] = acceleration_limit;
|
||||
planner->axis_min[axis] = -INFINITY;
|
||||
planner->axis_max[axis] = INFINITY;
|
||||
}
|
||||
planner->velocity_scale = 1.0;
|
||||
planner->jog_scale = 1.0;
|
||||
planner->scale_minimum = 0.0;
|
||||
planner->status.feed_scale = 1.0; planner->status.rapid_scale = 1.0; planner->status.net_feed_scale = 1.0;
|
||||
planner->status.enables_new = FS_ENABLED | FH_ENABLED; planner->status.enables_queued = planner->status.enables_new;
|
||||
planner->status.motionFlag = EMCMOT_MOTION_ENABLE_BIT | EMCMOT_MOTION_COORD_BIT | EMCMOT_MOTION_INPOS_BIT;
|
||||
@@ -182,12 +221,12 @@ int32_t sim_linuxcnc_tp_set_profile(sim_linuxcnc_tp *planner, int profile, doubl
|
||||
}
|
||||
|
||||
int32_t sim_linuxcnc_tp_set_feed_override(sim_linuxcnc_tp *planner, double scale) {
|
||||
if (planner == NULL || !planner->initialized || !isfinite(scale) || scale < 0.0 || scale > 2.0) return -1207;
|
||||
if (planner == NULL || !planner->initialized || !isfinite(scale) || scale < 0.0 || scale > planner->config.maxFeedScale) return -1207;
|
||||
planner->status.net_feed_scale = scale; planner->status.feed_scale = scale; return 0;
|
||||
}
|
||||
|
||||
int32_t sim_linuxcnc_tp_set_velocity_scale(sim_linuxcnc_tp *planner, double scale) {
|
||||
if (planner == NULL || !planner->initialized || !isfinite(scale) || scale < 0.06 || scale > 1.0) return -1211;
|
||||
if (planner == NULL || !planner->initialized || !isfinite(scale) || scale <= 0.0 || scale < planner->scale_minimum || scale > 1.0) return -1211;
|
||||
active_planner = planner;
|
||||
if (tpSetVlimit(&planner->tp, planner->config.limitVel * scale) != TP_ERR_OK) return -1212;
|
||||
planner->velocity_scale = scale;
|
||||
@@ -195,7 +234,7 @@ int32_t sim_linuxcnc_tp_set_velocity_scale(sim_linuxcnc_tp *planner, double scal
|
||||
}
|
||||
|
||||
int32_t sim_linuxcnc_tp_set_jog_scale(sim_linuxcnc_tp *planner, double scale) {
|
||||
if (planner == NULL || !planner->initialized || !isfinite(scale) || scale < 0.06 || scale > 1.0) return -1213;
|
||||
if (planner == NULL || !planner->initialized || !isfinite(scale) || scale <= 0.0 || scale < planner->scale_minimum || scale > 1.0) return -1213;
|
||||
planner->jog_scale = scale;
|
||||
return 0;
|
||||
}
|
||||
@@ -207,6 +246,36 @@ int32_t sim_linuxcnc_tp_set_axis_limit(sim_linuxcnc_tp *planner, int axis, doubl
|
||||
return 0;
|
||||
}
|
||||
|
||||
int32_t sim_linuxcnc_tp_set_axis_dynamics(sim_linuxcnc_tp *planner, int axis, double velocity, double acceleration) {
|
||||
if (planner == NULL || !planner->initialized || axis < 0 || axis >= 9 || !isfinite(velocity) || velocity <= 0.0 || !isfinite(acceleration) || acceleration <= 0.0) return -1214;
|
||||
planner->axis_velocity[axis] = velocity;
|
||||
planner->axis_acceleration[axis] = acceleration;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int32_t sim_linuxcnc_tp_set_feed_limit(sim_linuxcnc_tp *planner, double maximum) {
|
||||
if (planner == NULL || !planner->initialized || !isfinite(maximum) || maximum < 1.0) return -1207;
|
||||
planner->config.maxFeedScale = maximum;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int32_t sim_linuxcnc_tp_set_scale_minimum(sim_linuxcnc_tp *planner, double minimum) {
|
||||
if (planner == NULL || !planner->initialized || !isfinite(minimum) || minimum <= 0.0 || minimum > 1.0) return -1211;
|
||||
planner->scale_minimum = minimum;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int32_t sim_linuxcnc_tp_set_blend(sim_linuxcnc_tp *planner, int enable, int fallback, int depth, int gap, double ramp, double kink) {
|
||||
if (planner == NULL || !planner->initialized || (enable != 0 && enable != 1) || (fallback != 0 && fallback != 1) || depth < 1 || gap < 0 || !isfinite(ramp) || ramp <= 0.0 || !isfinite(kink) || kink <= 0.0) return -1205;
|
||||
planner->config.arcBlendEnable = enable;
|
||||
planner->config.arcBlendFallbackEnable = fallback;
|
||||
planner->config.arcBlendOptDepth = depth;
|
||||
planner->config.arcBlendGapCycles = gap;
|
||||
planner->config.arcBlendRampFreq = ramp;
|
||||
planner->config.arcBlendTangentKinkRatio = kink;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int32_t sim_linuxcnc_tp_jog(sim_linuxcnc_tp *planner, int axis, int direction, double increment, uint32_t tag) {
|
||||
EmcPose end;
|
||||
double *value;
|
||||
|
||||
@@ -318,6 +318,30 @@ static void reset_runtime(sim_program *program) {
|
||||
|
||||
uint32_t sim_program_size(void) { return (uint32_t)sizeof(sim_program); }
|
||||
|
||||
typedef struct { uint32_t magic, size; sim_program program; } program_checkpoint;
|
||||
uint32_t sim_program_checkpoint_size(void) { return sizeof(program_checkpoint) + sim_interpreter_checkpoint_size(); }
|
||||
int32_t sim_program_checkpoint_export(const sim_program *program, uint8_t *output, uint32_t capacity) {
|
||||
if (program == NULL || output == NULL || capacity != sim_program_checkpoint_size()) return SIM_PROGRAM_ERR_ARGUMENT;
|
||||
program_checkpoint *saved = (program_checkpoint *)output;
|
||||
memset(saved, 0, sizeof(*saved)); saved->magic = UINT32_C(0x50524331); saved->size = sizeof(*saved);
|
||||
saved->program = *program;
|
||||
saved->program.source = NULL; saved->program.line_offsets = NULL;
|
||||
saved->program.source_capacity = 0; saved->program.line_capacity = 0;
|
||||
return sim_interpreter_checkpoint_export(&program->interpreter, output + sizeof(*saved), sim_interpreter_checkpoint_size()) == SIM_INTERP_OK ? SIM_PROGRAM_OK : SIM_PROGRAM_ERR_INTERPRETER;
|
||||
}
|
||||
int32_t sim_program_checkpoint_restore(sim_program *program, const uint8_t *input, uint32_t length) {
|
||||
if (program == NULL || input == NULL || length != sim_program_checkpoint_size()) return SIM_PROGRAM_ERR_ARGUMENT;
|
||||
const program_checkpoint *saved = (const program_checkpoint *)input;
|
||||
if (saved->magic != UINT32_C(0x50524331) || saved->size != sizeof(*saved) || saved->program.source != NULL || saved->program.line_offsets != NULL || saved->program.source_length != program->source_length || saved->program.line_count != program->line_count || saved->program.current_line > program->line_count || saved->program.call_depth > SIM_PROGRAM_CALL_DEPTH || saved->program.subroutine_count > SIM_PROGRAM_SUBROUTINE_CAPACITY || saved->program.status > SIM_PROGRAM_ERROR) return SIM_PROGRAM_ERR_ARGUMENT;
|
||||
if (sim_interpreter_checkpoint_restore(&program->interpreter, input + sizeof(*saved), sim_interpreter_checkpoint_size()) != SIM_INTERP_OK) return SIM_PROGRAM_ERR_INTERPRETER;
|
||||
char *source = program->source; uint32_t *lines = program->line_offsets;
|
||||
uint32_t source_capacity = program->source_capacity, line_capacity = program->line_capacity;
|
||||
*program = saved->program;
|
||||
program->source = source; program->line_offsets = lines;
|
||||
program->source_capacity = source_capacity; program->line_capacity = line_capacity;
|
||||
return SIM_PROGRAM_OK;
|
||||
}
|
||||
|
||||
int32_t sim_program_init(sim_program *program) {
|
||||
if (program == NULL) return SIM_PROGRAM_ERR_ARGUMENT;
|
||||
memset(program, 0, sizeof(*program));
|
||||
|
||||
@@ -12,6 +12,18 @@ typedef struct {
|
||||
|
||||
static protocol_state state;
|
||||
|
||||
uint32_t sim_protocol_checkpoint_size(void) { return sizeof(state); }
|
||||
int32_t sim_protocol_checkpoint_export(uint8_t *output, uint32_t capacity) {
|
||||
if (output == NULL || capacity != sizeof(state)) return SIM_PROTOCOL_ERR_ARGUMENT;
|
||||
memcpy(output, &state, sizeof(state)); return SIM_PROTOCOL_OK;
|
||||
}
|
||||
int32_t sim_protocol_checkpoint_restore(const uint8_t *input, uint32_t length) {
|
||||
if (input == NULL || length != sizeof(state)) return SIM_PROTOCOL_ERR_ARGUMENT;
|
||||
const protocol_state *saved = (const protocol_state *)input;
|
||||
if (saved->state > SIM_PROTOCOL_DESTROYED) return SIM_PROTOCOL_ERR_ARGUMENT;
|
||||
memcpy(&state, saved, sizeof(state)); return SIM_PROTOCOL_OK;
|
||||
}
|
||||
|
||||
static int has_text(const char *value) {
|
||||
return value != NULL && value[0] != '\0' && strlen(value) <= 128;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static float get_float(const char *name) {
|
||||
sim_hal_value value = {0};
|
||||
@@ -32,6 +34,19 @@ int sim_hal_test_run(void) {
|
||||
assert(sim_hal_register_function("missing", "bad") == SIM_HAL_ERR_NOT_FOUND);
|
||||
assert(sim_hal_step(5) == SIM_HAL_OK);
|
||||
assert(sim_hal_tick() == 5 && sim_hal_function_calls("update") == 2);
|
||||
const uint32_t saved_size = sim_hal_checkpoint_size();
|
||||
uint8_t *saved = malloc(saved_size), *restored = malloc(saved_size);
|
||||
assert(saved && restored);
|
||||
assert(sim_hal_checkpoint_export(saved, saved_size) == SIM_HAL_OK);
|
||||
assert(sim_hal_step(3) == SIM_HAL_OK);
|
||||
assert(sim_hal_tick() == 8 && sim_hal_function_calls("update") == 4);
|
||||
assert(sim_hal_checkpoint_restore(saved, saved_size - 1) == SIM_HAL_ERR_ARGUMENT);
|
||||
assert(sim_hal_tick() == 8);
|
||||
assert(sim_hal_checkpoint_restore(saved, saved_size) == SIM_HAL_OK);
|
||||
assert(sim_hal_tick() == 5 && sim_hal_function_calls("update") == 2);
|
||||
assert(sim_hal_checkpoint_export(restored, saved_size) == SIM_HAL_OK);
|
||||
assert(memcmp(saved, restored, saved_size) == 0);
|
||||
free(saved); free(restored);
|
||||
assert(sim_hal_set_function_failure("update", 1) == SIM_HAL_OK);
|
||||
assert(sim_hal_step(1) == SIM_HAL_ERR_FUNCTION);
|
||||
assert(sim_hal_tick() == 5);
|
||||
|
||||
@@ -18,6 +18,26 @@ int main(void) {
|
||||
sim_posemath_pose first = pose(0, 0, 10);
|
||||
sim_posemath_pose second = pose(20, 20, 10);
|
||||
assert(planner != NULL);
|
||||
assert(sim_linuxcnc_tp_init(planner, .001, 35, 300) == 0);
|
||||
assert(sim_linuxcnc_tp_set_axis_dynamics(planner, 0, 7, 40) == 0);
|
||||
assert(fabs(sim_linuxcnc_tp_jog_velocity(planner, 0) - 7) < 1e-9);
|
||||
assert(sim_linuxcnc_tp_set_axis_dynamics(planner, 4, 11, 20) == 0);
|
||||
assert(fabs(sim_linuxcnc_tp_jog_velocity(planner, 4) - 11) < 1e-9);
|
||||
assert(sim_linuxcnc_tp_set_axis_dynamics(planner, 9, 7, 40) != 0);
|
||||
assert(sim_linuxcnc_tp_set_axis_dynamics(planner, 0, NAN, 40) != 0);
|
||||
assert(sim_linuxcnc_tp_set_axis_dynamics(planner, 0, 7, 0) != 0);
|
||||
assert(sim_linuxcnc_tp_set_feed_limit(planner, 1.3) == 0);
|
||||
assert(sim_linuxcnc_tp_set_feed_override(planner, 1.3) == 0);
|
||||
assert(sim_linuxcnc_tp_set_feed_override(planner, 1.31) != 0);
|
||||
assert(sim_linuxcnc_tp_set_scale_minimum(planner, .02) == 0);
|
||||
assert(sim_linuxcnc_tp_set_jog_scale(planner, .02) == 0);
|
||||
assert(sim_linuxcnc_tp_set_velocity_scale(planner, .02) == 0);
|
||||
assert(sim_linuxcnc_tp_set_jog_scale(planner, .01) != 0);
|
||||
assert(sim_linuxcnc_tp_set_scale_minimum(planner, NAN) != 0);
|
||||
assert(sim_linuxcnc_tp_set_blend(planner, 1, 0, 25, 3, 50, .2) == 0);
|
||||
assert(sim_linuxcnc_tp_set_blend(planner, 2, 0, 25, 3, 50, .2) != 0);
|
||||
assert(sim_linuxcnc_tp_set_blend(planner, 1, 0, 0, 3, 50, .2) != 0);
|
||||
assert(sim_linuxcnc_tp_set_blend(planner, 1, 0, 25, 3, NAN, .2) != 0);
|
||||
assert(sim_linuxcnc_tp_init(planner, 0.001, 35.0, 300.0) == 0);
|
||||
assert(sim_linuxcnc_tp_queue_capacity(planner) > 0);
|
||||
assert(sim_linuxcnc_tp_velocity_mm_min(planner) == 0.0);
|
||||
@@ -25,6 +45,28 @@ int main(void) {
|
||||
assert(sim_linuxcnc_tp_set_profile(planner, 0, 0.0) == 0);
|
||||
assert(sim_linuxcnc_tp_add_line(planner, &first, 20.0, 300.0, 16, 1) == 0);
|
||||
assert(sim_linuxcnc_tp_add_line(planner, &second, 35.0, 300.0, 18, 1) == 0);
|
||||
for (int index = 0; index < 20; ++index) assert(sim_linuxcnc_tp_step(planner) == 0);
|
||||
uint32_t checkpoint_size = sim_linuxcnc_tp_checkpoint_size();
|
||||
uint8_t *checkpoint = malloc(checkpoint_size);
|
||||
uint64_t continuation[100];
|
||||
assert(checkpoint != NULL);
|
||||
assert(sim_linuxcnc_tp_checkpoint_export(planner, checkpoint, checkpoint_size) == 0);
|
||||
for (int index = 0; index < 100; ++index) {
|
||||
assert(sim_linuxcnc_tp_step(planner) == 0);
|
||||
continuation[index] = sim_linuxcnc_tp_state_hash(planner);
|
||||
}
|
||||
const uint64_t before_invalid = sim_linuxcnc_tp_state_hash(planner);
|
||||
assert(sim_linuxcnc_tp_checkpoint_restore(planner, checkpoint, checkpoint_size - 1) != 0);
|
||||
checkpoint[0] ^= 1;
|
||||
assert(sim_linuxcnc_tp_checkpoint_restore(planner, checkpoint, checkpoint_size) != 0);
|
||||
assert(sim_linuxcnc_tp_state_hash(planner) == before_invalid);
|
||||
checkpoint[0] ^= 1;
|
||||
assert(sim_linuxcnc_tp_checkpoint_restore(planner, checkpoint, checkpoint_size) == 0);
|
||||
for (int index = 0; index < 100; ++index) {
|
||||
assert(sim_linuxcnc_tp_step(planner) == 0);
|
||||
assert(sim_linuxcnc_tp_state_hash(planner) == continuation[index]);
|
||||
}
|
||||
free(checkpoint);
|
||||
for (int index = 0; index < 5000; ++index) {
|
||||
int result = sim_linuxcnc_tp_step(planner);
|
||||
if (result != 0) fprintf(stderr, "tp_step[%d]=%d\\n", index, result);
|
||||
|
||||
@@ -61,7 +61,7 @@ export function normalizeAxisState(input = {}) {
|
||||
const hasEditor = input.has_editor !== false;
|
||||
return Object.freeze({
|
||||
taskState, interpState, mode, motionMode, programStatus, queue, queueLimit,
|
||||
taskFile, hasEditor, onAnyLimit: Boolean(limits), toolLoaded: toolNumber > 0,
|
||||
taskFile, hasEditor, onAnyLimit: Boolean(limits), toolLoaded: toolNumber > 0, motionQueueDepth: number(motion.queue_depth),
|
||||
allHomed: homed.length > 0 && homed.every(Boolean), homed, fault,
|
||||
});
|
||||
}
|
||||
@@ -101,6 +101,13 @@ export function axisGate(action, input = {}) {
|
||||
case "restart": enabled = machineOn && idle && state.taskFile; reason = enabled ? "axis_restart_available" : !machineOn ? "axis_machine_off" : !state.taskFile ? "axis_taskfile_missing" : "axis_restart_state_gate"; break;
|
||||
case "mdi": enabled = machineOn && state.queue < state.queueLimit && (idle || state.mode === "MDI"); reason = enabled ? "axis_mdi_available" : estop ? "axis_estop_active" : !machineOn ? "axis_machine_off" : state.queue >= state.queueLimit ? "axis_mdi_queue_full" : "axis_interp_running"; break;
|
||||
case "jogStart": case "jogStop": case "home": case "spindle": case "coolant": enabled = manualOk && state.mode === "MANUAL"; reason = enabled ? "axis_manual_available" : estop ? "axis_estop_active" : !machineOn ? "axis_machine_off" : state.mode !== "MANUAL" ? "axis_manual_mode_required" : "axis_manual_state_gate"; break;
|
||||
// Direct operator tool commands must wait for the interpreter and motion.
|
||||
// emcTaskCheckPreconditions waits for IO on PREPARE and motion+IO on LOAD;
|
||||
// the IO handler below this gate owns prepared/changed acknowledgements.
|
||||
case "tool": case "toolPrepare": case "toolChange":
|
||||
enabled = machineOn && idle && state.motionQueueDepth === 0 && ["MANUAL", "MDI"].includes(state.mode);
|
||||
reason = estop ? "axis_estop_active" : !machineOn ? "axis_machine_off" : !idle ? "axis_interp_running" : state.motionQueueDepth > 0 ? "axis_motion_active" : !["MANUAL", "MDI"].includes(state.mode) ? "axis_tool_operator_mode_required" : "axis_tool_available";
|
||||
break;
|
||||
case "touchOff": enabled = manualOk && state.mode === "MANUAL" && trajectoryTouchOffAllowed; reason = !manualOk ? "axis_manual_state_gate" : state.mode !== "MANUAL" ? "axis_manual_mode_required" : !trajectoryTouchOffAllowed ? "axis_joint_mode_required" : "axis_touch_off_available"; break;
|
||||
case "toolTouchOff": enabled = manualOk && state.mode === "MANUAL" && trajectoryTouchOffAllowed && state.toolLoaded; reason = !state.toolLoaded ? "axis_tool_missing" : state.mode !== "MANUAL" ? "axis_manual_mode_required" : !trajectoryTouchOffAllowed ? "axis_joint_mode_required" : !manualOk ? "axis_manual_state_gate" : "axis_tool_touch_off_available"; break;
|
||||
case "overrideLimits": enabled = state.onAnyLimit; reason = enabled ? "axis_limit_override_available" : "axis_no_active_limit"; break;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
export const AXIS_ASSET_ROOT = "/linuxcnc/share/axis/images";
|
||||
import { WEB_SETTINGS, axisImagePath } from "../workspace/web-settings.mjs";
|
||||
export const AXIS_ASSET_ROOT = `./${WEB_SETTINGS.assets.axis_images}`;
|
||||
|
||||
export function axisAsset(name) {
|
||||
return `${AXIS_ASSET_ROOT}/${name}`;
|
||||
return axisImagePath(name);
|
||||
}
|
||||
|
||||
export function appendAxisIcon(document, button, asset, label) {
|
||||
|
||||
@@ -31,4 +31,14 @@ assert.equal(axisGate("overrideLimits", limit).enabled, true);
|
||||
assert.equal(axisGateMatrix(ready).run.reason, "axis_run_available");
|
||||
assert.equal(AXIS_COMMANDS.run, "PROGRAM_RUN");
|
||||
assert.equal(AXIS_BOUNDARIES.physicalHardware, "REAL-EXCLUDED");
|
||||
for (const action of ["tool", "toolPrepare", "toolChange"]) {
|
||||
assert.equal(axisGate(action, ready).enabled, true);
|
||||
assert.equal(axisGate(action, running).enabled, false);
|
||||
assert.equal(axisGate(action, paused).enabled, false);
|
||||
assert.equal(axisGate(action, estop).enabled, false);
|
||||
assert.equal(axisGate(action, { ...ready, task: { ...ready.task, state: "OFF" } }).enabled, false);
|
||||
assert.equal(axisGate(action, { ...ready, motion: { ...ready.motion, queue_depth: 1 } }).reason, "axis_motion_active");
|
||||
assert.equal(axisGate(action, { ...ready, devices: { fault: true } }).reason, "axis_fault_active");
|
||||
assert.equal(axisGate(action, { ...ready, task: { ...ready.task, mode: "AUTO" } }).enabled, false);
|
||||
}
|
||||
console.log(JSON.stringify({ schema: "axis-contract-sample.v1", status: "verified", commands: Object.keys(AXIS_COMMANDS).length, matrix: Object.keys(axisGateMatrix(ready)).length, blocked_reasons: [axisGate("mdi", queueFull).reason, axisGate("jogStart", estop).reason] }));
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { WEB_SETTINGS } from "../../workspace/web-settings.mjs";
|
||||
const VIEWER = WEB_SETTINGS.viewer;
|
||||
const CAMERA = VIEWER.camera;
|
||||
import * as THREE from "three";
|
||||
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
||||
import { RoomEnvironment } from "three/examples/jsm/environments/RoomEnvironment.js";
|
||||
@@ -11,7 +14,7 @@ type Segment = { id?: number; active?: boolean; motion_type?: number; samples?:
|
||||
type Props = { visual: XyzacVisualState; toolpath?: Segment[]; view?: "z" | "z2" | "x" | "y" | "y2" | "p"; zoom?: number; rotateMode?: boolean; showPath?: boolean; showLive?: boolean; showLimits?: boolean; showVelocity?: boolean; showOffsets?: boolean; showExtents?: boolean };
|
||||
type Controller = Record<string, () => void>;
|
||||
type Host = HTMLDivElement & { xyzacMachineController?: Controller };
|
||||
const toView = (point: Point) => new THREE.Vector3(Number(point.x ?? 0) * .01, Number(point.z ?? 0) * .01, Number(point.y ?? 0) * .01);
|
||||
const toView = (point: Point) => new THREE.Vector3(Number(point.x ?? 0) * VIEWER.mm_scale, Number(point.z ?? 0) * VIEWER.mm_scale, Number(point.y ?? 0) * VIEWER.mm_scale);
|
||||
|
||||
export function XyzacMachineView({ visual, toolpath, view = "p", zoom = 1, rotateMode = false, showPath = false, showLive = false, showLimits = false, showVelocity = false, showOffsets = false, showExtents = false }: Props) {
|
||||
const host = useRef<Host>(null);
|
||||
@@ -23,7 +26,7 @@ export function XyzacMachineView({ visual, toolpath, view = "p", zoom = 1, rotat
|
||||
const element = host.current;
|
||||
if (!element) return;
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false, preserveDrawingBuffer: true });
|
||||
renderer.setPixelRatio(Math.min(globalThis.devicePixelRatio || 1, 2));
|
||||
renderer.setPixelRatio(Math.min(globalThis.devicePixelRatio || 1, VIEWER.pixel_ratio_limit));
|
||||
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||
renderer.toneMappingExposure = 1;
|
||||
renderer.shadowMap.enabled = true;
|
||||
@@ -31,7 +34,7 @@ export function XyzacMachineView({ visual, toolpath, view = "p", zoom = 1, rotat
|
||||
renderer.domElement.dataset.engine = "three.js r185 / xyzac-trt-vismach";
|
||||
element.appendChild(renderer.domElement);
|
||||
|
||||
const scene = new THREE.Scene(); scene.background = new THREE.Color(0x252b2c);
|
||||
const scene = new THREE.Scene(); scene.background = new THREE.Color(VIEWER.colors.background);
|
||||
const studio = new RoomEnvironment();
|
||||
const pmrem = new THREE.PMREMGenerator(renderer);
|
||||
const environment = pmrem.fromScene(studio, .04);
|
||||
@@ -60,11 +63,11 @@ export function XyzacMachineView({ visual, toolpath, view = "p", zoom = 1, rotat
|
||||
const nameplate = new THREE.Mesh(new THREE.PlaneGeometry(.84, .21), new THREE.MeshBasicMaterial({ map: nameplateTexture }));
|
||||
nameplate.position.set(-.57, -1.48, -1.046); nameplate.rotation.y = Math.PI; assembly.machine.add(nameplate);
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(36, 1, .02, 150);
|
||||
const camera = new THREE.PerspectiveCamera(CAMERA.fov, 1, CAMERA.near, CAMERA.far);
|
||||
const controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.enableDamping = true; controls.dampingFactor = .12;
|
||||
controls.enableDamping = true; controls.dampingFactor = CAMERA.damping;
|
||||
controls.enableRotate = true;
|
||||
controls.screenSpacePanning = true; controls.minDistance = .25; controls.maxDistance = 65;
|
||||
controls.screenSpacePanning = true; controls.minDistance = CAMERA.min_distance; controls.maxDistance = CAMERA.max_distance;
|
||||
// Keep the standard Three.js mouse contract: left-drag orbits, middle
|
||||
// drag zooms, right-drag pans, and the wheel zooms. The machine canvas
|
||||
// must remain directly navigable without first toggling a mode button.
|
||||
@@ -96,7 +99,7 @@ export function XyzacMachineView({ visual, toolpath, view = "p", zoom = 1, rotat
|
||||
const point = new THREE.Vector3(x, y, z).sub(modelCenter);
|
||||
distance = Math.max(distance, point.dot(direction) + Math.max(Math.abs(point.dot(right)) / (tanV * camera.aspect), Math.abs(point.dot(up)) / tanV));
|
||||
}
|
||||
controls.target.copy(modelCenter); camera.position.copy(modelCenter).addScaledVector(direction, distance * 1.12 / Math.max(.5, scale));
|
||||
controls.target.copy(modelCenter); camera.position.copy(modelCenter).addScaledVector(direction, distance * CAMERA.fit_margin / Math.max(.5, scale));
|
||||
camera.lookAt(controls.target); controls.update();
|
||||
};
|
||||
const resize = () => {
|
||||
@@ -106,7 +109,7 @@ export function XyzacMachineView({ visual, toolpath, view = "p", zoom = 1, rotat
|
||||
};
|
||||
const observer = new ResizeObserver(resize); observer.observe(element); resize();
|
||||
const pan = (x: number, y: number) => {
|
||||
const amount = camera.position.distanceTo(controls.target) * .035;
|
||||
const amount = camera.position.distanceTo(controls.target) * CAMERA.pan_fraction;
|
||||
camera.updateMatrix();
|
||||
const offset = new THREE.Vector3().setFromMatrixColumn(camera.matrix, 0).multiplyScalar(x * amount)
|
||||
.addScaledVector(new THREE.Vector3().setFromMatrixColumn(camera.matrix, 1), y * amount);
|
||||
@@ -118,12 +121,12 @@ export function XyzacMachineView({ visual, toolpath, view = "p", zoom = 1, rotat
|
||||
camera.position.copy(controls.target).add(offset); controls.update();
|
||||
};
|
||||
element.xyzacMachineController = {
|
||||
zoomIn: () => zoomCamera(.8), zoomOut: () => zoomCamera(1.25),
|
||||
zoomIn: () => zoomCamera(CAMERA.zoom_in), zoomOut: () => zoomCamera(CAMERA.zoom_out),
|
||||
panLeft: () => pan(-1, 0), panRight: () => pan(1, 0), panUp: () => pan(0, 1), panDown: () => pan(0, -1),
|
||||
rotateLeft: () => { controls.rotateLeft(Math.PI / 12); controls.update(); },
|
||||
rotateRight: () => { controls.rotateLeft(-Math.PI / 12); controls.update(); },
|
||||
rotateUp: () => { controls.rotateUp(Math.PI / 12); controls.update(); },
|
||||
rotateDown: () => { controls.rotateUp(-Math.PI / 12); controls.update(); },
|
||||
rotateLeft: () => { controls.rotateLeft(THREE.MathUtils.degToRad(CAMERA.rotate_degrees)); controls.update(); },
|
||||
rotateRight: () => { controls.rotateLeft(-THREE.MathUtils.degToRad(CAMERA.rotate_degrees)); controls.update(); },
|
||||
rotateUp: () => { controls.rotateUp(THREE.MathUtils.degToRad(CAMERA.rotate_degrees)); controls.update(); },
|
||||
rotateDown: () => { controls.rotateUp(-THREE.MathUtils.degToRad(CAMERA.rotate_degrees)); controls.update(); },
|
||||
reset: () => { lastPreset = ""; },
|
||||
tool: () => {
|
||||
const target = assembly.toolTip.getWorldPosition(new THREE.Vector3()).add(new THREE.Vector3(0, .2, 0));
|
||||
@@ -136,11 +139,11 @@ export function XyzacMachineView({ visual, toolpath, view = "p", zoom = 1, rotat
|
||||
// workpiece path together with the physical table.
|
||||
const pathGroup = new THREE.Group(); pathGroup.name = "xyzac-toolpath"; assembly.joints.rotate.add(pathGroup);
|
||||
const liveGroup = new THREE.Group(); liveGroup.name = "xyzac-live-toolpath"; assembly.joints.rotate.add(liveGroup);
|
||||
const pathMaterials = [0xffff00, 0xff9a55, 0x5c9d9a].map((color) => new THREE.LineBasicMaterial({ color }));
|
||||
const pathMaterials = [VIEWER.colors.path, VIEWER.colors.live, VIEWER.colors.rapid].map((color) => new THREE.LineBasicMaterial({ color }));
|
||||
const clearLines = (group: THREE.Group) => { group.children.forEach((child) => (child as THREE.Line).geometry.dispose()); group.clear(); };
|
||||
const limits = new THREE.Box3Helper(new THREE.Box3(), 0xdd7566); scene.add(limits);
|
||||
const limits = new THREE.Box3Helper(new THREE.Box3(), VIEWER.colors.limits); scene.add(limits);
|
||||
const offsets = new THREE.AxesHelper(.8); scene.add(offsets);
|
||||
const extents = new THREE.Box3Helper(initialBounds, 0xe6c56b); scene.add(extents);
|
||||
const extents = new THREE.Box3Helper(initialBounds, VIEWER.colors.extents); scene.add(extents);
|
||||
const render = () => {
|
||||
const state = visualRef.current, config = settings.current;
|
||||
assembly.update(state);
|
||||
@@ -165,7 +168,7 @@ export function XyzacMachineView({ visual, toolpath, view = "p", zoom = 1, rotat
|
||||
}
|
||||
offsets.visible = config.showOffsets;
|
||||
const coordinates = state.machineModel?.coordinate_system ?? "XYZAC";
|
||||
offsets.position.set(coordinates === "XYZBC" ? Number(state.offsets.x_offset ?? 0) * .01 : 0, state.offsets.z_offset * .01, state.offsets.y_offset * .01);
|
||||
offsets.position.set(coordinates === "XYZBC" ? Number(state.offsets.x_offset ?? 0) * VIEWER.mm_scale : 0, state.offsets.z_offset * VIEWER.mm_scale, state.offsets.y_offset * VIEWER.mm_scale);
|
||||
extents.visible = config.showExtents;
|
||||
const preset = `${config.view}:${config.zoom}`;
|
||||
if (preset !== lastPreset) { presetCamera(); lastPreset = preset; }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { WEB_SETTINGS } from "../../workspace/web-settings.mjs";
|
||||
import * as THREE from "three";
|
||||
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
||||
import { projectXyzacVisual, type XyzacVisualSource, type XyzacVisualState } from "./xyzac-visual-adapter";
|
||||
@@ -8,7 +9,7 @@ type Segment = { id?: number; source_line?: number | null; active?: boolean; kin
|
||||
type Props = { visual: XyzacVisualState; view?: "z" | "z2" | "x" | "y" | "y2" | "p"; zoom?: number; rotateMode?: boolean; showPath?: boolean; showLive?: boolean; showLimits?: boolean; showVelocity?: boolean; showOffsets?: boolean; showExtents?: boolean };
|
||||
|
||||
const DEG = Math.PI / 180;
|
||||
const MM = 0.01;
|
||||
const MM = WEB_SETTINGS.viewer.mm_scale;
|
||||
const sourcePoint = (point: Point) => new THREE.Vector3(Number(point.x ?? 0) * MM, Number(point.z ?? 0) * MM, Number(point.y ?? 0) * MM);
|
||||
|
||||
export function XyzacToolPreview({ visual, view = "p", zoom = 1, rotateMode = false, showPath = true, showLive = true, showLimits = false, showVelocity = false, showOffsets = false, showExtents = false }: Props) {
|
||||
@@ -24,11 +25,11 @@ export function XyzacToolPreview({ visual, view = "p", zoom = 1, rotateMode = fa
|
||||
const element = host.current;
|
||||
if (!element) return;
|
||||
const scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color(0x050a0e);
|
||||
scene.background = new THREE.Color(WEB_SETTINGS.viewer.toolpath_colors.background);
|
||||
scene.add(new THREE.HemisphereLight(0xdcecff, 0x121820, 1.4));
|
||||
let camera: THREE.PerspectiveCamera | THREE.OrthographicCamera = new THREE.PerspectiveCamera(40, 1, 0.001, 100);
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false, preserveDrawingBuffer: true });
|
||||
renderer.setPixelRatio(Math.min(globalThis.devicePixelRatio || 1, 2));
|
||||
renderer.setPixelRatio(Math.min(globalThis.devicePixelRatio || 1, WEB_SETTINGS.viewer.pixel_ratio_limit));
|
||||
renderer.domElement.dataset.engine = "three.js r185 / xyzac-tool-preview";
|
||||
element.appendChild(renderer.domElement);
|
||||
let controls: OrbitControls<THREE.PerspectiveCamera | THREE.OrthographicCamera> = new OrbitControls(camera, renderer.domElement);
|
||||
@@ -85,12 +86,12 @@ export function XyzacToolPreview({ visual, view = "p", zoom = 1, rotateMode = fa
|
||||
}
|
||||
group.clear();
|
||||
};
|
||||
const completed = new THREE.LineBasicMaterial({ color: 0xd8dfe3, depthTest: false });
|
||||
const rapid = new THREE.LineDashedMaterial({ color: 0xa85d5d, dashSize: .025, gapSize: .018, depthTest: false });
|
||||
const completed = new THREE.LineBasicMaterial({ color: WEB_SETTINGS.viewer.toolpath_colors.completed, depthTest: false });
|
||||
const rapid = new THREE.LineDashedMaterial({ color: WEB_SETTINGS.viewer.toolpath_colors.rapid, dashSize: .025, gapSize: .018, depthTest: false });
|
||||
// LinuxCNC uses yellow for jog backplot. Coordinated program execution is
|
||||
// red (live plot), while an unambiguous current program block is cyan.
|
||||
const live = new THREE.LineBasicMaterial({ color: 0x00ffff, depthTest: false });
|
||||
const liveHistory = new THREE.LineBasicMaterial({ color: 0xff6b4a, depthTest: false });
|
||||
const live = new THREE.LineBasicMaterial({ color: WEB_SETTINGS.viewer.toolpath_colors.active, depthTest: false });
|
||||
const liveHistory = new THREE.LineBasicMaterial({ color: WEB_SETTINGS.viewer.toolpath_colors.live, depthTest: false });
|
||||
let frame = 0;
|
||||
let liveTick = -1;
|
||||
let liveTrace: THREE.Vector3[] = [];
|
||||
|
||||
2
web/react/src/legacy.d.ts
vendored
@@ -1,4 +1,6 @@
|
||||
declare module "*.mjs" {
|
||||
export const WEB_SETTINGS: typeof import("../../workspace/web-settings.json");
|
||||
export function axisImagePath(name: string, base?: string): string;
|
||||
export const TRT_GEOMETRY_KEYS: readonly string[];
|
||||
export function readTrtGeometry(configuration: { entrypoint: string; files: Record<string, string> }): { coordinates: string; offsets: Record<string, number>; sources: Record<string, { path: string; line: number; value: string }>; unsupported: string; state_hash: string };
|
||||
export function writeTrtGeometry<T extends { entrypoint: string; files: Record<string, string> }>(configuration: T, values: Record<string, string | number>): T;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// AXIS ships these exact 21/22/24px bitmap assets in the source tree.
|
||||
export const LINUXCNC_AXIS_ASSETS = "/linuxcnc/share/axis/images";
|
||||
import { axisImagePath, WEB_SETTINGS } from "../../workspace/web-settings.mjs";
|
||||
export const LINUXCNC_AXIS_ASSETS = `${import.meta.env.BASE_URL}${WEB_SETTINGS.assets.axis_images}`;
|
||||
|
||||
export function axisIcon(name: string): string {
|
||||
return `${LINUXCNC_AXIS_ASSETS}/${name}`;
|
||||
return axisImagePath(name, import.meta.env.BASE_URL);
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ async function execute(bundle, source) {
|
||||
const beforeRecovery = session.xyzacKinematics.snapshot();
|
||||
assert.equal(session.restoreCheckpoint(savedCheckpoint).status, "blocked");
|
||||
assert.equal(session.restoreCompactPowerOff(savedCompact, seq + 1).status, "blocked");
|
||||
assert.throws(() => session.importTrace(savedTrace), /trt_recovery_geometry_mismatch/);
|
||||
assert.throws(() => session.importTrace(savedTrace), /trt_recovery_(geometry|configuration)_mismatch/);
|
||||
assert.deepEqual(session.xyzacKinematics.snapshot(), beforeRecovery);
|
||||
assert.equal(session.wasmHomeLimit.read().state.homed, false);
|
||||
return { trace, offsets: expected, final: trace.at(-1), preview_hash: fnv1a64(preview.payload.preview), trace_hash: fnv1a64(trace), model_samples: samples.length };
|
||||
|
||||
@@ -42,7 +42,7 @@ assert.equal(visual.source, "linuxcnc-wasm-worker");
|
||||
assert.equal(visual.machineModel?.id, "xyzac-trt");
|
||||
assert.equal(visual.machineModel?.source_hash, "fnv1a64:model");
|
||||
assert.deepEqual(XYZAC_MACHINE_GROUPS, ["xyzac-machine", "table-x", "saddle-y", "spindle-z", "tilt-a", "rotate-c", "tool-offset", "xyzac-toolpath", "xyzac-live-toolpath"]);
|
||||
for (const token of ["table-x", "saddle-y", "spindle-z", "tilt-a", "rotate-c", "tool-offset", "tableX.position.x = -tableXJoint * MM", "saddle.position.z = -saddleYJoint * MM", "zslide.position.y = (200 - spindleZJoint) * MM", "tilt.rotation.x = -tiltAJoint * DEG", "rotate.rotation.y = -rotateCJoint * DEG", "tool.position.y = -state.offsets.tool_offset * MM", "assembly.joints.rotate.add(pathGroup)", "assembly.joints.rotate.add(liveGroup)", "dataset.xyzacToolpathFrame", "dataset.xyzacToolpathAttached", "data-xyzac-tick", "data-xyzac-state-hash", "data-xyzac-active-segment", "data-xyzac-joints", "data-xyzac-offsets"]) assert.ok(source.includes(token), token);
|
||||
for (const token of ["table-x", "saddle-y", "spindle-z", "tilt-a", "rotate-c", "tool-offset", "tableX.position.x = -tableXJoint * MM", "saddle.position.z = -saddleYJoint * MM", "zslide.position.y = (datum - spindleZJoint) * MM", "tilt.rotation.x = -tiltAJoint * DEG", "rotate.rotation.y = -rotateCJoint * DEG", "tool.position.y = -state.offsets.tool_offset * MM", "assembly.joints.rotate.add(pathGroup)", "assembly.joints.rotate.add(liveGroup)", "dataset.xyzacToolpathFrame", "dataset.xyzacToolpathAttached", "data-xyzac-tick", "data-xyzac-state-hash", "data-xyzac-active-segment", "data-xyzac-joints", "data-xyzac-offsets"]) assert.ok(source.includes(token), token);
|
||||
for (const forbidden of [/accept\s*\(/, /postMessage\s*\(/, /_sim_/, /_malloc/, /navigator\.storage/, /HAL_SET/, /KINEMATICS_SELECT/]) assert.equal(forbidden.test(source), false, String(forbidden));
|
||||
assert.ok(adapter.includes("source: \"linuxcnc-wasm-worker\""));
|
||||
assert.equal(source.includes("axis-preview-hud xyzac-machine-hud"), false, "machine preview HUD must stay empty");
|
||||
|
||||
@@ -2,27 +2,18 @@ import * as THREE from "three";
|
||||
import { RoundedBoxGeometry } from "three/examples/jsm/geometries/RoundedBoxGeometry.js";
|
||||
import type { XyzacVisualState } from "./xyzac-visual-adapter";
|
||||
|
||||
const MM = .01;
|
||||
import { WEB_SETTINGS } from "../../workspace/web-settings.mjs";
|
||||
const MM = WEB_SETTINGS.viewer.mm_scale;
|
||||
const DEG = Math.PI / 180;
|
||||
|
||||
// Source coordinates remain millimetres, Z up. The existing viewer uses Y up.
|
||||
function buildTrtMachine(coordinates = "XYZAC") {
|
||||
const isXyzbc = coordinates === "XYZBC";
|
||||
const datum = WEB_SETTINGS.viewer.models[isXyzbc ? "XYZBC" : "XYZAC"].datum_z_mm;
|
||||
// Separate source identities: XYZAC uses tilt-a, XYZBC uses tilt-b.
|
||||
const rotaryAxis = isXyzbc ? "b" : "a";
|
||||
const geometries = new Map<string, THREE.BufferGeometry>();
|
||||
const materials = {
|
||||
enamel: new THREE.MeshStandardMaterial({ color: 0xd9e1e1, roughness: .32, metalness: .25 }),
|
||||
teal: new THREE.MeshStandardMaterial({ color: 0x167f79, roughness: .3, metalness: .3 }),
|
||||
dark: new THREE.MeshStandardMaterial({ color: 0x303638, roughness: .42, metalness: .35 }),
|
||||
steel: new THREE.MeshStandardMaterial({ color: 0xb2bdc4, roughness: .27, metalness: .85 }),
|
||||
polished: new THREE.MeshStandardMaterial({ color: 0xe2e8ec, roughness: .19, metalness: .9 }),
|
||||
recess: new THREE.MeshStandardMaterial({ color: 0x111718, roughness: .66, metalness: .2 }),
|
||||
rubber: new THREE.MeshStandardMaterial({ color: 0x222729, roughness: .88 }),
|
||||
carbide: new THREE.MeshStandardMaterial({ color: 0xb9aa79, roughness: .29, metalness: .8 }),
|
||||
warning: new THREE.MeshStandardMaterial({ color: 0xe7bd48, roughness: .48, metalness: .12 }),
|
||||
lamp: new THREE.MeshStandardMaterial({ color: 0xe2f8ff, emissive: 0xd4f2ff, emissiveIntensity: 1.5 }),
|
||||
};
|
||||
const materials = Object.fromEntries(Object.entries(WEB_SETTINGS.viewer.materials).map(([name, properties]) => [name, new THREE.MeshStandardMaterial(properties)])) as Record<keyof typeof WEB_SETTINGS.viewer.materials, THREE.MeshStandardMaterial>;
|
||||
type Finish = keyof typeof materials;
|
||||
const geometry = (key: string, create: () => THREE.BufferGeometry) => {
|
||||
if (!geometries.has(key)) geometries.set(key, create());
|
||||
@@ -84,7 +75,7 @@ function buildTrtMachine(coordinates = "XYZAC") {
|
||||
box(machine, "z-servo", 0, -105, -169, 49, 21, 50, "dark", 3);
|
||||
box(machine, "z-servo-cover", 0, -117, -169, 40, 3, 38, "teal", 2);
|
||||
|
||||
const head = group(machine, "head", 0, 0, isXyzbc ? 150 : 200);
|
||||
const head = group(machine, "head", 0, 0, datum);
|
||||
// XYZAC has the long moving knee bridge; XYZBC has the shorter moving
|
||||
// spindle head from xyzbc-trt-gui.py.
|
||||
const headDepth = isXyzbc ? 130 : 270;
|
||||
@@ -175,8 +166,8 @@ function buildTrtMachine(coordinates = "XYZAC") {
|
||||
}
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const slot = box(rotate, "radial-t-slot", 0, 0, .14, 3, 42, .3, "recess", .1);
|
||||
slot.position.x = Math.sin(i * Math.PI / 2) * .28;
|
||||
slot.position.z = Math.cos(i * Math.PI / 2) * .28;
|
||||
slot.position.x = Math.sin(i * Math.PI / 2) * (28 * MM);
|
||||
slot.position.z = Math.cos(i * Math.PI / 2) * (28 * MM);
|
||||
slot.rotation.y = i * Math.PI / 2;
|
||||
}
|
||||
for (let i = 0; i < 72; i++) {
|
||||
@@ -197,18 +188,18 @@ function buildTrtMachine(coordinates = "XYZAC") {
|
||||
tableX.position.x = -tableXJoint * MM;
|
||||
saddle.position.z = -saddleYJoint * MM;
|
||||
if (coordinates === "XYZBC") {
|
||||
head.position.y = (150 + spindleZJoint) * MM;
|
||||
head.position.y = (datum + spindleZJoint) * MM;
|
||||
// XYZBC is a table tilt: the source model rotates the B assembly
|
||||
// around machine Y. The Web scene maps source Y to Three.js Z.
|
||||
head.rotation.set(0, 0, 0);
|
||||
zslide.position.y = 150 * MM;
|
||||
zslide.position.y = datum * MM;
|
||||
tilt.rotation.z = -tiltAJoint * DEG;
|
||||
tilt.position.set(xOffset * MM, zOffset * MM, 0).add(pivot);
|
||||
brackets.position.copy(tilt.position);
|
||||
rotaryBase.position.set(-xOffset * MM, -zOffset * MM, 0);
|
||||
} else {
|
||||
zslide.position.y = (200 - spindleZJoint) * MM;
|
||||
head.position.y = 2;
|
||||
zslide.position.y = (datum - spindleZJoint) * MM;
|
||||
head.position.y = datum * MM;
|
||||
head.rotation.set(0, 0, 0);
|
||||
// XYZAC source transform: tilt.rotation.x = -tiltAJoint * DEG.
|
||||
tilt.rotation.x = -tiltAJoint * DEG;
|
||||
@@ -251,8 +242,8 @@ function createFlutedCutter() {
|
||||
const angle = col / radial * Math.PI * 2;
|
||||
const phase = (angle * 4 - t * Math.PI * 2 + Math.PI * 4) % (Math.PI * 2);
|
||||
const relief = Math.pow(Math.max(0, Math.sin(phase / 2)), 2);
|
||||
const radius = (3 - 1.35 * relief) * .01;
|
||||
positions.push(radius * Math.cos(angle), t * .08, radius * Math.sin(angle));
|
||||
const radius = (3 - 1.35 * relief) * MM;
|
||||
positions.push(radius * Math.cos(angle), t * 8 * MM, radius * Math.sin(angle));
|
||||
if (row < axial && col < radial) {
|
||||
const a = row * (radial + 1) + col, b = a + radial + 1;
|
||||
indices.push(a, b, a + 1, a + 1, b, b + 1);
|
||||
@@ -260,7 +251,7 @@ function createFlutedCutter() {
|
||||
}
|
||||
}
|
||||
const bottom = positions.length / 3; positions.push(0, 0, 0);
|
||||
const top = positions.length / 3; positions.push(0, .08, 0);
|
||||
const top = positions.length / 3; positions.push(0, 8 * MM, 0);
|
||||
for (let col = 0; col < radial; col++) {
|
||||
indices.push(bottom, col, col + 1);
|
||||
const a = axial * (radial + 1) + col;
|
||||
|
||||
@@ -8,7 +8,7 @@ import tailwindcss from "@tailwindcss/vite";
|
||||
const repoRoot = fileURLToPath(new URL("../../", import.meta.url));
|
||||
const sourceRoot = resolve(repoRoot, "linuxcnc");
|
||||
const contentTypes: Record<string, string> = {
|
||||
".ini": "text/plain; charset=utf-8", ".hal": "text/plain; charset=utf-8", ".inc": "text/plain; charset=utf-8", ".tbl": "text/plain; charset=utf-8", ".var": "text/plain; charset=utf-8", ".tcl": "text/plain; charset=utf-8", ".ngc": "text/plain; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".mjs": "text/javascript; charset=utf-8", ".gif": "image/gif", ".png": "image/png", ".svg": "image/svg+xml",
|
||||
".json": "application/json; charset=utf-8", ".ini": "text/plain; charset=utf-8", ".hal": "text/plain; charset=utf-8", ".inc": "text/plain; charset=utf-8", ".tbl": "text/plain; charset=utf-8", ".var": "text/plain; charset=utf-8", ".tcl": "text/plain; charset=utf-8", ".ngc": "text/plain; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".mjs": "text/javascript; charset=utf-8", ".gif": "image/gif", ".png": "image/png", ".svg": "image/svg+xml",
|
||||
};
|
||||
|
||||
function linuxcncAssets(): Plugin {
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { fnv1a64 } from "./replay.mjs";
|
||||
import { WEB_SETTINGS } from "../workspace/web-settings.mjs";
|
||||
const CONFIG = WEB_SETTINGS.runtime.compatibility;
|
||||
|
||||
export const AUTHORITATIVE_RUNTIME_SCHEMA = "sim_authoritative_runtime.v1";
|
||||
const AXES = Object.freeze(["x", "y", "z"]);
|
||||
const STEP_FRACTION = 0.25;
|
||||
const STEP_FRACTION = CONFIG.step_fraction;
|
||||
const KINEMATICS = new Set(["trivkins", "corexy", "tripod", "lineardelta", "rotarydelta"]);
|
||||
const KINEMATICS_ALIASES = new Map([["corexykins", "corexy"], ["tripodkins", "tripod"], ["lineardeltakins", "lineardelta"], ["rotarydeltakins", "rotarydelta"]]);
|
||||
const TRIPOD_BASE = Object.freeze([[0, 0], [10, 0], [5, 7.071]]);
|
||||
const LINEAR_DELTA_RADIUS = 130.25;
|
||||
const LINEAR_DELTA_ROD = 269;
|
||||
const ROTARY_DELTA_PLATFORM = 10;
|
||||
const ROTARY_DELTA_THIGH = 10;
|
||||
const ROTARY_DELTA_SHIN = 14;
|
||||
const ROTARY_DELTA_FOOT = 6;
|
||||
const TRIPOD_BASE = CONFIG.tripod_base;
|
||||
const LINEAR_DELTA_RADIUS = CONFIG.linear_delta.radius;
|
||||
const LINEAR_DELTA_ROD = CONFIG.linear_delta.rod;
|
||||
const ROTARY_DELTA_PLATFORM = CONFIG.rotary_delta.platform;
|
||||
const ROTARY_DELTA_THIGH = CONFIG.rotary_delta.thigh;
|
||||
const ROTARY_DELTA_SHIN = CONFIG.rotary_delta.shin;
|
||||
const ROTARY_DELTA_FOOT = CONFIG.rotary_delta.foot;
|
||||
const DEG_TO_RAD = Math.PI / 180;
|
||||
const RAD_TO_DEG = 180 / Math.PI;
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { fnv1a64 } from "./replay.mjs";
|
||||
import { WEB_SETTINGS } from "../workspace/web-settings.mjs";
|
||||
const CONFIG = WEB_SETTINGS.runtime.compatibility;
|
||||
|
||||
export const MOTION_SCHEMA = "sim_motion.v1";
|
||||
const AXES = Object.freeze(["x", "y", "z"]);
|
||||
const MAX_POSITION = 1e9;
|
||||
const MAX_SPEED = 1e6;
|
||||
const CYCLE_DISTANCE = 0.001;
|
||||
const MAX_POSITION = CONFIG.max_position;
|
||||
const MAX_SPEED = CONFIG.max_speed;
|
||||
const CYCLE_DISTANCE = CONFIG.cycle_time;
|
||||
|
||||
function clone(value) { return structuredClone(value); }
|
||||
function finite(value) { return typeof value === "number" && Number.isFinite(value); }
|
||||
|
||||
55
web/worker/native-checkpoint.mjs
Normal file
@@ -0,0 +1,55 @@
|
||||
import { fnv1a64 } from "./replay.mjs";
|
||||
|
||||
function buildId(core) {
|
||||
if (typeof core._sim_checkpoint_build_id !== "function") throw new Error("native_checkpoint_build_id_missing");
|
||||
const pointer = core._sim_checkpoint_build_id();
|
||||
const id = new TextDecoder().decode(core.HEAPU8.subarray(pointer, pointer + 64));
|
||||
if (!/^[a-f0-9]{64}$/.test(id)) throw new Error("native_checkpoint_unversioned_build");
|
||||
return id;
|
||||
}
|
||||
|
||||
// Copy opaque native state with zero-filled blocks omitted. No arithmetic or
|
||||
// state reconstruction occurs here; each C adapter owns its layout/validation.
|
||||
export function exportNativeCheckpoint(core, prefix, owner = null) {
|
||||
const length = core[`_${prefix}_checkpoint_size`]();
|
||||
const pointer = core._malloc(length);
|
||||
if (!pointer) throw new Error(`${prefix}_checkpoint_allocation`);
|
||||
try {
|
||||
const args = owner === null ? [pointer, length] : [owner, pointer, length];
|
||||
const code = core[`_${prefix}_checkpoint_export`](...args);
|
||||
if (code !== 0) throw new Error(`${prefix}_checkpoint_export_${code}`);
|
||||
const bytes = core.HEAPU8.subarray(pointer, pointer + length), blocks = [];
|
||||
for (let offset = 0; offset < length; offset += 256) {
|
||||
const block = bytes.subarray(offset, Math.min(offset + 256, length));
|
||||
if (block.some((value) => value !== 0)) blocks.push([offset, Array.from(block, (value) => value.toString(16).padStart(2, "0")).join("")]);
|
||||
}
|
||||
const value = { schema: "native-checkpoint.v1", component: prefix, build_id: buildId(core), byte_length: length, blocks };
|
||||
return { ...value, state_hash: fnv1a64(value) };
|
||||
} finally { core._free(pointer); }
|
||||
}
|
||||
|
||||
export function validateNativeCheckpoint(core, prefix, value) {
|
||||
if (value?.build_id !== buildId(core)) throw new Error(`${prefix}_checkpoint_build_mismatch`);
|
||||
const length = core[`_${prefix}_checkpoint_size`]();
|
||||
if (!value || value.schema !== "native-checkpoint.v1" || value.component !== prefix || value.byte_length !== length || !Array.isArray(value.blocks) || fnv1a64(Object.fromEntries(Object.entries(value).filter(([key]) => key !== "state_hash"))) !== value.state_hash) throw new Error(`${prefix}_checkpoint_invalid`);
|
||||
const bytes = new Uint8Array(length); let end = 0;
|
||||
for (const [offset, hex] of value.blocks) {
|
||||
if (!Number.isSafeInteger(offset) || offset < end || typeof hex !== "string" || !/^(?:[0-9a-f]{2})+$/.test(hex) || offset + hex.length / 2 > length) throw new Error(`${prefix}_checkpoint_blocks`);
|
||||
for (let index = 0; index < hex.length; index += 2) bytes[offset + index / 2] = Number.parseInt(hex.slice(index, index + 2), 16);
|
||||
end = offset + hex.length / 2;
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
export function restoreNativeCheckpoint(core, prefix, value, owner = null) {
|
||||
const bytes = validateNativeCheckpoint(core, prefix, value);
|
||||
const length = bytes.length;
|
||||
const pointer = core._malloc(length);
|
||||
if (!pointer) throw new Error(`${prefix}_checkpoint_allocation`);
|
||||
try {
|
||||
core.HEAPU8.set(bytes, pointer);
|
||||
const args = owner === null ? [pointer, length] : [owner, pointer, length];
|
||||
const code = core[`_${prefix}_checkpoint_restore`](...args);
|
||||
if (code !== 0) throw new Error(`${prefix}_checkpoint_restore_${code}`);
|
||||
} finally { core._free(pointer); }
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { fnv1a64 } from "./replay.mjs";
|
||||
import { DEFAULT_MACHINE } from "../workspace/machine-definitions.mjs";
|
||||
|
||||
export const OPFS_UI_STATE_SCHEMA = "linuxcnc-ui-temp-json.v1";
|
||||
export const OPFS_UI_STATE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
@@ -77,7 +78,7 @@ async function writeFile(project, name, value) {
|
||||
return { status: "verified", bytes: bytes.byteLength };
|
||||
}
|
||||
|
||||
export async function loadUiState({ contextId, key = "xyzac-trt", storage } = {}) {
|
||||
export async function loadUiState({ contextId, key = DEFAULT_MACHINE.id, storage } = {}) {
|
||||
const resolved = await handles(key, false, storage).catch((error) => ({ status: "blocked", code: "opfs_open_failed", reason: error?.name ?? "unknown" }));
|
||||
if (resolved.status === "blocked" && resolved.reason === "NotFoundError") return emptyResult(contextId, key);
|
||||
if (resolved.status !== "verified") return { schema: OPFS_UI_STATE_SCHEMA, ...resolved, context_id: contextId, key };
|
||||
@@ -92,7 +93,7 @@ export async function loadUiState({ contextId, key = "xyzac-trt", storage } = {}
|
||||
return { schema: OPFS_UI_STATE_SCHEMA, status: "verified", storage: "opfs", context_id: contextId, key, generation: selected.generation, state: selected.state, state_hash: selected.state_hash };
|
||||
}
|
||||
|
||||
export async function saveUiState({ contextId, key = "xyzac-trt", state, storage } = {}) {
|
||||
export async function saveUiState({ contextId, key = DEFAULT_MACHINE.id, state, storage } = {}) {
|
||||
if (!validObject(state) || !finiteTree(state)) return { schema: OPFS_UI_STATE_SCHEMA, status: "blocked", code: "opfs_ui_state_invalid", context_id: contextId, key };
|
||||
const resolved = await handles(key, true, storage).catch((error) => ({ status: "blocked", code: "opfs_open_failed", reason: error?.name ?? "unknown" }));
|
||||
if (resolved.status !== "verified") return { schema: OPFS_UI_STATE_SCHEMA, ...resolved, context_id: contextId, key };
|
||||
@@ -108,23 +109,23 @@ export async function saveUiState({ contextId, key = "xyzac-trt", state, storage
|
||||
return { schema: OPFS_UI_STATE_SCHEMA, status: "verified", storage: "opfs", context_id: contextId, key, generation, slot, state_hash: next.state_hash, bytes: written.bytes };
|
||||
}
|
||||
|
||||
export async function clearUiState({ contextId, key = "xyzac-trt", storage } = {}) {
|
||||
export async function clearUiState({ contextId, key = DEFAULT_MACHINE.id, storage } = {}) {
|
||||
const resolved = await handles(key, false, storage).catch((error) => ({ status: "blocked", code: "opfs_open_failed", reason: error?.name ?? "unknown" }));
|
||||
if (resolved.status !== "verified") return { schema: OPFS_UI_STATE_SCHEMA, ...resolved, context_id: contextId, key };
|
||||
for (const name of [...SLOT_NAMES, MANIFEST_NAME]) { try { await resolved.project.removeEntry(name); } catch {} }
|
||||
return { schema: OPFS_UI_STATE_SCHEMA, status: "verified", storage: "opfs", context_id: contextId, key, cleared: true };
|
||||
}
|
||||
|
||||
export async function savePowerOffCheckpoint({ contextId, checkpoint, key = "xyzac-trt", storage } = {}) {
|
||||
export async function savePowerOffCheckpoint({ contextId, checkpoint, key = DEFAULT_MACHINE.id, storage } = {}) {
|
||||
return saveUiState({ contextId, key: `${key}-power-off`, state: { kind: "power_off_checkpoint", checkpoint }, storage });
|
||||
}
|
||||
|
||||
export async function loadPowerOffCheckpoint({ contextId, key = "xyzac-trt", storage } = {}) {
|
||||
export async function loadPowerOffCheckpoint({ contextId, key = DEFAULT_MACHINE.id, storage } = {}) {
|
||||
const result = await loadUiState({ contextId, key: `${key}-power-off`, storage });
|
||||
if (result.status !== "verified" || result.state?.kind !== "power_off_checkpoint" || !result.state.checkpoint) return { ...result, checkpoint: null };
|
||||
return { ...result, checkpoint: result.state.checkpoint };
|
||||
}
|
||||
|
||||
export async function clearPowerOffCheckpoint({ contextId, key = "xyzac-trt", storage } = {}) {
|
||||
export async function clearPowerOffCheckpoint({ contextId, key = DEFAULT_MACHINE.id, storage } = {}) {
|
||||
return clearUiState({ contextId, key: `${key}-power-off`, storage });
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
savePowerOffCheckpoint,
|
||||
saveUiState,
|
||||
} from "./opfs-ui-state.mjs";
|
||||
import { DEFAULT_MACHINE, machineDefinition } from "../workspace/machine-definitions.mjs";
|
||||
|
||||
const UI_COMMANDS = new Set(["UI_STATE_SAVE", "UI_STATE_LOAD", "UI_STATE_CLEAR"]);
|
||||
|
||||
@@ -34,7 +35,7 @@ export async function handleWorkerMessage(session, input) {
|
||||
// UI cache is a Worker storage service, not a LinuxCNC command. It never
|
||||
// consumes the WASM protocol sequence or changes task/motion authority.
|
||||
const payload = object(input.payload);
|
||||
const options = { contextId: input.context_id, key: payload.key ?? "xyzac-trt" };
|
||||
const options = { contextId: input.context_id, key: payload.key ?? session.machineDefinition?.id ?? DEFAULT_MACHINE.id };
|
||||
const result = input.kind === "UI_STATE_SAVE"
|
||||
? await saveUiState({ ...options, state: payload.state })
|
||||
: input.kind === "UI_STATE_LOAD"
|
||||
@@ -44,18 +45,32 @@ export async function handleWorkerMessage(session, input) {
|
||||
}
|
||||
|
||||
let request = input;
|
||||
if (input?.kind === "LOAD_WORKSPACE" && !object(input.payload).recovery_checkpoint) {
|
||||
const persisted = await loadPowerOffCheckpoint({ contextId: input.context_id, key: "xyzac-trt" });
|
||||
if (persisted.status === "verified" && persisted.checkpoint) request = { ...input, payload: { ...object(input.payload), recovery_checkpoint: persisted.checkpoint } };
|
||||
let recoveryNotice = null;
|
||||
const configuration = object(input.payload).configuration;
|
||||
const requestedMachine = machineDefinition(configuration?.entrypoint);
|
||||
const key = requestedMachine?.id ?? session.machineDefinition?.id ?? DEFAULT_MACHINE.id;
|
||||
// Automatic restoration belongs to a new Worker session. Explicit INI
|
||||
// changes and machine switches must not be overwritten by saved state.
|
||||
if (input?.kind === "LOAD_WORKSPACE" && !session.machineDefinition && (!configuration?.entrypoint || requestedMachine) && !object(input.payload).recovery_checkpoint) {
|
||||
const persisted = await loadPowerOffCheckpoint({ contextId: input.context_id, key });
|
||||
if (persisted.status === "verified" && persisted.checkpoint) {
|
||||
try {
|
||||
if (configuration && session.core && typeof session.validateRecoveryGeometry === "function") session.validateRecoveryGeometry(persisted.checkpoint.state, configuration);
|
||||
request = { ...input, payload: { ...object(input.payload), recovery_checkpoint: persisted.checkpoint } };
|
||||
} catch (error) {
|
||||
recoveryNotice = { status: "ignored", code: String(error?.message ?? error), reason: "saved_checkpoint_incompatible_with_current_configuration" };
|
||||
}
|
||||
}
|
||||
}
|
||||
const response = session.accept(request);
|
||||
const payload = object(response?.payload);
|
||||
if (recoveryNotice) payload.recovery_persistence = recoveryNotice;
|
||||
if (input?.kind === "POWER" && response?.kind === "COMMAND_RESULT" && payload.status === "verified") {
|
||||
if (payload.active === false && payload.recovery_checkpoint) {
|
||||
const persisted = await savePowerOffCheckpoint({ contextId: input.context_id, checkpoint: payload.recovery_checkpoint, key: "xyzac-trt" });
|
||||
const persisted = await savePowerOffCheckpoint({ contextId: input.context_id, checkpoint: payload.recovery_checkpoint, key });
|
||||
payload.recovery_persistence = persisted;
|
||||
} else if (payload.active === true) {
|
||||
payload.recovery_persistence = await clearPowerOffCheckpoint({ contextId: input.context_id, key: "xyzac-trt" });
|
||||
payload.recovery_persistence = await clearPowerOffCheckpoint({ contextId: input.context_id, key });
|
||||
}
|
||||
}
|
||||
return response;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { exportNativeCheckpoint, restoreNativeCheckpoint, validateNativeCheckpoint } from "./native-checkpoint.mjs";
|
||||
import { ReplayTimeline, fnv1a64 } from "./replay.mjs";
|
||||
import { capabilities, validateLimit } from "./capabilities.mjs";
|
||||
import { mapCoreError } from "./error-map.mjs";
|
||||
@@ -10,7 +11,7 @@ import { PythonBridge } from "./python-bridge.mjs";
|
||||
import { ProgramRunner } from "./program-runner.mjs";
|
||||
import { SimTask } from "./sim-task.mjs";
|
||||
import { SimScheduler, SCHEDULER_PHASES } from "./sim-scheduler.mjs";
|
||||
import { OperationLedger } from "./operation-ledger.mjs";
|
||||
import { OperationLedger, LEDGER_SCHEMA } from "./operation-ledger.mjs";
|
||||
import { MotionState } from "./motion-state.mjs";
|
||||
import { PlcRuntime } from "./plc-runtime.mjs";
|
||||
import { HalRuntime } from "./hal-runtime.mjs";
|
||||
@@ -31,8 +32,12 @@ import { WasmXyzacIo } from "./wasm-xyzac-io.mjs";
|
||||
import { WasmXyzacDeviceIo } from "./wasm-xyzac-device-io.mjs";
|
||||
import { WasmProgramRuntime } from "./wasm-program-runtime.mjs";
|
||||
import { axisGate, xyzacGate, xyzacGateMatrix } from "../app/axis-contract.mjs";
|
||||
import { machineDefinition } from "../workspace/machine-definitions.mjs";
|
||||
import { readTrtGeometry, TRT_GEOMETRY_KEYS } from "../workspace/trt-geometry.mjs";
|
||||
import { machineDefinition, DEFAULT_MACHINE } from "../workspace/machine-definitions.mjs";
|
||||
import { WEB_SETTINGS } from "../workspace/web-settings.mjs";
|
||||
import { TRT_GEOMETRY_KEYS } from "../workspace/trt-geometry.mjs";
|
||||
import { readTrtConfiguration } from "../workspace/trt-configuration.mjs";
|
||||
|
||||
const TRT_EXECUTION_KEYS = Object.freeze(["planner", "xyzacProgramKinematicsMode", "pendingProgramControl", "pendingTaskSync", "pendingXyzacMotion", "xyzacStepTargetId", "pendingServoSync", "xyzacExecutionWaitNextMotion", "xyzacExecutionWaitSamples", "xyzacLastEnqueuedKinematicsMode", "xyzacDeferredPhysicalSwitchMode", "xyzacDeferredBlockedActiveId", "xyzacDeferredControlMotionType", "xyzacQueuedLookaheadTargetId", "xyzacAllowQueuedLookahead", "xyzacLastServoActiveId", "xyzacLastQueuedMotion", "xyzacDeferredPhysicalPose", "xyzacDeferredPhysicalPoseApplied", "xyzacAppendDeferredOnActive", "xyzacDeferredCompletionActiveId"]);
|
||||
|
||||
const PROTOCOL_SCHEMA = 1;
|
||||
const CONTEXT_ID = "linuxcnc-2.10.0-pre0-wasm-m1-py-c1";
|
||||
@@ -138,10 +143,11 @@ export class SimSession {
|
||||
this.xyzacDeviceIo = null;
|
||||
this.xyzacProgram = null;
|
||||
this.machineDefinition = null;
|
||||
this.machineConfigurationHash = null;
|
||||
this.homingHistory = [];
|
||||
this.xyzacProgramKinematicsMode = XYZAC_MODES.identity;
|
||||
this.xyzacRapidVelocity = 35;
|
||||
this.xyzacMotionCycleTime = 0.01;
|
||||
this.xyzacRapidVelocity = null;
|
||||
this.xyzacMotionCycleTime = WEB_SETTINGS.runtime.io.cycle_time;
|
||||
this.xyzacTaskCycleTicks = 1;
|
||||
this.xyzacAxisVelocityLimits = {};
|
||||
this.xyzacAxisAccelerationLimits = {};
|
||||
@@ -300,7 +306,7 @@ export class SimSession {
|
||||
velocity: wasmMotion.status === "verified" ? wasmMotion.state.velocity : 0,
|
||||
velocity_mm_min: Number(wasmMotion.state?.velocity_mm_min ?? 0),
|
||||
feed_command_mm_min: Number(sourceProgram?.feed ?? 0),
|
||||
feed_override: Number(wasmMotion.state?.feed_override ?? 1),
|
||||
feed_override: Number(wasmMotion.state?.feed_override ?? this.wasmMotion.controls?.feed_override ?? 1),
|
||||
spindle_override: Number(ioState?.spindles?.[0]?.override ?? 1),
|
||||
spindle_speed_rpm: Number(spindle?.speed_rpm ?? 0),
|
||||
spindle_command_rpm: Number(spindle?.command_rpm ?? 0),
|
||||
@@ -310,6 +316,7 @@ export class SimSession {
|
||||
jog_scale: Number(wasmMotion.state?.jog_scale ?? 1),
|
||||
jog_velocity_linear: Number(wasmMotion.state?.jog_velocity_linear ?? 0),
|
||||
jog_velocity_angular: Number(wasmMotion.state?.jog_velocity_angular ?? 0),
|
||||
max_feed_override: this.wasmMotion.maxFeedOverride ?? null,
|
||||
max_velocity: Number(wasmMotion.state?.velocity_limit ?? 0),
|
||||
homed: Boolean(home.state?.homed),
|
||||
homed_axes: Array.from({ length: 5 }, (_, index) => Boolean(Number(home.state?.homed_mask ?? 0) & (1 << index))),
|
||||
@@ -330,7 +337,7 @@ export class SimSession {
|
||||
if (this.xyzacProgram) return this.wasmMotionProjection();
|
||||
if (this.motionAuthority !== "wasm") return this.motion.export();
|
||||
const wasm = this.wasmMotion.read();
|
||||
return wasm.status === "verified" ? { ...this.motion.export(), axes: { ...wasm.state.position } } : this.motion.export();
|
||||
return wasm.status === "verified" ? { ...this.motion.export(), axes: { ...wasm.state.position }, queue_depth: wasm.state.queue_depth } : this.motion.export();
|
||||
}
|
||||
|
||||
wasmDeviceProjection({ includeBytes = false } = {}) {
|
||||
@@ -359,7 +366,7 @@ export class SimSession {
|
||||
if (requirement.status !== "verified") return requirement;
|
||||
if (!requirement.required) return { status: "verified", count: 0 };
|
||||
const basePath = String(programPath ?? "").split("/").slice(0, -1).join("/");
|
||||
const coordinates = this.machineDefinition?.coordinates ?? "XYZAC";
|
||||
const coordinates = this.machineDefinition?.coordinates ?? DEFAULT_MACHINE.coordinates;
|
||||
const stem = coordinates.toLowerCase();
|
||||
const rows = [[`remap_subs/${stem}_switchkins_sub.ngc`, `${stem}_switchkins_sub`], [`remap_subs/helix_${stem.slice(3)}.ngc`, `helix_${stem.slice(3)}`]];
|
||||
for (const [suffix, name] of rows) {
|
||||
@@ -407,7 +414,7 @@ export class SimSession {
|
||||
? { schema: "wasm_authority_boundary.v1", source: "linuxcnc-wasm-worker", state_hash: fnv1a64({ motion: wasmMotion, kinematics: this.xyzacKinematics?.snapshot() ?? null, task }) }
|
||||
: this.authoritative.export();
|
||||
const axis = axisState({ task, program: program ?? {}, motion, devices, io: this.xyzacIo ? this.xyzacIo.export() : {}, device_io: deviceIo });
|
||||
const xyzac = this.xyzacKinematics ? { coordinates: this.machineDefinition?.coordinates ?? "XYZAC", joint_count: 5, hal_pin_vars: true, switchkins_net: true, finite_offsets: true, userk_capability: true, active_type: this.xyzacKinematics.snapshot().active_type } : null;
|
||||
const xyzac = this.xyzacKinematics ? { coordinates: this.machineDefinition?.coordinates ?? DEFAULT_MACHINE.coordinates, joint_count: this.xyzacKinematics.coordinates.length, hal_pin_vars: true, switchkins_net: true, finite_offsets: true, userk_capability: true, active_type: this.xyzacKinematics.snapshot().active_type } : null;
|
||||
const gateInput = { task, program: program ?? {}, motion, devices, io: this.xyzacIo ? this.xyzacIo.export() : {}, device_io: deviceIo, xyzac, taskfile: Boolean(this.xyzacProgram ? this.xyzacProgram.snapshot().status !== 0 : this.program.loaded), kinematicsIdentity: this.motionAuthority === "wasm" };
|
||||
const gateActions = ["load", "run", "runLine", "step", "pause", "resume", "abort", "restart", "reload", "edit", "mdi", "jogStart", "jogStop", "home", "estop", "recover", "override", "overrideLimits", "reset", "spindle", "coolant", "touchOff", "toolTouchOff"];
|
||||
const ui = axisSourceUi({ task_state: axis.source_task_state, interp_state: axis.source_interp_state,
|
||||
@@ -473,7 +480,7 @@ export class SimSession {
|
||||
// renderer can audit that its geometry matches the loaded LinuxCNC case.
|
||||
const modelResource = this.configurationState?.exact_resources?.find?.((row) => row.role === "vismach_source");
|
||||
const machineDefinition = this.machineDefinition;
|
||||
const modelSourcePath = this.machineDefinition?.source_path ?? "linuxcnc/src/hal/user_comps/vismach/xyzac-trt-gui.py";
|
||||
const modelSourcePath = this.machineDefinition?.source_path ?? DEFAULT_MACHINE.source_path;
|
||||
const modelSourceText = this.workspaceFiles?.[modelSourcePath];
|
||||
const machineModel = this.xyzacProgram ? {
|
||||
schema: "linuxcnc-vismach-model.v1",
|
||||
@@ -481,7 +488,7 @@ export class SimSession {
|
||||
source: "linuxcnc-wasm-worker",
|
||||
source_path: modelResource?.path ?? modelSourcePath,
|
||||
source_hash: modelResource?.data_hash ?? this.visualModelSourceHash ?? (typeof modelSourceText === "string" ? fnv1a64(modelSourceText) : null),
|
||||
coordinate_system: machineDefinition?.coordinates ?? "XYZAC",
|
||||
coordinate_system: machineDefinition?.coordinates ?? DEFAULT_MACHINE.coordinates,
|
||||
geometry_variant: machineDefinition?.coordinates === "XYZBC" ? "moving-head-y-tilt" : "moving-knee-x-tilt",
|
||||
tilt_axis: machineDefinition?.coordinates === "XYZBC" ? "Y" : "X",
|
||||
rotary_axis: "Z",
|
||||
@@ -507,7 +514,7 @@ export class SimSession {
|
||||
coordinate_offset: program?.coordinate_context?.offsets?.[(program?.coordinate_context?.system ?? 1) - 1]?.slice(0, 3) ?? null,
|
||||
...(machineModel ? { machine_model: machineModel } : {}),
|
||||
};
|
||||
const coordinateAxes = machineDefinition?.axes ?? ["x", "y", "z", "a", "c"];
|
||||
const coordinateAxes = machineDefinition?.axes ?? DEFAULT_MACHINE.axes;
|
||||
const axisCoordinates = coordinateAxes.map((axis) => position[axis] ?? 0);
|
||||
const coordinateContext = program?.coordinate_context;
|
||||
const activeOffsets = coordinateContext?.offsets?.[coordinateContext.system - 1] ?? Array(9).fill(0);
|
||||
@@ -540,7 +547,7 @@ export class SimSession {
|
||||
authoritative,
|
||||
motion_authority: this.motionAuthority,
|
||||
task_authority: this.task.authority(),
|
||||
...(this.xyzacKinematics ? { kinematics: this.xyzacKinematics.snapshot() } : {}),
|
||||
...(this.xyzacKinematics ? { kinematics: this.xyzacKinematics.snapshot(), machine_configuration_hash: this.machineConfigurationHash } : {}),
|
||||
...(includeStableRuntime && this.xyzacIo ? { io: this.xyzacIo.export() } : {}),
|
||||
...(xyzac ? { xyzac: { ...xyzac, coordinates: machineDefinition?.coordinates ?? xyzac.coordinates } } : {}),
|
||||
};
|
||||
@@ -571,7 +578,7 @@ export class SimSession {
|
||||
commandGate(input) {
|
||||
const task = this.task.export();
|
||||
const program = this.xyzacProgram ? this.xyzacProgram.snapshot() : this.program.loaded ? this.program.snapshot() : {};
|
||||
const motion = this.xyzacProgram ? this.wasmMotionProjection() : this.motion.export();
|
||||
const motion = this.motionProjection();
|
||||
const devices = this.wasmDeviceProjection();
|
||||
// The selected XYZAC case is governed by the native WASM task/IO/program
|
||||
// handlers. JS only receives the result; it must not pre-reject a command
|
||||
@@ -582,7 +589,7 @@ export class SimSession {
|
||||
const type = Number(input.payload?.type);
|
||||
const action = input.kind === "VISUAL_CLEAR" ? "vismachClear" : type === 0 ? "switchkinsIdentity" : type === 1 ? "switchkinsXyzac" : type === 2 ? "switchkinsUserk" : null;
|
||||
if (!action) return { action: null, enabled: false, reason: "xyzac_kinematics_type_invalid" };
|
||||
return xyzacGate(action, { task, program, motion, devices, xyzac: this.xyzacKinematics ? { coordinates: this.machineDefinition?.coordinates ?? "XYZAC", joint_count: 5, hal_pin_vars: true, switchkins_net: true, finite_offsets: true, userk_capability: true } : null });
|
||||
return xyzacGate(action, { task, program, motion, devices, xyzac: this.xyzacKinematics ? { coordinates: this.machineDefinition?.coordinates ?? DEFAULT_MACHINE.coordinates, joint_count: this.xyzacKinematics.coordinates.length, hal_pin_vars: true, switchkins_net: true, finite_offsets: true, userk_capability: true } : null });
|
||||
}
|
||||
const action = PANEL_GATE_ACTIONS[input.kind];
|
||||
if (!action) return { action: null, enabled: true, reason: "axis_internal_command" };
|
||||
@@ -688,7 +695,7 @@ export class SimSession {
|
||||
if (!this.xyzacProgram) return { status: "error", code: "xyzac_program_unavailable" };
|
||||
const payload = input.payload ?? {};
|
||||
const axis = String(payload.axis ?? "").trim().toUpperCase();
|
||||
const coordinateAxes = this.machineDefinition?.axes ?? ["x", "y", "z", "a", "c"];
|
||||
const coordinateAxes = this.machineDefinition?.axes ?? DEFAULT_MACHINE.axes;
|
||||
if (!coordinateAxes.includes(axis.toLowerCase())) return { status: "error", code: "touch_off_axis_invalid" };
|
||||
const value = Number(payload.value);
|
||||
if (!Number.isFinite(value)) return { status: "error", code: "touch_off_value_invalid" };
|
||||
@@ -732,6 +739,14 @@ export class SimSession {
|
||||
}
|
||||
}
|
||||
|
||||
checkpointLedger() {
|
||||
// Durable receipts must not recursively contain older checkpoints or
|
||||
// complete UI snapshots. Preserve intent/result identity for deduplication.
|
||||
const omitted = new Set(["panel_snapshot", "configuration", "program", "motion", "wasm_motion", "wasm_home_limit", "device_io", "devices", "hal_runtime", "snapshot", "state", "checkpoint", "recovery_checkpoint", "preview", "operations", "ledger"]);
|
||||
const entries = [...this.ledger.entries.values()].map((entry) => ({ ...entry, response: entry.response ? { ...entry.response, payload: Object.fromEntries(Object.entries(entry.response.payload ?? {}).filter(([key]) => !omitted.has(key))) } : null }));
|
||||
return { schema: LEDGER_SCHEMA, entries: structuredClone(entries) };
|
||||
}
|
||||
|
||||
stateForCheckpoint() {
|
||||
const state = {
|
||||
context_id: this.contextId,
|
||||
@@ -742,7 +757,7 @@ export class SimSession {
|
||||
devices: this.wasmDeviceProjection({ includeBytes: true }),
|
||||
motion: this.xyzacProgram ? this.wasmMotionProjection() : this.motion.export(),
|
||||
plc: this.plc.export(),
|
||||
hal_runtime: this.xyzacIo ? this.xyzacIo.halExport() : this.hal.export(),
|
||||
hal_runtime: this.xyzacIo ? this.xyzacIo.halExport({ includeState: true }) : this.hal.export(),
|
||||
device_io: this.xyzacDeviceIo ? this.xyzacDeviceIo.export() : this.xyzacIo ? this.xyzacIo.export() : this.deviceIo.export(),
|
||||
configuration_batch: this.configurationBatch.export(),
|
||||
io_ack: this.ioAck.export(),
|
||||
@@ -754,15 +769,19 @@ export class SimSession {
|
||||
wasm_motion: this.wasmMotion.exportState(),
|
||||
task_authority: this.task.authority(),
|
||||
wasm_home_limit: this.wasmHomeLimit.exportState(),
|
||||
...(this.xyzacKinematics ? { kinematics: this.xyzacKinematics.snapshot() } : {}),
|
||||
...(this.xyzacKinematics ? { kinematics: this.xyzacKinematics.snapshot(), machine_configuration_hash: this.machineConfigurationHash } : {}),
|
||||
preview: this.previewState,
|
||||
live_path_epoch: this.livePathEpoch,
|
||||
task: this.task.export(),
|
||||
scheduler: this.scheduler.export(),
|
||||
authoritative: this.xyzacProgram ? { schema: "wasm_authority_boundary.v1", source: "linuxcnc-wasm-worker" } : this.authoritative.export(),
|
||||
ledger: this.ledger.export(),
|
||||
ledger: this.xyzacProgram ? this.checkpointLedger() : this.ledger.export(),
|
||||
};
|
||||
if (this.xyzacProgram) state.program = { ...this.xyzacProgram.snapshot(), source_text: this.xyzacProgram.source };
|
||||
if (this.xyzacProgram) {
|
||||
state.program = { ...this.xyzacProgram.checkpoint(), source_text: this.xyzacProgram.source };
|
||||
state.trt_execution = Object.fromEntries(TRT_EXECUTION_KEYS.map((key) => [key, structuredClone(this[key])]));
|
||||
state.native_runtime = Object.fromEntries(["sim_core", "sim_protocol", "sim_event"].map((prefix) => [prefix, exportNativeCheckpoint(this.core, prefix)]));
|
||||
}
|
||||
else if (this.program.loaded) state.program = this.program.exportState();
|
||||
return state;
|
||||
}
|
||||
@@ -792,7 +811,7 @@ export class SimSession {
|
||||
task_authority: this.task.authority(),
|
||||
wasm_home_limit: this.wasmHomeLimit.exportState(),
|
||||
live_path_epoch: this.livePathEpoch,
|
||||
...(this.xyzacKinematics ? { kinematics: this.xyzacKinematics.snapshot() } : {}),
|
||||
...(this.xyzacKinematics ? { kinematics: this.xyzacKinematics.snapshot(), machine_configuration_hash: this.machineConfigurationHash } : {}),
|
||||
};
|
||||
if (this.xyzacProgram) value.program = { ...this.xyzacProgram.snapshot(), source_text: this.xyzacProgram.source };
|
||||
else if (this.program.loaded) value.program = this.program.exportState();
|
||||
@@ -824,6 +843,8 @@ export class SimSession {
|
||||
// recovery command, but are redundant for a power-off restart. Omitting
|
||||
// them keeps the browser durable record bounded during long JOG runs.
|
||||
delete state.ledger;
|
||||
state.power_off_preview_hash = state.preview?.preview_hash ?? null;
|
||||
delete state.preview;
|
||||
const compact = {
|
||||
schema: "xyzac-power-off-checkpoint.v1",
|
||||
context_id: this.contextId,
|
||||
@@ -834,10 +855,51 @@ export class SimSession {
|
||||
return { ...compact, checkpoint_hash: fnv1a64(compact) };
|
||||
}
|
||||
|
||||
validateRecoveryGeometry(state) {
|
||||
if (!this.xyzacKinematics) return;
|
||||
const current = this.xyzacKinematics.snapshot();
|
||||
restoreTrtExecution(state, transport = true) {
|
||||
if (!state.trt_execution || !state.native_runtime || !TRT_EXECUTION_KEYS.every((key) => Object.hasOwn(state.trt_execution, key))) throw new Error("trt_checkpoint_execution_required");
|
||||
for (const key of TRT_EXECUTION_KEYS) this[key] = structuredClone(state.trt_execution[key]);
|
||||
restoreNativeCheckpoint(this.core, "sim_core", state.native_runtime.sim_core);
|
||||
if (transport) for (const prefix of ["sim_protocol", "sim_event"]) restoreNativeCheckpoint(this.core, prefix, state.native_runtime[prefix]);
|
||||
this.xyzacIo.restoreHalState(state.hal_runtime);
|
||||
}
|
||||
|
||||
validateRecoveryGeometry(state, configuration = null) {
|
||||
if (!this.xyzacKinematics && !configuration) return;
|
||||
const supplied = configuration ? readTrtConfiguration(configuration) : null;
|
||||
const expectedHash = supplied ? fnv1a64({ kinematics: supplied.kinematics, home_limits: supplied.home_limits, motion_scale_min: WEB_SETTINGS.ui.motion_scale_min }) : this.machineConfigurationHash;
|
||||
if (state?.machine_configuration_hash !== expectedHash) throw new Error("trt_recovery_configuration_mismatch");
|
||||
const current = supplied?.kinematics ?? this.xyzacKinematics.snapshot();
|
||||
if (!state?.kinematics || state.kinematics.coordinates !== current.coordinates || TRT_GEOMETRY_KEYS.some((key) => Number(state.kinematics.offsets?.[key] ?? 0) !== current.offsets[key])) throw new Error("trt_recovery_geometry_mismatch");
|
||||
if (!state.trt_execution || !TRT_EXECUTION_KEYS.every((key) => Object.hasOwn(state.trt_execution, key))) throw new Error("trt_checkpoint_execution_required");
|
||||
for (const [prefix, value] of [["sim_program", state.program?.native_state], ["sim_linuxcnc_tp", state.wasm_motion?.native_state], ["sim_hal", state.hal_runtime?.native_state], ...["sim_core", "sim_protocol", "sim_event"].map((key) => [key, state.native_runtime?.[key]])]) validateNativeCheckpoint(this.core, prefix, value);
|
||||
}
|
||||
|
||||
restoreTrtProgram(state) {
|
||||
if (typeof state.program?.source_text !== "string") throw new Error("trt_checkpoint_program_missing");
|
||||
const previous = this.xyzacProgram;
|
||||
const candidate = new WasmProgramRuntime(this.core);
|
||||
try {
|
||||
if (state.program.source_text) {
|
||||
const loaded = candidate.load(state.program.source_text, state.program.path ?? null);
|
||||
if (loaded.status !== "verified") throw new Error(loaded.code);
|
||||
if (Array.isArray(state.program.subroutine_sources)) {
|
||||
for (const sub of state.program.subroutine_sources) {
|
||||
const added = candidate.addSubroutine(sub.name, sub.source, sub.path);
|
||||
if (added.status !== "verified") throw new Error(added.code);
|
||||
}
|
||||
} else {
|
||||
this.xyzacProgram = candidate;
|
||||
const subroutines = this.loadXyzacSubroutines(state.program.path ?? null);
|
||||
if (subroutines.status !== "verified") throw new Error(subroutines.code);
|
||||
}
|
||||
}
|
||||
const result = candidate.restoreState(state.program);
|
||||
if (result.status === "error") throw new Error(result.code ?? "trt_checkpoint_program_restore_failed");
|
||||
this.xyzacProgram = candidate;
|
||||
previous.destroy();
|
||||
} catch (error) {
|
||||
this.xyzacProgram = previous; candidate.destroy(); throw error;
|
||||
}
|
||||
}
|
||||
|
||||
restoreCompactPowerOff(record, transportSeq) {
|
||||
@@ -850,17 +912,13 @@ export class SimSession {
|
||||
const liveOperations = [...this.operations.values()];
|
||||
try {
|
||||
this.validateRecoveryGeometry(state);
|
||||
if (!this.xyzacProgram || !state.program?.source_text) throw new Error("power_off_program_missing");
|
||||
const loaded = this.xyzacProgram.load(state.program.source_text, state.program.path ?? null);
|
||||
if (loaded.status !== "verified") throw new Error(loaded.code);
|
||||
const subroutines = this.loadXyzacSubroutines(state.program.path ?? null);
|
||||
if (subroutines.status !== "verified") throw new Error(subroutines.code);
|
||||
if (this.xyzacProgram.restoreState(state.program).status === "error") throw new Error("power_off_program_restore_failed");
|
||||
this.restoreTrtProgram(state);
|
||||
if (state.wasm_motion) this.wasmMotion.restoreState(state.wasm_motion);
|
||||
if (state.wasm_home_limit) this.wasmHomeLimit.restoreState(state.wasm_home_limit);
|
||||
if (state.device_io && this.xyzacDeviceIo) this.xyzacDeviceIo.restoreState(state.device_io);
|
||||
if (state.devices && this.xyzacIo) this.xyzacIo.restoreState(state.devices);
|
||||
if (state.kinematics && this.xyzacKinematics) this.xyzacKinematics.restoreState(state.kinematics);
|
||||
this.restoreTrtExecution(state, false);
|
||||
if (state.task) {
|
||||
const task = { ...state.task, session_id: this.sessionId, seq: transportSeq, simulation_tick: record.simulation_tick };
|
||||
delete task.state_hash;
|
||||
@@ -875,7 +933,12 @@ export class SimSession {
|
||||
this.callbacks = new PythonCallbacks(); if (state.callbacks) this.callbacks.restore(state.callbacks);
|
||||
this.configurationState = state.configuration ?? this.configurationState;
|
||||
this.halState = state.hal ?? this.halState;
|
||||
this.previewState = state.preview ?? this.previewState;
|
||||
if (state.preview) this.previewState = state.preview;
|
||||
else if (state.program.source_text) {
|
||||
const preview = this.xyzacProgram.preview();
|
||||
if (preview.status !== "verified" || state.power_off_preview_hash && state.power_off_preview_hash !== preview.preview_hash) throw new Error("power_off_preview_restore_mismatch");
|
||||
this.previewState = preview;
|
||||
} else this.previewState = null;
|
||||
this.livePathEpoch = Number.isSafeInteger(state.live_path_epoch) ? state.live_path_epoch : this.livePathEpoch;
|
||||
this.python = new PythonBridge(); if (state.python) this.python.restoreState(state.python);
|
||||
const liveEvents = liveTimeline.events.events;
|
||||
@@ -897,6 +960,7 @@ export class SimSession {
|
||||
|
||||
restorePersistedPowerOff(checkpoint, transportSeq) {
|
||||
if (!checkpoint || !Number.isSafeInteger(transportSeq) || transportSeq < 1) return { status: "blocked", code: "runtime_checkpoint_input_invalid", reason: "checkpoint_or_sequence_invalid" };
|
||||
if (Number.isSafeInteger(checkpoint.simulation_tick) && checkpoint.simulation_tick < this.timeline.events.tick) return { status: "blocked", code: "runtime_checkpoint_requires_new_session", reason: "recovery_must_not_rewrite_transport_history" };
|
||||
if (checkpoint.schema === "xyzac-power-off-checkpoint.v1") return this.restoreCompactPowerOff(checkpoint, transportSeq);
|
||||
const liveTimeline = this.timeline.export();
|
||||
const liveLedger = this.ledger;
|
||||
@@ -914,7 +978,7 @@ export class SimSession {
|
||||
const unsigned = Object.fromEntries(Object.entries(checkpointForRestore).filter(([key]) => key !== "checkpoint_hash"));
|
||||
checkpointForRestore.checkpoint_hash = fnv1a64(unsigned);
|
||||
}
|
||||
const restored = this.restoreCheckpoint(checkpointForRestore);
|
||||
const restored = this.restoreCheckpoint(checkpointForRestore, null, undefined, false);
|
||||
if (restored.status !== "recovered") return restored;
|
||||
const restoredTick = this.tick;
|
||||
const restoredTask = this.task.export();
|
||||
@@ -953,6 +1017,12 @@ export class SimSession {
|
||||
importTrace(trace) {
|
||||
const validated = validateTrace(trace, { contextId: this.contextId });
|
||||
this.validateRecoveryGeometry(trace.state);
|
||||
if (this.xyzacProgram && trace.state?.program?.native_state) {
|
||||
const checkpoint = createCheckpoint({ contextId: this.contextId, timeline: validated.timeline, state: trace.state, operations: trace.state.ledger?.entries ?? [] });
|
||||
const result = this.restoreCheckpoint(checkpoint);
|
||||
if (result.status !== "recovered") throw new Error(result.blocked?.[0]?.reason ?? "trt_trace_restore_failed");
|
||||
return { status: "verified", trace_hash: trace.trace_hash, state_hash: fnv1a64(trace.state), seq: trace.seq, tick: trace.tick };
|
||||
}
|
||||
this.timeline = validated.timeline;
|
||||
this.created = Boolean(trace.state?.created);
|
||||
if (trace.state?.ledger?.entries) this.ledger = new OperationLedger(trace.state.ledger.entries);
|
||||
@@ -986,27 +1056,22 @@ export class SimSession {
|
||||
return { status: "verified", trace_hash: trace.trace_hash, state_hash: fnv1a64(trace.state), seq: trace.seq, tick: trace.tick };
|
||||
}
|
||||
|
||||
restoreCheckpoint(checkpoint, events = null, reconcile = undefined) {
|
||||
restoreCheckpoint(checkpoint, events = null, reconcile = undefined, transport = true) {
|
||||
try {
|
||||
validateCheckpoint(checkpoint, { contextId: this.contextId });
|
||||
this.validateRecoveryGeometry(checkpoint.state);
|
||||
} catch (error) {
|
||||
return { schema: "simulation-recovery.v1", status: "blocked", context_id: this.contextId, checkpoint_hash: checkpoint?.checkpoint_hash ?? null, blocked: [{ reason: String(error?.message ?? error) }] };
|
||||
}
|
||||
const tailEvents = Array.isArray(events) ? events : events?.events?.events ?? events?.events ?? [];
|
||||
if (this.xyzacProgram && Array.isArray(tailEvents) && tailEvents.some((event) => event.seq > checkpoint.event_cursor)) return { schema: "simulation-recovery.v1", status: "blocked", context_id: this.contextId, blocked: [{ reason: "trt_checkpoint_tail_requires_explicit_commands" }] };
|
||||
const replayMotionAuthority = checkpoint?.state?.motion_authority ?? this.motionAuthority;
|
||||
if (replayMotionAuthority === "wasm" && !checkpoint?.state?.wasm_motion) {
|
||||
return { schema: "simulation-recovery.v1", status: "blocked", context_id: this.contextId, checkpoint_hash: checkpoint?.checkpoint_hash ?? null, blocked: [{ reason: "wasm_motion_checkpoint_missing" }] };
|
||||
}
|
||||
if (this.xyzacProgram && checkpoint?.state?.program?.source_text) {
|
||||
if (this.xyzacProgram && checkpoint?.state?.program) {
|
||||
try {
|
||||
const loaded = this.xyzacProgram.load(checkpoint.state.program.source_text, checkpoint.state.program.path ?? null);
|
||||
if (loaded.status !== "verified") throw new Error(loaded.code);
|
||||
const subroutines = this.loadXyzacSubroutines(checkpoint.state.program.path ?? null);
|
||||
if (subroutines.status !== "verified") throw new Error(subroutines.code);
|
||||
const targetLine = Number.isSafeInteger(checkpoint.state.program.current_line) ? checkpoint.state.program.current_line : 0;
|
||||
const seek = this.xyzacProgram.seek(targetLine);
|
||||
if (seek.status !== "verified") throw new Error(seek.code);
|
||||
if (checkpoint.state.program) this.xyzacProgram.restoreState(checkpoint.state.program);
|
||||
this.restoreTrtProgram(checkpoint.state);
|
||||
if (checkpoint.state.wasm_motion) this.wasmMotion.restoreState(checkpoint.state.wasm_motion);
|
||||
if (checkpoint.state.wasm_home_limit) this.wasmHomeLimit.restoreState(checkpoint.state.wasm_home_limit);
|
||||
let restoredWasmDeviceIo = false;
|
||||
@@ -1028,11 +1093,11 @@ export class SimSession {
|
||||
this.livePathEpoch = Number.isSafeInteger(checkpoint.state.live_path_epoch) ? checkpoint.state.live_path_epoch : this.livePathEpoch;
|
||||
this.python = new PythonBridge(); if (checkpoint.state.python) this.python.restoreState(checkpoint.state.python);
|
||||
this.timeline = new ReplayTimeline(checkpoint.timeline);
|
||||
if (checkpoint.state.hal_runtime?.schema === "wasm_xyzac_hal.v1" && this.xyzacIo) this.xyzacIo.halCommand({ action: "step", ticks: checkpoint.state.hal_runtime.tick ?? 0 });
|
||||
this.restoreTrtExecution(checkpoint.state, transport);
|
||||
if (!restoredWasmDeviceIo && checkpoint.state.device_io?.schema === "wasm_xyzac_device_io.v1" && this.xyzacDeviceIo) this.xyzacDeviceIo.command("IO_STEP", { ticks: checkpoint.state.device_io.state?.tick ?? 0 });
|
||||
const currentOperations = [...this.operations.values()];
|
||||
this.operations = new Map([...checkpoint.operations ?? [], ...currentOperations].map((operation) => [operation.operation_id, operation]));
|
||||
const checkpointEntries = checkpoint.operations ?? [];
|
||||
const checkpointEntries = checkpoint.state.ledger?.entries ?? checkpoint.operations ?? [];
|
||||
const currentEntries = this.ledger.export().entries.filter((entry) => !checkpointEntries.some((operation) => operation.operation_id === entry.operation_id));
|
||||
this.ledger = new this.ledger.constructor();
|
||||
for (const operation of [...checkpointEntries, ...currentEntries]) {
|
||||
@@ -1317,6 +1382,7 @@ export class SimSession {
|
||||
let ledgerPreparation;
|
||||
try {
|
||||
ledgerPreparation = this.ledger.prepare(input);
|
||||
if (ledgerPreparation.replay && !ledgerPreparation.entry?.response) throw new Error("ledger_replay_result_missing");
|
||||
if (ledgerPreparation.replay && ledgerPreparation.entry?.response) {
|
||||
const replayTick = Number.isSafeInteger(input.simulation_tick) && input.simulation_tick >= this.tick ? input.simulation_tick : this.tick;
|
||||
this.timeline.appendEvent(input.seq, replayTick, "OPERATION_REPLAY", { operation_id: input.operation_id, original_seq: ledgerPreparation.entry.seq });
|
||||
@@ -1324,7 +1390,9 @@ export class SimSession {
|
||||
if (replayEventResult !== 0) return this.error(input, "event_log", "core_event_append_failed");
|
||||
this.tick = replayTick;
|
||||
try { this.task.setCommand(input.seq, replayTick); } catch (error) { return this.error(input, "task_state", String(error?.message ?? error)); }
|
||||
return { ...ledgerPreparation.entry.response, replayed: true, seq: input.seq, ledger: this.ledger.reconcile(input.operation_id) };
|
||||
const cached = ledgerPreparation.entry.response;
|
||||
const payload = this.xyzacProgram ? { ...cached.payload, task: this.task.export(), program: this.xyzacProgram.snapshot(), motion: this.wasmMotionProjection(), kinematics: this.xyzacKinematics.snapshot() } : cached.payload;
|
||||
return { ...this.envelope(cached.kind, input, payload), replayed: true, ledger: this.ledger.reconcile(input.operation_id) };
|
||||
}
|
||||
if (ledgerPreparation.entry) this.ledger.execute(input.operation_id);
|
||||
} catch (error) {
|
||||
@@ -1610,8 +1678,7 @@ export class SimSession {
|
||||
let configuration = input.payload?.configuration ?? null;
|
||||
if (machineDefinition(configuration?.entrypoint) || machineDefinition(configuration?.kinematics?.type)) {
|
||||
try {
|
||||
const geometry = readTrtGeometry(configuration);
|
||||
configuration = { ...configuration, kinematics: { ...configuration.kinematics, offsets: geometry.offsets } };
|
||||
configuration = { ...configuration, ...readTrtConfiguration(configuration) };
|
||||
} catch (error) {
|
||||
return finish(this.error(input, "trt_geometry", error.message), "failed");
|
||||
}
|
||||
@@ -1619,7 +1686,7 @@ export class SimSession {
|
||||
if (configuration) {
|
||||
this.machineDefinition = machineDefinition(configuration.entrypoint) ?? machineDefinition(configuration.kinematics?.type) ?? this.machineDefinition;
|
||||
this.workspaceFiles = { ...(configuration.files ?? configuration.workspace ?? configuration.workspace_files ?? {}) };
|
||||
const visualSourcePath = (this.machineDefinition ?? machineDefinition(configuration.entrypoint))?.source_path ?? "linuxcnc/src/hal/user_comps/vismach/xyzac-trt-gui.py";
|
||||
const visualSourcePath = (this.machineDefinition ?? machineDefinition(configuration.entrypoint))?.source_path ?? DEFAULT_MACHINE.source_path;
|
||||
this.visualModelSourceHash = typeof this.workspaceFiles[visualSourcePath] === "string" ? fnv1a64(this.workspaceFiles[visualSourcePath]) : null;
|
||||
const startupWorkspace = configuration.workspace ?? configuration.files ?? configuration.workspace_files;
|
||||
if (startupWorkspace) {
|
||||
@@ -1672,22 +1739,23 @@ export class SimSession {
|
||||
this.xyzacKinematics?.destroy();
|
||||
this.xyzacKinematics = new WasmXyzacKinematics(this.core, { ...configuration.kinematics.offsets, coordinates: configuredMachine?.coordinates ?? configuration.kinematics.coordinates ?? "XYZAC", limits: configuration.home_limits });
|
||||
this.xyzacIo?.destroy();
|
||||
this.xyzacIo = new WasmXyzacIo(this.core, { spindleCount: 2, toolCapacity: 1001 });
|
||||
this.xyzacIo = new WasmXyzacIo(this.core, { spindleCount: configuration.kinematics.io.spindle_count, toolCapacity: configuration.kinematics.io.tool_capacity, cycleTime: configuration.kinematics.io.cycle_time, spindleAcceleration: configuration.kinematics.io.spindle_acceleration, spindleTolerance: configuration.kinematics.io.spindle_tolerance });
|
||||
const initialPower = this.xyzacIo.command("POWER", { active: false });
|
||||
if (initialPower.status !== "verified") throw new Error(initialPower.code ?? "wasm_power_off_init");
|
||||
this.xyzacDeviceIo?.destroy();
|
||||
this.xyzacDeviceIo = new WasmXyzacDeviceIo(this.core, { encoderCount: 2 });
|
||||
this.xyzacDeviceIo = new WasmXyzacDeviceIo(this.core, { encoderCount: configuration.kinematics.io.encoder_count, cycleTime: configuration.kinematics.io.cycle_time });
|
||||
this.xyzacProgram?.destroy();
|
||||
this.wasmMotion.destroy();
|
||||
this.xyzacMotionCycleTime = Number(configuration.kinematics?.servo_cycle_time ?? configuration.kinematics?.cycle_time ?? 0.01);
|
||||
this.xyzacMotionCycleTime = configuration.kinematics.servo_cycle_time;
|
||||
if (!Number.isFinite(this.xyzacMotionCycleTime) || this.xyzacMotionCycleTime <= 0) throw new Error("wasm_xyzac_cycle_time_invalid");
|
||||
const taskCycleTime = Number(configuration.kinematics?.task_cycle_time ?? this.xyzacMotionCycleTime);
|
||||
this.xyzacTaskCycleTicks = Math.max(1, Math.round(taskCycleTime / this.xyzacMotionCycleTime));
|
||||
const taskCycleTime = configuration.kinematics.task_cycle_time;
|
||||
this.xyzacTaskCycleTicks = Math.round(taskCycleTime / this.xyzacMotionCycleTime);
|
||||
this.xyzacRapidVelocity = configuration.kinematics.max_linear_velocity;
|
||||
// The selected XYZAC TRT case always uses the upstream LinuxCNC TP;
|
||||
// a caller must not silently downgrade it to the compatibility
|
||||
// motion queue by omitting the planner option.
|
||||
this.wasmMotion = isTrt
|
||||
? new WasmLinuxcncTp(this.core, { cycleTime: this.xyzacMotionCycleTime, velocityLimit: this.xyzacRapidVelocity, accelerationLimit: Number(configuration.kinematics?.max_acceleration ?? 300), limits: configuration.home_limits ?? {} })
|
||||
? new WasmLinuxcncTp(this.core, { cycleTime: this.xyzacMotionCycleTime, velocityLimit: this.xyzacRapidVelocity, accelerationLimit: configuration.kinematics.max_acceleration, limits: configuration.kinematics.axis_limits, axisVelocities: configuration.kinematics.axis_velocity_limits, axisAccelerations: configuration.kinematics.axis_acceleration_limits, maxFeedOverride: configuration.kinematics.max_feed_override, blend: configuration.kinematics.blend })
|
||||
: new WasmMotionQueue(this.core, { cycleTime: this.xyzacMotionCycleTime, velocityLimit: 1e9 });
|
||||
this.xyzacProgram = new WasmProgramRuntime(this.core);
|
||||
this.previewState = null;
|
||||
@@ -1696,14 +1764,8 @@ export class SimSession {
|
||||
this.homingHistory = [];
|
||||
this.xyzacProgramKinematicsMode = XYZAC_MODES.identity;
|
||||
if (typeof this.wasmMotion.setKinematics === "function") this.wasmMotion.setKinematics(this.xyzacKinematics);
|
||||
this.xyzacRapidVelocity = Number(configuration.kinematics?.max_linear_velocity ?? 35);
|
||||
const linearLimit = Number(configuration.kinematics?.axis_linear_velocity_limit ?? 20);
|
||||
const angularLimit = Number(configuration.kinematics?.axis_angular_velocity_limit ?? 30);
|
||||
const rotaryAxis = (configuredMachine?.coordinates ?? "XYZAC")[3].toLowerCase();
|
||||
this.xyzacAxisVelocityLimits = { x: linearLimit, y: linearLimit, z: linearLimit, [rotaryAxis]: angularLimit, c: angularLimit };
|
||||
const accelerationLimit = Number(configuration.kinematics?.max_acceleration ?? 300);
|
||||
this.xyzacAxisAccelerationLimits = { x: accelerationLimit, y: accelerationLimit, z: accelerationLimit, [rotaryAxis]: accelerationLimit, c: accelerationLimit };
|
||||
this.wasmMotion.setDynamics(Number(configuration.kinematics?.max_acceleration ?? 300), Number(configuration.kinematics?.max_jerk ?? 1000000000));
|
||||
this.xyzacAxisVelocityLimits = configuration.kinematics.axis_velocity_limits;
|
||||
this.xyzacAxisAccelerationLimits = configuration.kinematics.axis_acceleration_limits;
|
||||
this.xyzacProgram.setRapidVelocity(this.xyzacRapidVelocity);
|
||||
}
|
||||
if (configuration?.home_limits && this.motionAuthority === "wasm") {
|
||||
@@ -1734,6 +1796,7 @@ export class SimSession {
|
||||
};
|
||||
}
|
||||
const configuredKinematics = input.payload?.kinematics ?? configuration?.kinematics?.type ?? null;
|
||||
if (isTrt) this.machineConfigurationHash = fnv1a64({ kinematics: configuration.kinematics, home_limits: configuration.home_limits, motion_scale_min: WEB_SETTINGS.ui.motion_scale_min });
|
||||
if (configuredKinematics && !(this.motionAuthority === "wasm" && isTrt)) {
|
||||
const kinematics = this.authoritative.setKinematics(configuredKinematics);
|
||||
if (kinematics.status === "blocked") return finish(this.envelope("ERROR", input, { status: "blocked", code: kinematics.code, kinematics: this.authoritative.export() }), "failed");
|
||||
@@ -1757,7 +1820,7 @@ export class SimSession {
|
||||
motion_authority: this.motionAuthority,
|
||||
state_hash: this.task.hash(),
|
||||
task: this.task.export(),
|
||||
...(powerOffRecovery ? { power_off_recovery: powerOffRecovery } : {}),
|
||||
...(powerOffRecovery ? { power_off_recovery: { ...powerOffRecovery, program: { source: this.xyzacProgram.source, path: this.xyzacProgram.path } } } : {}),
|
||||
...(this.configurationState?.schema === "configuration-startup-runtime.v1" ? { configuration: this.configurationState } : {}),
|
||||
})) : finish(this.coreError(input, loadResult, "sim_load_workspace"), "failed");
|
||||
}
|
||||
@@ -2081,7 +2144,7 @@ export class SimSession {
|
||||
motion_trace.push({
|
||||
tick: nextTick,
|
||||
pose: { ...traceState.position },
|
||||
joints: traceKinematics?.joints?.length === 5 ? [...traceKinematics.joints] : [],
|
||||
joints: Array.isArray(traceState.joint_commanded) ? [...traceState.joint_commanded] : traceKinematics?.joints?.length === 5 ? [...traceKinematics.joints] : [],
|
||||
active_segment_id: traceState.active_id,
|
||||
active_progress: traceState.progress,
|
||||
});
|
||||
@@ -2618,7 +2681,7 @@ export class SimSession {
|
||||
}
|
||||
if (input.kind === "SET_OVERRIDE") {
|
||||
const feed = Number(input.payload?.feed ?? input.payload?.feed_override);
|
||||
if (!Number.isFinite(feed) || feed < 0 || feed > 2) return finish(this.error(input, "motion_override", "motion_override_range"), "failed");
|
||||
if (!Number.isFinite(feed) || feed < 0 || feed > (this.wasmMotion.maxFeedOverride ?? WEB_SETTINGS.runtime.planner.max_feed_override)) return finish(this.error(input, "motion_override", "motion_override_range"), "failed");
|
||||
try {
|
||||
this.wasmMotion.setFeedOverride(feed);
|
||||
} catch (error) {
|
||||
@@ -2629,7 +2692,7 @@ export class SimSession {
|
||||
}
|
||||
if (input.kind === "SET_JOG_SCALE" || input.kind === "SET_MAX_VELOCITY") {
|
||||
const scale = Number(input.payload?.scale ?? Number(input.payload?.percent) / 100);
|
||||
if (!Number.isFinite(scale) || scale < 0.06 || scale > 1) return finish(this.error(input, "motion_scale", input.kind === "SET_JOG_SCALE" ? "jog_scale_range" : "max_velocity_scale_range"), "failed");
|
||||
if (!Number.isFinite(scale) || scale < WEB_SETTINGS.ui.motion_scale_min || scale > 1) return finish(this.error(input, "motion_scale", input.kind === "SET_JOG_SCALE" ? "jog_scale_range" : "max_velocity_scale_range"), "failed");
|
||||
try {
|
||||
if (input.kind === "SET_JOG_SCALE") this.wasmMotion.setJogScale(scale);
|
||||
else this.wasmMotion.setVelocityScale(scale);
|
||||
@@ -2641,7 +2704,7 @@ export class SimSession {
|
||||
}
|
||||
if (input.kind === "FOLLOWING_ERROR") return finish(this.error(input, "motion_following_error", "wasm_following_error_unavailable"), "failed");
|
||||
const axis = String(input.payload?.axis ?? "").toLowerCase();
|
||||
const availableAxes = this.machineDefinition?.axes ?? ["x", "y", "z", "a", "c"];
|
||||
const availableAxes = this.machineDefinition?.axes ?? DEFAULT_MACHINE.axes;
|
||||
if (!availableAxes.includes(axis)) return finish(this.error(input, "motion_axis", "motion_axis_invalid"), "failed");
|
||||
const direction = input.payload?.direction === -1 ? -1 : input.payload?.direction === 1 ? 1 : null;
|
||||
if (direction === null) return finish(this.error(input, "motion_jog", "motion_jog_range"), "failed");
|
||||
@@ -2790,7 +2853,13 @@ export class SimSession {
|
||||
}
|
||||
}
|
||||
case "RECOVER": {
|
||||
const result = this.restoreCheckpoint(input.payload?.checkpoint, input.payload?.events, input.payload?.reconcile);
|
||||
const recoveryEvents = input.payload?.events;
|
||||
const tailEvents = Array.isArray(recoveryEvents) ? recoveryEvents : recoveryEvents?.events?.events ?? recoveryEvents?.events ?? [];
|
||||
if (this.xyzacProgram && Array.isArray(tailEvents) && tailEvents.some((event) => event.seq > (input.payload?.checkpoint?.event_cursor ?? 0))) return finish(this.envelope("RECOVERY_STATUS", input, { status: "blocked", code: "trt_checkpoint_tail_requires_explicit_commands" }), "failed");
|
||||
let result = this.xyzacProgram
|
||||
? this.restorePersistedPowerOff(input.payload?.checkpoint, input.seq)
|
||||
: this.restoreCheckpoint(input.payload?.checkpoint, input.payload?.events, input.payload?.reconcile);
|
||||
if (this.xyzacProgram && result.status === "verified") result = { ...result, schema: "simulation-recovery.v1", status: "recovered", storage: "checkpoint", state: this.stateForCheckpoint() };
|
||||
return finish(this.envelope("RECOVERY_STATUS", input, result), result.status === "blocked" ? "failed" : "verified");
|
||||
}
|
||||
case "CANCEL": {
|
||||
|
||||
@@ -39,9 +39,10 @@ const wasm = await createModule({ wasmBinary: await readFile(new URL("../../buil
|
||||
const session = new SimSession(wasm, { sessionId: "axis3-selected-case" });
|
||||
const base = { schema_version: 1, context_id: CONTEXT_ID, session_id: session.sessionId };
|
||||
let sequence = 0;
|
||||
const send = (kind, operationId, payload = {}) => {
|
||||
const send = (kind, operationId, payload = {}, expected = "ok") => {
|
||||
const response = session.accept({ ...base, kind, seq: ++sequence, operation_id: operationId, payload });
|
||||
assert.notEqual(response.kind, "ERROR", `${kind}:${response.payload?.code ?? "error"}`);
|
||||
if (expected === "error") assert.equal(response.kind, "ERROR");
|
||||
else assert.notEqual(response.kind, "ERROR", `${kind}:${response.payload?.code ?? "error"}`);
|
||||
return response;
|
||||
};
|
||||
|
||||
@@ -67,6 +68,12 @@ const checkpoint = send("CHECKPOINT", "checkpoint").payload.checkpoint;
|
||||
send("SIM_STEP", "program-step", { cycles: 8 });
|
||||
const programSnapshot = send("SNAPSHOT_REQUEST", "program-snapshot");
|
||||
assert.ok(["running", "completed"].includes(programSnapshot.payload.snapshot?.state?.program?.program_status ?? "running"));
|
||||
assert.equal(send("SPINDLE_COMMAND", "spindle-during-motion", { spindle: 0, rpm: 600, enabled: true }, "error").payload.code, "axis_gate");
|
||||
assert.equal(send("TOOL_PREPARE", "tool-during-motion", { tool: 1, pocket: 1 }, "error").payload.code, "axis_gate");
|
||||
for (let tick = 0; tick < 200 && (session.program.snapshot().program_status !== "completed" || session.wasmMotion.read().state.queue_depth > 0); tick++) send("SIM_STEP", `finish-program-${tick}`, { cycles: 100 });
|
||||
assert.equal(session.program.snapshot().program_status, "completed");
|
||||
assert.equal(session.wasmMotion.read().state.queue_depth, 0);
|
||||
send("SET_MODE", "manual-after-program", { mode: "MANUAL" });
|
||||
|
||||
assert.equal(send("SPINDLE_COMMAND", "spindle-on", { spindle: 0, rpm: 600, enabled: true }).payload.status, "verified");
|
||||
assert.equal(send("IO_STEP", "spindle-step", { ticks: 5 }).payload.status, "verified");
|
||||
|
||||
@@ -46,6 +46,20 @@ await handleWorkerMessage(session, { ...base, kind: "LOAD_WORKSPACE", seq: 2, op
|
||||
assert.deepEqual(lastAccepted.payload.recovery_checkpoint, checkpoint);
|
||||
assert.equal(accepted, 2);
|
||||
assert.equal((await loadUiState({ contextId, key: "xyzac-trt-power-off" })).status, "verified");
|
||||
session.machineDefinition = { id: "xyzbc-trt" };
|
||||
const bcCheckpoint = { ...checkpoint, state_hash: "bc-test" };
|
||||
await handleWorkerMessage(session, { ...base, kind: "POWER", seq: 3, operation_id: "bc-off", payload: { active: false, checkpoint: bcCheckpoint } });
|
||||
assert.equal((await loadUiState({ contextId, key: "xyzbc-trt-power-off" })).state.checkpoint.state_hash, "bc-test");
|
||||
assert.equal((await loadUiState({ contextId, key: "xyzac-trt-power-off" })).state.checkpoint.state_hash, "test");
|
||||
await handleWorkerMessage(session, { ...base, kind: "LOAD_WORKSPACE", seq: 4, operation_id: "reload", payload: { root_path: "/workspace" } });
|
||||
assert.equal(lastAccepted.payload.recovery_checkpoint, undefined, "active-session configuration reload must not inject old cached state");
|
||||
delete session.machineDefinition;
|
||||
session.core = {};
|
||||
session.validateRecoveryGeometry = () => { throw new Error("sim_program_checkpoint_build_mismatch"); };
|
||||
const incompatible = await handleWorkerMessage(session, { ...base, kind: "LOAD_WORKSPACE", seq: 5, operation_id: "incompatible", payload: { root_path: "/workspace", configuration: { entrypoint: "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini" } } });
|
||||
assert.equal(lastAccepted.payload.recovery_checkpoint, undefined);
|
||||
assert.equal(incompatible.payload.recovery_persistence.status, "ignored");
|
||||
assert.equal(incompatible.payload.recovery_persistence.code, "sim_program_checkpoint_build_mismatch");
|
||||
|
||||
if (previousNavigator === undefined) delete globalThis.navigator;
|
||||
else Object.defineProperty(globalThis, "navigator", { configurable: true, value: previousNavigator });
|
||||
|
||||
142
web/worker/test_trt_checkpoint_exact.mjs
Normal file
@@ -0,0 +1,142 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile, mkdir, writeFile } from "node:fs/promises";
|
||||
import createModule from "../../build-wasm/linuxcnc-sim-core.js";
|
||||
import { SimSession, CONTEXT_ID } from "./sim-session.mjs";
|
||||
import { loadSimulationCase, toWorkerConfiguration } from "../workspace/sim-case-loader.mjs";
|
||||
import { MACHINE_DEFINITIONS } from "../workspace/machine-definitions.mjs";
|
||||
import { fnv1a64 } from "./replay.mjs";
|
||||
|
||||
const root = new URL("../../", import.meta.url);
|
||||
const wasmBinary = await readFile(new URL("build-wasm/linuxcnc-sim-core.wasm", root));
|
||||
const fetchImpl = async (url) => {
|
||||
try { const text = await readFile(new URL(String(url).replace(/^\//, ""), root), "utf8"); return { ok: true, text: async () => text }; }
|
||||
catch { return { ok: false }; }
|
||||
};
|
||||
async function open(machine) {
|
||||
const bundle = await loadSimulationCase({ path: machine.entrypoint, fetchImpl });
|
||||
const subroutine = machine.subroutines[0];
|
||||
const subPath = machine.entrypoint.replace(/[^/]+$/, `remap_subs/${subroutine}.ngc`);
|
||||
bundle.files[subPath] = `O<${subroutine}> sub\n#31=[#31+#1]\nG1 X#31 Y4 Z2 ${machine.coordinates[3]}8 C12 F500\nO<${subroutine}> endsub\n`;
|
||||
const core = await createModule({ wasmBinary });
|
||||
// Give each module a different heap layout; native restore must rebind pointers.
|
||||
core._malloc(4096 + rows.length * 128);
|
||||
const session = new SimSession(core, { sessionId: `exact-${machine.id}`, planner: "linuxcnc-tp" });
|
||||
let seq = 0;
|
||||
const send = (kind, payload = {}, operationId = null) => {
|
||||
const result = session.accept({ schema_version: 1, context_id: CONTEXT_ID, session_id: session.sessionId, kind, seq: ++seq, operation_id: operationId ?? `exact-${seq}`, payload });
|
||||
assert.notEqual(result.kind, "ERROR", `${kind}: ${result.payload?.message_key ?? result.payload?.code ?? JSON.stringify({ preview: result.payload?.preview?.code, line: result.payload?.preview?.line, program_error: result.payload?.program?.last_error })}`);
|
||||
return result;
|
||||
};
|
||||
send("HELLO"); send("SESSION_ATTACH"); send("LOAD_WORKSPACE", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) });
|
||||
send("POWER", { active: true }); send("HOME", { all: true });
|
||||
for (let i = 0; i < 500 && !session.wasmHomeLimit.read().state.homed; i++) send("SIM_STEP", { cycles: 1 });
|
||||
assert.ok(session.wasmHomeLimit.read().state.homed);
|
||||
return { session, core, send, adoptSequence: (value) => { seq = value; } };
|
||||
}
|
||||
const rows = [];
|
||||
for (const machine of MACHINE_DEFINITIONS) {
|
||||
const original = await open(machine);
|
||||
const idle = original.send("CHECKPOINT").payload.checkpoint;
|
||||
const idleRestored = await open(machine);
|
||||
assert.equal(idleRestored.session.restoreCheckpoint(idle).status, "recovered");
|
||||
assert.equal(fnv1a64(idleRestored.session.workspacePackageState()), fnv1a64(original.session.workspacePackageState()), "idle machine without a loaded program");
|
||||
const rotary = machine.coordinates[3];
|
||||
const source = `G21 G90 G64\nM428\n#31=3\nG1 X6 Y0 Z2 ${rotary}5 C8 F600\nG3 X0 Y6 I-6 J0 ${rotary}10 C16\nO<${machine.subroutines[0]}> call [3]\nM429\nG1 X2 Y3 Z4 F600\nM2\n`;
|
||||
original.send("PROGRAM_LOAD", { source, path: machine.program.replace(/[^/]+$/, "checkpoint-exact.ngc") }); original.send("PROGRAM_RUN");
|
||||
const seen = new Set(); let samples = 0;
|
||||
for (let index = 0; index < 500; index++) {
|
||||
original.send("SIM_STEP", { cycles: 5, response: "motion_delta" });
|
||||
const current = original.session.wasmMotion.read().state;
|
||||
const program = original.session.xyzacProgram.snapshot();
|
||||
const phase = program.call_depth > 0 ? "subroutine" : current.motion_type === 3 ? "arc" : current.queue_depth > 0 ? "line" : program.program_status === "completed" ? "completed" : "barrier";
|
||||
if (!seen.has(phase)) {
|
||||
seen.add(phase);
|
||||
const checkpoint = original.send("CHECKPOINT").payload.checkpoint;
|
||||
assert.ok(checkpoint?.state.program.native_state);
|
||||
assert.ok(!JSON.stringify(checkpoint.state.ledger).includes('"checkpoint":'), "durable ledger must not recursively contain earlier checkpoints");
|
||||
const restarted = await open(machine);
|
||||
restarted.session.workspaceFiles[machine.entrypoint.replace(/[^/]+$/, `remap_subs/${machine.subroutines[0]}.ngc`)] = "unrelated new subroutine contents";
|
||||
const result = restarted.session.restoreCheckpoint(checkpoint);
|
||||
assert.equal(result.status, "recovered", JSON.stringify(result.blocked));
|
||||
restarted.adoptSequence(checkpoint.event_cursor);
|
||||
assert.equal(fnv1a64(restarted.session.workspacePackageState()), fnv1a64(original.session.workspacePackageState()), `${machine.id} ${phase}: initial state`);
|
||||
for (let step = 0; step < 12; step++) {
|
||||
original.send("SIM_STEP", { cycles: 1, response: "motion_delta" }); restarted.send("SIM_STEP", { cycles: 1, response: "motion_delta" });
|
||||
const expected = original.session.workspacePackageState(), actual = restarted.session.workspacePackageState();
|
||||
assert.equal(fnv1a64(actual), fnv1a64(expected), `${machine.id} ${phase}: divergent tick ${original.session.tick}`);
|
||||
samples++;
|
||||
}
|
||||
rows.push({ machine: machine.id, phase, checkpoint_tick: checkpoint.simulation_tick, continuation_tick: original.session.tick, state_hash: fnv1a64(original.session.workspacePackageState()), samples: 12, first_divergent_tick: null });
|
||||
if (phase === "subroutine") {
|
||||
original.send("PROGRAM_PAUSE");
|
||||
const paused = original.send("CHECKPOINT").payload.checkpoint;
|
||||
const resumed = await open(machine);
|
||||
assert.equal(resumed.session.restoreCheckpoint(paused).status, "recovered");
|
||||
resumed.adoptSequence(paused.event_cursor);
|
||||
const transported = await open(machine);
|
||||
assert.equal(transported.send("RECOVER", { checkpoint: paused }).payload.status, "recovered");
|
||||
original.send("SIM_STEP", { cycles: 2 }); resumed.send("SIM_STEP", { cycles: 2 });
|
||||
assert.equal(fnv1a64(resumed.session.workspacePackageState()), fnv1a64(original.session.workspacePackageState()), "paused state must stay unchanged");
|
||||
original.send("PROGRAM_RESUME"); resumed.send("PROGRAM_RESUME"); transported.send("PROGRAM_RESUME");
|
||||
for (let step = 0; step < 12; step++) {
|
||||
original.send("SIM_STEP", { cycles: 1, response: "motion_delta" }); resumed.send("SIM_STEP", { cycles: 1, response: "motion_delta" });
|
||||
transported.send("SIM_STEP", { cycles: 1, response: "motion_delta" });
|
||||
assert.equal(fnv1a64(resumed.session.workspacePackageState()), fnv1a64(original.session.workspacePackageState()), `paused resume divergent tick ${original.session.tick}`);
|
||||
assert.deepEqual(transported.session.wasmMotion.read(), original.session.wasmMotion.read(), "new transport must continue the same physical trajectory");
|
||||
}
|
||||
rows.push({ machine: machine.id, phase: "paused-resumed", checkpoint_tick: paused.simulation_tick, continuation_tick: original.session.tick, state_hash: fnv1a64(original.session.workspacePackageState()), samples: 12, new_transport_recovery: true, first_divergent_tick: null });
|
||||
}
|
||||
}
|
||||
if (original.session.xyzacProgram.snapshot().program_status === "completed" && original.session.wasmMotion.read().state.queue_depth === 0) break;
|
||||
}
|
||||
assert.equal(original.session.xyzacProgram.snapshot().program_status, "completed");
|
||||
assert.ok(seen.has("line") && seen.has("arc"), [...seen].join(","));
|
||||
assert.ok(samples >= 24);
|
||||
const trace = original.session.exportTrace();
|
||||
const imported = await open(machine);
|
||||
assert.equal(imported.session.importTrace(trace).status, "verified");
|
||||
assert.equal(fnv1a64(imported.session.workspacePackageState()), fnv1a64(original.session.workspacePackageState()), "native trace import");
|
||||
const checkpoint = original.send("CHECKPOINT").payload.checkpoint;
|
||||
const invalid = structuredClone(checkpoint);
|
||||
invalid.state.wasm_motion.native_state.blocks[0][1] = "invalid";
|
||||
invalid.state_hash = fnv1a64(invalid.state);
|
||||
invalid.checkpoint_hash = fnv1a64(Object.fromEntries(Object.entries(invalid).filter(([key]) => key !== "checkpoint_hash")));
|
||||
const unchanged = fnv1a64(original.session.workspacePackageState());
|
||||
assert.equal(original.session.restoreCheckpoint(invalid).status, "blocked");
|
||||
assert.equal(fnv1a64(original.session.workspacePackageState()), unchanged, "corrupt native blob rejected before mutation");
|
||||
const foreignBuild = structuredClone(checkpoint);
|
||||
foreignBuild.state.wasm_motion.native_state.build_id = "0".repeat(64);
|
||||
foreignBuild.state_hash = fnv1a64(foreignBuild.state);
|
||||
foreignBuild.checkpoint_hash = fnv1a64(Object.fromEntries(Object.entries(foreignBuild).filter(([key]) => key !== "checkpoint_hash")));
|
||||
const rejectedBuild = original.session.restoreCheckpoint(foreignBuild);
|
||||
assert.equal(rejectedBuild.status, "blocked");
|
||||
assert.match(rejectedBuild.blocked[0].reason, /checkpoint_build_mismatch/);
|
||||
assert.equal(fnv1a64(original.session.workspacePackageState()), unchanged);
|
||||
const transported = await open(machine);
|
||||
const recovery = transported.send("RECOVER", { checkpoint });
|
||||
assert.equal(recovery.payload.status, "recovered", JSON.stringify(recovery.payload));
|
||||
const next = transported.send("SIM_STEP", { cycles: 1 });
|
||||
assert.equal(next.payload.program.program_status, "completed", "transport sequence continues after recovery");
|
||||
assert.deepEqual(next.payload.wasm_motion.state.position, original.session.wasmMotion.read().state.position);
|
||||
rows.push({ machine: machine.id, phase: "trace-and-transport", state_hash: fnv1a64(original.session.workspacePackageState()), first_divergent_tick: null });
|
||||
transported.send("SET_MODE", { mode: "MANUAL" });
|
||||
transported.send("PROGRAM_LOAD", { source: "G21 G90\nM2\n", path: "jog-after-recovery.ngc" });
|
||||
transported.send("JOG_START", { axis: "x", direction: 1, increment: .1 });
|
||||
let jogSamples = 0;
|
||||
while (transported.session.wasmMotion.read().state.queue_depth > 0 && jogSamples < 100) {
|
||||
const delta = transported.send("SIM_STEP", { cycles: 1, response: "motion_delta" });
|
||||
assert.deepEqual(delta.payload.motion_sample.joints, transported.session.wasmMotion.read().state.joint_commanded, "identity motion delta must use the current planner joints");
|
||||
jogSamples++;
|
||||
}
|
||||
assert.equal(transported.session.wasmMotion.read().state.queue_depth, 0);
|
||||
rows.push({ machine: machine.id, phase: "post-recovery-jog", samples: jogSamples, first_divergent_tick: null });
|
||||
const beforeRewind = transported.session.wasmMotion.read();
|
||||
const rewind = transported.send("RECOVER", { checkpoint: idle });
|
||||
assert.equal(rewind.payload.status, "blocked");
|
||||
assert.equal(rewind.payload.code, "runtime_checkpoint_requires_new_session");
|
||||
assert.deepEqual(transported.session.wasmMotion.read(), beforeRewind);
|
||||
}
|
||||
const report = { report_id: "trt-checkpoint-exact-v1", status: "verified", safeguards: { corrupt_native_rejected: true, foreign_build_rejected: true, frozen_subroutines: true, heap_relocation_checked: true, receipts_nonrecursive: true, backward_transport_restore_rejected: true }, rows, state_hash: fnv1a64(rows) };
|
||||
await mkdir(new URL("coverage/recovery/", root), { recursive: true });
|
||||
await writeFile(new URL("coverage/recovery/exact-checkpoint.report.json", root), JSON.stringify(report, null, 2) + "\n");
|
||||
console.log(JSON.stringify(report));
|
||||
98
web/worker/test_trt_configuration.mjs
Normal file
@@ -0,0 +1,98 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile, mkdir, writeFile } from "node:fs/promises";
|
||||
import createModule from "../../build-wasm/linuxcnc-sim-core.js";
|
||||
import { loadSimulationCase, toWorkerConfiguration } from "../workspace/sim-case-loader.mjs";
|
||||
import { readTrtConfiguration } from "../workspace/trt-configuration.mjs";
|
||||
import { MACHINE_DEFINITIONS } from "../workspace/machine-definitions.mjs";
|
||||
import { SimSession, CONTEXT_ID } from "./sim-session.mjs";
|
||||
import { fnv1a64 } from "./replay.mjs";
|
||||
|
||||
const root = new URL("../../", import.meta.url);
|
||||
const fetchImpl = async (url) => {
|
||||
try { const source = await readFile(new URL(String(url).replace(/^\//, ""), root), "utf8"); return { ok: true, text: async () => source }; }
|
||||
catch { return { ok: false }; }
|
||||
};
|
||||
const wasmBinary = await readFile(new URL("build-wasm/linuxcnc-sim-core.wasm", root));
|
||||
function edit(bundle, values) {
|
||||
let section = "";
|
||||
const content = bundle.files[bundle.entrypoint].split("\n").map((line) => {
|
||||
const heading = /^\s*\[([^\]]+)\]/.exec(line); if (heading) section = heading[1];
|
||||
const assignment = /^\s*([A-Z_]+)\s*=/.exec(line);
|
||||
const value = values[`${section}.${assignment?.[1]}`];
|
||||
return value === undefined ? line : `${assignment[1]} = ${value}`;
|
||||
}).join("\n");
|
||||
return { ...bundle, files: { ...bundle.files, [bundle.entrypoint]: content } };
|
||||
}
|
||||
const rows = [];
|
||||
for (const machine of MACHINE_DEFINITIONS) {
|
||||
const original = await loadSimulationCase({ path: machine.entrypoint, fetchImpl });
|
||||
const modified = edit(original, { "AXIS_X.MAX_VELOCITY": 4, "AXIS_X.MAX_ACCELERATION": 8, "AXIS_X.MAX_LIMIT": 15, "JOINT_0.MAX_LIMIT": 17, "TRAJ.MAX_LINEAR_VELOCITY": 13, "EMCMOT.SERVO_PERIOD": 2000000, "TASK.CYCLE_TIME": .02, "DISPLAY.MAX_FEED_OVERRIDE": 1.3 });
|
||||
const config = toWorkerConfiguration(modified);
|
||||
const zeroTolerance = { ...modified, files: { ...modified.files, [modified.entrypoint]: `${modified.files[modified.entrypoint]}\n[WEB_RUNTIME]\nSPINDLE_TOLERANCE = 0\n` } };
|
||||
assert.equal(readTrtConfiguration(zeroTolerance).kinematics.io.spindle_tolerance, 0);
|
||||
const blendingCase = { ...modified, files: { ...modified.files, [modified.entrypoint]: `${modified.files[modified.entrypoint]}\n[TRAJ]\nARC_BLEND_ENABLE = 1\nARC_BLEND_OPTIMIZATION_DEPTH = 25\nARC_BLEND_GAP_CYCLES = 3\nARC_BLEND_RAMP_FREQ = 50\nARC_BLEND_TANGENT_KINK_RATIO = 0.2\n` } };
|
||||
const configuredBlend = readTrtConfiguration(blendingCase).kinematics.blend;
|
||||
assert.equal(configuredBlend.optimization_depth, 25);
|
||||
assert.equal(readTrtConfiguration(edit(original, { "TRAJ.MAX_LINEAR_ACCELERATION": 8 })).kinematics.max_acceleration, 8);
|
||||
for (const declaration of ["SPINDLE_COUNT = 2.5", "TOOL_CAPACITY = 1002", "ENCODER_COUNT = 9"]) {
|
||||
const invalid = { ...modified, files: { ...modified.files, [modified.entrypoint]: `${modified.files[modified.entrypoint]}\n[WEB_RUNTIME]\n${declaration}\n` } };
|
||||
assert.throws(() => readTrtConfiguration(invalid), /trt_configuration_io_capacity/);
|
||||
}
|
||||
assert.deepEqual(config.home_limits.x, [-200, 17]);
|
||||
assert.deepEqual(config.kinematics.axis_limits.x, [-200, 15]);
|
||||
assert.equal(config.kinematics.axis_velocity_limits.x, 4);
|
||||
for (const values of [{ "AXIS_X.MAX_VELOCITY": "NaN" }, { "AXIS_X.MAX_ACCELERATION": 0 }, { "JOINT_0.MAX_LIMIT": -300 }, { "KINS.JOINTS": 4 }, { "TASK.CYCLE_TIME": .0015 }, { "DISPLAY.MAX_FEED_OVERRIDE": .5 }]) assert.throws(() => readTrtConfiguration(edit(original, values)), /trt_configuration/);
|
||||
const core = await createModule({ wasmBinary });
|
||||
const session = new SimSession(core, { sessionId: `configuration-${machine.id}` }); let seq = 0;
|
||||
const send = (kind, payload = {}, expected = "ok") => {
|
||||
const result = session.accept({ schema_version: 1, context_id: CONTEXT_ID, session_id: session.sessionId, kind, seq: ++seq, operation_id: `cfg-${seq}`, payload });
|
||||
if (expected === "error") assert.equal(result.kind, "ERROR");
|
||||
else assert.notEqual(result.kind, "ERROR", `${kind}: ${JSON.stringify(result.payload)}`);
|
||||
return result;
|
||||
};
|
||||
send("HELLO"); send("SESSION_ATTACH");
|
||||
send("LOAD_WORKSPACE", { root_path: "/workspace", configuration: toWorkerConfiguration(original) });
|
||||
// Existing adapter values are untrusted duplicates: the files must win.
|
||||
config.home_limits.x = [-1, 1]; config.kinematics.max_linear_velocity = 99;
|
||||
send("LOAD_WORKSPACE", { root_path: "/workspace", configuration: config });
|
||||
assert.equal(session.wasmMotion.velocityLimit, 13);
|
||||
assert.equal(session.wasmMotion.cycleTime, .002);
|
||||
assert.equal(session.wasmHomeLimit.period, .002);
|
||||
assert.equal(session.xyzacTaskCycleTicks, 10);
|
||||
assert.equal(readTrtConfiguration(modified).kinematics.io.cycle_time, .02);
|
||||
assert.equal(session.wasmMotion.jogVelocity("x"), 4);
|
||||
assert.deepEqual(session.xyzacKinematics.snapshot().limits.x, [-200, 17]);
|
||||
send("POWER", { active: true }); send("HOME", { all: true });
|
||||
for (let i = 0; i < 500 && !session.wasmHomeLimit.read().state.homed; i++) send("SIM_STEP", { cycles: 1 });
|
||||
assert.ok(session.wasmHomeLimit.read().state.homed);
|
||||
send("SET_OVERRIDE", { feed: 1.31 }, "error"); send("SET_OVERRIDE", { feed: 1.3 }); send("SET_OVERRIDE", { feed: 1 });
|
||||
send("JOG_START", { axis: "x", direction: 1, increment: 16 }, "error");
|
||||
send("MDI_EXECUTE", { block: "G21 G90 G1 X10 F1200" });
|
||||
let maxVelocity = 0, maxAcceleration = 0, previousVelocity = 0;
|
||||
const trace = [];
|
||||
for (let i = 0; i < 2000 && session.wasmMotion.read().state.queue_depth > 0; i++) {
|
||||
send("SIM_STEP", { cycles: 1 });
|
||||
const state = session.wasmMotion.read().state;
|
||||
maxVelocity = Math.max(maxVelocity, state.velocity);
|
||||
maxAcceleration = Math.max(maxAcceleration, Math.abs(state.velocity - previousVelocity) / .02);
|
||||
previousVelocity = state.velocity;
|
||||
trace.push({ tick: session.tick, x: state.position.x, velocity: state.velocity });
|
||||
}
|
||||
assert.equal(session.wasmMotion.read().state.queue_depth, 0);
|
||||
assert.ok(maxVelocity > 3.9 && maxVelocity <= 4.000001, `${machine.id} velocity ${maxVelocity}`);
|
||||
assert.ok(maxAcceleration <= 8.000001, `${machine.id} acceleration ${maxAcceleration}`);
|
||||
assert.ok(Math.abs(session.wasmMotion.read().state.position.x - 10) < 1e-8);
|
||||
rows.push({ machine: machine.id, max_velocity: maxVelocity, max_acceleration: maxAcceleration, period: session.wasmMotion.cycleTime, joint_limit: session.xyzacKinematics.snapshot().limits.x, trace });
|
||||
const checkpoint = session.createCheckpoint();
|
||||
send("POWER", { active: false });
|
||||
const newSpeed = toWorkerConfiguration(edit(modified, { "AXIS_X.MAX_VELOCITY": 5 }));
|
||||
send("LOAD_WORKSPACE", { root_path: "/workspace", configuration: newSpeed });
|
||||
const restored = session.restoreCheckpoint(checkpoint);
|
||||
assert.equal(restored.status, "blocked");
|
||||
assert.equal(restored.blocked[0].reason, "trt_recovery_configuration_mismatch");
|
||||
assert.equal(session.wasmMotion.jogVelocity("x"), 5);
|
||||
}
|
||||
const report = { report_id: "trt-configuration-v1", status: "verified", rows };
|
||||
await mkdir(new URL("coverage/configuration/", root), { recursive: true });
|
||||
await writeFile(new URL("coverage/configuration/trt-runtime.report.json", root), JSON.stringify({ ...report, state_hash: fnv1a64(report) }, null, 2) + "\n");
|
||||
console.log(JSON.stringify({ status: report.status, report_id: report.report_id, rows: rows.map(({ trace, ...row }) => row) }));
|
||||
@@ -30,7 +30,7 @@ async function create(id) {
|
||||
assert.equal(send("POWER", "power-on", { active: true }).payload.status, "verified");
|
||||
assert.equal(send("HOME", "home-all", { all: true }).payload.homing.phase, "accepted");
|
||||
assert.equal(send("SIM_STEP", "home-complete", { cycles: 1 }).payload.panel_snapshot.motion.homed, true);
|
||||
return { core, session, send };
|
||||
return { core, session, send, adoptSequence: (value) => { seq = value; } };
|
||||
}
|
||||
|
||||
const continuous = await create("xyzac-recovery-continuous");
|
||||
@@ -42,6 +42,7 @@ continuous.send("PROGRAM_RUN", "program-run");
|
||||
const stepped = continuous.send("SIM_STEP", "motion-step", { cycles: 2 });
|
||||
assert.equal(stepped.payload.status, "verified");
|
||||
const checkpointResponse = continuous.send("CHECKPOINT", "checkpoint");
|
||||
assert.equal(checkpointResponse.kind, "CHECKPOINT_READY", checkpointResponse.payload.message_key);
|
||||
const checkpoint = checkpointResponse.payload.checkpoint;
|
||||
assert.equal(checkpoint.state.kinematics.active_type, 1);
|
||||
assert.equal(checkpoint.state.kinematics.coordinates, "XYZAC");
|
||||
@@ -57,7 +58,10 @@ continuous.send("SIM_STEP", "after-checkpoint", { cycles: 2 });
|
||||
const endpoint = continuous.session.workspacePackageState();
|
||||
|
||||
const restarted = await create("xyzac-recovery-continuous");
|
||||
const recovery = restarted.session.restoreCheckpoint(checkpoint, continuous.session.timeline.export(), (operation) => operation);
|
||||
const rejectedTail = restarted.session.restoreCheckpoint(checkpoint, continuous.session.timeline.export(), (operation) => operation);
|
||||
assert.equal(rejectedTail.status, "blocked");
|
||||
assert.equal(rejectedTail.blocked[0].reason, "trt_checkpoint_tail_requires_explicit_commands");
|
||||
const recovery = restarted.session.restoreCheckpoint(checkpoint);
|
||||
if (recovery.status !== "recovered") console.error(JSON.stringify(recovery));
|
||||
assert.equal(recovery.status, "recovered");
|
||||
const restored = restarted.session.workspacePackageState();
|
||||
@@ -68,11 +72,16 @@ assert.equal(restored.kinematics.state_hash, before.kinematics.state_hash);
|
||||
assert.equal(restored.kinematics.switch_count, switchCountBefore);
|
||||
assert.equal(restored.program.source_hash, before.program.source_hash);
|
||||
assert.equal(restored.motion_authority, "wasm");
|
||||
if (fnv1a64(before) !== fnv1a64(restored)) console.error(JSON.stringify({ differing: Object.fromEntries(Object.keys(before).filter((key) => fnv1a64(before[key]) !== fnv1a64(restored[key])).map((key) => [key, { before: before[key], restored: restored[key] }])) }));
|
||||
assert.equal(fnv1a64(restored), fnv1a64(before), "checkpoint must restore complete runtime state");
|
||||
|
||||
const replay = restarted.session.ledger.reconcile("switch-once");
|
||||
assert.equal(replay.status, "committed");
|
||||
assert.equal(restarted.session.xyzacKinematics.snapshot().switch_count, switchCountBefore);
|
||||
restarted.adoptSequence(checkpoint.event_cursor);
|
||||
const replayedSwitch = restarted.send("MDI_EXECUTE", "switch-once", { block: "M428" });
|
||||
assert.equal(replayedSwitch.replayed, true);
|
||||
assert.equal(restarted.session.xyzacKinematics.snapshot().switch_count, switchCountBefore);
|
||||
assert.deepEqual(replayedSwitch.payload.panel_snapshot.visual.joints, before.motion.joints);
|
||||
|
||||
const corruptedState = structuredClone(checkpoint);
|
||||
corruptedState.state.kinematics.pose.x += 1;
|
||||
@@ -111,8 +120,10 @@ console.log(JSON.stringify({
|
||||
status: "verified",
|
||||
case: CASE,
|
||||
checkpoint: { required_fields: ["context_id", "simulation_tick", "event_cursor", "state_hash", "task", "program", "kinematics", "motion", "wasm_motion", "ledger"], active_type: checkpoint.state.kinematics.active_type, state_hash: checkpoint.state.kinematics.state_hash },
|
||||
continuous_endpoint_hash: fnv1a64(endpoint),
|
||||
continuous_endpoint_hash: fnv1a64(before),
|
||||
restored_endpoint_hash: fnv1a64(restored),
|
||||
checkpoint_tick: checkpoint.simulation_tick,
|
||||
unreplayed_tail_endpoint_hash: fnv1a64(endpoint),
|
||||
endpoint_at_checkpoint_equal: fnv1a64(before) === fnv1a64(restored),
|
||||
no_duplicate_m428: restarted.session.xyzacKinematics.snapshot().switch_count === switchCountBefore,
|
||||
replayed_operation: replay.status === "committed",
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { fnv1a64 } from "./replay.mjs";
|
||||
import { exportNativeCheckpoint, restoreNativeCheckpoint } from "./native-checkpoint.mjs";
|
||||
import { WEB_SETTINGS } from "../workspace/web-settings.mjs";
|
||||
const DEFAULTS = WEB_SETTINGS.runtime.planner;
|
||||
|
||||
const POSE_BYTES = 72;
|
||||
const STATE_BYTES = 112;
|
||||
@@ -15,7 +18,7 @@ function poseArray(value = {}) { return ["x", "y", "z", "a", "b", "c", "u", "v",
|
||||
function toPose(values) { return Object.fromEntries(["x", "y", "z", "a", "b", "c", "u", "v", "w"].map((axis, index) => [axis, Number(values[index] ?? 0)])); }
|
||||
|
||||
export class WasmLinuxcncTp {
|
||||
constructor(core, { cycleTime = 0.001, velocityLimit = 35, accelerationLimit = 300, limits = {} } = {}) {
|
||||
constructor(core, { cycleTime = DEFAULTS.cycle_time, velocityLimit = DEFAULTS.velocity_limit, accelerationLimit = DEFAULTS.acceleration_limit, limits = {}, axisVelocities = {}, axisAccelerations = {}, maxFeedOverride = DEFAULTS.max_feed_override, blend = DEFAULTS.blend } = {}) {
|
||||
this.core = core;
|
||||
this.available = ["sim_linuxcnc_tp_size", "sim_linuxcnc_tp_queue_capacity", "sim_linuxcnc_tp_init", "sim_linuxcnc_tp_read_state", "sim_linuxcnc_tp_step", "sim_linuxcnc_tp_idle", "sim_linuxcnc_tp_add_line", "sim_linuxcnc_tp_set_profile", "sim_linuxcnc_tp_set_feed_override", "sim_linuxcnc_tp_set_velocity_scale", "sim_linuxcnc_tp_set_jog_scale", "sim_linuxcnc_tp_set_axis_limit", "sim_linuxcnc_tp_jog", "sim_linuxcnc_tp_velocity_scale", "sim_linuxcnc_tp_velocity_limit", "sim_linuxcnc_tp_velocity_mm_min", "sim_linuxcnc_tp_jog_scale", "sim_linuxcnc_tp_jog_velocity", "sim_linuxcnc_tp_feed_override", "sim_linuxcnc_tp_set_dynamics", "sim_linuxcnc_tp_set_paused", "sim_linuxcnc_tp_abort", "sim_linuxcnc_tp_set_goal_position", "sim_linuxcnc_tp_queue_depth", "sim_linuxcnc_tp_queue_segment", "sim_linuxcnc_tp_restore_progress"].every((name) => typeof core?.[`_${name}`] === "function" || typeof core?.[name] === "function");
|
||||
this.cycleTime = cycleTime;
|
||||
@@ -37,7 +40,8 @@ export class WasmLinuxcncTp {
|
||||
this.lastState = null;
|
||||
this.queueDepthOverride = null;
|
||||
this.kinematics = null;
|
||||
this.controls = { profile: 0, lookahead: 50, acceleration_limit: accelerationLimit, jerk_limit: 1e9, feed_override: 1, velocity_scale: 1, jog_scale: 1, paused: false };
|
||||
this.controls = { profile: 0, lookahead: blend.optimization_depth, acceleration_limit: accelerationLimit, jerk_limit: DEFAULTS.compatibility_jerk_limit, feed_override: 1, velocity_scale: 1, jog_scale: 1, paused: false };
|
||||
this.maxFeedOverride = maxFeedOverride;
|
||||
if (!this.available || typeof core?._malloc !== "function") return;
|
||||
this.planner = core._malloc(call(core, "sim_linuxcnc_tp_size", "number", [], []));
|
||||
this.posePointer = core._malloc(POSE_BYTES);
|
||||
@@ -55,6 +59,17 @@ export class WasmLinuxcncTp {
|
||||
const index = { x: 0, y: 1, z: 2, a: 3, b: 4, c: 5, u: 6, v: 7, w: 8 }[axis];
|
||||
if (index !== undefined && Array.isArray(limit) && limit.length === 2) this.setAxisLimit(index, Number(limit[0]), Number(limit[1]));
|
||||
}
|
||||
for (const [axis, velocity] of Object.entries(axisVelocities)) {
|
||||
const index = "xyzabcuvw".indexOf(axis);
|
||||
const configured = call(core, "sim_linuxcnc_tp_set_axis_dynamics", "number", ["number", "number", "number", "number"], [this.planner, index, velocity, axisAccelerations[axis]]);
|
||||
if (configured !== 0) throw new Error(`linuxcnc_tp_axis_dynamics_${axis}_${configured}`);
|
||||
}
|
||||
const feedLimit = call(core, "sim_linuxcnc_tp_set_feed_limit", "number", ["number", "number"], [this.planner, maxFeedOverride]);
|
||||
if (feedLimit !== 0) throw new Error(`linuxcnc_tp_feed_limit_${feedLimit}`);
|
||||
const scale = call(core, "sim_linuxcnc_tp_set_scale_minimum", "number", ["number", "number"], [this.planner, WEB_SETTINGS.ui.motion_scale_min]);
|
||||
if (scale !== 0) throw new Error(`linuxcnc_tp_scale_minimum_${scale}`);
|
||||
const blending = call(core, "sim_linuxcnc_tp_set_blend", "number", Array(7).fill("number"), [this.planner, blend.enable, blend.fallback_enable, blend.optimization_depth, blend.gap_cycles, blend.ramp_frequency, blend.tangent_kink_ratio]);
|
||||
if (blending !== 0) throw new Error(`linuxcnc_tp_blend_${blending}`);
|
||||
}
|
||||
|
||||
setPosition(value) {
|
||||
@@ -247,24 +262,19 @@ export class WasmLinuxcncTp {
|
||||
}
|
||||
} finally { this.core._free(pointer); }
|
||||
}
|
||||
const unsigned = { schema: SOURCE_TP_SCHEMA, cycle_time: this.cycleTime, velocity_limit: this.velocityLimit, controls: { ...this.controls }, execution_state: this.executionState, active_tag: this.activeTag, state: current.state, segments };
|
||||
const unsigned = { schema: SOURCE_TP_SCHEMA, cycle_time: this.cycleTime, velocity_limit: this.velocityLimit, controls: { ...this.controls }, execution_state: this.executionState, active_tag: this.activeTag, state: current.state, segments, segment_tags: [...this.segmentTags], last_active_id: this.lastActiveId, queue_depth_override: this.queueDepthOverride, native_state: exportNativeCheckpoint(this.core, "sim_linuxcnc_tp", this.planner) };
|
||||
return { ...unsigned, state_hash: fnv1a64(unsigned) };
|
||||
}
|
||||
restoreState(value) {
|
||||
if (!value || value.schema !== SOURCE_TP_SCHEMA || value.cycle_time !== this.cycleTime || value.velocity_limit !== this.velocityLimit || value.state_hash !== fnv1a64(Object.fromEntries(Object.entries(value).filter(([key]) => key !== "state_hash")))) throw new Error("linuxcnc_tp_restore_schema");
|
||||
this.reset();
|
||||
this.executionState = Number(value.execution_state ?? 3);
|
||||
this.setProfile(value.controls?.profile ?? 0, value.controls?.tolerance ?? 0);
|
||||
this.setFeedOverride(value.controls?.feed_override ?? 1);
|
||||
this.setVelocityScale(value.controls?.velocity_scale ?? 1);
|
||||
this.setJogScale(value.controls?.jog_scale ?? 1);
|
||||
this.setPosition(value.state?.position ?? {});
|
||||
for (const segment of value.segments ?? []) this.enqueue({ ...segment, kinematics_mode: segment.kinematics_mode ?? value.active_tag ?? 0 });
|
||||
if (this.available && value.state) {
|
||||
this.core.HEAPF64.set(poseArray(value.state.position), this.posePointer / 8);
|
||||
const restored = call(this.core, "sim_linuxcnc_tp_restore_progress", "number", ["number", "number", "number", "number"], [this.planner, this.posePointer, Number(value.state.velocity ?? 0), Number(value.state.tick ?? 0)]);
|
||||
if (restored !== 0) throw new Error(`linuxcnc_tp_restore_progress_${restored}`);
|
||||
}
|
||||
if (!value.native_state || !Array.isArray(value.segment_tags)) throw new Error("linuxcnc_tp_native_checkpoint_required");
|
||||
restoreNativeCheckpoint(this.core, "sim_linuxcnc_tp", value.native_state, this.planner);
|
||||
this.executionState = value.execution_state;
|
||||
this.activeTag = value.active_tag;
|
||||
this.controls = { ...value.controls };
|
||||
this.segmentTags = new Map(value.segment_tags);
|
||||
this.lastActiveId = value.last_active_id;
|
||||
this.queueDepthOverride = value.queue_depth_override;
|
||||
return this.read();
|
||||
}
|
||||
destroy() { for (const pointer of [this.planner, this.posePointer, this.centerPointer, this.normalPointer, this.statePointer]) if (pointer && typeof this.core?._free === "function") this.core._free(pointer); this.planner = this.posePointer = this.centerPointer = this.normalPointer = this.statePointer = 0; }
|
||||
|
||||
@@ -6,7 +6,7 @@ export class WasmNativeHoming {
|
||||
constructor(core, configuration, kinematics) {
|
||||
this.core = core;
|
||||
this.kinematics = kinematics;
|
||||
this.jointCount = 5;
|
||||
this.jointCount = configuration.home_limits.joint_count;
|
||||
this.size = core._sim_native_home_size();
|
||||
this.pointer = core._malloc(this.size);
|
||||
this.buffer = core._malloc(12 * 8);
|
||||
@@ -17,7 +17,7 @@ export class WasmNativeHoming {
|
||||
if (typeof value !== "string") throw new Error(`home_ini_missing:${path}`);
|
||||
return value;
|
||||
} } });
|
||||
this.parameters = Array.from({ length: 5 }, (_, j) => {
|
||||
this.parameters = Array.from({ length: this.jointCount }, (_, j) => {
|
||||
const section = parsed.sections[`JOINT_${j}`] ?? {};
|
||||
const n = (key, fallback) => section[key] === undefined ? fallback : Number(section[key]);
|
||||
const flags = [["HOME_IGNORE_LIMITS", 1], ["HOME_USE_INDEX", 2], ["HOME_IS_SHARED", 4], ["LOCKING_INDEXER", 8], ["HOME_INDEX_NO_ENCODER_RESET", 128]].reduce((mask, [key, bit]) => mask | (n(key, 0) ? bit : 0), 0);
|
||||
@@ -25,7 +25,7 @@ export class WasmNativeHoming {
|
||||
if (![0, 1, 2].includes(encoder)) throw new Error("native_home_absolute_encoder_invalid");
|
||||
return [n("HOME_OFFSET", 0), n("HOME", 0), n("HOME_FINAL_VEL", -1), n("HOME_SEARCH_VEL", 0), n("HOME_LATCH_VEL", 0), flags | (encoder ? 16 | 32 | (encoder === 2 ? 64 : 0) : 0), n("HOME_SEQUENCE", 999), n("VOLATILE_HOME", 0), n("MIN_LIMIT", -1e9), n("MAX_LIMIT", 1e9), n("MAX_VELOCITY", 1), n("MAX_ACCELERATION", 1)];
|
||||
});
|
||||
this.period = Number(parsed.sections.EMCMOT?.SERVO_PERIOD ?? 1000000) / 1e9;
|
||||
this.period = configuration.kinematics.servo_cycle_time;
|
||||
this.reset();
|
||||
}
|
||||
invoke(name, ...args) {
|
||||
@@ -41,13 +41,13 @@ export class WasmNativeHoming {
|
||||
step() { this.invoke("step"); return this.read(); }
|
||||
cancel(volatile = false) { this.invoke("cancel", Number(volatile)); return this.read(); }
|
||||
reset() {
|
||||
this.invoke("init", 5, this.period);
|
||||
this.invoke("init", this.jointCount, this.period);
|
||||
this.parameters.forEach((p, j) => { this.core.HEAPF64.set(p, this.buffer / 8); this.invoke("config", j, this.buffer); });
|
||||
return this.read();
|
||||
}
|
||||
check(joints) { if (!this.active) this.sync(joints); return this.read(); }
|
||||
read() {
|
||||
const joint = Array.from({ length: 5 }, (_, j) => ({
|
||||
const joint = Array.from({ length: this.jointCount }, (_, j) => ({
|
||||
joint: j, homing: Boolean(this.get(j, 4)), homed: Boolean(this.get(j, 5)),
|
||||
home_state: this.get(j, 6), home_state_name: `HOME_${STATES[this.get(j, 6)]}`,
|
||||
position: this.get(j, 7), motor_position: this.get(j, 8), motor_offset: this.get(j, 9),
|
||||
@@ -56,7 +56,7 @@ export class WasmNativeHoming {
|
||||
const joints = joint.map((j) => j.position);
|
||||
const result = this.kinematics.forward(joints);
|
||||
return { status: result.status, ...(result.code ? { code: result.code } : {}), state: {
|
||||
schema: "sim_home_limit_state.v1", source: "linuxcnc-homing.c", joint_count: 5,
|
||||
schema: "sim_home_limit_state.v1", source: "linuxcnc-homing.c", joint_count: this.jointCount,
|
||||
homed: joint.every((j) => j.homed), homed_mask: joint.reduce((mask, j) => mask | (j.homed ? 1 << j.joint : 0), 0),
|
||||
homing: this.active, motion_mode: this.mode, joint, joints, pose: result.pose ?? null,
|
||||
home_sequence: this.get(0, 3), tick: this.get(0, 2), last_error: result.result ?? 0, limit_error: result.result === -702 ? -702 : 0, kinematics_error: result.result ?? 0,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { fnv1a64 } from "./replay.mjs";
|
||||
import { exportNativeCheckpoint, restoreNativeCheckpoint } from "./native-checkpoint.mjs";
|
||||
|
||||
const SCHEMA = "wasm_program_runtime.v1";
|
||||
const SEGMENT_BYTES = 216;
|
||||
@@ -237,8 +238,15 @@ export class WasmProgramRuntime {
|
||||
seek(line) { return this.invoke("sim_program_seek", [line], "program_seek"); }
|
||||
|
||||
restoreState(value) {
|
||||
if (!value || ![SCHEMA, "sim_program.v1"].includes(value.schema) || !Number.isSafeInteger(value.current_line) || value.current_line < 0 || !this.source) throw new Error("wasm_program_restore_schema");
|
||||
if (!value || ![SCHEMA, "sim_program.v1"].includes(value.schema) || !Number.isSafeInteger(value.current_line) || value.current_line < 0 || !this.source && !value.native_state) throw new Error("wasm_program_restore_schema");
|
||||
if (value.source_hash && value.source_hash !== this.sourceHash) throw new Error("wasm_program_restore_source_hash");
|
||||
if (value.native_state) {
|
||||
if (value.execution_source_hash !== fnv1a64(this.executionSource)) throw new Error("wasm_program_restore_execution_source_hash");
|
||||
restoreNativeCheckpoint(this.core, "sim_program", value.native_state, this.pointer);
|
||||
this.synchronizedPosition = structuredClone(value.synchronized_position);
|
||||
this.rapidVelocity = value.rapid_velocity;
|
||||
return this.snapshot();
|
||||
}
|
||||
const subroutines = [...this.subroutines];
|
||||
const loaded = this.load(this.source, this.path);
|
||||
if (loaded.status !== "verified") throw new Error(loaded.code);
|
||||
@@ -333,6 +341,10 @@ export class WasmProgramRuntime {
|
||||
return { status: "verified", program_status: state.status, line: state.current_line, control_code: state.control_code ?? 0, has_control: state.has_control === 1, program: state, motion: this.readMotion(state) };
|
||||
}
|
||||
|
||||
checkpoint() {
|
||||
return { ...this.snapshot(), subroutine_sources: structuredClone(this.subroutines), synchronized_position: structuredClone(this.synchronizedPosition), execution_source_hash: fnv1a64(this.executionSource), native_state: exportNativeCheckpoint(this.core, "sim_program", this.pointer) };
|
||||
}
|
||||
|
||||
snapshot() {
|
||||
if (!this.available) return { schema: SCHEMA, status: "blocked", code: "wasm_program_unavailable" };
|
||||
const required = call(this.core, "sim_program_canonical", "number", ["number", "number", "number"], [this.pointer, 0, 0]);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { fnv1a64 } from "./replay.mjs";
|
||||
import { WEB_SETTINGS } from "../workspace/web-settings.mjs";
|
||||
|
||||
const DEVICE_SCHEMA = "wasm_xyzac_device_io.v1";
|
||||
const DEVICE_ERROR_REASONS = Object.freeze({
|
||||
@@ -42,7 +43,7 @@ function operationNumber(value) {
|
||||
}
|
||||
|
||||
export class WasmXyzacDeviceIo {
|
||||
constructor(core, { encoderCount = 2 } = {}) {
|
||||
constructor(core, { encoderCount = WEB_SETTINGS.runtime.io.encoder_count, cycleTime = WEB_SETTINGS.runtime.io.cycle_time } = {}) {
|
||||
this.core = core;
|
||||
this.available = ["sim_device_io_size", "sim_device_init", "sim_device_canonical", "sim_device_state_hash"].every((name) => typeof core?.[`_${name}`] === "function" || typeof core?.[name] === "function");
|
||||
this.pointer = 0;
|
||||
@@ -50,7 +51,7 @@ export class WasmXyzacDeviceIo {
|
||||
const size = call(core, "sim_device_io_size", "number", [], []);
|
||||
this.pointer = core._malloc(size);
|
||||
if (!this.pointer) throw new Error("wasm_xyzac_device_io_allocation_failed");
|
||||
const result = call(core, "sim_device_init", "number", ["number", "number", "number"], [this.pointer, encoderCount, 0.01]);
|
||||
const result = call(core, "sim_device_init", "number", ["number", "number", "number"], [this.pointer, encoderCount, cycleTime]);
|
||||
if (result !== 0) throw new Error(`wasm_xyzac_device_io_init_${result}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { fnv1a64 } from "./replay.mjs";
|
||||
import { exportNativeCheckpoint, restoreNativeCheckpoint } from "./native-checkpoint.mjs";
|
||||
import { WEB_SETTINGS } from "../workspace/web-settings.mjs";
|
||||
const DEFAULTS = WEB_SETTINGS.runtime.io;
|
||||
|
||||
const IO_SCHEMA = "wasm_xyzac_io.v1";
|
||||
const HAL_SCHEMA = "wasm_xyzac_hal.v1";
|
||||
@@ -44,7 +47,7 @@ function hexToBytes(value) {
|
||||
}
|
||||
|
||||
export class WasmXyzacIo {
|
||||
constructor(core, { spindleCount = 2, toolCapacity = 1001 } = {}) {
|
||||
constructor(core, { spindleCount = DEFAULTS.spindle_count, toolCapacity = DEFAULTS.tool_capacity, cycleTime = DEFAULTS.cycle_time, spindleAcceleration = DEFAULTS.spindle_acceleration, spindleTolerance = DEFAULTS.spindle_tolerance } = {}) {
|
||||
this.core = core;
|
||||
this.available = ["sim_io_size", "sim_io_init", "sim_io_canonical", "sim_io_state_hash"].every((name) => typeof core?.[`_${name}`] === "function" || typeof core?.[name] === "function");
|
||||
this.pointer = 0;
|
||||
@@ -58,7 +61,7 @@ export class WasmXyzacIo {
|
||||
this.pointer = core._malloc(this.ioSize);
|
||||
this.posePointer = core._malloc(POSE_BYTES);
|
||||
if (!this.pointer || !this.posePointer) throw new Error("wasm_xyzac_io_allocation_failed");
|
||||
const result = call(core, "sim_io_init", "number", ["number", "number", "number", "number", "number", "number"], [this.pointer, spindleCount, toolCapacity, 0.01, 600, 5]);
|
||||
const result = call(core, "sim_io_init", "number", ["number", "number", "number", "number", "number", "number"], [this.pointer, spindleCount, toolCapacity, cycleTime, spindleAcceleration, spindleTolerance]);
|
||||
if (result !== 0) throw new Error(`wasm_xyzac_io_init_${result}`);
|
||||
}
|
||||
|
||||
@@ -102,7 +105,7 @@ export class WasmXyzacIo {
|
||||
return this.export();
|
||||
}
|
||||
|
||||
halExport() {
|
||||
halExport({ includeState = false } = {}) {
|
||||
if (!this.available) return { schema: HAL_SCHEMA, source: "wasm_hal", status: "blocked", code: "wasm_hal_unavailable" };
|
||||
const tick = call(this.core, "sim_hal_tick", "number", [], []);
|
||||
const unsigned = {
|
||||
@@ -115,10 +118,16 @@ export class WasmXyzacIo {
|
||||
thread_count: call(this.core, "sim_hal_thread_count", "number", [], []),
|
||||
function_count: call(this.core, "sim_hal_function_count", "number", [], []),
|
||||
component_count: call(this.core, "sim_hal_component_count", "number", [], []),
|
||||
...(includeState ? { native_state: exportNativeCheckpoint(this.core, "sim_hal") } : {}),
|
||||
};
|
||||
return { ...unsigned, state_hash: fnv1a64(unsigned) };
|
||||
}
|
||||
|
||||
restoreHalState(value) {
|
||||
if (!value?.native_state) throw new Error("wasm_hal_native_checkpoint_required");
|
||||
restoreNativeCheckpoint(this.core, "sim_hal", value.native_state);
|
||||
}
|
||||
|
||||
halValue(name, value, type = 4, param = false) {
|
||||
if (!this.available) return { status: "blocked", code: "wasm_hal_unavailable" };
|
||||
if (param || !HAL_WRITE_ALLOWLIST.has(name)) return { status: "error", code: "wasm_hal_write_denied", name, target: param ? "param" : "pin" };
|
||||
|
||||
@@ -16,7 +16,6 @@ const SWITCH = Object.freeze({ switching: 16, active_mode: 20, pending_mode: 24,
|
||||
const TYPE = Object.freeze({ identity: 1, xyzac: 2, xyzbc: 3, userk: 1 });
|
||||
const MODE = Object.freeze({ identity: 0, xyzac: 1, userk: 2 });
|
||||
const AXES = Object.freeze(["x", "y", "z", "a", "b", "c", "u", "v", "w"]);
|
||||
const DEFAULT_LIMITS = Object.freeze({ x: [-200, 200], y: [-100, 100], z: [-120, 120], a: [-100, 50], b: [-100, 50], c: [-36000, 36000] });
|
||||
|
||||
function call(core, name, returnType, argTypes, args) {
|
||||
const direct = core?.[`_${name}`] ?? core?.[name];
|
||||
@@ -44,7 +43,8 @@ function configFor(core, pointer, type, options, coordinates) {
|
||||
view.setUint32(CONFIG.extra_joint_count, 0, true);
|
||||
setCoordinates(core, pointer, coordinates);
|
||||
for (const [index, axis] of coordinates.toLowerCase().split("").entries()) {
|
||||
const limit = options.limits?.[axis] ?? DEFAULT_LIMITS[axis];
|
||||
const limit = options.limits?.[axis];
|
||||
if (!Array.isArray(limit) || limit.length !== 2 || !limit.every(Number.isFinite) || limit[0] >= limit[1]) throw new Error(`wasm_trt_limit_required:${axis}`);
|
||||
core.HEAPU8[pointer + CONFIG.limit_enabled + index] = 1;
|
||||
view.setFloat64(CONFIG.joint_min + index * 8, Number(limit[0]), true);
|
||||
view.setFloat64(CONFIG.joint_max + index * 8, Number(limit[1]), true);
|
||||
@@ -69,11 +69,10 @@ function resultCode(operation, result) {
|
||||
export class WasmXyzacKinematics {
|
||||
constructor(core, options = {}) {
|
||||
this.core = core;
|
||||
this.coordinates = String(options.coordinates ?? "XYZAC").toUpperCase();
|
||||
this.coordinates = String(options.coordinates ?? options.limits?.coordinates ?? "XYZAC").toUpperCase();
|
||||
if (!["XYZAC", "XYZBC"].includes(this.coordinates)) throw new Error("wasm_trt_coordinates_invalid");
|
||||
this.kinematicsType = this.coordinates === "XYZBC" ? "xyzbc-trt-kins" : "xyzac-trt-kins";
|
||||
this.options = { x_rot_point: 0, y_rot_point: 0, z_rot_point: 0, x_offset: 0, y_offset: 20, z_offset: 10, tool_offset: 0, limits: DEFAULT_LIMITS, ...options };
|
||||
if (this.coordinates === "XYZBC" && options.limits === undefined) this.options.limits = { x: [-200, 200], y: [-100, 100], z: [-120, 120], b: [-100, 50], c: [-36000, 36000] };
|
||||
this.options = { x_rot_point: 0, y_rot_point: 0, z_rot_point: 0, x_offset: 0, y_offset: 0, z_offset: 0, tool_offset: 0, ...options };
|
||||
this.available = ["sim_kinematics_init", "sim_kinematics_switch_init", "sim_kinematics_switch_register", "sim_kinematics_switch_select", "sim_kinematics_switch_select_physical", "sim_kinematics_switch_forward", "sim_kinematics_switch_inverse", "sim_kinematics_switch_inverse_mode", "sim_kinematics_switch_active", "sim_kinematics_switch_state_hash"].every((name) => typeof core?.[`_${name}`] === "function" || typeof core?.[name] === "function");
|
||||
this.lastCommand = null;
|
||||
this.modules = new Map();
|
||||
@@ -198,7 +197,7 @@ export class WasmXyzacKinematics {
|
||||
const activeMode = this.activeMode;
|
||||
const nativeType = activeMode === MODE.xyzac ? this.kinematicsType : activeMode === MODE.userk ? "userk-identity-template" : "identity";
|
||||
const view = new DataView(this.core.HEAPU8.buffer, this.switchPointer, SWITCH_SIZE);
|
||||
const limits = Object.fromEntries(this.coordinates.toLowerCase().split("").map((axis) => [axis, [...(this.options.limits?.[axis] ?? DEFAULT_LIMITS[axis])]]));
|
||||
const limits = Object.fromEntries(this.coordinates.toLowerCase().split("").map((axis) => [axis, [...this.options.limits[axis]]]));
|
||||
const unsigned = { schema: "wasm-xyzac-kinematics.v1", active_type: activeMode, native_type: nativeType, coordinates: this.coordinates, joint_count: 5, linear_units: "mm", angular_units: "deg", pose: currentPose, joints: joints.slice(0, 5), offsets: { x_rot_point: this.options.x_rot_point, y_rot_point: this.options.y_rot_point, z_rot_point: this.options.z_rot_point, x_offset: this.options.x_offset, y_offset: this.options.y_offset, z_offset: this.options.z_offset, tool_offset: this.options.tool_offset }, limits, generation: view.getUint32(SWITCH.generation, true), switch_count: view.getUint32(SWITCH.switch_count, true), abort_count: view.getUint32(SWITCH.abort_count, true), last_command: this.lastCommand, capability: { userk: "linked_identity_template", dynamic_modules: "denied" }, source: "linuxcnc-wasm-sim_kinematics_switch" };
|
||||
const hash = call(this.core, "sim_kinematics_switch_state_hash", "number", ["number", "number", "number", "number"], [this.switchPointer, this.jointPointer, 5, this.posePointer]);
|
||||
return { status: "verified", ...unsigned, wasm_state_hash: `fnv1a64:${BigInt.asUintN(64, BigInt(hash >>> 0)).toString(16).padStart(16, "0")}`, state_hash: fnv1a64(unsigned) };
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { SimSession, CONTEXT_ID, PROTOCOL_SCHEMA } from "./sim-session.mjs";
|
||||
import { XyzacCommandLedger, XYZAC_COMMAND_LEDGER_SCHEMA } from "./xyzac-command-ledger.mjs";
|
||||
import { XyzacScheduler, XYZAC_SCHEDULER_SCHEMA } from "./xyzac-scheduler.mjs";
|
||||
import { DEFAULT_MACHINE } from "../workspace/machine-definitions.mjs";
|
||||
|
||||
export const XYZAC_SESSION_SCHEMA = "xyzac-session.v1";
|
||||
|
||||
@@ -13,6 +14,7 @@ export class XyzacSession extends SimSession {
|
||||
}
|
||||
|
||||
authority() {
|
||||
const machine = this.machineDefinition ?? DEFAULT_MACHINE;
|
||||
return {
|
||||
schema: XYZAC_SESSION_SCHEMA,
|
||||
authority: "linuxcnc-wasm-worker",
|
||||
@@ -22,7 +24,7 @@ export class XyzacSession extends SimSession {
|
||||
protocol_schema: PROTOCOL_SCHEMA,
|
||||
ledger_schema: XYZAC_COMMAND_LEDGER_SCHEMA,
|
||||
scheduler_schema: XYZAC_SCHEDULER_SCHEMA,
|
||||
machine: { coordinates: "XYZAC", joint_count: 5, kinematics: "xyzac-trt", rotary_units: "degrees" },
|
||||
machine: { coordinates: machine.coordinates, joint_count: machine.joint_count, kinematics: machine.id, rotary_units: "degrees" },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
35
web/workspace/machine-catalog.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"default_machine": "xyzac-trt",
|
||||
"machines": [
|
||||
{
|
||||
"id": "xyzac-trt",
|
||||
"coordinates": "XYZAC",
|
||||
"kinematics_type": 2,
|
||||
"entrypoint": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
|
||||
"program": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/boat-xyzac.ngc",
|
||||
"switchkins_program": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins.ngc",
|
||||
"source_path": "linuxcnc/src/hal/user_comps/vismach/xyzac-trt-gui.py",
|
||||
"model": "trt-moving-knee",
|
||||
"subroutines": [
|
||||
"xyzac_switchkins_sub",
|
||||
"helix_ac"
|
||||
],
|
||||
"runtime_fixture": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini"
|
||||
},
|
||||
{
|
||||
"id": "xyzbc-trt",
|
||||
"coordinates": "XYZBC",
|
||||
"kinematics_type": 3,
|
||||
"entrypoint": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini",
|
||||
"program": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/boat-xyzbc.ngc",
|
||||
"switchkins_program": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzbc_switchkins.ngc",
|
||||
"source_path": "linuxcnc/src/hal/user_comps/vismach/xyzbc-trt-gui.py",
|
||||
"model": "trt-moving-head",
|
||||
"subroutines": [
|
||||
"xyzbc_switchkins_sub",
|
||||
"helix_bc"
|
||||
],
|
||||
"runtime_fixture": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,25 +1,12 @@
|
||||
const TRT_ROOT = "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting";
|
||||
import catalog from "./machine-catalog.json" with { type: "json" };
|
||||
|
||||
function trt(id, coordinates, type, offsets, model) {
|
||||
const stem = coordinates.toLowerCase();
|
||||
return Object.freeze({
|
||||
id, label: `${coordinates} TRT`, coordinates, axes: [...stem], joint_count: coordinates.length,
|
||||
entrypoint: `${TRT_ROOT}/${id}.ini`, program: `${TRT_ROOT}/demos/boat-${stem}.ngc`,
|
||||
switchkins_program: `${TRT_ROOT}/demos/${stem}_switchkins.ngc`,
|
||||
kinematics_type: type, runtime: "trt", model,
|
||||
source_path: `linuxcnc/src/hal/user_comps/vismach/${id}-gui.py`,
|
||||
subroutines: [`${stem}_switchkins_sub`, `helix_${stem.slice(3)}`],
|
||||
offsets,
|
||||
joint_limits: { x: [-200, 200], y: [-100, 100], z: [-120, 120], [stem[3]]: [-100, 50], c: [-36000, 36000] },
|
||||
axis_limits: { x: [-200, 200], y: [-100, 100], z: [-120, 120], [stem[3]]: coordinates === "XYZAC" ? [-100, 50] : [-36000, 36000], c: [-36000, 36000] },
|
||||
});
|
||||
}
|
||||
|
||||
export const MACHINE_DEFINITIONS = Object.freeze([
|
||||
trt("xyzac-trt", "XYZAC", 2, { x_rot_point: 0, y_offset: 20, z_offset: 10, tool_offset: 0 }, "trt-moving-knee"),
|
||||
trt("xyzbc-trt", "XYZBC", 3, { x_rot_point: 0, y_rot_point: 0, z_rot_point: 0, x_offset: -20, y_offset: 0, z_offset: -15, tool_offset: 0 }, "trt-moving-head"),
|
||||
]);
|
||||
export const DEFAULT_MACHINE = MACHINE_DEFINITIONS[0];
|
||||
export const MACHINE_DEFINITIONS = Object.freeze(catalog.machines.map((entry) => {
|
||||
if (!entry.id || !entry.entrypoint || !entry.program || !entry.source_path || !["XYZAC", "XYZBC"].includes(entry.coordinates)) throw new Error("machine_catalog_invalid");
|
||||
return Object.freeze({ ...entry, label: `${entry.coordinates} TRT`, axes: [...entry.coordinates.toLowerCase()], joint_count: entry.coordinates.length, runtime: "trt" });
|
||||
}));
|
||||
if (new Set(MACHINE_DEFINITIONS.map((entry) => entry.id)).size !== MACHINE_DEFINITIONS.length) throw new Error("machine_catalog_duplicate");
|
||||
export const DEFAULT_MACHINE = MACHINE_DEFINITIONS.find((entry) => entry.id === catalog.default_machine);
|
||||
if (!DEFAULT_MACHINE) throw new Error("machine_catalog_default_missing");
|
||||
export function machineDefinition(value) {
|
||||
return MACHINE_DEFINITIONS.find((entry) => [entry.id, entry.entrypoint, entry.coordinates].includes(value)) ?? null;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@ import { runtimeResourceFixture } from "./resource-resolver.mjs";
|
||||
import { SIM_CASE_CATALOG, SIM_FILE_INDEX, SIM_HAL_COMPONENTS } from "./sim-case-catalog.mjs";
|
||||
import { isOptionalSourceKey } from "./linuxcnc-source-semantics.mjs";
|
||||
import { MACHINE_DEFINITIONS, machineDefinition } from "./machine-definitions.mjs";
|
||||
import { readTrtGeometry } from "./trt-geometry.mjs";
|
||||
import resources from "./simulation-resources.json" with { type: "json" };
|
||||
import { parseIni } from "./ini-parser.mjs";
|
||||
import { WorkspaceRuntime } from "./runtime.mjs";
|
||||
import { readTrtConfiguration } from "./trt-configuration.mjs";
|
||||
|
||||
export const SIMULATION_CASE_SCHEMA = "simulation-case-bundle.v1";
|
||||
const RESOURCE_KEYS = new Set([
|
||||
@@ -12,53 +15,9 @@ const RESOURCE_KEYS = new Set([
|
||||
"PYVCP", "NGCGUI_SUBFILE", "INTRO_GRAPHIC", "SUBROUTINE_PATH", "REMAP",
|
||||
]);
|
||||
const MANIFEST = new Set(SIM_FILE_INDEX);
|
||||
const PRELOADED_HAL_COMPONENTS = Object.freeze(["corexykins", "lineardeltakins", "motmod", "pid", "rotarydeltakins", "stepgen", "trivkins", "xyzac-trt-kins", "xyzbc-trt-kins", "tpmod", "homemod"]);
|
||||
const CASE_KINEMATICS = Object.freeze({
|
||||
"linuxcnc/configs/sim/axis/corexy/corexy_by_kins.ini": Object.freeze({ type: "corexy", path_mode: "kins" }),
|
||||
"linuxcnc/configs/sim/axis/corexy/corexy_by_hal.ini": Object.freeze({ type: "corexy", path_mode: "hal" }),
|
||||
"linuxcnc/configs/sim/axis/ldelta.ini": Object.freeze({ type: "lineardelta", path_mode: "kins" }),
|
||||
"linuxcnc/configs/sim/axis/rdelta.ini": Object.freeze({ type: "rotarydelta", path_mode: "kins" }),
|
||||
"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini": Object.freeze({ type: "xyzac-trt", path_mode: "wasm", coordinates: "XYZAC", joint_count: 5, max_linear_velocity: 35, axis_linear_velocity_limit: 20, max_acceleration: 300, max_jerk: 1000000000, servo_cycle_time: 0.001, task_cycle_time: 0.01, cycle_time: 0.001, switchkins: { 0: "identity", 1: "xyzac", 2: "userk" } }),
|
||||
"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini": Object.freeze({ type: "xyzbc-trt", path_mode: "wasm", coordinates: "XYZBC", joint_count: 5, max_linear_velocity: 35, axis_linear_velocity_limit: 20, axis_angular_velocity_limit: 30, max_acceleration: 300, max_jerk: 1000000000, servo_cycle_time: 0.001, task_cycle_time: 0.01, cycle_time: 0.001, switchkins: { 0: "identity", 1: "xyzbc", 2: "userk" } }),
|
||||
});
|
||||
const RUNTIME_FIXTURES = Object.freeze({
|
||||
"linuxcnc/configs/sim/axis/minimal_xyz.ini": Object.freeze({
|
||||
generated_hal: "linuxcnc/configs/sim/axis/minimal_xyz_cmds.hal",
|
||||
replace: "HALFILE = LIB:basic_sim.tcl",
|
||||
with: "HALFILE = minimal_xyz_cmds.hal",
|
||||
reason: "LinuxCNC basic_sim.tcl generated HAL command fixture",
|
||||
}),
|
||||
"linuxcnc/configs/sim/axis/ja_tests/xz/xz.ini": Object.freeze({
|
||||
generated_hal: "linuxcnc/configs/sim/axis/ja_tests/xz/xz_cmds.hal",
|
||||
replace: "HALFILE = LIB:basic_sim.tcl",
|
||||
with: "HALFILE = xz_cmds.hal",
|
||||
reason: "LinuxCNC basic_sim.tcl generated XZ HAL command fixture",
|
||||
}),
|
||||
"linuxcnc/configs/sim/axis/ja_tests/xz/xz_both.ini": Object.freeze({
|
||||
generated_hal: "linuxcnc/configs/sim/axis/ja_tests/xz/xz_both_cmds.hal",
|
||||
replace: "HALFILE = LIB:basic_sim.tcl",
|
||||
with: "HALFILE = xz_both_cmds.hal",
|
||||
reason: "LinuxCNC basic_sim.tcl generated XZ BOTH HAL command fixture",
|
||||
}),
|
||||
"linuxcnc/configs/sim/axis/ja_tests/xz/xz_both_auto.ini": Object.freeze({
|
||||
generated_hal: "linuxcnc/configs/sim/axis/ja_tests/xz/xz_both_auto_cmds.hal",
|
||||
replace: "HALFILE = LIB:basic_sim.tcl",
|
||||
with: "HALFILE = xz_both_auto_cmds.hal",
|
||||
reason: "LinuxCNC basic_sim.tcl generated XZ BOTH AUTO HAL command fixture",
|
||||
}),
|
||||
"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini": Object.freeze({
|
||||
generated_hal: "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt-headless_cmds.hal",
|
||||
replace: "HALFILE = LIB:basic_sim.tcl",
|
||||
with: "HALFILE = xyzac-trt-headless_cmds.hal",
|
||||
reason: "LinuxCNC basic_sim.tcl generated XYZAC HAL command fixture",
|
||||
}),
|
||||
"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini": Object.freeze({
|
||||
generated_hal: "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt_cmds.hal",
|
||||
replace: "HALFILE = LIB:basic_sim.tcl",
|
||||
with: "HALFILE = xyzbc-trt_cmds.hal",
|
||||
reason: "LinuxCNC basic_sim.tcl generated XYZBC HAL command fixture",
|
||||
}),
|
||||
});
|
||||
const PRELOADED_HAL_COMPONENTS = Object.freeze(resources.preloaded_hal_components);
|
||||
const CASE_KINEMATICS = Object.freeze(resources.kinematics);
|
||||
const RUNTIME_FIXTURES = Object.freeze(resources.runtime_fixtures);
|
||||
|
||||
function clone(value) { return structuredClone(value); }
|
||||
function text(value) { return typeof value === "string" && value.trim() ? value.trim() : null; }
|
||||
@@ -180,7 +139,8 @@ function resourceReferences(source, content) {
|
||||
}
|
||||
|
||||
export function findSimulationCase(path) {
|
||||
return SIM_CASE_CATALOG.find((entry) => entry.path === path) ?? null;
|
||||
const registered = machineDefinition(path);
|
||||
return SIM_CASE_CATALOG.find((entry) => entry.path === path) ?? (registered ? { path: registered.entrypoint, coordinates: registered.coordinates, label: registered.label, status: "registered" } : null);
|
||||
}
|
||||
|
||||
export async function loadSimulationCase({ path, fetchImpl = globalThis.fetch, baseUrl = "" } = {}) {
|
||||
@@ -243,7 +203,7 @@ export async function loadSimulationCase({ path, fetchImpl = globalThis.fetch, b
|
||||
}
|
||||
}
|
||||
const sourceIni = files[catalog.path];
|
||||
const fixture = RUNTIME_FIXTURES[catalog.path];
|
||||
const fixture = RUNTIME_FIXTURES[catalog.path] ?? RUNTIME_FIXTURES[machine?.runtime_fixture];
|
||||
if (fixture) {
|
||||
if (!inManifest(fixture.generated_hal)) missing.push(fixture.generated_hal);
|
||||
else if (!files[fixture.generated_hal]) {
|
||||
@@ -280,7 +240,23 @@ export async function loadSimulationCase({ path, fetchImpl = globalThis.fetch, b
|
||||
export function toWorkerConfiguration(bundle) {
|
||||
if (!bundle || bundle.schema !== SIMULATION_CASE_SCHEMA) throw new Error("simulation_case_bundle_invalid");
|
||||
const kinematics = CASE_KINEMATICS[bundle.entrypoint] ? clone(CASE_KINEMATICS[bundle.entrypoint]) : null;
|
||||
if (machineDefinition(bundle.entrypoint)) kinematics.offsets = readTrtGeometry(bundle).offsets;
|
||||
const trt = machineDefinition(bundle.entrypoint) ? readTrtConfiguration(bundle) : null;
|
||||
let homeLimits = null;
|
||||
if (!trt) {
|
||||
const parsed = parseIni({ workspace: new WorkspaceRuntime({ files: bundle.files }), entrypoint: bundle.entrypoint });
|
||||
const coordinates = parsed.sections.TRAJ?.COORDINATES?.replace(/\s/g, "").toUpperCase();
|
||||
if (coordinates && /^[XYZABCUVW]+$/.test(coordinates)) {
|
||||
const limits = {};
|
||||
for (const [index, axis] of [...coordinates].entries()) {
|
||||
const section = parsed.sections[`JOINT_${index}`] ?? parsed.sections[`AXIS_${axis}`];
|
||||
if (!section || section.MIN_LIMIT === undefined || section.MAX_LIMIT === undefined) continue;
|
||||
const range = [Number(section.MIN_LIMIT), Number(section.MAX_LIMIT)];
|
||||
if (!range.every(Number.isFinite) || range[0] >= range[1]) throw new Error(`configuration_joint_limits:${index}`);
|
||||
limits[axis.toLowerCase()] = range;
|
||||
}
|
||||
if (Object.keys(limits).length) homeLimits = { coordinates, joint_count: coordinates.length, ...limits };
|
||||
}
|
||||
}
|
||||
return {
|
||||
entrypoint: bundle.entrypoint,
|
||||
files: clone(bundle.files),
|
||||
@@ -288,9 +264,8 @@ export function toWorkerConfiguration(bundle) {
|
||||
state_hash: bundle.state_hash,
|
||||
optional_missing: clone(bundle.optional_missing ?? []),
|
||||
runtime_fixtures: clone(bundle.runtime_fixtures ?? []),
|
||||
...(bundle.entrypoint === "linuxcnc/configs/sim/axis/vismach/3axis-tutorial/3axis-tutorial.ini" ? { home_limits: { z: [-240, 100] } } : {}),
|
||||
...(machineDefinition(bundle.entrypoint) ? { home_limits: { joint_count: 5, kinematics_type: machineDefinition(bundle.entrypoint).kinematics_type, coordinates: machineDefinition(bundle.entrypoint).coordinates, ...machineDefinition(bundle.entrypoint).joint_limits } } : {}),
|
||||
...(kinematics ? { kinematics } : {}),
|
||||
...(homeLimits ? { home_limits: homeLimits } : {}),
|
||||
...(trt ?? (kinematics ? { kinematics } : {})),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
71
web/workspace/simulation-resources.json
Normal file
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"kinematics": {
|
||||
"linuxcnc/configs/sim/axis/corexy/corexy_by_kins.ini": {
|
||||
"type": "corexy",
|
||||
"path_mode": "kins"
|
||||
},
|
||||
"linuxcnc/configs/sim/axis/corexy/corexy_by_hal.ini": {
|
||||
"type": "corexy",
|
||||
"path_mode": "hal"
|
||||
},
|
||||
"linuxcnc/configs/sim/axis/ldelta.ini": {
|
||||
"type": "lineardelta",
|
||||
"path_mode": "kins"
|
||||
},
|
||||
"linuxcnc/configs/sim/axis/rdelta.ini": {
|
||||
"type": "rotarydelta",
|
||||
"path_mode": "kins"
|
||||
}
|
||||
},
|
||||
"runtime_fixtures": {
|
||||
"linuxcnc/configs/sim/axis/minimal_xyz.ini": {
|
||||
"generated_hal": "linuxcnc/configs/sim/axis/minimal_xyz_cmds.hal",
|
||||
"replace": "HALFILE = LIB:basic_sim.tcl",
|
||||
"with": "HALFILE = minimal_xyz_cmds.hal",
|
||||
"reason": "LinuxCNC basic_sim.tcl generated HAL command fixture"
|
||||
},
|
||||
"linuxcnc/configs/sim/axis/ja_tests/xz/xz.ini": {
|
||||
"generated_hal": "linuxcnc/configs/sim/axis/ja_tests/xz/xz_cmds.hal",
|
||||
"replace": "HALFILE = LIB:basic_sim.tcl",
|
||||
"with": "HALFILE = xz_cmds.hal",
|
||||
"reason": "LinuxCNC basic_sim.tcl generated XZ HAL command fixture"
|
||||
},
|
||||
"linuxcnc/configs/sim/axis/ja_tests/xz/xz_both.ini": {
|
||||
"generated_hal": "linuxcnc/configs/sim/axis/ja_tests/xz/xz_both_cmds.hal",
|
||||
"replace": "HALFILE = LIB:basic_sim.tcl",
|
||||
"with": "HALFILE = xz_both_cmds.hal",
|
||||
"reason": "LinuxCNC basic_sim.tcl generated XZ BOTH HAL command fixture"
|
||||
},
|
||||
"linuxcnc/configs/sim/axis/ja_tests/xz/xz_both_auto.ini": {
|
||||
"generated_hal": "linuxcnc/configs/sim/axis/ja_tests/xz/xz_both_auto_cmds.hal",
|
||||
"replace": "HALFILE = LIB:basic_sim.tcl",
|
||||
"with": "HALFILE = xz_both_auto_cmds.hal",
|
||||
"reason": "LinuxCNC basic_sim.tcl generated XZ BOTH AUTO HAL command fixture"
|
||||
},
|
||||
"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini": {
|
||||
"generated_hal": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt-headless_cmds.hal",
|
||||
"replace": "HALFILE = LIB:basic_sim.tcl",
|
||||
"with": "HALFILE = xyzac-trt-headless_cmds.hal",
|
||||
"reason": "LinuxCNC basic_sim.tcl generated XYZAC HAL command fixture"
|
||||
},
|
||||
"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini": {
|
||||
"generated_hal": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt_cmds.hal",
|
||||
"replace": "HALFILE = LIB:basic_sim.tcl",
|
||||
"with": "HALFILE = xyzbc-trt_cmds.hal",
|
||||
"reason": "LinuxCNC basic_sim.tcl generated XYZBC HAL command fixture"
|
||||
}
|
||||
},
|
||||
"preloaded_hal_components": [
|
||||
"corexykins",
|
||||
"lineardeltakins",
|
||||
"motmod",
|
||||
"pid",
|
||||
"rotarydeltakins",
|
||||
"stepgen",
|
||||
"trivkins",
|
||||
"xyzac-trt-kins",
|
||||
"xyzbc-trt-kins",
|
||||
"tpmod",
|
||||
"homemod"
|
||||
]
|
||||
}
|
||||
74
web/workspace/trt-configuration.mjs
Normal file
@@ -0,0 +1,74 @@
|
||||
import { parseIni } from "./ini-parser.mjs";
|
||||
import { WorkspaceRuntime } from "./runtime.mjs";
|
||||
import { machineDefinition } from "./machine-definitions.mjs";
|
||||
import { readTrtGeometry } from "./trt-geometry.mjs";
|
||||
import { WEB_SETTINGS } from "./web-settings.mjs";
|
||||
|
||||
const NUMBER = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
|
||||
// ABI capacity constraints from sim_io.h and sim_device_io.h, not machine defaults.
|
||||
const IO_CAPACITY = Object.freeze({ spindle_count: 8, tool_capacity: 1001, encoder_count: 8 });
|
||||
export function readTrtConfiguration(configuration) {
|
||||
const machine = machineDefinition(configuration.entrypoint);
|
||||
if (!machine) throw new Error("trt_configuration_machine_missing");
|
||||
const parsed = parseIni({ workspace: new WorkspaceRuntime({ files: configuration.files }), entrypoint: configuration.entrypoint });
|
||||
const sections = parsed.sections;
|
||||
const number = (section, key, { positive = false, fallback } = {}) => {
|
||||
const raw = sections[section]?.[key];
|
||||
if (raw === undefined && fallback !== undefined) return fallback;
|
||||
if (typeof raw !== "string" || !NUMBER.test(raw) || !Number.isFinite(Number(raw)) || positive && Number(raw) <= 0) throw new Error(`trt_configuration_number:[${section}]${key}`);
|
||||
return Number(raw);
|
||||
};
|
||||
const geometry = readTrtGeometry(configuration);
|
||||
const coordinates = sections.TRAJ.COORDINATES.replace(/\s/g, "").toUpperCase();
|
||||
const axes = [...coordinates.toLowerCase()];
|
||||
const jointCount = number("KINS", "JOINTS", { positive: true });
|
||||
if (jointCount !== axes.length) throw new Error("trt_configuration_joint_count");
|
||||
if (sections.TRAJ.LINEAR_UNITS !== "mm" || sections.TRAJ.ANGULAR_UNITS !== "deg") throw new Error("trt_configuration_units_unsupported");
|
||||
if (!/\bsparm=identityfirst\b/.test(sections.KINS.KINEMATICS)) throw new Error("trt_configuration_requires_identityfirst");
|
||||
const limits = (section) => {
|
||||
const range = [number(section, "MIN_LIMIT"), number(section, "MAX_LIMIT")];
|
||||
if (range[0] >= range[1]) throw new Error(`trt_configuration_limits:${section}`);
|
||||
return range;
|
||||
};
|
||||
const jointLimits = {}, axisLimits = {}, velocity = {}, acceleration = {};
|
||||
for (const [joint, axis] of axes.entries()) {
|
||||
jointLimits[axis] = limits(`JOINT_${joint}`);
|
||||
axisLimits[axis] = limits(`AXIS_${axis.toUpperCase()}`);
|
||||
velocity[axis] = number(`AXIS_${axis.toUpperCase()}`, "MAX_VELOCITY", { positive: true });
|
||||
acceleration[axis] = number(`AXIS_${axis.toUpperCase()}`, "MAX_ACCELERATION", { positive: true });
|
||||
number(`JOINT_${joint}`, "MAX_VELOCITY", { positive: true });
|
||||
number(`JOINT_${joint}`, "MAX_ACCELERATION", { positive: true });
|
||||
for (const key of ["HOME", "HOME_OFFSET", "HOME_FINAL_VEL", "HOME_SEARCH_VEL", "HOME_LATCH_VEL", "HOME_SEQUENCE"]) if (sections[`JOINT_${joint}`][key] !== undefined) number(`JOINT_${joint}`, key);
|
||||
}
|
||||
const servo = number("EMCMOT", "SERVO_PERIOD", { positive: true }) / 1e9;
|
||||
const task = number("TASK", "CYCLE_TIME", { positive: true });
|
||||
const ticks = task / servo;
|
||||
if (!Number.isSafeInteger(Math.round(ticks)) || ticks < 1 || Math.abs(ticks - Math.round(ticks)) > 1e-9) throw new Error("trt_configuration_task_servo_ratio");
|
||||
if (servo > 60 || task > 10) throw new Error("trt_configuration_cycle_capacity");
|
||||
const maxFeed = number("DISPLAY", "MAX_FEED_OVERRIDE", { positive: true });
|
||||
if (maxFeed < 1) throw new Error("trt_configuration_feed_override");
|
||||
const blendKeys = { enable: "ARC_BLEND_ENABLE", fallback_enable: "ARC_BLEND_FALLBACK_ENABLE", optimization_depth: "ARC_BLEND_OPTIMIZATION_DEPTH", gap_cycles: "ARC_BLEND_GAP_CYCLES", ramp_frequency: "ARC_BLEND_RAMP_FREQ", tangent_kink_ratio: "ARC_BLEND_TANGENT_KINK_RATIO" };
|
||||
const blend = Object.fromEntries(Object.entries(blendKeys).map(([key, iniKey]) => [key, number("TRAJ", iniKey, { fallback: WEB_SETTINGS.runtime.planner.blend[key] })]));
|
||||
if (![0, 1].includes(blend.enable) || ![0, 1].includes(blend.fallback_enable) || !Number.isSafeInteger(blend.optimization_depth) || blend.optimization_depth < 1 || !Number.isSafeInteger(blend.gap_cycles) || blend.gap_cycles < 0 || blend.ramp_frequency <= 0 || blend.tangent_kink_ratio <= 0) throw new Error("trt_configuration_blend");
|
||||
const io = { ...Object.fromEntries(Object.entries(WEB_SETTINGS.runtime.io).filter(([key]) => key !== "cycle_time").map(([key, fallback]) => [key, number("WEB_RUNTIME", key.toUpperCase(), { positive: key !== "spindle_tolerance", fallback })])), cycle_time: task };
|
||||
if (io.spindle_tolerance < 0) throw new Error("trt_configuration_spindle_tolerance");
|
||||
for (const [key, maximum] of Object.entries(IO_CAPACITY)) if (!Number.isSafeInteger(io[key]) || io[key] < 1 || io[key] > maximum) throw new Error(`trt_configuration_io_capacity:${key}`);
|
||||
const defaultAcceleration = number("TRAJ", "DEFAULT_LINEAR_ACCELERATION", { positive: true });
|
||||
const maxAcceleration = number("TRAJ", "MAX_LINEAR_ACCELERATION", { positive: true });
|
||||
return {
|
||||
home_limits: { joint_count: jointCount, kinematics_type: machine.kinematics_type, coordinates, ...jointLimits },
|
||||
kinematics: {
|
||||
type: machine.id, path_mode: "wasm", coordinates, joint_count: jointCount, offsets: geometry.offsets,
|
||||
ini_hash: parsed.state_hash,
|
||||
servo_cycle_time: servo, cycle_time: servo, task_cycle_time: task,
|
||||
max_linear_velocity: number("TRAJ", "MAX_LINEAR_VELOCITY", { positive: true }),
|
||||
max_acceleration: Math.min(defaultAcceleration, maxAcceleration),
|
||||
max_linear_acceleration: maxAcceleration,
|
||||
axis_limits: axisLimits, axis_velocity_limits: velocity, axis_acceleration_limits: acceleration,
|
||||
max_feed_override: maxFeed,
|
||||
blend,
|
||||
io,
|
||||
switchkins: { 0: "identity", 1: coordinates.toLowerCase(), 2: "userk" },
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -12,7 +12,7 @@ export function readTrtGeometry({ entrypoint, files }) {
|
||||
const machine = machineDefinition(entrypoint);
|
||||
if (!machine) fail("trt_geometry_machine", entrypoint);
|
||||
const parsed = parseIni({ workspace: new WorkspaceRuntime({ files }), entrypoint });
|
||||
if (parsed.sections.TRAJ?.COORDINATES?.toUpperCase() !== machine.coordinates || parsed.sections.KINS?.KINEMATICS?.split(/\s+/)[0] !== `${machine.id}-kins`) fail("trt_geometry_machine_mismatch", entrypoint);
|
||||
if (parsed.sections.TRAJ?.COORDINATES?.replace(/\s/g, "").toUpperCase() !== machine.coordinates || parsed.sections.KINS?.KINEMATICS?.split(/\s+/)[0] !== `${machine.id}-kins`) fail("trt_geometry_machine_mismatch", entrypoint);
|
||||
const commands = parsed.entries.filter((e) => e.section.toUpperCase() === "HAL" && e.key.toUpperCase() === "HALCMD")
|
||||
.map((entry) => ({ entry, record: parseHalLine(entry.value, entry.line, parsed.sections) }));
|
||||
const unsupported = machine.coordinates === "XYZAC" ? "x_offset" : "y_offset";
|
||||
|
||||
180
web/workspace/web-settings.json
Normal file
@@ -0,0 +1,180 @@
|
||||
{
|
||||
"assets": {
|
||||
"axis_images": "linuxcnc/share/axis/images"
|
||||
},
|
||||
"ui": {
|
||||
"poll_ms": 50,
|
||||
"jog_cycles": 1,
|
||||
"run_cycles": 5,
|
||||
"preview_progress_ms": 100,
|
||||
"jog_increments": [
|
||||
"0.1000",
|
||||
"0.0100",
|
||||
"0.0010",
|
||||
"0.0001"
|
||||
],
|
||||
"initial_view": "p",
|
||||
"zoom": 1,
|
||||
"zoom_min": 0.5,
|
||||
"zoom_max": 2,
|
||||
"zoom_step": 0.2,
|
||||
"layers": {
|
||||
"grid": true,
|
||||
"tool": true,
|
||||
"path": true,
|
||||
"live": true,
|
||||
"limits": false,
|
||||
"velocity": false,
|
||||
"offsets": false,
|
||||
"extents": false
|
||||
},
|
||||
"motion_scale_min": 0.06,
|
||||
"override_debounce_ms": 80
|
||||
},
|
||||
"runtime": {
|
||||
"planner": {
|
||||
"cycle_time": 0.001,
|
||||
"velocity_limit": 35,
|
||||
"acceleration_limit": 300,
|
||||
"max_feed_override": 2,
|
||||
"compatibility_jerk_limit": 1000000000,
|
||||
"blend": {
|
||||
"enable": 1,
|
||||
"fallback_enable": 0,
|
||||
"optimization_depth": 50,
|
||||
"gap_cycles": 4,
|
||||
"ramp_frequency": 100,
|
||||
"tangent_kink_ratio": 0.1
|
||||
}
|
||||
},
|
||||
"io": {
|
||||
"spindle_count": 2,
|
||||
"tool_capacity": 1001,
|
||||
"cycle_time": 0.01,
|
||||
"spindle_acceleration": 600,
|
||||
"spindle_tolerance": 5,
|
||||
"encoder_count": 2
|
||||
},
|
||||
"compatibility": {
|
||||
"step_fraction": 0.25,
|
||||
"max_position": 1000000000.0,
|
||||
"max_speed": 1000000.0,
|
||||
"cycle_time": 0.001,
|
||||
"tripod_base": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
10,
|
||||
0
|
||||
],
|
||||
[
|
||||
5,
|
||||
7.071
|
||||
]
|
||||
],
|
||||
"linear_delta": {
|
||||
"radius": 130.25,
|
||||
"rod": 269
|
||||
},
|
||||
"rotary_delta": {
|
||||
"platform": 10,
|
||||
"thigh": 10,
|
||||
"shin": 14,
|
||||
"foot": 6
|
||||
}
|
||||
}
|
||||
},
|
||||
"viewer": {
|
||||
"mm_scale": 0.01,
|
||||
"pixel_ratio_limit": 2,
|
||||
"camera": {
|
||||
"fov": 36,
|
||||
"near": 0.02,
|
||||
"far": 150,
|
||||
"min_distance": 0.25,
|
||||
"max_distance": 65,
|
||||
"damping": 0.12,
|
||||
"fit_margin": 1.12,
|
||||
"pan_fraction": 0.035,
|
||||
"zoom_in": 0.8,
|
||||
"zoom_out": 1.25,
|
||||
"rotate_degrees": 15
|
||||
},
|
||||
"colors": {
|
||||
"background": "#252b2c",
|
||||
"path": "#ffff00",
|
||||
"live": "#ff9a55",
|
||||
"rapid": "#5c9d9a",
|
||||
"limits": "#dd7566",
|
||||
"extents": "#e6c56b"
|
||||
},
|
||||
"models": {
|
||||
"XYZAC": {
|
||||
"datum_z_mm": 200
|
||||
},
|
||||
"XYZBC": {
|
||||
"datum_z_mm": 150
|
||||
}
|
||||
},
|
||||
"materials": {
|
||||
"enamel": {
|
||||
"color": "#d9e1e1",
|
||||
"roughness": 0.32,
|
||||
"metalness": 0.25
|
||||
},
|
||||
"teal": {
|
||||
"color": "#167f79",
|
||||
"roughness": 0.3,
|
||||
"metalness": 0.3
|
||||
},
|
||||
"dark": {
|
||||
"color": "#303638",
|
||||
"roughness": 0.42,
|
||||
"metalness": 0.35
|
||||
},
|
||||
"steel": {
|
||||
"color": "#b2bdc4",
|
||||
"roughness": 0.27,
|
||||
"metalness": 0.85
|
||||
},
|
||||
"polished": {
|
||||
"color": "#e2e8ec",
|
||||
"roughness": 0.19,
|
||||
"metalness": 0.9
|
||||
},
|
||||
"recess": {
|
||||
"color": "#111718",
|
||||
"roughness": 0.66,
|
||||
"metalness": 0.2
|
||||
},
|
||||
"rubber": {
|
||||
"color": "#222729",
|
||||
"roughness": 0.88
|
||||
},
|
||||
"carbide": {
|
||||
"color": "#b9aa79",
|
||||
"roughness": 0.29,
|
||||
"metalness": 0.8
|
||||
},
|
||||
"warning": {
|
||||
"color": "#e7bd48",
|
||||
"roughness": 0.48,
|
||||
"metalness": 0.12
|
||||
},
|
||||
"lamp": {
|
||||
"color": "#e2f8ff",
|
||||
"emissive": "#d4f2ff",
|
||||
"emissiveIntensity": 1.5
|
||||
}
|
||||
},
|
||||
"toolpath_colors": {
|
||||
"background": "#050a0e",
|
||||
"completed": "#d8dfe3",
|
||||
"rapid": "#a85d5d",
|
||||
"active": "#00ffff",
|
||||
"live": "#ff6b4a"
|
||||
}
|
||||
}
|
||||
}
|
||||
34
web/workspace/web-settings.mjs
Normal file
@@ -0,0 +1,34 @@
|
||||
import values from "./web-settings.json" with { type: "json" };
|
||||
|
||||
function freeze(value) {
|
||||
if (value && typeof value === "object") { Object.values(value).forEach(freeze); Object.freeze(value); }
|
||||
return value;
|
||||
}
|
||||
function finitePositive(value, name) {
|
||||
if (!Number.isFinite(value) || value <= 0) throw new Error(`web_settings_invalid:${name}`);
|
||||
}
|
||||
for (const [name, value] of Object.entries(values.runtime.planner)) if (name !== "blend") finitePositive(value, `planner.${name}`);
|
||||
for (const [name, value] of Object.entries(values.runtime.io)) {
|
||||
if (name === "spindle_tolerance") {
|
||||
if (!Number.isFinite(value) || value < 0) throw new Error("web_settings_invalid:io.spindle_tolerance");
|
||||
} else finitePositive(value, `io.${name}`);
|
||||
}
|
||||
for (const [name, value] of Object.entries(values.viewer.camera)) finitePositive(value, `camera.${name}`);
|
||||
finitePositive(values.viewer.mm_scale, "mm_scale");
|
||||
finitePositive(values.viewer.pixel_ratio_limit, "pixel_ratio_limit");
|
||||
for (const key of ["poll_ms", "jog_cycles", "run_cycles", "preview_progress_ms", "zoom", "zoom_min", "zoom_max", "zoom_step"]) finitePositive(values.ui[key], key);
|
||||
finitePositive(values.ui.override_debounce_ms, "override_debounce_ms");
|
||||
if (!(values.ui.motion_scale_min > 0 && values.ui.motion_scale_min <= 1)) throw new Error("web_settings_invalid:motion_scale_min");
|
||||
if (!values.ui.jog_increments.every((value) => Number.isFinite(Number(value)) && Number(value) > 0)) throw new Error("web_settings_invalid:jog_increments");
|
||||
if (values.ui.zoom_min > values.ui.zoom_max || values.ui.zoom < values.ui.zoom_min || values.ui.zoom > values.ui.zoom_max) throw new Error("web_settings_invalid:zoom_range");
|
||||
if (!["x", "y", "y2", "z", "z2", "p"].includes(values.ui.initial_view)) throw new Error("web_settings_invalid:initial_view");
|
||||
for (const key of ["jog_cycles", "run_cycles"]) if (!Number.isSafeInteger(values.ui[key])) throw new Error(`web_settings_invalid:${key}`);
|
||||
if (values.viewer.camera.near >= values.viewer.camera.far || values.viewer.camera.min_distance >= values.viewer.camera.max_distance) throw new Error("web_settings_invalid:camera_range");
|
||||
for (const model of Object.values(values.viewer.models)) finitePositive(model.datum_z_mm, "model_datum");
|
||||
if (values.runtime.planner.max_feed_override < 1) throw new Error("web_settings_invalid:feed_limit");
|
||||
for (const key of ["spindle_count", "tool_capacity", "encoder_count"]) if (!Number.isSafeInteger(values.runtime.io[key])) throw new Error(`web_settings_invalid:io.${key}`);
|
||||
export const WEB_SETTINGS = freeze(values);
|
||||
|
||||
export function axisImagePath(name, base = ".") {
|
||||
return `${base.replace(/\/$/, "")}/${WEB_SETTINGS.assets.axis_images}/${name}`;
|
||||
}
|
||||