The packager will take blobs of arbitrary but bounded size and package them into packets that fit within the transmission medium specifications. For ESP-NOW, the data size is 250 bytes. For RYLR-998 LoRa module, the data size is 240 bytes. Packet schemas with these two sizes will be included to support at least these two modules. If necessary, additional schemas will be made to support modules with even smaller data size.
The primary encapsulation model is application message blobs within Packages within Packet(s). Additionally, applications will be able to encapsulate each other's Packages, e.g. to add a layer of encryption or use gossip for pub/sub.
|-- Packet/sequence of packets --------| | |-- Package ---------------------| | | | app_id: 16 | | | | half_sha256: 16 | | | | |-- blob: variable --------| | | | | | Application Message | | | | | |--------------------------| | | | |------------------------------- | | |--------------------------------------|
Takes an app ID and a blob, makes a Package, and then transmits it. Unless an interface is specified, the Package will be transmitted on all available network interfaces. If the Package is too large to fit in a single packet for a given interface, it will be broken into a sequence of packets. Unless an interface was specified, it will use a schema that works for all interfaces, which reduces performance of ESP-NOW for the benefit of reduced caching requirements and RYLR-998 compatibility.
Takes an app ID, blob, node ID, and optionally a schema ID. If the node is in the peer list, the Send method puts the blob into a Package and then transmits it to the peer using the interface with the highest bit rate. If the node is not a peer but the node's address is known, the Send method puts the blob into a Package and then attempts to route it. Transmission will attempt to use the specified schema if its ID was supplied, otherwise it will use a schema that can accommodate the Package size and is supported by all interfaces (to prevent resequencing in case of transmission failure via one interface).
Receives a Packet and the interface and MAC it came from, and determines what to do with it. If the Packet is routable, it will attempt to forward the packet.
When a node receives a Package in a single packet, it will check the Application ID and drop the Package if the Application has not registered to accept Packages. If the Application was registered, the Package will be delivered to the Application.
When a node receives a sequence, it will request retransmission of the first
packet in the sequence (packet_id=0) if it did not receive it, but it will not
request retransmission of other missing packets until it receives the first
packet (which contains the Application ID) and verifies that the Package will be
deliverable. If the Package is deliverable, the node will attempt to collect the
whole Package sequence and deliver the Package to the appropriate application.
This will be called by an asynchronous task that monitors network interfaces.
Takes a Package, originating Interface, and originating MAC address, then attempts to deliver the Package blob, Interface, and MAC to the Application. Invoked by the Receive operation, but can also be invoked through the Packager API directly. Directly invoking the Deliver operation may be useful for bootstrapping an Application or for inter-Application communication.
Registers an Application to receive Packages.
Deregestiers an Application so it will not receive Packages.
Asynchronous method that creates a task for each interface to let them process their pending actions, then process pending Packager actions. The specific sequence of operations is as follows:
- Schedule the events in
cls.new_events, putting them incls.schedule. - Remove every event in
cls.canceled_eventsfromcls.schedule. - Process interface actions by gathering asyncio tasks and running them concurrently.
- Read datagrams from each interface inbox and attempt to receive the Packet.
- Handle scheduled events, executing callbacks/tasks if the event is due.
- Remove all processed events from
cls.schedule. - Send queued datagrams to reachable peers.
Asynchronous method that loops indefinitely, running the Process operation and then sleeping for a short timeout.
An Application consists of a callback for accepting Package delivery and a set of callbacks to expose functions to other Applications. The Application class must be initialized with a name, a description, a verison number, a callback for receiving Packages, and a dict with any functions that should be exposed to other Applications. If callbacks are async, the results of invoking them will be a coroutine.
This method takes in a blob of bytes representing an application message, the interface on which it was received, and the MAC address from which it was received. It then invokes the callback, passing self, blob, the Interface, and the MAC.
Returns a list[str] of the names of available callbacks. If a str name is passed, it instead returns True if there is a callback with that name and False otherwise.
Takes a str name, args, and kwargs, and attempts to invoke the callback with the name, passing self, args, and kwargs. If the callback is async, a coroutine will be returned.
Each package will have the following format:
- 16
app_id - 16
half_sha256 - 0+
blob
The app_id is a 16 byte unique identifier for the application that created the
package. The half_sha256 is the first half of the sha256 digest of the blob.
The maximum size for a package will be 12.9375 MiB or 12.3125 MiB, corresponding
to blob sizes of 13,565,952 and 12,910,592 bytes, depending on whether it is
packaged for ESP-NOW or RYLR-998 LoRa (determined by passing an array of
acceptable schema IDs to the packager API).
The version field will be a u8 protocol version number.
The reserved field will be 8 bits reserved for future protocol development.
The schema field will be a u8 packet schema identifier. The schema determines
which fields are used in the packet.
The flags field will be 8 bits (bits 0-7):
- Bit 0: error - set in response as a generic error notification
- Bit 1: throttle - set when a node retransmitting packet is experiencing congestion
- Bits 2-4: encoded, mutually exclusive flags
- Bits 5: reserved1
- Bits 6: reserved2
- Bit 7: mode - set to switch the mode of a schema-specific feature
Bits 2, 3, and 4 are used to encode mutually-exclusive flags, which the Flags
class exposes as attributes:
- 0b000 - no-op
- 0b001 - ask: set when a transmitting node wants an ack for the packet
- 0b010 - ack: set when the transmitting node is responding to an ask
- 0b011 - rtx: set when requesting a packet retransmission
- 0b100 - rns: set when requesting a node's status
- 0b101 - nia: set to indicate node status response (i.e. the node is active)
- 0b110 - encoded6: no-op
- 0b111 - encoded7: no-op
The mutually exclusive flag 0b000 is a no-op value. 0b110 and 0b111 are reserved
and are currently no-op (flags.encoded6 and flags.encoded7). Bits 5-6 can be
added to the encoded flags field to increase the number of mutually exclusive
flags if necessary for future developments.
The packet_id field will be a u8 or u16 ID for the packet, depending on schema.
This value is used for parsing acks and to sequence parts within a package. In
schemas without seq_id and seq_size, the packet_id will increment mod 256
on each packet sent or received for every non-sequenced schema. In schemas with
sequencing, the packet_id is the packet index within the sequence.
The seq_id field will be a u8 sequence id. This is used to sequence a package
into packets, allowing for retransmission requests of any missed/dropped packets
within a sequence. This value is incremented mod 256 for each sequence sent or
received.
The seq_size field will be a u8 or u16 sequence size, depending on the schema.
This is used to inform a receiver the size of the sequence of packets needed to
reconstruct the package from its parts. This value will always be exactly 1 less
than the actual value; in other words, it will show the maximum packet_id for
the sequence. (I.e. add 1 because a sequence size of 0 is meaningless and bytes
are 0-indexed.)
The ttl field will be a u8 and will contain the hop limit for the packet.
Each relay will decrement this counter before retransmission; if it reaches 0,
the relay will instead set the error flag and send the packet back toward the
originating node. If a relay receives a packet with the error flag active, it
will increment the ttl field before retransmitting back toward the originating
node; if it hits 255, the packet will be dropped.
Packet schemas that do not use the ttl field will be ineligible for packet
switching beyond a single hop: if the relay cannot immediately deliver the
packet to the intended recipient, it will respond by returning the packet to
the originator with the error flag set.
The checksum field will be 4 bytes and will contain the CRC32 checksum for the
body.
The tree_state field will be 1 byte and will contain the first byte of a CRC32
checksum for the state of the spanning tree configuration.
The to_addr field will be 16 bytes representing the spanning tree address of
the intended recipient node. Relays will retransmit by decrementing the TTL and
forwarding to the next nearest node using a greedy algorithm (MAC address of the
peer for ESP-NOW and network ID + address for LoRa).
The from_addr field will be 16 bytes representing the spanning tree address of
the packet origination node. Transmission errors will propagate back to this
node.
All packets will start with the following fields:
- 1
version - 1
reserved - 1
schema - 1
flags
Packets are formatted for either the ESP-NOW or RYLR-998 modulators, which support maximum payload sizes of 250 bytes and 240 bytes, respectively.
Schemas 0-19 are reserved for ESP-NOW compatibility. Schemas 20-29 are reserved for RYLR-998 compatibility.
ESP-NOW; 245 B max Package size.
- 1
packet_id - 245
body
ESP-NOW; 241 B max Package size.
- 1
packet_id - 4
checksum - 241
body
ESP-NOW; 256 max sequence size; 60.75 KiB max Package size.
- 1
packet_id - 1
seq_id - 1
seq_size - 243
body
ESP-NOW; 256 max sequence size; 59.75 KiB max Package size.
- 1
packet_id - 1
seq_id - 1
seq_size - 4
checksum - 239
body
ESP-NOW; 65536 max sequence size; 14.8125 MiB max Package size.
- 2
packet_id - 1
seq_id - 2
seq_size - 4
checksum - 237
body
ESP-NOW; 211 B max Package size.
- 1
packet_id - 1
ttl - 1
tree_state - 16
to_addr - 16
from_addr - 211
body
ESP-NOW; 207 B max Package size.
- 1
packet_id - 1
ttl - 4
checksum - 1
tree_state - 16
to_addr - 16
from_addr - 207
body
ESP-NOW; 256 max sequence size; 52.75 KiB max Package size.
- 1
packet_id - 1
seq_id - 1
seq_size - 1
ttl - 1
tree_state - 16
to_addr - 16
from_addr - 209
body
ESP-NOW; 256 max sequence size; 51.25 KiB max Package size.
- 1
packet_id - 1
seq_id - 1
seq_size - 1
ttl - 4
checksum - 1
tree_state - 16
to_addr - 16
from_addr - 205
body
ESP-NOW; 65536 max sequence size; 12.9375 MiB max Package size.
- 2
packet_id - 1
seq_id - 2
seq_size - 1
ttl - 1
tree_state - 16
to_addr - 16
from_addr - 207
body
ESP-NOW; 65536 max sequence size; 12.6875 MiB max Package size.
- 2
packet_id - 1
seq_id - 2
seq_size - 1
ttl - 4
checksum - 1
tree_state - 16
to_addr - 16
from_addr - 203
body
ESP-NOW; one-hop routable; 216 max Package size.
- 1
packet_id - 1
tree_state - 16
to_addr - 16
from_addr - 216
body
ESP-NOW; one-hop routable; 256 max sequence size; 53.5 KiB max Package size.
- 1
packet_id - 1
seq_id - 1
seq_size - 1
tree_state - 16
to_addr - 16
from_addr - 214
body
ESP-NOW; one-hop routable; 65536 max sequence size; 13.25 MiB max Package size.
- 2
packet_id - 1
seq_id - 2
seq_size - 1
tree_state - 16
to_addr - 16
from_addr - 212
body
RYLR-998; 235 B max Package size.
- 1
packet_id - 235
body
RYLR-998; 231 B max Package size.
- 1
packet_id - 4
checksum - 231
body
RYLR-998; 256 max sequence size; 53.25 KiB max Package size.
- 1
packet_id - 1
seq_id - 1
seq_size - 233
body
RYLR-998; 256 max sequence size; 57.25 KiB max Package size.
- 1
packet_id - 1
seq_id - 1
seq_size - 4
checksum - 229
body
RYLR-998; 65536 max sequence size; 14.1875 MiB max Package size.
- 2
packet_id - 1
seq_id - 2
seq_size - 4
checksum - 227
body
RYLR-998; 201 B max Package size.
- 1
packet_id - 1
ttl - 1
tree_state - 16
to_addr - 16
from_addr - 201
body
RYLR-998; 197 B max Package size.
- 1
packet_id - 1
ttl - 4
checksum - 1
tree_state - 16
to_addr - 16
from_addr - 197
body
RYLR-998; 256 max sequence size; 49.75 KiB max Package size.
- 1
packet_id - 1
seq_id - 1
seq_size - 1
ttl - 1
tree_state - 16
to_addr - 16
from_addr - 199
body
RYLR-998; 256 max sequence size; 48.75 KiB max Package size.
- 1
packet_id - 1
seq_id - 1
seq_size - 1
ttl - 4
checksum - 1
tree_state - 16
to_addr - 16
from_addr - 195
body
RYLR-998; 65536 max sequence size; 12.3125 MiB max Package size.
- 2
packet_id - 1
seq_id - 2
seq_size - 1
ttl - 1
tree_state - 16
to_addr - 16
from_addr - 197
body
RYLR-998; 65536 max sequence size; 12.0625 MiB max Package size.
- 2
packet_id - 1
seq_id - 2
seq_size - 1
ttl - 4
checksum - 1
tree_state - 16
to_addr - 16
from_addr - 193
body
LYLR-998; one-hop routable; 206 max Package size.
- 1
packet_id - 1
tree_state - 16
to_addr - 16
from_addr - 206
body
LYLR-998; one-hop routable; 256 max sequence size; 51 KiB max Package size.
- 1
packet_id - 1
seq_id - 1
seq_size - 1
tree_state - 16
to_addr - 16
from_addr - 204
body
LYLR-998; one-hop routable; 65536 max sequence size; 12.625 MiB max Package size.
- 2
packet_id - 1
seq_id - 2
seq_size - 1
tree_state - 16
to_addr - 16
from_addr - 202
body
An Interface provides an API to an underlying transmission module that handles frame encapsulation and datagrams. It includes the following methods:
- configure: takes a dict of configuration values and configures the interface
- wake: re-activates/reconfigures any hardware after waking from modem sleep
- receive: returns a received datagram if there is one or None
- send: takes a datagram (with data and address) and transmits it
- broadcast: takes a datagram (without address) and broadcasts it
- process: processes the queued datagrams to be sent/broadcast and queues
datagrams in the inbox; async method that calls callbacks passed to
__init__ - validate: validates that the Interface has the necessary callbacks
- add_hook: takes a name and a callable hook; registers the hook with the name
- call_hook: takes a name and any args/kwargs, then calls the hook if it exists
The send and broadcast methods queue the data for asynchronous processing, and the receive method reads from a queue that is populated by an async task.
An Interface also has the following attributes:
- supported_schemas: list of schema IDs supported by the interface
The Interface class provides a logical framework for this. Initializing it
requires passing name: str, configure: function,
supported_schemas: list[int], and several synchronous and/or async callbacks.
Must provide one of each pair: (send_func, send_func_async); (receive_func,
receive_func_async); (broadcast_func, broadcast_func_async). When the
async process method is called, it first tries to queue a datagram from
calling one of the receive_ callbacks; it then tries to send a datagram queued
for sending using one of the send_ callbacks; it then tries to broadcast a
datagram queued for broadcast using one of the broadcast_ callbacks. The
receive_ callback must accept the Interface as its sole argument. The send_
and broadcast_ callbacks must accept a Datagram as their sole argument.
If the Interface will need to re-activate or reconfigure anything after waking
from modem sleep, pass the callable as the wake_func keyword argument.
A logical loopback Interface is provided so that apps can communicate with each
other locally. It is not excluded from Packager operations, but it will not be
used by Packager.send without first adding the local node as its own peer:
Packager.add_peer(Packager.node_id, InterAppInterface, b'mac'); calling
Packager.send(Packager.node_id, app_id, blob) would then use the
InterAppInterface to logically transmit the Package to the Application. It is
more efficient to use Packager.deliver for local inter-App communication, but
this Interface is left as an option. Any Package send with Packager.broadcast
will loopback and be received by the local Application.
To enable this broadcast loopback functionality, execute
Packager.add_interface(InterAppInterface). The intended use case is testing
Applications during development.
Takes a Packet and a retry counter (default=1).
Most applications will want to send data to specific node. This is supported
for all schemas. Packets can set flags.ask=1 to ensure packet delivery.
However, for multi-part packages, packets in the sequence will have
flags.ask=0 and rely upon the receiving node to request retransmission
for any dropped packets. Packets in this category will always have
flags.rtx=0.
For any packet sent with flags.ask=1, if a corresponding ack was not received
after a short timeout, retransmission will occur and a transmission attempt
counter will decrement.
All sequences should be cached when sent to enable retransmissions. All
sequences sent should include flags.ask=1 on a sample of packets. After all
requested acks have been received, the sequence can be
For applications that require broadcast, schemas that do not include the ttl,
to_addr, and from_addr fields (i.e. non-routed packets) can be broadcast on
configured interfaces. Broadcast packets may set flags.ask to 1;
however, only packets that do not use the seq_size and seq_id fields (i.e.
Packages that can be contained in the body of a single packet) can be broadcast
with flags.ask = 1 to avoid congestion. Additionally, only schemas
with a 1-byte packet_id field can be broadcast.
When a receiving node receives and processes a packet with flags.ask=1,
it will queue up a response packet using the same schema that has flags.ack=1,
an empty body, and the same packet_id as the received packet. If the
received packet was part of a sequence, the seq_size and seq_id fields will
be copied from the received packet. If the packet was routed, the to_addr will
be set to the from_addr of the original packet; the from_addr will be set to
the to_addr of the original packet; and the tree_state field will be copied.
When a receiving node receives an incomplete sequence, after a delay, it will
reques retransmission of missing packets in the sequence by using the simplest
appropriate schema with the seq_id and seq_size of the sequence and an empty
body field. It will send one packet with flags.rtx=1 for each
missing packet_id. Each retransmitted packet received will be added to the
sequence in the Packager.
After another delay, if no retransmissions were received, the node will count it as a failure and try again; if some retransmissions were received, the node will request retransmissions of remaining missing packets, but it will not count as a failure.
After two failures, the node will drop the package and remove the sequence from its memory/storage.
A retransmission is a Send of the original packet. For this feature, sequences will need to be cached in memory/storage for some time after the original for receipt of the packets at the destination and receipt of retransmission sequence transmission. The cache expiration should be twice the expected time requests by the originating node. The cache should also support item priority so that lower priority items can be preferentially removed from the cache when it is full and new space is needed.
Routing will be accomplished using a simplified adaptation of Practical
Isometric Embedding (PIE) protocol and Virtual Overlays Using Tree Embeddings
(VOUTE). It will embed a spanning tree into the graph of nodes; assign addresses
to encode the spanning tree structure; and provide 2 routing metrics for
forwarding packets. The routing metric to use will be determined by setting
flags.mode: 0 for tree distance and 1 for common prefix length distance.
The spanning tree system will be maintained by an application, and its Package
format will start with a 1-byte op field, which will be 0x00 for a periodic
broadcast containing current best root election claim and the node's address,
0x0f to respond to a 0x00 broadcast if the local node has a better root election
claim in its app state, 0xf0 for address assignment request, and 0xff for
address assignment response.
There are three separate aspects to consider: spanning tree creation/maintenance, coordinates, and routing.
The first step in spanning tree creation is root election: a score is calculated by XORing the sha256 of the node's public key with the sha256 of the protocol name; if the result is lower than the score of the current root, the node is elected. At the start of the protocol, a node broadcasts its own claim as root, including the public key, sha256 of the protocol name, and a Unix epoch timestamp, and any peer that has a current root with a lower score will respond with it.
The second step is for peers of the root to request an address from the root. The root will then assign an address to each peer, and those peers will then drop any old tree data, mark their current tree data as old, allocate new tree data, and update their tree state to the first byte of the CRC32 of the root public key, the sha256 of the protocol name, and the Unix epoch timestamp. This then generalizes for peers of peers of the root, etc.
Before requesting an address assignment, a node will wait ~20 seconds to collect broadcasts from its peers. If its own election claim is not the best, it will filter its peers for those with the best root election claim, then request an address assignment from the peer with the shortest distance to the root.
Every 20 seconds, a routine will run that will do the following:
- If the tree that the local node is a part of has expired, it will reset to the state of itself being the root.
- Expired election claims will be discarded.
- If the node has no parent, and there are known claims, it will evaluate the best known claim: if the claim is better than the local node's claim, it will request an address assignment from that peer; otherwise, it will update the tree timestamp and broadcast its periodic information package.
Future design; currently unimplemented
Address assignments take the form of a chain of simple certificates stemming from the root: the root signs an address assignment for a peer consisting of the tree state, that peer's public key, and the assigned address. That peer can then assign addresses to its children in the tree by signing address assignments with the same format and bundling with it the cert from the root. Every peer's address assignment then consists of its signed certificate from its parent and the chain of signed address assignment certificates leading back to the root.
The election of new roots will cause tree recalculation. To ensure that the tree is maintained, the root will periodically broadcast a new election claim with a new timestamp, which then creates a new tree state. If a root fails to broadcast after three consecutive periods, its term as root ends, and a new root is elected.
New elections after a term expiration are done by each node calculating the difference between its score and the previous root's score and waiting an amount of time proportional to the log of that difference before broadcasting its root election claim. This delay should give tree recalculations originating from election of nodes with lower scores time to propagate across the network and thus avoid unnecessary traffic and the creation of phantom tree states.
If a node drops its parent in the tree from the peer list, it will broadcast its own root election claim, restarting the process of acquiring an address from the peer that is closest to the root from the peers that respond to the broadcast.
Address assignments are simply the tree state, address, and node ID. These values are accepted on face value because elliptic curve cryptography is not currently supported in micropython.
Every tree message includes an age for the tree it pertains to. Tree age is
calculated by time() - tree_ts. During the tree maintenance event, the root
will update its internal tree timestamp before broadcasting its tree message,
setting the age of the tree to 0. Its children then set their internal tree
timestamp to time() - tmsg.age, also setting the age to 0 before broadcasting
to their children. This chains, updating the timestamps in all members of the
tree to keep the tree alive. If an intermediate node in the tree disconnects,
the tree will expire for its descendants, and they will recalculate their
subtree and addresses. If the root disconnects, the whole spanning tree will be
recalculated.
The VOUTE paper suggests having multiple embeddings for more reliability, and this may be implemented in the future, perhaps using the first bit of the tree state to select one of two simultaneous trees.
Addresses will encode up to 32 coordinates, each representing the index of a child at the parent. This will allow for a network of between 16 tree levels at up to 135 (7+128) children per parent and 32 tree levels at up to 7 children per parent. This allows for tree membership in the range between 1.1x10^27 and 1.2x10^34 nodes.
A zero value represents a lack of that coordinate. The root will have no coordinates, i.e. an address of all zeros. Each child will take its parent's coordinates as a prefix for its own; e.g. child 3 of the node with coordinates (12, 1) will have coordinates (12, 1, 3).
Coordinates are encoded as follows: if the coordinate is <8, it is encoded in a nibble with the high bit set to 0; if the coordinate is >7, it is encoded by subtracting 8, converting to an octet, and setting the high bit to 1. Coordinates can thus have a value between 1 and 135.
Coordinates are decoded as follows: split the address into nibbles; for each nibble, if the high bit is not set, then the next 3 bits (the rest of the nibble) encode the coordinate; if the high bit is set, then the next 7 bits (the rest of the nibble and then the next) encode the coordinate, and we add the integer 8 to that value.
Examples:
- (3, 1) => 0b0011 0b0001
- (8, 3) => 0b1000000 0b0011
- (4, 12) => 0b0100 0b10000100
Addresses have a user-friendly string representation with the following format:
123-235783::. The number before the dash is the decimal int representation of
the tree state. The string after the dash is the coordinate encoded as a hex
string in which trailing or internal stretches of 00 are replaced with a double
colon. The empty coordinate replacement will compress only the first instance of
at least 2 consecutive null bytes (4 coordinates of zero).
Routing is done by calculating the selected distance metric for each peer and forwarding to the peer closest (i.e. with the shortest distance) to the destination. The distance metrics are defined as follows. For these definitions, cpl(x1, x2) is the "common prefix length" and means the number of consecutive coordinates starting at the beginning that are shared between addresses x1 and x2. Also, |x1| means the number of coordinates in address x1, and L = 33.
Tree distance, or dTree, is defined as follows:
dTree(x1, x2) = |x1| + |x2| - 2 * cpl(x1, x2)
Greedy routing with this metric tends to favor shorter paths but may congest nodes closer to the root.
Note that client nodes will have addresses with empty coordinates between the router coordinates and the client coordinates, and only the router coordinates should be used for routing.
CPL Distance, or dCPL, is defined as follows:
For x1 != x2:
dCPL(x1, x2) = L - cpl(x1, x2) - 1 / (|x1| + |x2| + 1)
For x1 == x2:
dCPL(x1, x2) = 0
Greedy routing with this metric tends to favor routing further away from the nodes closer to the root, but sometimes takes longer paths.
Note that all coordinates must be used for dCPL routing, including any empty trailing or internal coordinates.
The Packager system will include peer discovery and management logic. This will maintain a list of peers (i.e. directly connected nodes reachable without packet routing) and nodes (reachable with or without routing) and the applications they support, and it will expose this data to other applications.
Each node will maintain a peer list containing the following information:
- id: the public key of the peer
- interfaces: a list of tuples each containing (mac, interface_id)
- timeout: an int used for automatic peer disconnects
- throttle: an int used to throttle bandwidth for a peer
- last_tx: an int used to track transmissions for throttling
When a node adds a peer or receives a Beacon from that peer, it sets the timeout value to 4. After a node sends its own Beacon, it decrements every peer timeout counter, then drops all peers with a timeout counter of 0.
When a node receives a packet from a peer that includes flags.throttle=1, it
will increment the throttle count for that peer; when it receives a packet from
a peer that includes flags.throttle=0, it will decrement the throttle count
for that peer unless it is already 0. When a node tries to send a packet to a
peer with a throttle value greater than 0, it first checks the last_tx value; if
it is more than throttle * 1000 ms in the past, transmit the packet, otherwise
schedule the transmission for last_tx + throttle * 1000.
Each node will maintain a node list containing the following information:
- id: the public key of the node
- apps: list of IDs for apps the node supports
- ts: int timestamp of last update
See the Beacon and Gossip (Application Discovery) sections for details on how this is populated and updated. Nodes will be dropped from the list after 30 minutes of the last update.
At initialization and periodically thereafter, each node will broadcast a beacon
Package introducing the node. The body of this beacon will be the byte 0x00, the
node's ed25519 public key and up to 10 app IDs it supports, and it will use the
simplest possible schema and all flags set to 0. This message format will be
compatible with both ESP-NOW and RYLR-998 using Packet Schemas 0 and 20,
respectively. If a node supports more than 10 apps, it will broadcast several
beacons.
Upon receiving a beacon from an unknown peer, a node will add this peer to its peer list and then respond by sending a beacon response to that peer. The beacon response Package body will contain the byte 0x01, the node's public key, and up to 10 app IDs it supports. If the responding node supports mode than 10 apps, it will send additional beacon responses.
When a node receives a beacon response Package, it will add that peer to its peer list.
Before disabling its radios, a node should send a disconnect Package with a body that is the byte 0xff and the node's public key.
When a node receives a Beacon from a peer, it sets the peer timeout value to 4. After a node sends out its periodic Beacon, it will decrement the timeout for each peer in its peer list; if the timeout for a peer reaches 0, the peer is removed from the peer list.
To save battery power, the Packager.work method will use machine.lightsleep
if it is available and the use_modem_sleep param is set to True. It will
enter light sleep for modem_sleep_ms (default MODEM_SLEEP_MS) and then
stay active for a minimum of modem_active_ms (default MODEM_WAKE_MS).
Anytime a node receives or tries to send a datagram, it will queue skipping
1 modem sleep cycle, and that queue will have capacity for a maximum of 10 skips.
The following constants are defined for this feature and may be adjusted in future versions to tune performance:
- MODEM_SLEEP_MS = 90
- MODEM_WAKE_MS = 40
- MODEM_INTERSECT_INTERVAL = floor(0.9 * MODEM_WAKE_MS)
- MODEM_INTERSECT_RTX_TIMES = 1 + floor((MODEM_SLEEP_MS + MODEM_WAKE_MS) / MODEM_INTERSECT_INTERVAL)
When a node wants to transmit to another node, since it could be in modem sleep,
it will first send a packet with flags.rns=1 (Request Node Status) to test if
the node is active. An active node that receives such a packet will respond with
flags.nia=1 (Node Is Active).
The RNS will be sent MODEM_INTERSECT_RTX_TIMES times with a delay between of
MODEM_INTERSECT_INTERVAL, after which all datagrams queued for the peer will
be cleared if no NIA has been received. Upon receiving NIA, peer.last_rx will
be updated, and thus datagrams will begin transmitting to that peer; the
scheduled RNS event will also be canceled.
To enable additional functionality, primarily to allow nodes that run a shared application to find each other across the network, a gossip application will be included. This application will use broadcasts and sends to transmit Packages to peers and thus disseminate them across the network.
There are six types of gossip Messages (Package bodies): Publish, Notify, Request, Request IDs, Respond, and Respond IDs. Messages are serialized as follows:
- 1 type
- 16 id_field
- 0+ data
A Request Message contains the following:
- 1 type: 0x00 (0)
- 16 message_id: the half_sha256 of the serialized Publish Message received in a Notify Message
- 32 node_id: the node_id of requester
A Request IDs Message contains the following:
- 1 type: 0x01 (1)
- 16 topic_id: half_sha256 of the topic data
- 32 node_id: the node_id of requester
A Notify Message contains the following:
- 1 type: 0x0f (15)
- 16 topic_id: half_sha256 of the topic data
- 16 message_id: the half_sha256 of the serialized Publish Message
A Publish Message contains the following:
- 1 type: 0xf0 (240)
- 16 topic_id: half_sha256 of the topic data
- 0+ data: the data for the topic subscribers to injest
A Respond Message contains the following:
- 1 type: 0xfe (254)
- 16 message_id: the half_sha256 of the serialized Publish Message received in a Notify Message
- 0+ data: the data for the topic subscribers to injest
A Respond IDs Message contains the following:
- 1 type: 0xff (255)
- 16 topic_id: half_sha256 of the topic data
- 0+ data: the Message IDs available for the topic
Makes a Publish Message out of a topic_id and data, then delivers the Publish Message locally, which then broadcasts a Publish or Notify Message to peers, depending on the size of the data.
When a Publish Message is delivered locally, the node broadcasts either the Message if it can fit in a single packet or a Notify for the Publish. If it broadcasts a Notify, then it awaits Requests from its peers and responds by sending the Message. The Message is also put into a cache to mark it as seen so that future deliveries of the same Message are rejected and future Notifications for the Message ID are ignored.
When a Notify Message is received, if the Message ID is not in the cache, the node will send a Request to the peer that sent the Notify.
When a Request is received, if a Message with the requested ID is in the cache, the node will send the Message to the peer that sent the Request. If the requested Message can fit into a single packet, the message type will be changed to a Respond to avoid further forwarding by the recipient; otherwise, it will assume that this was a response to a Notify.
When a Request IDs is received, the node will send a Respond IDs for each topic ID for which an application is subscribed.
When a Notify is received for an unseen Message ID, the node will send a Request. When a list of Message IDs is received, the node will send a Request for each unseen Message ID.
When a new peer is added, the node will send a Request IDs for each topic ID for which an application is subscribed.
Associates an application with a topic ID. All new Messages delivered will be forwarded to the subscribed application.
Removes an application from the subscriber list for a topic ID. If no applications are subscribed to the topic ID, the topic ID will be dropped.
import network
import espnow
# A WLAN interface must be active to send()/recv()
network.WLAN(network.STA_IF).active(True) # Or network.AP_IF
e = espnow.ESPNow()
e.active(True)
# or for async
import aioespnow- https://docs.micropython.org/en/latest/library/espnow.html
- Cycle through channels broadcasting beacons until a response is received
ESPNow.config(rxbuf=1052)optionally to double the receive bufferESPNow.config(channel=int)optionally to set the WiFi channelESPNow.any() -> boolto check if data is ready to be receivedESPNow.recv() -> list[bytes, bytes]to receieve[from_mac, data]ESPNow.send(mac: bytes, msg: bytes, sync=False)to send a message- https://docs.espressif.com/projects/esp-idf/en/latest/esp32c3/api-reference/network/esp_now.html
- https://github.com/espressif/esp-now
- https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/wifi.html
- https://reyax.com/products/rylr998/
- https://reyax.com//upload/products_download/download_file/RYLR998_EN.pdf
- https://reyax.com//upload/products_download/download_file/LoRa_AT_Command_RYLR998_RYLR498_EN.pdf
- https://docs.micropython.org/en/latest/library/machine.UART.html
- https://docs.micropython.org/en/latest/library/network.WLAN.html
- https://hackaday.io/project/161896-linux-espnow
- https://docs.espressif.com/projects/esp-idf/en/latest/esp32c3/api-reference/network/esp_now.html
- https://github.com/espressif/esp-now
- https://docs.micropython.org/en/latest/library/bluetooth.html
- https://docs.micropython.org/en/latest/library/bluetooth.html#l2cap-connection-oriented-channels
- deque (doule-ended queue) has two thread safe operations: append and popleft
After abandoning the idea of using machine learning to approximate coordinates in N-dimensional space as the primary method of creating addresses upon which greedy routing could be accomplished, I rewrote the spec to include methods that actually could work. Below are the links to the original concept, the outcomes of the simulation studies that convinced me it would not work (specifically the orienting of a second axis by adding a third node, which almost always failed to produce a valid/accurate result), and the first attempt to specify a network system with any detail.