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
|
LongName string
|
||||||
Inputs []Universe
|
Inputs []Universe
|
||||||
Outputs []Universe
|
Outputs []Universe
|
||||||
|
RDMUIDs map[Universe][]RDMUID
|
||||||
LastSeen time.Time
|
LastSeen time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,8 +129,9 @@ func (d *Discovery) HandlePollReply(src *net.UDPAddr, pkt *PollReplyPacket) {
|
|||||||
node, exists := d.nodes[ip]
|
node, exists := d.nodes[ip]
|
||||||
if !exists {
|
if !exists {
|
||||||
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) {
|
||||||
|
|||||||
96
protocol.go
96
protocol.go
@@ -13,15 +13,16 @@ const (
|
|||||||
Port = 6454
|
Port = 6454
|
||||||
ProtocolVersion = 14
|
ProtocolVersion = 14
|
||||||
|
|
||||||
OpPoll uint16 = 0x2000
|
OpPoll uint16 = 0x2000
|
||||||
OpPollReply uint16 = 0x2100
|
OpPollReply uint16 = 0x2100
|
||||||
OpDmx uint16 = 0x5000
|
OpDmx uint16 = 0x5000
|
||||||
OpSync uint16 = 0x5200
|
OpSync uint16 = 0x5200
|
||||||
OpAddress uint16 = 0x6000
|
OpAddress uint16 = 0x6000
|
||||||
OpInput uint16 = 0x7000
|
OpInput uint16 = 0x7000
|
||||||
OpTodData uint16 = 0x8100
|
OpTodRequest uint16 = 0x8000
|
||||||
|
OpTodData uint16 = 0x8100
|
||||||
OpTodControl uint16 = 0x8200
|
OpTodControl uint16 = 0x8200
|
||||||
OpRdm uint16 = 0x8300
|
OpRdm uint16 = 0x8300
|
||||||
|
|
||||||
PortTypeOutput uint8 = 0x80
|
PortTypeOutput uint8 = 0x80
|
||||||
PortTypeInput uint8 = 0x40
|
PortTypeInput uint8 = 0x40
|
||||||
@@ -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[:])
|
||||||
|
|||||||
10
receiver.go
10
receiver.go
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user