├── LICENSE ├── README.md ├── async ├── README.md ├── as_nrf_json.py ├── as_nrf_simple.py ├── as_nrf_stream.py ├── as_nrf_test.py └── asconfig.py └── radio-fast ├── README.md ├── config.py ├── msg.py ├── nbtest.py ├── radio_fast.py ├── rftest.py └── tests.py /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Peter Hinch 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # micropython-radio 2 | 3 | This repo comprises two protocols for the nRF24L01+ radio module. Each 4 | implements a bidirectional link between two radios. 5 | 6 | ## radio-fast 7 | 8 | A driver for short fixed-length records. This is a thin layer over the official 9 | driver which makes it easier to ensure mutually compatible configurations of 10 | the radios. This is done by deploying a common config file to both nodes. 11 | 12 | The nRF24L01 provides data integrity but successful reception is not guaranteed 13 | as radio outages can occur (see below). 14 | 15 | In this protocol one radio acts as master (initiating communications) and the 16 | other acts as slave (responding to transmissions) 17 | 18 | See [README](./radio-fast/README.md) 19 | 20 | ## as_nrf_stream.py 21 | 22 | See [README](./async/README.md) 23 | 24 | Radio links are inherently unreliable, not least since receiver and transmitter 25 | may move out of range. The link may also be disrupted by radio frequency 26 | interference. This driver mitigates this by ensuring that, in the event of a 27 | link outage, data transfer resumes without loss when connectivity is restored. 28 | 29 | The use of `uasyncio V3` stream I/O means that the interface matches that of 30 | objects such as sockets and UARTs. Objects exchanged are `bytes` instances, 31 | typically terminated by a newline character (`b'\n'`). Lengths of the `bytes` 32 | objects are arbitrary and are allowed to vary at runtime. Consequently it is 33 | easy to exchange Python objects via serialisation libraries such as `pickle` 34 | and `ujson`. 35 | 36 | The underlying protocol's API hides the following details: 37 | 1. The radio hardware is half-duplex (it cannot simultaneously transmit and 38 | receive). 39 | 2. The chip has a 32 byte limit on message length. 40 | 3. To address point 1 the protocol is asymmetrical with a master/slave design 41 | which is transparent to the user. 42 | 43 | The driver provides a symmetrical full-duplex interface in which either node 44 | can initiate a transmission at any time. The cost relative to the `radio-fast` 45 | module is some loss in maximum throughput and an increase in latency. Gains 46 | are: 47 | * The ability to exchange relatively large, dynamic objects. 48 | * Data integrity with each message being correctly received exactly once. 49 | * A standard bidirectional (full duplex) stream interface. 50 | * Asynchronous code: in the event of an outage communication will inevitably 51 | stall for the duration, but other coroutines will continue to run. 52 | 53 | ## Obsolete modules 54 | 55 | The `as_nrf_stream` driver replaces the old `radio-pickle` and 56 | `async-radio-pickle` modules which pre-dated the `uasyncio` I/O interface. This 57 | module is simpler, smaller and more efficient. Lastly the old asynchronous 58 | driver allowed duplicate messages to be received. The new protocol ensures that 59 | each record is received exactly once. 60 | -------------------------------------------------------------------------------- /async/README.md: -------------------------------------------------------------------------------- 1 | # 1. nRF24l01 Stream driver 2 | 3 | This enables a pair of nRF24l01 radios to provide a link that implements the 4 | MicroPython `uasyncio` I/O interface. The object is to enable a pair of radios 5 | to have the same asynchronous interface as a pair of UARTs or sockets. 6 | 7 | Users may choose a serialisation library to exchange Python objects. 8 | Alternatively an application may simply exchange fixed or variable length 9 | `bytes` instances. Demo scripts illustrate both approaches including the use of 10 | `ujson` for Python object interchange. 11 | 12 | ## 1.1. Overview 13 | 14 | Radio links are inherently unreliable, not least since receiver and transmitter 15 | may move out of range. The link may also be disrupted by radio frequency 16 | interference. This driver mitigates this by ensuring that, in the event of a 17 | link outage, data transfer will resume without loss when connectivity is 18 | restored. 19 | 20 | The use of stream I/O means that the interface matches that of objects such as 21 | sockets and UARTs. The objects exchanged are `bytes` instances. 22 | 23 | Where an application uses a serialisation library like `pickle` or `ujson`, the 24 | resultant `bytes` objects will be of variable length. This raises the issue of 25 | how the recipient determines the end of a message. The simplest approach is for 26 | the application to terminate the `bytes` with a newline character `b'\n'`. 27 | This allows the recipient to use the `StreamReader.readline` method. 28 | 29 | In this doc an application-level `bytes` object is termed a `message` as 30 | distinct from a `packet` which is the fixed length `bytes` object exchanged by 31 | the radios. 32 | 33 | The underlying protocol's API hides the following details: 34 | * The fact that the radio hardware is half-duplex. 35 | * The hardware limit on message length. 36 | * The asymmetrical master/slave design of the underlying protocol. 37 | 38 | It provides a symmetrical full-duplex interface: either node can send a 39 | `message` at any time. The cost is some loss in throughput and increase in 40 | latency relative to the `radio-fast` module. 41 | 42 | # 2. Dependencies 43 | 44 | The library requires the 45 | [official nRF24l01 driver](https://github.com/micropython/micropython-lib/blob/master/micropython/drivers/radio/nrf24l01/nrf24l01.py) 46 | which should be copied to both targets' filesystems. It requires `uasyncio` 47 | version 3 which is built in to the firmware. 48 | 49 | # 3. Files and installation 50 | 51 | 1. `as_nrf_stream.py` The library. 52 | 2. `asconfig.py` User-definable hardware configuration for the radios. 53 | 3. `as_nrf_simple.py` Minimal demo of exchanging `bytes` objects. 54 | 4. `as_nrf_json.py` Demo of exchanging Python objects and detecting outages. 55 | 5. `as_nrf_test.py` Test script. This transmits and reports statistics showing 56 | link characteristics. 57 | 58 | To install, adapt `asconfig.py` to match your hardware. Copy it and 59 | `as_nrf_stream` to both targets. Ensure dependencies are satisfied. Copy any of 60 | the above test scripts to both targets. Test scripts print running instructions 61 | on import. 62 | 63 | # 4. Usage examples 64 | 65 | ## 4.1 Exchanging bytes objects 66 | 67 | Taken from `as_nrf_simple.py`. This is run by issuing 68 | ```python 69 | as_nrf_simple.test(True) 70 | ``` 71 | on the master, and the same with arg `False` on the slave. Note `asconfig.py` 72 | must be adapted for your hardware and deployed to both nodes. 73 | ```python 74 | import uasyncio as asyncio 75 | from as_nrf_stream import Master, Slave 76 | from asconfig import config_master, config_slave # Hardware configuration 77 | 78 | async def sender(device): 79 | swriter = asyncio.StreamWriter(device, {}) 80 | while True: 81 | swriter.write(b'Hello receiver\n') # Must be bytes, newline terminated 82 | await swriter.drain() 83 | await asyncio.sleep(1) 84 | 85 | async def receiver(device): 86 | sreader = asyncio.StreamReader(device) 87 | while True: 88 | res = await sreader.readline() 89 | if res: # Can return b'' 90 | print('Received:', res) 91 | 92 | async def main(master): 93 | device = Master(config_master) if master else Slave(config_slave) 94 | asyncio.create_task(receiver(device)) 95 | await sender(device) 96 | 97 | def test(master): 98 | try: 99 | asyncio.run(main(master)) 100 | finally: # Reset uasyncio case of KeyboardInterrupt 101 | asyncio.new_event_loop() 102 | ``` 103 | Note that the radios could be replaced by a UART by changing initialisation 104 | only. The `sender` and `receiver` coroutines would be identical. 105 | 106 | ## 4.2 Exchanging Python objects 107 | 108 | In this example a list is passed. Any object supported by ujson may be used. 109 | ```python 110 | import uasyncio as asyncio 111 | import ujson 112 | from as_nrf_stream import Master, Slave 113 | from asconfig import config_master, config_slave # Hardware configuration 114 | 115 | async def sender(device): 116 | ds = [0, 0] # Data object for transmission 117 | swriter = asyncio.StreamWriter(device, {}) 118 | while True: 119 | s = ''.join((ujson.dumps(ds), '\n')) 120 | swriter.write(s.encode()) # convert to bytes 121 | await swriter.drain() 122 | await asyncio.sleep(1) 123 | ds[0] += 1 # Record number 124 | 125 | async def receiver(device): 126 | sreader = asyncio.StreamReader(device) 127 | while True: 128 | res = await sreader.readline() # Can return b'' 129 | if res: 130 | try: 131 | dat = ujson.loads(res) 132 | except ValueError: # Extremely rare. See section 10.1 133 | pass 134 | else: 135 | print(dat) 136 | 137 | async def main(master): 138 | device = Master(config_master) if master else Slave(config_slave) 139 | asyncio.create_task(receiver(device)) 140 | await sender(device) 141 | 142 | def test(master): 143 | try: 144 | asyncio.run(main(master)) 145 | finally: # Reset uasyncio case of KeyboardInterrupt 146 | asyncio.new_event_loop() 147 | ``` 148 | 149 | # 5. Configuration: asconfig.py 150 | 151 | This file is intended for user adaptation and contains configuration details 152 | for the two radios. It is intended that a common `asconfig.py` is deployed to 153 | both nodes. 154 | 155 | Node hardware may or may not be identical: the two nodes may use different pin 156 | numbers or SPI bus numbers. The `RadioSetup` class uses instance variables for 157 | values which may differ between nodes and class variables for those which must 158 | be common to both. 159 | 160 | ## 5.1 Class RadioSetup 161 | 162 | This is intended to facilitate sharing a configuration between master and slave 163 | devices to reduce the risk of misconfiguration. A `RadioSetup` object may be 164 | instantiated and configured in a module common to master and slave. An example 165 | may be found in `asconfig.py`. 166 | 167 | #### Class variables (shared by both nodes) 168 | 169 | * `tx_ms = 200` Defines the maximum time a transmitter will wait for a successful 170 | data transfer. 171 | * `channel` Defines the radios' carrier frequency. See 172 | [section 7](./README.md#7-radio-channels). 173 | 174 | #### Constructor (args may differ between nodes) 175 | 176 | This takes the following arguments being objects instantiated by the `machine` 177 | module: 178 | * `spi` An SPI bus instance. 179 | * `csn` Pin instance connected to CSN. 180 | * `ce` Pin instance linked to CE. 181 | * `stats=False` If `True` the driver gathers statistics including a count of 182 | transmit and receive timeouts. These can be used to glean a rough measure of 183 | link quality. See `as_nrf_json.py` for an example of displaying these. If 184 | `False` a (tiny) amount of RAM is saved. See 185 | [section 8](./README.md#8-statistics). 186 | 187 | # 6. API: as_nrf_stream 188 | 189 | The library provides two classes, `Master` and `Slave`. The device at one end 190 | of the link must be instantiated as `Master`, the other as `Slave`. Their user 191 | interfaces are identical. At application level it does not matter which is 192 | chosen for a given purpose as the API is provided by a common base class. The 193 | following applies to both classes. 194 | 195 | #### Constructor 196 | 197 | This takes a single argument, being an instance of the `RadioSetup` class as 198 | described above. 199 | 200 | #### Methods 201 | 202 | * `t_last_ms` No args. Return value: the time in ms since the last packet was 203 | received. May be used to detect outages. See `as_nrf_test.py` for an example. 204 | * `stats` If specified in the config file, performance counters are maintained 205 | in a list of integers. This method returns that list, or `None` if the config 206 | has disabled statistics. See [section 8](./README.md#8-statistics). 207 | 208 | #### Typical sender coroutine 209 | 210 | This instantiates a `StreamWriter` from a `Master` or `Slave` instance and 211 | writes `bytes` objects to it as required. If the reader is to use `.readline` 212 | these should be newline terminated. The `drain` method queues the bytes for 213 | transmission. If transmission of the previous call to `drain` has completed, 214 | return will be "immediate"; otherwise the coroutine will pause until 215 | transmission is complete and reception has been acknowledged. In the event of 216 | an outage, the pause duration will be that of the outage. 217 | ```python 218 | async def sender(device): 219 | swriter = asyncio.StreamWriter(device, {}) 220 | while True: 221 | swriter.write(b'Hello receiver\n') # Must be bytes. Newline terminated 222 | await swriter.drain() # May pause until received. 223 | await asyncio.sleep(1) 224 | ``` 225 | 226 | #### Typical receiver coroutine 227 | 228 | This instantiates a `StreamReader` from a `Master` or `Slave` instance and 229 | waits until a complete line is received. 230 | ```python 231 | async def receiver(device): 232 | sreader = asyncio.StreamReader(device) 233 | while True: 234 | res = await sreader.readline() 235 | if res: # Can return b'' 236 | print('Received:', res) 237 | ``` 238 | In order to keep data interchange running fast and efficiently, applications 239 | should run a coroutine which spends most of its time in `.readline`. Where slow 240 | operations are needed to process incoming data, these should be delegated to 241 | other concurrent tasks. 242 | 243 | The `.readline` method has two possible return values: a single complete line 244 | or an empty `bytes` instance. Applications should check for and ignore the 245 | latter. 246 | 247 | # 7. Radio channels 248 | 249 | The RF frequency is determined by the `RadioSetup` instance as described above. 250 | The `channel` value maps onto frequency by means of the following formula: 251 | freq = 2400 + channel [MHz] 252 | The maximum channel no. is 125. The ISM (Industrial, Scientific and Medical) 253 | band covers 2400-2500MHz and is licensed for use in most jurisdictions. It is, 254 | however, shared with many other devices including WiFi, Bluetooth and microwave 255 | ovens. WiFi and Bluetooth generally cut off at 2.4835GHz so channels 85-99 256 | should avoid the risk mutual interference. Note that frquencies of 2.5GHz and 257 | above are not generally licensed for use: check local regulations before using 258 | these devices. 259 | 260 | # 8. Statistics 261 | 262 | These monitor the internal behaviour of the driver and may be used as a crude 263 | measure of link quality. If enabled in the config the device's `stats` method 264 | returns a list of four integers. These are counters initialised to zero and 265 | incrementing when a given event occurs. Their meaning (by offset) is: 266 | 0. Receive timeouts. Relevant only to `Master`: increments when the slave does 267 | not respond in 1.5* `tx_ms` [section 5.1](./README.md#51-radio-setup). These 268 | will repeatedly occur during an outage. 269 | 1. Transmit timeouts. Counts instances where transmit fails to complete in 270 | `tx_ms`. In my testing this rarely occurs, and only during an outage. 271 | 2. Received data packets. Counts all packets received with a data payload. 272 | 3. Received non-duplicate data packets. 273 | 274 | The driver handles a stream and has no knowledge of application-level record 275 | structure. Received data statistics refer to packets. In general the mapping of 276 | records onto packets is one-to-many. Further, if a node has nothing to send, it 277 | sends a packet with no payload. 278 | 279 | Ways of using these statistics to gauge link quality are to measure the rate at 280 | which receive timeouts occur on the `Master` or to measure the rate at which 281 | duplicate packets are received - along lines of 282 | `(stats[2] - stats[3)/seconds`. 283 | 284 | Receive timeouts occur on `Master` only if `Master` detects no response from 285 | `Slave` in a period of just over 1.5*`tx_ms`. These may occur because the slave 286 | did not receive the packet from `Master`, or because `Master` did not receive 287 | the response from `Slave`. 288 | 289 | Duplicate packets occur when one node fails to receive a transmission from the 290 | other. In this event it keeps trying to send the same packet until a response 291 | is detected (the driver detects and discards dupes). 292 | 293 | # 9. Protocol 294 | 295 | The underlying communications channel is unreliable inasmuch as transmitted 296 | packets may not be received. However, if a packet is received, the radio 297 | hardware aims to guarantee that its contents are correct. In practice this 298 | "guarantee" is not perfect: see [section 10](./README.md#101-message-integrity). 299 | 300 | The protocol has two aspects: management of send/receive and management of 301 | packets. There is no point in a node transmitting if its peer is not listening. 302 | Any packet sent may be lost: this needs to be detected and rectified. 303 | 304 | ## 9.1 Packets 305 | 306 | Packets comprise 32 bytes. Byte 0 is a command, byte 1 is the payload length. 307 | The remaining bytes are the payload padded with 0. The payload may be empty, 308 | the length byte then being 0. Packets carrying a payload are known as payload 309 | packets (PP). 310 | 311 | There are two commands: `MSG` and `ACK`. Only `Master` sends the `ACK` command. 312 | `MSG` denotes a normal packet, `ACK` signifies acknowledgement of a PP from 313 | `Slave`. Both packet types may or may not be PPs. 314 | 315 | The command byte includes a 1-bit packet ID. This toggles each time a new PP 316 | is constructed. It is used by the recipient for duplicate detection. The 317 | protocol forbids either node from sending a new payload until the prior one is 318 | acknowledged. Consequently a single bit suffices as a packet ID. 319 | 320 | ## 9.2 Direction management 321 | 322 | A difference between `Master` and `Slave` is that `Master` issues unsolicited 323 | packets. `Slave` listens constantly and transmits exactly one packet each time 324 | it receives a packet from `Master`. `Master` listens for a period after each 325 | transmission: if nothing is received in that time it repeats the last packet 326 | sent. After sending a packet in response to one received, `Slave` listens with an 327 | infinite timeout. The concept here is that `Slave` will eventually receive a 328 | packet from `Master`: if `Slave` timed out, there would be nothing it could do. 329 | Sending is disallowed because `Master` might not be listening. 330 | 331 | ## 9.3 Packet management 332 | 333 | `Master` controls the process by periodically sending packets. If it has data 334 | to send they will carry a payload; otherwise they will be empty packets serving 335 | to poll `Slave` which might have data to send. Likewise packets from `Slave` 336 | may or may not carry a payload. 337 | 338 | Any packet received by `Master` is an acknowledgement that the last packet it 339 | sent was correctly received. If no packet is received in the timeout period 340 | then either the transmitted packet or `Slave`'s response was lost. `Master` 341 | continues transmitting the same packet until it receives a response. 342 | 343 | In the case where `Slave` correctly received a PP, but its response was lost, 344 | `Slave` will subsequently receive one or more duplicates: these are recognised 345 | by virtue of the unchanged packet ID and discarded. 346 | 347 | Responses from `Slave` may or may not be PPs (depending on whether the 348 | application has queued a line for transmission). `Master` ignores non-PP 349 | packets aside from recognising the implied acknowledgement of transmission. Any 350 | payload is checked to see if it is a duplicate and, if not, appended to the 351 | receive queue. Duplicates will occur if `Master`'s `ACK` is lost. `Master` 352 | acknowledges all PPs so this sequence will end when connectivity is re- 353 | established. 354 | 355 | When `Slave` sends a PP it waits for an `ACK` packet from `Master` (if its PP 356 | was lost, the received packet will be `MSG`). `Slave` continues to respond with 357 | the same packet until `ACK` is received. 358 | 359 | # 10. Performance 360 | 361 | ## 10.1 Message integrity 362 | 363 | The script `as_nrf_test.py` transmits messages with incrementing ID's. 364 | Reception checks for non-consecutive received ID's. Under normal conditions the 365 | radio hardware ensures that received packets are correct, and the protocol 366 | ensures that no packets are lost. 367 | 368 | Testing was also done at the very limit of wireless range, outages occurring 369 | about once per minute over 10 hours. After 612 outages one instance of data 370 | corruption occurred. A packet had two bytes with single bit errors. It should 371 | be noted that this was an extreme test. With better (but still imperfect) 372 | connectivity no errors were observed in long periods of testing. 373 | 374 | ## 10.2 Latency and throughput 375 | 376 | The interval between queueing a message for transmission and its complete 377 | transmission depends on a number of factors, principally link quality and 378 | message length. Each packet holds a maximum payload of 30 bytes; long messages 379 | require successful transmission of each packet. Packet transmission takes a 380 | minimum of 10ms but potentially much longer if retransmissions occur. In the 381 | event of an outage latency can be as long as the outage duration. 382 | 383 | Throughput will also depend on the number and nature of competing user tasks. 384 | If a node sends a message, the peer checks for its arrival once per iteration 385 | of the scheduler. The worst-case latency is the sum of the worst-case latency 386 | imposed by each competing task. 387 | 388 | # 11. Design notes 389 | 390 | Both nodes delegate reception to the `iostream` mechanism. When an application 391 | issues 392 | ```python 393 | res = await sreader.readline() 394 | ``` 395 | the `ioctl` method causes the coroutine to pause until data is available. In 396 | the case of `Slave` data availability causes the `_process_packet` to update 397 | the receive queue, trigger transmission of a response packet, and terminate. 398 | 399 | In the case of `Master` a similar mechanism is used for incoming packets but 400 | in this case an `Event` is triggered which is picked up by the continuously 401 | running task `._run`. 402 | 403 | The protocol works as follows. `Master` sends a packet, then waits on the 404 | `Event`. This wait is subject to a timeout. If the `Event` is set, `Master` can 405 | be sure that `Slave` received the packet: it updates the data to be transmitted 406 | to the next packet (if any). If the timeout occurred, either `Slave` failed to 407 | receive the packet or its response was lost. In either case, `Master` 408 | retransmits the packet. 409 | 410 | Retransmission implies that sometimes duplicate packets will be received. The 411 | `Packet` class enables the recipient to detect and discard dupes by virtue of a 412 | single bit packet ID. 413 | 414 | `Slave` always responds to packet reception by immediately sending a single 415 | response packet. Consequently any packet received by `Master` is an 416 | acknowledgement of reception of `Master`'s last transmission. 417 | 418 | This does not apply in the other direction. A response packet from `Slave` 419 | containing data may be lost. In this case the next packet from `Master` would 420 | result from the timeout. Consequently `Master` sends a specific `ACK` command 421 | to acknowledge successful reception of a packet. `Slave` retransmits a packet 422 | until it receives an `ACK`. 423 | 424 | The protocol provides limited handling of power outages. If one node has an 425 | outage while the other does not, the running node may receive an incomplete 426 | `message`. The protocol detects this and discards the incomplete data. This 427 | ensures that `message` instances should always have the expected structure, 428 | but does imply message loss. 429 | 430 | # 12. Notes for protocol designers 431 | 432 | The nRF24l01 and its driver have the following "gotchas" calling for workrounds. 433 | 1. The response from `.send_done` cannot be relied upon. A result code of 2 434 | indicates transmission failure. On occasion 2 is returned when successful 435 | reception has occurred. 436 | 2. The `.send_done` response if the peer is down is to return `None` until 437 | connectivity resumes. This is reasonable but a protocol may need a timeout here. 438 | 3. If a message is sent and you then wait for a response there is a risk that 439 | the reponder sends a reply before the sender's radio is receiving. This can be 440 | averted by ensuring that a minimum delay occurs between receiving a packet and 441 | transmitting a response. 442 | 4. Under very rare conditions at the limit of range a corrupt packet may be 443 | received. I suspect this lay behind my failure to achieve 100% reliability 444 | with the old protocols. Here is the smoking gun. 445 | 446 | Data sent: 447 | ``` 448 | b'[13398, 2, [62541, 0, 45113, 38714], "abcdefgh"]\n' 449 | ``` 450 | Data received: 451 | ``` 452 | JSON error b'\xdb13398, 2, [62541< 0, 45113, 38714], "abcdefgh"]\n' 453 | ``` 454 | Note `ord('[') == 0x5b`, `ord(',') == 0x2c`, and `ord('<') == 0x3c`. In each 455 | case a 0 is received as a 1. 456 | 457 | This occurred with the official driver on default settings apart from RF 458 | channel. 459 | -------------------------------------------------------------------------------- /async/as_nrf_json.py: -------------------------------------------------------------------------------- 1 | # as_nrf_json.py Test script for as_nrf_stream 2 | 3 | # (C) Peter Hinch 2020 4 | # Released under the MIT licence 5 | 6 | import uasyncio as asyncio 7 | import ujson 8 | import time 9 | from as_nrf_stream import Master, Slave 10 | from asconfig import config_master, config_slave # Hardware configuration 11 | 12 | try: 13 | from pyb import LED 14 | except ImportError: # Non-pyboard platform: dummy LED 15 | class LED: 16 | on = lambda _ : None 17 | off = lambda _ : None 18 | toggle = lambda _ : None 19 | 20 | led = LED(1) # Red lit during an outage. 21 | green = LED(2) # Message received 22 | 23 | async def sender(device): 24 | ds = [0, 0] # Data object for transmission 25 | swriter = asyncio.StreamWriter(device, {}) 26 | while True: 27 | s = ''.join((ujson.dumps(ds), '\n')) 28 | swriter.write(s.encode()) # convert to bytes 29 | await swriter.drain() 30 | await asyncio.sleep(2) 31 | ds[0] += 1 # Record number 32 | ds[1] = device.t_last_ms() 33 | 34 | async def receiver(device): 35 | sreader = asyncio.StreamReader(device) 36 | while True: 37 | res = await sreader.readline() # Can return b'' 38 | if res: 39 | green.toggle() 40 | try: 41 | dat = ujson.loads(res) 42 | except ValueError: # Extremely rare case of data corruption. See docs. 43 | pass 44 | else: 45 | print('Received values: {:5d} {:5d}'.format(*dat)) 46 | 47 | async def fail_detect(device): 48 | while True: 49 | if device.t_last_ms() > 5000: 50 | print('Remote outage') 51 | led.on() 52 | while device.t_last_ms() > 5000: 53 | await asyncio.sleep(1) 54 | print('Remote has reconnected') 55 | led.off() 56 | await asyncio.sleep(1) 57 | 58 | 59 | async def main(master): 60 | global tstart 61 | tstart = time.time() 62 | # This line is the only *necessary* diffference between master and slave: 63 | device = Master(config_master) if master else Slave(config_slave) 64 | asyncio.create_task(sender(device)) 65 | asyncio.create_task(receiver(device)) 66 | await fail_detect(device) 67 | 68 | def test(master): 69 | try: 70 | asyncio.run(main(master)) 71 | finally: # Reset uasyncio case of KeyboardInterrupt 72 | asyncio.new_event_loop() 73 | 74 | msg = '''Test script for as_nrf_stream driver for nRF24l01 radios. 75 | On master issue 76 | as_nrf_json.test(True) 77 | On slave issue 78 | as_nrf_json.test(False) 79 | ''' 80 | print(msg) 81 | -------------------------------------------------------------------------------- /async/as_nrf_simple.py: -------------------------------------------------------------------------------- 1 | # as_nrf_simple.py Test script for as_nrf_stream 2 | 3 | # (C) Peter Hinch 2020 4 | # Released under the MIT licence 5 | 6 | import uasyncio as asyncio 7 | from as_nrf_stream import Master, Slave 8 | from asconfig import config_master, config_slave # Hardware configuration 9 | 10 | 11 | async def sender(device): 12 | swriter = asyncio.StreamWriter(device, {}) 13 | while True: 14 | swriter.write(b'Hello receiver\n') # Must be bytes 15 | await swriter.drain() 16 | await asyncio.sleep(1) 17 | 18 | async def receiver(device): 19 | sreader = asyncio.StreamReader(device) 20 | while True: 21 | res = await sreader.readline() 22 | if res: # Can return b'' 23 | print('Received:', res) 24 | 25 | async def main(master): 26 | device = Master(config_master) if master else Slave(config_slave) 27 | asyncio.create_task(receiver(device)) 28 | await sender(device) 29 | 30 | def test(master): 31 | try: 32 | asyncio.run(main(master)) 33 | finally: # Reset uasyncio case of KeyboardInterrupt 34 | asyncio.new_event_loop() 35 | 36 | msg = '''Test script for as_nrf_stream driver for nRF24l01 radios. 37 | On master issue 38 | as_nrf_simple.test(True) 39 | On slave issue 40 | as_nrf_simple.test(False) 41 | ''' 42 | print(msg) 43 | -------------------------------------------------------------------------------- /async/as_nrf_stream.py: -------------------------------------------------------------------------------- 1 | # as_nrf_stream.py uasyncio stream interface for nRF24l01 radio 2 | 3 | # (C) Peter Hinch 2020 4 | # Released under the MIT licence 5 | 6 | import io 7 | import ustruct 8 | import uasyncio as asyncio 9 | from time import ticks_ms, ticks_diff 10 | from micropython import const 11 | from nrf24l01 import NRF24L01 12 | 13 | __version__ = (0, 1, 0) 14 | 15 | # I/O interface 16 | MP_STREAM_POLL_RD = const(1) 17 | MP_STREAM_POLL_WR = const(4) 18 | MP_STREAM_POLL = const(3) 19 | MP_STREAM_ERROR = const(-1) 20 | 21 | # Command bits. Notionally LS 4 bits are command, upper 4 status 22 | MSG = const(0) # Normal packet. May carry data. 23 | ACK = const(1) # Acknowledge. May carry data. 24 | PWR = const(0x40) # Node has powered up: peer clears rxq. 25 | PID = const(0x80) # 1-bit PID. 26 | CMDMASK = const(0x0f) # LS bits is cmd 27 | 28 | # Timing 29 | SEND_DELAY = const(10) # Transmit delay (give remote time to turn round) 30 | 31 | # Optional statistics 32 | S_RX_TIMEOUTS = 0 33 | S_TX_TIMEOUTS = 1 34 | S_RX_ALL = 2 35 | S_RX_DATA = 3 36 | 37 | # Packet class creates nRF24l01 a fixed size 32-byte packet from the tx queue 38 | class Packet: 39 | def __init__(self): 40 | self._fmt = 'BB30s' # Format is cmd nbytes data 41 | 42 | class TxPacket(Packet): 43 | def __init__(self): 44 | super().__init__() 45 | self._buf = bytearray(32) 46 | self._pid = 0 47 | self._len = 0 48 | self._ploads = 0 # No. of payloads sent 49 | 50 | # Update command byte prior to transmit. Send PWR bit until 2nd update: by 51 | # then we must have had an ACK from 1st payload. 52 | def __call__(self, txcmd): 53 | self._buf[0] = txcmd | self._pid if self else txcmd 54 | # 1st packet has PWR bit set so RX clears down rxq. 55 | if self._ploads < 2: # Stop with 2nd payload. 56 | self._buf[0] |= PWR 57 | return self._buf 58 | 59 | # Update the buffer with data from the tx queue. Return the new reduced 60 | # queue instance. 61 | def update(self, txq): 62 | txd = txq[:30] # Get current data for tx up to interface maximum 63 | self._len = len(txd) 64 | if self: # Has payload 65 | self._pid ^= PID 66 | ustruct.pack_into(self._fmt, self._buf, 0, 0, self._len, txd) 67 | if self._ploads < 2: 68 | self._ploads += 1 # Payloads sent. 69 | return txq[30:] 70 | 71 | def __bool__(self): # True if packet has payload 72 | return self._len > 0 73 | 74 | class RxPacket(Packet): 75 | def __init__(self): 76 | super().__init__() 77 | self._pid = None # PID from last data packet 78 | 79 | def __call__(self, data): # Split a raw 32 byte packet into fields 80 | rxcmd, nbytes, d = ustruct.unpack(self._fmt, data) 81 | cmd = rxcmd & CMDMASK # Split rxcmd byte 82 | rxpid = rxcmd & PID 83 | pwr = bool(rxcmd & PWR) # Peer has power cycled. 84 | dupe = False # Assume success 85 | if nbytes: # Dupe detection only relevant to a data payload 86 | if (self._pid is None) or (rxpid != self._pid): 87 | # 1st packet or new PID received. Not a dupe. 88 | self._pid = rxpid # Save PID to check next packet 89 | else: 90 | dupe = True 91 | return d[:nbytes], cmd, dupe, pwr 92 | 93 | # Base class for Master and Slave 94 | class AS_NRF24L01(io.IOBase): 95 | pipes = (b'\xf0\xf0\xf0\xf7\xe1', b'\xf0\xf0\xf0\xf7\xd2') 96 | 97 | def __init__(self, config): 98 | master = int(isinstance(self, Master)) 99 | # Support gathering statistics. Delay until protocol running. 100 | self._is_running = False 101 | if config.stats: 102 | self._stats = [0, 0, 0, 0] 103 | self._do_stats = self._stat_update 104 | else: 105 | self._stats = None 106 | self._do_stats = lambda _ : None 107 | 108 | self._tx_ms = config.tx_ms # Max time master or slave can transmit 109 | radio = NRF24L01(config.spi, config.csn, config.ce, config.channel, 32) 110 | radio.open_tx_pipe(self.pipes[master ^ 1]) 111 | radio.open_rx_pipe(1, self.pipes[master]) 112 | self._radio = radio 113 | self._txq = b'' # Transmit and receive queues 114 | self._rxq = b'' 115 | self._txpkt = TxPacket() 116 | self._rxpkt = RxPacket() 117 | self._tlast = ticks_ms() # Time of last communication 118 | self._txbusy = False # Don't call ._radio.any() while sending. 119 | 120 | # **** uasyncio stream interface **** 121 | def ioctl(self, req, arg): 122 | ret = MP_STREAM_ERROR 123 | if req == MP_STREAM_POLL: 124 | ret = 0 125 | if arg & MP_STREAM_POLL_RD: 126 | if not self._txbusy and (self._radio.any() or self._rxq): 127 | ret |= MP_STREAM_POLL_RD 128 | if arg & MP_STREAM_POLL_WR: 129 | if not self._txq: 130 | ret |= MP_STREAM_POLL_WR 131 | return ret 132 | 133 | # .write is called by drain - ioctl postpones until .txq is empty 134 | def write(self, buf): 135 | self._txq = bytes(buf) # Arg is a memoryview 136 | return len(buf) # Assume eventual success. 137 | 138 | # Return a maximum of one line; ioctl postpones until .rxq is not 139 | def readline(self): # empty or if radio has a packet to read 140 | if self._radio.any(): 141 | self._process_packet() # Update ._rxq 142 | n = self._rxq.find(b'\n') + 1 143 | if not n: # Leave incomplete line on queue. 144 | return b'' 145 | res = self._rxq[:n] # Return 1st line on queue 146 | self._rxq = self._rxq[n:] 147 | return res 148 | 149 | def read(self, n): 150 | if self._radio.any(): 151 | self._process_packet() 152 | res = self._rxq[:n] 153 | self._rxq = self._rxq[n:] 154 | return res 155 | 156 | # **** private methods **** 157 | # Control radio tx/rx 158 | def _listen(self, val): 159 | if val: 160 | self._radio.start_listening() # Turn off tx 161 | self._txbusy = False 162 | else: 163 | self._txbusy = True # Prevent calls to ._process_packet 164 | self._radio.stop_listening() 165 | 166 | # Send a 32 byte buffer subject to a timeout. The value returned by 167 | # .send_done does not reliably distinguish success from failure. 168 | # Consequently ._send makes no attempt to distinguish success, fail and 169 | # timeout. This is handled by the protocol. 170 | async def _send(self, buf): 171 | self._listen(False) 172 | await asyncio.sleep_ms(SEND_DELAY) # Give remote time to start listening 173 | t = ticks_ms() 174 | self._radio.send_start(buf) # Initiate tx 175 | while self._radio.send_done() is None: # tx in progress 176 | if ticks_diff(ticks_ms(), t) > self._tx_ms: 177 | self._do_stats(S_TX_TIMEOUTS) # Optionally count instances 178 | break 179 | await asyncio.sleep_ms(0) # Await completion, timeout or failure 180 | self._listen(True) # Turn off tx 181 | 182 | # Update an individual statistic 183 | def _stat_update(self, idx): 184 | if self._stats is not None and self._is_running: 185 | self._stats[idx] += 1 186 | 187 | # **** API **** 188 | def t_last_ms(self): # Return the time (in ms) since last communication 189 | return ticks_diff(ticks_ms(), self._tlast) 190 | 191 | def stats(self): 192 | return self._stats 193 | 194 | # Master sends one ACK. If slave doesn't receive the ACK it retransmits same data. 195 | # Master discards it as a dupe and sends another ACK. 196 | class Master(AS_NRF24L01): 197 | def __init__(self, config): 198 | from uasyncio import Event 199 | super().__init__(config) 200 | self._txcmd = MSG 201 | self._pkt_rec = Event() 202 | asyncio.create_task(self._run()) 203 | 204 | async def _run(self): 205 | # Await incoming for 1.5x max slave transmit time 206 | rx_time = int(SEND_DELAY + 1.5 * self._tx_ms) / 1000 # Seem to have lost wait_for_ms 207 | while True: 208 | self._pkt_rec.clear() 209 | await self._send(self._txpkt(self._txcmd)) 210 | # Default command for next packet may be changed by ._process_packet 211 | self._txcmd = MSG 212 | try: 213 | await asyncio.wait_for(self._pkt_rec.wait(), rx_time) 214 | except asyncio.TimeoutError: 215 | self._do_stats(S_RX_TIMEOUTS) # Loop again to retransmit pkt. 216 | else: # Pkt was received so last was acknowledged. Create the next one. 217 | self._txq = self._txpkt.update(self._txq) 218 | self._is_running = True # Start gathering stats now 219 | 220 | # A packet is ready. Any response implies an ACK: slave never transmits 221 | # unsolicited messages 222 | def _process_packet(self): 223 | rxdata, _, dupe, pwrup = self._rxpkt(self._radio.recv()) 224 | if pwrup: # Slave has had a power outage 225 | self._rxq = b'' 226 | self._tlast = ticks_ms() # User outage detection 227 | self._pkt_rec.set() 228 | if rxdata: # Packet has data. ACK even if a dupe. 229 | self._do_stats(S_RX_ALL) # Optionally count instances 230 | self._txcmd = ACK 231 | if not dupe: # Add new packets to receive queue 232 | self._do_stats(S_RX_DATA) 233 | self._rxq = b''.join((self._rxq, rxdata)) 234 | 235 | class Slave(AS_NRF24L01): 236 | def __init__(self, config): 237 | super().__init__(config) 238 | self._listen(True) 239 | self._is_running = True # Start gathering stats immediately 240 | 241 | def _process_packet(self): 242 | rxdata, rxcmd, dupe, pwrup = self._rxpkt(self._radio.recv()) 243 | if pwrup: # Master has had a power outage 244 | self._rxq = b'' 245 | self._tlast = ticks_ms() 246 | if rxdata: 247 | self._do_stats(S_RX_ALL) # Optionally count instances 248 | if not dupe: # New data received. 249 | self._do_stats(S_RX_DATA) 250 | self._rxq = b''.join((self._rxq, rxdata)) 251 | # If last packet was empty or was acknowledged, get next one. 252 | if (rxcmd == ACK) or not self._txpkt: 253 | self._txq = self._txpkt.update(self._txq) # Replace txq 254 | asyncio.create_task(self._send(self._txpkt(MSG))) 255 | # Issues start_listening when done. 256 | -------------------------------------------------------------------------------- /async/as_nrf_test.py: -------------------------------------------------------------------------------- 1 | # as_nrf_test.py Test script for as_nrf_stream 2 | 3 | # (C) Peter Hinch 2020 4 | # Released under the MIT licence 5 | 6 | import uasyncio as asyncio 7 | import ujson 8 | import time 9 | from as_nrf_stream import Master, Slave 10 | from asconfig import config_master, config_slave # Hardware configuration 11 | 12 | try: 13 | from pyb import LED 14 | except ImportError: # Non-pyboard platform: dummy LED 15 | class LED: 16 | on = lambda _ : None 17 | off = lambda _ : None 18 | toggle = lambda _ : None 19 | 20 | led = LED(1) # Red lit during an outage. 21 | green = LED(2) # Message received 22 | missed = 0 # Non-sequential records 23 | outages = 0 24 | tstart = 0 25 | 26 | # Generator produces variable length strings: test for issues mapping onto 27 | # nRF24l01 fixed size 32 byte records. 28 | def gen_str(maxlen=65): 29 | while True: 30 | s = '' 31 | x = ord('a') 32 | while len(s) < maxlen: 33 | s += chr(x) 34 | yield s 35 | x = x + 1 if x < ord('z') else ord('a') 36 | 37 | async def sender(device, interval): 38 | gs = gen_str() 39 | ds = [0, 0, [], ''] # Data object for transmission 40 | swriter = asyncio.StreamWriter(device, {}) 41 | while True: 42 | s = ''.join((ujson.dumps(ds), '\n')) 43 | swriter.write(s.encode()) # convert to bytes 44 | await swriter.drain() 45 | await asyncio.sleep_ms(interval) 46 | ds[0] += 1 # Record number 47 | ds[1] = missed # Send local missed record count to remote 48 | ds[2] = device.stats() 49 | ds[3] = next(gs) # Range of possible string lengths 50 | 51 | async def receiver(device): 52 | global missed 53 | msg = 'Missed record count: {:3d} (local) {:3d} (remote).' 54 | tmsg = 'Uptime: {:7.2f}hrs Outages: {:3d}' 55 | smsg = '{} statistics. Timeouts: RX {} TX {} Received packets: All {} Non-duplicate data {}' 56 | sreader = asyncio.StreamReader(device) 57 | x = 0 58 | last = None # Record no. of last received data 59 | while True: 60 | res = await sreader.readline() # Can return b'' 61 | if res: 62 | green.toggle() 63 | try: 64 | dat = ujson.loads(res) 65 | except ValueError: # Extremely rare case of data corruption. See docs. 66 | print('JSON error', res) 67 | else: 68 | print('Received record no: {:5d} text: {:s}'.format(dat[0], dat[3])) 69 | if last is not None and (last + 1) != dat[0]: 70 | missed += 1 71 | last = dat[0] 72 | x += 1 73 | x %= 20 74 | if not x: 75 | print(msg.format(missed, dat[1])) 76 | print(tmsg.format((time.time() - tstart)/3600, outages)) 77 | if isinstance(dat[2], list): 78 | print(smsg.format('Remote', *dat[2])) 79 | local_stats = device.stats() 80 | if isinstance(local_stats, list): 81 | print(smsg.format('Local', *local_stats)) 82 | 83 | async def fail_detect(device): 84 | global outages 85 | while True: 86 | if device.t_last_ms() > 5000: 87 | outages += 1 88 | print('Remote outage') 89 | led.on() 90 | while device.t_last_ms() > 5000: 91 | await asyncio.sleep(1) 92 | print('Remote has reconnected') 93 | led.off() 94 | await asyncio.sleep(1) 95 | 96 | 97 | async def main(master): 98 | global tstart 99 | tstart = time.time() 100 | # This line is the only *necessary* diffference between master and slave: 101 | device = Master(config_master) if master else Slave(config_slave) 102 | # But script uses different periods test for timing issues: 103 | asyncio.create_task(sender(device, 2000 if master else 1777)) 104 | asyncio.create_task(receiver(device)) 105 | await fail_detect(device) 106 | 107 | def test(master): 108 | try: 109 | asyncio.run(main(master)) 110 | finally: # Reset uasyncio case of KeyboardInterrupt 111 | asyncio.new_event_loop() 112 | 113 | msg = '''Test script for as_nrf_stream driver for nRF24l01 radios. 114 | On master issue 115 | as_nrf_test.test(True) 116 | On slave issue 117 | as_nrf_test.test(False) 118 | ''' 119 | print(msg) 120 | -------------------------------------------------------------------------------- /async/asconfig.py: -------------------------------------------------------------------------------- 1 | # Config data for your hardware: adapt as required. 2 | from machine import SPI, Pin 3 | # config file instantiates a RadioSetup for each end of the link 4 | class RadioSetup: # Configuration for an nRF24L01 radio 5 | channel = 97 # Necessarily shared by both instances 6 | tx_ms = 200 # Max ms either end waits for successful transmission 7 | 8 | def __init__(self, spi, csn, ce, stats=False): 9 | self.spi = spi 10 | self.csn = csn 11 | self.ce = ce 12 | self.stats = stats 13 | 14 | # Note: gathering statistics. as_nrf_test will display them. 15 | config_testbox = RadioSetup(SPI(1), Pin('X5'), Pin('Y11'), True) # My testbox 16 | config_v1 = RadioSetup(SPI(1), Pin('X5'), Pin('X4'), True) # V1 Micropower PCB 17 | config_v2 = RadioSetup(SPI(1), Pin('X5'), Pin('X2'), True) # V2 Micropower PCB with SD card 18 | config_master = config_v1 19 | #config_slave = config_v2 20 | config_slave = config_testbox 21 | -------------------------------------------------------------------------------- /radio-fast/README.md: -------------------------------------------------------------------------------- 1 | The radio_fast module 2 | --------------------- 3 | 4 | The test scripts which use `uasyncio` have been updated for `uasyncio` V3 and 5 | require this version. To ensure this, use a daily build of firmware or a 6 | release build after V1.2. 7 | 8 | This module uses the nRF24l01+ chip and MicroPython driver to create a wireless link between two points. 9 | Wherease the radio_pickle module is designed for ease of use and support of dynamically variable data, 10 | radio_fast is optimised for speed and range. The cost is a restriction to a fixed record length determined 11 | at the design time of your application. Further, a grasp of the Python ``struct`` module is required to 12 | customise the message format for the application. The file ``config.py`` is intended for adaptation by 13 | the user to define message formats and hardware onfiguration. 14 | 15 | The payoff is a typical turnround time of 4mS for exchanging 12 byte messages. This can potentially be 16 | improved - at some cost in range - by using one of the nRF24l01's high speed modes. 17 | 18 | [Back](../README.md) 19 | 20 | Introduction 21 | ------------ 22 | 23 | The first bit of advice to anyone considering using this chip is to buy a decent quality breakout board. 24 | Many cheap ones work poorly, if at all. I've used Sparkfun boards to good effect. 25 | 26 | The nRF24l01+ is a highly versatile device, but for many applications only a subset of its capabilities 27 | is required. In particular significant simplifications are possible if you need to communicate between 28 | two devices only, and can adopt "half duplex" communications. A half duplex link is one where one device acts 29 | as a master and the other a slave. Only the master is permitted to send unsolicited messages: if it wants 30 | a response from the slave it sends a request and awaits the response. The slave must always respond. This 31 | restriction ensures that you don't encounter the case where both ends transmit at the same time, simplifying 32 | the programming. Many applications fit this model naturally, notably remote control/monitoring and data logging. 33 | 34 | The nRF24l01 aims to provide "reliable" communication in a manner largely hidden from the user. Here the 35 | term "reliable" means that messages will always have the correct contents. It does not mean that a message 36 | will always get through: the nature of wireless communication precludes this. The transmitter may have 37 | moved out of range of the receiver or interference may make communication impossible - "The falcon 38 | cannot hear the falconer...". A communications protocol is required to ensure that the master and slave 39 | are correctly synchronised in the presence of errors and regain synchronisation after a timeout. It must 40 | also provide feedback to the user program as to whether communications were successful. This driver 41 | implements such a protocol. 42 | 43 | "Reliable" communications are achieved by the nRF24l01 as follows. When one device transmits a message it 44 | includes a CRC code. The receiver checks this, if it matches the data it's received it sends back an 45 | acknowledgement, otherwise it sends back an error code. In the latter case the transmission and checking 46 | process is repeated. This continues until either the data is received successfully or a defined number of 47 | attempts is reached. All this happens entirely without programmer intervention. However it does mean that 48 | sending a message takes a variable length of time depending on factors such as distance and interference. 49 | 50 | Dependencies 51 | ------------ 52 | 53 | The library requires the nrf24l01.py driver from the source tree. 54 | [nrf24l01.py](https://github.com/micropython/micropython-lib/blob/master/micropython/drivers/radio/nrf24l01/nrf24l01.py) 55 | 56 | Example code 57 | ------------ 58 | 59 | The following samples show typical usage. To run these you need to edit ``config.py`` which 60 | defines hardware connections and message formats. 61 | 62 | ```python 63 | import pyb, radio_fast 64 | from config import master_config, slave_config, FromMaster, ToMaster 65 | def test_slave(): 66 | s = radio_fast.Slave(slave_config) 67 | send_msg = ToMaster() 68 | while True: 69 | rx_msg = s.exchange(send_msg, block = True) # Wait for master 70 | if rx_msg is not None: 71 | print(rx_msg.i0) 72 | else: 73 | print('Timeout') 74 | send_msg.i0 += 1 75 | ``` 76 | 77 | ```python 78 | import pyb, radio_fast 79 | from config import master_config, slave_config, FromMaster, ToMaster 80 | def test_master(): 81 | m = radio_fast.Master(master_config) 82 | send_msg = FromMaster() 83 | while True: 84 | rx_msg = m.exchange(send_msg) 85 | if rx_msg is not None: 86 | print(rx_msg.i0) 87 | else: 88 | print('Timeout') 89 | send_msg.i0 += 1 90 | pyb.delay(1000) 91 | ``` 92 | 93 | Module radio_fast.py 94 | -------------------- 95 | 96 | Class Master 97 | ------------ 98 | 99 | The class hierarchy is Master-RadioFast-NRF24L01 (in nrf24l01.py). 100 | 101 | Constructor 102 | This takes one mandatory argument, a ``RadioConfig`` object. This (defined in ``msg.py`` and 103 | instantiated in ``config.py``) details the connections to the nRF24l01 module and channel in use. 104 | 105 | method exchange() 106 | Argument: A ``FromMaster`` message object for transmission. The method Attempts to send it to the 107 | slave and returns a response. 108 | 109 | Results: 110 | On success, returns a ``ToMaster`` message instance with contents unpacked from the byte stream. 111 | On timeout returns None. 112 | 113 | Class Slave 114 | ----------- 115 | 116 | The class hierarchy is Slave-RadioFast-NRF24L01 (in nrf24l01.py). 117 | 118 | Constructor: 119 | This takes one mandatory argument, a ``RadioConfig`` object. This (defined in ``msg.py`` and 120 | instantiated in ``config.py``) details the connections to the nRF24l01 module and channel in use. 121 | 122 | method exchange() 123 | Arguments: 124 | 1. A ``ToMaster`` message object for transmission. 125 | 2. ``block`` Boolean, default True. Determines whether ``exchange()`` waits for a transmission 126 | from the master (blocking transfer) or returns immediately with a status. If a message is received 127 | from the master it is unpacked. 128 | 129 | Results: 130 | On success, returns an unpacked ``FromMaster`` message object. 131 | On timeout returns None. 132 | If no data has been sent (nonblocking read only) returns False. 133 | 134 | Class RadioFast 135 | --------------- 136 | 137 | This is subclassed from NRF24L01 in nrf24l01.py. It contains one user configurable class variable ``timeout`` 138 | which determines the maximum time ``Master`` or ``Slave`` instances will wait to send or receive a message. 139 | In practice with short messages the radio times out in less than the default of 100mS, but this variable aims 140 | to set an approximate maximum. 141 | 142 | Class RadioConfig 143 | ----------------- 144 | 145 | This should be fairly self-explanatory: it provides a means of defining physical connections to the 146 | nRF24l01 and the channel number (the latter is a class variable as its value must be identical for both 147 | ends of the link). 148 | 149 | Module config.py 150 | ---------------- 151 | 152 | This module is intended to be modified by the user. It defines the message format for messages from master 153 | to slave and from slave to master. As configured three integers are sent in either direction - in practice 154 | these message formats will be adjusted to suit the application. Both messages must pack to the same length: 155 | if neccessary use redundant data items to achieve this. An assertion failure will be raised when a message 156 | is instantiated if this condition is not met. Tha packed message length must be <= 32 bytes: an 157 | assertion failure will occur otherwise when a radio is instantiated. 158 | 159 | It also implements ``RadioConfig`` instances corresponding to the hardware in use. 160 | 161 | Classes FromMaster and ToMaster 162 | ------------------------------- 163 | 164 | These define the message contents for messages sent from master to slave and vice versa. To adapt these 165 | for an application the instance variables, ``fmt`` format string, ``pack()`` and ``unpack()`` methods 166 | will need to be adjusted. Message formats may differ so long as their packed sizes are identical and in 167 | range 1 to 32 bytes. 168 | 169 | Module msg.py 170 | ------------- 171 | 172 | This defines the ``RadioConfig`` and ``msg`` classes used by ``config.py``. 173 | 174 | Performance 175 | ----------- 176 | 177 | With messages of 12 bytes and under good propagation conditions a message exchange takes about 4mS. Where 178 | timeouts occur these take about 25mS. 179 | 180 | Channels 181 | -------- 182 | 183 | The RF frequency is determined by the ``RadioSetup`` instance as described above. The ``channel`` value maps 184 | onto frequency by means of the following formula: 185 | freq = 2400 + channel [MHz] 186 | The maximum channel no. is 125. The ISM (Industrial, Scientific and Medical) band covers 2400-2500MHz and is 187 | licensed for use in most jurisdictions. It is, however, shared with many other devices including WiFi, Bluetooth 188 | and microwave ovens. WiFi and Bluetooth generally cut off at 2.4835GHz so channels 85-99 should avoid the risk 189 | mutual interefrence. Note that frequencies of 2.5GHz and above are not generally licensed for use. I am neither 190 | a lawyer nor an expert on spectrum allocation: check local regulations before using these devices. 191 | 192 | FILES 193 | ----- 194 | 195 | ``radio_fast.py`` The driver. 196 | ``msg.py`` Classes used by ``config.py`` 197 | ``config.py`` Example config module. Adapt for your wiring and message formats. 198 | ``tests.py`` Test programs to run on any Pyboard/nRF24l01. 199 | ``rftest.py``, nbtest.py Test programs for my own specific hardware. These illustrate use with an LCD display and microthreading 200 | scheduler. The latter tests slave nonblocking reads. 201 | ``README.md`` This file 202 | -------------------------------------------------------------------------------- /radio-fast/config.py: -------------------------------------------------------------------------------- 1 | # config.py Configuration file for radio_fast.py 2 | # This contains the user defined configuration 3 | # (C) Copyright Peter Hinch 2017 4 | # Released under the MIT licence 5 | import ustruct 6 | from msg import RadioConfig, msg 7 | 8 | # Choose a channel (or accept default 99) 9 | #RadioConfig.channel = 99 10 | # Modify for your hardware 11 | testbox_config = RadioConfig(spi_no = 1, csn_pin = 'X5', ce_pin = 'Y11') # My testbox 12 | v1_config = RadioConfig(spi_no = 1, csn_pin = 'X5', ce_pin = 'X4') # V1 Micropower PCB 13 | v2_config = RadioConfig(spi_no = 1, csn_pin = 'X5', ce_pin = 'X2') # V2 Micropower PCB with SD card 14 | master_config = v1_config 15 | slave_config = v2_config 16 | 17 | # For both messages need to alter fmt, instance variables, pack() and unpack() methods, to suit application. 18 | # Both messages must pack to the same length otherwise an assertion failure will occur at runtime. 19 | class FromMaster(msg): 20 | fmt = 'iii' 21 | def __init__(self): 22 | super().__init__(FromMaster, ToMaster) 23 | self.i0 = 0 24 | self.i1 = 0 25 | self.i2 = 0 26 | 27 | def pack(self): 28 | ustruct.pack_into(self.fmt, self.buf, 0, self.i0, self.i1, self.i2) 29 | return self.buf 30 | 31 | def unpack(self): 32 | self.i0, self.i1, self.i2 = ustruct.unpack(self.fmt, self.buf) 33 | return self 34 | 35 | class ToMaster(msg): 36 | fmt = 'iii' 37 | def __init__(self): 38 | super().__init__(FromMaster, ToMaster) 39 | self.i0 = 0 40 | self.i1 = 0 41 | self.i2 = 0 42 | 43 | def pack(self): 44 | ustruct.pack_into(self.fmt, self.buf, 0, self.i0, self.i1, self.i2) 45 | return self.buf 46 | 47 | def unpack(self): 48 | self.i0, self.i1, self.i2 = ustruct.unpack(self.fmt, self.buf) 49 | return self 50 | -------------------------------------------------------------------------------- /radio-fast/msg.py: -------------------------------------------------------------------------------- 1 | # msg.py Message base class for radio-fast protocol 2 | 3 | import ustruct 4 | 5 | class RadioConfig(object): # Configuration for an nRF24L01 radio 6 | channel = 99 # Necessarily shared by master and slave instances. 7 | def __init__(self, *, spi_no, csn_pin, ce_pin):# May differ between instances 8 | self.spi_no = spi_no 9 | self.ce_pin = ce_pin 10 | self.csn_pin = csn_pin 11 | 12 | # Message base class. 13 | class msg(object): 14 | errmsg = 'config.py: ToMaster and FromMaster messages have mismatched payload sizes/formats' 15 | def __init__(self, cls1, cls2): 16 | self.buf = bytearray(cls1.payload_size()) 17 | self.mvbuf = memoryview(self.buf) 18 | assert cls1.payload_size() == cls2.payload_size(), self.errmsg 19 | 20 | def store(self, data): 21 | self.mvbuf[:] = data 22 | 23 | @classmethod 24 | def payload_size(cls): 25 | return ustruct.calcsize(cls.fmt) # Size of subclass packed data 26 | -------------------------------------------------------------------------------- /radio-fast/nbtest.py: -------------------------------------------------------------------------------- 1 | # nbtest.py Test nonblocking read on slave. 2 | 3 | # Author: Peter Hinch 4 | # Copyright Peter Hinch 2020 Released under the MIT license 5 | 6 | # Requires uasyncio V3 and as_drivers directory (plus contents) from 7 | # https://github.com/peterhinch/micropython-async/tree/master/v3 8 | 9 | from time import ticks_ms, ticks_diff 10 | import uasyncio as asyncio 11 | from as_drivers.hd44780.alcd import LCD, PINLIST # Library supporting Hitachi LCD module 12 | import radio_fast as rf 13 | from config import FromMaster, ToMaster, testbox_config, v2_config # Configs for my hardware 14 | 15 | st = ''' 16 | On slave (with LCD) issue nbtest.test(False) 17 | On master issue nbtest.test() 18 | ''' 19 | 20 | print(st) 21 | 22 | async def run_master(): 23 | m = rf.Master(v2_config) # Master runs on V2 PCB with SD card 24 | send_msg = FromMaster() 25 | while True: 26 | result = m.exchange(send_msg) 27 | if result is not None: 28 | print(result.i0) 29 | else: 30 | print('Timeout') 31 | send_msg.i0 += 1 32 | await asyncio.sleep(1) 33 | 34 | async def slave(lcd): 35 | await asyncio.sleep(0) 36 | s = rf.Slave(testbox_config) # Slave on testbox 37 | send_msg = ToMaster() 38 | while True: 39 | start = ticks_ms() 40 | result = None 41 | while not s.any(): # Wait for master to send 42 | await asyncio.sleep(0) 43 | t = ticks_diff(ticks_ms(), start) 44 | if t > 4000: 45 | break 46 | else: # Master has sent 47 | start = ticks_ms() 48 | result = s.exchange(send_msg) 49 | t = ticks_diff(ticks_ms(), start) 50 | if result is None: 51 | lcd[0] = 'Timeout' 52 | elif result: 53 | lcd[0] = str(result.i0) 54 | lcd[1] = 't = {}mS'.format(t) 55 | await asyncio.sleep(0) 56 | send_msg.i0 += 1 57 | 58 | def test(master=True): 59 | lcd = LCD(PINLIST, cols = 24) 60 | try: 61 | asyncio.run(run_master() if master else slave(lcd)) 62 | except KeyboardInterrupt: 63 | print('Interrupted') 64 | finally: 65 | asyncio.new_event_loop() 66 | -------------------------------------------------------------------------------- /radio-fast/radio_fast.py: -------------------------------------------------------------------------------- 1 | # radio_fast.py A simple nRF24L01 point-to-point half duplex protocol for fixed length messages. 2 | # (C) Copyright Peter Hinch 2016-2020 3 | # Released under the MIT licence 4 | 5 | from machine import SPI, Pin 6 | from time import ticks_diff, ticks_ms 7 | from nrf24l01 import NRF24L01, POWER_3, SPEED_250K 8 | from config import FromMaster, ToMaster # User defined message classes and hardware config 9 | 10 | class RadioFast(NRF24L01): 11 | pipes = (b'\xf0\xf0\xf0\xf0\xe1', b'\xf0\xf0\xf0\xf0\xd2') 12 | timeout = 100 13 | def __init__(self, master, config): 14 | super().__init__(SPI(config.spi_no), Pin(config.csn_pin), Pin(config.ce_pin), config.channel, FromMaster.payload_size()) 15 | if master: 16 | self.open_tx_pipe(RadioFast.pipes[0]) 17 | self.open_rx_pipe(1, RadioFast.pipes[1]) 18 | else: 19 | self.open_tx_pipe(RadioFast.pipes[1]) 20 | self.open_rx_pipe(1, RadioFast.pipes[0]) 21 | self.set_power_speed(POWER_3, SPEED_250K) # Best range for point to point links 22 | self.start_listening() 23 | 24 | def get_latest_msg(self, msg_rx): 25 | if self.any(): 26 | while self.any(): # Discard any old buffered messages 27 | data = self.recv() 28 | msg_rx.store(data) # Can raise OSError but only as a result of programming error 29 | return True 30 | return False 31 | 32 | def sendbuf(self, msg_send): 33 | self.stop_listening() 34 | try: 35 | self.send(msg_send.pack(), timeout = self.timeout) 36 | except OSError: # Sometimes throws even when successful. 37 | self.start_listening() 38 | return False 39 | self.start_listening() 40 | return True 41 | 42 | def await_message(self, msg_rx): 43 | start = ticks_ms() 44 | while ticks_diff(ticks_ms(), start) <= self.timeout: 45 | try: 46 | if self.get_latest_msg(msg_rx): 47 | return True 48 | except OSError: 49 | pass # Bad message length. Try again. 50 | return False # Timeout 51 | 52 | class Master(RadioFast): 53 | def __init__(self, config): 54 | super().__init__(True, config) 55 | 56 | def exchange(self, msg_send): # Call when transmit-receive required. 57 | msg_rx = ToMaster() 58 | if self.sendbuf(msg_send): 59 | if self.await_message(msg_rx): 60 | self.stop_listening() 61 | return msg_rx.unpack() 62 | self.stop_listening() 63 | return None # Timeout 64 | 65 | class Slave(RadioFast): 66 | def __init__(self, config): 67 | super().__init__(False, config) 68 | 69 | def exchange(self, msg_send, block = True): 70 | if block: # Blocking read returns message on success, 71 | while not self.any(): # None on timeout 72 | pass 73 | else: # Nonblocking read returns message on success, 74 | if not self.any(): # None on timeout, False on no data 75 | return False 76 | msg_rx = FromMaster() 77 | if self.await_message(msg_rx): 78 | self.sendbuf(msg_send) # Sometimes returns False when it has actually worked. 79 | return msg_rx.unpack() # In this instance don't discard received data. 80 | return None # Timeout 81 | -------------------------------------------------------------------------------- /radio-fast/rftest.py: -------------------------------------------------------------------------------- 1 | # Tests for radio-fast module. 2 | 3 | # Author: Peter Hinch 4 | # Copyright Peter Hinch 2020 Released under the MIT license 5 | 6 | # Requires uasyncio V3 and as_drivers directory (plus contents) from 7 | # https://github.com/peterhinch/micropython-async/tree/master/v3 8 | 9 | from time import ticks_ms, ticks_diff 10 | import uasyncio as asyncio 11 | import radio_fast as rf 12 | from as_drivers.hd44780.alcd import LCD, PINLIST # Library supporting Hitachi LCD module 13 | from config import FromMaster, ToMaster, testbox_config, v2_config # Configs for my hardware 14 | 15 | st = ''' 16 | On master (with LCD) issue rftest.test() 17 | On slave issue rftest.test(False) 18 | ''' 19 | 20 | print(st) 21 | 22 | async def slave(): 23 | # power control done in main.py 24 | s = rf.Slave(v2_config) # Slave runs on V2 PCB (with SD card) 25 | send_msg = ToMaster() 26 | while True: 27 | await asyncio.sleep(0) 28 | result = s.exchange(send_msg) # Wait for master 29 | if result is not None: 30 | print(result.i0) 31 | else: 32 | print('Timeout') 33 | send_msg.i0 += 1 34 | 35 | async def run_master(lcd): 36 | await asyncio.sleep(0) 37 | m = rf.Master(testbox_config) 38 | send_msg = FromMaster() 39 | while True: 40 | start = ticks_ms() 41 | result = m.exchange(send_msg) 42 | t = ticks_diff(ticks_ms(), start) 43 | lcd[1] = 't = {}mS'.format(t) 44 | if result is not None: 45 | lcd[0] = str(result.i0) 46 | else: 47 | lcd[0] = 'Timeout' 48 | await asyncio.sleep(1) 49 | send_msg.i0 += 1 50 | 51 | def test(master=True): 52 | lcd = LCD(PINLIST, cols = 24) 53 | try: 54 | asyncio.run(run_master(lcd) if master else slave()) 55 | except KeyboardInterrupt: 56 | print('Interrupted') 57 | finally: 58 | asyncio.new_event_loop() 59 | -------------------------------------------------------------------------------- /radio-fast/tests.py: -------------------------------------------------------------------------------- 1 | # Generic Tests for radio-fast module. 2 | # Modify config.py to provide master_config and slave_config for your hardware. 3 | import pyb, radio_fast 4 | from config import master_config, slave_config, FromMaster, ToMaster 5 | 6 | def test_master(): 7 | m = radio_fast.Master(master_config) 8 | send_msg = FromMaster() 9 | while True: 10 | result = m.exchange(send_msg) 11 | if result is not None: 12 | print(result.i0) 13 | else: 14 | print('Timeout') 15 | send_msg.i0 += 1 16 | pyb.delay(1000) 17 | 18 | def test_slave(): 19 | s = radio_fast.Slave(slave_config) 20 | send_msg = ToMaster() 21 | while True: 22 | result = s.exchange(send_msg) # Wait for master 23 | if result is not None: 24 | print(result.i0) 25 | else: 26 | print('Timeout') 27 | send_msg.i0 += 1 28 | --------------------------------------------------------------------------------