fix 0x141a record parsing and add dante channel type detection
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -171,10 +171,11 @@ func (d *DanteFlows) LogAll() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
type channelFlow struct {
|
type channelFlow struct {
|
||||||
sourceName string
|
sourceName string
|
||||||
txCh string
|
txCh string
|
||||||
rxName string
|
rxName string
|
||||||
rxCh string
|
rxCh string
|
||||||
|
channelType string
|
||||||
}
|
}
|
||||||
var allChannelFlows []channelFlow
|
var allChannelFlows []channelFlow
|
||||||
var allNoChannelFlows []string
|
var allNoChannelFlows []string
|
||||||
@@ -196,11 +197,18 @@ func (d *DanteFlows) LogAll() {
|
|||||||
for _, ch := range sub.Channels {
|
for _, ch := range sub.Channels {
|
||||||
parts := strings.Split(ch, "->")
|
parts := strings.Split(ch, "->")
|
||||||
if len(parts) == 2 {
|
if len(parts) == 2 {
|
||||||
|
rxPart := parts[1]
|
||||||
|
chType := ""
|
||||||
|
if idx := strings.LastIndex(rxPart, ":"); idx != -1 {
|
||||||
|
chType = rxPart[idx+1:]
|
||||||
|
rxPart = rxPart[:idx]
|
||||||
|
}
|
||||||
allChannelFlows = append(allChannelFlows, channelFlow{
|
allChannelFlows = append(allChannelFlows, channelFlow{
|
||||||
sourceName: sourceName,
|
sourceName: sourceName,
|
||||||
txCh: parts[0],
|
txCh: parts[0],
|
||||||
rxName: subName,
|
rxName: subName,
|
||||||
rxCh: parts[1],
|
rxCh: rxPart,
|
||||||
|
channelType: chType,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
allNoChannelFlows = append(allNoChannelFlows, fmt.Sprintf("%s -> %s[%s]", sourceName, subName, ch))
|
allNoChannelFlows = append(allNoChannelFlows, fmt.Sprintf("%s -> %s[%s]", sourceName, subName, ch))
|
||||||
@@ -225,7 +233,11 @@ func (d *DanteFlows) LogAll() {
|
|||||||
sort.Strings(allNoChannelFlows)
|
sort.Strings(allNoChannelFlows)
|
||||||
|
|
||||||
for _, cf := range allChannelFlows {
|
for _, cf := range allChannelFlows {
|
||||||
log.Printf("[sigusr1] %s[%s] -> %s[%s]", cf.sourceName, cf.txCh, cf.rxName, cf.rxCh)
|
if cf.channelType != "" {
|
||||||
|
log.Printf("[sigusr1] %s[%s] -> %s[%s] (%s)", cf.sourceName, cf.txCh, cf.rxName, cf.rxCh, cf.channelType)
|
||||||
|
} else {
|
||||||
|
log.Printf("[sigusr1] %s[%s] -> %s[%s]", cf.sourceName, cf.txCh, cf.rxName, cf.rxCh)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for _, flow := range allNoChannelFlows {
|
for _, flow := range allNoChannelFlows {
|
||||||
log.Printf("[sigusr1] %s", flow)
|
log.Printf("[sigusr1] %s", flow)
|
||||||
@@ -266,8 +278,11 @@ func (t *Tendrils) queryDanteDeviceWithPort(ip net.IP, port int) *DanteDeviceInf
|
|||||||
if t.DebugDante {
|
if t.DebugDante {
|
||||||
log.Printf("[dante] %s: 0x3000 returned %d subscriptions, hasMulticast=%v", ip, len(info.Subscriptions), info.HasMulticast)
|
log.Printf("[dante] %s: 0x3000 returned %d subscriptions, hasMulticast=%v", ip, len(info.Subscriptions), info.HasMulticast)
|
||||||
}
|
}
|
||||||
if len(info.Subscriptions) == 0 && info.RxChannelCount > 0 {
|
if info.RxChannelCount > 0 && (len(info.Subscriptions) == 0 || info.HasMulticast) {
|
||||||
info.Subscriptions = t.queryDanteSubscriptions3400(conn, ip, info.RxChannelCount)
|
subs3400 := t.queryDanteSubscriptions3400(conn, ip, info.RxChannelCount)
|
||||||
|
if len(subs3400) > 0 {
|
||||||
|
info.Subscriptions = subs3400
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,10 +303,30 @@ type DanteDeviceInfo struct {
|
|||||||
HasMulticast bool
|
HasMulticast bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DanteChannelType uint16
|
||||||
|
|
||||||
|
const (
|
||||||
|
DanteChannelUnknown DanteChannelType = 0
|
||||||
|
DanteChannelAudio DanteChannelType = 0x000f
|
||||||
|
DanteChannelVideo DanteChannelType = 0x000e
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t DanteChannelType) String() string {
|
||||||
|
switch t {
|
||||||
|
case DanteChannelAudio:
|
||||||
|
return "audio"
|
||||||
|
case DanteChannelVideo:
|
||||||
|
return "video"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type DanteSubscription struct {
|
type DanteSubscription struct {
|
||||||
RxChannel int
|
RxChannel int
|
||||||
TxDeviceName string
|
TxDeviceName string
|
||||||
TxChannelName string
|
TxChannelName string
|
||||||
|
ChannelType DanteChannelType
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildDantePacket(cmd uint16, args []byte) []byte {
|
func buildDantePacket(cmd uint16, args []byte) []byte {
|
||||||
@@ -610,12 +645,34 @@ func (t *Tendrils) queryDanteSubscriptions3400(conn *net.UDPConn, ip net.IP, rxC
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
rawOffset := int(binary.BigEndian.Uint16(resp[offsetPos : offsetPos+2]))
|
rawOffset := int(binary.BigEndian.Uint16(resp[offsetPos : offsetPos+2]))
|
||||||
if rawOffset+48 > len(resp) {
|
if rawOffset+28 > len(resp) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
txChOffset := int(binary.BigEndian.Uint16(resp[rawOffset+44 : rawOffset+46]))
|
var channelType DanteChannelType
|
||||||
txDevOffset := int(binary.BigEndian.Uint16(resp[rawOffset+46 : rawOffset+48]))
|
var txChOffset, txDevOffset int
|
||||||
|
|
||||||
|
marker := binary.BigEndian.Uint16(resp[rawOffset : rawOffset+2])
|
||||||
|
if marker == 0x141c {
|
||||||
|
if rawOffset+48 > len(resp) {
|
||||||
|
log.Printf("[ERROR] [dante] %s: 0x3400 record %d at 0x%04x: 0x141c record truncated (need %d, have %d)", ip, i, rawOffset, rawOffset+48, len(resp))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
channelType = DanteChannelType(binary.BigEndian.Uint16(resp[rawOffset+14 : rawOffset+16]))
|
||||||
|
txChOffset = int(binary.BigEndian.Uint16(resp[rawOffset+44 : rawOffset+46]))
|
||||||
|
txDevOffset = int(binary.BigEndian.Uint16(resp[rawOffset+46 : rawOffset+48]))
|
||||||
|
} else if marker == 0x141a {
|
||||||
|
if rawOffset+48 > len(resp) {
|
||||||
|
log.Printf("[ERROR] [dante] %s: 0x3400 record %d at 0x%04x: 0x141a record truncated", ip, i, rawOffset)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
channelType = DanteChannelVideo
|
||||||
|
txChOffset = int(binary.BigEndian.Uint16(resp[rawOffset+44 : rawOffset+46]))
|
||||||
|
txDevOffset = int(binary.BigEndian.Uint16(resp[rawOffset+46 : rawOffset+48]))
|
||||||
|
} else {
|
||||||
|
log.Printf("[ERROR] [dante] %s: 0x3400 record %d at 0x%04x: unknown marker 0x%04x (bytes: %x)", ip, i, rawOffset, marker, resp[rawOffset:rawOffset+8])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if txChOffset == 0 && txDevOffset == 0 {
|
if txChOffset == 0 && txDevOffset == 0 {
|
||||||
continue
|
continue
|
||||||
@@ -635,13 +692,14 @@ func (t *Tendrils) queryDanteSubscriptions3400(conn *net.UDPConn, ip net.IP, rxC
|
|||||||
|
|
||||||
rxChannel := startChannel + i
|
rxChannel := startChannel + i
|
||||||
if t.DebugDante {
|
if t.DebugDante {
|
||||||
log.Printf("[dante] %s: 0x3400 sub: rx=%d txDev=%q txCh=%q", ip, rxChannel, txDeviceName, txChannelName)
|
log.Printf("[dante] %s: 0x3400 sub: rx=%d txDev=%q txCh=%q type=%s", ip, rxChannel, txDeviceName, txChannelName, channelType)
|
||||||
}
|
}
|
||||||
|
|
||||||
subscriptions = append(subscriptions, DanteSubscription{
|
subscriptions = append(subscriptions, DanteSubscription{
|
||||||
RxChannel: rxChannel,
|
RxChannel: rxChannel,
|
||||||
TxDeviceName: txDeviceName,
|
TxDeviceName: txDeviceName,
|
||||||
TxChannelName: txChannelName,
|
TxChannelName: txChannelName,
|
||||||
|
ChannelType: channelType,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -674,8 +732,8 @@ func (t *Tendrils) probeDanteDeviceWithPort(ip net.IP, port int) {
|
|||||||
needIGMPFallback := info.HasMulticast && info.Name != ""
|
needIGMPFallback := info.HasMulticast && info.Name != ""
|
||||||
for _, sub := range info.Subscriptions {
|
for _, sub := range info.Subscriptions {
|
||||||
if t.DebugDante {
|
if t.DebugDante {
|
||||||
log.Printf("[dante] %s: subscription rx=%d -> %s@%s",
|
log.Printf("[dante] %s: subscription rx=%d -> %s@%s type=%s",
|
||||||
ip, sub.RxChannel, sub.TxChannelName, sub.TxDeviceName)
|
ip, sub.RxChannel, sub.TxChannelName, sub.TxDeviceName, sub.ChannelType)
|
||||||
}
|
}
|
||||||
if sub.TxDeviceName != "" && info.Name != "" {
|
if sub.TxDeviceName != "" && info.Name != "" {
|
||||||
txDeviceName := sub.TxDeviceName
|
txDeviceName := sub.TxDeviceName
|
||||||
@@ -684,7 +742,12 @@ func (t *Tendrils) probeDanteDeviceWithPort(ip net.IP, port int) {
|
|||||||
}
|
}
|
||||||
channelInfo := ""
|
channelInfo := ""
|
||||||
if sub.TxChannelName != "" {
|
if sub.TxChannelName != "" {
|
||||||
channelInfo = fmt.Sprintf("%s->%02d", sub.TxChannelName, sub.RxChannel)
|
typeStr := sub.ChannelType.String()
|
||||||
|
if typeStr != "" {
|
||||||
|
channelInfo = fmt.Sprintf("%s->%02d:%s", sub.TxChannelName, sub.RxChannel, typeStr)
|
||||||
|
} else {
|
||||||
|
channelInfo = fmt.Sprintf("%s->%02d", sub.TxChannelName, sub.RxChannel)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sourceNode := t.nodes.GetOrCreateByName(txDeviceName)
|
sourceNode := t.nodes.GetOrCreateByName(txDeviceName)
|
||||||
subscriberNode := t.nodes.GetOrCreateByName(info.Name)
|
subscriberNode := t.nodes.GetOrCreateByName(info.Name)
|
||||||
|
|||||||
@@ -72,19 +72,44 @@ Zero offset = no more records.
|
|||||||
|
|
||||||
### Subscription Record
|
### Subscription Record
|
||||||
|
|
||||||
Each record is 56+ bytes. The offset table points to record start.
|
Two record formats exist, distinguished by marker position.
|
||||||
|
|
||||||
|
#### Format 1: 0x141c records (numbered audio/video channels)
|
||||||
|
|
||||||
```
|
```
|
||||||
record+0x00: [40] record_header
|
record+0x00: u16 0x141c # record marker
|
||||||
record+0x28: u16 0x0608 # marker
|
record+0x02: u16 channel_num # RX channel number
|
||||||
record+0x2a: u16 0x0000
|
record+0x04: u16 0x0000
|
||||||
|
record+0x06: u16 0x0003
|
||||||
|
record+0x08: u16 channel_num # repeated
|
||||||
|
record+0x0a: u16 0x0000
|
||||||
|
record+0x0c: u16 0x0000
|
||||||
|
record+0x0e: u16 channel_type # 0x000f=audio, 0x000e=video
|
||||||
|
...
|
||||||
record+0x2c: u16 tx_ch_offset # absolute offset to TX channel name string
|
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+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)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Channel type (at record+0x0e):**
|
||||||
|
- `0x000f` = audio channel
|
||||||
|
- `0x000e` = video channel
|
||||||
|
|
||||||
|
#### Format 2: 0x141a records (special channels: Video, Serial, USB)
|
||||||
|
|
||||||
|
```
|
||||||
|
record+0x00: u16 0x141a # record marker
|
||||||
|
record+0x02: u16 channel_num # e.g., 0x0009 = channel 9
|
||||||
|
record+0x04: u16 ??
|
||||||
|
record+0x06: u16 ??
|
||||||
|
...
|
||||||
|
record+0x2c: u16 tx_ch_offset # same position as 0x141c
|
||||||
|
record+0x2e: u16 tx_dev_offset # same position as 0x141c
|
||||||
|
```
|
||||||
|
|
||||||
|
These are video channels (Dante AV "Video" aggregate channel).
|
||||||
|
|
||||||
|
Both 0x141c and 0x141a records use the same offsets for tx_ch and tx_dev (+44 and +46).
|
||||||
|
|
||||||
**Subscription status:**
|
**Subscription status:**
|
||||||
- Both offsets non-zero = subscribed
|
- Both offsets non-zero = subscribed
|
||||||
- Both offsets zero = unsubscribed
|
- Both offsets zero = unsubscribed
|
||||||
@@ -126,9 +151,85 @@ Record at 0x0258 (RX ch 25):
|
|||||||
|
|
||||||
1. Check magic=0x28, command=0x3400, status=0x8112
|
1. Check magic=0x28, command=0x3400, status=0x8112
|
||||||
2. Read 16 offsets from 0x12-0x31
|
2. Read 16 offsets from 0x12-0x31
|
||||||
3. For each non-zero offset:
|
3. For each non-zero offset, detect record format:
|
||||||
- Read u16 at offset+0x2c (tx_ch_offset)
|
- If offset+0x00 == 0x141c: Format 1 (numbered channel)
|
||||||
- Read u16 at offset+0x2e (tx_dev_offset)
|
- channel_type at offset+0x0e (0x000f=audio, 0x000e=video)
|
||||||
- If both zero: skip (unsubscribed)
|
- tx_ch_offset at offset+0x2c
|
||||||
- Else: read null-terminated strings at those offsets
|
- tx_dev_offset at offset+0x2e
|
||||||
- RX channel = page_start + record_index
|
- If offset+0x00 == 0x141a: Format 2 (special channel)
|
||||||
|
- Assume video type
|
||||||
|
- tx_ch_offset at offset+0x2c (same as 0x141c)
|
||||||
|
- tx_dev_offset at offset+0x2e (same as 0x141c)
|
||||||
|
4. If tx_ch_offset and tx_dev_offset are both zero: skip (unsubscribed)
|
||||||
|
5. Read null-terminated strings at those offsets
|
||||||
|
6. RX channel = page_start + record_index
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Additional Commands
|
||||||
|
|
||||||
|
### 0x2000 - TX Channel Info
|
||||||
|
|
||||||
|
Request args (6 bytes):
|
||||||
|
```
|
||||||
|
0x00: u16 0x0001
|
||||||
|
0x02: u16 page_num
|
||||||
|
0x04: u16 0x0000
|
||||||
|
```
|
||||||
|
|
||||||
|
Response contains TX channel entries:
|
||||||
|
```
|
||||||
|
0x00: u16 channel_num
|
||||||
|
0x02: u16 channel_type # see below
|
||||||
|
0x04: u16 name_offset
|
||||||
|
0x06: u16 unknown
|
||||||
|
```
|
||||||
|
|
||||||
|
Observed channel_type values:
|
||||||
|
- `0x0107` = audio channel (seen on MICS, SQ-7, speaker devices)
|
||||||
|
- `0x0007` = possibly video or different encoding?
|
||||||
|
|
||||||
|
Sample rate `0xbb80` (48000) appears in responses.
|
||||||
|
|
||||||
|
### 0x3600 - TX Flow Info
|
||||||
|
|
||||||
|
Uses magic=0x28. Returns info about outgoing flows.
|
||||||
|
|
||||||
|
Response includes IP addresses of flow destinations.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Multicast Group Ranges
|
||||||
|
|
||||||
|
Audio and video use different multicast IP ranges:
|
||||||
|
- `239.69.x.x` - `239.71.x.x` = Dante audio multicast
|
||||||
|
- `239.253.x.x` = Dante AV (video) multicast
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Channel Type Detection
|
||||||
|
|
||||||
|
**SOLVED**: Audio vs video channels are distinguished by the channel_type field at record+0x0e in 0x3400 responses:
|
||||||
|
- `0x000f` = audio channel
|
||||||
|
- `0x000e` = video channel
|
||||||
|
|
||||||
|
Dante AV devices (TX-*, RX-* naming convention) have both audio and video channels. The type must be checked per-channel, not per-device.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
### Other Channel Types
|
||||||
|
|
||||||
|
Video devices have additional channel types with marker 0x141a instead of 0x141c:
|
||||||
|
- "Video" channels (the actual video stream)
|
||||||
|
- "Serial" channels
|
||||||
|
- "USB" channels
|
||||||
|
|
||||||
|
These use a different record structure. Need to decode the 0x141a record format.
|
||||||
|
|
||||||
|
### 0x2000 Channel Type Field
|
||||||
|
|
||||||
|
The 0x2000 response has a channel_type field at entry+0x02. Observed values:
|
||||||
|
- `0x0007` seen on Ultimo X (audio devices)
|
||||||
|
- Need to compare with video device 0x2000 responses
|
||||||
|
|||||||
Reference in New Issue
Block a user