feat(fanuc): 打通飞拍轨迹完整执行链路

* 增加 J519 稠密发送采样校验与保姿回发逻辑
* 调整 saveTrajectory 导出与 sequence buffer 行为
* 补充 10010 解析脚本、ICSP 说明和回归测试
This commit is contained in:
2026-05-08 13:25:02 +08:00
parent c6829d214a
commit 1779067b5c
13 changed files with 1363 additions and 33 deletions

View 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)