Add ArtRDM TOD discovery support

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ian Gulliver
2026-01-31 07:47:23 -08:00
parent 17ab11b048
commit a1431d5274
4 changed files with 142 additions and 10 deletions

View File

@@ -14,6 +14,7 @@ type Node struct {
LongName string LongName string
Inputs []Universe Inputs []Universe
Outputs []Universe Outputs []Universe
RDMUIDs map[Universe][]RDMUID
LastSeen time.Time LastSeen time.Time
} }
@@ -130,6 +131,7 @@ func (d *Discovery) HandlePollReply(src *net.UDPAddr, pkt *PollReplyPacket) {
node = &Node{ node = &Node{
IP: src.IP, IP: src.IP,
Port: pkt.Port, Port: pkt.Port,
RDMUIDs: map[Universe][]RDMUID{},
} }
d.nodes[ip] = node d.nodes[ip] = node
} }
@@ -139,6 +141,7 @@ func (d *Discovery) HandlePollReply(src *net.UDPAddr, pkt *PollReplyPacket) {
node.MAC = pkt.MACAddr() node.MAC = pkt.MACAddr()
node.LastSeen = time.Now() node.LastSeen = time.Now()
var newOutputs []Universe
for _, u := range pkt.InputUniverses() { for _, u := range pkt.InputUniverses() {
if !containsUniverse(node.Inputs, u) { if !containsUniverse(node.Inputs, u) {
node.Inputs = append(node.Inputs, u) node.Inputs = append(node.Inputs, u)
@@ -147,12 +150,45 @@ func (d *Discovery) HandlePollReply(src *net.UDPAddr, pkt *PollReplyPacket) {
for _, u := range pkt.OutputUniverses() { for _, u := range pkt.OutputUniverses() {
if !containsUniverse(node.Outputs, u) { if !containsUniverse(node.Outputs, u) {
node.Outputs = append(node.Outputs, u) node.Outputs = append(node.Outputs, u)
newOutputs = append(newOutputs, u)
} }
} }
if d.onChange != nil { if d.onChange != nil {
d.onChange(node) d.onChange(node)
} }
if len(newOutputs) > 0 {
go d.requestTod(src, newOutputs)
}
}
func (d *Discovery) requestTod(addr *net.UDPAddr, universes []Universe) {
for _, u := range universes {
d.sender.SendTodRequest(addr, u)
time.Sleep(50 * time.Millisecond)
}
}
func (d *Discovery) HandleTodData(src *net.UDPAddr, pkt *TodDataPacket) {
d.nodesMu.Lock()
defer d.nodesMu.Unlock()
ip := src.IP.String()
node, exists := d.nodes[ip]
if !exists {
return
}
if node.RDMUIDs == nil {
node.RDMUIDs = map[Universe][]RDMUID{}
}
node.RDMUIDs[pkt.Universe] = pkt.UIDs
if d.onChange != nil {
d.onChange(node)
}
} }
func (d *Discovery) HandlePoll(src *net.UDPAddr) { func (d *Discovery) HandlePoll(src *net.UDPAddr) {

View File

@@ -19,6 +19,7 @@ const (
OpSync uint16 = 0x5200 OpSync uint16 = 0x5200
OpAddress uint16 = 0x6000 OpAddress uint16 = 0x6000
OpInput uint16 = 0x7000 OpInput uint16 = 0x7000
OpTodRequest uint16 = 0x8000
OpTodData uint16 = 0x8100 OpTodData uint16 = 0x8100
OpTodControl uint16 = 0x8200 OpTodControl uint16 = 0x8200
OpRdm uint16 = 0x8300 OpRdm uint16 = 0x8300
@@ -29,6 +30,8 @@ const (
GoodOutputDataTransmitted uint8 = 0x80 GoodOutputDataTransmitted uint8 = 0x80
GoodInputDataReceived uint8 = 0x80 GoodInputDataReceived uint8 = 0x80
TodCommandFull uint8 = 0x00
StyleNode uint8 = 0x00 StyleNode uint8 = 0x00
StyleController uint8 = 0x01 StyleController uint8 = 0x01
StyleMedia uint8 = 0x02 StyleMedia uint8 = 0x02
@@ -148,6 +151,33 @@ func (p *PollReplyPacket) OutputUniverses() []Universe {
return result return result
} }
type RDMUID [6]byte
func (u RDMUID) Manufacturer() uint16 {
return uint16(u[0])<<8 | uint16(u[1])
}
func (u RDMUID) Device() uint32 {
return uint32(u[2])<<24 | uint32(u[3])<<16 | uint32(u[4])<<8 | uint32(u[5])
}
func (u RDMUID) String() string {
return fmt.Sprintf("%04x:%08x", u.Manufacturer(), u.Device())
}
type TodDataPacket struct {
RdmVer uint8
Port uint8
BindIndex uint8
Net uint8
Command uint8
Universe Universe
UidTotal uint16
BlockCount uint8
UidCount uint8
UIDs []RDMUID
}
func ParsePacket(data []byte) (uint16, interface{}, error) { func ParsePacket(data []byte) (uint16, interface{}, error) {
if len(data) < 10 { if len(data) < 10 {
return 0, nil, ErrPacketTooShort return 0, nil, ErrPacketTooShort
@@ -169,6 +199,9 @@ func ParsePacket(data []byte) (uint16, interface{}, error) {
case OpPollReply: case OpPollReply:
pkt, err := parsePollReplyPacket(data) pkt, err := parsePollReplyPacket(data)
return opCode, pkt, err return opCode, pkt, err
case OpTodData:
pkt, err := parseTodDataPacket(data)
return opCode, pkt, err
default: default:
return opCode, nil, nil return opCode, nil, nil
} }
@@ -279,6 +312,53 @@ func BuildPollPacket() []byte {
return buf return buf
} }
func parseTodDataPacket(data []byte) (*TodDataPacket, error) {
if len(data) < 28 {
return nil, ErrPacketTooShort
}
pkt := &TodDataPacket{
RdmVer: data[10],
Port: data[11],
BindIndex: data[20],
Net: data[21],
Command: data[22],
Universe: NewUniverse(data[21], data[23]>>4, data[23]&0x0F),
UidTotal: binary.BigEndian.Uint16(data[24:26]),
BlockCount: data[26],
UidCount: data[27],
}
uidCount := int(pkt.UidCount)
if uidCount > 200 {
uidCount = 200
}
expectedLen := 28 + uidCount*6
if len(data) < expectedLen {
uidCount = (len(data) - 28) / 6
}
pkt.UIDs = make([]RDMUID, uidCount)
for i := 0; i < uidCount; i++ {
copy(pkt.UIDs[i][:], data[28+i*6:28+i*6+6])
}
return pkt, nil
}
func BuildTodRequestPacket(net, subnet, universe uint8) []byte {
buf := make([]byte, 25)
copy(buf[0:8], ID[:])
binary.LittleEndian.PutUint16(buf[8:10], OpTodRequest)
binary.BigEndian.PutUint16(buf[10:12], ProtocolVersion)
buf[20] = net
buf[21] = TodCommandFull
buf[22] = 1
buf[23] = subnet<<4 | (universe & 0x0F)
return buf
}
func BuildPollReplyPacket(ip [4]byte, mac [6]byte, shortName, longName string, universes []Universe, isInput bool) []byte { func BuildPollReplyPacket(ip [4]byte, mac [6]byte, shortName, longName string, universes []Universe, isInput bool) []byte {
buf := make([]byte, 240) buf := make([]byte, 240)
copy(buf[0:8], ID[:]) copy(buf[0:8], ID[:])

View File

@@ -13,6 +13,10 @@ type Handler interface {
HandlePollReply(src *net.UDPAddr, pkt *PollReplyPacket) HandlePollReply(src *net.UDPAddr, pkt *PollReplyPacket)
} }
type TodDataHandler interface {
HandleTodData(src *net.UDPAddr, pkt *TodDataPacket)
}
type Receiver struct { type Receiver struct {
conn *net.UDPConn conn *net.UDPConn
handler Handler handler Handler
@@ -128,5 +132,11 @@ func (r *Receiver) handle(src *net.UDPAddr, data []byte) {
if reply, ok := pkt.(*PollReplyPacket); ok { if reply, ok := pkt.(*PollReplyPacket); ok {
r.handler.HandlePollReply(src, reply) r.handler.HandlePollReply(src, reply)
} }
case OpTodData:
if todHandler, ok := r.handler.(TodDataHandler); ok {
if tod, ok := pkt.(*TodDataPacket); ok {
todHandler.HandleTodData(src, tod)
}
}
} }
} }

View File

@@ -81,6 +81,12 @@ func (s *Sender) SendPollReply(addr *net.UDPAddr, localIP [4]byte, localMAC [6]b
return err return err
} }
func (s *Sender) SendTodRequest(addr *net.UDPAddr, universe Universe) error {
pkt := BuildTodRequestPacket(universe.Net(), universe.SubNet(), universe.Universe())
_, err := s.conn.WriteToUDP(pkt, addr)
return err
}
func (s *Sender) SendRaw(addr *net.UDPAddr, data []byte) error { func (s *Sender) SendRaw(addr *net.UDPAddr, data []byte) error {
_, err := s.conn.WriteToUDP(data, addr) _, err := s.conn.WriteToUDP(data, addr)
return err return err