2525 DatagramAddr ,
2626)
2727from .network import AF_PACKET , getifaddrs
28+ from .rdm import RDMDevice
2829
2930# Art-Net implementation for Python asyncio
3031# Any page references to 'spec' refer to
@@ -98,6 +99,8 @@ def __init__(self, client: "ArtNetClient"):
9899 0x2000 : self .on_art_poll ,
99100 0x2100 : self .on_art_poll_reply ,
100101 0x5000 : self .on_art_dmx ,
102+ 0x8000 : self .on_art_tod_request ,
103+ 0x8100 : self .on_art_tod_data ,
101104 }
102105 client .protocol = self
103106 self .node_report_counter = 0
@@ -115,7 +118,7 @@ def datagram_received(self, data: bytes, addr: DatagramAddr) -> None:
115118 if h :
116119 h (addr , data [10 :])
117120 else :
118- logger .debug (
121+ logger .info (
119122 f"Received unsupported Art-Net: op { hex (opcode )} from { addr } : { data [10 :]!r} "
120123 )
121124 else :
@@ -196,7 +199,9 @@ def on_art_poll_reply(self, addr: DatagramAddr, data: bytes) -> None:
196199
197200 # iterate through the ports and create ports and universes
198201 portList = []
199- for _type , _in , _out , _swin , _swout in zip (ptype , ins , outs , swin , swout ):
202+ for _type , _in , _out , _swin , _swout , _goodout in zip (
203+ ptype , ins , outs , swin , swout , goodout
204+ ):
200205 in_port_addr = (
201206 ((netsw & 0x7F ) << 8 ) + ((subsw & 0x0F ) << 4 ) + (_swin & 0x0F )
202207 )
@@ -206,11 +211,13 @@ def on_art_poll_reply(self, addr: DatagramAddr, data: bytes) -> None:
206211 if _type & 0b10000000 :
207212 outu = self .client ._get_create_universe (out_port_addr )
208213 portList .append (
209- ArtNetPort (nn , False , _type & 0x1F , out_port_addr , outu )
214+ ArtNetPort (nn , False , _type & 0x1F , out_port_addr , outu , _goodout )
210215 )
211216 if _type & 0b01000000 :
212217 inu = self .client ._get_create_universe (in_port_addr )
213- portList .append (ArtNetPort (nn , True , _type & 0x1F , in_port_addr , inu ))
218+ portList .append (
219+ ArtNetPort (nn , True , _type & 0x1F , in_port_addr , inu , 0 )
220+ )
214221
215222 # track which 'pages' of port bindings we have seen
216223 old_ports = nn ._portBinds [bindindex ]
@@ -284,6 +291,44 @@ def on_art_dmx(self, addr: DatagramAddr, data: bytes) -> None:
284291 if u in self .client ._subscribing :
285292 self .client ._dispatch_event (UniverseDMX (u , channel_data ))
286293
294+ def on_art_tod_request (self , addr : DatagramAddr , data : bytes ) -> None :
295+ (ver ,) = struct .unpack ("<H" , data [0 :2 ])
296+ net = data [11 ]
297+ cmd = data [12 ]
298+ address_count = data [13 ]
299+ # net+address => universe address
300+ for universe in [int (x ) + net << 8 for x in data [14 : 14 + address_count ]]:
301+ logger .debug (
302+ f"Received Art-Net TOD request: ver { ver } cmd { cmd } universe { universe } from { addr } "
303+ )
304+ # TODO: if we are an output, answer this with our cached tod table by sending
305+ # art_tod_data packets
306+
307+ def on_art_tod_data (self , addr : DatagramAddr , data : bytes ) -> None :
308+ ver , rdm_ver , port = struct .unpack ("<HBB" , data [0 :4 ])
309+ # 6 bytes spare
310+ bind_index , net , response , address , tot_uid , block_count , uid_count = (
311+ struct .unpack ("<BBBBHBB" , data [10 :18 ])
312+ )
313+ portaddress = net << 8 + address
314+ TOD_FULL = 0
315+
316+ if response != TOD_FULL :
317+ logger .info (
318+ "Got unexpected art_tod_data with response {response} from {addr}"
319+ )
320+ return
321+
322+ u = self .client ._get_create_universe (portaddress )
323+
324+ # I think tot_uid and block_count are *paging* related if more tod UIDs than can fit in a packet
325+ for i in range (uid_count ):
326+ uid = data [18 + i * 6 : 18 + (i + 1 ) * 6 ]
327+ u ._tod [uid ] = RDMDevice (uid )
328+ logger .info (
329+ f"Received Art-Net TOD entry: universe { portaddress } uid={ uid .hex ()} from { addr } "
330+ )
331+
287332 async def art_poll_task (self ) -> None :
288333 while True :
289334 await asyncio .sleep (0.1 )
@@ -292,6 +337,8 @@ async def art_poll_task(self) -> None:
292337 for u in self .client ._publishing :
293338 if t > u ._last_publish + 1.0 :
294339 self ._send_art_dmx (u )
340+ if t > u ._last_tod_request + 5.0 :
341+ self ._send_art_tod_request (u )
295342
296343 if t > self ._last_poll + 2.0 :
297344 self ._send_art_poll ()
@@ -422,6 +469,37 @@ def _send_art_dmx_subscriber(
422469 if self .transport :
423470 self .transport .sendto (message , addr = (node .ip , node .udpport ))
424471
472+ def _send_art_tod_request (self , universe : ArtNetUniverse ) -> None :
473+ logger .debug (f"sending art tod request for { universe } " )
474+ universe ._last_tod_request = time .time ()
475+
476+ subuni = universe .portaddress & 0xFF
477+ net = universe .portaddress >> 8
478+ message = ARTNET_PREFIX + struct .pack (
479+ "<HBBBBBBBBBBBBBBB" ,
480+ 0x8000 ,
481+ 0 ,
482+ 14 ,
483+ 0 ,
484+ 0 ,
485+ 0 ,
486+ 0 ,
487+ 0 ,
488+ 0 ,
489+ 0 ,
490+ 0 ,
491+ 0 ,
492+ net ,
493+ 0 , # command TodFull
494+ 1 ,
495+ subuni ,
496+ )
497+
498+ if self .transport :
499+ # send direct to node?
500+ # self.transport.sendto(message, addr=(node.ip, node.udpport))
501+ self .transport .sendto (message , addr = (self .client .broadcast_ip , ARTNET_PORT ))
502+
425503 def error_received (self , exc : Exception ) -> None :
426504 logger .warn ("Error received:" , exc )
427505
@@ -540,6 +618,7 @@ def set_port_config(
540618 universe : UniverseKey ,
541619 is_input : bool = False ,
542620 is_output : bool = False ,
621+ rdm : bool = False ,
543622 ) -> ArtNetUniverse :
544623 port_addr = self ._parse_universe (universe )
545624
@@ -560,7 +639,12 @@ def set_port_config(
560639
561640 if is_input or is_output :
562641 port = ArtNetPort (
563- node = None , is_input = is_input , media = 0 , portaddr = port_addr , universe = u
642+ node = None ,
643+ is_input = is_input ,
644+ media = 0 ,
645+ portaddr = port_addr ,
646+ universe = u ,
647+ flags = 0 ,
564648 )
565649 self .ports .append (port )
566650 logger .debug (f"configured own port { port } " )
0 commit comments