diff --git a/devTools/photon.lua b/devTools/photon.lua new file mode 100644 index 000000000..d1dd06567 --- /dev/null +++ b/devTools/photon.lua @@ -0,0 +1,165 @@ +-- PhotonVision Time Synchronization Protocol Dissector +-- Protocol runs on UDP port 5810 +-- Reference: https://docs.photonvision.org/en/v2026.0.0-alpha-1/docs/contributing/design-descriptions/time-sync.html + +photon_timesync_proto = Proto("photon_timesync", "PhotonVision Time Sync Protocol") + +-- Protocol fields +local pf_version = ProtoField.uint8("photon_timesync.version", "Version", base.DEC) +local pf_message_id = ProtoField.uint8("photon_timesync.message_id", "Message ID", base.DEC, { + [0] = "Ping", + [1] = "Pong" +}) +local pf_client_time = ProtoField.uint64("photon_timesync.client_time", "Client Time (μs)", base.DEC) +local pf_server_time = ProtoField.uint64("photon_timesync.server_time", "Server Time (μs)", base.DEC) +local pf_response_in = ProtoField.framenum("photon_timesync.response_in", "Response In Frame", base.NONE, + frametype.RESPONSE) +local pf_response_to = ProtoField.framenum("photon_timesync.response_to", "Response To Frame", base.NONE, + frametype.REQUEST) +local pf_response_time = ProtoField.relative_time("photon_timesync.response_time", "Response Time") + +-- Register fields +photon_timesync_proto.fields = { + pf_version, + pf_message_id, + pf_client_time, + pf_server_time, + pf_response_in, + pf_response_to, + pf_response_time +} + +-- Table to track ping/pong relationships +-- Key: client_time as string, Value: frame number of ping +local ping_table = {} +-- Table to store pong responses for pings +-- Key: ping frame number, Value: pong frame number +local pong_table = {} + +-- Dissector function +function photon_timesync_proto.dissector(buffer, pinfo, tree) + -- Check if buffer has minimum length (TspPing = 10 bytes) + local length = buffer:len() + if length < 10 then + return 0 + end + + -- Set protocol column + pinfo.cols.protocol = photon_timesync_proto.name + + -- Create protocol tree + local subtree = tree:add(photon_timesync_proto, buffer(), "PhotonVision Time Sync Protocol Data") + + -- Parse version (1 byte) + local version = buffer(0, 1):uint() + subtree:add(pf_version, buffer(0, 1)) + + -- Parse message_id (1 byte) + local msg_id = buffer(1, 1):uint() + subtree:add(pf_message_id, buffer(1, 1)) + + -- Parse client_time (8 bytes, little-endian uint64) + local client_time = buffer(2, 8):le_uint64() + subtree:add_le(pf_client_time, buffer(2, 8)) + + -- Convert client_time to string for use as key + local client_time_key = tostring(client_time) + local frame_num = pinfo.number + + -- Track relationships between ping and pong + if not pinfo.visited then + -- First pass: build the relationship tables + if msg_id == 1 then + -- This is a Ping - store it + ping_table[client_time_key] = frame_num + elseif msg_id == 2 then + -- This is a Pong - find matching Ping + local ping_frame = ping_table[client_time_key] + if ping_frame then + pong_table[ping_frame] = frame_num + end + end + end + + -- Update info column and parse based on message type + if msg_id == 1 then + -- TspPing: version(1) + message_id(1) + client_time(8) = 10 bytes + pinfo.cols.info = string.format("Time Sync Ping (client_time: %s μs)", tostring(client_time)) + + -- Check if we have a response for this ping + local pong_frame = pong_table[frame_num] + if pong_frame then + local response_item = subtree:add(pf_response_in, pong_frame) + response_item:set_generated() + end + elseif msg_id == 2 then + -- TspPong: TspPing + server_time(8) = 18 bytes + pinfo.cols.info = "Time Sync Pong" + + if length >= 18 then + local server_time = buffer(10, 8):le_uint64() + subtree:add_le(pf_server_time, buffer(10, 8)) + pinfo.cols.info = string.format("Time Sync Pong (client: %s, server: %s μs)", + tostring(client_time), tostring(server_time)) + + -- Find the matching ping frame + local ping_frame = ping_table[client_time_key] + if ping_frame then + local request_item = subtree:add(pf_response_to, ping_frame) + request_item:set_generated() + + -- Calculate response time if we can get the ping packet + local ping_time = pinfo.abs_ts - pinfo.rel_ts + -- Note: This is an approximation. For accurate timing, we'd need to + -- store the timestamp of the ping packet + end + end + else + pinfo.cols.info = string.format("Time Sync Unknown (ID: %d)", msg_id) + end + + return length +end + +-- Register dissector on UDP port 5810 +local udp_port = DissectorTable.get("udp.port") +udp_port:add(5810, photon_timesync_proto) + +-- Heuristic dissector function +local function heuristic_checker(buffer, pinfo, tree) + local length = buffer:len() + + -- Check minimum length (TspPing = 10 bytes) + if length < 10 then + return false + end + + local version = buffer(0, 1):uint() + local msg_id = buffer(1, 1):uint() + + -- Check if this looks like our protocol + -- Version should be reasonable (0-10), message_id should be 1 or 2 + if version <= 10 and (msg_id == 1 or msg_id == 2) then + -- Validate packet structure + if msg_id == 1 and length == 10 then + -- TspPing is exactly 10 bytes + photon_timesync_proto.dissector(buffer, pinfo, tree) + return true + elseif msg_id == 2 and length == 18 then + -- TspPong is exactly 18 bytes + photon_timesync_proto.dissector(buffer, pinfo, tree) + return true + end + end + + return false +end + +-- Register heuristic dissector +photon_timesync_proto:register_heuristic("udp", heuristic_checker) + +-- Initialize function to reset tables on new capture +function photon_timesync_proto.init() + ping_table = {} + pong_table = {} +end diff --git a/docs/source/docs/contributing/design-descriptions/images/wireshark.jpg b/docs/source/docs/contributing/design-descriptions/images/wireshark.jpg new file mode 100644 index 000000000..a9c358e4f Binary files /dev/null and b/docs/source/docs/contributing/design-descriptions/images/wireshark.jpg differ diff --git a/docs/source/docs/contributing/design-descriptions/time-sync.md b/docs/source/docs/contributing/design-descriptions/time-sync.md index 6d051c566..793e85182 100644 --- a/docs/source/docs/contributing/design-descriptions/time-sync.md +++ b/docs/source/docs/contributing/design-descriptions/time-sync.md @@ -109,3 +109,13 @@ Clients may publish statistics to NetworkTables. If they do, they shall publish | rtt2_us | Integer | The time in us from last complete (ping transmission to pong reception) | PhotonVision has chosen to publish to the sub-table `/photonvision/.timesync/{DEVICE_HOSTNAME}`. Future implementations of this protocol may decide to implement this as a structured data type. + +## Wireshark Dissector + +![](images/wireshark.jpg) + +A [WireShark dissector](https://raw.githubusercontent.com/PhotonVision/photonvision/refs/heads/main/devTools/photon.lua) created for Wireshark ~=4.6 can be used to inspect Time Syncronization messages. Copy the dissector to your Wireshark plugin directory (for me, that's `C:\Users\Me\AppData\Roaming\Wireshark\plugins`), and open the capture. Because TSP uses UDP Unicast, data must be collected on the coprocessor or robot processor using a command similar to: + +``` +sudo tcpdump -i any port 5810 -w tsp_capture.pcap +```