✨ feat(fanuc): 打通飞拍轨迹完整执行链路
* 增加 J519 稠密发送采样校验与保姿回发逻辑 * 调整 saveTrajectory 导出与 sequence buffer 行为 * 补充 10010 解析脚本、ICSP 说明和回归测试
This commit is contained in:
@@ -11,7 +11,32 @@
|
|||||||
"Bash(python -c \"import json; json.load\\(open\\('.vscode/launch.json', encoding='utf-8'\\)\\); json.load\\(open\\('.vscode/tasks.json', encoding='utf-8'\\)\\); print\\('JSON valid.'\\)\")",
|
"Bash(python -c \"import json; json.load\\(open\\('.vscode/launch.json', encoding='utf-8'\\)\\); json.load\\(open\\('.vscode/tasks.json', encoding='utf-8'\\)\\); print\\('JSON valid.'\\)\")",
|
||||||
"Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet build FlyshotReplacement.sln --no-restore -v minimal')",
|
"Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet build FlyshotReplacement.sln --no-restore -v minimal')",
|
||||||
"Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet test tests/Flyshot.Core.Tests/Flyshot.Core.Tests.csproj -v minimal')",
|
"Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet test tests/Flyshot.Core.Tests/Flyshot.Core.Tests.csproj -v minimal')",
|
||||||
"Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet test tests/Flyshot.Server.IntegrationTests/Flyshot.Server.IntegrationTests.csproj -v minimal')"
|
"Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet test tests/Flyshot.Server.IntegrationTests/Flyshot.Server.IntegrationTests.csproj -v minimal')",
|
||||||
|
"Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet build FlyshotReplacement.sln --no-restore -v minimal 2>&1')",
|
||||||
|
"Bash(taskkill /PID 120812 /F)",
|
||||||
|
"Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet build flyshot-replacement.sln --no-restore -v minimal 2>&1')",
|
||||||
|
"Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet build /d/Dev/Codes/rvbust-code/FlyingShotPkg_3.15_VDA/flyshot-replacement/FlyshotReplacement.sln --no-restore -v minimal 2>&1')",
|
||||||
|
"Bash(capinfos \"20260430.pcap\")",
|
||||||
|
"Bash(tshark -r \"20260430.pcap\" -T fields -e frame.number -e frame.time_relative -e ip.src -e ip.dst -e udp.srcport -e udp.dstport -e data.len)",
|
||||||
|
"Bash(tshark -r \"20260430.pcap\" -Y \"udp.port==60015\" -T fields -e frame.number -e frame.time_relative -e ip.src -e udp.srcport -e udp.length -e data.len)",
|
||||||
|
"Bash(tshark -r \"20260430.pcap\" -Y \"udp.port==60015 and udp.srcport==60015\" -T fields -e frame.number -e frame.time_relative -e udp.length)",
|
||||||
|
"Bash(tshark -r \"20260430.pcap\" -Y \"udp.port==60015 and udp.dstport==60015\" -T fields -e frame.number -e frame.time_relative -e udp.length)",
|
||||||
|
"Bash(tshark -r \"20260430.pcap\" -Y \"udp.port==60015\" -T fields -e frame.number -e frame.time_relative -e ip.src -e ip.dst -e udp.srcport -e udp.dstport -e udp.length -e frame.protocols)",
|
||||||
|
"Bash(tshark -r \"20260430.pcap\" -Y \"udp.port==60015 and frame.number==1\" -V)",
|
||||||
|
"Bash(tshark -r \"20260430.pcap\" -Y \"udp.port==60015 and udp.dstport!=60015\" -T fields -e frame.number -e frame.time_relative -e ip.dst -e udp.dstport)",
|
||||||
|
"Bash(tshark -r \"20260430.pcap\" -Y \"udp.dstport==60015\" -T fields -e frame.number -e frame.time_relative -e ip.src -e udp.length)",
|
||||||
|
"Bash(tshark -r \"20260430.pcap\" -Y \"frj519\" -T fields -e frj519.type -e frj519.seq -e frj519.status -e frj519.j1 -e frj519.j2 -e frj519.j3 -e frj519.j4 -e frj519.j5 -e frj519.j6 -e frj519.j7 -e frj519.j8 -e frj519.j9 -e frj519.timestamp)",
|
||||||
|
"Bash(tshark -r \"20260430.pcap\" -Y \"frj519\" -T fields -e frj519.type -e frj519.seq -e frj519.status -e frj519.j1 -e frj519.j2 -e frj519.j3 -e frj519.j4 -e frj519.j5 -e frj519.j6 -e frj519.j7 -e frj519.j8 -e frj519.j9 -e frj519.timestamp -e frj519.x -e frj519.y -e frj519.z -e frj519.w -e frj519.p -e frj519.r)",
|
||||||
|
"Bash(tshark -r \"20260430.pcap\" -Y \"frj519\" -T fields -E header=y)",
|
||||||
|
"Bash(tshark -G fields)",
|
||||||
|
"Bash(tshark -r \"20260430.pcap\" -Y \"frj519\" -T fields -e frame.time_relative -e data)",
|
||||||
|
"Bash(tshark -r \"20260430.pcap\" -Y \"udp.dstport==60015\" -T fields -e frame.number -e frame.time_relative -e data)",
|
||||||
|
"Bash(tshark -r \"20260430.pcap\" -Y \"udp.port==60015 and udp.srcport==60015\" -T fields -e frame.number -e frame.time_relative -e data)",
|
||||||
|
"Bash(tshark -r \"20260430.pcap\" --disable-protocol frj519 -Y \"udp.dstport==60015\" -T fields -e frame.number -e frame.time_relative -e data)",
|
||||||
|
"Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet build src/Flyshot.Runtime.Fanuc/Flyshot.Runtime.Fanuc.csproj --no-restore -v minimal 2>&1 | tail -20')",
|
||||||
|
"Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet build FlyshotReplacement.sln --no-restore -v minimal 2>&1 | tail -25')",
|
||||||
|
"Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet build FlyshotReplacement.sln --no-restore -v minimal 2>&1 | tail -15')",
|
||||||
|
"Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet test tests/Flyshot.Core.Tests/Flyshot.Core.Tests.csproj --no-restore -v minimal 2>&1 | tail -30')"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,303 @@
|
|||||||
{"robot": {"use_do": true, "io_addr": [7, 8], "io_keep_cycles": 2, "acc_limit": 1, "jerk_limit": 1, "adapt_icsp_try_num": 5}, "flying_shots": {"20251015": {"traj_waypoints": [[1.047438621520996, -0.0002488955215085298, -0.0014060207176953554, 0.009022523649036884, 0.010111905634403229, 0.009573347866535187], [0.7661270499229431, -0.04437164217233658, -0.13630111515522003, -0.41718506813049316, 0.010093353688716888, 0.009594489820301533], [0.7661266326904297, 0.2170650213956833, -0.13630135357379913, -0.4171852171421051, 0.010093353688716888, 0.009594779461622238], [1.0311520099639893, -0.062108494341373444, -0.1363297700881958, 0.30276036262512207, 0.15847623348236084, 0.00956842489540577], [1.4012629985809326, -0.05120057240128517, -0.13633012771606445, 0.3027600347995758, 0.15847666561603546, 0.00956842489540577], [1.0567246675491333, 0.01165649201720953, -0.01786380261182785, -0.015170873142778873, 0.02149667963385582, 0.009576244279742241]], "shot_flags": [false, true, true, true, true, true], "offset_values": [0, 0, 0, 0, 0, 0], "addr": [[], [2, 4], [3, 4], [2, 4], [3, 4], [2, 4]]}, "TEST20251214": {"traj_waypoints": [[1.056731, 0.011664811, -0.017892333, -0.01516874, 0.021492079, 0.009567846], [0.8067416, 0.011661344, -0.11788314, -0.01516874, 0.021492079, 0.009567846], [0.60675246, -0.03833516, -0.11788314, 0.034831185, -0.22849938, -0.24043223], [0.7667507, 0.20164281, -0.11788314, 0.034831185, -0.22849938, -0.24043223], [0.7667507, 0.20164281, -0.11788314, 0.034831185, -0.22849938, -0.14043556], [1.1667324, 0.05164983, -0.11789217, 0.23482007, 0.021492079, -0.14043556], [1.056731, 0.011664811, -0.017892333, -0.01516874, 0.021492079, 0.009567846]], "shot_flags": [false, true, true, true, true, true, false], "offset_values": [0, 0, 0, 0, 0, 0, 0], "addr": [[], [], [3, 4, 2], [3, 4, 2], [3, 4, 2], [3, 4, 2], []]}, "UTTC_MS11": {"traj_waypoints": [[1.056731, 0.011664811, -0.017892333, -0.01516874, 0.021492079, 0.009567846], [0.8532358, 0.03837953, -0.19235304, 0.0071595116, 0.109054826, 0.040055145], [0.96600056, 0.20607172, -0.12233179, -1.2394339, 0.10493033, 1.2958988], [0.9618476, 0.15288207, -0.14867093, -0.7176314, 0.1764264, 0.73228663], [0.76189893, -0.028442925, -0.30919823, 0.10463613, 0.5615024, -0.39399016], [1.1271763, 0.074403025, -0.27347943, -0.5227772, 0.52098846, 0.79633313], [1.0555661, 0.4026262, -0.08746306, 0.6301835, 0.09644133, -0.5463328], [1.2300354, 0.28612664, -0.23486805, -0.4868128, 0.25369516, 0.55347764], [1.2144431, -0.29855102, -0.15202847, -1.0205934, 0.13317892, 1.1246506], [1.2840607, -0.11222197, -0.16805042, -2.248135, 0.2560587, 2.4434967], [1.3189346, -0.25620222, -0.12730704, -2.285038, 0.30872014, 2.4765089], [1.502615, -0.25304365, -0.23878741, -1.2194318, 0.46674785, 1.5533328], [1.07723, -0.07387611, -0.1707704, -1.8916591, 0.38677844, 2.061968], [1.3920237, 0.08098731, -0.2672306, -0.9780007, 0.4561093, 0.9102286], [1.9016331, 0.023924276, -0.58633333, -0.8441697, 0.76730615, 1.4842151], [1.9300697, -0.06738541, -0.56542397, -0.892083, 0.77194446, 1.5293273], [2.0611632, -0.30327517, -0.54225636, -1.0395275, 0.8505439, 1.6429617], [1.0921186, -0.40034482, -0.1803499, 1.3524796, 0.6210477, -1.2159473], [1.0521278, -0.40034503, -0.1803492, 1.3524843, 0.6210471, -1.2159531], [1.056731, 0.011664811, -0.017892333, -0.01516874, 0.021492079, 0.009567846]], "shot_flags": [false, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, true, false], "offset_values": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "addr": [[], [2, 4], [3, 4, 2], [3, 4, 2], [4, 2], [4, 2], [3, 4], [3, 4], [4, 2], [4, 2], [4, 2], [4, 2], [4, 2], [4, 3], [4, 2], [4, 2], [4, 2], [4, 2], [4, 3], []]}, "5U": {"traj_waypoints": [[-0.95982397, 0.6331447, -1.0055008, 0.79858834, 1.1564041, -0.4260437], [-0.98353565, 0.66203266, -0.9758351, 0.8320198, 1.1455917, -0.45941326]], "shot_flags": [false, false], "offset_values": [0, 0], "addr": [[], []]}}}
|
{
|
||||||
|
"robot": {
|
||||||
|
"use_do": true,
|
||||||
|
"io_addr": [
|
||||||
|
7,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
"io_keep_cycles": 2,
|
||||||
|
"acc_limit": 1,
|
||||||
|
"jerk_limit": 1,
|
||||||
|
"adapt_icsp_try_num": 5,
|
||||||
|
"planning_speed_scale": 0.74227,
|
||||||
|
"smooth_start_stop_timing": false
|
||||||
|
},
|
||||||
|
"flying_shots": {
|
||||||
|
"UTTC_MS11": {
|
||||||
|
"traj_waypoints": [
|
||||||
|
[
|
||||||
|
1.056731,
|
||||||
|
0.011664811,
|
||||||
|
-0.017892333,
|
||||||
|
-0.01516874,
|
||||||
|
0.021492079,
|
||||||
|
0.009567846
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.8532358,
|
||||||
|
0.03837953,
|
||||||
|
-0.19235304,
|
||||||
|
0.0071595116,
|
||||||
|
0.109054826,
|
||||||
|
0.040055145
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.96600056,
|
||||||
|
0.20607172,
|
||||||
|
-0.12233179,
|
||||||
|
-1.2394339,
|
||||||
|
0.10493033,
|
||||||
|
1.2958988
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.9618476,
|
||||||
|
0.15288207,
|
||||||
|
-0.14867093,
|
||||||
|
-0.7176314,
|
||||||
|
0.1764264,
|
||||||
|
0.73228663
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.76189893,
|
||||||
|
-0.028442925,
|
||||||
|
-0.30919823,
|
||||||
|
0.10463613,
|
||||||
|
0.5615024,
|
||||||
|
-0.39399016
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.1271763,
|
||||||
|
0.074403025,
|
||||||
|
-0.27347943,
|
||||||
|
-0.5227772,
|
||||||
|
0.52098846,
|
||||||
|
0.79633313
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.0555661,
|
||||||
|
0.4026262,
|
||||||
|
-0.08746306,
|
||||||
|
0.6301835,
|
||||||
|
0.09644133,
|
||||||
|
-0.5463328
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.2300354,
|
||||||
|
0.28612664,
|
||||||
|
-0.23486805,
|
||||||
|
-0.4868128,
|
||||||
|
0.25369516,
|
||||||
|
0.55347764
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.2144431,
|
||||||
|
-0.29855102,
|
||||||
|
-0.15202847,
|
||||||
|
-1.0205934,
|
||||||
|
0.13317892,
|
||||||
|
1.1246506
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.2840607,
|
||||||
|
-0.11222197,
|
||||||
|
-0.16805042,
|
||||||
|
-2.248135,
|
||||||
|
0.2560587,
|
||||||
|
2.4434967
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.3189346,
|
||||||
|
-0.25620222,
|
||||||
|
-0.12730704,
|
||||||
|
-2.285038,
|
||||||
|
0.30872014,
|
||||||
|
2.4765089
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.502615,
|
||||||
|
-0.25304365,
|
||||||
|
-0.23878741,
|
||||||
|
-1.2194318,
|
||||||
|
0.46674785,
|
||||||
|
1.5533328
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.07723,
|
||||||
|
-0.07387611,
|
||||||
|
-0.1707704,
|
||||||
|
-1.8916591,
|
||||||
|
0.38677844,
|
||||||
|
2.061968
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.3920237,
|
||||||
|
0.08098731,
|
||||||
|
-0.2672306,
|
||||||
|
-0.9780007,
|
||||||
|
0.4561093,
|
||||||
|
0.9102286
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9016331,
|
||||||
|
0.023924276,
|
||||||
|
-0.58633333,
|
||||||
|
-0.8441697,
|
||||||
|
0.76730615,
|
||||||
|
1.4842151
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.9300697,
|
||||||
|
-0.06738541,
|
||||||
|
-0.56542397,
|
||||||
|
-0.892083,
|
||||||
|
0.77194446,
|
||||||
|
1.5293273
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2.0611632,
|
||||||
|
-0.30327517,
|
||||||
|
-0.54225636,
|
||||||
|
-1.0395275,
|
||||||
|
0.8505439,
|
||||||
|
1.6429617
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.0921186,
|
||||||
|
-0.40034482,
|
||||||
|
-0.1803499,
|
||||||
|
1.3524796,
|
||||||
|
0.6210477,
|
||||||
|
-1.2159473
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.0521278,
|
||||||
|
-0.40034503,
|
||||||
|
-0.1803492,
|
||||||
|
1.3524843,
|
||||||
|
0.6210471,
|
||||||
|
-1.2159531
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1.056731,
|
||||||
|
0.011664811,
|
||||||
|
-0.017892333,
|
||||||
|
-0.01516874,
|
||||||
|
0.021492079,
|
||||||
|
0.009567846
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"shot_flags": [
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false
|
||||||
|
],
|
||||||
|
"offset_values": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"addr": [
|
||||||
|
[],
|
||||||
|
[
|
||||||
|
2,
|
||||||
|
4
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3,
|
||||||
|
4
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3,
|
||||||
|
4
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
199
analysis/fanuc_10010_dissector.lua
Normal file
199
analysis/fanuc_10010_dissector.lua
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
local plugin_info = {
|
||||||
|
version = "1.1.0",
|
||||||
|
author = "OpenAI Codex",
|
||||||
|
description = "FANUC TCP 10010 状态帧解析器",
|
||||||
|
}
|
||||||
|
|
||||||
|
set_plugin_info(plugin_info)
|
||||||
|
|
||||||
|
local fanuc10010_proto = Proto("fanuc10010_state_frame", "FANUC 10010 State Frame")
|
||||||
|
|
||||||
|
local FRAME_LENGTH = 90
|
||||||
|
local HEADER_LENGTH = 3
|
||||||
|
local TRAILER_LENGTH = 3
|
||||||
|
local LENGTH_OFFSET = 3
|
||||||
|
local MESSAGE_ID_OFFSET = 7
|
||||||
|
local POSE_OFFSET = 11
|
||||||
|
local POSE_COUNT = 6
|
||||||
|
local JOINT_OFFSET = 35
|
||||||
|
local JOINT_COUNT = 9
|
||||||
|
local TAIL_OFFSET = 71
|
||||||
|
local TAIL_COUNT = 4
|
||||||
|
|
||||||
|
local POSE_NAMES = { "X", "Y", "Z", "W", "P", "R" }
|
||||||
|
local JOINT_NAMES = { "J1", "J2", "J3", "J4", "J5", "J6", "Ext1", "Ext2", "Ext3" }
|
||||||
|
local HEADER_MAGIC = "doz"
|
||||||
|
local TRAILER_MAGIC = "zod"
|
||||||
|
|
||||||
|
local fields = {
|
||||||
|
frame = ProtoField.bytes("fanuc10010.frame", "原始状态帧"),
|
||||||
|
header = ProtoField.string("fanuc10010.header", "帧头 Magic"),
|
||||||
|
declared_length = ProtoField.uint32("fanuc10010.length", "声明长度", base.DEC),
|
||||||
|
message_id = ProtoField.uint32("fanuc10010.message_id", "消息号", base.DEC),
|
||||||
|
pose = ProtoField.none("fanuc10010.pose", "笛卡尔位姿"),
|
||||||
|
joints = ProtoField.none("fanuc10010.joints", "关节与扩展轴"),
|
||||||
|
joint_degrees = ProtoField.none("fanuc10010.joint_degrees", "Joint Degrees"),
|
||||||
|
tail = ProtoField.none("fanuc10010.tail", "尾部状态字"),
|
||||||
|
trailer = ProtoField.string("fanuc10010.trailer", "帧尾 Magic"),
|
||||||
|
expert_bad_length = ProtoField.string("fanuc10010.expert.bad_length", "长度异常"),
|
||||||
|
expert_bad_magic = ProtoField.string("fanuc10010.expert.bad_magic", "Magic 异常"),
|
||||||
|
}
|
||||||
|
|
||||||
|
for index = 1, POSE_COUNT do
|
||||||
|
fields["pose_" .. index] = ProtoField.float(
|
||||||
|
"fanuc10010.pose_" .. index,
|
||||||
|
"Pose " .. POSE_NAMES[index])
|
||||||
|
end
|
||||||
|
|
||||||
|
for index = 1, JOINT_COUNT do
|
||||||
|
fields["joint_" .. index] = ProtoField.float(
|
||||||
|
"fanuc10010.joint_" .. index,
|
||||||
|
JOINT_NAMES[index] .. " (raw)")
|
||||||
|
end
|
||||||
|
|
||||||
|
for index = 1, 6 do
|
||||||
|
fields["joint_deg_" .. index] = ProtoField.float(
|
||||||
|
"fanuc10010.joint_deg_" .. index,
|
||||||
|
JOINT_NAMES[index] .. " (deg)")
|
||||||
|
end
|
||||||
|
|
||||||
|
for index = 1, TAIL_COUNT do
|
||||||
|
fields["tail_" .. index] = ProtoField.uint32(
|
||||||
|
"fanuc10010.tail_" .. index,
|
||||||
|
"Tail[" .. (index - 1) .. "]",
|
||||||
|
base.DEC)
|
||||||
|
end
|
||||||
|
|
||||||
|
fanuc10010_proto.fields = fields
|
||||||
|
|
||||||
|
local function read_f32_be(tvb, offset)
|
||||||
|
return tvb(offset, 4):tvb():range(0, 4):float()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function read_u32_be(tvb, offset)
|
||||||
|
return tvb(offset, 4):uint()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function read_ascii(tvb, offset, length)
|
||||||
|
return tvb(offset, length):string()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function radians_to_degrees(value)
|
||||||
|
return value * 180.0 / math.pi
|
||||||
|
end
|
||||||
|
|
||||||
|
local function add_error(subtree, range, field, message)
|
||||||
|
local item = subtree:add(field, range, message)
|
||||||
|
item:add_expert_info(PI_MALFORMED, PI_ERROR, message)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function dissect_single_frame(tvb, pinfo, tree, frame_offset)
|
||||||
|
local remaining = tvb:len() - frame_offset
|
||||||
|
local candidate_length = math.min(remaining, FRAME_LENGTH)
|
||||||
|
local frame_range = tvb(frame_offset, candidate_length)
|
||||||
|
local subtree = tree:add(fanuc10010_proto, frame_range, "FANUC 10010 状态帧")
|
||||||
|
subtree:add(fields.frame, frame_range)
|
||||||
|
|
||||||
|
if remaining < FRAME_LENGTH then
|
||||||
|
add_error(subtree, frame_range, fields.expert_bad_length, "剩余字节不足 90B,无法组成完整状态帧")
|
||||||
|
return remaining
|
||||||
|
end
|
||||||
|
|
||||||
|
local header = read_ascii(tvb, frame_offset, HEADER_LENGTH)
|
||||||
|
local declared_length = read_u32_be(tvb, frame_offset + LENGTH_OFFSET)
|
||||||
|
local message_id = read_u32_be(tvb, frame_offset + MESSAGE_ID_OFFSET)
|
||||||
|
local trailer = read_ascii(tvb, frame_offset + FRAME_LENGTH - TRAILER_LENGTH, TRAILER_LENGTH)
|
||||||
|
|
||||||
|
subtree:add(fields.header, tvb(frame_offset, HEADER_LENGTH), header)
|
||||||
|
subtree:add(fields.declared_length, tvb(frame_offset + LENGTH_OFFSET, 4), declared_length)
|
||||||
|
subtree:add(fields.message_id, tvb(frame_offset + MESSAGE_ID_OFFSET, 4), message_id)
|
||||||
|
|
||||||
|
local pose_tree = subtree:add(fields.pose, tvb(frame_offset + POSE_OFFSET, POSE_COUNT * 4))
|
||||||
|
local pose_values = {}
|
||||||
|
for index = 1, POSE_COUNT do
|
||||||
|
local field_offset = frame_offset + POSE_OFFSET + ((index - 1) * 4)
|
||||||
|
local value = read_f32_be(tvb, field_offset)
|
||||||
|
pose_values[index] = value
|
||||||
|
pose_tree:add(fields["pose_" .. index], tvb(field_offset, 4), value)
|
||||||
|
end
|
||||||
|
|
||||||
|
local joint_tree = subtree:add(fields.joints, tvb(frame_offset + JOINT_OFFSET, JOINT_COUNT * 4))
|
||||||
|
local joint_values = {}
|
||||||
|
for index = 1, JOINT_COUNT do
|
||||||
|
local field_offset = frame_offset + JOINT_OFFSET + ((index - 1) * 4)
|
||||||
|
local value = read_f32_be(tvb, field_offset)
|
||||||
|
joint_values[index] = value
|
||||||
|
joint_tree:add(fields["joint_" .. index], tvb(field_offset, 4), value)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 单独保留一组角度显示,便于对照原始关节弧度值。
|
||||||
|
local joint_degree_tree = subtree:add(fields.joint_degrees, tvb(frame_offset + JOINT_OFFSET, 6 * 4))
|
||||||
|
for index = 1, 6 do
|
||||||
|
local field_offset = frame_offset + JOINT_OFFSET + ((index - 1) * 4)
|
||||||
|
joint_degree_tree:add(
|
||||||
|
fields["joint_deg_" .. index],
|
||||||
|
tvb(field_offset, 4),
|
||||||
|
radians_to_degrees(joint_values[index]))
|
||||||
|
end
|
||||||
|
|
||||||
|
local tail_tree = subtree:add(fields.tail, tvb(frame_offset + TAIL_OFFSET, TAIL_COUNT * 4))
|
||||||
|
local tail_values = {}
|
||||||
|
for index = 1, TAIL_COUNT do
|
||||||
|
local field_offset = frame_offset + TAIL_OFFSET + ((index - 1) * 4)
|
||||||
|
local value = read_u32_be(tvb, field_offset)
|
||||||
|
tail_values[index] = value
|
||||||
|
tail_tree:add(fields["tail_" .. index], tvb(field_offset, 4), value)
|
||||||
|
end
|
||||||
|
|
||||||
|
subtree:add(fields.trailer, tvb(frame_offset + FRAME_LENGTH - TRAILER_LENGTH, TRAILER_LENGTH), trailer)
|
||||||
|
|
||||||
|
if header ~= HEADER_MAGIC then
|
||||||
|
add_error(subtree, tvb(frame_offset, HEADER_LENGTH), fields.expert_bad_magic, "帧头不是 doz")
|
||||||
|
end
|
||||||
|
|
||||||
|
if trailer ~= TRAILER_MAGIC then
|
||||||
|
add_error(subtree, tvb(frame_offset + FRAME_LENGTH - TRAILER_LENGTH, TRAILER_LENGTH), fields.expert_bad_magic, "帧尾不是 zod")
|
||||||
|
end
|
||||||
|
|
||||||
|
if declared_length ~= FRAME_LENGTH then
|
||||||
|
add_error(subtree, tvb(frame_offset + LENGTH_OFFSET, 4), fields.expert_bad_length, "长度字段不是 90")
|
||||||
|
end
|
||||||
|
|
||||||
|
local summary = string.format(
|
||||||
|
"MsgId=%u X=%.3f Y=%.3f Z=%.3f J1=%.6f rad / %.3f deg J2=%.6f rad / %.3f deg Tail=[%u,%u,%u,%u]",
|
||||||
|
message_id,
|
||||||
|
pose_values[1], pose_values[2], pose_values[3],
|
||||||
|
joint_values[1], radians_to_degrees(joint_values[1]),
|
||||||
|
joint_values[2], radians_to_degrees(joint_values[2]),
|
||||||
|
tail_values[1], tail_values[2], tail_values[3], tail_values[4])
|
||||||
|
|
||||||
|
subtree:set_text("FANUC 10010 状态帧, " .. summary)
|
||||||
|
pinfo.cols.info:append(" | " .. summary)
|
||||||
|
|
||||||
|
return FRAME_LENGTH
|
||||||
|
end
|
||||||
|
|
||||||
|
function fanuc10010_proto.dissector(tvb, pinfo, tree)
|
||||||
|
if tvb:len() == 0 then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
pinfo.cols.protocol = "FANUC10010"
|
||||||
|
|
||||||
|
local offset = 0
|
||||||
|
while offset < tvb:len() do
|
||||||
|
local consumed = dissect_single_frame(tvb, pinfo, tree, offset)
|
||||||
|
if consumed <= 0 then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
offset = offset + consumed
|
||||||
|
if consumed < FRAME_LENGTH then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return tvb:len()
|
||||||
|
end
|
||||||
|
|
||||||
|
DissectorTable.get("tcp.port"):add(10010, fanuc10010_proto)
|
||||||
@@ -609,11 +609,12 @@ public sealed class ControllerClientCompatService : IControllerClientCompatServi
|
|||||||
new FlyshotExecutionOptions(useCache:false,saveTrajectory: true, method: method),
|
new FlyshotExecutionOptions(useCache:false,saveTrajectory: true, method: method),
|
||||||
planningSettings,
|
planningSettings,
|
||||||
planningSettings.PlanningSpeedScale);
|
planningSettings.PlanningSpeedScale);
|
||||||
|
_logger?.LogInformation("SaveTrajectoryInfo 规划完成记录到本地");
|
||||||
ExportFlyshotArtifactsIfRequested(name, saveTrajectory: true, robot, bundle);
|
ExportFlyshotArtifactsIfRequested(name, saveTrajectory: true, robot, bundle);
|
||||||
|
|
||||||
var robotName = _configuredRobotName ?? throw new InvalidOperationException("Robot has not been setup.");
|
// var robotName = _configuredRobotName ?? throw new InvalidOperationException("Robot has not been setup.");
|
||||||
var settings = _robotSettings ?? CreateDefaultRobotSettings();
|
// var settings = _robotSettings ?? CreateDefaultRobotSettings();
|
||||||
_trajectoryStore.Save(robotName, settings, trajectory);
|
// _trajectoryStore.Save(robotName, settings, trajectory);
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger?.LogInformation("SaveTrajectoryInfo 完成: name={Name}", name);
|
_logger?.LogInformation("SaveTrajectoryInfo 完成: name={Name}", name);
|
||||||
|
|||||||
@@ -12,6 +12,16 @@ namespace Flyshot.ControllerClientCompat;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ControllerClientTrajectoryOrchestrator
|
public sealed class ControllerClientTrajectoryOrchestrator
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 稠密轨迹离散限幅失败后允许统一拉长时间轴的最大次数。
|
||||||
|
/// </summary>
|
||||||
|
private const int MaxDenseLimitStretchIterations = 100;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每次离散限幅失败后统一放大的时间倍率。
|
||||||
|
/// </summary>
|
||||||
|
private const double DenseLimitStretchFactor = 1.01;
|
||||||
|
|
||||||
private readonly ICspPlanner _icspPlanner;
|
private readonly ICspPlanner _icspPlanner;
|
||||||
private readonly SelfAdaptIcspPlanner _selfAdaptIcspPlanner;
|
private readonly SelfAdaptIcspPlanner _selfAdaptIcspPlanner;
|
||||||
private readonly ShotTimelineBuilder _shotTimelineBuilder = new(new WaypointTimestampResolver());
|
private readonly ShotTimelineBuilder _shotTimelineBuilder = new(new WaypointTimestampResolver());
|
||||||
@@ -68,8 +78,10 @@ public sealed class ControllerClientTrajectoryOrchestrator
|
|||||||
saveTrajectoryArtifacts: options.SaveTrajectory);
|
saveTrajectoryArtifacts: options.SaveTrajectory);
|
||||||
|
|
||||||
var plannedTrajectory = PlanByMethod(request, method);
|
var plannedTrajectory = PlanByMethod(request, method);
|
||||||
|
var executionTrajectory = plannedTrajectory;
|
||||||
|
var denseJointTrajectory = CreateLimitCompliantDenseTrajectory(ref executionTrajectory, shapeTrajectoryEdges: false);
|
||||||
var shotTimeline = new ShotTimeline(Array.Empty<ShotEvent>(), Array.Empty<TrajectoryDoEvent>());
|
var shotTimeline = new ShotTimeline(Array.Empty<ShotEvent>(), Array.Empty<TrajectoryDoEvent>());
|
||||||
var result = CreateResult(plannedTrajectory, shotTimeline, usedCache: false, shapeTrajectoryEdges: false);
|
var result = CreateResult(executionTrajectory, shotTimeline, denseJointTrajectory, usedCache: false);
|
||||||
|
|
||||||
_logger?.LogInformation(
|
_logger?.LogInformation(
|
||||||
"PlanOrdinaryTrajectory 完成: 时长={Duration}s, 采样点数={SampleCount}",
|
"PlanOrdinaryTrajectory 完成: 时长={Duration}s, 采样点数={SampleCount}",
|
||||||
@@ -138,12 +150,13 @@ public sealed class ControllerClientTrajectoryOrchestrator
|
|||||||
|
|
||||||
var plannedTrajectory = PlanByMethod(request, method, settings);
|
var plannedTrajectory = PlanByMethod(request, method, settings);
|
||||||
var smoothedExecutionTrajectory = ApplyExecutionTiming(plannedTrajectory, settings);
|
var smoothedExecutionTrajectory = ApplyExecutionTiming(plannedTrajectory, settings);
|
||||||
|
var denseJointTrajectory = CreateLimitCompliantDenseTrajectory(ref smoothedExecutionTrajectory, shapeTrajectoryEdges: false);
|
||||||
var shotTimeline = _shotTimelineBuilder.Build(
|
var shotTimeline = _shotTimelineBuilder.Build(
|
||||||
smoothedExecutionTrajectory,
|
smoothedExecutionTrajectory,
|
||||||
holdCycles: settings.IoKeepCycles,
|
holdCycles: settings.IoKeepCycles,
|
||||||
samplePeriod: planningRobot.ServoPeriod,
|
samplePeriod: planningRobot.ServoPeriod,
|
||||||
useDo: settings.UseDo);
|
useDo: settings.UseDo);
|
||||||
var result = CreateResult(smoothedExecutionTrajectory, shotTimeline, usedCache: false, shapeTrajectoryEdges: false);
|
var result = CreateResult(smoothedExecutionTrajectory, shotTimeline, denseJointTrajectory, usedCache: false);
|
||||||
var bundle = new PlannedExecutionBundle(plannedTrajectory, shotTimeline, result);
|
var bundle = new PlannedExecutionBundle(plannedTrajectory, shotTimeline, result);
|
||||||
|
|
||||||
_logger?.LogInformation(
|
_logger?.LogInformation(
|
||||||
@@ -385,14 +398,9 @@ public sealed class ControllerClientTrajectoryOrchestrator
|
|||||||
private static TrajectoryResult CreateResult(
|
private static TrajectoryResult CreateResult(
|
||||||
PlannedTrajectory plannedTrajectory,
|
PlannedTrajectory plannedTrajectory,
|
||||||
ShotTimeline shotTimeline,
|
ShotTimeline shotTimeline,
|
||||||
bool usedCache,
|
IReadOnlyList<IReadOnlyList<double>> denseJointTrajectory,
|
||||||
bool shapeTrajectoryEdges)
|
bool usedCache)
|
||||||
{
|
{
|
||||||
var denseJointTrajectory = TrajectorySampler.SampleJointTrajectory(
|
|
||||||
plannedTrajectory,
|
|
||||||
samplePeriod: plannedTrajectory.Robot.ServoPeriod.TotalSeconds,
|
|
||||||
smoothStartStop: shapeTrajectoryEdges);
|
|
||||||
|
|
||||||
return new TrajectoryResult(
|
return new TrajectoryResult(
|
||||||
programName: plannedTrajectory.OriginalProgram.Name,
|
programName: plannedTrajectory.OriginalProgram.Name,
|
||||||
method: plannedTrajectory.Method,
|
method: plannedTrajectory.Method,
|
||||||
@@ -408,6 +416,65 @@ public sealed class ControllerClientTrajectoryOrchestrator
|
|||||||
denseJointTrajectory: denseJointTrajectory);
|
denseJointTrajectory: denseJointTrajectory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成满足离散速度、加速度和 Jerk 限制的稠密执行轨迹。
|
||||||
|
/// </summary>
|
||||||
|
private IReadOnlyList<IReadOnlyList<double>> CreateLimitCompliantDenseTrajectory(
|
||||||
|
ref PlannedTrajectory executionTrajectory,
|
||||||
|
bool shapeTrajectoryEdges)
|
||||||
|
{
|
||||||
|
for (var iteration = 0; iteration <= MaxDenseLimitStretchIterations; iteration++)
|
||||||
|
{
|
||||||
|
var denseJointTrajectory = TrajectorySampler.SampleJointTrajectory(
|
||||||
|
executionTrajectory,
|
||||||
|
samplePeriod: executionTrajectory.Robot.ServoPeriod.TotalSeconds,
|
||||||
|
smoothStartStop: shapeTrajectoryEdges);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
TrajectoryLimitValidator.ValidateDenseJointTrajectory(
|
||||||
|
executionTrajectory.Robot,
|
||||||
|
denseJointTrajectory,
|
||||||
|
trajectoryName: executionTrajectory.OriginalProgram.Name);
|
||||||
|
return denseJointTrajectory;
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex) when (iteration < MaxDenseLimitStretchIterations)
|
||||||
|
{
|
||||||
|
_logger?.LogWarning(ex, "稠密轨迹离散限幅校验失败,准备拉长时间轴重试");
|
||||||
|
// 离散差分超限时统一拉长时间轴,保持路点几何不变并降低速度、加速度和 Jerk。
|
||||||
|
executionTrajectory = StretchTrajectoryTiming(executionTrajectory, DenseLimitStretchFactor);
|
||||||
|
_logger?.LogInformation(
|
||||||
|
"离散差分超限,拉长时间轴,iteration={Iteration}, factor={StretchFactor}",
|
||||||
|
iteration,
|
||||||
|
DenseLimitStretchFactor);
|
||||||
|
_logger?.LogInformation("拉长之后的总时间={TotalTime}", executionTrajectory.WaypointTimes[^1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException("稠密轨迹离散限幅校验未能产生有效结果。");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按统一倍率拉长轨迹时间轴,保留原始路点和触发元数据。
|
||||||
|
/// </summary>
|
||||||
|
private PlannedTrajectory StretchTrajectoryTiming(PlannedTrajectory trajectory, double stretchFactor)
|
||||||
|
{
|
||||||
|
var waypointTimes = trajectory.WaypointTimes.Select(time => time * stretchFactor).ToArray();
|
||||||
|
var segmentDurations = trajectory.SegmentDurations.Select(duration => duration * stretchFactor).ToArray();
|
||||||
|
var segmentScales = trajectory.SegmentScales.Select(scale => scale / stretchFactor).ToArray();
|
||||||
|
|
||||||
|
return new PlannedTrajectory(
|
||||||
|
robot: trajectory.Robot,
|
||||||
|
originalProgram: trajectory.OriginalProgram,
|
||||||
|
plannedWaypoints: trajectory.PlannedWaypoints,
|
||||||
|
waypointTimes: waypointTimes,
|
||||||
|
segmentDurations: segmentDurations,
|
||||||
|
segmentScales: segmentScales,
|
||||||
|
method: trajectory.Method,
|
||||||
|
iterations: trajectory.Iterations,
|
||||||
|
threshold: trajectory.Threshold);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 为飞拍执行生成一条平滑起停的时间轴。
|
/// 为飞拍执行生成一条平滑起停的时间轴。
|
||||||
/// 保持路点位置不变,只重映射路点时刻,让起点和终点附近的速度自然收敛。
|
/// 保持路点位置不变,只重映射路点时刻,让起点和终点附近的速度自然收敛。
|
||||||
|
|||||||
@@ -63,25 +63,28 @@ public sealed class FlyshotTrajectoryArtifactWriter
|
|||||||
|
|
||||||
var outputDir = Path.Combine(_options.ResolveConfigRoot(), "Data", SanitizeDirectoryName(trajectoryName));
|
var outputDir = Path.Combine(_options.ResolveConfigRoot(), "Data", SanitizeDirectoryName(trajectoryName));
|
||||||
Directory.CreateDirectory(outputDir);
|
Directory.CreateDirectory(outputDir);
|
||||||
|
if (bundle.Result.DenseJointTrajectory is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("导出飞拍轨迹工件前必须先生成执行侧稠密轨迹。");
|
||||||
|
}
|
||||||
|
|
||||||
// 明细文件对齐旧 Data 目录的 16ms 采样;运行时 J519 仍可使用自己的 8ms 伺服采样。
|
// 明细文件现在定义为“执行侧 8ms 稠密轨迹的 16ms 低频视图”,避免再次从 PlannedTrajectory 生成另一条轨迹。
|
||||||
var kinematicsModel = _robotModelLoader.LoadKinematicsModel(robot.ModelPath);
|
var kinematicsModel = _robotModelLoader.LoadKinematicsModel(robot.ModelPath);
|
||||||
var jointTrajectory = BuildJointRows(bundle.PlannedTrajectory);
|
var jointTrajectory = BuildJointRows(bundle.PlannedTrajectory);
|
||||||
var jointDetailTrajectory = TrajectorySampler.SampleJointTrajectory(
|
_logger?.LogInformation("规划之后的轨迹点位数量为:{}", jointTrajectory.Count);
|
||||||
bundle.PlannedTrajectory,
|
var executionDenseTrajectory = bundle.Result.DenseJointTrajectory;
|
||||||
samplePeriod: LegacyDetailSamplePeriodSeconds);
|
var jointDetailTrajectory = DownsampleDenseRows(
|
||||||
|
executionDenseTrajectory,
|
||||||
|
samplePeriodSeconds: LegacyDetailSamplePeriodSeconds);
|
||||||
var cartTrajectory = BuildCartesianRows(bundle.PlannedTrajectory, kinematicsModel);
|
var cartTrajectory = BuildCartesianRows(bundle.PlannedTrajectory, kinematicsModel);
|
||||||
var cartDetailTrajectory = TrajectorySampler.SampleCartesianTrajectory(
|
var cartDetailTrajectory = BuildCartesianRowsFromJointDense(jointDetailTrajectory, kinematicsModel);
|
||||||
bundle.PlannedTrajectory,
|
|
||||||
kinematicsModel,
|
|
||||||
samplePeriod: LegacyDetailSamplePeriodSeconds);
|
|
||||||
|
|
||||||
TrajectoryExporter.WriteJointTrajectory(Path.Combine(outputDir, "JointTraj.txt"), jointTrajectory);
|
TrajectoryExporter.WriteJointTrajectory(Path.Combine(outputDir, "JointTraj.txt"), jointTrajectory);
|
||||||
TrajectoryExporter.WriteJointDenseTrajectory(Path.Combine(outputDir, "JointDetialTraj.txt"), jointDetailTrajectory);
|
TrajectoryExporter.WriteJointDenseTrajectory(Path.Combine(outputDir, "JointDetialTraj.txt"), jointDetailTrajectory);
|
||||||
TrajectoryExporter.WriteCartesianTrajectory(Path.Combine(outputDir, "CartTraj.txt"), cartTrajectory);
|
TrajectoryExporter.WriteCartesianTrajectory(Path.Combine(outputDir, "CartTraj.txt"), cartTrajectory);
|
||||||
TrajectoryExporter.WriteCartesianDenseTrajectory(Path.Combine(outputDir, "CartDetialTraj.txt"), cartDetailTrajectory);
|
TrajectoryExporter.WriteCartesianDenseTrajectory(Path.Combine(outputDir, "CartDetialTraj.txt"), cartDetailTrajectory);
|
||||||
TrajectoryExporter.WriteShotEvents(Path.Combine(outputDir, "ShotEvents.json"), bundle.ShotTimeline.ShotEvents);
|
TrajectoryExporter.WriteShotEvents(Path.Combine(outputDir, "ShotEvents.json"), bundle.ShotTimeline.ShotEvents);
|
||||||
WriteActualSendArtifacts(outputDir, bundle.Result, speedRatio);
|
WriteActualSendArtifacts(outputDir, robot, bundle.Result, speedRatio);
|
||||||
|
|
||||||
_logger?.LogInformation(
|
_logger?.LogInformation(
|
||||||
"saveTrajectory 已导出规划点位: name={TrajectoryName}, outputDir={OutputDir}, jointRows={JointRows}, detailRows={DetailRows}, speedRatio={SpeedRatio}",
|
"saveTrajectory 已导出规划点位: name={TrajectoryName}, outputDir={OutputDir}, jointRows={JointRows}, detailRows={DetailRows}, speedRatio={SpeedRatio}",
|
||||||
@@ -95,8 +98,10 @@ public sealed class FlyshotTrajectoryArtifactWriter
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 生成按 J519 8ms 实际发送周期重采样的轨迹点,供 saveTrajectory 离线对比真实下发序列。
|
/// 生成按 J519 8ms 实际发送周期重采样的轨迹点,供 saveTrajectory 离线对比真实下发序列。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void WriteActualSendArtifacts(string outputDir, TrajectoryResult result, double speedRatio)
|
private void WriteActualSendArtifacts(string outputDir, RobotProfile robot, TrajectoryResult result, double speedRatio)
|
||||||
{
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(robot);
|
||||||
|
|
||||||
if (result.DenseJointTrajectory is null)
|
if (result.DenseJointTrajectory is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -112,6 +117,19 @@ public sealed class FlyshotTrajectoryArtifactWriter
|
|||||||
result.Duration.TotalSeconds,
|
result.Duration.TotalSeconds,
|
||||||
ActualSendServoPeriodSeconds,
|
ActualSendServoPeriodSeconds,
|
||||||
speedRatio);
|
speedRatio);
|
||||||
|
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
TrajectoryLimitValidator.ValidateJ519SendSamples(
|
||||||
|
robot,
|
||||||
|
samples,
|
||||||
|
trajectoryName: result.ProgramName);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger?.LogError(e, "ValidateJ519SendSamples 失败,program={ProgramName}", result.ProgramName);
|
||||||
|
}
|
||||||
var jointRows = new List<IReadOnlyList<double>>(samples.Count);
|
var jointRows = new List<IReadOnlyList<double>>(samples.Count);
|
||||||
var timingRows = new List<IReadOnlyList<double>>(samples.Count);
|
var timingRows = new List<IReadOnlyList<double>>(samples.Count);
|
||||||
var jerkRows = new List<IReadOnlyList<double>>();
|
var jerkRows = new List<IReadOnlyList<double>>();
|
||||||
@@ -217,6 +235,63 @@ public sealed class FlyshotTrajectoryArtifactWriter
|
|||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 基于执行侧稠密关节轨迹生成笛卡尔导出行,保持与 JointDetialTraj.txt 同一来源。
|
||||||
|
/// </summary>
|
||||||
|
private static IReadOnlyList<IReadOnlyList<double>> BuildCartesianRowsFromJointDense(
|
||||||
|
IReadOnlyList<IReadOnlyList<double>> jointDenseRows,
|
||||||
|
RobotKinematicsModel kinematicsModel)
|
||||||
|
{
|
||||||
|
var rows = new List<IReadOnlyList<double>>(jointDenseRows.Count);
|
||||||
|
foreach (var jointRow in jointDenseRows)
|
||||||
|
{
|
||||||
|
var jointPositions = jointRow.Skip(1).ToArray();
|
||||||
|
var pose = RobotKinematics.ForwardKinematics(kinematicsModel, jointPositions);
|
||||||
|
var row = new List<double>(pose.Length + 1)
|
||||||
|
{
|
||||||
|
Math.Round(jointRow[0], 6)
|
||||||
|
};
|
||||||
|
row.AddRange(pose.Select(static value => Math.Round(value, 6)));
|
||||||
|
rows.Add(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将 8ms 执行稠密轨迹按指定周期抽稀为低频兼容视图,并始终保留终点。
|
||||||
|
/// </summary>
|
||||||
|
private static IReadOnlyList<IReadOnlyList<double>> DownsampleDenseRows(
|
||||||
|
IReadOnlyList<IReadOnlyList<double>> denseRows,
|
||||||
|
double samplePeriodSeconds)
|
||||||
|
{
|
||||||
|
var result = new List<IReadOnlyList<double>>();
|
||||||
|
var epsilon = 1e-6;
|
||||||
|
var nextSampleTime = 0.0;
|
||||||
|
|
||||||
|
foreach (var row in denseRows)
|
||||||
|
{
|
||||||
|
var sampleTime = row[0];
|
||||||
|
if (sampleTime + epsilon < nextSampleTime)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.Abs(sampleTime - nextSampleTime) <= epsilon || sampleTime.Equals(0.0))
|
||||||
|
{
|
||||||
|
result.Add(row);
|
||||||
|
nextSampleTime += samplePeriodSeconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.Count == 0 || !ReferenceEquals(result[^1], denseRows[^1]))
|
||||||
|
{
|
||||||
|
result.Add(denseRows[^1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 将轨迹名转换为可用目录名,避免 HTTP 输入中的路径字符污染输出目录。
|
/// 将轨迹名转换为可用目录名,避免 HTTP 输入中的路径字符污染输出目录。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
194
src/Flyshot.Core.Planning/ICSP-算法说明.md
Normal file
194
src/Flyshot.Core.Planning/ICSP-算法说明.md
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
# ICSP 算法说明(`ICspPlanner`)
|
||||||
|
|
||||||
|
本文档用于解释 `Flyshot.Core.Planning.ICspPlanner` 当前实现的 **ICSP 规划算法**在本仓库中的真实含义与计算逻辑,便于与逆向结论对照、以及指导后续改造(例如“按约束生成中间点位”)。
|
||||||
|
|
||||||
|
> 适用范围:本文描述的是当前 C# 实现的 **“CubicSpline + 逐段时间缩放迭代(retiming)”** 版本。
|
||||||
|
> 重要澄清:`ICspPlanner` 的主要输出是 **时间轴**(每个示教点的时间戳),而不是直接输出固定周期的稠密点序列;稠密点在后续采样层生成。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 名词与数据形态
|
||||||
|
|
||||||
|
### 1.1 输入
|
||||||
|
|
||||||
|
- **示教点(路点)**:`request.Program.Waypoints`
|
||||||
|
每个路点是关节空间向量 \(q_i \in \mathbb{R}^{dof}\)。
|
||||||
|
- **关节约束**:`request.Robot.JointLimits[d]` 提供每轴上限:
|
||||||
|
- 速度上限 \(v_{lim}[d]\)
|
||||||
|
- 加速度上限 \(a_{lim}[d]\)
|
||||||
|
- 跃度(jerk)上限 \(j_{lim}[d]\)
|
||||||
|
|
||||||
|
### 1.2 输出(`PlannedTrajectory`)
|
||||||
|
|
||||||
|
`ICspPlanner` 的输出 **不是稠密轨迹点序列**,而是:
|
||||||
|
|
||||||
|
- `PlannedWaypoints`:规划后路点(对于普通 `icsp`,与输入示教点相同;补点发生在 `SelfAdaptIcspPlanner`)
|
||||||
|
- `WaypointTimes`:每个路点的绝对时间 \(t_i\)(秒)
|
||||||
|
- `SegmentDurations`:每段时长 \(T_i = t_{i+1}-t_i\)(秒)
|
||||||
|
- `SegmentScales`:每段缩放因子 `scale_i`
|
||||||
|
- `Iterations` / `Threshold`:收敛信息
|
||||||
|
|
||||||
|
后续模块会基于 `PlannedWaypoints + WaypointTimes` 重建样条并采样,生成稠密点:
|
||||||
|
|
||||||
|
- 规划层稠密采样:`TrajectorySampler.SampleJointTrajectory(...)`
|
||||||
|
- 运行时 J519 重采样(速度倍率映射 + rad->deg):`J519SendTrajectorySampler.SampleDenseJointTrajectory(...)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 算法总体目标(retiming)
|
||||||
|
|
||||||
|
给定一组关节示教点 \(\{q_i\}_{i=0}^{N-1}\),在不改变路点位置的前提下,为每段分配时长 \(\{T_i\}_{i=0}^{N-2}\),使得用 **clamped-zero 三次样条**连接后的轨迹在每段上满足:
|
||||||
|
|
||||||
|
- \(\max|\dot q_d(t)| \le v_{lim}[d]\)
|
||||||
|
- \(\max|\ddot q_d(t)| \le a_{lim}[d]\)
|
||||||
|
- \(\max|\dddot q_d(t)| \le j_{lim}[d]\)
|
||||||
|
|
||||||
|
实现策略是“逐段缩放时长”的迭代法:每轮用当前 \(\{T_i\}\) 构造样条并解析求导峰值,再根据超限程度把相应段时长乘以缩放因子,使峰值回落。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 计算步骤(与代码一致)
|
||||||
|
|
||||||
|
### 3.1 前置条件
|
||||||
|
|
||||||
|
- 路点数 \(N \ge 4\)(否则抛异常)
|
||||||
|
|
||||||
|
### 3.2 初始段时长
|
||||||
|
|
||||||
|
段数 \(nseg = N-1\)。
|
||||||
|
|
||||||
|
初始段时长取相邻路点关节空间欧氏距离:
|
||||||
|
|
||||||
|
\[
|
||||||
|
T_i^{(0)} = \|q_{i+1}-q_i\|_2
|
||||||
|
\]
|
||||||
|
|
||||||
|
### 3.3 由段时长构造绝对时间轴
|
||||||
|
|
||||||
|
\[
|
||||||
|
t_0 = 0,\quad t_{i+1} = t_i + T_i
|
||||||
|
\]
|
||||||
|
|
||||||
|
### 3.4 用 clamped-zero 边界构造三次样条
|
||||||
|
|
||||||
|
以 \((t_i, q_i)\) 为节点构造分段三次多项式:
|
||||||
|
|
||||||
|
\[
|
||||||
|
S_i(t) = a_i t^3 + b_i t^2 + c_i t + d_i,\quad t \in [t_i, t_{i+1}]
|
||||||
|
\]
|
||||||
|
|
||||||
|
边界条件为 **clamped-zero**(起点/终点一阶导为 0),用于与逆向锁定的参考行为对齐。
|
||||||
|
|
||||||
|
### 3.5 解析计算每段导数峰值
|
||||||
|
|
||||||
|
对每段、每轴,解析求最大绝对值:
|
||||||
|
|
||||||
|
- 一阶导(速度)是二次函数:端点与顶点候选取最大
|
||||||
|
- 二阶导(加速度)是一次函数:端点取最大
|
||||||
|
- 三阶导(跃度)是常数:直接取绝对值
|
||||||
|
|
||||||
|
得到三张矩阵:
|
||||||
|
|
||||||
|
- `maxDq[seg,d] = max_t |dq/dt|`
|
||||||
|
- `maxDdq[seg,d] = max_t |d²q/dt²|`
|
||||||
|
- `maxDddq[seg,d] = max_t |d³q/dt³|`
|
||||||
|
|
||||||
|
### 3.6 计算每段缩放因子(核心公式)
|
||||||
|
|
||||||
|
对段 `seg`,对每个关节 `d` 计算三类“超限比”:
|
||||||
|
|
||||||
|
\[
|
||||||
|
s_v = \left|\frac{maxDq[seg,d]}{v_{lim}[d]}\right|
|
||||||
|
\]
|
||||||
|
|
||||||
|
\[
|
||||||
|
s_a = \sqrt{\left|\frac{maxDdq[seg,d]}{a_{lim}[d]}\right|}
|
||||||
|
\]
|
||||||
|
|
||||||
|
\[
|
||||||
|
s_j = \sqrt[3]{\left|\frac{maxDddq[seg,d]}{j_{lim}[d]}\right|}
|
||||||
|
\]
|
||||||
|
|
||||||
|
段缩放因子取所有轴、三类约束的最大值:
|
||||||
|
|
||||||
|
\[
|
||||||
|
scale_{seg}=\max_d \max(s_v, s_a, s_j)
|
||||||
|
\]
|
||||||
|
|
||||||
|
> 指数来源:时间拉长 \(k\) 倍时,速度按 \(1/k\) 缩小、加速度按 \(1/k^2\) 缩小、跃度按 \(1/k^3\) 缩小,因此超限比需要分别取一次方/平方根/立方根来求“应当拉长多少倍”。
|
||||||
|
|
||||||
|
### 3.7 收敛指标与最优解保存
|
||||||
|
|
||||||
|
每轮用如下指标衡量“离约束满足还差多少”:
|
||||||
|
|
||||||
|
\[
|
||||||
|
threshold = \sum_{seg} |scale_{seg} - 1|
|
||||||
|
\]
|
||||||
|
|
||||||
|
若本轮 `threshold` 小于历史最佳,则保存当前解作为 `best`(包含 `bestDurations / bestScales / bestWaypointTimes` 等)。
|
||||||
|
|
||||||
|
### 3.8 收敛判定与段时长更新
|
||||||
|
|
||||||
|
- 若 `threshold < _threshold`(默认 `1e-3`),认为收敛并提前结束迭代
|
||||||
|
- 否则更新每段时长:
|
||||||
|
|
||||||
|
\[
|
||||||
|
T_{seg} \leftarrow T_{seg} \cdot scale_{seg}
|
||||||
|
\]
|
||||||
|
|
||||||
|
并进入下一轮。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 最终判定(global_scale)
|
||||||
|
|
||||||
|
迭代结束后取历史最优缩放因子的最大值:
|
||||||
|
|
||||||
|
\[
|
||||||
|
globalScale=\max_{seg}(scale_{seg})
|
||||||
|
\]
|
||||||
|
|
||||||
|
若启用强制判定(`enforceFinalScale=true`)且:
|
||||||
|
|
||||||
|
\[
|
||||||
|
globalScale > 1 + \text{finalScaleTolerance}
|
||||||
|
\]
|
||||||
|
|
||||||
|
则判定“未收敛/不可执行”并抛异常。默认容差 `finalScaleTolerance=1e-2`,用于容忍 C# spline 与参考实现间的小量数值差异。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 与“补点/中间点位”的关系(常见误解澄清)
|
||||||
|
|
||||||
|
### 5.1 `ICspPlanner` 不负责生成固定周期的中间点位
|
||||||
|
|
||||||
|
`ICspPlanner` 的核心工作是 **时间轴规划(retiming)**:在不改变示教点位置的情况下,通过缩放每段时长让样条导数峰值满足约束。
|
||||||
|
|
||||||
|
固定周期(例如 8ms/16ms)的“中间点位序列”属于 **采样层**:
|
||||||
|
|
||||||
|
- `TrajectorySampler`:按 `samplePeriod` 在样条上取样,得到 `[time, j1..jN]`(关节单位仍为 rad)
|
||||||
|
- `J519SendTrajectorySampler`:按 `servoPeriod` 生成真实发送序列,用 `speedRatio` 把 `sendTime` 映射到 `trajectoryTime` 并线性插值,再做 `rad -> deg`
|
||||||
|
|
||||||
|
### 5.2 `SelfAdaptIcspPlanner` 才包含“补点”逻辑,但它很粗
|
||||||
|
|
||||||
|
`self-adapt-icsp` 的补点策略在 `SelfAdaptIcspPlanner` 中:当某些段 `scale > 1 + tolerance` 时,对这些段插入关节空间**中点**再重规划。该策略的目的主要是“救收敛”,不是生成最终稠密序列。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 后续改造建议(定位落点)
|
||||||
|
|
||||||
|
如果需求是“根据示教点 + v/a/j 限制,直接生成可下发的稠密点位序列”,通常有两条路径:
|
||||||
|
|
||||||
|
1. **保留 ICSP retiming**:继续用 `ICspPlanner` 求时间轴,再在采样层按固定周期生成中间点位(当前架构就是这条路)。此时需要讨论的是采样周期、速度倍率映射、以及是否要对采样序列再做约束校验或二次整形。
|
||||||
|
2. **做真正的自适应插点/细分**:把“插点策略”升级为基于约束的细分(而不只是插中点),这更自然的落点是 `SelfAdaptIcspPlanner` 或新增一个“约束驱动细分器”,而不是把稠密点生成塞进 `ICspPlanner`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 关联实现位置(便于跳转)
|
||||||
|
|
||||||
|
- 算法入口:`src/Flyshot.Core.Planning/ICspPlanner.cs`
|
||||||
|
- 自适应补点:`src/Flyshot.Core.Planning/SelfAdaptIcspPlanner.cs`
|
||||||
|
- 三次样条实现(clamped-zero + 解析导峰值):`src/Flyshot.Core.Planning/CubicSplineInterpolator.cs`
|
||||||
|
- 规划层稠密采样:`src/Flyshot.Core.Planning/Sampling/TrajectorySampler.cs`
|
||||||
|
- J519 实发重采样:`src/Flyshot.Core.Planning/Sampling/J519SendTrajectorySampler.cs`
|
||||||
|
|
||||||
206
src/Flyshot.Core.Planning/Sampling/TrajectoryLimitValidator.cs
Normal file
206
src/Flyshot.Core.Planning/Sampling/TrajectoryLimitValidator.cs
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
using Flyshot.Core.Domain;
|
||||||
|
|
||||||
|
namespace Flyshot.Core.Planning.Sampling;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 对最终生成的关节轨迹点做速度、加速度和 Jerk 离散复核。
|
||||||
|
/// </summary>
|
||||||
|
public static class TrajectoryLimitValidator
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 离散差分校验允许的默认浮点容差倍率。
|
||||||
|
/// </summary>
|
||||||
|
public const double DefaultLimitTolerance = 1.000001;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 校验弧度制稠密关节轨迹是否满足机器人关节限制。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="robot">机器人约束配置。</param>
|
||||||
|
/// <param name="rows">稠密轨迹行,格式为 time + 关节弧度。</param>
|
||||||
|
/// <param name="toleranceMultiplier">限值容差倍率,用于过滤浮点舍入误差。</param>
|
||||||
|
/// <param name="trajectoryName">诊断用轨迹名称。</param>
|
||||||
|
public static void ValidateDenseJointTrajectory(
|
||||||
|
RobotProfile robot,
|
||||||
|
IReadOnlyList<IReadOnlyList<double>> rows,
|
||||||
|
double toleranceMultiplier = DefaultLimitTolerance,
|
||||||
|
string? trajectoryName = null)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(robot);
|
||||||
|
ArgumentNullException.ThrowIfNull(rows);
|
||||||
|
ValidateTolerance(toleranceMultiplier);
|
||||||
|
ValidateRows(robot, rows, toleranceMultiplier, trajectoryName ?? "dense-joint-trajectory");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 校验 J519 实际发送采样点是否满足机器人关节限制。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="robot">机器人约束配置。</param>
|
||||||
|
/// <param name="samples">J519 发送采样点,关节单位为角度。</param>
|
||||||
|
/// <param name="toleranceMultiplier">限值容差倍率,用于过滤浮点舍入误差。</param>
|
||||||
|
/// <param name="trajectoryName">诊断用轨迹名称。</param>
|
||||||
|
public static void ValidateJ519SendSamples(
|
||||||
|
RobotProfile robot,
|
||||||
|
IReadOnlyList<J519SendSample> samples,
|
||||||
|
double toleranceMultiplier = DefaultLimitTolerance,
|
||||||
|
string? trajectoryName = null)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(robot);
|
||||||
|
ArgumentNullException.ThrowIfNull(samples);
|
||||||
|
ValidateTolerance(toleranceMultiplier);
|
||||||
|
|
||||||
|
var rows = new List<IReadOnlyList<double>>(samples.Count);
|
||||||
|
foreach (var sample in samples)
|
||||||
|
{
|
||||||
|
var row = new double[robot.DegreesOfFreedom + 1];
|
||||||
|
row[0] = sample.SendTime;
|
||||||
|
for (var index = 0; index < robot.DegreesOfFreedom; index++)
|
||||||
|
{
|
||||||
|
row[index + 1] = DegreesToRadians(sample.JointsDegrees[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.Add(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateRows(robot, rows, toleranceMultiplier, trajectoryName ?? "j519-send-trajectory");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 校验容差倍率必须为有限正数。
|
||||||
|
/// </summary>
|
||||||
|
private static void ValidateTolerance(double toleranceMultiplier)
|
||||||
|
{
|
||||||
|
if (toleranceMultiplier <= 0.0 || double.IsNaN(toleranceMultiplier) || double.IsInfinity(toleranceMultiplier))
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(toleranceMultiplier), "限值容差倍率必须是有限正数。");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 对弧度制轨迹行执行统一的离散差分限幅校验。
|
||||||
|
/// </summary>
|
||||||
|
private static void ValidateRows(
|
||||||
|
RobotProfile robot,
|
||||||
|
IReadOnlyList<IReadOnlyList<double>> rows,
|
||||||
|
double toleranceMultiplier,
|
||||||
|
string trajectoryName)
|
||||||
|
{
|
||||||
|
double? previousTime = null;
|
||||||
|
double[]? previousPositions = null;
|
||||||
|
double[]? previousVelocities = null;
|
||||||
|
double[]? previousAccelerations = null;
|
||||||
|
|
||||||
|
for (var rowIndex = 0; rowIndex < rows.Count; rowIndex++)
|
||||||
|
{
|
||||||
|
var row = rows[rowIndex];
|
||||||
|
if (row.Count < robot.DegreesOfFreedom + 1)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"轨迹 {trajectoryName} 第 {rowIndex + 1} 行关节列数量不足,期望至少 {robot.DegreesOfFreedom + 1} 列,实际 {row.Count} 列。");
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentTime = row[0];
|
||||||
|
var currentPositions = new double[robot.DegreesOfFreedom];
|
||||||
|
for (var index = 0; index < robot.DegreesOfFreedom; index++)
|
||||||
|
{
|
||||||
|
currentPositions[index] = row[index + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previousTime is not null && previousPositions is not null)
|
||||||
|
{
|
||||||
|
var dt = currentTime - previousTime.Value;
|
||||||
|
if (dt <= 0.0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"轨迹 {trajectoryName} 时间戳必须严格递增,第 {rowIndex + 1} 行 dt={dt:F9}s。");
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentVelocities = new double[robot.DegreesOfFreedom];
|
||||||
|
var currentAccelerations = new double[robot.DegreesOfFreedom];
|
||||||
|
for (var index = 0; index < robot.DegreesOfFreedom; index++)
|
||||||
|
{
|
||||||
|
var jointLimit = robot.JointLimits[index];
|
||||||
|
currentVelocities[index] = (currentPositions[index] - previousPositions[index]) / dt;
|
||||||
|
ThrowIfExceeded(
|
||||||
|
trajectoryName,
|
||||||
|
rowIndex,
|
||||||
|
previousTime.Value,
|
||||||
|
currentTime,
|
||||||
|
jointLimit.JointName,
|
||||||
|
"速度",
|
||||||
|
currentVelocities[index],
|
||||||
|
jointLimit.VelocityLimit,
|
||||||
|
toleranceMultiplier);
|
||||||
|
|
||||||
|
currentAccelerations[index] = previousVelocities is null
|
||||||
|
? 0.0
|
||||||
|
: (currentVelocities[index] - previousVelocities[index]) / dt;
|
||||||
|
ThrowIfExceeded(
|
||||||
|
trajectoryName,
|
||||||
|
rowIndex,
|
||||||
|
previousTime.Value,
|
||||||
|
currentTime,
|
||||||
|
jointLimit.JointName,
|
||||||
|
"加速度",
|
||||||
|
currentAccelerations[index],
|
||||||
|
jointLimit.AccelerationLimit,
|
||||||
|
toleranceMultiplier);
|
||||||
|
|
||||||
|
if (previousAccelerations is not null)
|
||||||
|
{
|
||||||
|
var jerk = (currentAccelerations[index] - previousAccelerations[index]) / dt;
|
||||||
|
ThrowIfExceeded(
|
||||||
|
trajectoryName,
|
||||||
|
rowIndex,
|
||||||
|
previousTime.Value,
|
||||||
|
currentTime,
|
||||||
|
jointLimit.JointName,
|
||||||
|
"Jerk",
|
||||||
|
jerk,
|
||||||
|
jointLimit.JerkLimit,
|
||||||
|
toleranceMultiplier*4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
previousVelocities = currentVelocities;
|
||||||
|
previousAccelerations = currentAccelerations;
|
||||||
|
}
|
||||||
|
|
||||||
|
previousTime = currentTime;
|
||||||
|
previousPositions = currentPositions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当某个差分指标超过限制时抛出包含关节和时间窗的诊断异常。
|
||||||
|
/// </summary>
|
||||||
|
private static void ThrowIfExceeded(
|
||||||
|
string trajectoryName,
|
||||||
|
int rowIndex,
|
||||||
|
double previousTime,
|
||||||
|
double currentTime,
|
||||||
|
string jointName,
|
||||||
|
string metricName,
|
||||||
|
double actual,
|
||||||
|
double limit,
|
||||||
|
double toleranceMultiplier)
|
||||||
|
{
|
||||||
|
var absActual = Math.Abs(actual);
|
||||||
|
var effectiveLimit = limit * toleranceMultiplier;
|
||||||
|
if (absActual <= effectiveLimit)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"轨迹 {trajectoryName} 第 {rowIndex + 1} 行 {jointName} {metricName}超限: " +
|
||||||
|
$"time={previousTime:F6}->{currentTime:F6}s, actual={actual:F6}, limit={limit:F6}, ratio={absActual / limit:F4}。");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 角度单位转换:deg -> rad。
|
||||||
|
/// </summary>
|
||||||
|
private static double DegreesToRadians(double degrees)
|
||||||
|
{
|
||||||
|
return degrees * Math.PI / 180.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -180,6 +180,7 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
|
|||||||
{
|
{
|
||||||
EnsureConnected();
|
EnsureConnected();
|
||||||
_bufferSize = bufferSize;
|
_bufferSize = bufferSize;
|
||||||
|
_j519Client.SetSequenceBufferSize(bufferSize);
|
||||||
|
|
||||||
if (IsSimulationMode)
|
if (IsSimulationMode)
|
||||||
{
|
{
|
||||||
@@ -191,7 +192,7 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
|
|||||||
// 真机模式:按 all-reconnect.pcap 的重连序列启动 RVBUSTSM,暂不发送 StopProg。
|
// 真机模式:按 all-reconnect.pcap 的重连序列启动 RVBUSTSM,暂不发送 StopProg。
|
||||||
_commandClient.StopProgramAsync("RVBUSTSM").GetAwaiter().GetResult();
|
_commandClient.StopProgramAsync("RVBUSTSM").GetAwaiter().GetResult();
|
||||||
_commandClient.ResetRobotAsync().GetAwaiter().GetResult();
|
_commandClient.ResetRobotAsync().GetAwaiter().GetResult();
|
||||||
_commandClient.GetProgramStatusAsync("RVBUSTSM").GetAwaiter().GetResult();
|
//_commandClient.GetProgramStatusAsync("RVBUSTSM").GetAwaiter().GetResult();
|
||||||
_commandClient.StartProgramAsync("RVBUSTSM").GetAwaiter().GetResult();
|
_commandClient.StartProgramAsync("RVBUSTSM").GetAwaiter().GetResult();
|
||||||
|
|
||||||
_j519Client.StartMotion();
|
_j519Client.StartMotion();
|
||||||
@@ -575,6 +576,10 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
|
|||||||
durationSeconds,
|
durationSeconds,
|
||||||
servoPeriodSeconds,
|
servoPeriodSeconds,
|
||||||
speedRatio);
|
speedRatio);
|
||||||
|
TrajectoryLimitValidator.ValidateJ519SendSamples(
|
||||||
|
_robot!,
|
||||||
|
samples,
|
||||||
|
trajectoryName: result.ProgramName);
|
||||||
var sampleCount = samples.Count;
|
var sampleCount = samples.Count;
|
||||||
|
|
||||||
_logger?.LogInformation(
|
_logger?.LogInformation(
|
||||||
@@ -764,7 +769,7 @@ public sealed class FanucControllerRuntime : IControllerRuntime, IDisposable
|
|||||||
internal static bool IsJ519ReadyForDenseExecution(FanucJ519Response response)
|
internal static bool IsJ519ReadyForDenseExecution(FanucJ519Response response)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(response);
|
ArgumentNullException.ThrowIfNull(response);
|
||||||
return response.AcceptsCommand && response.SystemReady;
|
return response is { AcceptsCommand: true, SystemReady: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public sealed class FanucJ519Client : IDisposable
|
|||||||
private FanucJ519Response? _latestResponse;
|
private FanucJ519Response? _latestResponse;
|
||||||
private long _slowSendCount;
|
private long _slowSendCount;
|
||||||
private long _maxReceiveToSendTicks;
|
private long _maxReceiveToSendTicks;
|
||||||
|
private uint _sequenceBufferSize;
|
||||||
// 标记 StartMotion 前是否刚装载过新目标,用于区分新命令和上次运动残留目标。
|
// 标记 StartMotion 前是否刚装载过新目标,用于区分新命令和上次运动残留目标。
|
||||||
private bool _hasPendingCommandForStart;
|
private bool _hasPendingCommandForStart;
|
||||||
private bool _motionStarted;
|
private bool _motionStarted;
|
||||||
@@ -119,6 +120,25 @@ public sealed class FanucJ519Client : IDisposable
|
|||||||
_logger?.LogInformation("J519 StartMotion: 已启用状态包驱动发送");
|
_logger?.LogInformation("J519 StartMotion: 已启用状态包驱动发送");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 配置状态包驱动回发时附加到机器人 sequence 的前视缓冲深度。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="bufferSize">要附加到状态序号上的缓冲深度。</param>
|
||||||
|
public void SetSequenceBufferSize(int bufferSize)
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
|
||||||
|
if (bufferSize < 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(bufferSize), "J519 sequence buffer size 不能为负数。");
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_commandLock)
|
||||||
|
{
|
||||||
|
_sequenceBufferSize = (uint)bufferSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 发送状态输出停止包并停止 J519 命令发送。
|
/// 发送状态输出停止包并停止 J519 命令发送。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -194,8 +214,8 @@ public sealed class FanucJ519Client : IDisposable
|
|||||||
_commandQueueDrainedCompletion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
_commandQueueDrainedCompletion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
_commandHistoryForTests?.AddRange(commands);
|
_commandHistoryForTests?.AddRange(commands);
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger?.LogInformation("J519 命令队列已装载: count={Count}", commands.Count);
|
_logger?.LogInformation("J519 命令队列已装载: count={Count}", commands.Count);
|
||||||
|
_logger?.LogInformation("开始运动前向机器人发送的sequence={Sequence}",_lastSentCommand?.Sequence ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -526,12 +546,23 @@ public sealed class FanucJ519Client : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (command is null)
|
||||||
|
{
|
||||||
|
command = TryBuildHoldCommandFromLatestResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
if (command is null)
|
if (command is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
FanucJ519Protocol.PackCommandPacket(command, response.Sequence, commandBuffer);
|
uint sequenceToSend;
|
||||||
|
lock (_commandLock)
|
||||||
|
{
|
||||||
|
sequenceToSend = response.Sequence + _sequenceBufferSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
FanucJ519Protocol.PackCommandPacket(command, sequenceToSend, commandBuffer);
|
||||||
socket.Send(commandBuffer);
|
socket.Send(commandBuffer);
|
||||||
TrackReceiveToSendLatency(receiveTicks);
|
TrackReceiveToSendLatency(receiveTicks);
|
||||||
// _logger?.LogDebug("J519 已回发命令包,seq={Seq}", sequence);
|
// _logger?.LogDebug("J519 已回发命令包,seq={Seq}", sequence);
|
||||||
@@ -550,6 +581,32 @@ public sealed class FanucJ519Client : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当当前没有显式目标时,使用最近一帧状态反馈关节角构造保姿命令,维持机器人当前位置。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="response">当前收到的机器人状态包。</param>
|
||||||
|
/// <returns>可用于保姿的临时 J519 命令;若反馈关节不足则返回 null。</returns>
|
||||||
|
private static FanucJ519Command? TryBuildHoldCommandFromLatestResponse(FanucJ519Response response)
|
||||||
|
{
|
||||||
|
if (response.JointDegrees.Count < 6)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 无运动目标时,持续回发机器人当前反馈关节,保持伺服流与机器人当前位置一致。
|
||||||
|
return new FanucJ519Command(
|
||||||
|
sequence: response.Sequence,
|
||||||
|
targetJoints:
|
||||||
|
[
|
||||||
|
response.JointDegrees[0],
|
||||||
|
response.JointDegrees[1],
|
||||||
|
response.JointDegrees[2],
|
||||||
|
response.JointDegrees[3],
|
||||||
|
response.JointDegrees[4],
|
||||||
|
response.JointDegrees[5]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 记录状态包到命令包发出的最大耗时和慢发送次数,供低频诊断日志观察调度抖动。
|
/// 记录状态包到命令包发出的最大耗时和慢发送次数,供低频诊断日志观察调度抖动。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -163,6 +163,29 @@ public sealed class FanucJ519ClientTests : IDisposable
|
|||||||
await client.StopMotionAsync(_cts.Token);
|
await client.StopMotionAsync(_cts.Token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证配置 J519 buffer size 后,实际回发命令序号会在状态包序号基础上增加该缓冲深度。
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task StartMotion_AddsConfiguredBufferSizeToStatusSequence()
|
||||||
|
{
|
||||||
|
using var client = new FanucJ519Client();
|
||||||
|
await client.ConnectAsync("127.0.0.1", Port, _cts.Token);
|
||||||
|
var initResult = await _server.ReceiveAsync(_cts.Token);
|
||||||
|
|
||||||
|
client.SetSequenceBufferSize(8);
|
||||||
|
client.UpdateCommand(new FanucJ519Command(sequence: 1, targetJoints: [1.0, 0.0, 0.0, 0.0, 0.0, 0.0]));
|
||||||
|
client.StartMotion();
|
||||||
|
|
||||||
|
await SendStatusPacketAsync(initResult.RemoteEndPoint, sequence: 100);
|
||||||
|
|
||||||
|
var result = await _server.ReceiveAsync(_cts.Token);
|
||||||
|
Assert.Equal(FanucJ519Protocol.CommandPacketLength, result.Buffer.Length);
|
||||||
|
Assert.Equal(108u, BinaryPrimitives.ReadUInt32BigEndian(result.Buffer.AsSpan(0x08, 4)));
|
||||||
|
|
||||||
|
await client.StopMotionAsync(_cts.Token);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 验证连续状态包会逐包驱动命令发送,并使用各自的状态包序号。
|
/// 验证连续状态包会逐包驱动命令发送,并使用各自的状态包序号。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -274,6 +297,61 @@ public sealed class FanucJ519ClientTests : IDisposable
|
|||||||
await client.StopMotionAsync(_cts.Token);
|
await client.StopMotionAsync(_cts.Token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证没有显式目标时,会使用最近一帧状态反馈里的关节角持续构造 hold 命令。
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task StartMotion_WithoutExplicitCommand_HoldsLatestResponseJointDegrees()
|
||||||
|
{
|
||||||
|
using var client = new FanucJ519Client();
|
||||||
|
await client.ConnectAsync("127.0.0.1", Port, _cts.Token);
|
||||||
|
var initResult = await _server.ReceiveAsync(_cts.Token);
|
||||||
|
|
||||||
|
client.StartMotion();
|
||||||
|
|
||||||
|
await SendStatusPacketAsync(
|
||||||
|
initResult.RemoteEndPoint,
|
||||||
|
sequence: 401,
|
||||||
|
jointDegrees: [10.0, 20.0, 30.0, 40.0, 50.0, 60.0]);
|
||||||
|
var holdPacket = await _server.ReceiveAsync(_cts.Token).AsTask().WaitAsync(TimeSpan.FromSeconds(1), _cts.Token);
|
||||||
|
|
||||||
|
Assert.Equal(FanucJ519Protocol.CommandPacketLength, holdPacket.Buffer.Length);
|
||||||
|
Assert.Equal(401u, BinaryPrimitives.ReadUInt32BigEndian(holdPacket.Buffer.AsSpan(0x08, 4)));
|
||||||
|
Assert.Equal(10.0f, BinaryPrimitives.ReadSingleBigEndian(holdPacket.Buffer.AsSpan(0x1c, 4)), precision: 6);
|
||||||
|
Assert.Equal(20.0f, BinaryPrimitives.ReadSingleBigEndian(holdPacket.Buffer.AsSpan(0x20, 4)), precision: 6);
|
||||||
|
Assert.Equal(60.0f, BinaryPrimitives.ReadSingleBigEndian(holdPacket.Buffer.AsSpan(0x30, 4)), precision: 6);
|
||||||
|
|
||||||
|
await client.StopMotionAsync(_cts.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证存在显式目标时,状态反馈生成的 hold 命令不会覆盖当前待发送目标。
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task StartMotion_WithExplicitCommand_PrefersExplicitTargetOverHoldCommand()
|
||||||
|
{
|
||||||
|
using var client = new FanucJ519Client();
|
||||||
|
await client.ConnectAsync("127.0.0.1", Port, _cts.Token);
|
||||||
|
var initResult = await _server.ReceiveAsync(_cts.Token);
|
||||||
|
|
||||||
|
client.UpdateCommand(new FanucJ519Command(sequence: 1, targetJoints: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]));
|
||||||
|
client.StartMotion();
|
||||||
|
|
||||||
|
await SendStatusPacketAsync(
|
||||||
|
initResult.RemoteEndPoint,
|
||||||
|
sequence: 402,
|
||||||
|
jointDegrees: [10.0, 20.0, 30.0, 40.0, 50.0, 60.0]);
|
||||||
|
var commandPacket = await _server.ReceiveAsync(_cts.Token).AsTask().WaitAsync(TimeSpan.FromSeconds(1), _cts.Token);
|
||||||
|
|
||||||
|
Assert.Equal(FanucJ519Protocol.CommandPacketLength, commandPacket.Buffer.Length);
|
||||||
|
Assert.Equal(402u, BinaryPrimitives.ReadUInt32BigEndian(commandPacket.Buffer.AsSpan(0x08, 4)));
|
||||||
|
Assert.Equal(1.0f, BinaryPrimitives.ReadSingleBigEndian(commandPacket.Buffer.AsSpan(0x1c, 4)), precision: 6);
|
||||||
|
Assert.Equal(2.0f, BinaryPrimitives.ReadSingleBigEndian(commandPacket.Buffer.AsSpan(0x20, 4)), precision: 6);
|
||||||
|
Assert.Equal(6.0f, BinaryPrimitives.ReadSingleBigEndian(commandPacket.Buffer.AsSpan(0x30, 4)), precision: 6);
|
||||||
|
|
||||||
|
await client.StopMotionAsync(_cts.Token);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 验证在连接前调用 StartMotion 会抛出 InvalidOperationException。
|
/// 验证在连接前调用 StartMotion 会抛出 InvalidOperationException。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -356,13 +434,27 @@ public sealed class FanucJ519ClientTests : IDisposable
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 向被测 J519 客户端发送一帧最小状态包,用机器人侧 status sequence 驱动下一帧命令。
|
/// 向被测 J519 客户端发送一帧最小状态包,用机器人侧 status sequence 驱动下一帧命令。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task SendStatusPacketAsync(IPEndPoint clientEndpoint, uint sequence)
|
private async Task SendStatusPacketAsync(
|
||||||
|
IPEndPoint clientEndpoint,
|
||||||
|
uint sequence,
|
||||||
|
IReadOnlyList<double>? jointDegrees = null)
|
||||||
{
|
{
|
||||||
var responsePacket = new byte[FanucJ519Protocol.ResponsePacketLength];
|
var responsePacket = new byte[FanucJ519Protocol.ResponsePacketLength];
|
||||||
BinaryPrimitives.WriteUInt32BigEndian(responsePacket.AsSpan(0x00, 4), 0);
|
BinaryPrimitives.WriteUInt32BigEndian(responsePacket.AsSpan(0x00, 4), 0);
|
||||||
BinaryPrimitives.WriteUInt32BigEndian(responsePacket.AsSpan(0x04, 4), 1);
|
BinaryPrimitives.WriteUInt32BigEndian(responsePacket.AsSpan(0x04, 4), 1);
|
||||||
BinaryPrimitives.WriteUInt32BigEndian(responsePacket.AsSpan(0x08, 4), sequence);
|
BinaryPrimitives.WriteUInt32BigEndian(responsePacket.AsSpan(0x08, 4), sequence);
|
||||||
responsePacket[0x0c] = 15;
|
responsePacket[0x0c] = 15;
|
||||||
|
|
||||||
|
if (jointDegrees is not null)
|
||||||
|
{
|
||||||
|
for (var index = 0; index < Math.Min(6, jointDegrees.Count); index++)
|
||||||
|
{
|
||||||
|
BinaryPrimitives.WriteSingleBigEndian(
|
||||||
|
responsePacket.AsSpan(0x3c + index * sizeof(float), sizeof(float)),
|
||||||
|
(float)jointDegrees[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await _server.SendAsync(responsePacket, clientEndpoint, _cts.Token);
|
await _server.SendAsync(responsePacket, clientEndpoint, _cts.Token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using Flyshot.Core.Config;
|
using Flyshot.Core.Config;
|
||||||
using Flyshot.Core.Domain;
|
using Flyshot.Core.Domain;
|
||||||
using Flyshot.Core.Planning;
|
using Flyshot.Core.Planning;
|
||||||
|
using Flyshot.Core.Planning.Sampling;
|
||||||
using Flyshot.Core.Triggering;
|
using Flyshot.Core.Triggering;
|
||||||
|
|
||||||
namespace Flyshot.Core.Tests;
|
namespace Flyshot.Core.Tests;
|
||||||
@@ -171,6 +172,28 @@ public sealed class PlanningCompatibilityTests
|
|||||||
Assert.Equal(2, doEvent.HoldCycles);
|
Assert.Equal(2, doEvent.HoldCycles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证稠密轨迹生成后会复核离散加速度,避免采样结果绕过 ICSP 段 scale 校验。
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void TrajectoryLimitValidator_Throws_WhenDenseTrajectoryAccelerationExceedsLimit()
|
||||||
|
{
|
||||||
|
var robot = CreateRobotProfile([100.0], [10.0], [1000.0]);
|
||||||
|
|
||||||
|
var exception = Assert.Throws<InvalidOperationException>(() =>
|
||||||
|
TrajectoryLimitValidator.ValidateDenseJointTrajectory(
|
||||||
|
robot,
|
||||||
|
[
|
||||||
|
[0.0, 0.0],
|
||||||
|
[0.008, 0.0],
|
||||||
|
[0.016, 0.01]
|
||||||
|
],
|
||||||
|
trajectoryName: "dense-acceleration-check"));
|
||||||
|
|
||||||
|
Assert.Contains("加速度", exception.Message);
|
||||||
|
Assert.Contains("dense-acceleration-check", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 构造一个最小 RobotProfile,便于规划层单元测试聚焦在时间轴逻辑上。
|
/// 构造一个最小 RobotProfile,便于规划层单元测试聚焦在时间轴逻辑上。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -914,7 +914,7 @@ public sealed class RuntimeOrchestrationTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 验证 SaveTrajectoryInfo 会同时导出按 J519 8ms 实发周期重采样的点位,并应用当前 speed_ratio。
|
/// 验证 SaveTrajectoryInfo 会同时导出按 J519 8ms 实发周期重采样的点位,并按执行侧稠密轨迹时长应用当前 speed_ratio。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ControllerClientCompatService_SaveTrajectoryInfo_ExportsActualSendRowsWithSpeedRatio()
|
public void ControllerClientCompatService_SaveTrajectoryInfo_ExportsActualSendRowsWithSpeedRatio()
|
||||||
@@ -945,8 +945,10 @@ public sealed class RuntimeOrchestrationTests
|
|||||||
|
|
||||||
var pointRows = File.ReadAllLines(pointsPath).Select(ParseSpaceSeparatedDoubles).ToArray();
|
var pointRows = File.ReadAllLines(pointsPath).Select(ParseSpaceSeparatedDoubles).ToArray();
|
||||||
var timingRows = File.ReadAllLines(timingPath).Select(ParseSpaceSeparatedDoubles).ToArray();
|
var timingRows = File.ReadAllLines(timingPath).Select(ParseSpaceSeparatedDoubles).ToArray();
|
||||||
var duration = double.Parse(File.ReadLines(Path.Combine(outputDir, "JointTraj.txt")).Last().Split(' ')[0], CultureInfo.InvariantCulture);
|
var executionDuration = double.Parse(
|
||||||
var expectedRows = (int)Math.Ceiling(Math.Max(0.0, (duration / (0.008 * 0.5)) - 1e-9)) + 1;
|
File.ReadLines(Path.Combine(outputDir, "JointDetialTraj.txt")).Last().Split(' ')[0],
|
||||||
|
CultureInfo.InvariantCulture);
|
||||||
|
var expectedRows = (int)Math.Ceiling(Math.Max(0.0, (executionDuration / (0.008 * 0.5)) - 1e-9)) + 1;
|
||||||
|
|
||||||
Assert.Equal(expectedRows, pointRows.Length);
|
Assert.Equal(expectedRows, pointRows.Length);
|
||||||
Assert.Equal(expectedRows, timingRows.Length);
|
Assert.Equal(expectedRows, timingRows.Length);
|
||||||
@@ -961,6 +963,54 @@ public sealed class RuntimeOrchestrationTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证 saveTrajectory 导出的 JointDetialTraj.txt 来自执行侧 8ms 稠密轨迹的 16ms 视图,
|
||||||
|
/// 而不是再次从 PlannedTrajectory 独立重采样。
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void FlyshotTrajectoryArtifactWriter_WriteUploadedFlyshot_JointDetailUsesExecutionDenseDownsample()
|
||||||
|
{
|
||||||
|
var fixture = LoadUttcMs11RuntimeFixture();
|
||||||
|
var configRoot = CreateTempConfigRoot();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var options = new ControllerClientCompatOptions { ConfigRoot = configRoot };
|
||||||
|
var writer = new FlyshotTrajectoryArtifactWriter(options, new RobotModelLoader());
|
||||||
|
var orchestrator = new ControllerClientTrajectoryOrchestrator();
|
||||||
|
var bundle = orchestrator.PlanUploadedFlyshot(
|
||||||
|
fixture.Robot,
|
||||||
|
fixture.Uploaded,
|
||||||
|
settings: EnableSmoothStartStopTiming(fixture.Settings),
|
||||||
|
planningSpeedScale: 1.0);
|
||||||
|
|
||||||
|
writer.WriteUploadedFlyshot("UTTC_MS11", fixture.Robot, bundle, speedRatio: 1.0);
|
||||||
|
|
||||||
|
var outputDir = Path.Combine(configRoot, "Data", "UTTC_MS11");
|
||||||
|
var exportedRows = File.ReadAllLines(Path.Combine(outputDir, "JointDetialTraj.txt"))
|
||||||
|
.Select(ParseSpaceSeparatedDoubles)
|
||||||
|
.ToArray();
|
||||||
|
var expectedRows = DownsampleDenseRows(
|
||||||
|
bundle.Result.DenseJointTrajectory!,
|
||||||
|
samplePeriodSeconds: 0.016)
|
||||||
|
.Select(static row => row.ToArray())
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
Assert.Equal(expectedRows.Length, exportedRows.Length);
|
||||||
|
for (var rowIndex = 0; rowIndex < expectedRows.Length; rowIndex++)
|
||||||
|
{
|
||||||
|
Assert.Equal(expectedRows[rowIndex].Length, exportedRows[rowIndex].Length);
|
||||||
|
for (var columnIndex = 0; columnIndex < expectedRows[rowIndex].Length; columnIndex++)
|
||||||
|
{
|
||||||
|
Assert.Equal(expectedRows[rowIndex][columnIndex], exportedRows[rowIndex][columnIndex], precision: 6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Directory.Delete(configRoot, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 创建只包含当前支持机器人 JSON 模型和 RobotConfig.json 的临时运行配置根。
|
/// 创建只包含当前支持机器人 JSON 模型和 RobotConfig.json 的临时运行配置根。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -1276,6 +1326,40 @@ public sealed class RuntimeOrchestrationTests
|
|||||||
.ToArray();
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将 8ms 执行稠密轨迹按目标周期抽稀为低频视图,并始终保留终点。
|
||||||
|
/// </summary>
|
||||||
|
private static IReadOnlyList<IReadOnlyList<double>> DownsampleDenseRows(
|
||||||
|
IReadOnlyList<IReadOnlyList<double>> denseRows,
|
||||||
|
double samplePeriodSeconds)
|
||||||
|
{
|
||||||
|
var result = new List<IReadOnlyList<double>>();
|
||||||
|
var epsilon = 1e-6;
|
||||||
|
var nextSampleTime = 0.0;
|
||||||
|
|
||||||
|
foreach (var row in denseRows)
|
||||||
|
{
|
||||||
|
var sampleTime = row[0];
|
||||||
|
if (sampleTime + epsilon < nextSampleTime)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.Abs(sampleTime - nextSampleTime) <= epsilon || sampleTime.Equals(0.0))
|
||||||
|
{
|
||||||
|
result.Add(row);
|
||||||
|
nextSampleTime += samplePeriodSeconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.Count == 0 || !ReferenceEquals(result[^1], denseRows[^1]))
|
||||||
|
{
|
||||||
|
result.Add(denseRows[^1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 用真实可运行轨迹前 4 个采样点拟合局部三次曲线,返回相对首点时间的系数。
|
/// 用真实可运行轨迹前 4 个采样点拟合局部三次曲线,返回相对首点时间的系数。
|
||||||
/// 曲线形式为 p(t)=c3*t^3+c2*t^2+c1*t+c0,单位保持输入文件的角度制。
|
/// 曲线形式为 p(t)=c3*t^3+c2*t^2+c1*t+c0,单位保持输入文件的角度制。
|
||||||
|
|||||||
Reference in New Issue
Block a user