diff --git a/.github/workflows/finish_pylint.py b/.github/workflows/finish_pylint.py new file mode 100644 index 0000000..cddac04 --- /dev/null +++ b/.github/workflows/finish_pylint.py @@ -0,0 +1,4 @@ +import sys +import os + +sys.exit(os.environ.get("linting_status", "failed") == "failed") diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml new file mode 100644 index 0000000..6635e75 --- /dev/null +++ b/.github/workflows/pylint.yml @@ -0,0 +1,36 @@ +name: pylint + +on: [push] + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.9 + uses: actions/setup-python@v2 + with: + python-version: 3.9 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pylint + - name: Analysing the code with pylint + id: pylint + run: python3 .github/workflows/run_pylint.py + - name: Generating badge + uses: schneegans/dynamic-badges-action@v1.1.0 + with: + auth: ${{ secrets.GIST_SECRET }} + gistID: 385e5dc0d1b4f63dffa3de2db8695a69 + filename: test.json + label: pylint + message: ${{ steps.pylint.outputs.rating }} + color: ${{ steps.pylint.outputs.color }} + namedLogo: Python + style: flat + - name: Return final success code + env: + linting_status: ${{ steps.pylint.outputs.linting_status }} + run: python3 .github/workflows/finish_pylint.py diff --git a/.github/workflows/run_pylint.py b/.github/workflows/run_pylint.py new file mode 100644 index 0000000..419808d --- /dev/null +++ b/.github/workflows/run_pylint.py @@ -0,0 +1,26 @@ +import sys +from glob import glob +from pylint.lint import Run + +# Define thresholds: <3=red, <6=orange <8=yellow <9.5=green <10=brightgreen +thresholds = {3: 'red', + 6: 'orange', + 8: 'yellow', + 9.5: 'green', + 10: 'brightgreen'} + +results = Run(['--disable=import-error,unused-wildcard-import,wildcard-import,line-too-long,invalid-name,missing-module-docstring,too-many-lines,too-many-instance-attributes,consider-using-f-string,too-many-locals,too-few-public-methods,too-many-branches,duplicate-code', 'RFM69'] + glob("tests/*.py") + glob("examples/*.py"), do_exit=False) + +if results.linter.stats["fatal"] + results.linter.stats["error"] + results.linter.stats["warning"] > 0: + print("##[set-output name=rating]failing!") + print("##[set-output name=color]red") + print("##[set-output name=linting_status]failed") +else: + rating = results.linter.stats['global_note'] + print("##[set-output name=rating]{:.2f}".format(rating)) + for value in thresholds.keys(): + if rating <= value: + print("##[set-output name=color]{}".format(thresholds[value])) + break + print("##[set-output name=linting_status]passed") +sys.exit(0) diff --git a/.gitignore b/.gitignore index dce56ae..a4b25f6 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,10 @@ __pycache__/ *.DS_Store docs/build *.vscode +\#*\# +*~ +.coverage +.\#* # Setuptools distribution folder. /dist/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 89e06ef..1d002f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.4.0 +- Made the Radio class threadsafe, and added threadsafe methods for accessing packets +- Added testing for the threadsafe methods +- Added pylinting and made some cosmetic changes to get a good pylint score +- Added coverage testing via coveralls.io, and instructions for doing so + ## 0.3.0 - Added support for sendListenModeBurst - Made tests more configurable diff --git a/README.md b/README.md index 092597c..e4ddd84 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +[![Documentation Status](https://readthedocs.org/projects/rpi-rfm69/badge/?version=latest)](https://rpi-rfm69.readthedocs.io/en/latest/?badge=latest) +[![pylint Status](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/jgillula/385e5dc0d1b4f63dffa3de2db8695a69/raw/test.json)](https://github.com/jgillula/rpi-rfm69/actions/workflows/pylint.yml) +[![Coverage Status](https://coveralls.io/repos/github/jgillula/rpi-rfm69/badge.svg)](https://coveralls.io/github/jgillula/rpi-rfm69) + # RFM69 Radio interface for the Raspberry Pi This package provides a Python wrapper of the [LowPowerLabs RFM69 library](https://github.com/LowPowerLab/RFM69) and is largely based on the work of [Eric Trombly](https://github.com/etrombly/RFM69) who ported the library from C. diff --git a/RFM69/config.py b/RFM69/config.py index 5b6b788..bde4e97 100644 --- a/RFM69/config.py +++ b/RFM69/config.py @@ -1,61 +1,65 @@ from .registers import * -frfMSB = {RF69_315MHZ: RF_FRFMSB_315, RF69_433MHZ: RF_FRFMSB_433, RF69_868MHZ: RF_FRFMSB_868, RF69_915MHZ: RF_FRFMSB_915} -frfMID = {RF69_315MHZ: RF_FRFMID_315, RF69_433MHZ: RF_FRFMID_433, RF69_868MHZ: RF_FRFMID_868, RF69_915MHZ: RF_FRFMID_915} -frfLSB = {RF69_315MHZ: RF_FRFLSB_315, RF69_433MHZ: RF_FRFLSB_433, RF69_868MHZ: RF_FRFLSB_868, RF69_915MHZ: RF_FRFLSB_915} +frfMSB = {RF69_315MHZ: RF_FRFMSB_315, RF69_433MHZ: RF_FRFMSB_433, + RF69_868MHZ: RF_FRFMSB_868, RF69_915MHZ: RF_FRFMSB_915} +frfMID = {RF69_315MHZ: RF_FRFMID_315, RF69_433MHZ: RF_FRFMID_433, + RF69_868MHZ: RF_FRFMID_868, RF69_915MHZ: RF_FRFMID_915} +frfLSB = {RF69_315MHZ: RF_FRFLSB_315, RF69_433MHZ: RF_FRFLSB_433, + RF69_868MHZ: RF_FRFLSB_868, RF69_915MHZ: RF_FRFLSB_915} +# pylint: disable=missing-function-docstring def get_config(freqBand, networkID): return { - 0x01: [REG_OPMODE, RF_OPMODE_SEQUENCER_ON | RF_OPMODE_LISTEN_OFF | RF_OPMODE_STANDBY], - #no shaping - 0x02: [REG_DATAMODUL, RF_DATAMODUL_DATAMODE_PACKET | RF_DATAMODUL_MODULATIONTYPE_FSK | RF_DATAMODUL_MODULATIONSHAPING_00], - #default:4.8 KBPS - 0x03: [REG_BITRATEMSB, RF_BITRATEMSB_55555], - 0x04: [REG_BITRATELSB, RF_BITRATELSB_55555], - #default:5khz, (FDEV + BitRate/2 <= 500Khz) - 0x05: [REG_FDEVMSB, RF_FDEVMSB_50000], - 0x06: [REG_FDEVLSB, RF_FDEVLSB_50000], + 0x01: [REG_OPMODE, RF_OPMODE_SEQUENCER_ON | RF_OPMODE_LISTEN_OFF | RF_OPMODE_STANDBY], + #no shaping + 0x02: [REG_DATAMODUL, RF_DATAMODUL_DATAMODE_PACKET | RF_DATAMODUL_MODULATIONTYPE_FSK | RF_DATAMODUL_MODULATIONSHAPING_00], + #default:4.8 KBPS + 0x03: [REG_BITRATEMSB, RF_BITRATEMSB_55555], + 0x04: [REG_BITRATELSB, RF_BITRATELSB_55555], + #default:5khz, (FDEV + BitRate/2 <= 500Khz) + 0x05: [REG_FDEVMSB, RF_FDEVMSB_50000], + 0x06: [REG_FDEVLSB, RF_FDEVLSB_50000], - 0x07: [REG_FRFMSB, frfMSB[freqBand]], - 0x08: [REG_FRFMID, frfMID[freqBand]], - 0x09: [REG_FRFLSB, frfLSB[freqBand]], + 0x07: [REG_FRFMSB, frfMSB[freqBand]], + 0x08: [REG_FRFMID, frfMID[freqBand]], + 0x09: [REG_FRFLSB, frfLSB[freqBand]], - # looks like PA1 and PA2 are not implemented on RFM69W, hence the max output power is 13dBm - # +17dBm and +20dBm are possible on RFM69HW - # +13dBm formula: Pout=-18+OutputPower (with PA0 or PA1**) - # +17dBm formula: Pout=-14+OutputPower (with PA1 and PA2)** - # +20dBm formula: Pout=-11+OutputPower (with PA1 and PA2)** and high power PA settings (section 3.3.7 in datasheet) - #0x11: [REG_PALEVEL, RF_PALEVEL_PA0_ON | RF_PALEVEL_PA1_OFF | RF_PALEVEL_PA2_OFF | RF_PALEVEL_OUTPUTPOWER_11111], - #over current protection (default is 95mA) - #0x13: [REG_OCP, RF_OCP_ON | RF_OCP_TRIM_95], + # looks like PA1 and PA2 are not implemented on RFM69W, hence the max output power is 13dBm + # +17dBm and +20dBm are possible on RFM69HW + # +13dBm formula: Pout=-18+OutputPower (with PA0 or PA1**) + # +17dBm formula: Pout=-14+OutputPower (with PA1 and PA2)** + # +20dBm formula: Pout=-11+OutputPower (with PA1 and PA2)** and high power PA settings (section 3.3.7 in datasheet) + #0x11: [REG_PALEVEL, RF_PALEVEL_PA0_ON | RF_PALEVEL_PA1_OFF | RF_PALEVEL_PA2_OFF | RF_PALEVEL_OUTPUTPOWER_11111], + #over current protection (default is 95mA) + #0x13: [REG_OCP, RF_OCP_ON | RF_OCP_TRIM_95], - # RXBW defaults are { REG_RXBW, RF_RXBW_DCCFREQ_010 | RF_RXBW_MANT_24 | RF_RXBW_EXP_5} (RxBw: 10.4khz) - #//(BitRate < 2 * RxBw) - 0x19: [REG_RXBW, RF_RXBW_DCCFREQ_010 | RF_RXBW_MANT_16 | RF_RXBW_EXP_2], - #for BR-19200: //* 0x19 */ { REG_RXBW, RF_RXBW_DCCFREQ_010 | RF_RXBW_MANT_24 | RF_RXBW_EXP_3 }, - #DIO0 is the only IRQ we're using - 0x25: [REG_DIOMAPPING1, RF_DIOMAPPING1_DIO0_01], - #must be set to dBm = (-Sensitivity / 2) - default is 0xE4=228 so -114dBm - 0x29: [REG_RSSITHRESH, 220], - #/* 0x2d */ { REG_PREAMBLELSB, RF_PREAMBLESIZE_LSB_VALUE } // default 3 preamble bytes 0xAAAAAA - 0x2e: [REG_SYNCCONFIG, RF_SYNC_ON | RF_SYNC_FIFOFILL_AUTO | RF_SYNC_SIZE_2 | RF_SYNC_TOL_0], - #attempt to make this compatible with sync1 byte of RFM12B lib - 0x2f: [REG_SYNCVALUE1, 0x2D], - #NETWORK ID - 0x30: [REG_SYNCVALUE2, networkID], - 0x37: [REG_PACKETCONFIG1, RF_PACKET1_FORMAT_VARIABLE | RF_PACKET1_DCFREE_OFF | - RF_PACKET1_CRC_ON | RF_PACKET1_CRCAUTOCLEAR_ON | RF_PACKET1_ADRSFILTERING_OFF], - #in variable length mode: the max frame size, not used in TX - 0x38: [REG_PAYLOADLENGTH, 66], - #* 0x39 */ { REG_NODEADRS, nodeID }, //turned off because we're not using address filtering - #TX on FIFO not empty - 0x3C: [REG_FIFOTHRESH, RF_FIFOTHRESH_TXSTART_FIFONOTEMPTY | RF_FIFOTHRESH_VALUE], - #RXRESTARTDELAY must match transmitter PA ramp-down time (bitrate dependent) - 0x3d: [REG_PACKETCONFIG2, RF_PACKET2_RXRESTARTDELAY_2BITS | RF_PACKET2_AUTORXRESTART_ON | RF_PACKET2_AES_OFF], - #for BR-19200: //* 0x3d */ { REG_PACKETCONFIG2, RF_PACKET2_RXRESTARTDELAY_NONE | RF_PACKET2_AUTORXRESTART_ON | RF_PACKET2_AES_OFF }, //RXRESTARTDELAY must match transmitter PA ramp-down time (bitrate dependent) - #* 0x6F */ { REG_TESTDAGC, RF_DAGC_CONTINUOUS }, // run DAGC continuously in RX mode - # run DAGC continuously in RX mode, recommended default for AfcLowBetaOn=0 - 0x6F: [REG_TESTDAGC, RF_DAGC_IMPROVED_LOWBETA0], - 0x00: [255, 0] - } \ No newline at end of file + # RXBW defaults are { REG_RXBW, RF_RXBW_DCCFREQ_010 | RF_RXBW_MANT_24 | RF_RXBW_EXP_5} (RxBw: 10.4khz) + #//(BitRate < 2 * RxBw) + 0x19: [REG_RXBW, RF_RXBW_DCCFREQ_010 | RF_RXBW_MANT_16 | RF_RXBW_EXP_2], + #for BR-19200: //* 0x19 */ { REG_RXBW, RF_RXBW_DCCFREQ_010 | RF_RXBW_MANT_24 | RF_RXBW_EXP_3 }, + #DIO0 is the only IRQ we're using + 0x25: [REG_DIOMAPPING1, RF_DIOMAPPING1_DIO0_01], + #must be set to dBm = (-Sensitivity / 2) - default is 0xE4=228 so -114dBm + 0x29: [REG_RSSITHRESH, 220], + #/* 0x2d */ { REG_PREAMBLELSB, RF_PREAMBLESIZE_LSB_VALUE } // default 3 preamble bytes 0xAAAAAA + 0x2e: [REG_SYNCCONFIG, RF_SYNC_ON | RF_SYNC_FIFOFILL_AUTO | RF_SYNC_SIZE_2 | RF_SYNC_TOL_0], + #attempt to make this compatible with sync1 byte of RFM12B lib + 0x2f: [REG_SYNCVALUE1, 0x2D], + #NETWORK ID + 0x30: [REG_SYNCVALUE2, networkID], + 0x37: [REG_PACKETCONFIG1, RF_PACKET1_FORMAT_VARIABLE | RF_PACKET1_DCFREE_OFF | + RF_PACKET1_CRC_ON | RF_PACKET1_CRCAUTOCLEAR_ON | RF_PACKET1_ADRSFILTERING_OFF], + #in variable length mode: the max frame size, not used in TX + 0x38: [REG_PAYLOADLENGTH, 66], + #* 0x39 */ { REG_NODEADRS, nodeID }, //turned off because we're not using address filtering + #TX on FIFO not empty + 0x3C: [REG_FIFOTHRESH, RF_FIFOTHRESH_TXSTART_FIFONOTEMPTY | RF_FIFOTHRESH_VALUE], + #RXRESTARTDELAY must match transmitter PA ramp-down time (bitrate dependent) + 0x3d: [REG_PACKETCONFIG2, RF_PACKET2_RXRESTARTDELAY_2BITS | RF_PACKET2_AUTORXRESTART_ON | RF_PACKET2_AES_OFF], + #for BR-19200: //* 0x3d */ { REG_PACKETCONFIG2, RF_PACKET2_RXRESTARTDELAY_NONE | RF_PACKET2_AUTORXRESTART_ON | RF_PACKET2_AES_OFF }, //RXRESTARTDELAY must match transmitter PA ramp-down time (bitrate dependent) + #* 0x6F */ { REG_TESTDAGC, RF_DAGC_CONTINUOUS }, // run DAGC continuously in RX mode + # run DAGC continuously in RX mode, recommended default for AfcLowBetaOn=0 + 0x6F: [REG_TESTDAGC, RF_DAGC_IMPROVED_LOWBETA0], + 0x00: [255, 0] + } diff --git a/RFM69/packet.py b/RFM69/packet.py index 9c7cfb2..d243cd9 100644 --- a/RFM69/packet.py +++ b/RFM69/packet.py @@ -1,8 +1,9 @@ import json from datetime import datetime -class Packet(object): - """Object to represent received packet. Created internally and returned by radio when getPackets() is called. +class Packet: + """Object to represent received packet. Created internally and + returned by radio when getPackets() is called. Args: receiver (int): Node ID of receiver @@ -11,27 +12,29 @@ class Packet(object): data (list): Raw transmitted data """ - + # Declare slots to reduce memory __slots__ = 'received', 'receiver', 'sender', 'RSSI', 'data' - + def __init__(self, receiver, sender, RSSI, data): self.received = datetime.utcnow() self.receiver = receiver self.sender = sender self.RSSI = RSSI self.data = data - + def to_dict(self, dateFormat=None): """Returns a dictionary representation of the class data""" if dateFormat is None: return_date = self.received else: return_date = datetime.strftime(self.received, dateFormat) - return dict(received=return_date, receiver=self.receiver, sender=self.sender, rssi=self.RSSI, data=self.data) + return dict(received=return_date, receiver=self.receiver, + sender=self.sender, rssi=self.RSSI, data=self.data) @property def data_string(self): + """Returns the data as a string""" return "".join([chr(letter) for letter in self.data]) def __str__(self): @@ -39,4 +42,3 @@ def __str__(self): def __repr__(self): return "Radio({}, {}, {}, [data])".format(self.receiver, self.sender, self.RSSI) - diff --git a/RFM69/radio.py b/RFM69/radio.py index 5869c38..d94cb28 100644 --- a/RFM69/radio.py +++ b/RFM69/radio.py @@ -1,66 +1,78 @@ -import sys, time, logging -from datetime import datetime +import time import logging +import threading +import warnings + import spidev -import RPi.GPIO as GPIO +import RPi.GPIO as GPIO # pylint: disable=consider-using-from-import + from .registers import * from .packet import Packet from .config import get_config -class Radio(object): - - def __init__(self, freqBand, nodeID, networkID=100, **kwargs): - """RFM69 Radio interface for the Raspberry PI. - - An RFM69 module is expected to be connected to the SPI interface of the Raspberry Pi. The class is as a context manager so you can instantiate it using the 'with' keyword. - Args: - freqBand: Frequency band of radio - 315MHz, 868Mhz, 433MHz or 915MHz. - nodeID (int): The node ID of this device. - networkID (int): The network ID +class Radio: + """RFM69 Radio interface for the Raspberry PI. + + An RFM69 module is expected to be connected to the SPI interface + of the Raspberry Pi. The class is as a context manager so you can + instantiate it using the 'with' keyword. + + Args: + freqBand: Frequency band of radio - 315MHz, 868Mhz, 433MHz or 915MHz. + nodeID (int): The node ID of this device. + networkID (int): The network ID + + Keyword Args: + auto_acknowledge (bool): Automatically send acknowledgements + isHighPower (bool): Is this a high power radio model + power (int): Power level - a percentage in range 10 to 100. + use_board_pin_numbers (bool): Use BOARD (not BCM) pin numbers. Defaults to True. + interruptPin (int): Pin number of interrupt pin. This is a pin index not a GPIO number. + resetPin (int): Pin number of reset pin. This is a pin index not a GPIO number. + spiBus (int): SPI bus number. + spiDevice (int): SPI device number. + promiscuousMode (bool): Listen to all messages not just those addressed to this node ID. + encryptionKey (str): 16 character encryption key. + verbose (bool): Verbose mode - Activates logging to console. + """ - Keyword Args: - auto_acknowledge (bool): Automatically send acknowledgements - isHighPower (bool): Is this a high power radio model - power (int): Power level - a percentage in range 10 to 100. - interruptPin (int): Pin number of interrupt pin. This is a pin index not a GPIO number. - resetPin (int): Pin number of reset pin. This is a pin index not a GPIO number. - spiBus (int): SPI bus number. - spiDevice (int): SPI device number. - promiscuousMode (bool): Listen to all messages not just those addressed to this node ID. - encryptionKey (str): 16 character encryption key. - verbose (bool): Verbose mode - Activates logging to console. - - """ + def __init__(self, freqBand, nodeID, networkID=100, **kwargs): self.logger = None if kwargs.get('verbose', False): self.logger = self._init_log() self.auto_acknowledge = kwargs.get('autoAcknowledge', True) self.isRFM69HW = kwargs.get('isHighPower', True) - self.intPin = kwargs.get('interruptPin', 18) - self.rstPin = kwargs.get('resetPin', 29) + self._use_board_pin_numbers = kwargs.get('use_board_pin_numbers', True) + self.intPin = kwargs.get('interruptPin', 18 if self._use_board_pin_numbers else 24) + self.rstPin = kwargs.get('resetPin', 29 if self._use_board_pin_numbers else 5) self.spiBus = kwargs.get('spiBus', 0) self.spiDevice = kwargs.get('spiDevice', 0) self.promiscuousMode = kwargs.get('promiscuousMode', 0) - - self.intLock = False - self.sendLock = False + + # Thread-safe locks + self._spiLock = threading.Lock() + self._sendLock = threading.Condition() + self._intLock = threading.Lock() + self._ackLock = threading.Condition() + self._modeLock = threading.RLock() + self.mode = "" self.mode_name = "" - + + self.address = None + self._networkID = None + # ListenMode members self._isHighSpeed = True self._encryptKey = None - self.listenModeSetDurations(DEFAULT_LISTEN_RX_US, DEFAULT_LISTEN_IDLE_US) - - self.sendSleepTime = 0.05 + self.listen_mode_set_durations(DEFAULT_LISTEN_RX_US, DEFAULT_LISTEN_IDLE_US) - # - self.packets = [] + self._packets = [] + self._packetLock = threading.Condition() + # self._packetQueue = queue.Queue() self.acks = {} - # - # self._init_spi() self._init_gpio() @@ -73,20 +85,21 @@ def __init__(self, freqBand, nodeID, networkID=100, **kwargs): def _initialize(self, freqBand, nodeID, networkID): self._reset_radio() self._set_config(get_config(freqBand, networkID)) - self._setHighPower(self.isRFM69HW) + self._setHighPower(self.isRFM69HW) # Wait for ModeReady while (self._readReg(REG_IRQFLAGS1) & RF_IRQFLAGS1_MODEREADY) == 0x00: pass - self.address = nodeID + self._setAddress(nodeID) self._freqBand = freqBand self._networkID = networkID self._init_interrupt() - return True - def _init_gpio(self): - GPIO.setmode(GPIO.BOARD) + if self._use_board_pin_numbers: + GPIO.setmode(GPIO.BOARD) + else: + GPIO.setmode(GPIO.BCM) GPIO.setup(self.intPin, GPIO.IN) GPIO.setup(self.rstPin, GPIO.OUT) @@ -104,15 +117,15 @@ def _reset_radio(self): time.sleep(0.3) #verify chip is syncing? start = time.time() - while self._readReg(REG_SYNCVALUE1) != 0xAA: + while self._readReg(REG_SYNCVALUE1) != 0xAA: # pragma: no cover self._writeReg(REG_SYNCVALUE1, 0xAA) - if time.time() - start > 15000: - raise Exception('Failed to sync with chip') + if time.time() - start > 15: + raise Exception('Failed to sync with radio') start = time.time() - while self._readReg(REG_SYNCVALUE1) != 0x55: + while self._readReg(REG_SYNCVALUE1) != 0x55: # pragma: no cover self._writeReg(REG_SYNCVALUE1, 0x55) - if time.time() - start > 15000: - raise Exception('Failed to sync with chip') + if time.time() - start > 15: + raise Exception('Failed to sync with radio') def _set_config(self, config): for value in config.values(): @@ -123,9 +136,9 @@ def _init_interrupt(self): GPIO.add_event_detect(self.intPin, GPIO.RISING, callback=self._interruptHandler) - # + # # End of Init - # + # def __enter__(self): """When the context begins""" @@ -136,9 +149,9 @@ def __enter__(self): def __exit__(self, *args): """When context exits (including when the script is terminated)""" - self._shutdown() - - def set_frequency(self, FRF): + self._shutdown() + + def set_frequency(self, FRF): # pragma: no cover """Set the radio frequency""" self._writeReg(REG_FRFMSB, FRF >> 16) self._writeReg(REG_FRFMID, FRF >> 8) @@ -150,62 +163,60 @@ def sleep(self): def set_network(self, network_id): """Set the network ID (sync) - + Args: network_id (int): Value between 1 and 254. """ - assert type(network_id) == int + assert isinstance(network_id, int) assert network_id > 0 and network_id < 255 + self._networkID = network_id self._writeReg(REG_SYNCVALUE2, network_id) def set_power_level(self, percent): """Set the transmit power level - + Args: percent (int): Value between 0 and 100. """ - assert type(percent) == int - self.powerLevel = int( round(31 * (percent / 100))) + assert isinstance(percent, int) #type(percent) == int + self.powerLevel = int(round(31 * (percent / 100))) self._writeReg(REG_PALEVEL, (self._readReg(REG_PALEVEL) & 0xE0) | self.powerLevel) - def _send(self, toAddress, buff = "", requestACK = False): - self._writeReg(REG_PACKETCONFIG2, (self._readReg(REG_PACKETCONFIG2) & 0xFB) | RF_PACKET2_RXRESTART) + def _send(self, toAddress, buff="", requestACK=False): + self._writeReg(REG_PACKETCONFIG2, + (self._readReg(REG_PACKETCONFIG2) & 0xFB) | RF_PACKET2_RXRESTART) now = time.time() while (not self._canSend()) and time.time() - now < RF69_CSMA_LIMIT_S: - self.has_received_packet() + pass #self.has_received_packet() self._sendFrame(toAddress, buff, requestACK, False) - def broadcast(self, buff = ""): - """Broadcast a message to network i.e. sends to node 255 with no ACK request. + def broadcast(self, buff=""): + """Broadcast a message to network Args: - buff (str): Message buffer to send - + buff (str): Message buffer to send """ + self.send(RF69_BROADCAST_ADDR, buff, attempts=1, require_ack=False) - broadcastAddress = 255 - self.send(broadcastAddress, buff, require_ack=False) - - def send(self, toAddress, buff = "", **kwargs): + def send(self, toAddress, buff="", **kwargs): """Send a message - + Args: toAddress (int): Recipient node's ID - buff (str): Message buffer to send - + buff (str): Message buffer to send + Keyword Args: attempts (int): Number of attempts wait (int): Milliseconds to wait for acknowledgement require_ack(bool): Require Acknowledgement. If Attempts > 1 this is auto set to True. + Returns: bool: If acknowledgement received or None is no acknowledgement requested - """ - attempts = kwargs.get('attempts', 3) wait_time = kwargs.get('wait', 50) require_ack = kwargs.get('require_ack', True) @@ -213,23 +224,20 @@ def send(self, toAddress, buff = "", **kwargs): require_ack = True for _ in range(0, attempts): - self._send(toAddress, buff, attempts>0 ) + self._send(toAddress, buff, attempts > 1) if not require_ack: return None - sentTime = time.time() - while (time.time() - sentTime) * 1000 < wait_time: - self._debug("Waiting line 203") - time.sleep(.05) - if self._ACKReceived(toAddress): + with self._ackLock: + if self._ackLock.wait_for(lambda: self._ACKReceived(toAddress), wait_time/1000): return True return False def read_temperature(self, calFactor=0): """Read the temperature of the radios CMOS chip. - + Args: calFactor: Additional correction to corrects the slope, rising temp = rising val @@ -247,7 +255,7 @@ def read_temperature(self, calFactor=0): def calibrate_radio(self): """Calibrate the internal RC oscillator for use in wide temperature variations. - + See RFM69 datasheet section [4.3.5. RC Timer Accuracy] for more information. """ self._writeReg(REG_OSC1, RF_OSC1_RCCAL_START) @@ -267,24 +275,23 @@ def read_registers(self): def begin_receive(self): """Begin listening for packets""" - while self.intLock: - time.sleep(.1) - - if (self._readReg(REG_IRQFLAGS2) & RF_IRQFLAGS2_PAYLOADREADY): - # avoid RX deadlocks - self._writeReg(REG_PACKETCONFIG2, (self._readReg(REG_PACKETCONFIG2) & 0xFB) | RF_PACKET2_RXRESTART) - #set DIO0 to "PAYLOADREADY" in receive mode - self._writeReg(REG_DIOMAPPING1, RF_DIOMAPPING1_DIO0_01) - self._setMode(RF69_MODE_RX) + with self._intLock: + if self._readReg(REG_IRQFLAGS2) & RF_IRQFLAGS2_PAYLOADREADY: + # avoid RX deadlocks + self._writeReg(REG_PACKETCONFIG2, (self._readReg(REG_PACKETCONFIG2) & 0xFB) | RF_PACKET2_RXRESTART) + #set DIO0 to "PAYLOADREADY" in receive mode + self._writeReg(REG_DIOMAPPING1, RF_DIOMAPPING1_DIO0_01) + self._setMode(RF69_MODE_RX) def has_received_packet(self): """Check if packet received Returns: bool: True if packet has been received - """ - return len(self.packets) > 0 + # return self._packetQueue.qsize() > 0 + with self._packetLock: + return len(self._packets) > 0 def get_packets(self): """Get newly received packets. @@ -292,58 +299,109 @@ def get_packets(self): Returns: list: Returns a list of RFM69.Packet objects. """ - # Create packet - packets = list(self.packets) - self.packets = [] - return packets - - - def send_ack(self, toAddress, buff = ""): - """Send an acknowledgemet packet + # packets = [] + # try: + # while True: + # packets.append(self._packetQueue.get_nowait()) + # except queue.Empty: + # pass + # return packets + with self._packetLock: + packets = list(self._packets) + self._packets = [] + return packets + + + def send_ack(self, toAddress, buff=""): + """Send an acknowledgement packet - Args: + Args: toAddress (int): Recipient node's ID """ while not self._canSend(): - self.has_received_packet() + pass #self.has_received_packet() self._sendFrame(toAddress, buff, False, True) - # + # pylint: disable=missing-function-docstring + @property + def packets(self): + warnings.simplefilter("default") + warnings.warn("The packets property will be deprecated in a future version. Please use get_packets() and num_packets() instead.", DeprecationWarning) + return self._packets + + + def num_packets(self): + """Returns the number of received packets + + Returns: + int: Number of packets in the received queue + """ + # return self._packetQueue.qsize() + with self._packetLock: + return len(self._packets) + + def get_packet(self, block=True, timeout=None): + """Gets a single packet (thread-safe) + + Args: + block (bool): Block until a packet is available + timeout (int): Time to wait if blocking. Set to None to wait forever + + Returns: + Packet: The oldest packet received if available, or None if no packet is available + """ + # try: + # return self._packetQueue.get(block, timeout) + # except queue.Empty: + # return None + with self._packetLock: + # Regardless of blocking, if there's a packet available, return it + if len(self._packets) > 0: + return self._packets.pop(0) + # Otherwise, if we're blocking... + if block: + # Wait for us to get a packet + if self._packetLock.wait_for(self.has_received_packet, timeout): + # If we didn't timeout, the above is True, so we pop a packet + return self._packets.pop(0) + + return None + + # # Internal functions - # + # def _setMode(self, newMode): - if newMode == self.mode: - return - if newMode == RF69_MODE_TX: - self.mode_name = "TX" - self._writeReg(REG_OPMODE, (self._readReg(REG_OPMODE) & 0xE3) | RF_OPMODE_TRANSMITTER) - if self.isRFM69HW: - self._setHighPowerRegs(True) - elif newMode == RF69_MODE_RX: - self.mode_name = "RX" - self._writeReg(REG_OPMODE, (self._readReg(REG_OPMODE) & 0xE3) | RF_OPMODE_RECEIVER) - if self.isRFM69HW: - self._setHighPowerRegs(False) - elif newMode == RF69_MODE_SYNTH: - self.mode_name = "Synth" - self._writeReg(REG_OPMODE, (self._readReg(REG_OPMODE) & 0xE3) | RF_OPMODE_SYNTHESIZER) - elif newMode == RF69_MODE_STANDBY: - self.mode_name = "Standby" - self._writeReg(REG_OPMODE, (self._readReg(REG_OPMODE) & 0xE3) | RF_OPMODE_STANDBY) - elif newMode == RF69_MODE_SLEEP: - self.mode_name = "Sleep" - self._writeReg(REG_OPMODE, (self._readReg(REG_OPMODE) & 0xE3) | RF_OPMODE_SLEEP) - else: - self.mode_name = "Unknown" - return - # we are using packet mode, so this check is not really needed - # but waiting for mode ready is necessary when going from sleep because the FIFO may not be immediately available from previous mode - while self.mode == RF69_MODE_SLEEP and self._readReg(REG_IRQFLAGS1) & RF_IRQFLAGS1_MODEREADY == 0x00: - pass - self.mode = newMode + with self._modeLock: + if newMode == self.mode or newMode not in [RF69_MODE_TX, RF69_MODE_RX, RF69_MODE_SYNTH, RF69_MODE_STANDBY, RF69_MODE_SLEEP]: + return + if newMode == RF69_MODE_TX: + self.mode_name = "TX" + self._writeReg(REG_OPMODE, (self._readReg(REG_OPMODE) & 0xE3) | RF_OPMODE_TRANSMITTER) + if self.isRFM69HW: + self._setHighPowerRegs(True) + elif newMode == RF69_MODE_RX: + self.mode_name = "RX" + self._writeReg(REG_OPMODE, (self._readReg(REG_OPMODE) & 0xE3) | RF_OPMODE_RECEIVER) + if self.isRFM69HW: + self._setHighPowerRegs(False) + elif newMode == RF69_MODE_SYNTH: + self.mode_name = "Synth" + self._writeReg(REG_OPMODE, (self._readReg(REG_OPMODE) & 0xE3) | RF_OPMODE_SYNTHESIZER) + elif newMode == RF69_MODE_STANDBY: + self.mode_name = "Standby" + self._writeReg(REG_OPMODE, (self._readReg(REG_OPMODE) & 0xE3) | RF_OPMODE_STANDBY) + elif newMode == RF69_MODE_SLEEP: + self.mode_name = "Sleep" + self._writeReg(REG_OPMODE, (self._readReg(REG_OPMODE) & 0xE3) | RF_OPMODE_SLEEP) + # we are using packet mode, so this check is not really needed + # but waiting for mode ready is necessary when going from sleep because the FIFO may not be immediately available from previous mode + while self.mode == RF69_MODE_SLEEP and self._readReg(REG_IRQFLAGS1) & RF_IRQFLAGS1_MODEREADY == 0x00: + pass + + self.mode = newMode def _setAddress(self, addr): self.address = addr @@ -364,11 +422,6 @@ def _ACKReceived(self, fromNodeID): self.acks.pop(fromNodeID, None) return True return False - # if self.has_received_packet(): - # return (self.SENDERID == fromNodeID or fromNodeID == RF69_BROADCAST_ADDR) and self.ACK_RECEIVED - # return False - - def _sendFrame(self, toAddress, buff, requestACK, sendACK): #turn off receiver to prevent reception while filling fifo @@ -379,7 +432,7 @@ def _sendFrame(self, toAddress, buff, requestACK, sendACK): # DIO0 is "Packet Sent" self._writeReg(REG_DIOMAPPING1, RF_DIOMAPPING1_DIO0_00) - if (len(buff) > RF69_MAX_DATA_LEN): + if len(buff) > RF69_MAX_DATA_LEN: buff = buff[0:RF69_MAX_DATA_LEN] ack = 0 @@ -387,22 +440,18 @@ def _sendFrame(self, toAddress, buff, requestACK, sendACK): ack = 0x80 elif requestACK: ack = 0x40 - if isinstance(buff, str): - self.spi.xfer2([REG_FIFO | 0x80, len(buff) + 3, toAddress, self.address, ack] + [int(ord(i)) for i in list(buff)]) - else: - self.spi.xfer2([REG_FIFO | 0x80, len(buff) + 3, toAddress, self.address, ack] + buff) + with self._spiLock: + if isinstance(buff, str): + self.spi.xfer2([REG_FIFO | 0x80, len(buff) + 3, toAddress, self.address, ack] + [int(ord(i)) for i in list(buff)]) + else: + self.spi.xfer2([REG_FIFO | 0x80, len(buff) + 3, toAddress, self.address, ack] + buff) - self.sendLock = True - self._setMode(RF69_MODE_TX) - slept = 0 - while self.sendLock: - time.sleep(self.sendSleepTime) - slept += self.sendSleepTime - if slept > 1.0: - break + with self._sendLock: + self._setMode(RF69_MODE_TX) + self._sendLock.wait(1.0) self._setMode(RF69_MODE_RX) - def _readRSSI(self, forceTrigger = False): + def _readRSSI(self, forceTrigger=False): rssi = 0 if forceTrigger: self._writeReg(REG_RSSICONFIG, RF_RSSI_START) @@ -416,17 +465,20 @@ def _encrypt(self, key): self._setMode(RF69_MODE_STANDBY) if key != 0 and len(key) == 16: self._encryptKey = key - self.spi.xfer([REG_AESKEY1 | 0x80] + [int(ord(i)) for i in list(key)]) - self._writeReg(REG_PACKETCONFIG2,(self._readReg(REG_PACKETCONFIG2) & 0xFE) | RF_PACKET2_AES_ON) + with self._spiLock: + self.spi.xfer([REG_AESKEY1 | 0x80] + [int(ord(i)) for i in list(key)]) + self._writeReg(REG_PACKETCONFIG2, (self._readReg(REG_PACKETCONFIG2) & 0xFE) | RF_PACKET2_AES_ON) else: self._encryptKey = None - self._writeReg(REG_PACKETCONFIG2,(self._readReg(REG_PACKETCONFIG2) & 0xFE) | RF_PACKET2_AES_OFF) + self._writeReg(REG_PACKETCONFIG2, (self._readReg(REG_PACKETCONFIG2) & 0xFE) | RF_PACKET2_AES_OFF) def _readReg(self, addr): - return self.spi.xfer([addr & 0x7F, 0])[1] + with self._spiLock: + return self.spi.xfer([addr & 0x7F, 0])[1] def _writeReg(self, addr, value): - self.spi.xfer([addr | 0x80, value]) + with self._spiLock: + self.spi.xfer([addr | 0x80, value]) def _promiscuous(self, onOff): self.promiscuousMode = onOff @@ -454,102 +506,122 @@ def _shutdown(self): Puts the radio to sleep and cleans up the GPIO connections. """ + GPIO.remove_event_detect(self.intPin) + self._modeLock.acquire() self._setHighPower(False) self.sleep() - GPIO.cleanup() + GPIO.cleanup([self.intPin, self.rstPin]) + self._intLock.acquire() + self._spiLock.acquire() + self.spi.close() - def __str__(self): + def __str__(self): # pragma: no cover return "Radio RFM69" - def __repr__(self): + def __repr__(self): # pragma: no cover return "Radio()" + # pylint: disable=no-self-use def _init_log(self): logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(__name__) handler = logging.StreamHandler() handler.setLevel(logging.DEBUG) - formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(thread)d - %(message)s') handler.setFormatter(formatter) logger.addHandler(handler) logger.propagate = False return logger - def _debug(self, *args): - if self.logger is not None: - self.logger.debug(*args) - - def _error(self, *args): + def _debug(self, *args): # pragma: no cover if self.logger is not None: - self.logger.error(*args) - - # - # Radio interrupt handler - # + self.logger.debug(*args) - def _interruptHandler(self, pin): - self.intLock = True - self.sendLock = False + def _error(self, *args): # pragma: no cover + if self.logger is not None: + self.logger.error(*args) - if self.mode == RF69_MODE_RX and self._readReg(REG_IRQFLAGS2) & RF_IRQFLAGS2_PAYLOADREADY: - self._setMode(RF69_MODE_STANDBY) - - payload_length, target_id, sender_id, CTLbyte = self.spi.xfer2([REG_FIFO & 0x7f,0,0,0,0])[1:] - - if payload_length > 66: - payload_length = 66 - - if not (self.promiscuousMode or target_id == self.address or target_id == RF69_BROADCAST_ADDR): - self._debug("Ignore Interrupt") - self.intLock = False + # + # Radio interrupt handler + # + + # pylint: disable=unused-argument + def _interruptHandler(self, pin): # pragma: no cover + self._intLock.acquire() + with self._modeLock: + with self._sendLock: + self._sendLock.notify_all() + + if self.mode == RF69_MODE_RX and self._readReg(REG_IRQFLAGS2) & RF_IRQFLAGS2_PAYLOADREADY: + self._setMode(RF69_MODE_STANDBY) + + with self._spiLock: + payload_length, target_id, sender_id, CTLbyte = self.spi.xfer2([REG_FIFO & 0x7f, 0, 0, 0, 0])[1:] + + if payload_length > 66: + payload_length = 66 + + if not (self.promiscuousMode or target_id == self.address or target_id == RF69_BROADCAST_ADDR): + self._debug("Ignore Interrupt") + self._intLock.release() + self.begin_receive() + return + + data_length = payload_length - 3 + ack_received = bool(CTLbyte & 0x80) + ack_requested = bool(CTLbyte & 0x40) and target_id == self.address # Only send back an ack if we're the intended recipient + with self._spiLock: + data = self.spi.xfer2([REG_FIFO & 0x7f] + [0 for i in range(0, data_length)])[1:] + rssi = self._readRSSI() + + if ack_received: + self._debug("Incoming ack from {}".format(sender_id)) + # Record acknowledgement + with self._ackLock: + self.acks.setdefault(sender_id, 1) + self._ackLock.notify_all() + elif ack_requested: + self._debug("replying to ack request") + else: + self._debug("Other ??") + + # When message received + if not ack_received: + self._debug("Incoming data packet") + # self._packetQueue.put( + # Packet(int(target_id), int(sender_id), int(rssi), list(data)) + # ) + with self._packetLock: + self._packets.append( + Packet(int(target_id), int(sender_id), int(rssi), list(data)) + ) + self._packetLock.notify_all() + + # Send acknowledgement if needed + if ack_requested and self.auto_acknowledge: + self._debug("Sending an ack") + self._intLock.release() + self.send_ack(sender_id) + self.begin_receive() + return + + self._intLock.release() self.begin_receive() return - data_length = payload_length - 3 - ack_received = bool(CTLbyte & 0x80) - ack_requested = bool(CTLbyte & 0x40) - data = self.spi.xfer2([REG_FIFO & 0x7f] + [0 for i in range(0, data_length)])[1:] - rssi = self._readRSSI() - - if ack_received: - self._debug("Incoming ack") - self._debug(sender_id) - # Record acknowledgement - self.acks.setdefault(sender_id, 1) - - elif ack_requested: - self._debug("replying to ack request") - else: - self._debug("Other ??") - - # When message received - if not ack_received: - self._debug("Incoming data packet") - self.packets.append( - Packet(int(target_id), int(sender_id), int(rssi), list(data)) - ) - - # Send acknowledgement if needed - if ack_requested and self.auto_acknowledge: - self.intLock = False - self.send_ack(sender_id) - - self.intLock = False - self.begin_receive() + self._intLock.release() - # + # # ListenMode functions - # + # def _reinitRadio(self): - if (not self._initialize(self._freqBand, self.address, self._networkID)): - return False - if (self._encryptKey): - self._encrypt(self._encryptKey); # Restore the encryption key if necessary - if (self._isHighSpeed): + self._initialize(self._freqBand, self.address, self._networkID) + if self._encryptKey: + self._encrypt(self._encryptKey) # Restore the encryption key if necessary + if self._isHighSpeed: self._writeReg(REG_LNA, (self._readReg(REG_LNA) & ~0x3) | RF_LNA_GAINSELECT_AUTO) - return True def _getUsForResolution(self, resolution): if resolution == RF_LISTEN1_RESOL_RX_64 or resolution == RF_LISTEN1_RESOL_IDLE_64: @@ -558,101 +630,112 @@ def _getUsForResolution(self, resolution): return 4100 elif resolution == RF_LISTEN1_RESOL_RX_262000 or resolution == RF_LISTEN1_RESOL_IDLE_262000: return 262000 - else: - return 0 - + + return 0 # pragma: no cover + def _getCoefForResolution(self, resolution, duration): resolDuration = self._getUsForResolution(resolution) result = int(duration / resolDuration) # If the next-higher coefficient is closer, use that - if (abs(duration - ((result + 1) * resolDuration)) < abs(duration - (result * resolDuration))): + if abs(duration - ((result + 1) * resolDuration)) < abs(duration - (result * resolDuration)): return result + 1 return result - - def listenModeHighSpeed(self, highSpeed): - self._isHighSpeed = highSpeed def _chooseResolutionAndCoef(self, resolutions, duration): for resolution in resolutions: coef = self._getCoefForResolution(resolution, duration) - if (coef <= 255): + if coef <= 255: coefOut = coef resolOut = resolution return (resolOut, coefOut) # out of range return (None, None) - - def listenModeSetDurations(self, rxDuration, idleDuration): - rxResolutions = [ RF_LISTEN1_RESOL_RX_64, RF_LISTEN1_RESOL_RX_4100, RF_LISTEN1_RESOL_RX_262000, 0 ] - idleResolutions = [ RF_LISTEN1_RESOL_IDLE_64, RF_LISTEN1_RESOL_IDLE_4100, RF_LISTEN1_RESOL_IDLE_262000, 0 ] + + def listen_mode_set_durations(self, rxDuration, idleDuration): + """Set the duty cycle for listen mode + + The values used may be slightly different to accomodate what + is allowed by the radio. This function returns the actual + values used. + + Args: + rxDuration (int): number of microseconds to be in receive mode + idleDuration (int): number of microseconds to be sleeping + + Returns: + (int, int): the actual (rxDuration, idleDuration) used + """ + rxResolutions = [RF_LISTEN1_RESOL_RX_64, RF_LISTEN1_RESOL_RX_4100, RF_LISTEN1_RESOL_RX_262000] + idleResolutions = [RF_LISTEN1_RESOL_IDLE_64, RF_LISTEN1_RESOL_IDLE_4100, RF_LISTEN1_RESOL_IDLE_262000] (resolOut, coefOut) = self._chooseResolutionAndCoef(rxResolutions, rxDuration) - if(resolOut and coefOut): + if resolOut and coefOut: self._rxListenResolution = resolOut self._rxListenCoef = coefOut else: return (None, None) - + (resolOut, coefOut) = self._chooseResolutionAndCoef(idleResolutions, idleDuration) if(resolOut and coefOut): self._idleListenResolution = resolOut self._idleListenCoef = coefOut else: return (None, None) - + rxDuration = self._getUsForResolution(self._rxListenResolution) * self._rxListenCoef idleDuration = self._getUsForResolution(self._idleListenResolution) * self._idleListenCoef self._listenCycleDurationUs = rxDuration + idleDuration return (rxDuration, idleDuration) - - def listenModeGetDurations(self): + + def listen_mode_get_durations(self): rxDuration = self._getUsForResolution(self._rxListenResolution) * self._rxListenCoef idleDuration = self._getUsForResolution(self._idleListenResolution) * self._idleListenCoef return (rxDuration, idleDuration) - - def listenModeApplyHighSpeedSettings(self): - if (not self._isHighSpeed): return + + def _listenModeApplyHighSpeedSettings(self): + if not self._isHighSpeed: + return self._writeReg(REG_BITRATEMSB, RF_BITRATEMSB_200000) self._writeReg(REG_BITRATELSB, RF_BITRATELSB_200000) self._writeReg(REG_FDEVMSB, RF_FDEVMSB_100000) self._writeReg(REG_FDEVLSB, RF_FDEVLSB_100000) - self._writeReg( REG_RXBW, RF_RXBW_DCCFREQ_000 | RF_RXBW_MANT_20 | RF_RXBW_EXP_0 ) + self._writeReg(REG_RXBW, RF_RXBW_DCCFREQ_000 | RF_RXBW_MANT_20 | RF_RXBW_EXP_0) - def listenModeSendBurst(self, toAddress, buff): + def listen_mode_send_burst(self, toAddress, buff): """Send a message to nodes in listen mode as a burst - + Args: toAddress (int): Recipient node's ID - buff (str): Message buffer to send - + buff (str): Message buffer to send """ GPIO.remove_event_detect(self.intPin) # detachInterrupt(_interruptNum) self._setMode(RF69_MODE_STANDBY) - self._writeReg(REG_PACKETCONFIG1, RF_PACKET1_FORMAT_VARIABLE | RF_PACKET1_DCFREE_WHITENING | RF_PACKET1_CRC_ON | RF_PACKET1_CRCAUTOCLEAR_ON ) + self._writeReg(REG_PACKETCONFIG1, RF_PACKET1_FORMAT_VARIABLE | RF_PACKET1_DCFREE_WHITENING | RF_PACKET1_CRC_ON | RF_PACKET1_CRCAUTOCLEAR_ON) self._writeReg(REG_PACKETCONFIG2, RF_PACKET2_RXRESTARTDELAY_NONE | RF_PACKET2_AUTORXRESTART_ON | RF_PACKET2_AES_OFF) self._writeReg(REG_SYNCVALUE1, 0x5A) self._writeReg(REG_SYNCVALUE2, 0x5A) - self.listenModeApplyHighSpeedSettings() + self._listenModeApplyHighSpeedSettings() self._writeReg(REG_FRFMSB, self._readReg(REG_FRFMSB) + 1) self._writeReg(REG_FRFLSB, self._readReg(REG_FRFLSB)) # MUST write to LSB to affect change! - + cycleDurationMs = int(self._listenCycleDurationUs / 1000) timeRemaining = int(cycleDurationMs) self._setMode(RF69_MODE_TX) - numSent = 0 startTime = int(time.time() * 1000) #millis() - while(timeRemaining > 0): - if isinstance(buff, str): - self.spi.xfer2([REG_FIFO | 0x80, len(buff) + 4, toAddress, self.address, timeRemaining & 0xFF, (timeRemaining >> 8) & 0xFF] + [int(ord(i)) for i in list(buff)]) - else: - self.spi.xfer2([REG_FIFO | 0x80, len(buff) + 4, toAddress, self.address, timeRemaining & 0xFF, (timeRemaining >> 8) & 0xFF] + buff) - - while ((self._readReg(REG_IRQFLAGS2) & RF_IRQFLAGS2_FIFONOTEMPTY) != 0x00): + while timeRemaining > 0: + with self._spiLock: + if isinstance(buff, str): + self.spi.xfer2([REG_FIFO | 0x80, len(buff) + 4, toAddress, self.address, timeRemaining & 0xFF, (timeRemaining >> 8) & 0xFF] + [int(ord(i)) for i in list(buff)]) + else: + self.spi.xfer2([REG_FIFO | 0x80, len(buff) + 4, toAddress, self.address, timeRemaining & 0xFF, (timeRemaining >> 8) & 0xFF] + buff) + + while (self._readReg(REG_IRQFLAGS2) & RF_IRQFLAGS2_FIFONOTEMPTY) != 0x00: pass # make sure packet is sent before putting more into the FIFO timeRemaining = cycleDurationMs - (int(time.time()*1000) - startTime) self._setMode(RF69_MODE_STANDBY) self._reinitRadio() + self.begin_receive() diff --git a/RFM69/registers.py b/RFM69/registers.py index 0723db9..8681262 100644 --- a/RFM69/registers.py +++ b/RFM69/registers.py @@ -664,7 +664,7 @@ RF_AGCTHRESH2_STEP2_0 = 0x00 RF_AGCTHRESH2_STEP2_1 = 0x10 RF_AGCTHRESH2_STEP2_2 = 0x20 -RF_AGCTHRESH2_STEP2_3 = 0x30 # XXX wrong -- Default +RF_AGCTHRESH2_STEP2_3 = 0x30 RF_AGCTHRESH2_STEP2_4 = 0x40 RF_AGCTHRESH2_STEP2_5 = 0x50 RF_AGCTHRESH2_STEP2_6 = 0x60 @@ -1084,7 +1084,9 @@ RF69_868MHZ = 86 RF69_915MHZ = 91 -RF69_MAX_DATA_LEN = 61 # to take advantage of the built in AES/CRC we want to limit the frame size to the internal FIFO size (66 bytes - 3 bytes overhead) +# to take advantage of the built in AES/CRC we want to limit the frame +# size to the internal FIFO size (66 bytes - 3 bytes overhead) +RF69_MAX_DATA_LEN = 61 CSMA_LIMIT = -90 # upper RX signal sensitivity threshold in dBm for carrier sense access RF69_MODE_SLEEP = 0 # XTAL OFF @@ -1093,8 +1095,9 @@ RF69_MODE_RX = 3 # RX MODE RF69_MODE_TX = 4 # TX MODE -COURSE_TEMP_COEF = -90 # puts the temperature reading in the ballpark, user can fine tune the returned value -RF69_BROADCAST_ADDR = 255 +# puts the temperature reading in the ballpark, user can fine tune the returned value +COURSE_TEMP_COEF = -90 +RF69_BROADCAST_ADDR = 0 RF69_CSMA_LIMIT_MS = 1000 RF69_CSMA_LIMIT_S = 1 diff --git a/VERSION b/VERSION index 0d91a54..1d0ba9e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.0 +0.4.0 diff --git a/build/README.md b/build/README.md index b1d37d6..ae38df3 100644 --- a/build/README.md +++ b/build/README.md @@ -3,7 +3,7 @@ Part of these instructions are cribbed from [https://packaging.python.org/tutorials/packaging-projects/](https://packaging.python.org/tutorials/packaging-projects/), and are meant for a Linux OS. They assume you've already set up an account and [gotten an API token on pypi.org](https://pypi.org/manage/account/#api-tokens). 1. Create a branch named with the new version number, if you haven't already -1. Ensure that the version number is set in [```../setup.py```](../setup.py) +1. Ensure that the version number is set in [```../VERSION```](../VERSION) 1. Make sure to update [```../CHANGELOG.md```](../CHANGELOG.md) 1. Update the documentation as necessary 1. Make sure all the tests pass per the instructions at [```../tests/readme.md```](../tests/readme.md) diff --git a/docs/source/example_asyncio.rst b/docs/source/example_asyncio.rst index 95ed91b..d663a49 100644 --- a/docs/source/example_asyncio.rst +++ b/docs/source/example_asyncio.rst @@ -18,6 +18,6 @@ Asyncio RESTful API Gateway --------------------------- The destination url is set to http://httpbin.org/post. This is a free online service which will echo back the post data sent to the service. It has a whole host (pardon the pun) of other tools for testing HTTP clients. -.. literalinclude:: ../../examples/script_async_gateway.py +.. literalinclude:: ../../examples/example_async_gateway.py :language: python diff --git a/docs/source/example_basic.rst b/docs/source/example_basic.rst index 2b16f63..d9ed79b 100644 --- a/docs/source/example_basic.rst +++ b/docs/source/example_basic.rst @@ -22,11 +22,16 @@ This ensures that the necessary clean-up code is executed when you exit the cont Frequency selection: FREQ_315MHZ, FREQ_433MHZ, FREQ_868MHZ or FREQ_915MHZ. Select the band appropriate to the radio you have. - Simple Transceiver ------------------ +Here's a simple transceiver example. -.. literalinclude:: ../../examples/script_rxtx.py +.. literalinclude:: ../../examples/example_rxtx.py :language: python +Better Transceiver +------------------ +Here's an even better way to do deal with sending and receiving packets: start up a separate thread to handle incoming packets. Now we've eliminated the complex timing code altogether, and the result is much more readable. +.. literalinclude:: ../../examples/example_rxtx_threaded.py + :language: python diff --git a/docs/source/hookup.rst b/docs/source/hookup.rst index 5ae0dff..a0b4011 100644 --- a/docs/source/hookup.rst +++ b/docs/source/hookup.rst @@ -18,7 +18,7 @@ Pinout guide PI Name, 3v3 [#f1]_ , Ground, MOSI, MISO, SCLK, ID_SC [#f2]_ , CE0 PI GPIO [#f3]_, , , 10, 9, 11, , 8, 5 - PI Pin, 17, 25, 19, 21, 23, 18, 24, 29 + PI Pin, 17, 25, 19, 21, 23, 28, 24, 29 Adafruit, Vin, GND, MOSI, MISO, CLK, G0, CS, RST Sparkfun, 3.3v, GND, MOSI, MISO, SCK, DIO0, NSS, RESET diff --git a/examples/example-node/example-node.ino b/examples/example-node/example-node.ino index ccd07ef..cb9e785 100644 --- a/examples/example-node/example-node.ino +++ b/examples/example-node/example-node.ino @@ -57,10 +57,11 @@ void setup() { #if defined(RF69_LISTENMODE_ENABLE) radio.listenModeEnd(); #endif - radio.spyMode(true); + #ifdef IS_RFM69HW_HCW radio.setHighPower(); //must include this only for RFM69HW/HCW! #endif + Serial.println("Setup complete"); Serial.println(); } diff --git a/examples/script_async_gateway.py b/examples/example_async_gateway.py similarity index 93% rename from examples/script_async_gateway.py rename to examples/example_async_gateway.py index 64668d0..18f86ae 100644 --- a/examples/script_async_gateway.py +++ b/examples/example_async_gateway.py @@ -1,3 +1,5 @@ +# pylint: disable=missing-function-docstring,redefined-outer-name + import asyncio from aiohttp import ClientSession from RFM69 import Radio, FREQ_433MHZ @@ -10,7 +12,7 @@ async def call_API(url, packet): print("Server responded", response) async def receiver(radio): - while True: + while True: print("Receiver") for packet in radio.get_packets(): print("Packet received", packet.to_dict()) @@ -23,7 +25,7 @@ async def send(radio, to, message): print ("Acknowledgement received") else: print ("No Acknowledgement") - + async def pinger(radio): print("Pinger") counter = 0 diff --git a/examples/example_rxtx.py b/examples/example_rxtx.py new file mode 100644 index 0000000..8d1834e --- /dev/null +++ b/examples/example_rxtx.py @@ -0,0 +1,39 @@ +# pylint: disable=unused-import + +import time +from RFM69 import Radio, FREQ_315MHZ, FREQ_433MHZ, FREQ_868MHZ, FREQ_915MHZ + +node_id = 1 +network_id = 100 +recipient_id = 2 + +# The following are for an Adafruit RFM69HCW Transceiver Radio +# Bonnet https://www.adafruit.com/product/4072 +# You should adjust them to whatever matches your radio +with Radio(FREQ_915MHZ, node_id, network_id, isHighPower=True, verbose=False, + interruptPin=15, resetPin=22, spiDevice=1) as radio: + print ("Starting loop...") + + while True: + startTime = time.time() + # Get packets for at most 5 seconds + while time.time() - startTime < 5: + # We end at (startTime+5), so we have (startTime+5 - time.time()) + # seconds left + timeRemaining = max(0, startTime + 5 - time.time()) + + # This call will block until a packet is received, + # or timeout in however much time we have left + packet = radio.get_packet(timeout = timeRemaining) + + # If radio.get_packet times out, it will return None + if packet is not None: + # Process packet + print (packet) + + # After 5 seconds send a message + print ("Sending") + if radio.send(recipient_id, "TEST", attempts=3, waitTime=100): + print ("Acknowledgement received") + else: + print ("No Acknowledgement") diff --git a/examples/example_rxtx_threaded.py b/examples/example_rxtx_threaded.py new file mode 100644 index 0000000..052ee62 --- /dev/null +++ b/examples/example_rxtx_threaded.py @@ -0,0 +1,38 @@ +# pylint: disable=missing-function-docstring,unused-import,redefined-outer-name + +import time +import threading +from RFM69 import Radio, FREQ_315MHZ, FREQ_433MHZ, FREQ_868MHZ, FREQ_915MHZ + +node_id = 1 +network_id = 100 +recipient_id = 2 + +# We'll run this function in a separate thread +def receiveFunction(radio): + while True: + # This call will block until a packet is received + packet = radio.get_packet() + print("Got a packet: ", end="") + # Process packet + print(packet) + +# The following are for an Adafruit RFM69HCW Transceiver Radio +# Bonnet https://www.adafruit.com/product/4072 +# You should adjust them to whatever matches your radio +with Radio(FREQ_915MHZ, node_id, network_id, isHighPower=True, verbose=False, + interruptPin=15, resetPin=22, spiDevice=1) as radio: + print ("Starting loop...") + + # Create a thread to run receiveFunction in the background and start it + receiveThread = threading.Thread(target = receiveFunction, args=(radio,)) + receiveThread.start() + + while True: + # After 5 seconds send a message + time.sleep(5) + print ("Sending") + if radio.send(recipient_id, "TEST", attempts=3, waitTime=100): + print ("Acknowledgement received") + else: + print ("No Acknowledgement") diff --git a/examples/script_async.py b/examples/script_async.py deleted file mode 100644 index 58a3305..0000000 --- a/examples/script_async.py +++ /dev/null @@ -1,37 +0,0 @@ -import asyncio -from aiohttp import ClientSession -from RFM69 import Radio, FREQ_433MHZ - -async def receiver(radio): - while True: - print("Receiver") - for packet in radio.get_packets(): - print("Packet received", packet.to_dict()) - await asyncio.sleep(10) - -async def send(radio, to, message): - print ("Sending") - if radio.send(to, message, attempts=3, waitTime=100): - print ("Acknowledgement received") - else: - print ("No Acknowledgement") - -async def pinger(radio): - print("Pinger") - counter = 0 - while True: - await send(radio, 2, "ping {}".format(counter)) - counter += 1 - await asyncio.sleep(5) - - -loop = asyncio.get_event_loop() -node_id = 1 -network_id = 100 -with Radio(FREQ_433MHZ, node_id, network_id, isHighPower=True, verbose=False) as radio: - print ("Started radio") - loop.create_task(receiver(radio)) - loop.create_task(pinger(radio)) - loop.run_forever() - -loop.close() diff --git a/examples/script_rxtx.py b/examples/script_rxtx.py deleted file mode 100644 index 9ca91c9..0000000 --- a/examples/script_rxtx.py +++ /dev/null @@ -1,42 +0,0 @@ -from RFM69 import Radio -import datetime -import time -from config import * - -node_id = 1 -network_id = 100 -recipient_id = 2 - -with Radio(FREQUENCY, node_id, network_id, isHighPower=True, verbose=True, interruptPin=INTERRUPT_PIN, resetPin=RESET_PIN, spiDevice=SPI_DEVICE) as radio: - print ("Starting loop...") - - rx_counter = 0 - tx_counter = 0 - - while True: - - # Every 10 seconds get packets - if rx_counter > 10: - rx_counter = 0 - - # Process packets - for packet in radio.get_packets(): - print (packet) - - # Every 5 seconds send a message - if tx_counter > 5: - tx_counter=0 - - # Send - print ("Sending") - if radio.send(2, "TEST", attempts=3, waitTime=100): - print ("Acknowledgement received") - else: - print ("No Acknowledgement") - - - print("Listening...", len(radio.packets), radio.mode_name) - delay = 0.5 - rx_counter += delay - tx_counter += delay - time.sleep(delay) diff --git a/tests/readme.md b/tests/README.md similarity index 66% rename from tests/readme.md rename to tests/README.md index 27462e1..747c044 100644 --- a/tests/readme.md +++ b/tests/README.md @@ -18,21 +18,29 @@ Inside this directory in a Python 3 environment on your local machine (i.e. not 1. (Optional) Setup a virtual environment by running ``` python3 -m venv venv_test -source test_venv/bin/activate +source venv_test/bin/activate ``` -2. Edit [```config.py```](config.py) to choose the right frequency and pins -3. (Optional) If you want to test listenModeSendBurst, uncomment the ```TEST_LISTEN_MODE_SEND_BURST``` flag in [```config.py```](config.py) +2. Edit [```test_config.py```](test_config.py) to choose the right frequency and pins +3. (Optional) If you want to test listenModeSendBurst, uncomment the ```TEST_LISTEN_MODE_SEND_BURST``` flag in [```test_config.py```](test_config.py) 4. Run the following commands (still on your local machine, i.e. not on your Raspberry Pi). ``` pip3 install --upgrade pip pip3 install -r requirements_local.txt -fab init -H raspberrypi.local +fab -H raspberrypi.local init ``` where ```raspberrypi.local``` is the hostname of your Raspberry Pi. ## Run tests on remote environment From inside your testing environment on your local machine run: ``` -fab test -H raspberrypi.local +fab -H raspberrypi.local test ``` where ```raspberrypi.local``` is the hostname of your Raspberry Pi. + +## Generate and upload test coverage data +1. From inside your testing environment on your local machine run the following line. (This will also run tests first if any relevant files have changed since coverage data was last generated.) +``` +fab -H raspberrypi.local coverage +``` +2. If your local repo is not on the same commit as the origin repo (i.e. the Github repo) or if you have untracked files, the script will ask you if you still want to proceed. (This is necessary because [coveralls.io](https://coveralls.io/github/jgillula/rpi-rfm69) needs to pull a copy of the repo from Github to show its analysis, and if the local code you're testing is different, the analysis won't match.) +3. Copy the coveralls repo token from [https://coveralls.io/github/jgillula/rpi-rfm69](https://coveralls.io/github/jgillula/rpi-rfm69) and provide it when prompted. diff --git a/tests/fabfile.py b/tests/fabfile.py index c177052..d36744b 100644 --- a/tests/fabfile.py +++ b/tests/fabfile.py @@ -1,50 +1,51 @@ #encoding:UTF-8 +# pylint: disable=missing-docstring # ============================================================================= # This fabfile will setup and run test on a remote Raspberry Pi # ============================================================================= -import socket -from os import sep, remove -from fabric.api import cd, lcd, task -from fabric.operations import run, local, prompt, put, sudo -from fabric.network import needs_host -from fabric.state import env, output +from os import environ +from fabric.api import cd, task, shell_env +from fabric.operations import run, prompt, sudo +from fabric.state import env from fabric.contrib import files from fabric.contrib.project import rsync_project -from fabtools import mysql -from fabtools import user, group, require, deb -from fabtools.python import virtualenv, install_requirements, install +from fabtools.python import virtualenv, install_requirements from termcolor import colored -from unipath import Path, DIRS +from git import Repo # ============================================================================= -# SETTINGS +# SETTINGS # ============================================================================= class Settings: DEPLOY_USER = "pi" # Username for access to pi - ROOT_NAME = "rfm69-test" # A system friendly name for test project - DIR_PROJ = "/srv/" + ROOT_NAME + "/" # The root - DIR_ENVS = DIR_PROJ + 'envs/' # Where the Virtual will live - DIR_CODE = DIR_PROJ + 'tests/' # Where the tests will live - - SYNC_DIRS = [ - ("./", DIR_CODE), - ("../RFM69", DIR_CODE) - ] + ROOT_NAME = "rpi-rfm69-test" # A system friendly name for test project + DIR_PROJ = "/srv/" + ROOT_NAME + "/" # The root + DIR_ENVS = DIR_PROJ + 'envs/' # Where the Virtual environment will live + DIR_CODE = DIR_PROJ + 'code/' # Where the code will live + + SYNC_DIRS = [ + ("../", DIR_CODE), + ] # Requirements REQUIREMENTS_FILES = [ - DIR_CODE + 'requirements_remote.txt', + DIR_CODE + 'tests/requirements_remote.txt', ] - TEST_PYTHON_VERSIONS = [ (3,7) ] + TEST_PYTHON_VERSIONS = [ (3,7) ] # pylint: disable= # ============================================================================= -# END OF SETTINGS +# END OF SETTINGS # ============================================================================= env.user = Settings.DEPLOY_USER +@task +def coverage(): + sync_files() + run_coverage() + @task def sync(): sync_files() @@ -64,7 +65,6 @@ def init(): create_virtualenv(version) install_venv_requirements(version) - # ============================================================================= # SUB TASKS # ============================================================================= @@ -75,17 +75,17 @@ def init(): def print_title(title): pad = "-" * (80 - len(title) - 4) - print (colored("-- {} {}".format(title,pad), 'blue', 'on_yellow')) + print(colored("-- {} {}".format(title, pad), 'blue', 'on_yellow')) def print_test_title(title): pad = "-" * (80 - len(title) - 4) - print (colored("-- {} {}".format(title,pad), 'white', 'on_blue')) + print(colored("-- {} {}".format(title, pad), 'white', 'on_blue')) def print_error(message): - print (colored(message, 'red')) + print(colored(message, 'red')) def print_success(message): - print (colored(message, 'green')) + print(colored(message, 'green')) # ---------------------------------------------------------------------------------------- # Sub Tasks - Project @@ -94,7 +94,7 @@ def print_success(message): # Make project folders def make_dirs(): print_title('Making folders') - for d in [Settings.DIR_PROJ, Settings.DIR_ENVS] + [ y for x, y in Settings.SYNC_DIRS]: + for d in [Settings.DIR_PROJ, Settings.DIR_ENVS] + [y for x, y in Settings.SYNC_DIRS]: exists = files.exists(d) print("File", d, "exists?", exists) if not exists: @@ -102,16 +102,17 @@ def make_dirs(): sudo('chown -R %s %s' % (env.user, d)) set_permissions() -# Sync project fioles to server +# Sync project files to server def sync_files(): print_title('Synchronising code') for local_dir, remote_dir in Settings.SYNC_DIRS: print('Copy from {} to {}'.format(local_dir, remote_dir)) - rsync_project( + rsync_project( remote_dir=remote_dir, local_dir=local_dir, - exclude=("fabfile.py","*.pyc",".git","*.db","*.sqlite3", "*.log", "*.csv", '__pycache__', '*.md','*.DS_Store', 'test-node/', 'requirements_local.txt', 'test_venv'), - extra_opts="--filter 'protect *.csv' --filter 'protect *.json' --filter 'protect *.db'", + exclude=("*.pyc", "*.db", "*.sqlite3", "*.log", "*.csv", + '__pycache__', '*.DS_Store', '*~', 'venv_*'), + extra_opts="--filter 'protect *.csv' --filter 'protect *.json' --filter 'protect *.db' --exclude-from=../.gitignore", delete=False ) @@ -139,7 +140,7 @@ def create_virtualenv(py_version): # Install Python requirments def install_venv_requirements(py_version): - env_path, ver_name, env_name = get_env(py_version) + env_path, _, _ = get_env(py_version) print_title('Installing remote virtual env requirements') with virtualenv(env_path): for path in Settings.REQUIREMENTS_FILES: @@ -155,4 +156,57 @@ def run_tests(py_version): print_test_title('Running tests in venv: {}'.format(env_path)) with virtualenv(env_path): with cd(Settings.DIR_CODE): - run('pytest -x -rs') + run('coverage run --omit=RFM69/registers.py --branch --concurrency=thread --source=RFM69 -m pytest -x -rs tests/') + +def run_coverage(): + repo = Repo("../.") + + uncommitted_files = [] + for diff in repo.head.commit.diff(None): + file_path = diff.b_rawpath.decode("utf-8") + if file_path.startswith("RFM69"): + uncommitted_files.append(file_path) + active_branch = repo.active_branch + repo.remote().fetch() + num_unpushed_commits = len(list(repo.iter_commits("{0}@{{u}}..{0}".format(active_branch)))) + num_unpulled_commits = len(list(repo.iter_commits("{0}..{0}@{{u}}".format(active_branch)))) + if uncommitted_files: + print("There are uncommitted changes in your repository:") + for file_path in uncommitted_files: + print(file_path) + if num_unpushed_commits > 0: + print("Your branch is ahead of '{}/{}' by {} commit(s)".format(repo.remote().name, active_branch, num_unpushed_commits)) + if num_unpulled_commits > 0: + print("Your branch is behind '{}/{}' by {} commit(s)".format(repo.remote().name, active_branch, num_unpulled_commits)) + if (uncommitted_files or + num_unpushed_commits > 0 or + num_unpulled_commits > 0): + run_anyway = prompt("Continue running coverage anyway?", default="N") + if run_anyway.lower() not in ["y", "yes"]: + return + + py_version = Settings.TEST_PYTHON_VERSIONS[0] + env_path, _, _ = get_env(py_version) + print_test_title('Running coverage in venv: {}'.format(env_path)) + with virtualenv(env_path): + with cd(Settings.DIR_CODE): + repo_token = None + need_to_rerun_tests = not files.exists(".coverage") + if not need_to_rerun_tests: + get_mtime_cmd = "date -r \"{}\" +%s" + coverage_last_run_time = int(run(get_mtime_cmd.format(".coverage"), quiet=True)) + for filename in run("ls -1 tests/test_*.py", quiet=True).split() + run("ls -1 RFM69/*.py", quiet=True).split(): + mtime = int(run(get_mtime_cmd.format(filename), quiet=True)) + need_to_rerun_tests |= (mtime > coverage_last_run_time) + if need_to_rerun_tests: + print_title("Need to run tests to generate coverage file first") + run_tests(py_version) + if files.exists(".coverage"): + run('coverage report') + if "COVERALLS_REPO_TOKEN" not in environ: + repo_token = prompt("Enter your coveralls repo_token to upload the results (or just hit enter to cancel):") + else: + repo_token = environ["COVERALLS_REPO_TOKEN"] + if repo_token: + with shell_env(COVERALLS_REPO_TOKEN=repo_token): + run("coveralls") diff --git a/tests/requirements_local.txt b/tests/requirements_local.txt index 3c9e1df..a74a9eb 100644 --- a/tests/requirements_local.txt +++ b/tests/requirements_local.txt @@ -1,4 +1,4 @@ termcolor -unipath fabric3 -fabtools3 \ No newline at end of file +fabtools3 +GitPython diff --git a/tests/requirements_remote.txt b/tests/requirements_remote.txt index a7526a2..c47352e 100644 --- a/tests/requirements_remote.txt +++ b/tests/requirements_remote.txt @@ -1,4 +1,4 @@ pytest -pytest-timeout -RPI.GPIO -spidev \ No newline at end of file +coveralls +RPi.GPIO +spidev diff --git a/tests/test-node/test-node.ino b/tests/test-node/test-node.ino index 6825751..9dd4e09 100644 --- a/tests/test-node/test-node.ino +++ b/tests/test-node/test-node.ino @@ -46,6 +46,8 @@ #define RF69_IRQ_PIN 3 #endif +#define RUN_TEST(testName, delayTime) runTest(#testName, testName, delayTime) ? numPassed++ : numFailed++; + RFM69 radio(RF69_SPI_CS, RF69_IRQ_PIN, false); @@ -57,13 +59,13 @@ void setup() { #if defined(RF69_LISTENMODE_ENABLE) radio.listenModeEnd(); #endif - radio.spyMode(true); + #ifdef IS_RFM69HW_HCW radio.setHighPower(); //must include this only for RFM69HW/HCW! #endif Serial.println("Setup complete"); #if defined(RF69_LISTENMODE_ENABLE) - Serial.println("Note: Tests will include listenModeSendBurst"); + Serial.println("Note: Tests will include listen_mode_send_burst"); #else Serial.println("Note: Skipping testing listenModeSendBurst since it's not set up"); #endif @@ -71,31 +73,103 @@ void setup() { } +char* data = null; +uint8_t datalen = 0; + +bool use_encryption = false; + void loop() { Serial.println("Ready to begin tests"); - // All test names are as named in test_radio.py - char* data = null; - uint8_t datalen = 0; bool success; + uint8_t numPassed = 0; + uint8_t numFailed = 0; + + // test_radio.py + radio.encrypt("sampleEncryptKey"); + // This is a hack since there's a bug regarding listen mode and encryption in the RFM69 library + use_encryption = true; + + RUN_TEST(test_transmit, 0); + RUN_TEST(test_receive, 1000); + RUN_TEST(test_txrx, 0); +#if defined(RF69_LISTENMODE_ENABLE) + RUN_TEST(test_listenModeSendBurst, 0); +#endif - // test_transmit - Serial.println("----- test_transmit -----"); + // test_radio_broadcast.py + RUN_TEST(test_broadcast_and_promiscuous_mode, 0); + + // test_radio_threadsafe.py + radio.encrypt(0); + // This is a hack since there's a bug regarding listen mode and encryption in the RFM69 library + use_encryption = false; + RUN_TEST(test_transmit, 0); + RUN_TEST(test_receive, 1000); + RUN_TEST(test_txrx, 0); +#if defined(RF69_LISTENMODE_ENABLE) + RUN_TEST(test_listenModeSendBurst, 0); +#endif + + + Serial.println(String("Tests complete: ") + numPassed + String(" passed, ") + numFailed + String(" failed.")); + Serial.println(); +} + + +// ********************************************************************************** +// Tests +// ********************************************************************************** + +bool test_broadcast_and_promiscuous_mode(String& failureReason) { while (!radio.receiveDone()) delay(1); getMessage(data, datalen); - if (radio.ACKRequested()) radio.sendACK(radio.SENDERID); - Serial.println(); - // test_receive - Serial.println("----- test_receive -----"); + char* response = new char[datalen]; + for (uint8_t i = 0; i < datalen; i++) { + response[i] = data[datalen - i - 1]; + } + Serial.println("Replying with '" + bufferToString(response, datalen) + "' (length " + String(datalen, DEC) + ")..."); + delay(100); + radio.send(47, response, datalen); + + return true; +} + +bool test_transmit(String& failureReason) { + bool result = false; + while (!radio.receiveDone()) delay(1); + getMessage(data, datalen); + if (radio.ACKRequested()) { + radio.sendACK(radio.SENDERID); + char goal_string[6] = {'B', 'a', 'n', 'a', 'n', 'a'}; + if (datalen >= sizeof(goal_string)) { + if (strncmp(data, goal_string, 6) == 0) { + return true; + } else { + failureReason = String("Received string '") + String(data) + String("' is not identical to '") + String(goal_string) + String("'"); + } + } else { + failureReason = String("Failed! Datalen should have been ") + sizeof("Banana") + String(" but was ") + String(datalen); + } + } + + return false; +} + +bool test_receive(String& failureReason) { char test_message[] = "Apple"; - delay(1000); - Serial.print(String("Sending test message '") + test_message + String("' of size ") + String(sizeof(test_message), DEC) + String("...")); - success = radio.sendWithRetry(1, test_message, sizeof(test_message), 0); - Serial.println(success ? "Success!" : "Failed"); - Serial.println(); + Serial.println(String("Sending test message '") + test_message + String("' of size ") + String(sizeof(test_message), DEC) + String("...")); + bool result = radio.sendWithRetry(1, test_message, sizeof(test_message), 5, 1000); + if (result) { + return true; + } else { + failureReason = String("No ack to our message"); + } - // test_txrx - Serial.println("----- test_txrx -----"); + return false; +} + +bool test_txrx(String& failureReason) { while (!radio.receiveDone()) delay(1); getMessage(data, datalen); if (radio.ACKRequested()) radio.sendACK(radio.SENDERID); @@ -103,15 +177,18 @@ void loop() { for (uint8_t i = 0; i < datalen; i++) { response[i] = data[datalen - i - 1]; } - Serial.print("Replying with '" + bufferToString(response, datalen) + "' (length " + String(datalen, DEC) + ")..."); - success = radio.sendWithRetry(1, response, datalen, 0); - Serial.println(success ? "Success!" : "Failed"); + Serial.println("Replying with '" + bufferToString(response, datalen) + "' (length " + String(datalen, DEC) + ")..."); + delay(100); + bool result = radio.sendWithRetry(1, response, datalen, 5, 1000); + if (!result) { + failureReason = String("No ack to our message"); + } delete response; - Serial.println(); -#if defined(RF69_LISTENMODE_ENABLE) - // test_listenModeSendBurst - Serial.println("----- test_listenModeSendBurst -----"); + return result; +} + +bool test_listenModeSendBurst(String& failureReason) { Serial.println("Entering listen mode and going to sleep"); Serial.flush(); radio.listenModeStart(); @@ -119,23 +196,40 @@ void loop() { LowPower.powerDown(SLEEP_FOREVER, ADC_OFF, BOD_OFF); if (radio.DATALEN > 0) burst_time_remaining = radio.RF69_LISTEN_BURST_REMAINING_MS; getMessage(data, datalen); - radio.listenModeEnd(); + Serial.println(String("Powering down for ") + burst_time_remaining + String("msec")); Serial.flush(); - delay(1000); LowPower.longPowerDown(burst_time_remaining); - response = new char[datalen]; + radio.listenModeEnd(); + char* response = new char[datalen]; for (uint8_t i = 0; i < datalen; i++) { response[i] = data[datalen - i - 1]; } - Serial.print("Replying with '" + bufferToString(response, datalen) + "' (length " + String(datalen, DEC) + ")..."); - success = radio.sendWithRetry(1, response, datalen, 0); - Serial.println(success ? "Success!" : "Failed"); + if (use_encryption) radio.encrypt("sampleEncryptKey"); + delay(10); + Serial.println("Replying with '" + bufferToString(response, datalen) + "' (length " + String(datalen, DEC) + ")..."); + bool result = radio.sendWithRetry(1, response, datalen, 5, 1000); + if (!result) { + failureReason = String("No ack to our message"); + } delete response; - Serial.println(); -#endif - Serial.println("Tests complete"); + return result; +} + +// ********************************************************************************** +// Utility functions +// ********************************************************************************** +bool runTest(String testName, bool (*test)(String&), uint32_t delayTime) { + Serial.println(String("----- ") + testName + String(" -----")); + if (delayTime > 0) Serial.println(String("Waiting ") + delayTime / 1000.0 + String(" seconds before continuing...")); + delay(delayTime); + String failureReason = String(); + bool result = test(failureReason); + if (!result) { + Serial.println(String("Failed! ") + failureReason); + } Serial.println(); + return result; } diff --git a/tests/config.py b/tests/test_config.py similarity index 91% rename from tests/config.py rename to tests/test_config.py index 77b638d..96c3395 100644 --- a/tests/config.py +++ b/tests/test_config.py @@ -1,3 +1,5 @@ +# pylint: disable=unused-import,missing-docstring + from RFM69 import FREQ_315MHZ, FREQ_433MHZ, FREQ_868MHZ, FREQ_915MHZ # You must uncomment one of these for tests to work diff --git a/tests/test_init.py b/tests/test_init.py index deb0fc5..b9f66ad 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -1,8 +1,11 @@ +# pylint: disable=pointless-statement,missing-docstring,undefined-variable + +import warnings import pytest +from test_config import * from RFM69 import Radio -from config import * -def test_config(): +def test_config_set_up(): try: FREQUENCY INTERRUPT_PIN @@ -10,12 +13,12 @@ def test_config(): SPI_DEVICE IS_HIGH_POWER except NameError: - pytest.fail("You must define your radio configuration in tests/config.py in order to run the tests") + pytest.fail("You must define your radio configuration in tests/test_config.py in order to run the tests") def test_init_success(): radio = Radio(FREQUENCY, 1, interruptPin=INTERRUPT_PIN, resetPin=RESET_PIN, spiDevice=SPI_DEVICE) - assert type(radio) == Radio + assert isinstance(radio, Radio) def test_init_bad_interupt(): with pytest.raises(ValueError) as _: @@ -33,6 +36,8 @@ def test_init_bad_spi_device(): with pytest.raises(IOError) as _: Radio(FREQUENCY, 1, spiDevice=-1, interruptPin=INTERRUPT_PIN, resetPin=RESET_PIN) -# def test_encryption_key_set(): -# with Radio(FREQUENCY, 1, encryptionKey="sampleEncryptKey") as radio: -# assert radio._enc +def test_deprecation_warnings(): + with Radio(FREQUENCY, 1, 100, verbose=True, interruptPin=INTERRUPT_PIN, resetPin=RESET_PIN, spiDevice=SPI_DEVICE, isHighPower=IS_HIGH_POWER, encryptionKey="sampleEncryptKey") as radio: + with warnings.catch_warnings(record=True) as w: + radio.packets + assert issubclass(w[-1].category, DeprecationWarning) diff --git a/tests/test_packet.py b/tests/test_packet.py new file mode 100644 index 0000000..0043db9 --- /dev/null +++ b/tests/test_packet.py @@ -0,0 +1,12 @@ +# pylint: disable=missing-docstring + +from RFM69 import Packet + +def test_packet(): + # Data corresponds to the string "this is a test" + packet = Packet(1, 2, -10, [116, 104, 105, 115, 32, 105, 115, 32, 97, 32, 116, 101, 115, 116]) + print(packet.to_dict()) + print(packet.to_dict("%b %d %Y %H:%M:%S")) + assert packet.data_string == "this is a test" + print(repr(packet)) + print(str(packet)) diff --git a/tests/test_radio.py b/tests/test_radio.py index 433b045..31c835a 100644 --- a/tests/test_radio.py +++ b/tests/test_radio.py @@ -1,55 +1,90 @@ -import pytest +# pylint: disable=pointless-statement,missing-docstring,protected-access,undefined-variable + import time import random +import pytest +import RPi.GPIO as GPIO # pylint: disable=consider-using-from-import +from test_config import * from RFM69 import Radio, RF69_MAX_DATA_LEN -from config import * def test_transmit(): - with Radio(FREQUENCY, 1, 100, verbose=True, interruptPin=INTERRUPT_PIN, resetPin=RESET_PIN, spiDevice=SPI_DEVICE, isHighPower=IS_HIGH_POWER) as radio: + with Radio(FREQUENCY, 1, 99, verbose=True, interruptPin=INTERRUPT_PIN, resetPin=RESET_PIN, spiDevice=SPI_DEVICE, isHighPower=IS_HIGH_POWER, encryptionKey="sampleEncryptKey") as radio: + # Test setting the network ID to the value we'll actually test with + radio.set_network(100) + # Try sending to a node that isn't on, and don't require an ack + success = radio.send(47, "Not a banana", attempts=1, require_ack=False) + assert success is None + # Try sending to a node that isn't on, and require an ack, should return false + success = radio.send(47, "This should return false", attempts=2, waitTime=10) + assert success is False success = radio.send(2, "Banana", attempts=5, waitTime=100) - assert success == True + assert success is True def test_receive(): - with Radio(FREQUENCY, 1, 100, verbose=True, interruptPin=INTERRUPT_PIN, resetPin=RESET_PIN, spiDevice=SPI_DEVICE, isHighPower=IS_HIGH_POWER) as radio: + with Radio(FREQUENCY, 1, 100, verbose=True, interruptPin=INTERRUPT_PIN, resetPin=RESET_PIN, spiDevice=SPI_DEVICE, isHighPower=IS_HIGH_POWER, encryptionKey="sampleEncryptKey") as radio: timeout = time.time() + 5 while time.time() < timeout: - for packet in radio.get_packets(): - assert packet.data == [ord(x) for x in "Apple\0"] - return True - time.sleep(0.1) + if radio.num_packets() > 0: + for packet in radio.get_packets(): + assert packet.data == [ord(x) for x in "Apple\0"] + time.sleep(1.0) + return True + time.sleep(0.01) return False - + def test_txrx(): - with Radio(FREQUENCY, 1, 100, verbose=True, interruptPin=INTERRUPT_PIN, resetPin=RESET_PIN, spiDevice=SPI_DEVICE, isHighPower=IS_HIGH_POWER) as radio: - test_message = [random.randint(0,255) for i in range(RF69_MAX_DATA_LEN)] + # For more test coverage, we'll test this using BCM pin numbers + # To do that, we have to cleanup the entire GPIO object first + GPIO.setwarnings(False) + GPIO.cleanup() + # The format of this dict is (Raspberry Pi pin number: GPIO number) + board_to_bcm_map = {3: 2, 5: 3, 7: 4, 8: 14, 10: 15, 11: 17, 12: 18, 13: 27, 15: 22, 16: 23, 18: 24, 19: 10, 21: 9, 22: 25, 23: 11, 24: 8, 26: 7, 27: 0, 28: 1, 29: 5, 31: 6, 32: 12, 33: 13, 35: 19, 36: 16, 37: 26, 38: 20, 40: 21} + with Radio(FREQUENCY, 1, 100, verbose=True, use_board_pin_numbers=False, interruptPin=board_to_bcm_map[INTERRUPT_PIN], resetPin=board_to_bcm_map[RESET_PIN], spiDevice=SPI_DEVICE, isHighPower=IS_HIGH_POWER, encryptionKey="sampleEncryptKey") as radio: + test_message = [random.randint(0, 255) for i in range(RF69_MAX_DATA_LEN)] success = radio.send(2, test_message, attempts=5, waitTime=100) - assert success == True - radio.begin_receive() + assert success is True timeout = time.time() + 5 while (not radio.has_received_packet()) and (time.time() < timeout): - time.sleep(0.1) + time.sleep(0.01) assert radio.has_received_packet() packets = radio.get_packets() - print(test_message) - print(packets[0].data) - assert packets[0].data == [x for x in reversed(test_message)] - + assert packets[0].data == list(reversed(test_message)) + time.sleep(1.0) + # Since we used BCM pin numbers, we have to clean up all of GPIO again + GPIO.cleanup() -def test_listenModeSendBurst(): +def test_listen_mode_send_burst(): try: TEST_LISTEN_MODE_SEND_BURST - time.sleep(1) - with Radio(FREQUENCY, 1, 100, verbose=True, interruptPin=INTERRUPT_PIN, resetPin=RESET_PIN, spiDevice=SPI_DEVICE, isHighPower=IS_HIGH_POWER) as radio: + with Radio(FREQUENCY, 1, 100, verbose=True, interruptPin=INTERRUPT_PIN, resetPin=RESET_PIN, spiDevice=SPI_DEVICE, isHighPower=IS_HIGH_POWER, encryptionKey="sampleEncryptKey") as radio: + # For more test coverage, let's try setting the listen mode durations outside the acceptable range, like 70 seconds + radio.listen_mode_set_durations(256, 70000000) + radio.listen_mode_set_durations(70000000, 1000400) + # And then let's check and make sure the default values are still being used + assert radio.listen_mode_get_durations() == (256, 1000400) # These are the default values test_message = "listen mode test" - radio.listenModeSendBurst(2, test_message) - radio.begin_receive() + radio.listen_mode_send_burst(2, test_message) timeout = time.time() + 5 while (not radio.has_received_packet()) and (time.time() < timeout): time.sleep(0.01) assert radio.has_received_packet() packets = radio.get_packets() assert packets[0].data == [ord(x) for x in reversed(test_message)] + time.sleep(1.0) except NameError: - print("Skipping testing listenModeSendBurst") - pytest.skip("Skipping testing listenModeSendBurst since it's not set up") + print("Skipping testing listen_mode_send_burst") + pytest.skip("Skipping testing listen_mode_send_burst since it's not set up") + +def test_general(): + with Radio(FREQUENCY, 1, 100, verbose=True, interruptPin=INTERRUPT_PIN, resetPin=RESET_PIN, spiDevice=SPI_DEVICE, isHighPower=IS_HIGH_POWER, encryptionKey="sampleEncryptKey") as radio: + # This is just here for test coverage + radio._readRSSI(True) + radio.read_registers() + # Get more test coverage in _canSend + radio.sleep() + assert radio._canSend() is False + # Put the radio in standby to do more test coverage for _canSend + radio._setMode(1) + assert radio._canSend() is True + radio._setMode(2) diff --git a/tests/test_radio_broadcast.py b/tests/test_radio_broadcast.py new file mode 100644 index 0000000..2cefc99 --- /dev/null +++ b/tests/test_radio_broadcast.py @@ -0,0 +1,17 @@ +# pylint: disable=protected-access,missing-docstring,undefined-variable + +import random +from test_config import * +from RFM69 import Radio, RF69_MAX_DATA_LEN + + +def test_broadcast_and_promiscuous_mode(): + with Radio(FREQUENCY, 1, 100, verbose=True, interruptPin=INTERRUPT_PIN, resetPin=RESET_PIN, spiDevice=SPI_DEVICE, isHighPower=IS_HIGH_POWER, encryptionKey="sampleEncryptKey") as radio: + test_message = [random.randint(0, 255) for i in range(RF69_MAX_DATA_LEN)] + # first we send the message out to the world + radio.broadcast(test_message) + # And we expect to get it back by promiscuous mode + radio._promiscuous(True) + packet = radio.get_packet() + assert packet.data == list(reversed(test_message)) + assert packet.receiver != 1 diff --git a/tests/test_radio_threadsafe.py b/tests/test_radio_threadsafe.py new file mode 100644 index 0000000..07a33a9 --- /dev/null +++ b/tests/test_radio_threadsafe.py @@ -0,0 +1,53 @@ +# pylint: disable=pointless-statement,missing-docstring,undefined-variable + +import time +import random +import pytest +from test_config import * +from RFM69 import Radio, RF69_MAX_DATA_LEN + + +def test_transmit_threadsafe(): + with Radio(FREQUENCY, 1, 100, verbose=True, interruptPin=INTERRUPT_PIN, resetPin=RESET_PIN, spiDevice=SPI_DEVICE, isHighPower=IS_HIGH_POWER) as radio: + # Test getting a packet when we shouldn't have one + packet = radio.get_packet(block=False) + assert packet is None + # Then test sending a packet + success = radio.send(2, "Banana", attempts=5, waitTime=1000) + assert success is True + +def test_receive_threadsafe(): + with Radio(FREQUENCY, 1, 100, verbose=True, interruptPin=INTERRUPT_PIN, resetPin=RESET_PIN, spiDevice=SPI_DEVICE, isHighPower=IS_HIGH_POWER) as radio: + packet = radio.get_packet(timeout=5) + if packet: + assert packet.data == [ord(x) for x in "Apple\0"] + return True + return False + +def test_txrx_threadsafe(): + with Radio(FREQUENCY, 1, 100, verbose=True, interruptPin=INTERRUPT_PIN, resetPin=RESET_PIN, spiDevice=SPI_DEVICE, isHighPower=IS_HIGH_POWER) as radio: + # We'll try sending too big a packet to get some code coverage + test_message = [random.randint(0, 255) for i in range(RF69_MAX_DATA_LEN+10)] + success = radio.send(2, test_message, attempts=5, waitTime=100) + assert success is True + # If we sleep for one second, the packet should already be there so we can get more code coverage in testing + time.sleep(1) + packet = radio.get_packet(timeout=5) + assert packet is not None + assert packet.data == list(reversed(test_message[0:RF69_MAX_DATA_LEN])) + + +def test_listen_mode_send_burst_threadsafe(): + try: + TEST_LISTEN_MODE_SEND_BURST + with Radio(FREQUENCY, 1, 100, verbose=True, interruptPin=INTERRUPT_PIN, resetPin=RESET_PIN, spiDevice=SPI_DEVICE, isHighPower=IS_HIGH_POWER) as radio: + # Try sending bytes instead of a string to get more test coverage + test_message = [108, 105, 115, 116, 101, 110, 32, 109, 111, 100, 101, 32, 116, 101, 115, 116] # this corresponds to the string "listen mode test" + radio.listen_mode_send_burst(2, test_message) + radio.begin_receive() + packet = radio.get_packet(timeout=5) + assert packet is not None + assert packet.data == list(reversed(test_message)) + except NameError: + print("Skipping testing listen_mode_send_burst") + pytest.skip("Skipping testing listen_mode_send_burst since it's not set up")