diff --git a/circuitpython_nrf24l01/network/constants.py b/circuitpython_nrf24l01/network/constants.py index 524434d..ed30e05 100644 --- a/circuitpython_nrf24l01/network/constants.py +++ b/circuitpython_nrf24l01/network/constants.py @@ -29,6 +29,11 @@ NETWORK_DEFAULT_ADDR = const(0o4444) #: Primarily used by RF24Mesh. MAX_FRAG_SIZE = const(24) #: Maximum message size for a single frame's message. NETWORK_MULTICAST_ADDR = const(0o100) #: A reserved address for multicast messages. +#: A reserved address for multicast messages to level 2 +NETWORK_MULTICAST_ADDR_LVL_2 = const(0o10) +#: A reserved address for multicast messages to level 4 +NETWORK_MULTICAST_ADDR_LVL_4 = const(0o1000) + MESH_LOOKUP_TIMEOUT = const(135) #: Used for `lookup_address()` & `lookup_node_id()` MESH_MAX_POLL = const(4) #: The max number of contacts made during `renew_address()`. MESH_MAX_CHILDREN = const(4) #: The max number of children for 1 mesh node. diff --git a/circuitpython_nrf24l01/network/mixins.py b/circuitpython_nrf24l01/network/mixins.py index 65564e6..9e8d7f5 100644 --- a/circuitpython_nrf24l01/network/mixins.py +++ b/circuitpython_nrf24l01/network/mixins.py @@ -551,9 +551,8 @@ def _write_to_pipe(self, to_node: int, to_pipe: int, is_multicast: bool) -> bool result = self._tx_standby(self.tx_timeout) else: # break message into fragments and send the multiple resulting frames - total = bool(len(self.frame_buf.message) % MAX_FRAG_SIZE) + int( - len(self.frame_buf.message) / MAX_FRAG_SIZE - ) + msg_len = len(self.frame_buf.message) + total = bool(msg_len % MAX_FRAG_SIZE) + int(msg_len / MAX_FRAG_SIZE) msg_t = self.frame_buf.header.message_type for count in range(total): buf_start = count * MAX_FRAG_SIZE diff --git a/circuitpython_nrf24l01/network/structs.py b/circuitpython_nrf24l01/network/structs.py index 1fe1393..5b782ad 100644 --- a/circuitpython_nrf24l01/network/structs.py +++ b/circuitpython_nrf24l01/network/structs.py @@ -31,6 +31,8 @@ from .constants import ( NETWORK_EXT_DATA, NETWORK_MULTICAST_ADDR, + NETWORK_MULTICAST_ADDR_LVL_2, + NETWORK_MULTICAST_ADDR_LVL_4, MSG_FRAG_FIRST, MSG_FRAG_MORE, MSG_FRAG_LAST, @@ -41,7 +43,11 @@ def is_address_valid(address: Optional[int]) -> bool: """Test if a given address is a valid :ref:`Logical Address `.""" if address is None: return False - if address == NETWORK_MULTICAST_ADDR: + if address in ( + NETWORK_MULTICAST_ADDR, + NETWORK_MULTICAST_ADDR_LVL_2, + NETWORK_MULTICAST_ADDR_LVL_4, + ): return True byte_count = 0 while address: diff --git a/circuitpython_nrf24l01/rf24.py b/circuitpython_nrf24l01/rf24.py index 748371f..cdd7f00 100644 --- a/circuitpython_nrf24l01/rf24.py +++ b/circuitpython_nrf24l01/rf24.py @@ -319,8 +319,8 @@ def send( self._ce_pin.value = False if isinstance(buf, (list, tuple)): result = [] - for byte in buf: - result.append(self.send(byte, ask_no_ack, force_retry, send_only)) + for b_array in buf: + result.append(self.send(b_array, ask_no_ack, force_retry, send_only)) return result # type: ignore[return-value] if self._in[0] & 0x10 or self._in[0] & 1: self.flush_tx() diff --git a/circuitpython_nrf24l01/rf24_mesh.py b/circuitpython_nrf24l01/rf24_mesh.py index af44a93..0f7c0b3 100644 --- a/circuitpython_nrf24l01/rf24_mesh.py +++ b/circuitpython_nrf24l01/rf24_mesh.py @@ -29,7 +29,7 @@ except ImportError: pass # some CircuitPython boards don't have the json module try: - from typing import Union, Dict, List, Optional, Callable, Any + from typing import Union, Dict, Set, Optional, Callable, Any except ImportError: pass import busio # type:ignore[import] @@ -40,6 +40,7 @@ NETWORK_DEFAULT_ADDR, NETWORK_MULTICAST_ADDR, NETWORK_POLL, + NETWORK_PING, MESH_ADDR_RELEASE, MESH_ADDR_LOOKUP, MESH_ID_LOOKUP, @@ -162,13 +163,22 @@ def _lookup_2_master(self, number: int, lookup_type: int) -> int: return struct.unpack(" bool: + def check_connection(self, attempts: int = 3, ping_master: bool = False) -> bool: """Check for network connectivity (not for use on master node).""" - # do a double check as a manual retry in lack of using auto-ack - if self.lookup_address(self._id) < 1: - if self.lookup_address(self._id) < 1: - return False - return True + if not self._id: + return True + if self._addr == NETWORK_DEFAULT_ADDR: + return False + for _ in range(attempts): + if ping_master: + result = self.lookup_address(self._id) + if result == -2: + return False + if result == self._addr: + return True + elif self.write(self._parent, NETWORK_PING, b""): + return True + return False def update(self) -> int: """Checks for incoming network data and returns last message type (if any)""" @@ -214,20 +224,21 @@ def _get_level(address: int) -> int: break if callable(self.block_less_callback): self.block_less_callback() - if new_addr is None: - return False - super()._begin(new_addr) - # print("new address assigned:", oct(new_addr)) - # do a double check as a manual retry in lack of using auto-ack - if self.lookup_node_id(self._addr) != self._id: + if new_addr is None: + continue + super()._begin(new_addr) + # print("new address assigned:", oct(new_addr)) + # do a double check as a manual retry in lack of using auto-ack if self.lookup_node_id(self._addr) != self._id: - super()._begin(NETWORK_DEFAULT_ADDR) - return False - return True + if self.lookup_node_id(self._addr) != self._id: + super()._begin(NETWORK_DEFAULT_ADDR) + continue + return True + return False - def _make_contact(self, lvl: int) -> List[int]: - """Make a list of connections after multicasting a `NETWORK_POLL` message.""" - responders: List[int] = [] + def _make_contact(self, lvl: int) -> Set[int]: + """Make a set of connections after multicasting a `NETWORK_POLL` message.""" + responders: Set[int] = set() self.frame_buf.header.to_node = NETWORK_MULTICAST_ADDR self.frame_buf.header.from_node = NETWORK_DEFAULT_ADDR self.frame_buf.header.message_type = NETWORK_POLL @@ -237,13 +248,7 @@ def _make_contact(self, lvl: int) -> List[int]: timeout = 55000000 + time.monotonic_ns() while time.monotonic_ns() < timeout and len(responders) < MESH_MAX_POLL: if self._net_update() == NETWORK_POLL: - contacted = self.frame_buf.header.from_node - is_duplicate = False - for contact in responders: - if contacted == contact: - is_duplicate = True - if not is_duplicate: - responders.append(contacted) + responders.add(self.frame_buf.header.from_node) return responders @property @@ -316,6 +321,16 @@ def __init__( #: A `dict` that enables master nodes to act as a DNS. self.dhcp_dict: Dict[int, int] = {} + def renew_address(self, timeout: Union[float, int] = 7.5): + if not self._id: + return 0 + return super().renew_address(timeout) + + def check_connection(self, attempts: int = 3, ping_master: bool = False) -> bool: + if not self._id: + return True + return super().check_connection(attempts, ping_master) + def update(self) -> int: """Checks for incoming network data and returns last message type (if any)""" msg_t = super().update() @@ -336,10 +351,7 @@ def update(self) -> int: self.frame_buf.message = bytes([ret_val]) self._write(self.frame_buf.header.to_node, TX_NORMAL) elif msg_t == MESH_ADDR_RELEASE: - for n_id, addr in self.dhcp_dict.items(): - if addr == self.frame_buf.header.from_node: - del self.dhcp_dict[n_id] - break + self.release_address(self.frame_buf.header.from_node) self._dhcp() return msg_t @@ -438,6 +450,22 @@ def print_details(self, dump_pipes: bool = False, network_only: bool = False): if dump_pipes: self._rf24.print_pipes() + def release_address(self, address: int = 0) -> bool: + """Release an assigned address from any corresponding mesh node's ID. + + .. important:: + This function is meant for use on master nodes. If the ``address`` + parameter is not specified, then + `RF24MeshNoMaster.release_address()` is called. + """ + if not address: + return super().release_address() + for id, addr in self.dhcp_dict.items(): + if addr == address: + del self.dhcp_dict[id] + return True + return False + def lookup_address(self, node_id: Optional[int] = None) -> int: """Convert a node's unique ID number into its corresponding :ref:`Logical Address `.""" diff --git a/docs/network_docs/mesh_api.rst b/docs/network_docs/mesh_api.rst index b1f1544..f19a66e 100644 --- a/docs/network_docs/mesh_api.rst +++ b/docs/network_docs/mesh_api.rst @@ -31,6 +31,15 @@ RF24MeshNoMaster class .. seealso:: For all parameters' descriptions, see the :py:class:`~circuitpython_nrf24l01.rf24.RF24` class' constructor documentation. +.. automethod:: circuitpython_nrf24l01.rf24_mesh.RF24MeshNoMaster.release_address + + .. hint:: + This should be called from a mesh network node that is disconnecting from the network. + This is also recommended for mesh network nodes that are entering a powered down (or + sleep) mode. + + :returns: `True` if the address was released, otherwise `False`. + RF24Mesh class ************** @@ -168,12 +177,41 @@ Advanced API .. automethod:: circuitpython_nrf24l01.rf24_mesh.RF24Mesh.check_connection + :param attempts: The number of attempts to test for active connection to the mesh network. + :param ping_master: If this parameter is set to `True`, then this function will verify the + connectivity by using `lookup_address()` to transact with the master node. Setting this + parameter to `False` will simply ping the node's parent. + + .. warning:: + Setting this parameter to `True` can result in performance cost when used in + a large mesh network. The disadvantages in such a situation are: + + - increased load on master node + - increased network congestion + - unreliable connectivity information when a parent or grandparent of the current + node briefly loses connection. + + :returns: + - `True` if connected to the mesh network (or current node is the master node). + - `False` if not connected to the mesh network or mesh network is unresponsive. + + .. versionchanged:: 2.2.0 + Added ``attempts`` and ``ping_master`` parameters; changed return value for master nodes + + Previously, this function would return `False` when called from a master node. + This was changed to return `True` to help avoid erroneous user code calling + `renew_address()` on a master node. + .. automethod:: circuitpython_nrf24l01.rf24_mesh.RF24Mesh.release_address - .. hint:: - This should be called from a mesh network node that is disconnecting from the network. - This is also recommended for mesh network nodes that are entering a powered down (or - sleep) mode. + :param address: The address to release. + :returns: `True` if the address was released, otherwise `False`. + + .. versionadded:: 2.2.0 + Allows master nodes to forcibly release an assigned address. + + This function is essentially an overload of `RF24MeshNoMaster.release_address()` + specifically designed for use on a master node. .. autoproperty:: circuitpython_nrf24l01.rf24_mesh.RF24Mesh.allow_children diff --git a/examples/nrf24l01_network_test.py b/examples/nrf24l01_network_test.py index 6a4ddc7..eeb8c8b 100644 --- a/examples/nrf24l01_network_test.py +++ b/examples/nrf24l01_network_test.py @@ -72,7 +72,7 @@ CSN_PIN = DigitalInOut(board.D5) -# initialize this node as the network +# initialize this node as part of the network nrf = Network(SPI_BUS, CSN_PIN, CE_PIN, THIS_NODE) # TMRh20 examples use channel 97 for RF24Mesh library @@ -87,15 +87,22 @@ # list to store our number of the payloads sent packets_sent = [0] + +def connect_mesh_node(timeout: float = 7.5) -> bool: + """For RF24Mesh child nodes only! This function attempts to (re)connect to a mesh + network. This is not needed for RF24Mesh master nodes. + + :param timeout: The time in seconds spent trying to connect. See renew_address(). + """ + print("Connecting to mesh network...", end=" ") + result = nrf.renew_address(timeout) is not None + print(("assigned address " + oct(nrf.node_address)) if result else "failed.") + return result + + if THIS_NODE: # if this node is not the network master node if IS_MESH: # mesh nodes need to bond with the master node - print("Connecting to mesh network...", end=" ") - - # get this node's assigned address and connect to network - if nrf.renew_address() is None: - print("failed. Please try again manually with `nrf.renew_address()`") - else: - print("assigned address:", oct(nrf.node_address)) + connect_mesh_node() else: print("Acting as network master node.") @@ -152,17 +159,28 @@ def emit( # TMRh20's RF24Network examples use 2 long ints, so add another message += struct.pack(" 1' to emit fragmented messages.\n" ) if IS_MESH and THIS_NODE: - if nrf.node_address == NETWORK_DEFAULT_ADDR: + if nrf.check_connection(): prompt += "!!! Mesh node not connected.\n" prompt += "*** Enter 'C' to connect to to mesh master node.\n" user_input = (input(prompt + "*** Enter 'Q' to quit example.\n") or "?").split() if user_input[0].upper().startswith("C"): - print("Connecting to mesh network...", end=" ") - result = nrf.renew_address(*[int(x) for x in user_input[1:2]]) is not None - print(("assigned address " + oct(nrf.node_address)) if result else "failed.") + if IS_MESH and THIS_NODE: + connect_mesh_node(*[int(x) for x in user_input[1:2]]) return True if user_input[0].upper().startswith("I"): idle(*[int(x) for x in user_input[1:3]]) diff --git a/examples/nrf24l01_scanner_test.py b/examples/nrf24l01_scanner_test.py index d1fad0d..07bc06f 100644 --- a/examples/nrf24l01_scanner_test.py +++ b/examples/nrf24l01_scanner_test.py @@ -40,16 +40,20 @@ # 21 = bus 2, CE1 # enable SPI bus 1 prior to running this # turn off RX features specific to the nRF24L01 module -nrf.auto_ack = False nrf.dynamic_payloads = False +nrf.auto_ack = False nrf.crc = 0 nrf.arc = 0 nrf.allow_ask_no_ack = False # use reverse engineering tactics for a better "snapshot" nrf.address_length = 2 -nrf.open_rx_pipe(1, b"\0\x55") -nrf.open_rx_pipe(0, b"\0\xaa") +nrf.open_rx_pipe(0, b"\xaa\xaa") +nrf.open_rx_pipe(1, b"\x55\x55") +nrf.open_rx_pipe(2, b"\0\xaa") +nrf.open_rx_pipe(3, b"\0\x55") +nrf.open_rx_pipe(4, b"\xa0\xaa") +nrf.open_rx_pipe(5, b"\x50\x55") def scan(timeout=30): @@ -67,32 +71,49 @@ def scan(timeout=30): print(str(i % 10), sep="", end="") print("\n" + "~" * 126) + sweeps = 0 signals = [0] * 126 # store the signal count for each channel curr_channel = 0 start_timer = time.monotonic() # start the timer while time.monotonic() - start_timer < timeout: nrf.channel = curr_channel - if nrf.available(): - nrf.flush_rx() # flush the RX FIFO because it asserts the RPD flag - nrf.listen = 1 # start a RX session + # nrf.flush_rx() + nrf.listen = True # start a RX session time.sleep(0.00013) # wait 130 microseconds - signals[curr_channel] += nrf.rpd # if interference is present - nrf.listen = 0 # end the RX session - curr_channel = curr_channel + 1 if curr_channel < 125 else 0 + found_signal = nrf.rpd + nrf.listen = False # end the RX session + found_signal = found_signal or nrf.rpd or nrf.available() + + # count signal as interference + signals[curr_channel] += found_signal + # clear the RX FIFO if a signal was detected/captured + if found_signal: + nrf.flush_rx() # flush the RX FIFO because it asserts the RPD flag + endl = False + if curr_channel > 124: + sweeps += 1 + if sweeps >= 15: + endl = True + sweeps = 0 # output the signal counts per channel sig_cnt = signals[curr_channel] print( - ("%X" % min(15, sig_cnt)) if sig_cnt else "-", + ("%X" % sig_cnt) if sig_cnt else "-", sep="", - end="" if curr_channel < 125 else "\r", + end="" if curr_channel < 125 else ("\n" if endl else "\r"), ) + curr_channel = curr_channel + 1 if curr_channel < 125 else 0 + if endl: + signals = [0] * 126 + # finish printing results and end with a new line while curr_channel < len(signals) - 1: curr_channel += 1 sig_cnt = signals[curr_channel] - print(("%X" % min(15, sig_cnt)) if sig_cnt else "-", sep="", end="") + print(("%X" % sig_cnt) if sig_cnt else "-", sep="", end="") print("") + nrf.flush_rx() # flush the RX FIFO for continued operation def noise(timeout=1, channel=None): @@ -107,13 +128,12 @@ def noise(timeout=1, channel=None): nrf.listen = True timeout += time.monotonic() while time.monotonic() < timeout: - signal = nrf.read() - if signal: - print(address_repr(signal, False, " ")) + if nrf.available(): + print(address_repr(nrf.read(32), False, " ")) nrf.listen = False while not nrf.fifo(False, True): # dump the left overs in the RX FIFO - print(address_repr(nrf.read(), False, " ")) + print(address_repr(nrf.read(32), False, " ")) def set_role(): diff --git a/requirements.txt b/requirements.txt index c90d28e..684830d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ Adafruit-Blinka adafruit-circuitpython-busdevice -spidev; sys_platform == 'linux' and platform_machine == 'armv7l' or platform_machine == 'armv6l' -typing-extensions +spidev; sys_platform == 'linux' and ( platform_machine == 'armv7l' or platform_machine == 'armv6l' or platform_machine == 'aarch64' ) +typing-extensions