2828import asyncio
2929from ipaddress import ip_address , IPv4Address
3030from concurrent import futures
31- from socket import AF_INET , gaierror
31+ from socket import AF_INET , SOCK_DGRAM , IPPROTO_IP , IP_MULTICAST_TTL , \
32+ SOL_SOCKET , SO_BROADCAST
33+ from socket import socket , timeout as sotimeout , gaierror
34+ from struct import pack
3235# Internal
3336from .base import BOFNetworkError , BOFProgrammingError , log
3437
38+ ###############################################################################
39+ # Global network-related constants and functions #
40+ ###############################################################################
41+
42+ def IS_IP (ip : str ):
43+ """Check that ip is a valid IPv4 address."""
44+ try :
45+ ip_address (ip )
46+ except ValueError :
47+ raise BOFProgrammingError ("Invalid IP {0}" .format (ip )) from None
48+
3549###############################################################################
3650# Asyncio classes for UDP and TCP #
3751###############################################################################
@@ -182,7 +196,7 @@ def _handle_exception(self, loop:object, context) -> None:
182196 .. seealso:: bof.base.BOFNetworkError"""
183197 message = context if isinstance (context , str ) else context .get ("exception" , context ["message" ])
184198 log ("Exception occurred: {0}" .format (message ), "ERROR" )
185- self .disconnect ()
199+ # self.disconnect()
186200 raise BOFNetworkError (message ) from None
187201
188202 def _receive (self , data :bytes , address :tuple ) -> None :
@@ -196,6 +210,28 @@ def _receive(self, data:bytes, address:tuple) -> None:
196210 log ("Queue is full" , "ERROR" )
197211 raise BOFNetworkError ("Queue is full" )
198212
213+ def _argument_check (data :bytes , address :tuple ) -> None :
214+ """Check that parameters to send ``data`` to an ``address`` are valid.
215+ If so, they are changed to appropriate format for sockets.
216+
217+ :param data: Raw byte array or string to send.
218+ :param address: Remote network address with format tuple ``(ip, port)``.
219+ :returns: data, address
220+ :raises BOFNetworkError: If either parameter is invalid.
221+ """
222+ try :
223+ if isinstance (data , str ):
224+ data = data .encode ('utf-8' )
225+ else :
226+ data = bytes (data )
227+ except TypeError :
228+ raise BOFProgrammingError ("Invalid data type (must be bytes)." ) from None
229+ try :
230+ address = str (ip_address (address [0 ])), address [1 ]
231+ except (ValueError , TypeError ):
232+ raise BOFProgrammingError ("Invalid address {0}" .format (address )) from None
233+ return data , address
234+
199235 #-------------------------------------------------------------------------#
200236 # Private #
201237 #-------------------------------------------------------------------------#
@@ -215,6 +251,13 @@ async def __listen_once(self, timeout:float=1.0) -> (bytes, tuple):
215251 # Properties #
216252 #-------------------------------------------------------------------------#
217253
254+ @property
255+ def is_connected (self ):
256+ """Returns true if a connection has been established.
257+ Relies on the values of _socket and _transport to find out.
258+ """
259+ return True if self ._socket and self ._transport else False
260+
218261 @property
219262 def transport (self ):
220263 """Get transport object depending on the protocol.
@@ -266,8 +309,81 @@ class UDP(_Transport):
266309 """
267310
268311 #-------------------------------------------------------------------------#
269- # Public #
312+ # Static #
313+ #-------------------------------------------------------------------------#
314+
315+ @staticmethod
316+ def multicast (data :bytes , address :tuple , timeout :float = 1.0 ) -> list :
317+ """Sends a multicast request to specified ip address and port (UDP).
318+
319+ Expects devices subscribed to the address to respond and return
320+ responses as a list of frames with their source. Opens its own socket.
321+
322+ :param data: Raw byte array or string to send.
323+ :param address: Remote network address with format tuple ``(ip, port)``.
324+ :param timeout: Time out value in seconds, as a float (default is 1.0s).
325+ :returns: A list of tuples with format ``(response, (ip, port))``.
326+ :raises BOFNetworkError: If multicast parameters are invalid.
327+
328+ Example::
329+
330+ devices = UDP.multicast(b'\x06 \x10 ...', ('224.0.23.12', 3671))
331+ """
332+ responses = []
333+ ttl = pack ('b' , 1 )
334+ data , address = UDP ._argument_check (data , address )
335+ try :
336+ sock = socket (AF_INET , SOCK_DGRAM )
337+ sock .settimeout (timeout )
338+ sock .setsockopt (IPPROTO_IP , IP_MULTICAST_TTL , ttl )
339+ sock .sendto (data , address )
340+ while True :
341+ response , sender = sock .recvfrom (1024 )
342+ responses .append ((response , sender ))
343+ except OverflowError as exc : # Raised when port invalid
344+ sock .close ()
345+ raise BOFProgrammingError (str (exc ))
346+ except sotimeout as te :
347+ pass
348+ sock .close ()
349+ return responses
350+
351+ @staticmethod
352+ def broadcast (data :bytes , address :tuple , timeout :float = 1.0 ) -> list :
353+ """Broadcasts a request and waits for responses from devices (UDP).
354+
355+ :param data: Raw byte array or string to send.
356+ :param address: Remote network address with format tuple ``(ip, port)``.
357+ :param timeout: Time out value in seconds, as a float (default is 1.0s).
358+ :returns: A list of tuples with format ``(response, (ip, port))``.
359+ :raises BOFNetworkError: If multicast parameters are invalid.
360+
361+ Example::
362+
363+ devices = UDP.broadcast(b'\x06 \x10 ...', ('192.168.1.255', 3671))
364+ """
365+ responses = []
366+ data , address = UDP ._argument_check (data , address )
367+ # Broadcast request
368+ try :
369+ sock = socket (AF_INET , SOCK_DGRAM )
370+ sock .settimeout (timeout )
371+ sock .setsockopt (SOL_SOCKET , SO_BROADCAST , 1 )
372+ sock .sendto (data , address )
373+ while True :
374+ response , sender = sock .recvfrom (1024 )
375+ responses .append ((response , sender ))
376+ except OverflowError as exc : # Raised when port invalid
377+ sock .close ()
378+ raise BOFProgrammingError (str (exc ))
379+ except sotimeout as te :
380+ pass
381+ sock .close ()
382+ return responses
383+
270384 #-------------------------------------------------------------------------#
385+ # Public #
386+ #-------------------------------------------------------------------------#
271387
272388 def connect (self , ip :str , port :int ) -> object :
273389 """Initialize asynchronous connection using UDP on ``ip``:``port``.
0 commit comments