Add ArtRDM TOD discovery support
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
40
discovery.go
40
discovery.go
@@ -14,6 +14,7 @@ type Node struct {
|
||||
LongName string
|
||||
Inputs []Universe
|
||||
Outputs []Universe
|
||||
RDMUIDs map[Universe][]RDMUID
|
||||
LastSeen time.Time
|
||||
}
|
||||
|
||||
@@ -128,8 +129,9 @@ func (d *Discovery) HandlePollReply(src *net.UDPAddr, pkt *PollReplyPacket) {
|
||||
node, exists := d.nodes[ip]
|
||||
if !exists {
|
||||
node = &Node{
|
||||
IP: src.IP,
|
||||
Port: pkt.Port,
|
||||
IP: src.IP,
|
||||
Port: pkt.Port,
|
||||
RDMUIDs: map[Universe][]RDMUID{},
|
||||
}
|
||||
d.nodes[ip] = node
|
||||
}
|
||||
@@ -139,6 +141,7 @@ func (d *Discovery) HandlePollReply(src *net.UDPAddr, pkt *PollReplyPacket) {
|
||||
node.MAC = pkt.MACAddr()
|
||||
node.LastSeen = time.Now()
|
||||
|
||||
var newOutputs []Universe
|
||||
for _, u := range pkt.InputUniverses() {
|
||||
if !containsUniverse(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() {
|
||||
if !containsUniverse(node.Outputs, u) {
|
||||
node.Outputs = append(node.Outputs, u)
|
||||
newOutputs = append(newOutputs, u)
|
||||
}
|
||||
}
|
||||
|
||||
if d.onChange != nil {
|
||||
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) {
|
||||
|
||||
96
protocol.go
96
protocol.go
@@ -13,15 +13,16 @@ const (
|
||||
Port = 6454
|
||||
ProtocolVersion = 14
|
||||
|
||||
OpPoll uint16 = 0x2000
|
||||
OpPollReply uint16 = 0x2100
|
||||
OpDmx uint16 = 0x5000
|
||||
OpSync uint16 = 0x5200
|
||||
OpAddress uint16 = 0x6000
|
||||
OpInput uint16 = 0x7000
|
||||
OpTodData uint16 = 0x8100
|
||||
OpPoll uint16 = 0x2000
|
||||
OpPollReply uint16 = 0x2100
|
||||
OpDmx uint16 = 0x5000
|
||||
OpSync uint16 = 0x5200
|
||||
OpAddress uint16 = 0x6000
|
||||
OpInput uint16 = 0x7000
|
||||
OpTodRequest uint16 = 0x8000
|
||||
OpTodData uint16 = 0x8100
|
||||
OpTodControl uint16 = 0x8200
|
||||
OpRdm uint16 = 0x8300
|
||||
OpRdm uint16 = 0x8300
|
||||
|
||||
PortTypeOutput uint8 = 0x80
|
||||
PortTypeInput uint8 = 0x40
|
||||
@@ -29,6 +30,8 @@ const (
|
||||
GoodOutputDataTransmitted uint8 = 0x80
|
||||
GoodInputDataReceived uint8 = 0x80
|
||||
|
||||
TodCommandFull uint8 = 0x00
|
||||
|
||||
StyleNode uint8 = 0x00
|
||||
StyleController uint8 = 0x01
|
||||
StyleMedia uint8 = 0x02
|
||||
@@ -148,6 +151,33 @@ func (p *PollReplyPacket) OutputUniverses() []Universe {
|
||||
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) {
|
||||
if len(data) < 10 {
|
||||
return 0, nil, ErrPacketTooShort
|
||||
@@ -169,6 +199,9 @@ func ParsePacket(data []byte) (uint16, interface{}, error) {
|
||||
case OpPollReply:
|
||||
pkt, err := parsePollReplyPacket(data)
|
||||
return opCode, pkt, err
|
||||
case OpTodData:
|
||||
pkt, err := parseTodDataPacket(data)
|
||||
return opCode, pkt, err
|
||||
default:
|
||||
return opCode, nil, nil
|
||||
}
|
||||
@@ -279,6 +312,53 @@ func BuildPollPacket() []byte {
|
||||
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 {
|
||||
buf := make([]byte, 240)
|
||||
copy(buf[0:8], ID[:])
|
||||
|
||||
10
receiver.go
10
receiver.go
@@ -13,6 +13,10 @@ type Handler interface {
|
||||
HandlePollReply(src *net.UDPAddr, pkt *PollReplyPacket)
|
||||
}
|
||||
|
||||
type TodDataHandler interface {
|
||||
HandleTodData(src *net.UDPAddr, pkt *TodDataPacket)
|
||||
}
|
||||
|
||||
type Receiver struct {
|
||||
conn *net.UDPConn
|
||||
handler Handler
|
||||
@@ -128,5 +132,11 @@ func (r *Receiver) handle(src *net.UDPAddr, data []byte) {
|
||||
if reply, ok := pkt.(*PollReplyPacket); ok {
|
||||
r.handler.HandlePollReply(src, reply)
|
||||
}
|
||||
case OpTodData:
|
||||
if todHandler, ok := r.handler.(TodDataHandler); ok {
|
||||
if tod, ok := pkt.(*TodDataPacket); ok {
|
||||
todHandler.HandleTodData(src, tod)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +81,12 @@ func (s *Sender) SendPollReply(addr *net.UDPAddr, localIP [4]byte, localMAC [6]b
|
||||
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 {
|
||||
_, err := s.conn.WriteToUDP(data, addr)
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user