Wireshark dissector for Pro DJ Link protocol
1-- Pro DJ Link Full Wireshark Dissector (Lua)
2-- Supports UDP Ports 50000, 50001, 50002 and TCP DB Server traffic
3
4local p_djl = Proto("pdjl", "Pro DJ Link")
5
6-- ============================================================================
7-- COMMON FIELDS
8-- ============================================================================
9local f_header = ProtoField.string("pdjl.header", "Header")
10local f_type = ProtoField.uint8("pdjl.type", "Packet Type", base.HEX)
11local f_device_name = ProtoField.string("pdjl.device_name", "Device Name")
12local f_dev_num = ProtoField.uint8("pdjl.device_number", "Device Number", base.DEC)
13
14-- Port 50000 (Management)
15local f_mac = ProtoField.ether("pdjl.mac", "MAC Address")
16local f_ip = ProtoField.ipv4("pdjl.ip", "IP Address")
17local f_sequence = ProtoField.uint8("pdjl.sequence", "Sequence", base.DEC)
18local f_dev_type = ProtoField.uint8("pdjl.device_type", "Device Type", base.HEX)
19local f_peer_count = ProtoField.uint8("pdjl.peer_count", "Peer Count", base.DEC)
20
21-- Port 50001 (Beat Sync)
22local f_bpm_raw = ProtoField.uint16("pdjl.bpm_raw", "BPM (Raw x100)", base.DEC)
23local f_pitch = ProtoField.uint32("pdjl.pitch", "Pitch (Raw)", base.HEX)
24local f_beat_in_bar = ProtoField.uint8("pdjl.beat_in_bar", "Beat in Bar", base.DEC)
25local f_next_beat = ProtoField.uint32("pdjl.next_beat", "Next Beat (ms)", base.DEC)
26local f_second_beat = ProtoField.uint32("pdjl.second_beat", "Second Beat (ms)", base.DEC)
27local f_next_bar = ProtoField.uint32("pdjl.next_bar", "Next Bar (ms)", base.DEC)
28local f_fourth_beat = ProtoField.uint32("pdjl.fourth_beat", "Fourth Beat (ms)", base.DEC)
29local f_second_bar = ProtoField.uint32("pdjl.second_bar", "Second Bar (ms)", base.DEC)
30local f_eighth_beat = ProtoField.uint32("pdjl.eighth_beat", "Eighth Beat (ms)", base.DEC)
31local f_track_len = ProtoField.uint32("pdjl.track_length", "Track Length (s)", base.DEC)
32local f_playhead = ProtoField.uint32("pdjl.playhead", "Playhead (ms)", base.DEC)
33local f_pitch_abs = ProtoField.int32("pdjl.pitch_abs", "Position Pitch", base.DEC)
34local f_bpm_abs = ProtoField.uint32("pdjl.bpm_abs", "Position BPM", base.DEC)
35local f_sync_cmd = ProtoField.uint8("pdjl.sync_cmd", "Sync Command", base.HEX)
36
37-- Port 50002 (Status)
38local f_rb_id = ProtoField.uint32("pdjl.rekordbox_id", "Rekordbox ID", base.DEC)
39local f_track_num = ProtoField.uint16("pdjl.track_number", "Track Number", base.DEC)
40local f_track_dev = ProtoField.uint8("pdjl.track_device", "Track Host Device", base.DEC)
41local f_slot = ProtoField.uint8("pdjl.slot", "Slot", base.HEX)
42local f_track_type = ProtoField.uint8("pdjl.track_type", "Track Type", base.HEX)
43local f_activity = ProtoField.uint8("pdjl.activity", "Activity", base.HEX)
44local f_firmware = ProtoField.string("pdjl.firmware", "Firmware")
45local f_beat_count = ProtoField.uint32("pdjl.beat_count", "Beat Count", base.DEC)
46local f_play_mode_1 = ProtoField.uint8("pdjl.play_mode_1", "Play Mode 1", base.HEX)
47local f_play_mode_2 = ProtoField.uint8("pdjl.play_mode_2", "Play Mode 2", base.HEX)
48local f_pitch_1 = ProtoField.uint32("pdjl.pitch_1", "Effective Pitch", base.HEX)
49local f_pitch_2 = ProtoField.uint32("pdjl.pitch_2", "Local Pitch", base.HEX)
50local f_master_info = ProtoField.uint8("pdjl.master_info", "Master Info", base.HEX)
51local f_handoff = ProtoField.uint8("pdjl.handoff", "Master Handoff", base.DEC)
52local f_countdown = ProtoField.uint16("pdjl.cue_countdown", "Cue Countdown", base.DEC)
53local f_pitch_3 = ProtoField.uint32("pdjl.pitch_3", "Pitch 3", base.HEX)
54
55local f_queried_ip = ProtoField.ipv4("pdjl.queried_ip", "Queried IP")
56local f_queried_dev = ProtoField.uint8("pdjl.queried_device", "Target Device", base.DEC)
57local f_queried_slot = ProtoField.uint8("pdjl.queried_slot", "Target Slot", base.HEX)
58local f_slot_owner = ProtoField.uint8("pdjl.slot_owner", "Slot Owner Device", base.DEC)
59local f_playlist_count = ProtoField.uint16("pdjl.playlist_count", "Playlist Count", base.DEC)
60local f_total_media_space = ProtoField.uint64("pdjl.total_media_space", "Total Space", base.DEC)
61local f_avail_media_space = ProtoField.uint64("pdjl.avail_media_space", "Available Space", base.DEC)
62local f_interface_color = ProtoField.uint8("pdjl.interface_color", "Interface Color", base.DEC, INTERFACE_COLORS)
63local f_settings_present = ProtoField.bool("pdjl.settings_present", "MYSETTINGS.DAT present", 8, nil, 0x01)
64
65
66local f_media_type = ProtoField.uint8("pdjl.media_type", "Media Type", base.HEX)
67local f_media_status = ProtoField.uint8("pdjl.media_status", "Media Status", base.HEX)
68local f_media_tracks = ProtoField.uint16("pdjl.media_tracks", "Media Tracks", base.DEC)
69local f_media_name = ProtoField.string("pdjl.media_name", "Media Name")
70local f_media_db_date= ProtoField.string("pdjl.media_db_date", "DB Creation Date")
71
72local f_flags = ProtoField.uint8("pdjl.flags", "Status Flags", base.HEX)
73local f_flag_playing= ProtoField.bool("pdjl.flags.playing", "Is Playing", 8, nil, 0x40)
74local f_flag_master = ProtoField.bool("pdjl.flags.master", "Is Master", 8, nil, 0x20)
75local f_flag_sync = ProtoField.bool("pdjl.flags.sync", "Is Syncing", 8, nil, 0x10)
76local f_flag_onair = ProtoField.bool("pdjl.flags.onair", "Is On Air", 8, nil, 0x08)
77local f_flag_bpm = ProtoField.bool("pdjl.flags.bpm", "BPM Sync Active", 8, nil, 0x02)
78
79-- TCP DB Server
80local f_db_magic = ProtoField.uint32("pdjl.db.magic", "DB Magic", base.HEX)
81local f_db_tx_id = ProtoField.uint32("pdjl.db.tx_id", "Transaction ID", base.HEX)
82local f_db_msg_type = ProtoField.uint16("pdjl.db.msg_type", "DB Message Type", base.HEX)
83local f_db_arg_count= ProtoField.uint8("pdjl.db.arg_count", "Argument Count", base.DEC)
84local f_db_tag_blob = ProtoField.bytes("pdjl.db.tags", "Argument Tags")
85
86local f_db_field_u8 = ProtoField.uint8("pdjl.db.field.u8", "Field (U8)", base.DEC)
87local f_db_field_u16= ProtoField.uint16("pdjl.db.field.u16", "Field (U16)", base.DEC)
88local f_db_field_u32= ProtoField.uint32("pdjl.db.field.u32", "Field (U32)", base.HEX)
89local f_db_field_u64= ProtoField.uint64("pdjl.db.field.u64", "Field (U64)", base.DEC)
90local f_db_field_str= ProtoField.string("pdjl.db.field.str", "Field (String)")
91local f_db_field_bin= ProtoField.bytes("pdjl.db.field.bin", "Field (Blob)")
92
93-- DMST decoding
94local f_dmst = ProtoField.uint32("pdjl.dmst", "DMST Parameter", base.HEX)
95local f_dmst_dev = ProtoField.uint8("pdjl.dmst.device", "Device", base.DEC)
96local f_dmst_menu = ProtoField.uint8("pdjl.dmst.menu", "Menu", base.HEX)
97local f_dmst_slot = ProtoField.uint8("pdjl.dmst.slot", "Slot", base.HEX)
98local f_dmst_type = ProtoField.uint8("pdjl.dmst.type", "Type", base.HEX)
99
100local f_db_port = ProtoField.uint16("pdjl.db_port", "DB Server Port", base.DEC)
101local f_raw_payload = ProtoField.bytes("pdjl.raw_payload", "Raw Payload")
102
103p_djl.fields = {
104 f_header, f_type, f_device_name, f_dev_num,
105 f_mac, f_ip, f_sequence, f_dev_type, f_peer_count,
106 f_bpm_raw, f_pitch, f_beat_in_bar, f_next_beat, f_second_beat, f_next_bar, f_fourth_beat, f_second_bar, f_eighth_beat,
107 f_track_len, f_playhead, f_pitch_abs, f_bpm_abs, f_sync_cmd,
108 f_rb_id, f_track_num, f_track_dev, f_slot, f_track_type, f_activity, f_firmware, f_beat_count,
109 f_play_mode_1, f_play_mode_2, f_pitch_1, f_pitch_2, f_master_info, f_handoff, f_countdown, f_pitch_3,
110 f_queried_ip, f_queried_dev, f_queried_slot, f_slot_owner, f_playlist_count, f_total_media_space, f_avail_media_space,
111 f_interface_color, f_settings_present, f_media_type, f_media_status, f_media_tracks, f_media_name, f_media_db_date,
112 f_flags, f_flag_playing, f_flag_master, f_flag_sync, f_flag_onair, f_flag_bpm,
113 f_db_magic, f_db_tx_id, f_db_msg_type, f_db_arg_count, f_db_tag_blob,
114 f_db_field_u8, f_db_field_u16, f_db_field_u32, f_db_field_u64, f_db_field_str, f_db_field_bin,
115 f_dmst, f_dmst_dev, f_dmst_menu, f_dmst_slot, f_dmst_type,
116 f_db_port, f_raw_payload
117}
118
119-- ============================================================================
120-- HELPERS
121-- ============================================================================
122local PACKET_TYPES = {
123 [0x00] = "Channel Claim 1",
124 [0x02] = "Channel Claim 2",
125 [0x04] = "Channel Claim Final",
126 [0x06] = "Keep-Alive",
127 [0x0a] = "CDJ Status / Announcement",
128 [0x0b] = "Absolute Position",
129 [0x28] = "Beat",
130 [0x2a] = "Sync Control",
131 [0x29] = "Mixer Status"
132}
133
134local DB_MSG_TYPES = {
135 [0x0000] = "GetQueryContext",
136 [0x4000] = "Success/Ack",
137 [0x1001] = "Disconnect",
138 [0x2002] = "GetMetadata",
139 [0x3000] = "RenderMenu",
140 [0x4001] = "MenuHeader",
141 [0x4101] = "MenuItem",
142 [0x4201] = "MenuFooter",
143 [0x2003] = "GetArtwork",
144 [0x4002] = "ArtworkResp",
145 [0x2204] = "GetBeatGrid",
146 [0x4602] = "BeatGridResp",
147 [0x2104] = "GetCuePoints",
148 [0x4702] = "CuePointsResp",
149 [0x2b04] = "GetExtCuePoints",
150 [0x4e02] = "ExtCuePointsResp",
151 [0x2004] = "GetWaveformPreview",
152 [0x4402] = "WaveformPreviewResp",
153 [0x2904] = "GetWaveformDetail",
154 [0x4a02] = "WaveformDetailResp",
155 [0x2c04] = "GetAnalysisTag",
156 [0x4f02] = "AnalysisTagResp",
157 [0x1105] = "GetTrackCount",
158 [0x5105] = "TrackCountResp",
159 [0x1103] = "GetTrackList",
160 [0x5103] = "TrackListResp",
161}
162
163local PLAY_MODES = {
164 [0x00] = "No Track",
165 [0x02] = "Loading",
166 [0x03] = "Playing",
167 [0x04] = "Looping",
168 [0x05] = "Paused",
169 [0x06] = "Paused at Cue",
170 [0x07] = "Cue Play",
171 [0x08] = "Cue Scratch",
172 [0x09] = "Searching",
173 [0x0e] = "CD Spun Down",
174 [0x11] = "Ended",
175}
176
177local TRACK_TYPES = {
178 [0x00] = "None",
179 [0x01] = "Rekordbox",
180 [0x02] = "Unanalyzed",
181 [0x05] = "Audio CD",
182}
183
184local SYNC_COMMANDS = {
185 [0x10] = "Turn On Sync",
186 [0x20] = "Turn Off Sync",
187 [0x01] = "Become Master",
188}
189
190local DEV_TYPES = {
191 [0x01] = "Player (CDJ)",
192 [0x02] = "Player (XDJ)",
193 [0x03] = "Mixer",
194 [0x04] = "PC/Rekordbox",
195}
196
197local INTERFACE_COLORS = {
198 [0] = "Default",
199 [1] = "Pink",
200 [2] = "Red",
201 [3] = "Orange",
202 [4] = "Yellow",
203 [5] = "Green",
204 [6] = "Aqua",
205 [7] = "Blue",
206 [8] = "Purple",
207}
208
209local MENU_ITEM_TYPES = {
210 [0x0002] = "Album",
211 [0x0004] = "TrackTitle",
212 [0x0006] = "Genre",
213 [0x0007] = "Artist",
214 [0x000a] = "Rating",
215 [0x000b] = "Duration",
216 [0x000d] = "Tempo",
217 [0x000f] = "Key",
218 [0x0023] = "Comment",
219 [0x002e] = "DateAdded",
220}
221
222-- Label for specific arguments based on message type
223function get_arg_label(msg_type, arg_idx, val)
224 -- Many request types have DMST as the first argument
225 if arg_idx == 1 and (msg_type == 0x2002 or msg_type == 0x3000 or msg_type == 0x1105 or msg_type == 0x1103 or msg_type == 0x2003 or msg_type == 0x2204 or msg_type == 0x2104 or msg_type == 0x2b04 or msg_type == 0x2004 or msg_type == 0x2904 or msg_type == 0x2c04) then
226 return "DMST"
227 end
228
229 if msg_type == 0x4101 then -- MenuItem
230 local labels = {
231 [1] = "Parent ID",
232 [2] = "Main ID",
233 [3] = "Label 1 Len",
234 [4] = "Label 1",
235 [5] = "Label 2 Len",
236 [6] = "Label 2",
237 [7] = "Item Type",
238 [8] = "Flags",
239 [9] = "Artwork ID",
240 [10] = "Position",
241 [11] = "Unknown",
242 [12] = "Unknown"
243 }
244 return labels[arg_idx]
245 elseif msg_type == 0x4201 then -- MenuFooter
246 return arg_idx == 1 and "Status" or (arg_idx == 2 and "Total Items" or nil)
247 elseif msg_type == 0x4001 then -- MenuHeader
248 return arg_idx == 1 and "Item Count" or nil
249 elseif msg_type == 0x4000 then -- Success/Ack
250 return arg_idx == 1 and "Request Type Echo" or (arg_idx == 2 and "Items Found" or nil)
251 elseif msg_type == 0x4602 or msg_type == 0x4702 or msg_type == 0x4e02 or msg_type == 0x4a02 or msg_type == 0x4402 or msg_type == 0x4002 or msg_type == 0x4f02 then -- Blob Responses
252 return arg_idx == 1 and "Request Type Echo" or (arg_idx == 3 and "Blob Length" or (arg_idx == 4 and "Blob Data" or nil))
253 elseif msg_type == 0x3000 then -- RenderMenu
254 local labels = {
255 [1] = "DMST",
256 [2] = "Offset",
257 [3] = "Limit",
258 [4] = "Unknown",
259 [5] = "Total Items",
260 [6] = "Unknown"
261 }
262 return labels[arg_idx]
263 elseif msg_type == 0x0000 then -- Setup
264 return arg_idx == 1 and "Our Device Number" or nil
265 end
266 return nil
267end
268
269-- ============================================================================
270-- DB TAGGED FIELD DISSECTOR
271-- ============================================================================
272function dissect_db_field(buf, offset, tree, msg_type, arg_idx)
273 local len = buf:len()
274 if len - offset < 1 then return offset end
275 local tag = buf(offset, 1):uint()
276 local bytes_consumed = 1
277
278 local label = get_arg_label(msg_type, arg_idx)
279 local prefix = arg_idx and string.format("Arg %d", arg_idx) or "Field"
280 if label then prefix = prefix .. " (" .. label .. ")" end
281
282 if tag == 0x0f then -- U8
283 if len - offset >= 2 then
284 tree:add(f_db_field_u8, buf(offset+1, 1)):set_text(prefix .. ": " .. buf(offset+1, 1):uint())
285 bytes_consumed = 2
286 end
287 elseif tag == 0x10 then -- U16
288 if len - offset >= 3 then
289 tree:add(f_db_field_u16, buf(offset+1, 2)):set_text(prefix .. ": " .. buf(offset+1, 2):uint())
290 bytes_consumed = 3
291 end
292 elseif tag == 0x11 then -- U32
293 if len - offset >= 5 then
294 local val = buf(offset+1, 4):uint()
295 local item = tree:add(f_db_field_u32, buf(offset+1, 4))
296
297 -- Special handling for DMST
298 if label == "DMST" then
299 item:set_text(prefix .. ": " .. string.format("0x%08x", val))
300 local dmst_tree = item:add(f_dmst, buf(offset+1, 4))
301 dmst_tree:add(f_dmst_dev, buf(offset+1, 1))
302 dmst_tree:add(f_dmst_menu, buf(offset+2, 1))
303 dmst_tree:add(f_dmst_slot, buf(offset+3, 1))
304 dmst_tree:add(f_dmst_type, buf(offset+4, 1))
305 elseif label == "Item Type" and MENU_ITEM_TYPES[val] then
306 item:set_text(prefix .. ": " .. MENU_ITEM_TYPES[val] .. " (0x" .. string.format("%04x", val) .. ")")
307 else
308 item:set_text(prefix .. ": " .. val .. " (0x" .. string.format("%08x", val) .. ")")
309 end
310 bytes_consumed = 5
311 end
312 elseif tag == 0x14 then -- Blob
313 if len - offset >= 5 then
314 local blob_len = buf(offset+1, 4):uint()
315 if len - offset >= 5 + blob_len then
316 tree:add(f_db_field_bin, buf(offset+5, blob_len)):set_text(prefix .. ": [Blob " .. blob_len .. " bytes]")
317 bytes_consumed = 5 + blob_len
318 else
319 bytes_consumed = len - offset
320 end
321 end
322 elseif tag == 0x26 then -- String (UTF-16BE)
323 if len - offset >= 5 then
324 local chars = buf(offset+1, 4):uint()
325 local byte_len = chars * 2
326 if len - offset >= 5 + byte_len then
327 -- Use Wireshark's native UTF-16BE decoding
328 local enc = (ENC_UTF_16 or 2) + (ENC_BIG_ENDIAN or 0)
329 local str = buf(offset+5, byte_len):string(enc)
330 -- Strip trailing nulls to clean up UI display
331 str = str:gsub("%z", "")
332
333 -- Use f_db_field_bin for the highlight range to avoid "trailing stray chars" warning
334 -- caused by Wireshark's internal string dissector seeing high-byte nulls.
335 tree:add(f_db_field_bin, buf(offset+5, byte_len)):set_text(prefix .. ": \"" .. str .. "\"")
336 bytes_consumed = 5 + byte_len
337 else
338 bytes_consumed = len - offset
339 end
340 end
341 end
342
343 return offset + bytes_consumed
344end
345
346-- ============================================================================
347-- MAIN DISSECTOR
348-- ============================================================================
349function p_djl.dissector(buf, pkt, root)
350 local length = buf:len()
351 local src_port = pkt.src_port
352 local dst_port = pkt.dst_port
353
354 -- TCP DB Port Discovery Response
355 if (src_port == 12523 or dst_port == 12523) and length == 2 then
356 local tree = root:add(p_djl, buf(0, 2))
357 pkt.cols.protocol = "PDJL (DB Port)"
358 tree:add(f_db_port, buf(0, 2))
359 return
360 end
361
362
363
364 if length < 5 then return end
365
366 -- Check for "Qspt1WmJOL" header (UDP)
367 local header_buf = buf(0, length >= 10 and 10 or length)
368 local header_str = header_buf:string()
369
370 if header_str ~= "Qspt1WmJOL" then
371 local off = 0
372 local found_db = false
373 local db_types = {}
374
375 while off + 5 <= length do
376 if buf(off, 1):uint() == 0x11 and buf(off+1, 4):uint() == 0x872349ae then
377 if not found_db then
378 pkt.cols.protocol = "Pro DJ Link (DB)"
379 found_db = true
380 end
381
382 local msg_tree = root:add(p_djl, buf(off, length - off))
383 msg_tree:add(f_db_magic, buf(off+1, 4))
384
385 local current = off + 5
386
387 -- Transaction ID
388 if length - current >= 5 then
389 current = dissect_db_field(buf, current, msg_tree)
390 end
391
392 -- Message Type
393 local m_type = 0
394 if length - current >= 3 then
395 local type_off = current
396 m_type = buf(type_off+1, 2):uint()
397 local m_type_str = DB_MSG_TYPES[m_type] or string.format("Unknown(0x%04x)", m_type)
398 table.insert(db_types, m_type_str)
399
400 current = dissect_db_field(buf, current, msg_tree)
401 msg_tree:append_text(": " .. m_type_str)
402 end
403
404 -- Arg Count
405 local arg_count = 0
406 if length - current >= 2 then
407 if buf(current, 1):uint() == 0x0f then
408 arg_count = buf(current + 1, 1):uint()
409 end
410 current = dissect_db_field(buf, current, msg_tree)
411 end
412
413 -- Arg Tag Blob
414 if current < length and buf(current, 1):uint() == 0x14 then
415 if length - current >= 5 then
416 local tag_len = buf(current+1, 4):uint()
417 if length - current >= 5 + tag_len then
418 msg_tree:add(f_db_tag_blob, buf(current+5, tag_len))
419 current = current + 5 + tag_len
420 else
421 current = length
422 end
423 end
424 end
425
426 -- Arguments
427 for i = 1, arg_count do
428 if current >= length then break end
429 local prev_current = current
430 current = dissect_db_field(buf, current, msg_tree, m_type, i)
431 if current <= prev_current then break end
432 end
433
434 msg_tree:set_len(current - off)
435 off = current
436 else
437 if off == 0 and length == 5 and buf(0, 1):uint() == 0x11 and buf(1, 4):uint() == 0x00000001 then
438 pkt.cols.protocol = "Pro DJ Link (DB)"
439 pkt.cols.info:set("DB: Greeting")
440 root:add(p_djl, buf(0, 5)):append_text(": Greeting (0x00000001)")
441 return
442 end
443 break
444 end
445 end
446
447 if found_db then
448 pkt.cols.info:set("DB: " .. table.concat(db_types, ", "))
449 pkt.cols.info:fence()
450 return
451 end
452 return
453 end
454
455 -- UDP Logic
456 pkt.cols.protocol = "Pro DJ Link"
457 local tree = root:add(p_djl, buf(0, length))
458 tree:add(f_header, header_buf)
459
460 local p_type = buf(10, 1):uint()
461 local p_type_str = PACKET_TYPES[p_type]
462
463 if dst_port == 50002 or src_port == 50002 then
464 if p_type == 0x05 then p_type_str = "Media Query" end
465 if p_type == 0x06 then p_type_str = "Media Response" end
466 end
467
468 if p_type_str then
469 tree:add(f_type, buf(10, 1)):append_text(" (" .. p_type_str .. ")")
470 pkt.cols.info:set(p_type_str)
471 else
472 local unknown_label = string.format("Unknown(0x%02x)", p_type)
473 tree:add(f_type, buf(10, 1)):append_text(" (" .. unknown_label .. ")")
474
475 if length > 11 then
476 local raw_hex = buf(11):bytes():tohex()
477 tree:add(f_raw_payload, buf(11))
478 pkt.cols.info:set(unknown_label .. ": " .. raw_hex)
479 else
480 pkt.cols.info:set(unknown_label)
481 end
482 end
483
484 pkt.cols.info:fence()
485
486 if dst_port == 50000 or src_port == 50000 then
487 tree:add(f_device_name, buf(0x0C, 20))
488 if p_type == 0x06 then -- Keep-Alive
489 tree:add(f_dev_num, buf(0x24, 1))
490 local dtype = buf(0x25, 1):uint()
491 tree:add(f_dev_type, buf(0x25, 1)):append_text(" (" .. (DEV_TYPES[dtype] or "Unknown") .. ")")
492 tree:add(f_mac, buf(0x26, 6))
493 tree:add(f_ip, buf(0x2C, 4))
494 tree:add(f_peer_count, buf(0x30, 1))
495 elseif p_type == 0x0a then -- Announcement
496 local dtype = buf(0x24, 1):uint()
497 tree:add(f_dev_type, buf(0x24, 1)):append_text(" (" .. (DEV_TYPES[dtype] or "Unknown") .. ")")
498 elseif p_type == 0x00 then -- Claim Stage 1
499 tree:add(f_sequence, buf(0x24, 1))
500 local dtype = buf(0x25, 1):uint()
501 tree:add(f_dev_type, buf(0x25, 1)):append_text(" (" .. (DEV_TYPES[dtype] or "Unknown") .. ")")
502 tree:add(f_mac, buf(0x26, 6))
503 elseif p_type == 0x02 then -- Claim Stage 2
504 tree:add(f_ip, buf(0x24, 4))
505 tree:add(f_mac, buf(0x28, 6))
506 tree:add(f_dev_num, buf(0x2E, 1))
507 tree:add(f_sequence, buf(0x2F, 1))
508 elseif p_type == 0x04 then -- Claim Final
509 tree:add(f_dev_num, buf(0x24, 1))
510 tree:add(f_sequence, buf(0x25, 1))
511 end
512 elseif dst_port == 50001 or src_port == 50001 then
513 if p_type == 0x28 then -- Beat
514 tree:add(f_device_name, buf(0x0B, 20))
515 tree:add(f_dev_num, buf(0x21, 1))
516 tree:add(f_next_beat, buf(0x24, 4))
517 tree:add(f_second_beat, buf(0x28, 4))
518 tree:add(f_next_bar, buf(0x2C, 4))
519 tree:add(f_fourth_beat, buf(0x30, 4))
520 tree:add(f_second_bar, buf(0x34, 4))
521 tree:add(f_eighth_beat, buf(0x38, 4))
522 tree:add(f_pitch, buf(0x54, 4))
523 tree:add(f_bpm_raw, buf(0x5a, 2))
524 tree:add(f_beat_in_bar, buf(0x5c, 1))
525 elseif p_type == 0x0b then -- Absolute Position
526 tree:add(f_dev_num, buf(0x21, 1))
527 tree:add(f_track_len, buf(0x24, 4))
528 tree:add(f_playhead, buf(0x28, 4))
529 tree:add(f_pitch_abs, buf(0x2C, 4))
530 tree:add(f_bpm_abs, buf(0x38, 4))
531 elseif p_type == 0x2a then -- Sync Control
532 tree:add(f_dev_num, buf(0x21, 1))
533 local scmd = buf(0x2b, 1):uint()
534 tree:add(f_sync_cmd, buf(0x2b, 1)):append_text(" (" .. (SYNC_COMMANDS[scmd] or "Unknown") .. ")")
535 end
536 elseif dst_port == 50002 or src_port == 50002 then
537 if p_type == 0x05 then -- Media Query
538 tree:add(f_device_name, buf(0x0B, 20))
539 tree:add(f_dev_num, buf(0x21, 1))
540 if length >= 0x30 then
541 tree:add(f_queried_ip, buf(0x24, 4))
542 tree:add(f_queried_dev, buf(0x2B, 1)) -- Target Device: 1 byte at 0x2B
543 tree:add(f_queried_slot, buf(0x2F, 1)) -- Target Slot: 1 byte at 0x2F
544 end
545 elseif p_type == 0x06 then -- Media Response
546 tree:add(f_device_name, buf(0x0B, 20))
547 tree:add(f_dev_num, buf(0x21, 1))
548
549 -- The real Pioneer response is 192 bytes (0xC0): 36 bytes header + 156 bytes payload
550 if length >= 0xC0 then
551 local enc = (ENC_UTF_16 or 2) + (ENC_BIG_ENDIAN or 0)
552 tree:add(f_slot_owner, buf(0x27, 1)) -- Target Device ($D_r$)
553 tree:add(f_queried_slot, buf(0x2B, 1)) -- Target Slot ($S_r$): 1 byte at 0x2B
554
555 -- Media Name starts at 0x2C (UTF-16, 64 bytes)
556 local name_str = buf(0x2C, 64):string(enc):gsub("%z", "")
557 tree:add(f_media_name, buf(0x2C, 64), name_str)
558
559 -- DB Creation Date starts at 0x6C (UTF-16, 40 bytes)
560 local date_str = buf(0x6C, 40):string(enc):gsub("%z", "")
561 tree:add(f_media_db_date, buf(0x6C, 40), date_str)
562
563 -- Unknown Text at 0x94–0xA5 (18 bytes)
564 local unknown_str = buf(0x94, 18):string(enc):gsub("%z", "")
565 tree:add(f_raw_payload, buf(0x94, 18)):set_text("Unknown Text: \"" .. unknown_str .. "\"")
566
567 tree:add(f_media_tracks, buf(0xa6, 2))
568 local c_val = buf(0xa8, 1):uint()
569 local c_str = INTERFACE_COLORS[c_val] or "Unknown"
570 tree:add(f_interface_color, buf(0xa8, 1)):set_text("Interface Color: " .. c_str .. " (" .. c_val .. ")")
571
572 tree:add(f_media_type, buf(0xaa, 1)) -- Media Type $T_r$ at correct offset
573 tree:add(f_settings_present, buf(0xab, 1)) -- Settings presence bit
574
575 tree:add(f_playlist_count, buf(0xae, 2))
576 tree:add(f_total_media_space, buf(0xb0, 8))
577 tree:add(f_avail_media_space, buf(0xb8, 8))
578 elseif length >= 0x2C + 32 then
579 -- Fallback: 80-byte synthetic format from our own NFS server (djlink/src/server.rs).
580 -- This does NOT match real Pioneer hardware; offsets differ from the spec above.
581 local enc = (ENC_ASCII or 0)
582 local name_str = buf(0x2C, 32):string(enc):gsub("%z", "")
583 tree:add(f_media_type, buf(0x26, 1))
584 tree:add(f_media_status, buf(0x27, 1))
585 tree:add(f_media_tracks, buf(0x28, 4))
586 tree:add(f_media_name, buf(0x2C, 32), name_str)
587 end
588 elseif p_type == 0x0a then -- CDJ Status
589 tree:add(f_device_name, buf(0x0B, 20))
590 tree:add(f_dev_num, buf(0x21, 1))
591
592 tree:add(f_activity, buf(0x27, 1))
593 tree:add(f_track_dev, buf(0x28, 1))
594 tree:add(f_slot, buf(0x29, 1))
595 local ttype = buf(0x2a, 1):uint()
596 tree:add(f_track_type, buf(0x2a, 1)):append_text(" (" .. (TRACK_TYPES[ttype] or "Unknown") .. ")")
597 tree:add(f_rb_id, buf(0x2c, 4))
598 tree:add(f_track_num, buf(0x32, 2))
599
600 local pmode1 = buf(0x7b, 1):uint()
601 tree:add(f_play_mode_1, buf(0x7b, 1)):append_text(" (" .. (PLAY_MODES[pmode1] or "Unknown") .. ")")
602 tree:add(f_firmware, buf(0x7c, 4))
603
604 local flag_tree = tree:add(f_flags, buf(0x89, 1))
605 flag_tree:add(f_flag_playing, buf(0x89, 1))
606 flag_tree:add(f_flag_master, buf(0x89, 1))
607 flag_tree:add(f_flag_sync, buf(0x89, 1))
608 flag_tree:add(f_flag_onair, buf(0x89, 1))
609 flag_tree:add(f_flag_bpm, buf(0x89, 1))
610
611 local pmode2 = buf(0x8b, 1):uint()
612 tree:add(f_play_mode_2, buf(0x8b, 1)):append_text(" (" .. (PLAY_MODES[pmode2] or "Unknown") .. ")")
613
614 tree:add(f_pitch_1, buf(0x8c, 4))
615 tree:add(f_bpm_raw, buf(0x92, 2))
616 tree:add(f_pitch_2, buf(0x98, 4))
617 tree:add(f_master_info, buf(0x9e, 1))
618 tree:add(f_handoff, buf(0x9f, 1))
619 tree:add(f_beat_count, buf(0xa0, 4))
620 tree:add(f_countdown, buf(0xa4, 2))
621 tree:add(f_beat_in_bar, buf(0xa6, 1))
622 tree:add(f_pitch_3, buf(0xc0, 4))
623 end
624 end
625end
626
627local udp_table = DissectorTable.get("udp.port")
628udp_table:add(50000, p_djl)
629udp_table:add(50001, p_djl)
630udp_table:add(50002, p_djl)
631
632local tcp_table = DissectorTable.get("tcp.port")
633tcp_table:add(12523, p_djl)
634tcp_table:add(1051, p_djl)
635tcp_table:add(1052, p_djl)