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
6local TRACK_SORT_MODES = {
7 [0x00] = "Default (By Title)",
8 [0x01] = "By Title With Artist",
9 [0x02] = "Artist",
10 [0x03] = "Album",
11 [0x04] = "Genre",
12 [0x05] = "BPM",
13 [0x06] = "Key",
14 [0x07] = "Rating",
15 [0x08] = "Duration",
16 [0x09] = "Color",
17 [0x0a] = "Date Added",
18 [0x0b] = "Label",
19 [0x0c] = "Bit Rate",
20 [0x0d] = "Year",
21 [0x0e] = "Original Artist",
22 [0x0f] = "Remixer",
23 [0x10] = "Comment",
24 [0x11] = "DJ Play Count",
25 [0x12] = "History",
26 [0x13] = "Matching",
27}
28
29local PACKET_TYPES = {
30 [0x00] = "Channel Claim 1",
31 [0x02] = "Channel Claim 2",
32 [0x04] = "Channel Claim Final",
33 [0x06] = "Keep-Alive",
34 [0x0a] = "CDJ Status / Announcement",
35 [0x0b] = "Absolute Position",
36 [0x28] = "Beat",
37 [0x2a] = "Sync Control",
38 [0x29] = "Mixer Status"
39}
40
41local DB_MSG_TYPES = {
42 [0x0000] = "GetQueryContext",
43 [0x0001] = "Termination / End Transaction",
44 [0x0100] = "Disconnect",
45 [0x1000] = "Root Menu",
46 [0x4000] = "Success/Ack",
47 [0x1001] = "Disconnect Acknowledgment",
48 [0x1002] = "Artist Menu",
49 [0x1003] = "Album Menu",
50 [0x1004] = "Track Menu",
51 [0x1006] = "BPM Menu",
52 [0x1007] = "Rating Menu",
53 [0x1008] = "Year Menu",
54 [0x100a] = "Label Menu",
55 [0x100d] = "Color Menu",
56 [0x1010] = "Time Menu",
57 [0x1011] = "Bitrate Menu",
58 [0x1012] = "History Menu",
59 [0x1013] = "Filename Menu",
60 [0x1014] = "Key Menu",
61 [0x2002] = "GetMetadata",
62 [0x3000] = "RenderMenu",
63 [0x4001] = "MenuHeader",
64 [0x4101] = "MenuItem",
65 [0x4201] = "MenuFooter",
66 [0x2003] = "GetArtwork",
67 [0x4002] = "ArtworkResponse",
68 [0x2204] = "GetBeatGrid",
69 [0x4602] = "BeatGridResponse",
70 [0x2104] = "GetCuePoints",
71 [0x4702] = "CuePointsResponse",
72 [0x2b04] = "GetExtCuePoints",
73 [0x4e02] = "ExtCuePointsResponse",
74 [0x2004] = "GetWaveformPreview",
75 [0x4402] = "WaveformPreviewResponse",
76 [0x2102] = "GetTrackStreamingReq",
77 [0x2904] = "GetWaveformDetail",
78 [0x4a02] = "WaveformDetailResponse",
79 [0x2c04] = "GetAnalysisTag",
80 [0x4f02] = "AnalysisTagResponse",
81 [0x3100] = "TrackLoadNotification",
82 [0x3e03] = "GetCapabilities",
83 [0x4b02] = "CapabilitiesResponse",
84 [0x3007] = "SetupBrowseContext",
85 [0x1101] = "ArtistsForGenre",
86 [0x1102] = "AlbumsForArtist",
87 [0x1103] = "GetTrackList",
88 [0x1114] = "DistancesForKey",
89 [0x1214] = "TracksNearKey",
90 [0x1300] = "Search",
91 [0x1301] = "TracksForGenreArtistAlbum",
92 [0x2006] = "Folder",
93 [0x1105] = "GetTrackCount",
94 [0x1106] = "BPM Distances",
95 [0x1206] = "TracksForBpmRange",
96 [0x5105] = "TrackCountResponse",
97 [0x5103] = "TrackListResponse",
98}
99
100local PLAY_MODES = {
101 [0x00] = "No Track",
102 [0x02] = "Loading",
103 [0x03] = "Playing",
104 [0x04] = "Looping",
105 [0x05] = "Paused",
106 [0x06] = "Paused at Cue",
107 [0x07] = "Cue Play",
108 [0x08] = "Cue Scratch",
109 [0x09] = "Searching",
110 [0x0e] = "CD Spun Down",
111 [0x11] = "Ended",
112}
113
114local TRACK_TYPES = {
115 [0x00] = "None",
116 [0x01] = "Rekordbox",
117 [0x02] = "Unanalyzed",
118 [0x05] = "Audio CD",
119}
120
121local SYNC_COMMANDS = {
122 [0x10] = "Turn On Sync",
123 [0x20] = "Turn Off Sync",
124 [0x01] = "Become Master",
125}
126
127local DEV_TYPES = {
128 [0x01] = "Player (CDJ)",
129 [0x02] = "Player (XDJ)",
130 [0x03] = "Mixer",
131 [0x04] = "PC/Rekordbox",
132}
133
134local INTERFACE_COLORS = {
135 [0] = "Default",
136 [1] = "Pink",
137 [2] = "Red",
138 [3] = "Orange",
139 [4] = "Yellow",
140 [5] = "Green",
141 [6] = "Aqua",
142 [7] = "Blue",
143 [8] = "Purple",
144}
145
146local DMST_MENUS = {
147 [0x01] = "Left/Main",
148 [0x02] = "Right/Split",
149 [0x03] = "Metadata Preview",
150 [0x08] = "Waveform/Art/Grid",
151}
152
153local DMST_SLOTS = {
154 [0x00] = "No Track",
155 [0x01] = "CD",
156 [0x02] = "SD",
157 [0x03] = "USB",
158 [0x04] = "Rekordbox",
159 [0x06] = "Streaming (Direct)",
160 [0x07] = "USB 2",
161 [0x09] = "Beatport LINK",
162}
163
164local DMST_TYPES = {
165 [0x01] = "Rekordbox",
166 [0x02] = "Unanalyzed",
167 [0x05] = "Audio CD",
168 [0x06] = "Streaming",
169}
170
171local ANALYSIS_TAGS = {
172 [0x49414d50] = "PMAI (Header)",
173 [0x5a545150] = "PQTZ (Beat Grid)",
174 [0x424f4350] = "PCOB (Cue List)",
175 [0x54504350] = "PCPT (Cue List Entry)",
176 [0x324f4350] = "PCO2 (Extended Cue List)",
177 [0x32504350] = "PCP2 (Extended Cue List Entry)",
178 [0x48545050] = "PPTH (Path)",
179 [0x52425650] = "PVBR (VBR Index)",
180 [0x56415750] = "PWAV (Waveform Preview)",
181 [0x32565750] = "PWV2 (Tiny Waveform Preview)",
182 [0x33565750] = "PWV3 (Waveform Detail)",
183 [0x34565750] = "PWV4 (Color Waveform Preview)",
184 [0x35565750] = "PWV5 (Color Waveform Detail)",
185 [0x36565750] = "PWV6 (3-Band Waveform Preview)",
186 [0x37565750] = "PWV7 (3-Band Waveform Detail)",
187 [0x49535350] = "PSSI (Song Structure)",
188}
189
190local ARG_TAGS = {
191 [0x00] = "none",
192 [0x02] = "str",
193 [0x03] = "blob",
194 [0x06] = "u32",
195}
196
197-- Table to track TCP streams that have been identified as PDJL
198local pdjl_streams = {}
199
200-- Table to track Analysis Tag requests by Transaction ID for showing in responses
201local analysis_requests = {} -- key: stream_id .. ":" .. tx_id, value: tag_name
202
203-- Helper to get a unique key for a bidirectional TCP stream
204local function get_stream_key(pkt)
205 local ip1 = tostring(pkt.net_src)
206 local ip2 = tostring(pkt.net_dst)
207 local p1 = pkt.src_port
208 local p2 = pkt.dst_port
209 if ip1 < ip2 or (ip1 == ip2 and p1 < p2) then
210 return ip1 .. ":" .. p1 .. "-" .. ip2 .. ":" .. p2
211 else
212 return ip2 .. ":" .. p2 .. "-" .. ip1 .. ":" .. p1
213 end
214end
215
216local MENU_ITEM_TYPES = {
217 [0x0001] = "Folder",
218 [0x0002] = "Album Title",
219 [0x0003] = "Disc",
220 [0x0004] = "Track Title",
221 [0x0006] = "Genre",
222 [0x0007] = "Artist",
223 [0x0008] = "Playlist",
224 [0x000a] = "Rating",
225 [0x000b] = "Duration",
226 [0x000d] = "Tempo",
227 [0x000e] = "Label",
228 [0x000f] = "Key",
229 [0x0010] = "Bit Rate",
230 [0x0011] = "Year",
231 [0x0013] = "Color None",
232 [0x0014] = "Color Pink",
233 [0x0015] = "Color Red",
234 [0x0016] = "Color Orange",
235 [0x0017] = "Color Yellow",
236 [0x0018] = "Color Green",
237 [0x0019] = "Color Aqua",
238 [0x001a] = "Color Blue",
239 [0x001b] = "Color Purple",
240 [0x0023] = "Comment",
241 [0x0024] = "History Playlist",
242 [0x0028] = "Original Artist",
243 [0x0029] = "Remixer",
244 [0x002e] = "Date Added",
245 [0x0080] = "Genre menu",
246 [0x0081] = "Artist menu",
247 [0x0082] = "Album menu",
248 [0x0083] = "Track menu",
249 [0x0084] = "Playlist menu",
250 [0x0085] = "Bpm menu",
251 [0x0086] = "Rating menu",
252 [0x0087] = "Year menu",
253 [0x0088] = "Remixer menu",
254 [0x0089] = "Label menu",
255 [0x008a] = "Original Artist menu",
256 [0x008b] = "Key menu",
257 [0x008c] = "Date Added menu",
258 [0x008e] = "Color menu",
259 [0x0090] = "Folder menu",
260 [0x0091] = "Search \"menu\"",
261 [0x0092] = "Time menu",
262 [0x0093] = "Bit Rate menu",
263 [0x0094] = "Filename menu",
264 [0x0095] = "History menu",
265 [0x0098] = "Hot cue bank menu",
266 [0x00a0] = "All",
267 [0x00aa] = "Matching",
268 [0x0204] = "Track Title and Album",
269 [0x0604] = "Track Title and Genre",
270 [0x0704] = "Track Title and Artist",
271 [0x0a04] = "Track Title and Rating",
272 [0x0b04] = "Track Title and Time",
273 [0x0d04] = "Track Title and BPM",
274 [0x0e04] = "Track Title and Label",
275 [0x0f04] = "Track Title and Key",
276 [0x1004] = "Track Title and Bit Rate",
277 [0x1a04] = "Track Title and Color",
278 [0x2304] = "Track Title and Comment",
279 [0x2804] = "Track Title and Original Artist",
280 [0x2904] = "Track Title and Remixer",
281 [0x2a04] = "Track Title and DJ Play Count",
282 [0x2e04] = "Track Title and Date Added",
283}
284
285
286-- ============================================================================
287-- COMMON FIELDS
288-- ============================================================================
289local f_header = ProtoField.string("pdjl.header", "Header")
290local f_type = ProtoField.uint8("pdjl.type", "Packet Type", base.HEX)
291local f_device_name = ProtoField.string("pdjl.device_name", "Device Name")
292local f_dev_num = ProtoField.uint8("pdjl.device_number", "Device Number", base.DEC)
293
294-- Port 50000 (Management)
295local f_mac = ProtoField.ether("pdjl.mac", "MAC Address")
296local f_ip = ProtoField.ipv4("pdjl.ip", "IP Address")
297local f_sequence = ProtoField.uint8("pdjl.sequence", "Sequence", base.DEC)
298local f_dev_type = ProtoField.uint8("pdjl.device_type", "Device Type", base.HEX)
299local f_peer_count = ProtoField.uint8("pdjl.peer_count", "Peer Count", base.DEC)
300
301-- Port 50001 (Beat Sync)
302local f_bpm_raw = ProtoField.uint16("pdjl.bpm_raw", "BPM", base.DEC)
303local f_pitch = ProtoField.uint32("pdjl.pitch", "Pitch", base.DEC)
304local f_beat_in_bar = ProtoField.uint8("pdjl.beat_in_bar", "Beat in Bar", base.DEC)
305local f_next_beat = ProtoField.uint32("pdjl.next_beat", "Next Beat (ms)", base.DEC)
306local f_second_beat = ProtoField.uint32("pdjl.second_beat", "Second Beat (ms)", base.DEC)
307local f_next_bar = ProtoField.uint32("pdjl.next_bar", "Next Bar (ms)", base.DEC)
308local f_fourth_beat = ProtoField.uint32("pdjl.fourth_beat", "Fourth Beat (ms)", base.DEC)
309local f_second_bar = ProtoField.uint32("pdjl.second_bar", "Second Bar (ms)", base.DEC)
310local f_eighth_beat = ProtoField.uint32("pdjl.eighth_beat", "Eighth Beat (ms)", base.DEC)
311local f_track_len = ProtoField.uint32("pdjl.track_length", "Track Length (s)", base.DEC)
312local f_playhead = ProtoField.uint32("pdjl.playhead", "Playhead (ms)", base.DEC)
313local f_pitch_abs = ProtoField.int32("pdjl.pitch_abs", "Position Pitch", base.DEC)
314local f_bpm_abs = ProtoField.uint32("pdjl.bpm_abs", "Position BPM", base.DEC)
315local f_sync_cmd = ProtoField.uint8("pdjl.sync_cmd", "Sync Command", base.HEX)
316
317-- Port 50002 (Status)
318local f_rb_id = ProtoField.uint32("pdjl.rekordbox_id", "Rekordbox ID", base.DEC)
319local f_track_num = ProtoField.uint16("pdjl.track_number", "Track Number", base.DEC)
320local f_track_sort = ProtoField.uint8("pdjl.track_sort", "Track Sort State", base.HEX, TRACK_SORT_MODES)
321local f_track_dev = ProtoField.uint8("pdjl.track_device", "Track Host Device", base.DEC)
322local f_slot = ProtoField.uint8("pdjl.slot", "Slot", base.HEX)
323local f_track_type = ProtoField.uint8("pdjl.track_type", "Track Type", base.HEX)
324local f_activity = ProtoField.uint8("pdjl.activity", "Activity", base.HEX)
325local f_firmware = ProtoField.string("pdjl.firmware", "Firmware")
326local f_beat_count = ProtoField.uint32("pdjl.beat_count", "Beat Count", base.DEC)
327local f_play_mode_1 = ProtoField.uint8("pdjl.play_mode_1", "Play Mode 1", base.HEX)
328local f_play_mode_2 = ProtoField.uint8("pdjl.play_mode_2", "Play Mode 2", base.HEX)
329local f_pitch_1 = ProtoField.uint32("pdjl.pitch_1", "Effective Pitch", base.DEC)
330local f_pitch_2 = ProtoField.uint32("pdjl.pitch_2", "Local Pitch", base.DEC)
331local f_master_info = ProtoField.uint8("pdjl.master_info", "Master Info", base.HEX)
332local f_handoff = ProtoField.uint8("pdjl.handoff", "Master Handoff", base.DEC)
333local f_countdown = ProtoField.uint16("pdjl.cue_countdown", "Cue Countdown", base.DEC)
334local f_pitch_3 = ProtoField.uint32("pdjl.pitch_3", "Pitch 3", base.DEC)
335
336local f_querying_ip = ProtoField.ipv4("pdjl.queried_ip", "Querying IP")
337local f_queried_dev = ProtoField.uint8("pdjl.queried_device", "Target Device", base.DEC)
338local f_queried_slot = ProtoField.uint8("pdjl.queried_slot", "Target Slot", base.HEX)
339local f_slot_owner = ProtoField.uint8("pdjl.slot_owner", "Slot Owner Device", base.DEC)
340local f_playlist_count = ProtoField.uint16("pdjl.playlist_count", "Playlist Count", base.DEC)
341local f_total_media_space = ProtoField.uint64("pdjl.total_media_space", "Total Space", base.DEC)
342local f_avail_media_space = ProtoField.uint64("pdjl.avail_media_space", "Available Space", base.DEC)
343local f_interface_color = ProtoField.uint8("pdjl.interface_color", "Interface Color", base.DEC, INTERFACE_COLORS)
344local f_settings_present = ProtoField.bool("pdjl.settings_present", "MYSETTINGS.DAT present", 1, nil, 0x01)
345
346
347local f_media_type = ProtoField.uint8("pdjl.media_type", "Media Type", base.HEX)
348local f_media_status = ProtoField.uint8("pdjl.media_status", "Media Status", base.HEX)
349local f_media_tracks = ProtoField.uint16("pdjl.media_tracks", "Media Tracks", base.DEC)
350local f_media_name = ProtoField.string("pdjl.media_name", "Media Name")
351local f_media_db_date= ProtoField.string("pdjl.media_db_date", "DB Creation Date")
352
353local f_flags = ProtoField.uint8("pdjl.flags", "Status Flags", base.HEX)
354local f_flag_playing= ProtoField.bool("pdjl.flags.playing", "Is Playing", 8, nil, 0x40)
355local f_flag_master = ProtoField.bool("pdjl.flags.master", "Is Master", 8, nil, 0x20)
356local f_flag_sync = ProtoField.bool("pdjl.flags.sync", "Is Syncing", 8, nil, 0x10)
357local f_flag_onair = ProtoField.bool("pdjl.flags.onair", "Is On Air", 8, nil, 0x08)
358local f_flag_bpm = ProtoField.bool("pdjl.flags.bpm", "BPM Sync Active", 8, nil, 0x02)
359
360-- TCP DB Server
361local f_db_magic = ProtoField.uint32("pdjl.db.magic", "DB Magic", base.HEX)
362local f_db_tx_id = ProtoField.uint32("pdjl.db.tx_id", "Transaction ID", base.HEX)
363local f_db_msg_type = ProtoField.uint16("pdjl.db.msg_type", "DB Message Type", base.HEX)
364local f_db_arg_count= ProtoField.uint8("pdjl.db.arg_count", "Argument Count", base.DEC)
365local f_db_tag_blob = ProtoField.bytes("pdjl.db.tags", "Argument Tags")
366
367local f_db_field_u8 = ProtoField.uint8("pdjl.db.field.u8", "Field (U8)", base.DEC)
368local f_db_field_u16= ProtoField.uint16("pdjl.db.field.u16", "Field (U16)", base.DEC)
369local f_db_field_u32= ProtoField.uint32("pdjl.db.field.u32", "Field (U32)", base.HEX)
370local f_db_field_u64= ProtoField.uint64("pdjl.db.field.u64", "Field (U64)", base.DEC)
371local f_db_field_str= ProtoField.string("pdjl.db.field.str", "Field (String)")
372local f_db_field_bin= ProtoField.bytes("pdjl.db.field.bin", "Field (Blob)")
373
374-- DMST decoding
375local f_dmst = ProtoField.uint32("pdjl.dmst", "DMST Parameter", base.HEX)
376local f_dmst_dev = ProtoField.uint8("pdjl.dmst.device", "Device", base.DEC)
377local f_dmst_menu = ProtoField.uint8("pdjl.dmst.menu", "Menu", base.HEX, DMST_MENUS)
378local f_dmst_slot = ProtoField.uint8("pdjl.dmst.slot", "Slot", base.HEX, DMST_SLOTS)
379local f_dmst_type = ProtoField.uint8("pdjl.dmst.type", "Type", base.HEX, DMST_TYPES)
380
381local f_db_port = ProtoField.uint16("pdjl.db_port", "DB Server Port", base.DEC)
382local f_raw_payload = ProtoField.bytes("pdjl.raw_payload", "Raw Payload")
383
384p_djl.fields = {
385 f_header, f_type, f_device_name, f_dev_num,
386 f_mac, f_ip, f_sequence, f_dev_type, f_peer_count,
387 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,
388 f_track_len, f_playhead, f_pitch_abs, f_bpm_abs, f_sync_cmd,
389 f_rb_id, f_track_num, f_track_sort, f_track_dev, f_slot, f_track_type, f_activity, f_firmware, f_beat_count,
390 f_play_mode_1, f_play_mode_2, f_pitch_1, f_pitch_2, f_master_info, f_handoff, f_countdown, f_pitch_3,
391 f_querying_ip, f_queried_dev, f_queried_slot, f_slot_owner, f_playlist_count, f_total_media_space, f_avail_media_space,
392 f_interface_color, f_settings_present, f_media_type, f_media_status, f_media_tracks, f_media_name, f_media_db_date,
393 f_flags, f_flag_playing, f_flag_master, f_flag_sync, f_flag_onair, f_flag_bpm,
394 f_db_magic, f_db_tx_id, f_db_msg_type, f_db_arg_count, f_db_tag_blob,
395 f_db_field_u8, f_db_field_u16, f_db_field_u32, f_db_field_u64, f_db_field_str, f_db_field_bin,
396 f_dmst, f_dmst_dev, f_dmst_menu, f_dmst_slot, f_dmst_type,
397 f_db_port, f_raw_payload
398}
399
400-- ============================================================================
401-- HELPERS
402-- ============================================================================
403
404-- Decodes a 4-byte buffer as a reverse-ASCII string
405local function get_reverse_ascii(buf)
406 local b = buf:bytes()
407 local s = ""
408 for j = 3, 0, -1 do
409 local c = b:get_index(j)
410 if c ~= 0 then s = s .. string.char(c) end
411 end
412 return s
413end
414
415-- Label for specific arguments based on message type
416function get_arg_label(msg_type, arg_idx, val)
417 -- Many request types have DMST as the first argument
418 if arg_idx == 1 and (
419 msg_type == 0x1000 or -- Root Menu
420 msg_type == 0x1001 or -- Genre Menu
421 msg_type == 0x1002 or -- Artist Menu
422 msg_type == 0x1003 or -- Album Menu
423 msg_type == 0x1004 or -- Track Menu
424 msg_type == 0x1006 or -- BPM Menu
425 msg_type == 0x1007 or -- Rating Menu
426 msg_type == 0x1008 or -- Year Menu
427 msg_type == 0x100a or -- Label Menu
428 msg_type == 0x100d or -- Color Menu
429 msg_type == 0x1010 or -- Time Menu
430 msg_type == 0x1011 or -- Bitrate Menu
431 msg_type == 0x1012 or -- History Menu
432 msg_type == 0x1013 or -- Filename Menu
433 msg_type == 0x1014 or -- Key Menu
434 msg_type == 0x1101 or -- ArtistsForGenre
435 msg_type == 0x1102 or -- AlbumsForArtist
436 msg_type == 0x1103 or -- GetTrackList
437 msg_type == 0x1105 or -- GetTrackCount
438 msg_type == 0x1106 or -- BPM Distances
439 msg_type == 0x1114 or -- DistancesForKey
440 msg_type == 0x1206 or -- TracksForBpmRange
441 msg_type == 0x1214 or -- TracksNearKey
442 msg_type == 0x1300 or -- Search
443 msg_type == 0x1301 or -- TracksForGenreArtistAlbum
444 msg_type == 0x2002 or -- GetMetadata
445 msg_type == 0x2003 or -- GetArtwork
446 msg_type == 0x2004 or -- GetWaveformPreview
447 msg_type == 0x2102 or -- GetTrackStreamingReq
448 msg_type == 0x2104 or -- GetCuePoints
449 msg_type == 0x2204 or -- GetBeatGrid
450 msg_type == 0x2904 or -- GetWaveformDetail
451 msg_type == 0x2b04 or -- GetExtCuePoints
452 msg_type == 0x2c04 or -- GetAnalysisTag
453 msg_type == 0x3000 or -- RenderMenu
454 msg_type == 0x3007 or -- SetupBrowseContext
455 msg_type == 0x3100 or -- TrackLoadNotification
456 msg_type == 0x3e03 -- GetCapabilities
457 ) then
458 return "DMST"
459 end
460
461 if msg_type == 0x1000 then -- Root Menu
462 local labels = {
463 [1] = "DMST",
464 [2] = "Sort",
465 [3] = "Unknown (0x00ffffff)"
466 }
467 return labels[arg_idx]
468 elseif msg_type == 0x3100 then -- TrackLoadNotification
469 local labels = {
470 [1] = "DMST",
471 [2] = "Rekordbox ID",
472 [3] = "Unknown 1",
473 [4] = "Unknown 2"
474 }
475 return labels[arg_idx]
476 elseif msg_type == 0x2c04 then -- GetAnalysisTag
477 local labels = {
478 [1] = "DMST",
479 [2] = "Rekordbox ID",
480 [3] = "Tag",
481 [4] = "Extension"
482 }
483 return labels[arg_idx]
484 elseif msg_type == 0x2102 then -- GetTrackStreamingReq
485 local labels = {
486 [1] = "DMST",
487 [2] = "Rekordbox ID"
488 }
489 return labels[arg_idx]
490 elseif msg_type == 0x4101 then -- MenuItem
491 local labels = {
492 [1] = "Parent ID",
493 [2] = "Main ID",
494 [3] = "Label 1 Len",
495 [4] = "Label 1",
496 [5] = "Label 2 Len",
497 [6] = "Label 2",
498 [7] = "Item Type",
499 [8] = "Flags",
500 [9] = "Artwork ID",
501 [10] = "Position",
502 [11] = "Unknown",
503 [12] = "Unknown"
504 }
505 return labels[arg_idx]
506 elseif msg_type == 0x4201 then -- MenuFooter
507 return arg_idx == 1 and "Status" or (arg_idx == 2 and "Total Items" or nil)
508 elseif msg_type == 0x4001 then -- MenuHeader
509 return arg_idx == 1 and "Item Count" or nil
510 elseif msg_type == 0x4000 then -- Success/Ack
511 return arg_idx == 1 and "Request Type Echo" or (arg_idx == 2 and "Items Found" or nil)
512 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
513 if arg_idx == 1 then return "Request Type Echo" end
514 if arg_idx == 2 then return "Status/Echo" end
515 if arg_idx == 3 then return "Length" end
516 if arg_idx == 4 then
517 local labels = {
518 [0x4002] = "Blob (image)",
519 [0x4602] = "Beat Grid Data",
520 [0x4702] = "Cue Points Data",
521 [0x4e02] = "Extended Cue Points Data",
522 [0x4402] = "Waveform Preview Data",
523 [0x4a02] = "Waveform Detail Data",
524 [0x4f02] = "Analysis Tag Data",
525 }
526 return labels[msg_type] or "Blob Data"
527 end
528 return nil
529 elseif msg_type == 0x3000 then -- RenderMenu
530 local labels = {
531 [1] = "DMST",
532 [2] = "Offset",
533 [3] = "Limit",
534 [4] = "Unknown",
535 [5] = "Total Items",
536 [6] = "Unknown"
537 }
538 return labels[arg_idx]
539 elseif msg_type == 0x0000 then -- Setup
540 return arg_idx == 1 and "Our Device Number" or nil
541 elseif msg_type == 0x4b02 then -- CapabilitiesResp
542 local labels = {
543 [1] = "Request Type Echo",
544 [2] = "Reserved",
545 [3] = "Capability Level",
546 [4] = "Placeholder (Empty String)"
547 }
548 return labels[arg_idx]
549 end
550 return nil
551end
552
553-- ============================================================================
554-- DB TAGGED FIELD DISSECTOR
555-- ============================================================================
556function dissect_db_field(buf, offset, tree, msg_type, arg_idx)
557 local len = buf:len()
558 if len - offset < 1 then return offset, nil, nil end
559 local tag = buf(offset, 1):uint()
560 local bytes_consumed = 1
561 local val = nil
562 local item = nil
563
564 local prefix
565 local label
566 if type(arg_idx) == "string" then
567 prefix = arg_idx
568 else
569 label = get_arg_label(msg_type, arg_idx)
570 prefix = arg_idx and string.format("Arg %d", arg_idx) or "Field"
571 if label then prefix = prefix .. " (" .. label .. ")" end
572 end
573
574 if tag == 0x0f then -- U8
575 if len - offset >= 2 then
576 val = buf(offset+1, 1):uint()
577 item = tree:add(f_db_field_u8, buf(offset+1, 1))
578 item:set_text(prefix .. ": " .. val)
579 bytes_consumed = 2
580 end
581 elseif tag == 0x10 then -- U16
582 if len - offset >= 3 then
583 val = buf(offset+1, 2):uint()
584 item = tree:add(f_db_field_u16, buf(offset+1, 2))
585 if prefix == "Message Type" then
586 local m_type_str = DB_MSG_TYPES[val] or string.format("Unknown(0x%04x)", val)
587 item:set_text(prefix .. ": " .. m_type_str .. " (0x" .. string.format("%04x", val) .. ")")
588 else
589 item:set_text(prefix .. ": " .. val .. " (0x" .. string.format("%04x", val) .. ")")
590 end
591 bytes_consumed = 3
592 end
593 elseif tag == 0x11 then -- U32
594 if len - offset >= 5 then
595 val = buf(offset+1, 4):uint()
596
597 -- Special handling for DMST to keep it as one expandable layer
598 if label == "DMST" then
599 local dmst_tree = tree:add(f_dmst, buf(offset+1, 4))
600 dmst_tree:set_text(prefix .. ": " .. string.format("0x%08x", val))
601 dmst_tree:add(f_dmst_dev, buf(offset+1, 1))
602 dmst_tree:add(f_dmst_menu, buf(offset+2, 1))
603 dmst_tree:add(f_dmst_slot, buf(offset+3, 1))
604 dmst_tree:add(f_dmst_type, buf(offset+4, 1))
605 item = dmst_tree
606 else
607 item = tree:add(f_db_field_u32, buf(offset+1, 4))
608 if label == "Transaction ID" and val == 0xfffffffe then
609 item:set_text(prefix .. ": Setup/Teardown (0xfffffffe)")
610 elseif label == "Item Type" and MENU_ITEM_TYPES[val] then
611 item:set_text(prefix .. ": " .. MENU_ITEM_TYPES[val] .. " (0x" .. string.format("%04x", val) .. ")")
612 elseif label == "Tag" then
613 local tag_str = ANALYSIS_TAGS[val] or get_reverse_ascii(buf(offset+1, 4))
614 item:set_text(prefix .. ": " .. tag_str .. " (0x" .. string.format("%08x", val) .. ")")
615 elseif label == "Extension" then
616 item:set_text(prefix .. ": \"" .. get_reverse_ascii(buf(offset+1, 4)) .. "\" (0x" .. string.format("%08x", val) .. ")")
617 else
618 item:set_text(prefix .. ": " .. val .. " (0x" .. string.format("%08x", val) .. ")")
619 end
620 end
621 bytes_consumed = 5
622 end
623 elseif tag == 0x14 then -- Blob
624 if len - offset >= 5 then
625 local blob_len = buf(offset+1, 4):uint()
626 local available = len - offset - 5
627 local to_read = math.min(blob_len, available)
628 item = tree:add(f_db_field_bin, buf(offset+5, to_read))
629 if to_read < blob_len then
630 item:set_text(prefix .. ": [Blob Truncated " .. to_read .. "/" .. blob_len .. " bytes]")
631 else
632 item:set_text(prefix .. ": [Blob " .. blob_len .. " bytes]")
633 end
634 bytes_consumed = 5 + to_read
635 val = buf(offset+5, to_read):bytes()
636 else
637 bytes_consumed = len - offset
638 end
639 elseif tag == 0x26 then -- String (UTF-16BE)
640 if len - offset >= 5 then
641 local chars = buf(offset+1, 4):uint()
642 local byte_len = chars * 2
643 if len - offset >= 5 + byte_len then
644 -- Use Wireshark's native UTF-16BE decoding
645 local enc = (ENC_UTF_16 or 2) + (ENC_BIG_ENDIAN or 0)
646 local str = buf(offset+5, byte_len):string(enc)
647 -- Strip trailing nulls to clean up UI display
648 str = str:gsub("%z", "")
649
650 -- Use f_db_field_bin for the highlight range to avoid "trailing stray chars" warning
651 -- caused by Wireshark's internal string dissector seeing high-byte nulls.
652 item = tree:add(f_db_field_bin, buf(offset+5, byte_len))
653 item:set_text(prefix .. ": \"" .. str .. "\"")
654 bytes_consumed = 5 + byte_len
655 val = str
656 else
657 bytes_consumed = len - offset
658 end
659 end
660 end
661
662 return offset + bytes_consumed, val, item
663end
664
665-- ============================================================================
666-- DB SERVER DISSECTION LOGIC
667-- ============================================================================
668
669-- Helper to calculate the full byte length of a DB message starting at `offset`.
670-- Returns:
671-- > 0: The total byte length of the complete message.
672-- < 0: The number of missing bytes needed to complete the message (returns -missing).
673-- 0: Not a valid DB message or parse error.
674local function get_db_msg_len(buf, offset)
675 local len = buf:len()
676 local available = len - offset
677
678 if available < 5 then return -(5 - available) end
679
680 local tag = buf(offset, 1):uint()
681 local magic = buf(offset+1, 4):uint()
682
683 if tag == 0x11 and magic == 0x00000001 then
684 return 5
685 end
686
687 if tag ~= 0x11 or magic ~= 0x872349ae then
688 return 0
689 end
690
691 local cur = offset + 5
692
693 local function read_field()
694 local a = len - cur
695 if a < 1 then return false, (1 - a) end
696 local ftag = buf(cur, 1):uint()
697 if ftag == 0x0f then
698 if a < 2 then return false, (2 - a) end
699 local val = buf(cur+1, 1):uint()
700 cur = cur + 2
701 return true, val
702 elseif ftag == 0x10 then
703 if a < 3 then return false, (3 - a) end
704 local val = buf(cur+1, 2):uint()
705 cur = cur + 3
706 return true, val
707 elseif ftag == 0x11 then
708 if a < 5 then return false, (5 - a) end
709 local val = buf(cur+1, 4):uint()
710 cur = cur + 5
711 return true, val
712 elseif ftag == 0x14 then
713 if a < 5 then return false, (5 - a) end
714 local blen = buf(cur+1, 4):uint()
715 if a < 5 + blen then return false, (5 + blen - a) end
716 cur = cur + 5 + blen
717 return true, nil
718 elseif ftag == 0x26 then
719 if a < 5 then return false, (5 - a) end
720 local chars = buf(cur+1, 4):uint()
721 local blen = chars * 2
722 if a < 5 + blen then return false, (5 + blen - a) end
723 cur = cur + 5 + blen
724 return true, nil
725 else
726 return false, nil
727 end
728 end
729
730 local ok, val = read_field() -- Tx ID
731 if not ok then return val and -val or 0 end
732
733 ok, val = read_field() -- Msg Type
734 if not ok then return val and -val or 0 end
735
736 ok, val = read_field() -- Arg Count
737 if not ok then return val and -val or 0 end
738 local arg_count = val
739
740 local a = len - cur
741 if a < 1 then return -(1 - a) end
742 if buf(cur, 1):uint() == 0x14 then
743 ok, val = read_field()
744 if not ok then return val and -val or 0 end
745 end
746
747 for i = 1, arg_count do
748 ok, val = read_field()
749 if not ok then return val and -val or 0 end
750 end
751
752 return cur - offset
753end
754
755local function dissect_db_tcp(buf, pkt, root, length)
756 local off = 0
757 local found_db = false
758 local db_types = {}
759 local tx_ids = {}
760
761 local src_port = pkt.src_port
762 local dst_port = pkt.dst_port
763 local stream_id = get_stream_key(pkt)
764
765 while off < length do
766 local available = length - off
767 if available < 5 then
768 pkt.desegment_offset = off
769 pkt.desegment_len = 5 - available
770 return true
771 end
772
773 local tag = buf(off, 1):uint()
774 local magic_or_val = buf(off+1, 4):uint()
775
776 if tag == 0x11 and (magic_or_val == 0x872349ae or magic_or_val == 0x00000001) then
777 pdjl_streams[stream_id] = true
778
779 local msg_len = get_db_msg_len(buf, off)
780 if msg_len < 0 then
781 pkt.desegment_offset = off
782 pkt.desegment_len = -msg_len
783 return true
784 elseif msg_len == 0 then
785 break -- parse error, stop processing
786 end
787
788 if magic_or_val == 0x872349ae then
789 if not found_db then
790 pkt.cols.protocol = "Pro DJ Link (DB)"
791 found_db = true
792 end
793
794 local msg_tree = root:add(p_djl, buf(off, msg_len))
795 msg_tree:add(f_db_magic, buf(off+1, 4))
796
797 local current = off + 5
798
799 -- Transaction ID
800 local tx_id = 0
801 if msg_len - (current - off) >= 5 then
802 local bytes_read, val = dissect_db_field(buf, current, msg_tree, nil, "Transaction ID")
803 tx_id = val or 0
804 local tx_str = (tx_id == 0xfffffffe) and "Setup/Teardown" or string.format("0x%08x", tx_id)
805
806 local already_added = false
807 for _, v in ipairs(tx_ids) do
808 if v == tx_str then already_added = true break end
809 end
810 if not already_added then
811 table.insert(tx_ids, tx_str)
812 end
813
814 current = bytes_read
815 end
816
817 -- Message Type
818 local m_type = 0
819 if msg_len - (current - off) >= 3 then
820 local bytes_read, val, m_type_item = dissect_db_field(buf, current, msg_tree, nil, "Message Type")
821 m_type = val or 0
822 local m_type_str = DB_MSG_TYPES[m_type] or string.format("Unknown(0x%04x)", m_type)
823
824 if m_type == 0x0100 and tx_id == 0xfffffffe then
825 m_type_str = "Device Disconnect"
826 if m_type_item then
827 m_type_item:set_text("Message Type: " .. m_type_str .. " (0x" .. string.format("%04x", m_type) .. ")")
828 end
829 end
830
831 table.insert(db_types, m_type_str)
832
833 current = bytes_read
834 msg_tree:append_text(": " .. m_type_str)
835 end
836
837 -- Arg Count
838 local arg_count = 0
839 if msg_len - (current - off) >= 2 then
840 local bytes_read, val = dissect_db_field(buf, current, msg_tree, nil, "Arg Count")
841 arg_count = val or 0
842 current = bytes_read
843 end
844
845 -- Arg Tag Blob (usually 12 bytes)
846 if msg_len - (current - off) >= 5 and buf(current, 1):uint() == 0x14 then
847 local tag_len = buf(current+1, 4):uint()
848 if msg_len - (current - off) >= 5 + tag_len then
849 local tag_item = msg_tree:add(f_db_tag_blob, buf(current+5, tag_len))
850 local arg_types = {}
851 for i = 0, tag_len - 1 do
852 local t = buf(current+5+i, 1):uint()
853 if t ~= 0 then
854 table.insert(arg_types, ARG_TAGS[t] or string.format("0x%02x", t))
855 end
856 end
857 if #arg_types > 0 then
858 tag_item:set_text("Arg Types: " .. table.concat(arg_types, ", "))
859 end
860 current = current + 5 + tag_len
861 end
862 end
863
864 -- Arguments
865 for i = 1, arg_count do
866 if current >= off + msg_len then break end
867 local prev_current = current
868 local bytes_read, val = dissect_db_field(buf, current, msg_tree, m_type, i)
869
870 -- If it's a Success/Ack, show what it's acking in the summary
871 if m_type == 0x4000 then
872 if i == 1 and val then
873 local ack_type_str = DB_MSG_TYPES[val] or string.format("0x%04x", val)
874 msg_tree:append_text(" - " .. ack_type_str)
875 db_types[#db_types] = "Ack " .. ack_type_str
876 elseif i == 2 and val then
877 msg_tree:append_text(": " .. val .. " Items")
878 db_types[#db_types] = db_types[#db_types] .. ": " .. val .. " Items"
879 end
880 end
881
882 -- If it's a GetAnalysisTag request, record the tag for the response
883 if m_type == 0x2c04 and i == 3 and val then
884 local tag_name = ANALYSIS_TAGS[val] or get_reverse_ascii(buf(prev_current+1, 4))
885 analysis_requests[stream_id .. ":" .. tx_id] = tag_name
886 msg_tree:append_text(" - " .. tag_name)
887 -- Update the last entry in db_types to include the tag
888 db_types[#db_types] = db_types[#db_types] .. " - " .. tag_name
889 end
890
891 -- If it's a TrackLoadNotification, show the Rekordbox ID in the summary
892 if m_type == 0x3100 and i == 2 and val then
893 msg_tree:append_text(" - RB ID: " .. val)
894 db_types[#db_types] = db_types[#db_types] .. " - RB ID: " .. val
895 end
896
897 -- If it's an AnalysisTagResponse, try to find the tag from the request
898 if m_type == 0x4f02 and i == 1 then
899 local tag_name = analysis_requests[stream_id .. ":" .. tx_id]
900 if tag_name then
901 msg_tree:append_text(" - " .. tag_name)
902 db_types[#db_types] = db_types[#db_types] .. " - " .. tag_name
903 end
904 end
905
906 current = bytes_read
907 if current <= prev_current then break end
908 end
909
910 -- Special handling for trailing binary data in blob responses
911 if (m_type == 0x4002 or m_type == 0x4602 or m_type == 0x4702 or m_type == 0x4e02 or m_type == 0x4a02 or m_type == 0x4402 or m_type == 0x4f02) and current < off + msg_len then
912 local label = get_arg_label(m_type, 4) or "Blob Data"
913 local remaining = (off + msg_len) - current
914 msg_tree:add(f_db_field_bin, buf(current, remaining)):set_text(label .. ": [" .. remaining .. " bytes]")
915 current = current + remaining
916 end
917
918 msg_tree:set_len(msg_len)
919 off = off + msg_len
920 elseif magic_or_val == 0x00000001 then
921 if not found_db then
922 pkt.cols.protocol = "Pro DJ Link (DB)"
923 found_db = true
924 end
925 table.insert(db_types, "Greeting")
926 root:add(p_djl, buf(off, 5)):set_text("Greeting (0x00000001)")
927 off = off + 5
928 end
929 else
930 break
931 end
932 end
933
934 if found_db then
935 -- Group consecutive message types for a concise summary (e.g., "MenuItem x 10")
936 local summarized_types = {}
937 local current_type = nil
938 local current_count = 0
939
940 for _, t in ipairs(db_types) do
941 if t == current_type then
942 current_count = current_count + 1
943 else
944 if current_type then
945 local entry = (current_count > 1) and (current_type .. " x " .. current_count) or current_type
946 table.insert(summarized_types, entry)
947 end
948 current_type = t
949 current_count = 1
950 end
951 end
952 -- Final entry
953 if current_type then
954 local entry = (current_count > 1) and (current_type .. " x " .. current_count) or current_type
955 table.insert(summarized_types, entry)
956 end
957
958 local info_str = table.concat(summarized_types, ", ")
959 -- Only append the first transaction ID found in the packet
960 if #tx_ids > 0 then
961 info_str = info_str .. " [tx: " .. tx_ids[1] .. "]"
962 end
963 pkt.cols.info:set(info_str)
964 pkt.cols.info:fence()
965 return true
966 end
967 return false
968end
969
970-- ============================================================================
971-- HELPERS - PORT 50000/50001/50002
972-- ============================================================================
973
974local function dissect_management_port(buf, pkt, tree, p_type)
975 tree:add(f_device_name, buf(0x0C, 20))
976 if p_type == 0x06 then -- Keep-Alive
977 tree:add(f_dev_num, buf(0x24, 1))
978 local dtype = buf(0x25, 1):uint()
979 tree:add(f_dev_type, buf(0x25, 1)):append_text(" (" .. (DEV_TYPES[dtype] or "Unknown") .. ")")
980 tree:add(f_mac, buf(0x26, 6))
981 tree:add(f_ip, buf(0x2C, 4))
982 tree:add(f_peer_count, buf(0x30, 1))
983 elseif p_type == 0x0a then -- Announcement
984 local dtype = buf(0x24, 1):uint()
985 tree:add(f_dev_type, buf(0x24, 1)):append_text(" (" .. (DEV_TYPES[dtype] or "Unknown") .. ")")
986 elseif p_type == 0x00 then -- Claim Stage 1
987 tree:add(f_sequence, buf(0x24, 1))
988 local dtype = buf(0x25, 1):uint()
989 tree:add(f_dev_type, buf(0x25, 1)):append_text(" (" .. (DEV_TYPES[dtype] or "Unknown") .. ")")
990 tree:add(f_mac, buf(0x26, 6))
991 elseif p_type == 0x02 then -- Claim Stage 2
992 tree:add(f_ip, buf(0x24, 4))
993 tree:add(f_mac, buf(0x28, 6))
994 tree:add(f_dev_num, buf(0x2E, 1))
995 tree:add(f_sequence, buf(0x2F, 1))
996 elseif p_type == 0x04 then -- Claim Final
997 tree:add(f_dev_num, buf(0x24, 1))
998 tree:add(f_sequence, buf(0x25, 1))
999 end
1000end
1001
1002local function dissect_beat_sync_port(buf, pkt, tree, p_type)
1003 if p_type == 0x28 then -- Beat
1004 tree:add(f_device_name, buf(0x0B, 20))
1005 tree:add(f_dev_num, buf(0x21, 1))
1006 tree:add(f_next_beat, buf(0x24, 4))
1007 tree:add(f_second_beat, buf(0x28, 4))
1008 tree:add(f_next_bar, buf(0x2C, 4))
1009 tree:add(f_fourth_beat, buf(0x30, 4))
1010 tree:add(f_second_bar, buf(0x34, 4))
1011 tree:add(f_eighth_beat, buf(0x38, 4))
1012
1013 local raw_pitch = buf(0x54, 4):uint()
1014 local pitch_pct = (raw_pitch / 0x100000 - 1.0) * 100
1015 tree:add(f_pitch, buf(0x54, 4)):set_text(string.format("Pitch: %+.2f%%", pitch_pct))
1016
1017 local bpm_val = buf(0x5a, 2):uint()
1018 tree:add(f_bpm_raw, buf(0x5a, 2)):set_text(string.format("BPM: %.2f", bpm_val / 100))
1019 tree:add(f_beat_in_bar, buf(0x5c, 1))
1020 elseif p_type == 0x0b then -- Absolute Position
1021 tree:add(f_dev_num, buf(0x21, 1))
1022 tree:add(f_track_len, buf(0x24, 4))
1023 tree:add(f_playhead, buf(0x28, 4))
1024 tree:add(f_pitch_abs, buf(0x2C, 4))
1025 tree:add(f_bpm_abs, buf(0x38, 4))
1026 elseif p_type == 0x2a then -- Sync Control
1027 tree:add(f_dev_num, buf(0x21, 1))
1028 local scmd = buf(0x2b, 1):uint()
1029 tree:add(f_sync_cmd, buf(0x2b, 1)):append_text(" (" .. (SYNC_COMMANDS[scmd] or "Unknown") .. ")")
1030 end
1031end
1032
1033local function dissect_status_media_port(buf, pkt, tree, p_type, length)
1034 if p_type == 0x05 then -- Media Query
1035 tree:add(f_device_name, buf(0x0B, 20))
1036 tree:add(f_dev_num, buf(0x21, 1))
1037 if length >= 0x30 then
1038 tree:add(f_querying_ip, buf(0x24, 4))
1039 tree:add(f_queried_dev, buf(0x2B, 1)) -- Target Device: 1 byte at 0x2B
1040 tree:add(f_queried_slot, buf(0x2F, 1)) -- Target Slot: 1 byte at 0x2F
1041 end
1042 elseif p_type == 0x06 then -- Media Response
1043 tree:add(f_device_name, buf(0x0B, 20))
1044 tree:add(f_dev_num, buf(0x21, 1))
1045
1046 -- The real Pioneer response is 192 bytes (0xC0): 36 bytes header + 156 bytes payload
1047 if length >= 0xC0 then
1048 local enc = (ENC_UTF_16 or 2) + (ENC_BIG_ENDIAN or 0)
1049 tree:add(f_slot_owner, buf(0x27, 1)) -- Target Device ($D_r$)
1050 tree:add(f_queried_slot, buf(0x2B, 1)) -- Target Slot ($S_r$): 1 byte at 0x2B
1051
1052 -- Media Name starts at 0x2C (UTF-16, 64 bytes)
1053 local name_str = buf(0x2C, 64):string(enc):gsub("%z", "")
1054 tree:add(f_media_name, buf(0x2C, 64), name_str)
1055
1056 -- DB Creation Date starts at 0x6C (UTF-16, 40 bytes)
1057 local date_str = buf(0x6C, 40):string(enc):gsub("%z", "")
1058 tree:add(f_media_db_date, buf(0x6C, 40), date_str)
1059
1060 -- Unknown Text at 0x94–0xA5 (18 bytes)
1061 local unknown_str = buf(0x94, 18):string(enc):gsub("%z", "")
1062 tree:add(f_raw_payload, buf(0x94, 18)):set_text("Unknown Text: \"" .. unknown_str .. "\"")
1063
1064 tree:add(f_media_tracks, buf(0xa6, 2))
1065 local c_val = buf(0xa8, 1):uint()
1066 local c_str = INTERFACE_COLORS[c_val] or "Unknown"
1067 tree:add(f_interface_color, buf(0xa8, 1)):set_text("Interface Color: " .. c_str .. " (" .. c_val .. ")")
1068
1069 tree:add(f_media_type, buf(0xaa, 1)) -- Media Type $T_r$ at correct offset
1070 tree:add(f_settings_present, buf(0xab, 1)) -- Settings presence bit
1071
1072 tree:add(f_playlist_count, buf(0xae, 2))
1073 tree:add(f_total_media_space, buf(0xb0, 8))
1074 tree:add(f_avail_media_space, buf(0xb8, 8))
1075 elseif length >= 0x2C + 32 then
1076 -- Fallback: 80-byte synthetic format from our own NFS server (djlink/src/server.rs).
1077 -- This does NOT match real Pioneer hardware; offsets differ from the spec above.
1078 local enc = (ENC_ASCII or 0)
1079 local name_str = buf(0x2C, 32):string(enc):gsub("%z", "")
1080 tree:add(f_media_type, buf(0x26, 1))
1081 tree:add(f_media_status, buf(0x27, 1))
1082 tree:add(f_media_tracks, buf(0x28, 4))
1083 tree:add(f_media_name, buf(0x2C, 32), name_str)
1084 end
1085 elseif p_type == 0x0a then -- CDJ Status
1086 tree:add(f_device_name, buf(0x0B, 20))
1087 tree:add(f_dev_num, buf(0x21, 1))
1088
1089 tree:add(f_activity, buf(0x27, 1))
1090 tree:add(f_track_dev, buf(0x28, 1))
1091 tree:add(f_slot, buf(0x29, 1))
1092 local ttype = buf(0x2a, 1):uint()
1093 tree:add(f_track_type, buf(0x2a, 1)):append_text(" (" .. (TRACK_TYPES[ttype] or "Unknown") .. ")")
1094 tree:add(f_rb_id, buf(0x2c, 4))
1095 tree:add(f_track_num, buf(0x32, 2))
1096 local tsort = buf(0x35, 1):uint()
1097 tree:add(f_track_sort, buf(0x35, 1))
1098
1099 local pmode1 = buf(0x7b, 1):uint()
1100 tree:add(f_play_mode_1, buf(0x7b, 1)):append_text(" (" .. (PLAY_MODES[pmode1] or "Unknown") .. ")")
1101 tree:add(f_firmware, buf(0x7c, 4))
1102
1103 local flag_tree = tree:add(f_flags, buf(0x89, 1))
1104 flag_tree:add(f_flag_playing, buf(0x89, 1))
1105 flag_tree:add(f_flag_master, buf(0x89, 1))
1106 flag_tree:add(f_flag_sync, buf(0x89, 1))
1107 flag_tree:add(f_flag_onair, buf(0x89, 1))
1108 flag_tree:add(f_flag_bpm, buf(0x89, 1))
1109
1110 local pmode2 = buf(0x8b, 1):uint()
1111 tree:add(f_play_mode_2, buf(0x8b, 1)):append_text(" (" .. (PLAY_MODES[pmode2] or "Unknown") .. ")")
1112
1113 local p_1 = buf(0x8c, 4):uint()
1114 local p_1_pct = (p_1 / 0x100000 - 1.0) * 100
1115 tree:add(f_pitch_1, buf(0x8c, 4)):set_text(string.format("Effective Pitch: %+.2f%%", p_1_pct))
1116
1117 local bpm_val = buf(0x92, 2):uint()
1118 if bpm_val == 0xffff then
1119 tree:add(f_bpm_raw, buf(0x92, 2)):set_text("BPM: None")
1120 else
1121 tree:add(f_bpm_raw, buf(0x92, 2)):set_text(string.format("BPM: %.2f", bpm_val / 100))
1122 end
1123
1124 local p_2 = buf(0x98, 4):uint()
1125 local p_2_pct = (p_2 / 0x100000 - 1.0) * 100
1126 tree:add(f_pitch_2, buf(0x98, 4)):set_text(string.format("Local Pitch: %+.2f%%", p_2_pct))
1127
1128 tree:add(f_master_info, buf(0x9e, 1))
1129 tree:add(f_handoff, buf(0x9f, 1))
1130
1131 local beat_count = buf(0xa0, 4):uint()
1132 if beat_count == 0xffffffff then
1133 tree:add(f_beat_count, buf(0xa0, 4)):set_text("Beat Count: None")
1134 else
1135 tree:add(f_beat_count, buf(0xa0, 4)):set_text("Beat Count: " .. beat_count)
1136 end
1137
1138 tree:add(f_countdown, buf(0xa4, 2))
1139 tree:add(f_beat_in_bar, buf(0xa6, 1))
1140
1141 local p_3 = buf(0xc0, 4):uint()
1142 local p_3_pct = (p_3 / 0x100000 - 1.0) * 100
1143 tree:add(f_pitch_3, buf(0xc0, 4)):set_text(string.format("Pitch 3: %+.2f%%", p_3_pct))
1144 end
1145end
1146
1147-- ============================================================================
1148-- MAIN DISSECTOR ENTRY
1149-- ============================================================================
1150
1151local function dissect_pdjl_main(buf, pkt, root, length)
1152 local src_port = pkt.src_port
1153 local dst_port = pkt.dst_port
1154
1155 -- TCP DB Port Discovery
1156 if (src_port == 12523 or dst_port == 12523) then
1157 if length == 2 then
1158 local tree = root:add(p_djl, buf(0, 2))
1159 pkt.cols.protocol = "Pro DJ Link (DB Port)"
1160 pkt.cols.info:set("Port Discovery Response")
1161 tree:add(f_db_port, buf(0, 2))
1162 return true
1163 elseif (length == 15 and buf(0, 1):uint() == 0x52) or (length == 19 and buf(0, 4):uint() == 0x0000000f) then
1164 local tree = root:add(p_djl, buf(0, length))
1165 pkt.cols.protocol = "Pro DJ Link (DB Port)"
1166 pkt.cols.info:set("Port Discovery Request")
1167 tree:add(f_raw_payload, buf(0, length)):set_text("Discovery Request: RemoteDBServer")
1168 return true
1169 end
1170 end
1171
1172 if length < 5 then return false end
1173
1174 -- Check for "Qspt1WmJOL" header (UDP)
1175 if length >= 10 and buf(0, 10):string() == "Qspt1WmJOL" then
1176 -- UDP Logic
1177 pkt.cols.protocol = "Pro DJ Link"
1178 local tree = root:add(p_djl, buf(0, length))
1179 tree:add(f_header, buf(0, 10))
1180
1181 local p_type = buf(10, 1):uint()
1182 local p_type_str = PACKET_TYPES[p_type]
1183
1184 if dst_port == 50002 or src_port == 50002 then
1185 if p_type == 0x05 then p_type_str = "Media Query" end
1186 if p_type == 0x06 then p_type_str = "Media Response" end
1187 end
1188
1189 if p_type_str then
1190 tree:add(f_type, buf(10, 1)):append_text(" (" .. p_type_str .. ")")
1191 pkt.cols.info:set(p_type_str)
1192 else
1193 local unknown_label = string.format("Unknown(0x%02x)", p_type)
1194 tree:add(f_type, buf(10, 1)):append_text(" (" .. unknown_label .. ")")
1195
1196 if length > 11 then
1197 local raw_hex = buf(11):bytes():tohex()
1198 tree:add(f_raw_payload, buf(11))
1199 pkt.cols.info:set(unknown_label .. ": " .. raw_hex)
1200 else
1201 pkt.cols.info:set(unknown_label)
1202 end
1203 end
1204
1205 pkt.cols.info:fence()
1206
1207 if dst_port == 50000 or src_port == 50000 then
1208 dissect_management_port(buf, pkt, tree, p_type)
1209 elseif dst_port == 50001 or src_port == 50001 then
1210 dissect_beat_sync_port(buf, pkt, tree, p_type)
1211 elseif dst_port == 50002 or src_port == 50002 then
1212 dissect_status_media_port(buf, pkt, tree, p_type, length)
1213 end
1214 return true
1215 else
1216 -- Fallback to DB TCP parser
1217 return dissect_db_tcp(buf, pkt, root, length)
1218 end
1219end
1220
1221function p_djl.dissector(buf, pkt, root)
1222 dissect_pdjl_main(buf, pkt, root, buf:len())
1223end
1224
1225-- Heuristic TCP dissector to pick up DB traffic on dynamic ports
1226p_djl:register_heuristic("tcp", function(buf, pkt, root)
1227 local stream_id = get_stream_key(pkt)
1228 if buf:len() < 5 then
1229 -- If we already know this stream, claim even small packets
1230 if pdjl_streams[stream_id] then
1231 return dissect_pdjl_main(buf, pkt, root, buf:len())
1232 end
1233 return false
1234 end
1235
1236 local tag = buf(0, 1):uint()
1237 local val = buf(1, 4):uint()
1238 -- Match DB Magic, DB Greeting, or a previously identified PDJL stream
1239 if (tag == 0x11 and (val == 0x872349ae or val == 0x00000001)) or pdjl_streams[stream_id] then
1240 return dissect_pdjl_main(buf, pkt, root, buf:len())
1241 end
1242 return false
1243end)
1244
1245local udp_table = DissectorTable.get("udp.port")
1246udp_table:add(50000, p_djl)
1247udp_table:add(50001, p_djl)
1248udp_table:add(50002, p_djl)
1249
1250local tcp_table = DissectorTable.get("tcp.port")
1251tcp_table:add(12523, p_djl)
1252tcp_table:add(1051, p_djl)
1253tcp_table:add(1052, p_djl)