예제 #1
0
    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
예제 #2
0
 def classDevice(self):
     ''' get end-device class '''
     if (self._is_fwrev_ge('1.0.5')):
         # get end-device class (FW >= 1.0.5)
         return self._command("mac get class")
     else:
         log.debug("FW rev. is lower than '1.0.5'")
         return __class__.LORA_DEVICES_CLASSES[0]
예제 #3
0
 def loraMacSave(self):
     ''' save LoRaWAN parameters in NVM '''
     self._command(
         "mac save"
     )  # we don't check answer because it takes time to save to flash ...
     # mayeb longer than read timeout
     # wait until an answer
     res = self.receiveData(timeout=10000)
     if (res is None or not res.startswith('ok')):
         raise Exception("Failure to save mac!")
     log.debug("FLASH stored LoRa parameters :)")
     return True
예제 #4
0
    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()
예제 #5
0
    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
예제 #6
0
 def macStatus(self):
     ''' read back mac status '''
     _status = None
     _retry = 3
     while (_retry > 0 and self._shutdown_event.is_set() is not True):
         _raw_status = self._command("mac get status")
         try:
             _status = int(_raw_status, 16)
             break
         except ValueError as ex:
             log.debug("mac status '%s' is not a hex value!" % _raw_status)
             time.sleep(1)
             _retry -= 1
             continue
     if (_status is None or not isinstance(_status, int)):
         raise Exception("unable to obtain mac status ?!?!?!")
     return _status
예제 #7
0
    def _command(self, cmdIn, check=False):
        ''' send command to LoRa radio module '''
        result = str()
        with self._serial_lock:
            # first, flush input (recv) and output (send) buffer
            self._link.reset_input_buffer()
            self._link.reset_output_buffer()
            _cmd = cmdIn + '\r\n'
            self._link.write(_cmd.encode("utf-8"))
            #hexdump(_cmd.encode("utf-8"))
            result = self._link.read_until().decode("utf-8").rstrip('\r\n')
            if check and (result is None or result != 'ok'):
                raise Exception("LoRa cmd '%s' responded with '%s'" %
                                (cmdIn, result))

        if (check):
            log.debug("[LoRaCMD] '%s' returned '%s'" % (cmdIn, result))
        else:
            log.debug("[LoRaCMD] '%s' executed" % (cmdIn))
        return result
예제 #8
0
    def radioModuleHWreset(self):
        ''' this will make use of the DTR line to force a hardware reset
            of the radio module.
            return: nothing '''
        if (self._gpio_rst is not None):
            log.debug("[radioModule] HW reset through GPIO %d ..." %
                      (self._gpio_rst))
            GPIO.setup(self._gpio_rst, GPIO.LOW)
            time.sleep(__class__.DEFAULT_SERIAL_TIMEOUT /
                       2)  # just to get coherent with DTR reset
            GPIO.setup(self._gpio_rst, GPIO.HIGH)
            time.sleep(__class__.DEFAULT_SERIAL_TIMEOUT / 2)

        elif (self._link is not None):
            log.debug("[radioModule] HW reset through DTR line ...")
            # set polarity of DTR (RST line of RN2483)
            self._link.setDTR(True)  # activate RN2483's RST line
            self._link.reset_input_buffer()
            time.sleep(__class__.DEFAULT_SERIAL_TIMEOUT / 2)
            self._link.setDTR(False)  # release RN2483's RST line
            time.sleep(__class__.DEFAULT_SERIAL_TIMEOUT / 2)
예제 #9
0
    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)
예제 #10
0
    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
예제 #11
0
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()
예제 #12
0
    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)