diff --git a/dante_control.go b/dante_control.go index 63e770f..8d45171 100644 --- a/dante_control.go +++ b/dante_control.go @@ -545,73 +545,34 @@ func (t *Tendrils) queryDanteSubscriptions3400(conn *net.UDPConn, ip net.IP, rxC log.Printf("[dante] %s: 0x3400 page %d: found %d records", ip, pageNum, recordCount) } - lastDeviceName := "" for i := 0; i < recordCount; i++ { offsetPos := 18 + i*2 if offsetPos+2 > len(resp) { break } rawOffset := int(binary.BigEndian.Uint16(resp[offsetPos : offsetPos+2])) - recordOffset := rawOffset - 30 - if recordOffset < 0 || recordOffset+24 >= len(resp) { + if rawOffset+48 > len(resp) { + continue + } + + txChOffset := int(binary.BigEndian.Uint16(resp[rawOffset+44 : rawOffset+46])) + txDevOffset := int(binary.BigEndian.Uint16(resp[rawOffset+46 : rawOffset+48])) + + if txChOffset == 0 && txDevOffset == 0 { continue } var txDeviceName, txChannelName string - firstByte := resp[recordOffset] - if firstByte >= 0x20 && firstByte < 0x7f { - txChannelName = extractNullTerminatedString(resp, recordOffset) - txDeviceName = extractNullTerminatedString(resp, recordOffset+len(txChannelName)+1) - } else { - stringStart := -1 - for j := recordOffset + 10; j < recordOffset+24 && j+3 < len(resp); j++ { - if resp[j] == 0x02 && resp[j+1] == 0x02 && resp[j+2] == 0x00 && resp[j+3] == 0x00 { - stringStart = j + 4 - break - } - } - if stringStart >= 0 && stringStart < len(resp) { - str1 := extractNullTerminatedString(resp, stringStart) - str2 := extractNullTerminatedString(resp, stringStart+len(str1)+1) - hasLetter := func(s string) bool { - for _, c := range s { - if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') { - return true - } - } - return false - } - if !hasLetter(str1) && !hasLetter(str2) { - continue - } - noDeviceIndicator := stringStart >= 6 && resp[stringStart-6] == 0x00 && resp[stringStart-5] == 0x00 - str2IsNumeric := len(str2) > 0 - for _, c := range str2 { - if c < '0' || c > '9' { - str2IsNumeric = false - break - } - } - if noDeviceIndicator && str2IsNumeric && lastDeviceName != "" { - txChannelName = str1 - txDeviceName = lastDeviceName - } else if str2IsNumeric { - txDeviceName = str1 - txChannelName = str2 - } else if hasLetter(str2) { - txDeviceName = str2 - txChannelName = str1 - } else { - txChannelName = str1 - txDeviceName = lastDeviceName - } - } + if txChOffset > 0 && txChOffset < len(resp) { + txChannelName = extractNullTerminatedString(resp, txChOffset) + } + if txDevOffset > 0 && txDevOffset < len(resp) { + txDeviceName = extractNullTerminatedString(resp, txDevOffset) } if txDeviceName == "" { continue } - lastDeviceName = txDeviceName rxChannel := startChannel + i if t.DebugDante { diff --git a/notes/dante.pcap b/notes/dante.pcap index bd45fa8..0a4bee5 100644 Binary files a/notes/dante.pcap and b/notes/dante.pcap differ diff --git a/notes/dantepacket.md b/notes/dantepacket.md new file mode 100644 index 0000000..109c597 --- /dev/null +++ b/notes/dantepacket.md @@ -0,0 +1,134 @@ +# Dante Control Protocol Notes + +All Dante control packets are UDP on port 4440. + +## Packet Header (10 bytes) + +``` +0x00: u8 magic # 0x27=standard, 0x28=extended +0x01: u8 seq_lo +0x02: u16 length # total packet length +0x04: u16 seq_id +0x06: u16 command +0x08: u16 status +0x0a: ... args/data +``` + +All multi-byte fields are big-endian. + +## Commands + +### 0x1000 - Channel Count Query + +Response: +``` +0x0c: u16 tx_count +0x0e: u16 rx_count +``` + +### 0x3000 - Subscription Query (Standard) + +Works for multicast; may return empty for unicast. + +Request args (6 bytes): +``` +0x00: u16 0x0001 +0x02: u16 page_num +0x04: u16 0x0000 +``` + +Response record types (at 0x0e): +- `0x0006` = unicast +- `0x000e` = multicast + +### 0x3400 - Subscription Query (Extended) + +Uses magic=0x28. Works for unicast flows. + +Request args (24 bytes): +``` +0x07: u8 0x01 +0x08: u16 page_type # 0x0001=first, 0x0003=subsequent +0x0a: u16 start_ch # 1, 17, 33, 49, ... +``` + +## 0x3400 Response + +``` +0x00: [10] header +0x0a: [8] extended_header +0x12: [32] offset_table # 16 × u16 record offsets +0x32: ... records + strings +``` + +Status (at 0x08): +- `0x8112` = has subscription data +- `0x0001` = empty page + +### Offset Table + +16 entries at 0x12-0x31, each a u16 absolute offset to a record. +Zero offset = no more records. + +### Subscription Record + +Each record is 56+ bytes. The offset table points to record start. + +``` +record+0x00: [40] record_header +record+0x28: u16 0x0608 # marker +record+0x2a: u16 0x0000 +record+0x2c: u16 tx_ch_offset # absolute offset to TX channel name string +record+0x2e: u16 tx_dev_offset # absolute offset to TX device name string +record+0x30: [4] flags +record+0x34: u32 0x02020000 # string marker +record+0x38: ... inline_strings # (unreliable, use offsets above) +``` + +**Subscription status:** +- Both offsets non-zero = subscribed +- Both offsets zero = unsubscribed + +### String Table + +Null-terminated strings referenced by absolute offset from packet start. +Multiple records share strings (e.g., same device name). + +## Example: Page 2 Response (channels 17-32) + +``` +Offset table: 0050 0090 00d0 0110 0158 0194 01d8 021c 0258 ... + +Record at 0x0050 (RX ch 17): + 0x0050+0x2c: 0032 0035 + String at 0x32: "01" -> TX channel + String at 0x35: "MICS-E" -> TX device + Result: MICS-E[01] -> ch17 + +Record at 0x0158 (RX ch 21): + 0x0158+0x2c: 0032 0148 + String at 0x32: "01" + String at 0x148: "TX-QLAB-1" + Result: TX-QLAB-1[01] -> ch21 + +Record at 0x01d8 (RX ch 23): + 0x01d8+0x2c: 01cc 01d1 + String at 0x1cc: "Left" + String at 0x1d1: "BT" + Result: BT[Left] -> ch23 + +Record at 0x0258 (RX ch 25): + 0x0258+0x2c: 0000 0000 + Result: UNSUBSCRIBED +``` + +## Parsing Algorithm + +1. Check magic=0x28, command=0x3400, status=0x8112 +2. Read 16 offsets from 0x12-0x31 +3. For each non-zero offset: + - Read u16 at offset+0x2c (tx_ch_offset) + - Read u16 at offset+0x2e (tx_dev_offset) + - If both zero: skip (unsubscribed) + - Else: read null-terminated strings at those offsets + - RX channel = page_start + record_index