示例#1
0
class LoRaWAN:
    def __init__(self, logger):
        """
        Connects to LoRaWAN using OTAA and sets up a ring buffer for storing messages.
        :param logger: status logger
        :type logger: LoggerFactory object
        """

        self.logger = logger
        self.message_limit = int(
            float(config.get_config("fair_access")) /
            (float(config.get_config("air_time")) / 1000))
        self.transmission_date = config.get_config(
            "transmission_date")  # last date when lora was transmitting
        today = time.gmtime()
        date = str(today[0]) + str(today[1]) + str(today[2])
        if self.transmission_date == date:  # if device was last transmitting today
            self.message_count = config.get_config(
                "message_count")  # get number of messages sent today
        else:
            self.message_count = 0  # if device was last transmitting a day or more ago, reset message_count for the day
            self.transmission_date = date
            config.save_config({
                "message_count": self.message_count,
                "transmission_date": date
            })

        regions = {
            "Europe": LoRa.EU868,
            "Asia": LoRa.AS923,
            "Australia": LoRa.AU915,
            "United States": LoRa.US915
        }
        region = regions[config.get_config("region")]

        self.lora = LoRa(mode=LoRa.LORAWAN, region=region, adr=True)

        # create an OTAA authentication parameters
        app_eui = ubinascii.unhexlify(config.get_config("application_eui"))
        app_key = ubinascii.unhexlify(config.get_config("app_key"))

        # join a network using OTAA (Over the Air Activation)
        self.lora.join(activation=LoRa.OTAA,
                       auth=(app_eui, app_key),
                       timeout=0)

        # create a LoRa socket
        self.lora_socket = socket.socket(socket.AF_LORA, socket.SOCK_RAW)

        # request acknowledgment of data sent
        # self.lora_socket.setsockopt(socket.SOL_LORA, socket.SO_CONFIRMED, True)

        # do not request acknowledgment of data sent
        self.lora_socket.setsockopt(socket.SOL_LORA, socket.SO_CONFIRMED,
                                    False)

        # sets timeout for sending data
        self.lora_socket.settimeout(
            int(config.get_config("lora_timeout")) * 1000)

        # set up callback for receiving downlink messages
        self.lora.callback(trigger=LoRa.RX_PACKET_EVENT,
                           handler=self.lora_recv)

        # initialises circular lora stack to back up data up to about 22.5 days depending on the length of the month
        self.lora_buffer = RingBuffer(self.logger, s.processing_path,
                                      s.lora_file_name,
                                      31 * self.message_limit, 100)

        try:  # this fails if the buffer is empty
            self.check_date()  # remove messages that are over a month old
        except Exception as e:
            pass

    def lora_recv(self, arg):
        """
        Callback for receiving packets through LoRaWAN. Decodes messages to commands for updating over WiFi.
        Requires a dummy argument.
        """

        payload = self.lora_socket.recv(600)  # receive bytes message
        self.logger.info("Lora message received")
        msg = payload.decode()  # convert to string

        try:
            if msg == "0":  # reboot device
                self.logger.info("Reset triggered over LoRa")
                self.logger.info("Rebooting...")
                machine.reset()
            elif msg == "1":  # start software update
                self.logger.info("Software update triggered over LoRa")
                config.save_config({"update": True})
                machine.reset()
            else:
                split_msg = msg.split(":")
                if split_msg[0] == "2":  # update wifi credentials
                    self.logger.info("WiFi credentials updated over LoRa")
                    config.save_config({
                        "SSID": split_msg[1],
                        "wifi_password": split_msg[2]
                    })
                elif split_msg[
                        0] == "3":  # update wifi credentials and start software update
                    self.logger.info("WiFi credentials updated over LoRa")
                    config.save_config({
                        "SSID": split_msg[1],
                        "wifi_password": split_msg[2]
                    })
                    self.logger.info("Software update triggered over LoRa")
                    config.save_config({"update": True})
                    machine.reset()
                else:
                    self.logger.error("Unknown command received over LoRa")
        except Exception as e:
            self.logger.exception(
                "Failed to interpret message received over LoRa")

    def lora_send(self):
        """Lora send method to run as a thread. Checks if messages are up to date in the lora buffer, pops the one on
        top of the stack, encodes it to a message and sends it to the right port.
        Takes two dummy arguments required by the threading library"""

        if lora_lock.locked():
            self.logger.debug("Waiting for other lora thread to finish")
        with lora_lock:
            self.logger.debug("LoRa thread started")

            try:
                self.check_date()  # remove messages that are over a month old

                if self.lora.has_joined():
                    self.logger.debug("LoRa connected")
                else:
                    raise Exception("LoRa is not connected")

                if s.lora_file_name not in os.listdir(s.root_path +
                                                      s.processing):
                    raise Exception('LoRa - File: {} does not exist'.format(
                        s.lora_file_name))
                else:
                    port, payload = self.get_sending_details()

                    self.lora_socket.bind(
                        port)  # bind to port to decode at backend
                    self.lora_socket.send(
                        payload)  # send payload to the connected socket
                    self.logger.debug("LoRa - sent payload")

                    self.message_count += 1  # increment number of files sent over LoRa today

                    config.save_config({"message_count": self.message_count
                                        })  # save number of messages today

                    # remove message sent
                    self.lora_buffer.remove_head()

            except Exception:
                self.logger.exception("Sending payload over LoRaWAN failed")
                blink_led((0x550000, 0.4, True))

    def get_sending_details(self):
        """
        Gets message sitting on top of the lora stack, and constructs payload according to its format
        :return: port, payload
        :rtype: int, bytes
        """

        buffer_line = self.lora_buffer.read()
        buffer_lst = buffer_line.split(
            ',')  # convert string to a list of strings

        # get structure and port from format
        fmt = buffer_lst[2]  # format is third item in the list
        fmt_dict = {
            "TPP": s.TPP,
            "TP": s.TP,
            "PP": s.PP,
            "P": s.P,
            "T": s.T,
            "G": s.G
        }
        port_struct_dict = fmt_dict[
            fmt]  # get dictionary corresponding to the format
        port = port_struct_dict["port"]  # get port corresponding to the format
        structure = port_struct_dict[
            "structure"]  # get structure corresponding to the format

        # cast message according to format to form valid payload
        lst_message = buffer_lst[3:]  # chop year, month and format off
        cast_lst_message = []
        for i in range(
            (len(structure) -
             1)):  # iterate for length of structure having '<' stripped
            if structure[i +
                         1] == 'f':  # iterate through structure ignoring '<'
                cast_lst_message.append(float(
                    lst_message[i]))  # cast to float if structure is 'f'
            else:
                cast_lst_message.append(int(
                    lst_message[i]))  # cast to int otherwise

        # pack payload
        self.logger.debug("Sending over LoRa: " + str(cast_lst_message))
        payload = struct.pack(
            structure, *cast_lst_message
        )  # define payload with given structure and list of averages

        return port, payload

    # removes messages from end of lora stack until they are all within a month
    def check_date(self):
        """
        Checks recursively if the message on the bottom of the stack is within a month
        """

        buffer_line = self.lora_buffer.read(
            read_tail=True)  # read the tail of the lora_buffer
        buffer_lst = buffer_line.split(
            ',')  # convert string to a list of strings
        time_now = time.gmtime()  # get current date

        # get year, month and minutes of the month now
        year_now, month_now, minutes_now = time_now[0], time_now[
            1], minutes_of_the_month()

        # get year, month and minutes of the month of the last message in the lora_buffer
        year_then, month_then, minutes_then = int(buffer_lst[0]) + 2000, int(
            buffer_lst[1]), int(buffer_lst[4])

        # logic to decide if message is older than a month
        if year_then < year_now or month_then < month_now:
            if (month_then + 1
                    == month_now) or (month_then == 12 and month_now == 1
                                      and year_then + 1 == year_now):
                if minutes_then < minutes_now + 24 * 60:
                    self.lora_buffer.remove_tail()  # remove message
                    self.check_date()  # call recursively
            else:
                self.lora_buffer.remove_tail()  # remove message
                self.check_date()  # call recursively