def transmitData(self, buf, port=1, ack=False): ''' transmit 'buf' data through an optional 'port' ack==False --> unconfirmed message, otherwise confirmed ''' if (self._link.is_open is not True): raise Exception( "serial port is not open, unable to transfer data!") log.info("[TX_DATA] payload = '%s'" % str(buf)) # check input parameters if (not isinstance(port, int) or int(port) not in __class__.LORA_TX_PORTS_RNG): raise Exception( "either port '%s' is not an integer or not in range!" % str(port)) # prepare data to get sent out = ''.join(['%02x' % ord(c) for c in buf]) _ackMode = "uncnf" if ack is False else "cnf" if (self._is_fwrev_ge('1.0.5')): _res = self._command("mac tx %s %d %s" % (_ackMode, port, out)) else: _res = self._command("mac tx %s %d %s" % (_ackMode, port, out) + '\r\n') if (_res != "ok"): log.debug("## TX refused: '%s'" % _res) return False log.debug( "TX msg has been accepted ... transmission will occur soon ...") 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 __del__(self): ''' destructor method ''' log.info("... call for destructor of '%s' instance ..." % __class__.__name__) # close LoRaWAN exchange # TODO! # stop serial link self.stop_serial() log.info("'%s' instance deleted" % __class__.__name__)
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 _loraABPjoin(self, deveui=None, nwkskey=None, aapskey=None): ''' join with ABP protocol ''' log.info("Start ABP join procedure ...") # ... configure additional bands log.debug("\t... adding channels and dutyCycles defs ...") self._command("mac set ch freq 3 867100000", True) self._command("mac set ch freq 4 867300000", True) self._command("mac set ch freq 5 867500000", True) self._command("mac set ch freq 6 867700000", True) self._command("mac set ch freq 7 867900000", True) # TODO!! raise NotImplementedError("ABP join not implemented") return False
def stop_serial(self): ''' release all resources allocated for RN2483 comm through serial link ''' log.info("stop of serial link is on way ...") # stop threads # free resources # close serial link if (self._serial_lock): self._serial_lock.acquire() if (self._link is not None and self._link.is_open is not False): self._link.close() self._link = None if (self._serial_lock): self._serial_lock.release() del (self.serial_lock) self._serial_lock = None
def _loraOTAAjoin(self, deveui=None, appeui=None, appkey=None): ''' join with OTAA protocol returns True, False or raise exception ''' log.info("Start OTAA join procedure ...") if (deveui is not None): self.devEUI = deveui if (appeui is not None): self.appEUI = appeui if (appkey is not None): self.appKEY = appkey # try to join _retry = 5 _sleep = 2 while (self._shutdown_event.is_set() is not True and _retry > 0): _res = self._command( "mac join otaa" ) # check=false because we don't want an exception if answer differs from 'ok' print("(1) join res = %s" % _res) # check for acceptance of command if (_res != "ok"): log.info( "[join][devEUI=0x%s, appEUI=0x%s, appKEY=0x%s] command failed: '%s'" % (self.devEUI, self.appEUI, self.appKEY, _res)) log.debug("... sleeping a bit before retrying join ...") time.sleep(_sleep) _sleep *= 2 _retry -= 1 continue # ... and now wating for the join result print("(2) join ...") _res = self.receiveData(timeout=10000) # wait up to ten seconds print("(3) join _res = %s" % str(_res)) if (_res is None or _res.endswith('\r\n') is not True): log.warning("# WARNING: partial retrieval from serial link !!") raise Eception("partial data from serial received :(") _res = _res.rstrip('\r\n') log.debug("\t received join answer: '%s'" % _res) if (_res == 'accepted'): log.info("\t JOIN successful :)") return True log.info( "[join][devEUI=0x%s, appEUI=0x%s, appKEY=0x%s] connection failed with code '%s'" % (self.devEUI, self.appEUI, self.appKEY, str(_res))) log.debug("... sleeping a bit before retrying join ...") time.sleep(_sleep) _sleep *= 2 _retry -= 1 return False
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)