def _is_fwrev_ge(self, val): ''' check if current fw release match is Greater or Equal to the one specified as a parameter. ''' _cur_fwrev = self._fw_rev[:] # extract integers try: _expected_fwrev = [int(x) for x in val.split('.')] except Exception as ex: log.error("unable to parse fw_rev '%s' " + str(ex)) raise ex # extends list to match each others difflen = len(_cur_fwrev) - len(_expected_fwrev) if (difflen > 0): _expected_fwrev.extend([0] * abs(difflen)) elif (difflen < 0): _cur_fwrev.extend([0] * abs(difflen)) # check fw revisions for cur, expected in zip(_cur_fwrev, _expected_fwrev): if (cur > expected): return True elif (cur < expected): return False # revisions are the same return True
def connect(self, mode=None, **kwargs): ''' join either in OTAA or ABP mode ''' # SF12 for join mode log.debug("[join] setting dr to 0 as default for join mode ...") self._command("mac set dr 0", True) self._command("radio set sf sf12", True) _res = None if (mode.lower() == 'otaa'): _res = self._loraOTAAjoin(**kwargs) elif (mode.lower() == 'abp'): _res = self._loraABPjoin(**kwargs) else: log.error("unknwon join mode '%s' ?!?!, ought to be one of %s" % (str(mode), str(__class__.LORA_JOIN_MODES))) return False if _res is not True: return False # set channels status and data rates if (mode.lower() == 'abp' or self._command("mac get adr") == 'off'): log.debug("\t... setting channels DataRates (DR) and status ...") # set data rates (useless in OTAA with ADR enabled ?) # 0 --> SF12/125kHz # 1 --> SF11/125kHz # 2 --> SF10/125kHz # 3 --> SF9/125kHz # 4 --> SF8/125kHz # 5 --> SF7/125kHz self._command("mac set ch drrange 3 0 5", True) self._command("mac set ch drrange 4 0 5", True) self._command("mac set ch drrange 5 0 5", True) self._command("mac set ch drrange 6 0 5", True) self._command("mac set ch drrange 7 0 5", True) # enable channels (useless in OTAA with ADR enabled ?) self._command("mac set ch status 3 on", True) self._command("mac set ch status 4 on", True) self._command("mac set ch status 5 on", True) self._command("mac set ch status 6 on", True) self._command("mac set ch status 7 on", True) # set duty cycle log.debug("\t... setting duty-cycle to '%d' ..." % self._dcycle) for _ch in range(8): self._command("mac set ch dcycle %d %d" % (_ch, self._dcycle), True) # is there specified a DR for data exchange ? if self._data_rate is not None: log.info("set data_rate = '%d' for DATA exchanges ..." % self._data_rate) self._command("mac set dr %d" % self._data_rate, True) # is there specified a SF for data exchange ? if self._data_sf is not None and self._data_sf.lower( ) in __class__.LORA_SPREAD_FACTOR: log.info("set data_sf = '%s' for DATA exchanges ..." % self._data_sf.lower()) self._command("radio set sf %s" % self._data_sf.lower(), True)
def wdt(self, val): ''' set rx watchdog ''' try: if (0 <= int(val) <= __class__.LORA_RX_WATCHDOG): self._command("radio set wdt " + str(val), True) return log.warning("out of limits rx watchdof '%s' ?!?!" % str(val)) except Exception as ex: log.error("error in watchdog timeout value '%s' ?!?!" % str(val)) raise ex
def pwr(self, val): ''' set TX power ''' try: if (int(val) not in __class__.LORA_RADIO_TX_POWER): log.warning("invalid radio tx pwer provided") return except ValueError as ex: log.error("tx PWR '%s' ought to be an integer ... aborting" % (str(val))) raise ex self._command("radio set pwr " + str(val), True)
def freq(self, val): ''' set radio frequency ''' try: for f in __class__.LORA_RADIO_FREQUENCIES: if (int(val) == int(f * 1000000)): self._command("radio set freq " + str(val), True) return log.warning("unknwon frequency '%s' ?!?!" % str(val)) except Exception as ex: log.error("error in frequency value '%s' ?!?!" % str(val)) raise ex
def bw(self, val): ''' set bandwidth ''' try: for _bw in __class__.LORA_BANDWIDTH: if (int(val) == _bw): self._command("radio set bw " + str(val), True) return log.warning("unknwon bandwidth '%s', ought to be one of %s" % (str(val), str(__class__.LORA_BANDWIDTH))) except Exception as ex: log.error("error in bandwidth value '%s' ?!?!" % str(val)) raise ex
def loraSleep(self, val): ''' activate lora system sleep ''' try: if (__class__.LORA_SYS_SLEEP[0] <= int(val) <= __class__.LORA_SYS_SLEEP[1]): log.info("LoRa module will enter in sleep mode for %dms ..." % int(val)) self._command("sys sleep " + str(val)) # respond 'ok' after wakeup return log.warning("out of range sleep value '%s' !" % str(val)) except Exception as ex: log.error("invalid sleep timeout (ms) '%s' !" % str(val))
def start_serial(self): ''' verify serial link is valid ... and a RN2483 is connected to ''' if (self._link is not None): log.warning("# serial link is already active !") return try: # warning: port is immediately opened on link creation log.info("Initialise serial '%s' at speed '%d' ..." % (self.serial_port, self.serial_speed)) self._link = serial.Serial( port=self.serial_port, baudrate=self.serial_speed, bytesize=__class__.DEFAULT_SERIAL_BITS, parity=__class__.DEFAULT_SERIAL_PARITY, stopbits=__class__.DEFAULT_SERIAL_STOP, timeout=__class__.DEFAULT_SERIAL_TIMEOUT, exclusive=True) # hardware reset of the radio module self.radioModuleHWreset() # on reset, RN2483 will send its firmware release # TODO: launch thread waiting for serial data _answer = str() _answer = self._link.read_until().decode( "utf-8") # serial timeout applies here if (len(_answer)): log.debug("[init] recveived str: %s" % (_answer)) if (not ("rn2483" in _answer.lower())): raise Exception( "RN2483 module not found because answer was: " + str(_answer)) self._fw_rev = [ int(x) for x in _answer.lower().split()[1].split('.') ] else: raise Exception("Lora module didn't answered!") except ValueError as ex: log.error("Firmware revision number are not integers: '%s'" % _answer.lower() + str(ex)) raise ex except Exception as ex: log.error("while opening serial port '%s' with baudrate=%d " % (self.serial_port, self.serial_speed) + str(ex)) raise ex log.debug("link '%s' @ '%d'bauds is validated as a serial port :)" % (self.serial_port, self.serial_speed)) # activate locking mechanism self._serial_lock = threading.Lock()
def batlvl(self, val): ''' set battery-level ''' if (val is None): self._batlvl = None return try: if (int(val) in __class__.LORA_BAT_LVL_RNG): _res = self._command("mac set bat %d" % val, True) if (_res is None or _res != 'ok'): raise Exception("error while setting battery level!") return log.warning("out-of-range battery-level '%s'" % str(val)) except Exception as ex: log.error("error in battery-level value '%s' ?!?!" % str(val)) raise ex
def main(): # Global variables global _shutdownEvent # print("\n###\nLoRaWAN powermeter demo based on RN2483") print("###") # create threading.event _shutdownEvent = threading.Event() # Trap CTRL+C (kill -2) signal.signal(signal.SIGINT, ctrlc_handler) # Parse arguments parser = argparse.ArgumentParser( description="LoRaWAN powermeter based on RN2483 \ \n Hit <ENTER> to terminate program.") parser.add_argument('-p', '--port', type=str, dest="serial_link", nargs='?', help="Serial port to use, eg. /dev/ttyAMA0.") parser.add_argument('-s', '--speed', type=int, dest="serial_link_speed", nargs='?', help="Absolute path to labels file.") parser.add_argument('--reset-pin', type=int, dest="reset_pin", nargs='?', help="RPi's GPIO connected to RST pin of RN2483.") parser.add_argument( '--set-dcycle', type=int, dest="duty_cycle", nargs='?', help="Set duty cycle percent from 0 to 100 on all channels.") # debug mode parser.add_argument('-d', '--debug', action="store_true", help="Toggle debug mode (True as default).") ARGS = parser.parse_args() #print(ARGS) # logging if (ARGS.debug is True): print("\n[DBG] DEBUG mode activated ... ") setLogLevel(logging.DEBUG) else: print("\n[LOG] level set to %d" % int(settings.log_level)) setLogLevel(settings.log_level) # Setup GPIOs GPIO.setmode(GPIO.BCM) ''' # # Modbus initialisation log.info("Instantiate and enable modbus backend ...") try: kwargs = dict() kwargs['shutdown_event'] = _shutdownEvent if( hasattr(settings, 'modbus_debug') is True ): kwargs['modbus_debug'] = settings.modbus_debug _backend = ModbusBackend(settings.modbus_link, settings.modbus_link_speed, **kwargs) _backend.enable() except Exception as ex: log.error("unable to initialise Modbus backend: " + str(ex) ) raise ex time.sleep(1) ''' # # LoRaWAN # # RN2483 serial initialisation log.info("Starting RN2483 initialisation ...") try: # instantiate device with optional specific configuration _deviceConfig = dict() # register shurdownEvent _deviceConfig['shutdown_event'] = _shutdownEvent # enable/disable ADR mode _deviceConfig['adr'] = True if (hasattr(settings, 'disable_adr') is True): if settings.disable_adr is True: _deviceConfig['adr'] = False # set data exchange DataRate mode 0:SF12/125kHz to 5:SF7/125kHz _deviceConfig['dr'] = 0 # SF12/125kHz if (hasattr(settings, 'data_rate') is True): _deviceConfig['dr'] = settings.data_rate # set data exchange DataRate spreading factor (default is SF12) if (hasattr(settings, 'data_sf') is True): _deviceConfig['sf'] = settings.data_sf # optional reset pin if (ARGS.reset_pin is not None): _deviceConfig['reset_pin'] = ARGS.reset_pin elif (hasattr(settings, 'reset_pin') is True): _deviceConfig['reset_pin'] = settings.reset_pin # optional duty cycle if (ARGS.duty_cycle is not None): _deviceConfig['duty_cycle'] = ARGS.duty_cycle elif (hasattr(settings, 'duty_cycle') is True): _deviceConfig['duty_cycle'] = settings.duty_cycle # # instantiate LoRaWAN device device = RN2483( ARGS.serial_link if ARGS.serial_link is not None else settings.serial_link, ARGS.serial_link_speed if ARGS.serial_link_speed is not None else settings.serial_link_speed, **_deviceConfig) # tell we're on external power supply device.batlvl = 0 except Exception as ex: log.error("### ERROR on RN2483 instantiation: " + str(ex)) raise ex log.info("RN2483 successfully instantiated :)") # RN2483 LoRaWAN initialisation log.info("Starting LoRaWAN initialisation ...") try: # [RADIO] LoRa parameters #device.mod = 'lora' # default modulation: LoRa #device.freq = int(868.1*1000000) # default to 868.1MHz device.pwr = 14 #device.sf = 'sf7' # default sf12 #device.cr = '4/8' # default 4/5 #device.bw = 250 # default 125kHz #device.crc = 'off' # default on # [MAC] LoRaWAN parameters device.devEUI = settings.deveui device.appEUI = settings.appeui device.appKEY = settings.appkey # save parameters #device.loraMacSave() except Exception as ex: log.error("### LoRaWAN initialization error: " + str(ex)) raise ex log.info("LoRaWAN setup successful :)") print(device) time.sleep(2) # # main loop txData = None while (not _shutdownEvent.is_set()): try: # need to join ? if (device.isConnected() is not True): # activate OTAA join device.connect(mode='otaa') # are we connected ? if (device.isConnected() is not True): time.sleep(5) continue # print device detailed status print(device) log.debug("sleeping a bit before continuing ...") time.sleep(5) _startTime = datetime.now() # # TX some data # TODO: make use of optional port :) log.info("Sending some data ...") txData = "hello from RN2483!" print("MyData = %s" % str(txData)) # # TX messages with or without acknowledge # Note: if 1% duty-cycle is disabled, best is to ask for acknowledge _ask4cnf = True if (hasattr(settings, 'ask_cnf') is True): _ask4cnf = settings.ask_cnf if (device.transmitData(str(txData), ack=_ask4cnf) is not True): log.debug("tx command not accepted by LoRaWAN stack :(") log.debug("... sleeping a bit before retrying ...") time.sleep(7) continue _txAnswer = None # LoRaWAN Class A | wait for some data in return from previous transmit log.info("Waiting for some data ...") while ((datetime.now() - _startTime).total_seconds() < settings.loraTimer and not _shutdownEvent.is_set()): print("_*_", end='', flush=True) rxData = device.receiveData() if (rxData is None): continue if (not rxData.endswith('\r\n')): log.warning("Partial data received !!\n\t'%s'" % str(rxData)) time.sleep(1) continue rxData = rxData.rstrip('\r\n') if (_txAnswer is not None): # this RX1 or RX2 or class C device ? log.info("Received data = '%s'" % str(rxData)) time.sleep(1) continue # TX answer received log.info("TX answer received: '%s'" % rxData) if (rxData == 'mac_tx_ok'): _txAnswer = rxData continue elif (rxData.startswith('mac_rx')): # RX1 or RX2 slots received data _rxFields = rxData.split() print("\n\tPort[%d] answer received: '%s'" % (int(_rxFields[1]), str(_rxFields[2:]))) # TODO: process incomming data print("\tTODO: process incoming data!") _txAnswer = rxData time.sleep(1) continue elif (rxData == 'mac_err'): # failure to send log.warning( "'mac_err' ... failed to transmit data ... mac status = 0x%08X ... try to resend :|" % (device.macStatus)) time.sleep(4) break elif (rxData.startswith('invalid_data_len')): # payload too large log.warning( "oversized payload ... (ADR mode??) ... try to resend :|" ) time.sleep(4) break elif (rxData.startswith('invalid')): # [aug.19] let's consider this as a warning ... log.warning( "there's something wrong in the TX process ... ... mac status = 0x%08X ... try to resend" % (device.macStatus)) time.sleep(4) else: log.warning("unknwown TX answer '%s' ?!?! ... continuing" % rxData) time.sleep(1) except Exception as ex: exc_type, exc_obj, exc_tb = sys.exc_info() log.error("exception '%s'(line %d): " % (exc_type, exc_tb.tb_lineno) + str(ex)) time.sleep(3) continue # destroy instance log.info("ending LoRaWAN ops ...") del (device) # GPIO cleanup GPIO.cleanup()
def __init__(self, port=None, speed=0, shutdown_event=None, *args, **kwargs): log.info("Initializing '%s' module ..." % __class__.__name__) self._addons = None self._link = None self.serial_lock = None self._appKEY = None self._batlvl = None self._data_rate = None self._shutdown_event = shutdown_event if kwargs is not None: self._addons = kwargs if port is not None: self.serial_port = port try: self.serial_speed = int(speed) if int( speed) != 0 else __class__.DEFAULT_SERIAL_SPEED except ValueError as ex: log.error("link_speed '%s' is not an integer ... aborting" % (str(speed))) raise ex # reset_pin option ? if kwargs.get('reset_pin', None) is not None: self._gpio_rst = int(kwargs['reset_pin']) GPIO.setup( self._gpio_rst, GPIO.HIGH ) # set HIGH before OUT mode to avoid spurious RST on RN2483 GPIO.setup(self._gpio_rst, GPIO.OUT) log.info("\tsetting GPIO '%d' as reset pin ..." % self._gpio_rst) # ... and remove from dict kwargs.pop('reset_pin', None) # thread related initialization self._condition = threading.Condition() self._thread = None # check link to test presence of serial adapter # and presence of a RN2483 module self.start_serial() if (self._fw_rev is None): raise Exception("unable to read FW revision of RN2483 module ?!?!") # radioModule factory reset settings self.loraFactoryReset() # LoRaWAN setup self._command("mac reset 868", True) # configure radio for 868MHz ops self._command( "mac set retx 2", True ) # for confirmed type messages, number of retransmissions until gateway ack self._command( "mac set dr 0", True ) # set data rate to 0:SF12/125kHz (slowest) 5:SF7/125kHz (fastest) self._command("mac set linkchk 600", True) # link check process interval #self._command("mac set ar on", True) # automatic reply on downlink reception # check for extra parameters if (self._addons): for key, val in self._addons.items(): # check for 'ADR' if (key.lower() == "adr"): if val is True: log.info("Enabling Adaptative Data Rate (ADR) ...") self._command("mac set adr on") else: log.info("Disabling Adaptative Data Rate (ADR) ...") self._command("mac set adr off") continue # check for 'DR' (i.e data rate) if (key.lower() == "dr"): log.info("Set DataRate ...") #self._command("mac set dr %d" % int(val)) self._data_rate = int(val) continue # check for 'duty_cycle' if (key.lower() == "duty_cycle"): log.info("Override duty cycle default value ...") self._dcycle = int(100 / int(val)) - 1 continue # check for 'SF' (i.e data spreading factor) if (key.lower() == "sf"): if val.lower() not in __class__.LORA_SPREAD_FACTOR: log.error("Specified SF '%s' not supported" % val.lower()) continue log.info("Set SpreadingFactor ...") #self._command("radio set sf %s" % val) self._data_sf = val.lower() continue log.debug("unknwon _addon parameter '%s' ?!?!" % str(key)) # end of setup time.sleep(0.5)