Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Network improvements #59

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from
5 changes: 5 additions & 0 deletions circuitpython_nrf24l01/network/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 2 additions & 3 deletions circuitpython_nrf24l01/network/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion circuitpython_nrf24l01/network/structs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -41,7 +43,11 @@ def is_address_valid(address: Optional[int]) -> bool:
"""Test if a given address is a valid :ref:`Logical Address <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:
Expand Down
4 changes: 2 additions & 2 deletions circuitpython_nrf24l01/rf24.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,8 +319,8 @@
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))

Check warning on line 323 in circuitpython_nrf24l01/rf24.py

View check run for this annotation

Codecov / codecov/patch

circuitpython_nrf24l01/rf24.py#L322-L323

Added lines #L322 - L323 were not covered by tests
return result # type: ignore[return-value]
if self._in[0] & 0x10 or self._in[0] & 1:
self.flush_tx()
Expand Down
88 changes: 58 additions & 30 deletions circuitpython_nrf24l01/rf24_mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -40,6 +40,7 @@
NETWORK_DEFAULT_ADDR,
NETWORK_MULTICAST_ADDR,
NETWORK_POLL,
NETWORK_PING,
MESH_ADDR_RELEASE,
MESH_ADDR_LOOKUP,
MESH_ID_LOOKUP,
Expand Down Expand Up @@ -162,13 +163,22 @@
return struct.unpack("<H", self.frame_buf.message[:2])[0]
return self.frame_buf.message[0]

def check_connection(self) -> 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

Check warning on line 181 in circuitpython_nrf24l01/rf24_mesh.py

View check run for this annotation

Codecov / codecov/patch

circuitpython_nrf24l01/rf24_mesh.py#L168-L181

Added lines #L168 - L181 were not covered by tests

def update(self) -> int:
"""Checks for incoming network data and returns last message type (if any)"""
Expand Down Expand Up @@ -214,20 +224,21 @@
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)

Check warning on line 229 in circuitpython_nrf24l01/rf24_mesh.py

View check run for this annotation

Codecov / codecov/patch

circuitpython_nrf24l01/rf24_mesh.py#L227-L229

Added lines #L227 - L229 were not covered by tests
# 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

Check warning on line 237 in circuitpython_nrf24l01/rf24_mesh.py

View check run for this annotation

Codecov / codecov/patch

circuitpython_nrf24l01/rf24_mesh.py#L233-L237

Added lines #L233 - L237 were not covered by tests

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()

Check warning on line 241 in circuitpython_nrf24l01/rf24_mesh.py

View check run for this annotation

Codecov / codecov/patch

circuitpython_nrf24l01/rf24_mesh.py#L241

Added line #L241 was not covered by tests
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
Expand All @@ -237,13 +248,7 @@
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)

Check warning on line 251 in circuitpython_nrf24l01/rf24_mesh.py

View check run for this annotation

Codecov / codecov/patch

circuitpython_nrf24l01/rf24_mesh.py#L251

Added line #L251 was not covered by tests
return responders

@property
Expand Down Expand Up @@ -316,6 +321,16 @@
#: 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)

Check warning on line 327 in circuitpython_nrf24l01/rf24_mesh.py

View check run for this annotation

Codecov / codecov/patch

circuitpython_nrf24l01/rf24_mesh.py#L325-L327

Added lines #L325 - L327 were not covered by tests

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)

Check warning on line 332 in circuitpython_nrf24l01/rf24_mesh.py

View check run for this annotation

Codecov / codecov/patch

circuitpython_nrf24l01/rf24_mesh.py#L330-L332

Added lines #L330 - L332 were not covered by tests

def update(self) -> int:
"""Checks for incoming network data and returns last message type (if any)"""
msg_t = super().update()
Expand All @@ -336,10 +351,7 @@
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)

Check warning on line 354 in circuitpython_nrf24l01/rf24_mesh.py

View check run for this annotation

Codecov / codecov/patch

circuitpython_nrf24l01/rf24_mesh.py#L354

Added line #L354 was not covered by tests
self._dhcp()
return msg_t

Expand Down Expand Up @@ -438,6 +450,22 @@
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

Check warning on line 467 in circuitpython_nrf24l01/rf24_mesh.py

View check run for this annotation

Codecov / codecov/patch

circuitpython_nrf24l01/rf24_mesh.py#L463-L467

Added lines #L463 - L467 were not covered by tests

def lookup_address(self, node_id: Optional[int] = None) -> int:
"""Convert a node's unique ID number into its corresponding
:ref:`Logical Address <Logical Address>`."""
Expand Down
46 changes: 42 additions & 4 deletions docs/network_docs/mesh_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
**************
Expand Down Expand Up @@ -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

Expand Down
51 changes: 34 additions & 17 deletions examples/nrf24l01_network_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.")

Expand Down Expand Up @@ -152,17 +159,28 @@ def emit(
# TMRh20's RF24Network examples use 2 long ints, so add another
message += struct.pack("<L", packets_sent[0])
result = False
print("Sending {} (len {})...".format(packets_sent[0], len(message)), end=" ")
start = time.monotonic_ns()
if IS_MESH: # send() is a little different for RF24Mesh vs RF24Network
result = nrf.send(node, "M", message)
else:
result = nrf.send(RF24NetworkHeader(node, "T"), message)
end = time.monotonic_ns()
print(
"Sending {} (len {})...".format(packets_sent[0], len(message)),
"ok." if result else "failed.",
"Transmission took {} ms".format(int((end - start) / 1000000)),
)
if result:
print("ok. Transmission took {} ms".format(int((end - start) / 1000000)))
else:
print("failed.")
# RF24Network is a network of static nodes. Its connectivity is determined
# by a node's ability to successfully transmit to other nodes.
if IS_MESH:
# RF24Mesh is more complex in terms of connectivity. We have special API
# to test and establish mesh network connectivity.
print("Testing connection to mesh network...")
if nrf.check_connection():
print("Connected to mesh network.")
elif not connect_mesh_node():
print("Aborting emit()")
return


def set_role():
Expand All @@ -175,14 +193,13 @@ def set_role():
"*** Enter 'E <node number> 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]])
Expand Down
Loading