mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-19 00:41:41 +00:00
## Description <img width="1918" height="1030" alt="image" src="https://github.com/user-attachments/assets/5af1a5ee-012d-461d-9162-2d4de6ad0c62" /> A wireshark dissector can be handy for quickly visualizing time sync messages. See the docs for how to use this! Full disclosure -- this dissector was generated by Claude 4.5, and I spot-checked all the numbers for correctness. This seems like idiomatic Lua to me, but I don't know Lua at all. I don't see a nice QOL thing nobody else will use as being a tech debt concern. ## Meta Merge checklist: - [x] Pull Request title is [short, imperative summary](https://cbea.ms/git-commit/) of proposed changes - [x] The description documents the _what_ and _why_ - [ ] If this PR changes behavior or adds a feature, user documentation is updated - [ ] If this PR touches photon-serde, all messages have been regenerated and hashes have not changed unexpectedly - [ ] If this PR touches configuration, this is backwards compatible with settings back to v2025.3.2 - [ ] If this PR touches pipeline settings or anything related to data exchange, the frontend typing is updated - [ ] If this PR addresses a bug, a regression test for it is added
166 lines
5.4 KiB
Lua
166 lines
5.4 KiB
Lua
-- 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
|