예제 #1
0
def loraConnection(lora=None):
    """Connection with OTAA to the LoRaWAN network"""
    if lora == None:
        lora = LoRa(mode=LoRa.LORAWAN)
        lora.power_mode(LoRa.TX_ONLY)
    lora.nvram_restore()
    if not lora.has_joined():
        joinRequestTimeout = False
        loRaWANTimeout = Timer.Chrono()
        app_eui = binascii.unhexlify(conf.APP_EUI.replace(' ', ''))
        app_key = binascii.unhexlify(conf.APP_KEY.replace(' ', ''))
        timeout = conf.JOIN_REQUEST_TIMEOUT
        lora.join(activation=LoRa.OTAA, auth=(app_eui, app_key), timeout=0)
        loRaWANTimeout.start()
        while not lora.has_joined():
            if loRaWANTimeout.read() > timeout:
                loRaWANTimeout.stop()
                joinRequestTimeout = True
                break
class NanoGateway:
    """
    Nano gateway class, set up by default for use with TTN, but can be configured
    for any other network supporting the Semtech Packet Forwarder.
    Only required configuration is wifi_ssid and wifi_password which are used for
    connecting to the Internet.
    """
    def __init__(self,
                 id,
                 frequency,
                 datarate,
                 ssid,
                 password,
                 server,
                 port,
                 ntp_server='pool.ntp.org',
                 ntp_period=3600):
        self.id = id
        self.server = server
        self.port = port

        self.frequency = frequency
        self.datarate = datarate

        self.ssid = ssid
        self.password = password

        self.ntp_server = ntp_server
        self.ntp_period = ntp_period

        self.server_ip = None

        self.rxnb = 0
        self.rxok = 0
        self.rxfw = 0
        self.dwnb = 0
        self.txnb = 0

        self.sf = self._dr_to_sf(self.datarate)
        self.bw = self._dr_to_bw(self.datarate)

        self.stat_alarm = None
        self.pull_alarm = None
        self.uplink_alarm = None

        self.wlan = None
        self.sock = None
        self.udp_stop = False
        self.udp_lock = _thread.allocate_lock()

        self.lora = None
        self.lora_sock = None

        self.rtc = machine.RTC()

    def start(self):
        """
        Starts the LoRaWAN nano gateway.
        """

        self._log('Starting LoRaWAN nano gateway with id: {}', self.id)

        # setup WiFi as a station and connect
        self.wlan = WLAN(mode=WLAN.STA)
        self._connect_to_wifi()

        # get a time sync
        self._log('Syncing time with {} ...', self.ntp_server)
        self.rtc.ntp_sync(self.ntp_server, update_period=self.ntp_period)
        while not self.rtc.synced():
            utime.sleep_ms(50)
        self._log("RTC NTP sync complete")

        # get the server IP and create an UDP socket
        self.server_ip = usocket.getaddrinfo(self.server, self.port)[0][-1]
        self._log('Opening UDP socket to {} ({}) port {}...', self.server,
                  self.server_ip[0], self.server_ip[1])
        self.sock = usocket.socket(usocket.AF_INET, usocket.SOCK_DGRAM,
                                   usocket.IPPROTO_UDP)
        self.sock.setsockopt(usocket.SOL_SOCKET, usocket.SO_REUSEADDR, 1)
        self.sock.setblocking(False)

        # push the first time immediatelly
        self._push_data(self._make_stat_packet())

        # create the alarms
        self.stat_alarm = Timer.Alarm(
            handler=lambda t: self._push_data(self._make_stat_packet()),
            s=60,
            periodic=True)
        self.pull_alarm = Timer.Alarm(handler=lambda u: self._pull_data(),
                                      s=25,
                                      periodic=True)

        # start the UDP receive thread
        self.udp_stop = False
        _thread.start_new_thread(self._udp_thread, ())

        # initialize the LoRa radio in LORA mode
        self._log('Setting up the LoRa radio at {} Mhz using {}',
                  self._freq_to_float(self.frequency), self.datarate)
        self.lora = LoRa(mode=LoRa.LORA,
                         frequency=self.frequency,
                         bandwidth=self.bw,
                         sf=self.sf,
                         preamble=8,
                         coding_rate=LoRa.CODING_4_5,
                         tx_iq=True)

        # create a raw LoRa socket
        self.lora_sock = usocket.socket(usocket.AF_LORA, usocket.SOCK_RAW)
        self.lora_sock.setblocking(False)
        self.lora_tx_done = False

        self.lora.callback(trigger=(LoRa.RX_PACKET_EVENT
                                    | LoRa.TX_PACKET_EVENT),
                           handler=self._lora_cb)
        self._log('LoRaWAN nano gateway online')

    def stop(self):
        """
        Stops the LoRaWAN nano gateway.
        """

        self._log('Stopping...')

        # send the LoRa radio to sleep
        self.lora.callback(trigger=None, handler=None)
        self.lora.power_mode(LoRa.SLEEP)

        # stop the NTP sync
        self.rtc.ntp_sync(None)

        # cancel all the alarms
        self.stat_alarm.cancel()
        self.pull_alarm.cancel()

        # signal the UDP thread to stop
        self.udp_stop = True
        while self.udp_stop:
            utime.sleep_ms(50)

        # disable WLAN
        self.wlan.disconnect()
        self.wlan.deinit()

    def _connect_to_wifi(self):
        self.wlan.connect(self.ssid, auth=(None, self.password))
        while not self.wlan.isconnected():
            utime.sleep_ms(50)
        self._log('WiFi connected to: {}', self.ssid)

    def _dr_to_sf(self, dr):
        sf = dr[2:4]
        if sf[1] not in '0123456789':
            sf = sf[:1]
        return int(sf)

    def _dr_to_bw(self, dr):
        bw = dr[-5:]
        if bw == 'BW125':
            return LoRa.BW_125KHZ
        elif bw == 'BW250':
            return LoRa.BW_250KHZ
        else:
            return LoRa.BW_500KHZ

    def _sf_bw_to_dr(self, sf, bw):
        dr = 'SF' + str(sf)
        if bw == LoRa.BW_125KHZ:
            return dr + 'BW125'
        elif bw == LoRa.BW_250KHZ:
            return dr + 'BW250'
        else:
            return dr + 'BW500'

    def _lora_cb(self, lora):
        """
        LoRa radio events callback handler.
        """

        events = lora.events()
        if events & LoRa.RX_PACKET_EVENT:
            self.rxnb += 1
            self.rxok += 1
            rx_data = self.lora_sock.recv(256)
            stats = lora.stats()
            packet = self._make_node_packet(rx_data, self.rtc.now(),
                                            stats.rx_timestamp, stats.sfrx,
                                            self.bw, stats.rssi, stats.snr)
            self._push_data(packet)
            self._log('Received packet: {}', packet)
            self.rxfw += 1
        if events & LoRa.TX_PACKET_EVENT:
            self.txnb += 1
            lora.init(mode=LoRa.LORA,
                      frequency=self.frequency,
                      bandwidth=self.bw,
                      sf=self.sf,
                      preamble=8,
                      coding_rate=LoRa.CODING_4_5,
                      tx_iq=True)

    def _freq_to_float(self, frequency):
        """
        MicroPython has some inprecision when doing large float division.
        To counter this, this method first does integer division until we
        reach the decimal breaking point. This doesn't completely elimate
        the issue in all cases, but it does help for a number of commonly
        used frequencies.
        """

        divider = 6
        while divider > 0 and frequency % 10 == 0:
            frequency = frequency // 10
            divider -= 1
        if divider > 0:
            frequency = frequency / (10**divider)
        return frequency

    def _make_stat_packet(self):
        now = self.rtc.now()
        STAT_PK["stat"]["time"] = "%d-%02d-%02d %02d:%02d:%02d GMT" % (
            now[0], now[1], now[2], now[3], now[4], now[5])
        STAT_PK["stat"]["rxnb"] = self.rxnb
        STAT_PK["stat"]["rxok"] = self.rxok
        STAT_PK["stat"]["rxfw"] = self.rxfw
        STAT_PK["stat"]["dwnb"] = self.dwnb
        STAT_PK["stat"]["txnb"] = self.txnb
        return ujson.dumps(STAT_PK)

    def _make_node_packet(self, rx_data, rx_time, tmst, sf, bw, rssi, snr):
        RX_PK["rxpk"][0]["time"] = "%d-%02d-%02dT%02d:%02d:%02d.%dZ" % (
            rx_time[0], rx_time[1], rx_time[2], rx_time[3], rx_time[4],
            rx_time[5], rx_time[6])
        RX_PK["rxpk"][0]["tmst"] = tmst
        RX_PK["rxpk"][0]["freq"] = self._freq_to_float(self.frequency)
        RX_PK["rxpk"][0]["datr"] = self._sf_bw_to_dr(sf, bw)
        RX_PK["rxpk"][0]["rssi"] = rssi
        RX_PK["rxpk"][0]["lsnr"] = snr
        RX_PK["rxpk"][0]["data"] = ubinascii.b2a_base64(rx_data)[:-1]
        RX_PK["rxpk"][0]["size"] = len(rx_data)
        return ujson.dumps(RX_PK)

    def _push_data(self, data):
        token = uos.urandom(2)
        packet = bytes([PROTOCOL_VERSION]) + token + bytes(
            [PUSH_DATA]) + ubinascii.unhexlify(self.id) + data
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except Exception as ex:
                self._log('Failed to push uplink packet to server: {}', ex)

    def _pull_data(self):
        token = uos.urandom(2)
        packet = bytes([PROTOCOL_VERSION]) + token + bytes(
            [PULL_DATA]) + ubinascii.unhexlify(self.id)
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except Exception as ex:
                self._log('Failed to pull downlink packets from server: {}',
                          ex)

    def _ack_pull_rsp(self, token, error):
        TX_ACK_PK["txpk_ack"]["error"] = error
        resp = ujson.dumps(TX_ACK_PK)
        packet = bytes([PROTOCOL_VERSION]) + token + bytes(
            [PULL_ACK]) + ubinascii.unhexlify(self.id) + resp
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except Exception as ex:
                self._log('PULL RSP ACK exception: {}', ex)

    def _send_down_link(self, data, tmst, datarate, frequency):
        """
        Transmits a downlink message over LoRa.
        """

        self.lora.init(mode=LoRa.LORA,
                       frequency=frequency,
                       bandwidth=self._dr_to_bw(datarate),
                       sf=self._dr_to_sf(datarate),
                       preamble=8,
                       coding_rate=LoRa.CODING_4_5,
                       tx_iq=True)
        while utime.ticks_cpu() < tmst:
            pass
        self.lora_sock.send(data)
        self._log(
            'Sent downlink packet scheduled on {:.3f}, at {:.3f} Mhz using {}: {}',
            tmst / 1000000, self._freq_to_float(frequency), datarate, data)

    def _udp_thread(self):
        """
        UDP thread, reads data from the server and handles it.
        """

        while not self.udp_stop:
            try:
                data, src = self.sock.recvfrom(1024)
                _token = data[1:3]
                _type = data[3]
                if _type == PUSH_ACK:
                    self._log("Push ack")
                elif _type == PULL_ACK:
                    self._log("Pull ack")
                elif _type == PULL_RESP:
                    self.dwnb += 1
                    ack_error = TX_ERR_NONE
                    tx_pk = ujson.loads(data[4:])
                    tmst = tx_pk["txpk"]["tmst"]
                    t_us = tmst - utime.ticks_cpu() - 15000
                    if t_us < 0:
                        t_us += 0xFFFFFFFF
                    if t_us < 20000000:
                        self.uplink_alarm = Timer.Alarm(
                            handler=lambda x: self._send_down_link(
                                ubinascii.a2b_base64(tx_pk["txpk"]["data"]),
                                tx_pk["txpk"]["tmst"] - 50, tx_pk["txpk"][
                                    "datr"],
                                int(tx_pk["txpk"]["freq"] * 1000) * 1000),
                            us=t_us)
                    else:
                        ack_error = TX_ERR_TOO_LATE
                        self._log('Downlink timestamp error!, t_us: {}', t_us)
                    self._ack_pull_rsp(_token, ack_error)
                    self._log("Pull rsp")
            except usocket.timeout:
                pass
            except OSError as ex:
                if ex.errno != errno.EAGAIN:
                    self._log('UDP recv OSError Exception: {}', ex)
            except Exception as ex:
                self._log('UDP recv Exception: {}', ex)

            # wait before trying to receive again
            utime.sleep_ms(UDP_THREAD_CYCLE_MS)

        # we are to close the socket
        self.sock.close()
        self.udp_stop = False
        self._log('UDP thread stopped')

    def _log(self, message, *args):
        """
        Outputs a log message to stdout.
        """

        print('[{:>10.3f}] {}'.format(utime.ticks_ms() / 1000,
                                      str(message).format(*args)))
예제 #3
0
def conn_cb (bt_o):
    events = bt_o.events()
    if  events & Bluetooth.CLIENT_CONNECTED:
        print("Client connected")
    elif events & Bluetooth.CLIENT_DISCONNECTED:
        print("Client disconnected")


bluetooth.callback(trigger=Bluetooth.CLIENT_CONNECTED | Bluetooth.CLIENT_DISCONNECTED, handler=conn_cb)


#BLE

bluetooth.deinit()
wlan.deinit()
lora.power_mode(LoRa.SLEEP)

bluetooth = Bluetooth()
pycom.heartbeat(False)
pycom.rgbled(0x000077)

print('sending BT')
bluetooth.set_advertisement(name='LoPy', service_uuid=b'1234567890abcdef')
bluetooth.advertise(True)

time.sleep(8)

#micro
bluetooth.deinit()
wlan.deinit()
lora.power_mode(LoRa.SLEEP)
예제 #4
0
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <https://www.gnu.org/licenses/>.


from network import LoRa
import binascii
import cbor
import pycom
import socket
import time

###Initialize LoRa###
lora = LoRa(mode=LoRa.LORAWAN)

#Power mode may be LoRa.ALWAYS_ON, LoRa.TX_ONLY or LoRa.SLEEP.
lora.power_mode(LoRa.TX_ONLY)

#Credentials
app_eui = b'\x20\x18\x20\x18\x20\x18\x20\x18'
app_key = binascii.unhexlify('da 9b cf 8a 6c e6 97 10 32 41 c9 2d d4 0e 9b 72'.replace(' ',''))

#Join the network (or re-join if connection lost)
lora.join(activation=LoRa.OTAA, auth=(app_eui, app_key), timeout=0)

#Initialize LoRaWAN socket
s = socket.socket(socket.AF_LORA, socket.SOCK_RAW)
#Configuring data rate (between 0 and 5)
s.setsockopt(socket.SOL_LORA, socket.SO_DR, 3)
#Selecting non-confirmed type of messages
s.setsockopt(socket.SOL_LORA, socket.SO_CONFIRMED, False)
예제 #5
0
class NanoGateway:
    """
    Nano gateway class, set up by default for use with TTN, but can be configured
    for any other network supporting the Semtech Packet Forwarder.

    Only required configuration is wifi_ssid and wifi_password which are used for
    connecting to the Internet.
    """

    PROTOCOL_VERSION = const(2)

    PUSH_DATA = const(0)
    PUSH_ACK = const(1)
    PULL_DATA = const(2)
    PULL_ACK = const(4)
    PULL_RESP = const(3)

    TX_ERR_NONE = 'NONE'
    TX_ERR_TOO_LATE = 'TOO_LATE'
    TX_ERR_TOO_EARLY = 'TOO_EARLY'
    TX_ERR_COLLISION_PACKET = 'COLLISION_PACKET'
    TX_ERR_COLLISION_BEACON = 'COLLISION_BEACON'
    TX_ERR_TX_FREQ = 'TX_FREQ'
    TX_ERR_TX_POWER = 'TX_POWER'
    TX_ERR_GPS_UNLOCKED = 'GPS_UNLOCKED'

    UDP_THREAD_CYCLE_MS = const(10)

    STAT_PK = {
        'stat': {
            'time': '',
            'lati': 0,
            'long': 0,
            'alti': 0,
            'rxnb': 0,
            'rxok': 0,
            'rxfw': 0,
            'ackr': 100.0,
            'dwnb': 0,
            'txnb': 0
        }
    }

    RX_PK = {
        'rxpk': [{
            'time': '',
            'tmst': 0,
            'chan': 0,
            'rfch': 0,
            'freq': 0,
            'stat': 1,
            'modu': 'LORA',
            'datr': '',
            'codr': '4/5',
            'rssi': 0,
            'lsnr': 0,
            'size': 0,
            'data': ''
        }]
    }

    TX_ACK_PK = {'txpk_ack': {'error': ''}}

    def __init__(self,
                 wifi_ssid,
                 wifi_password,
                 gateway_id=None,
                 server='router.eu.thethings.network',
                 port=1700,
                 frequency=868100000,
                 datarate='SF7BW125',
                 ntp_server='pool.ntp.org',
                 ntp_period=3600):
        # If unset, set the Gateway ID to be the first 3 bytes
        # of MAC address + 'FFFE' + last 3 bytes of MAC address
        if gateway_id is None:
            gateway_id = ubinascii.hexlify(machine.unique_id()).upper()
            gateway_id = gateway_id[:6] + 'FFFE' + gateway_id[6:12]
        self.gateway_id = gateway_id

        self.server = server
        self.port = port

        self.frequency = frequency
        self.datarate = datarate

        self.wifi_ssid = wifi_ssid
        self.wifi_password = wifi_password

        self.ntp_server = ntp_server
        self.ntp_period = ntp_period

        self.server_ip = None

        self.rxnb = 0
        self.rxok = 0
        self.rxfw = 0
        self.dwnb = 0
        self.txnb = 0

        self.sf = self._dr_to_sf(self.datarate)
        self.bw = self._dr_to_bw(self.datarate)

        self.stat_alarm = None
        self.pull_alarm = None
        self.uplink_alarm = None

        self.wlan = None
        self.sock = None
        self.udp_stop = False
        self.udp_lock = _thread.allocate_lock()

        self.lora = None
        self.lora_sock = None

        self.rtc = machine.RTC()

    def start(self):
        """
        Starts the nano gateway.
        """

        self.log('Starting nano gateway with id {}', self.gateway_id)

        # Change WiFi to STA mode and connect
        self.wlan = WLAN(mode=WLAN.STA)
        self._connect_to_wifi()

        # Get a time sync
        self.log('Syncing time with {} ...', self.ntp_server)
        self.rtc.ntp_sync(self.ntp_server, update_period=self.ntp_period)
        while not self.rtc.synced():
            utime.sleep_ms(50)
        self.log('RTC NTP sync complete')

        # Get the server IP and create an UDP socket
        self.server_ip = usocket.getaddrinfo(self.server, self.port)[0][-1]
        self.log('Opening UDP socket to {} ({}) port {}...', self.server,
                 self.server_ip[0], self.server_ip[1])
        self.sock = usocket.socket(usocket.AF_INET, usocket.SOCK_DGRAM,
                                   usocket.IPPROTO_UDP)
        self.sock.setsockopt(usocket.SOL_SOCKET, usocket.SO_REUSEADDR, 1)
        self.sock.setblocking(False)

        # Push the first time stat immediately
        self._push_data(self._make_stat_packet())

        # Create the alarms
        self.stat_alarm = machine.Timer.Alarm(
            handler=lambda t: self._push_data(self._make_stat_packet()),
            s=60,
            periodic=True)
        self.pull_alarm = machine.Timer.Alarm(
            handler=lambda u: self._pull_data(), s=25, periodic=True)

        # Start the UDP receive thread
        self.udp_stop = False
        _thread.start_new_thread(self._udp_thread, ())

        # Initialize the LoRa radio in LORA mode
        self.log('Setting up LoRa socket on {:.1f} Mhz using {}',
                 self._freq_to_float(self.frequency), self.datarate)
        self.lora = LoRa(mode=LoRa.LORA,
                         frequency=self.frequency,
                         bandwidth=self.bw,
                         sf=self.sf,
                         preamble=8,
                         coding_rate=LoRa.CODING_4_5,
                         tx_iq=True)

        # Create a raw LoRa socket
        self.lora_sock = usocket.socket(usocket.AF_LORA, usocket.SOCK_RAW)
        self.lora_sock.setblocking(False)

        self.lora.callback(trigger=(LoRa.RX_PACKET_EVENT
                                    | LoRa.TX_PACKET_EVENT),
                           handler=self._lora_cb)
        self.log('Nano gateway online')

    def stop(self):
        """
        Stops the nano gateway.
        """

        self.log('Stopping...')

        # Send the LoRa radio to sleep
        self.lora.callback(trigger=None, handler=None)
        self.lora.power_mode(LoRa.SLEEP)

        # Stop the NTP sync
        self.rtc.ntp_sync(None)

        # Cancel all the alarms
        self.stat_alarm.cancel()
        self.pull_alarm.cancel()

        # Signal the UDP thread to stop
        self.udp_stop = True
        while self.udp_stop:
            utime.sleep_ms(50)

        # Disable WLAN
        self.wlan.disconnect()
        self.wlan.deinit()

    def _connect_to_wifi(self):
        self.wlan.connect(self.wifi_ssid, auth=(None, self.wifi_password))
        while not self.wlan.isconnected():
            utime.sleep_ms(50)
        self.log('WiFi connected: {}', self.wifi_ssid)

    def _dr_to_sf(self, dr):
        sf = dr[2:4]
        if sf[1] not in '0123456789':
            sf = sf[:1]
        return int(sf)

    def _dr_to_bw(self, dr):
        bw = dr[-5:]
        if bw == 'BW125':
            return LoRa.BW_125KHZ
        elif bw == 'BW250':
            return LoRa.BW_250KHZ
        else:
            return LoRa.BW_500KHZ

    def _sf_bw_to_dr(self, sf, bw):
        dr = 'SF' + str(sf)
        if bw == LoRa.BW_125KHZ:
            return dr + 'BW125'
        elif bw == LoRa.BW_250KHZ:
            return dr + 'BW250'
        else:
            return dr + 'BW500'

    def _lora_cb(self, lora):
        """
        Event listener for LoRa radio events.
        """

        events = lora.events()
        if events & LoRa.RX_PACKET_EVENT:
            self.rxnb += 1
            self.rxok += 1
            rx_data = self.lora_sock.recv(256)
            stats = lora.stats()
            packet = self._make_node_packet(rx_data, self.rtc.now(),
                                            stats.rx_timestamp, stats.sfrx,
                                            self.bw, stats.rssi, stats.snr)
            self.log('Received packet: {}', packet)
            self._push_data(packet)
            self.rxfw += 1
        if events & LoRa.TX_PACKET_EVENT:
            self.log('Re-initing LoRa radio after transmission')
            self.txnb += 1
            lora.init(mode=LoRa.LORA,
                      frequency=self.frequency,
                      bandwidth=self.bw,
                      sf=self.sf,
                      preamble=8,
                      coding_rate=LoRa.CODING_4_5,
                      tx_iq=True)

    def _freq_to_float(self, frequency):
        """
        MicroPython has some inprecision when doing large float division.

        To counter this, this method first does integer division until we
        reach the decimal breaking point. This doesn't completely elimate
        the issue in all cases, but it does help for a number of commonly
        used frequencies.
        """

        divider = 6
        while divider > 0 and frequency % 10 == 0:
            frequency = frequency // 10
            divider -= 1
        if divider > 0:
            frequency = frequency / (10**divider)
        return frequency

    def _make_stat_packet(self):
        now = self.rtc.now()
        self.STAT_PK['stat']['time'] = '%d-%02d-%02d %02d:%02d:%02d GMT' % (
            now[0], now[1], now[2], now[3], now[4], now[5])
        self.STAT_PK['stat']['rxnb'] = self.rxnb
        self.STAT_PK['stat']['rxok'] = self.rxok
        self.STAT_PK['stat']['rxfw'] = self.rxfw
        self.STAT_PK['stat']['dwnb'] = self.dwnb
        self.STAT_PK['stat']['txnb'] = self.txnb
        return ujson.dumps(self.STAT_PK)

    def _make_node_packet(self, rx_data, rx_time, tmst, sf, bw, rssi, snr):
        self.RX_PK['rxpk'][0]['time'] = '%d-%02d-%02dT%02d:%02d:%02d.%dZ' % (
            rx_time[0], rx_time[1], rx_time[2], rx_time[3], rx_time[4],
            rx_time[5], rx_time[6])
        self.RX_PK['rxpk'][0]['tmst'] = tmst
        self.RX_PK['rxpk'][0]['freq'] = self._freq_to_float(self.frequency)
        self.RX_PK['rxpk'][0]['datr'] = self._sf_bw_to_dr(sf, bw)
        self.RX_PK['rxpk'][0]['rssi'] = rssi
        self.RX_PK['rxpk'][0]['lsnr'] = float(snr)
        self.RX_PK['rxpk'][0]['data'] = ubinascii.b2a_base64(rx_data)[:-1]
        self.RX_PK['rxpk'][0]['size'] = len(rx_data)
        return ujson.dumps(self.RX_PK)

    def _push_data(self, data):
        token = uos.urandom(2)
        packet = bytes([self.PROTOCOL_VERSION]) + token + bytes(
            [self.PUSH_DATA]) + ubinascii.unhexlify(self.gateway_id) + data
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except BaseException as ex:
                self.log('Failed to push uplink packet to server: {}', ex)

    def _pull_data(self):
        token = uos.urandom(2)
        packet = bytes([self.PROTOCOL_VERSION]) + token + bytes(
            [self.PULL_DATA]) + ubinascii.unhexlify(self.gateway_id)
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except BaseException as ex:
                self.log('Failed to pull downlink packets from server: {}', ex)

    def _ack_pull_rsp(self, token, error):
        self.TX_ACK_PK['txpk_ack']['error'] = error
        resp = ujson.dumps(self.TX_ACK_PK)
        packet = bytes([self.PROTOCOL_VERSION]) + token + bytes(
            [self.PULL_ACK]) + ubinascii.unhexlify(self.gateway_id) + resp
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except BaseException as ex:
                self.log('PULL RSP ACK exception: {}', ex)

    def _send_down_link(self, data, tmst, datarate, frequency):
        """
        Transmits a downlink message over LoRa.
        """

        self.lora.init(mode=LoRa.LORA,
                       frequency=frequency,
                       bandwidth=self._dr_to_bw(datarate),
                       sf=self._dr_to_sf(datarate),
                       preamble=8,
                       coding_rate=LoRa.CODING_4_5,
                       tx_iq=True)
        while utime.ticks_us() < tmst:
            pass
        self.lora_sock.send(data)
        self.log(
            'Sent downlink packet scheduled for {:.3f}, at {:.1f} Mhz using {}: {}',
            tmst / 1000000, self._freq_to_float(frequency), datarate, data)

    def _udp_thread(self):
        """
        UDP thread, reads data from the server and handles it.
        """

        while not self.udp_stop:
            try:
                data, src = self.sock.recvfrom(1024)
                _token = data[1:3]
                _type = data[3]
                if _type == self.PUSH_ACK:
                    self.log('Push ack')
                elif _type == self.PULL_ACK:
                    self.log('Pull ack')
                elif _type == self.PULL_RESP:
                    self.dwnb += 1
                    ack_error = self.TX_ERR_NONE
                    tx_pk = ujson.loads(data[4:])
                    tmst = tx_pk['txpk']['tmst']
                    t_us = tmst - utime.ticks_us() - 12500
                    if t_us < 0:
                        t_us += 0xFFFFFFFF
                    if t_us < 20000000:
                        self.uplink_alarm = machine.Timer.Alarm(
                            handler=lambda x: self._send_down_link(
                                ubinascii.a2b_base64(tx_pk['txpk']['data']),
                                tx_pk['txpk']['tmst'] - 50, tx_pk['txpk'][
                                    'datr'],
                                int(tx_pk['txpk']['freq'] * 1000000)),
                            us=t_us)
                    else:
                        ack_error = self.TX_ERR_TOO_LATE
                        self.log('Downlink timestamp error!, t_us: {}', t_us)
                    self._ack_pull_rsp(_token, ack_error)
                    self.log('Pull rsp')
                else:
                    self.log('Unknown message type from server: {}', _type)
            except usocket.timeout:
                pass
            except OSError as ex:
                if ex.errno != errno.EAGAIN:
                    self.log('UDP recv OSError Exception: {}', ex)
            except BaseException as ex:
                self.log('UDP recv Exception: {}', ex)

            # Wait before trying to receive again
            utime.sleep_ms(self.UDP_THREAD_CYCLE_MS)

        self.sock.close()
        self.udp_stop = False
        self.log('UDP thread stopped')

    def log(self, message, *args):
        """
        Prints a log message to the stdout.
        """

        print('[{:>10.3f}] {}'.format(utime.ticks_ms() / 1000,
                                      str(message).format(*args)))
예제 #6
0
 pycom.rgbled(red)
 on_time = chrono.read_ms()
 lora.init(mode=LoRa.LORA,
           tx_iq=True,
           region=LoRa.EU868,
           frequency=freqs[my_sf - 7],
           power_mode=LoRa.TX_ONLY,
           bandwidth=my_bw,
           sf=my_sf,
           tx_power=7)
 pkg = struct.pack(_LORA_PKG_FORMAT % len(msg), MY_ID, len(msg), msg)
 lora_sock.send(pkg)
 pycom.rgbled(blue)
 print("Message of " + str(len(pkg)) + " bytes sent at:",
       chrono.read_ms())
 lora.power_mode(LoRa.SLEEP)
 # print(lora.stats())
 cur_time = chrono.read_ms()
 active += (cur_time - on_time)
 t = round - int(cur_time - start)
 machine.idle()
 time.sleep_ms(t)
 if (i % sync_rate == 0):  # synchronisatio
     syncs += 1
     sync_slot = 100  # I have to fix this
     rec = 0
     lora.init(mode=LoRa.LORA,
               rx_iq=True,
               region=LoRa.EU868,
               frequency=freqs[my_sf - 7],
               power_mode=LoRa.ALWAYS_ON,
예제 #7
0
    pass
print("ifconfig", wlan.ifconfig())
print('IP:', wlan.ifconfig()[0])
print("mode", wlan.mode(), end=' ')
print_wifi_mode(wlan.mode())
print()

try:
    print("===== lora =======================================")
    from network import LoRa
    lora = LoRa(mode=LoRa.LORAWAN, region=LoRa.EU868)
    print("mac", binascii.hexlify(lora.mac()))
    print(lora.frequency())
    print(lora.has_joined())
    print(lora.tx_power())
    print(lora.power_mode())
    #print(lora.stats())
except:
    pass

try:
    print("===== sigfox =====================================")
    from network import Sigfox
    sigfox = Sigfox(mode=Sigfox.SIGFOX, rcz=Sigfox.RCZ1)
    print("id", binascii.hexlify(sigfox.id()))
    print("mac", binascii.hexlify(sigfox.mac()))
    print("pac", binascii.hexlify(sigfox.pac()))
    print("frequencies", sigfox.frequencies())
except:
    pass
예제 #8
0
        if debug:
            print("No gps")
    if sds011_res is not None:
        data[labels["dust_pm10"]] = sds011_res[0]
        data[labels["dust_pm25"]] = sds011_res[1]
    else:
        if debug:
            print("No dust")

    return data


if __name__ == '__main__':
    # Initialize LoRa
    lora_connection = LoRa(mode=LoRa.LORAWAN)
    lora_connection.power_mode(lora_mode)
    soc = join_lora_gw(lora_connection)

    # Launch the collect and send data loop
    t = -1

    lcd_connection, sds011_ok = None, False
    #my_i2c = pycom_monitor.init_i2c()
    # lcd_connection = pycom_monitor.init_lcd()#my_i2c)
    # lcd_connection.poweron()

    pycom_monitor.gps_init()
    pycom_monitor.init_co2_tvoc()

    while True:
        am2320, sgp30, gps, sds011 = None, None, None, None
예제 #9
0
class NanoGateway:
    """
    Nano gateway class, set up by default for use with TTN, but can be configured
    for any other network supporting the Semtech Packet Forwarder.
    Only required configuration is wifi_ssid and wifi_password which are used for
    connecting to the Internet.
    """

    def __init__(self, id, frequency, datarate, ssid, password, server, port, ntp_server='pool.ntp.org', ntp_period=3600):
        self.id = id
        self.server = server
        self.port = port

        self.frequency = frequency
        self.datarate = datarate

        self.ssid = ssid
        self.password = password

        self.ntp_server = ntp_server
        self.ntp_period = ntp_period

        self.server_ip = None

        self.rxnb = 0
        self.rxok = 0
        self.rxfw = 0
        self.dwnb = 0
        self.txnb = 0

        self.sf = self._dr_to_sf(self.datarate)
        self.bw = self._dr_to_bw(self.datarate)

        self.stat_alarm = None
        self.pull_alarm = None
        self.uplink_alarm = None

        self.wlan = None
        self.sock = None
        self.udp_stop = False
        self.udp_lock = _thread.allocate_lock()

        self.lora = None
        self.lora_sock = None

        self.rtc = machine.RTC()

    def start(self):
        """
        Starts the LoRaWAN nano gateway.
        """

        self._log('Starting LoRaWAN nano gateway with id: {}', self.id)

        # setup WiFi as a station and connect
        self.wlan = WLAN(mode=WLAN.STA)
        self._connect_to_wifi()

        # get a time sync
        self._log('Syncing time with {} ...', self.ntp_server)
        self.rtc.ntp_sync(self.ntp_server, update_period=self.ntp_period)
        while not self.rtc.synced():
            utime.sleep_ms(50)
        self._log("RTC NTP sync complete")

        # get the server IP and create an UDP socket
        self.server_ip = usocket.getaddrinfo(self.server, self.port)[0][-1]
        self._log('Opening UDP socket to {} ({}) port {}...', self.server, self.server_ip[0], self.server_ip[1])
        self.sock = usocket.socket(usocket.AF_INET, usocket.SOCK_DGRAM, usocket.IPPROTO_UDP)
        self.sock.setsockopt(usocket.SOL_SOCKET, usocket.SO_REUSEADDR, 1)
        self.sock.setblocking(False)

        # push the first time immediatelly
        self._push_data(self._make_stat_packet())

        # create the alarms
        self.stat_alarm = Timer.Alarm(handler=lambda t: self._push_data(self._make_stat_packet()), s=60, periodic=True)
        self.pull_alarm = Timer.Alarm(handler=lambda u: self._pull_data(), s=25, periodic=True)

        # start the UDP receive thread
        self.udp_stop = False
        _thread.start_new_thread(self._udp_thread, ())

        # initialize the LoRa radio in LORA mode
        self._log('Setting up the LoRa radio at {:.1f} Mhz using {}', self._freq_to_float(self.frequency), self.datarate)
        self.lora = LoRa(
            mode=LoRa.LORA,
            frequency=self.frequency,
            bandwidth=self.bw,
            sf=self.sf,
            preamble=8,
            coding_rate=LoRa.CODING_4_5,
            tx_iq=True
        )

        # create a raw LoRa socket
        self.lora_sock = usocket.socket(usocket.AF_LORA, usocket.SOCK_RAW)
        self.lora_sock.setblocking(False)
        self.lora_tx_done = False

        self.lora.callback(trigger=(LoRa.RX_PACKET_EVENT | LoRa.TX_PACKET_EVENT), handler=self._lora_cb)
        self._log('LoRaWAN nano gateway online')

    def stop(self):
        """
        Stops the LoRaWAN nano gateway.
        """

        self._log('Stopping...')

        # send the LoRa radio to sleep
        self.lora.callback(trigger=None, handler=None)
        self.lora.power_mode(LoRa.SLEEP)

        # stop the NTP sync
        self.rtc.ntp_sync(None)

        # cancel all the alarms
        self.stat_alarm.cancel()
        self.pull_alarm.cancel()

        # signal the UDP thread to stop
        self.udp_stop = True
        while self.udp_stop:
            utime.sleep_ms(50)

        # disable WLAN
        self.wlan.disconnect()
        self.wlan.deinit()

    def _connect_to_wifi(self):
        self.wlan.connect(self.ssid, auth=(None, self.password))
        while not self.wlan.isconnected():
            utime.sleep_ms(50)
        self._log('WiFi connected to: {}', self.ssid)

    def _dr_to_sf(self, dr):
        sf = dr[2:4]
        if sf[1] not in '0123456789':
            sf = sf[:1]
        return int(sf)

    def _dr_to_bw(self, dr):
        bw = dr[-5:]
        if bw == 'BW125':
            return LoRa.BW_125KHZ
        elif bw == 'BW250':
            return LoRa.BW_250KHZ
        else:
            return LoRa.BW_500KHZ

    def _sf_bw_to_dr(self, sf, bw):
        dr = 'SF' + str(sf)
        if bw == LoRa.BW_125KHZ:
            return dr + 'BW125'
        elif bw == LoRa.BW_250KHZ:
            return dr + 'BW250'
        else:
            return dr + 'BW500'

    def _lora_cb(self, lora):
        """
        LoRa radio events callback handler.
        """

        events = lora.events()
        if events & LoRa.RX_PACKET_EVENT:
            self.rxnb += 1
            self.rxok += 1
            rx_data = self.lora_sock.recv(256)
            stats = lora.stats()
            packet = self._make_node_packet(rx_data, self.rtc.now(), stats.rx_timestamp, stats.sfrx, self.bw, stats.rssi, stats.snr)
            self._push_data(packet)
            self._log('Received packet: {}', packet)
            self.rxfw += 1
        if events & LoRa.TX_PACKET_EVENT:
            self.txnb += 1
            lora.init(
                mode=LoRa.LORA,
                frequency=self.frequency,
                bandwidth=self.bw,
                sf=self.sf,
                preamble=8,
                coding_rate=LoRa.CODING_4_5,
                tx_iq=True
                )

    def _freq_to_float(self, frequency):
        """
        MicroPython has some inprecision when doing large float division.
        To counter this, this method first does integer division until we
        reach the decimal breaking point. This doesn't completely elimate
        the issue in all cases, but it does help for a number of commonly
        used frequencies.
        """

        divider = 6
        while divider > 0 and frequency % 10 == 0:
            frequency = frequency // 10
            divider -= 1
        if divider > 0:
            frequency = frequency / (10 ** divider)
        return frequency

    def _make_stat_packet(self):
        now = self.rtc.now()
        STAT_PK["stat"]["time"] = "%d-%02d-%02d %02d:%02d:%02d GMT" % (now[0], now[1], now[2], now[3], now[4], now[5])
        STAT_PK["stat"]["rxnb"] = self.rxnb
        STAT_PK["stat"]["rxok"] = self.rxok
        STAT_PK["stat"]["rxfw"] = self.rxfw
        STAT_PK["stat"]["dwnb"] = self.dwnb
        STAT_PK["stat"]["txnb"] = self.txnb
        return ujson.dumps(STAT_PK)

    def _make_node_packet(self, rx_data, rx_time, tmst, sf, bw, rssi, snr):
        RX_PK["rxpk"][0]["time"] = "%d-%02d-%02dT%02d:%02d:%02d.%dZ" % (rx_time[0], rx_time[1], rx_time[2], rx_time[3], rx_time[4], rx_time[5], rx_time[6])
        RX_PK["rxpk"][0]["tmst"] = tmst
        RX_PK["rxpk"][0]["freq"] = self._freq_to_float(self.frequency)
        RX_PK["rxpk"][0]["datr"] = self._sf_bw_to_dr(sf, bw)
        RX_PK["rxpk"][0]["rssi"] = rssi
        RX_PK["rxpk"][0]["lsnr"] = snr
        RX_PK["rxpk"][0]["data"] = ubinascii.b2a_base64(rx_data)[:-1]
        RX_PK["rxpk"][0]["size"] = len(rx_data)
        return ujson.dumps(RX_PK)

    def _push_data(self, data):
        token = uos.urandom(2)
        packet = bytes([PROTOCOL_VERSION]) + token + bytes([PUSH_DATA]) + ubinascii.unhexlify(self.id) + data
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except Exception as ex:
                self._log('Failed to push uplink packet to server: {}', ex)

    def _pull_data(self):
        token = uos.urandom(2)
        packet = bytes([PROTOCOL_VERSION]) + token + bytes([PULL_DATA]) + ubinascii.unhexlify(self.id)
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except Exception as ex:
                self._log('Failed to pull downlink packets from server: {}', ex)

    def _ack_pull_rsp(self, token, error):
        TX_ACK_PK["txpk_ack"]["error"] = error
        resp = ujson.dumps(TX_ACK_PK)
        packet = bytes([PROTOCOL_VERSION]) + token + bytes([PULL_ACK]) + ubinascii.unhexlify(self.id) + resp
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except Exception as ex:
                self._log('PULL RSP ACK exception: {}', ex)

    def _send_down_link(self, data, tmst, datarate, frequency):
        """
        Transmits a downlink message over LoRa.
        """

        self.lora.init(
            mode=LoRa.LORA,
            frequency=frequency,
            bandwidth=self._dr_to_bw(datarate),
            sf=self._dr_to_sf(datarate),
            preamble=8,
            coding_rate=LoRa.CODING_4_5,
            tx_iq=True
            )
        while utime.ticks_us() < tmst:
            pass
        self.lora_sock.send(data)
        self._log(
            'Sent downlink packet scheduled on {:.3f}, at {:.1f} Mhz using {}: {}',
            tmst / 1000000,
            self._freq_to_float(frequency),
            datarate,
            data
        )

    def _udp_thread(self):
        """
        UDP thread, reads data from the server and handles it.
        """

        while not self.udp_stop:
            try:
                data, src = self.sock.recvfrom(1024)
                _token = data[1:3]
                _type = data[3]
                if _type == PUSH_ACK:
                    self._log("Push ack")
                elif _type == PULL_ACK:
                    self._log("Pull ack")
                elif _type == PULL_RESP:
                    self.dwnb += 1
                    ack_error = TX_ERR_NONE
                    tx_pk = ujson.loads(data[4:])
                    tmst = tx_pk["txpk"]["tmst"]
                    t_us = tmst - utime.ticks_us() - 12500
                    if t_us < 0:
                        t_us += 0xFFFFFFFF
                    if t_us < 20000000:
                        self.uplink_alarm = Timer.Alarm(
                            handler=lambda x: self._send_down_link(
                                ubinascii.a2b_base64(tx_pk["txpk"]["data"]),
                                tx_pk["txpk"]["tmst"] - 50, tx_pk["txpk"]["datr"],
                                int(tx_pk["txpk"]["freq"] * 1000000)
                            ), 
                            us=t_us
                        )
                    else:
                        ack_error = TX_ERR_TOO_LATE
                        self._log('Downlink timestamp error!, t_us: {}', t_us)
                    self._ack_pull_rsp(_token, ack_error)
                    self._log("Pull rsp")
            except usocket.timeout:
                pass
            except OSError as ex:
                if ex.errno != errno.EAGAIN:
                    self._log('UDP recv OSError Exception: {}', ex)
            except Exception as ex:
                self._log('UDP recv Exception: {}', ex)

            # wait before trying to receive again
            utime.sleep_ms(UDP_THREAD_CYCLE_MS)

        # we are to close the socket
        self.sock.close()
        self.udp_stop = False
        self._log('UDP thread stopped')

    def _log(self, message, *args):
        """
        Outputs a log message to stdout.
        """

        print('[{:>10.3f}] {}'.format(
            utime.ticks_ms() / 1000,
            str(message).format(*args)
            ))
예제 #10
0
class LoRaWAN:

    DEBUG = False

    def __init__(self,
                 app_eui,
                 app_key,
                 region=LoRa.EU868,
                 sf=7,
                 adr=True,
                 dr=5,
                 timeout=15):
        """Setup LoRaWAN"""
        self._timeout = timeout
        self._app_eui = ubinascii.unhexlify(app_eui)
        self._app_key = ubinascii.unhexlify(app_key)
        self._socket = None
        self._dr = dr
        self.lora = LoRa(mode=LoRa.LORAWAN, region=region, sf=sf, adr=adr)
        self.setup()

    def setup(self):
        """Try to restore from nvram or join the network with OTAA"""
        self.lora.nvram_restore()

        if not self.lora.has_joined():
            self.join()
        else:
            self.open_socket()

    def join(self):
        try:
            self.dprint("Send join request")
            timeout = self._timeout * 1000
            self.lora.join(
                activation=LoRa.OTAA,
                auth=(self._app_eui, self._app_key),
                timeout=timeout,
                dr=self._dr,
            )

            if self.lora.has_joined():
                self.lora.nvram_save()
                self.open_socket()
                self.dprint("Joined network")
        except LoRa.timeout:
            self.dprint("Timeout error")
            raise

    def open_socket(self, timeout=6):
        self._socket = usocket.socket(usocket.AF_LORA, usocket.SOCK_RAW)
        self._socket.setsockopt(usocket.SOL_LORA, usocket.SO_DR, self._dr)
        self._socket.settimeout(timeout)

    def reset(self):
        """Reset socket, clear on device stored LoRaWAN session and re-join the network"""
        self._socket.close()
        self.lora.lora_erase()
        self.join()

    def send(self, payload, port=1):
        """Send out uplink data as bytes"""
        self._socket.bind(port)
        if self.lora.has_joined():
            if isinstance(payload, (float, str, int)):
                payload = bytes([payload])
            self.dprint("Send payload: {}".format(payload))
            self._socket.setblocking(True)
            self._socket.send(payload)
            self._socket.setblocking(False)
            self.lora.nvram_save()

    def recv(self, rbytes=1):
        """Receive bytes from downlink"""
        retval = self._socket.recvfrom(rbytes)
        self.dprint("Recv payload: {}, port: {}".format(retval[0], retval[1]))
        return retval

    def shutdown(self):
        """Shutdown LoRa modem"""
        self._socket.close()
        self.lora.power_mode(LoRa.SLEEP)

    def dprint(self, message):
        if self.DEBUG:
            print("LoRaWAN: {}".format(message))
예제 #11
0
class NanoGateway:
    """
    Nano gateway class, set up by default for use with TTN, but can be configured
    for any other network supporting the Semtech Packet Forwarder.
    Only required configuration is wifi_ssid and wifi_password which are used for
    connecting to the Internet.
    """
    def __init__(self,
                 id,
                 frequency,
                 datarate,
                 server,
                 port,
                 ntp_server='pool.ntp.org',
                 ntp_period=3600):
        self.id = id
        self.server = server
        self.port = port

        self.frequency = frequency
        self.datarate = datarate

        # self.ssid = ssid
        # self.password = password

        self.ntp_server = ntp_server
        self.ntp_period = ntp_period

        self.server_ip = None

        self.rxnb = 0
        self.rxok = 0
        self.rxfw = 0
        self.dwnb = 0
        self.txnb = 0

        self.sf = self._dr_to_sf(self.datarate)
        self.bw = self._dr_to_bw(self.datarate)

        self.stat_alarm = None
        self.pull_alarm = None
        self.uplink_alarm = None

        self.lte = None
        self.sock = None
        self.udp_stop = False
        self.udp_lock = _thread.allocate_lock()

        self.lora = None
        self.lora_sock = None

        self.rtc = machine.RTC()

    def start(self):
        """
        Starts the LoRaWAN nano gateway.
        """

        pycom.heartbeat(False)

        self._log('Starting LoRaWAN nano gateway with id: {}', self.id)

        # # setup WiFi as a station and connect
        # self.wlan = WLAN(mode=WLAN.STA)
        # self._connect_to_wifi()

        # setup LTE CATM1 connection
        self.lte = LTE(carrier="verizon")
        self._connect_to_LTE()

        # get a time sync
        self._log('Syncing time with {} ...', self.ntp_server)
        self.rtc.ntp_sync(self.ntp_server, update_period=self.ntp_period)
        while not self.rtc.synced():
            utime.sleep_ms(50)
        self._log("RTC NTP sync complete")

        # get the server IP and create an UDP socket
        self.server_ip = usocket.getaddrinfo(self.server, self.port)[0][-1]
        self._log('Opening UDP socket to {} ({}) port {}...', self.server,
                  self.server_ip[0], self.server_ip[1])
        self.sock = usocket.socket(usocket.AF_INET, usocket.SOCK_DGRAM,
                                   usocket.IPPROTO_UDP)
        self.sock.setsockopt(usocket.SOL_SOCKET, usocket.SO_REUSEADDR, 1)
        self.sock.setblocking(False)

        # push the first time immediatelly
        self._push_data(self._make_stat_packet())

        # create the alarms
        self.stat_alarm = Timer.Alarm(
            handler=lambda t: self._push_data(self._make_stat_packet()),
            s=60,
            periodic=True)
        self.pull_alarm = Timer.Alarm(handler=lambda u: self._pull_data(),
                                      s=25,
                                      periodic=True)

        # start the UDP receive thread
        self.udp_stop = False
        _thread.start_new_thread(self._udp_thread, ())

        # initialize the LoRa radio in LORA mode
        self._log('Setting up the LoRa radio at {} Mhz using {}',
                  self._freq_to_float(self.frequency), self.datarate)
        self.lora = LoRa(mode=LoRa.LORA,
                         frequency=self.frequency,
                         bandwidth=self.bw,
                         sf=self.sf,
                         preamble=8,
                         coding_rate=LoRa.CODING_4_5,
                         tx_iq=True)

        # create a raw LoRa socket
        self.lora_sock = usocket.socket(usocket.AF_LORA, usocket.SOCK_RAW)
        self.lora_sock.setblocking(False)
        self.lora_tx_done = False

        self.lora.callback(trigger=(LoRa.RX_PACKET_EVENT
                                    | LoRa.TX_PACKET_EVENT),
                           handler=self._lora_cb)
        self._log('LoRaWAN nano gateway online')

    def stop(self):
        """
        Stops the LoRaWAN nano gateway.
        """

        self._log('Stopping...')

        # send the LoRa radio to sleep
        self.lora.callback(trigger=None, handler=None)
        self.lora.power_mode(LoRa.SLEEP)

        # stop the NTP sync
        self.rtc.ntp_sync(None)

        # cancel all the alarms
        self.stat_alarm.cancel()
        self.pull_alarm.cancel()

        # signal the UDP thread to stop
        self.udp_stop = True
        while self.udp_stop:
            utime.sleep_ms(50)

        # disable LTE
        self.lte.disconnect()
        self.lte.dettach()

    def _connect_to_wifi(self):
        self.wlan.connect(self.ssid, auth=(None, self.password))
        while not self.wlan.isconnected():
            utime.sleep_ms(50)
        self._log('WiFi connected to: {}', self.ssid)

    def _connect_to_LTE(self):
        print("reset modem")
        try:
            self.lte.reset()
        except:
            print("Exception during reset")

        print("delay 5 secs")
        utime.sleep(5.0)

        if self.lte.isattached():
            try:
                print("LTE was already attached, disconnecting...")
                if self.lte.isconnected():
                    print("disconnect")
                    self.lte.disconnect()
            except:
                print("Exception during disconnect")

            try:
                if self.lte.isattached():
                    print("detach")
                    self.lte.dettach()
            except:
                print("Exception during dettach")

            try:
                print("resetting modem...")
                self.lte.reset()
            except:
                print("Exception during reset")

            print("delay 5 secs")
            utime.sleep(5.0)

        # enable network registration and location information, unsolicited result code
        self.at('AT+CEREG=2')

        # print("full functionality level")
        self.at('AT+CFUN=1')
        utime.sleep(1.0)

        # using Hologram SIM
        self.at('AT+CGDCONT=1,"IP","hologram"')

        print("attempt to attach cell modem to base station...")
        # lte.attach()  # do not use attach with custom init for Hologram SIM

        self.at("ATI")
        utime.sleep(2.0)

        i = 0
        while self.lte.isattached() == False:
            # get EPS Network Registration Status:
            # +CEREG: <stat>[,[<tac>],[<ci>],[<AcT>]]
            # <tac> values:
            # 0 - not registered
            # 1 - registered, home network
            # 2 - not registered, but searching...
            # 3 - registration denied
            # 4 - unknown (out of E-UTRAN coverage)
            # 5 - registered, roaming
            r = self.at('AT+CEREG?')
            try:
                r0 = r[0]  # +CREG: 2,<tac>
                r0x = r0.split(',')  # ['+CREG: 2',<tac>]
                tac = int(r0x[1])  # 0..5
                print("tac={}".format(tac))
            except IndexError:
                tac = 0
                print("Index Error!!!")

            # get signal strength
            # +CSQ: <rssi>,<ber>
            # <rssi>: 0..31, 99-unknown
            r = self.at('AT+CSQ')

            # extended error report
            # r = at('AT+CEER')

            if self.lte.isattached():
                print("Modem attached (isattached() function worked)!!!")
                break

            if (tac == 1) or (tac == 5):
                print("Modem attached!!!")
                break

            i = i + 5
            print("not attached: {} secs".format(i))

            if (tac != 0):
                self.blink(BLUE, tac)
            else:
                self.blink(RED, 5)

            utime.sleep(2)

        self.at('AT+CEREG?')
        print("connect: start a data session and obtain an IP address")
        self.lte.connect(cid=3)
        i = 0
        while not self.lte.isconnected():
            i = i + 1
            print("not connected: {}".format(i))
            self.blink(YELLOW, 1)
            utime.sleep(1.0)

        print("connected!!!")
        pycom.rgbled(GREEN)

    def _dr_to_sf(self, dr):
        sf = dr[2:4]
        if sf[1] not in '0123456789':
            sf = sf[:1]
        return int(sf)

    def _dr_to_bw(self, dr):
        bw = dr[-5:]
        if bw == 'BW125':
            return LoRa.BW_125KHZ
        elif bw == 'BW250':
            return LoRa.BW_250KHZ
        else:
            return LoRa.BW_500KHZ

    def _sf_bw_to_dr(self, sf, bw):
        dr = 'SF' + str(sf)
        if bw == LoRa.BW_125KHZ:
            return dr + 'BW125'
        elif bw == LoRa.BW_250KHZ:
            return dr + 'BW250'
        else:
            return dr + 'BW500'

    def _lora_cb(self, lora):
        """
        LoRa radio events callback handler.
        """

        events = lora.events()
        if events & LoRa.RX_PACKET_EVENT:
            self.rxnb += 1
            self.rxok += 1
            rx_data = self.lora_sock.recv(256)
            stats = lora.stats()
            packet = self._make_node_packet(rx_data, self.rtc.now(),
                                            stats.rx_timestamp, stats.sfrx,
                                            self.bw, stats.rssi, stats.snr)
            self._push_data(packet)
            self._log('Received packet: {}', packet)
            self.rxfw += 1
        if events & LoRa.TX_PACKET_EVENT:
            self.txnb += 1
            lora.init(mode=LoRa.LORA,
                      frequency=self.frequency,
                      bandwidth=self.bw,
                      sf=self.sf,
                      preamble=8,
                      coding_rate=LoRa.CODING_4_5,
                      tx_iq=True)

    def _freq_to_float(self, frequency):
        """
        MicroPython has some inprecision when doing large float division.
        To counter this, this method first does integer division until we
        reach the decimal breaking point. This doesn't completely elimate
        the issue in all cases, but it does help for a number of commonly
        used frequencies.
        """

        divider = 6
        while divider > 0 and frequency % 10 == 0:
            frequency = frequency // 10
            divider -= 1
        if divider > 0:
            frequency = frequency / (10**divider)
        return frequency

    def _make_stat_packet(self):
        now = self.rtc.now()
        STAT_PK["stat"]["time"] = "%d-%02d-%02d %02d:%02d:%02d GMT" % (
            now[0], now[1], now[2], now[3], now[4], now[5])
        STAT_PK["stat"]["rxnb"] = self.rxnb
        STAT_PK["stat"]["rxok"] = self.rxok
        STAT_PK["stat"]["rxfw"] = self.rxfw
        STAT_PK["stat"]["dwnb"] = self.dwnb
        STAT_PK["stat"]["txnb"] = self.txnb
        return ujson.dumps(STAT_PK)

    def _make_node_packet(self, rx_data, rx_time, tmst, sf, bw, rssi, snr):
        RX_PK["rxpk"][0]["time"] = "%d-%02d-%02dT%02d:%02d:%02d.%dZ" % (
            rx_time[0], rx_time[1], rx_time[2], rx_time[3], rx_time[4],
            rx_time[5], rx_time[6])
        RX_PK["rxpk"][0]["tmst"] = tmst
        RX_PK["rxpk"][0]["freq"] = self._freq_to_float(self.frequency)
        RX_PK["rxpk"][0]["datr"] = self._sf_bw_to_dr(sf, bw)
        RX_PK["rxpk"][0]["rssi"] = rssi
        RX_PK["rxpk"][0]["lsnr"] = snr
        RX_PK["rxpk"][0]["data"] = ubinascii.b2a_base64(rx_data)[:-1]
        RX_PK["rxpk"][0]["size"] = len(rx_data)
        return ujson.dumps(RX_PK)

    def _push_data(self, data):
        token = uos.urandom(2)
        packet = bytes([PROTOCOL_VERSION]) + token + bytes(
            [PUSH_DATA]) + ubinascii.unhexlify(self.id) + data
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except Exception as ex:
                self._log('Failed to push uplink packet to server: {}', ex)

    def _pull_data(self):
        token = uos.urandom(2)
        packet = bytes([PROTOCOL_VERSION]) + token + bytes(
            [PULL_DATA]) + ubinascii.unhexlify(self.id)
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except Exception as ex:
                self._log('Failed to pull downlink packets from server: {}',
                          ex)

    def _ack_pull_rsp(self, token, error):
        TX_ACK_PK["txpk_ack"]["error"] = error
        resp = ujson.dumps(TX_ACK_PK)
        packet = bytes([PROTOCOL_VERSION]) + token + bytes(
            [PULL_ACK]) + ubinascii.unhexlify(self.id) + resp
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except Exception as ex:
                self._log('PULL RSP ACK exception: {}', ex)

    def _send_down_link(self, data, tmst, datarate, frequency):
        """
        Transmits a downlink message over LoRa.
        """

        self.lora.init(mode=LoRa.LORA,
                       frequency=frequency,
                       bandwidth=self._dr_to_bw(datarate),
                       sf=self._dr_to_sf(datarate),
                       preamble=8,
                       coding_rate=LoRa.CODING_4_5,
                       tx_iq=True)
        while utime.ticks_cpu() < tmst:
            pass
        self.lora_sock.send(data)
        self._log(
            'Sent downlink packet scheduled on {:.3f}, at {:.3f} Mhz using {}: {}',
            tmst / 1000000, self._freq_to_float(frequency), datarate, data)

    def _udp_thread(self):
        """
        UDP thread, reads data from the server and handles it.
        """

        while not self.udp_stop:
            try:
                data, src = self.sock.recvfrom(1024)
                _token = data[1:3]
                _type = data[3]
                if _type == PUSH_ACK:
                    self._log("Push ack")
                elif _type == PULL_ACK:
                    self._log("Pull ack")
                elif _type == PULL_RESP:
                    self.dwnb += 1
                    ack_error = TX_ERR_NONE
                    tx_pk = ujson.loads(data[4:])
                    tmst = tx_pk["txpk"]["tmst"]
                    t_us = tmst - utime.ticks_cpu() - 15000
                    if t_us < 0:
                        t_us += 0xFFFFFFFF
                    if t_us < 20000000:
                        self.uplink_alarm = Timer.Alarm(
                            handler=lambda x: self._send_down_link(
                                ubinascii.a2b_base64(tx_pk["txpk"]["data"]),
                                tx_pk["txpk"]["tmst"] - 50, tx_pk["txpk"][
                                    "datr"],
                                int(tx_pk["txpk"]["freq"] * 1000) * 1000),
                            us=t_us)
                    else:
                        ack_error = TX_ERR_TOO_LATE
                        self._log('Downlink timestamp error!, t_us: {}', t_us)
                    self._ack_pull_rsp(_token, ack_error)
                    self._log("Pull rsp")
            except usocket.timeout:
                pass
            except OSError as ex:
                if ex.errno != errno.EAGAIN:
                    self._log('UDP recv OSError Exception: {}', ex)
            except Exception as ex:
                self._log('UDP recv Exception: {}', ex)

            # wait before trying to receive again
            utime.sleep_ms(UDP_THREAD_CYCLE_MS)

        # we are to close the socket
        self.sock.close()
        self.udp_stop = False
        self._log('UDP thread stopped')

    def _log(self, message, *args):
        """
        Outputs a log message to stdout.
        """

        print('[{:>10.3f}] {}'.format(utime.ticks_ms() / 1000,
                                      str(message).format(*args)))

    def at(self, cmd):
        print("modem command: {}".format(cmd))
        r = self.lte.send_at_cmd(cmd).split('\r\n')
        r = list(filter(None, r))
        print("response={}".format(r))
        return r

    def blink(self, rgb, n):
        for i in range(n):
            pycom.rgbled(rgb)
            utime.sleep(0.25)
            pycom.rgbled(BLACK)
            utime.sleep(0.1)
예제 #12
0
class NanoGateway:
    """
    Nano gateway class, set up by default for use with TTN, but can be configured
    for any other network supporting the Semtech Packet Forwarder.
    Only required configuration is wifi_ssid and wifi_password which are used for
    connecting to the Internet.
    """

    def __init__(self, id, frequency, datarate, ssid, password, server, port, ntp_server='pool.ntp.org', ntp_period=3600):
        self.id = id
        self.server = server
        self.port = port

        self.frequency = frequency
        self.datarate = datarate

        self.ssid = ssid
        self.password = password

        self.ntp_server = ntp_server
        self.ntp_period = ntp_period

        self.server_ip = None

        self.rxnb = 0
        self.rxok = 0
        self.rxfw = 0
        self.dwnb = 0
        self.txnb = 0

        self.sf = self._dr_to_sf(self.datarate)
        self.bw = self._dr_to_bw(self.datarate)
        self.region = LoRa.AU915

        self.stat_alarm = None
        self.pull_alarm = None
        self.uplink_alarm = None

        self.wlan = None
        self.sock = None
        self.udp_stop = False
        self.udp_lock = _thread.allocate_lock()

        self.lora = None
        self.lora_sock = None

        self.rtc = machine.RTC()

    def start(self):
        """
        Starts the LoRaWAN nano gateway.
        """

        self._log('Starting LoRaWAN nano gateway with id: {}', self.id)

        # setup WiFi as a station and connect
        self.wlan = WLAN(mode=WLAN.STA)
        self._connect_to_wifi()

        # get a time sync
        self._log('Syncing time with {} ...', self.ntp_server)
        self.rtc.ntp_sync(self.ntp_server, update_period=self.ntp_period)
        while not self.rtc.synced():
            utime.sleep_ms(50)
        self._log("RTC NTP sync complete")

        # get the server IP and create an UDP socket
        self.server_ip = usocket.getaddrinfo(self.server, self.port)[0][-1]
        self._log('Opening UDP socket to {} ({}) port {}...', self.server, self.server_ip[0], self.server_ip[1])
        self.sock = usocket.socket(usocket.AF_INET, usocket.SOCK_DGRAM, usocket.IPPROTO_UDP)
        self.sock.setsockopt(usocket.SOL_SOCKET, usocket.SO_REUSEADDR, 1)
        self.sock.setblocking(False)

        # push the first time immediatelly
        self._push_data(self._make_stat_packet())

        # create the alarms
        self.stat_alarm = Timer.Alarm(handler=lambda t: self._push_data(self._make_stat_packet()), s=60, periodic=True)
        self.pull_alarm = Timer.Alarm(handler=lambda u: self._pull_data(), s=25, periodic=True)

        # start the UDP receive thread
        self.udp_stop = False
        _thread.start_new_thread(self._udp_thread, ())

        # initialize the LoRa radio in LORA mode
        self._log('Setting up the LoRa radio at {} Mhz using {}', self._freq_to_float(self.frequency), self.datarate)
        self.lora = LoRa(
            mode=LoRa.LORA,
            region=self.region,
            frequency=self.frequency,
            bandwidth=self.bw,
            sf=self.sf,
            preamble=8,
            coding_rate=LoRa.CODING_4_5,
            power_mode=LoRa.ALWAYS_ON,
            #tx_iq=True
        )

        # create a raw LoRa socket
        self.lora_sock = usocket.socket(usocket.AF_LORA, usocket.SOCK_RAW)
        self.lora_sock.setblocking(False)
        self.lora_tx_done = False

        self.lora.callback(trigger=(LoRa.RX_PACKET_EVENT | LoRa.TX_PACKET_EVENT), handler=self._lora_cb)
        
        if uos.uname()[0] == "LoPy":
            self.window_compensation = -1000
        else:
            self.window_compensation = -6000
        self.downlink_count = 0
        
        self._log('LoRaWAN nano gateway online')

    def stop(self):
        """
        Stops the LoRaWAN nano gateway.
        """

        self._log('Stopping...')

        # send the LoRa radio to sleep
        self.lora.callback(trigger=None, handler=None)
        self.lora.power_mode(LoRa.SLEEP)

        # stop the NTP sync
        self.rtc.ntp_sync(None)

        # cancel all the alarms
        self.stat_alarm.cancel()
        self.pull_alarm.cancel()

        # signal the UDP thread to stop
        self.udp_stop = True
        while self.udp_stop:
            utime.sleep_ms(50)

        # disable WLAN
        self.wlan.disconnect()
        self.wlan.deinit()

    def _connect_to_wifi(self):
        self.wlan.connect(self.ssid, auth=(None, self.password))
        while not self.wlan.isconnected():
            utime.sleep_ms(50)
        self._log('WiFi connected to: {}', self.ssid)

    def _dr_to_sf(self, dr):
        sf = dr[2:4]
        if sf[1] not in '0123456789':
            sf = sf[:1]
        return int(sf)

    def _dr_to_bw(self, dr):
        bw = dr[-5:]
        if bw == 'BW125':
            return LoRa.BW_125KHZ
        elif bw == 'BW250':
            return LoRa.BW_250KHZ
        else:
            return LoRa.BW_500KHZ

    def _sf_bw_to_dr(self, sf, bw):
        dr = 'SF' + str(sf)
        if bw == LoRa.BW_125KHZ:
            return dr + 'BW125'
        elif bw == LoRa.BW_250KHZ:
            return dr + 'BW250'
        else:
            return dr + 'BW500'

    def _lora_cb(self, lora):
        """
        LoRa radio events callback handler.
        """

        events = lora.events()
        if events & LoRa.RX_PACKET_EVENT:
            self.rxnb += 1
            self.rxok += 1
            rx_data = self.lora_sock.recv(256)
            stats = lora.stats()
            if DEBUG:
                self._log("stats "+ujson.dumps(stats))
                self._log('rx_timestamp diff: {}', utime.ticks_diff(stats.rx_timestamp,utime.ticks_cpu()))
            packet = self._make_node_packet(rx_data, self.rtc.now(), stats.rx_timestamp, stats.sfrx, self.bw, stats.rssi, stats.snr)
            packet = self.frequency_rounding_fix(packet, self.frequency)
            self._log('Received and uploading packet: {}', packet)
            self._push_data(packet)
            self._log('after _push_data')
            self.rxfw += 1
            
        if events & LoRa.TX_PACKET_EVENT:
            self.txnb += 1
            lora.init(
                mode=LoRa.LORA,
                region=self.region,
                frequency=self.frequency,
                bandwidth=self.bw,
                sf=self.sf,
                preamble=8,
                coding_rate=LoRa.CODING_4_5,
                power_mode=LoRa.ALWAYS_ON,
                #tx_iq=True
                )

    def _freq_to_float(self, frequency):
        """
        MicroPython has some inprecision when doing large float division.
        To counter this, this method first does integer division until we
        reach the decimal breaking point. This doesn't completely elimate
        the issue in all cases, but it does help for a number of commonly
        used frequencies.
        """

        divider = 6
        while divider > 0 and frequency % 10 == 0:
            frequency = frequency // 10
            divider -= 1
        if divider > 0:
            frequency = frequency / (10 ** divider)
        return frequency

    def frequency_rounding_fix(self, packet, frequency):
        freq = str(frequency)[0:3] + '.' + str(frequency)[3]

        start = packet.find("freq\":")
        end = packet.find(",", start)

        packet = packet[:start + 7] + freq + packet[end:]

        return packet

    def _make_stat_packet(self):
        now = self.rtc.now()
        STAT_PK["stat"]["time"] = "%d-%02d-%02d %02d:%02d:%02d GMT" % (now[0], now[1], now[2], now[3], now[4], now[5])
        STAT_PK["stat"]["rxnb"] = self.rxnb
        STAT_PK["stat"]["rxok"] = self.rxok
        STAT_PK["stat"]["rxfw"] = self.rxfw
        STAT_PK["stat"]["dwnb"] = self.dwnb
        STAT_PK["stat"]["txnb"] = self.txnb
        return ujson.dumps(STAT_PK)

    def _make_node_packet(self, rx_data, rx_time, tmst, sf, bw, rssi, snr):
        RX_PK["rxpk"][0]["time"] = "%d-%02d-%02dT%02d:%02d:%02d.%dZ" % (rx_time[0], rx_time[1], rx_time[2], rx_time[3], rx_time[4], rx_time[5], rx_time[6])
        RX_PK["rxpk"][0]["tmst"] = tmst
        RX_PK["rxpk"][0]["freq"] = self._freq_to_float(self.frequency)
        RX_PK["rxpk"][0]["datr"] = self._sf_bw_to_dr(sf, bw)
        RX_PK["rxpk"][0]["rssi"] = rssi
        RX_PK["rxpk"][0]["lsnr"] = snr
        RX_PK["rxpk"][0]["data"] = ubinascii.b2a_base64(rx_data)[:-1]
        RX_PK["rxpk"][0]["size"] = len(rx_data)
        return ujson.dumps(RX_PK)

    def _push_data(self, data):
        token = uos.urandom(2)
        packet = bytes([PROTOCOL_VERSION]) + token + bytes([PUSH_DATA]) + ubinascii.unhexlify(self.id) + data
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except Exception as ex:
                self._log('Failed to push uplink packet to server: {}', ex)

    def _pull_data(self):
        token = uos.urandom(2)
        packet = bytes([PROTOCOL_VERSION]) + token + bytes([PULL_DATA]) + ubinascii.unhexlify(self.id)
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except Exception as ex:
                self._log('Failed to pull downlink packets from server: {}', ex)

    def _ack_pull_rsp(self, token, error):
        TX_ACK_PK["txpk_ack"]["error"] = error
        resp = ujson.dumps(TX_ACK_PK)
        packet = bytes([PROTOCOL_VERSION]) + token + bytes([PULL_ACK]) + ubinascii.unhexlify(self.id) + resp
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except Exception as ex:
                self._log('PULL RSP ACK exception: {}', ex)

    def _send_down_link(self, data, tmst, datarate, frequency):
        """
        Transmits a downlink message over LoRa.
        """
        self.lora.init(
            mode=LoRa.LORA,
            region=self.region,
            frequency=frequency,
            bandwidth=self._dr_to_bw(datarate),     # LoRa.BW_125KHZ
            sf=self._dr_to_sf(datarate),
            preamble=8,
            coding_rate=LoRa.CODING_4_5,
            power_mode=LoRa.ALWAYS_ON,
            #tx_iq=True
        )

        if WINDOW_COMPENSATION=='cycle':
            self.window_compensation = -((self.downlink_count % 25) * 1000)
        else:
            self.window_compensation = WINDOW_COMPENSATION

        t_adj = utime.ticks_add(tmst, self.window_compensation)
        self.lora_sock.settimeout(1)
        t_cpu = utime.ticks_cpu()
        self._log("BEFORE spin wait at {} late {}",t_cpu,t_cpu-tmst)
        while utime.ticks_diff(t_adj, utime.ticks_cpu()) > 0:
            pass
        t_cpu = utime.ticks_cpu()
        self._log("BEFORE lora_sock.send at {} late {} window_compensation {}",t_cpu,t_cpu-tmst,self.window_compensation)
        self.lora_sock.send(data)
        self._log("AFTER lora_sock.send late {}",utime.ticks_cpu()-tmst)
        self.lora_sock.setblocking(False)
        self._log(
            'Sent downlink packet scheduled on {}, at {:,d} Hz using {}: {}',
            tmst,
            frequency,
            datarate,
            data
        )
        self.downlink_count += 1

    def _send_down_link_class_c(self, data, datarate, frequency):
        self.lora.init(
            mode=LoRa.LORA,
            frequency=frequency,
            bandwidth=self._dr_to_bw(datarate),
            sf=self._dr_to_sf(datarate),
            preamble=8,
            coding_rate=LoRa.CODING_4_5,
            region=self.region,
            power_mode=LoRa.ALWAYS_ON,
            #tx_iq=True,
            device_class=LoRa.CLASS_C
            )

        self.lora_sock.send(data)
        self._log(
            'Sent downlink packet scheduled on {}, at {:.3f} Mhz using {}: {}',
            utime.ticks_cpu(),
            self._freq_to_float(frequency),
            datarate,
            data
        )

    def _udp_thread(self):
        """
        UDP thread, reads data from the server and handles it.
        """
        loops = 0
        while not self.udp_stop:
            if loops % 20 == 19:
                b4 = utime.ticks_cpu()
                gc.collect()
                self._log("gc.collect for {} us",utime.ticks_diff(utime.ticks_cpu(),b4))
            b4 = utime.ticks_cpu()
            utime.sleep_ms(UDP_THREAD_CYCLE_MS)
            t_diff = utime.ticks_diff(utime.ticks_cpu(),b4)
            if t_diff > (UDP_THREAD_CYCLE_MS*1000*1.5):
                self._log("overslept! for {} us",t_diff)
            try:
                b4 = utime.ticks_cpu()
                data, src = self.sock.recvfrom(1024)
                self._log("sock.recvfrom for {} us",utime.ticks_diff(utime.ticks_cpu(),b4))
                _token = data[1:3]
                _type = data[3]
                if _type == PUSH_ACK:
                    self._log("Push ack")
                elif _type == PULL_ACK:
                    self._log("Pull ack")
                elif _type == PULL_RESP:
                    self._log("Pull resp")
                    self.dwnb += 1
                    ack_error = TX_ERR_NONE
                    tx_pk = ujson.loads(data[4:])
                    if DEBUG:
                       self._log("tx data "+ujson.dumps(tx_pk))
                    payload = ubinascii.a2b_base64(tx_pk["txpk"]["data"])
                    # depending on the board, pull the downlink message 1 or 6 ms upfronnt
                    
                    # tmst = utime.ticks_add(tx_pk["txpk"]["tmst"], self.window_compensation)
                    # t_us = utime.ticks_diff(utime.ticks_cpu(), utime.ticks_add(tmst, -15000))
                    tmst = tx_pk["txpk"]["tmst"]
                    t_req = utime.ticks_add(tmst, -RX_DELAY_TIMER_EARLY)
                    t_cpu = utime.ticks_cpu()
                    self._log("t_cpu {}",t_cpu)
                    t_us = utime.ticks_diff(t_req, t_cpu)
                    self._log("t_us {}",t_us)
                    if 1000 < t_us < 10000000:
                        self._log("Delaying for {} at {}, so should fire at t_req {}, compensated early_by {}",t_us,t_cpu,t_req,RX_DELAY_TIMER_EARLY)
                        def handler(x):
                            t_cpu = utime.ticks_cpu()
                            self._log("_send_down_link alarm fired at {} late {}us",t_cpu,t_cpu-t_req)
                            self._send_down_link(
                                payload,
                                tmst, tx_pk["txpk"]["datr"],
                                int(tx_pk["txpk"]["freq"] * 1000 + 0.0005) * 1000
                            )
                        self.uplink_alarm = Timer.Alarm(handler=handler, us=t_us)
                    else:
                        ack_error = TX_ERR_TOO_LATE
                        self._log('Downlink timestamp error!, t_us: {}', t_us)
                    self._ack_pull_rsp(_token, ack_error)
                    self._log("Pull rsp")
            except usocket.timeout:
                pass
            except OSError as ex:
                if ex.args[0] != errno.EAGAIN:
                    self._log('UDP recv OSError Exception: {}', ex)
            except Exception as ex:
                self._log('UDP recv Exception: {}', ex)

        # we are to close the socket
        self.sock.close()
        self.udp_stop = False
        self._log('UDP thread stopped')

    def _log(self, message, *args):
        """
        Outputs a log message to stdout.
        """
        if len(args)==0:
            print('[{}] '.format(utime.ticks_cpu()) + str(message))
        else:
            print('[{}] {}'.format(
                utime.ticks_cpu(),
                str(message).format(*args)
                ))
예제 #13
0
class NanoGateway:
    def __init__(self,
                 id,
                 frequency,
                 datarate,
                 ssid,
                 password,
                 server,
                 port,
                 ntp='pool.ntp.org',
                 ntp_period=3600):
        self.id = id
        self.frequency = frequency
        self.datarate = datarate
        self.sf = self._dr_to_sf(datarate)
        self.ssid = ssid
        self.password = password
        self.server = server
        self.port = port
        self.ntp = ntp
        self.ntp_period = ntp_period
        self.rtc = machine.RTC()

        self.rxnb = 0
        self.rxok = 0
        self.rxfw = 0
        self.dwnb = 0
        self.txnb = 0

        self.stat_alarm = None
        self.pull_alarm = None
        self.uplink_alarm = None

        self.udp_stop = False
        self.udp_lock = _thread.allocate_lock()

        self.lora = None
        self.lora_sock = None

    def start(self):
        # setup WiFi as a station and connect
        self.wlan = WLAN(mode=WLAN.STA)
        self._connect_to_wifi()

        # get a time sync
        self.rtc.ntp_sync(self.ntp, update_period=self.ntp_period)
        while not self.rtc.synced():
            time.sleep_ms(50)
        print("RTC NTP sync complete")

        # get the server IP and create an UDP socket
        self.server_ip = socket.getaddrinfo(self.server, self.port)[0][-1]
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM,
                                  socket.IPPROTO_UDP)
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.sock.setblocking(False)

        # push the first time immediatelly
        self._push_data(self._make_stat_packet())

        # create the alarms
        self.stat_alarm = Timer.Alarm(
            handler=lambda t: self._push_data(self._make_stat_packet()),
            s=60,
            periodic=True)
        self.pull_alarm = Timer.Alarm(handler=lambda u: self._pull_data(),
                                      s=25,
                                      periodic=True)

        # start the UDP receive thread
        self.udp_stop = False
        _thread.start_new_thread(self._udp_thread, ())

        # initialize the LoRa radio in LORA mode
        self.lora = LoRa(mode=LoRa.LORA,
                         frequency=self.frequency,
                         bandwidth=LoRa.BW_125KHZ,
                         sf=self.sf,
                         preamble=8,
                         coding_rate=LoRa.CODING_4_5,
                         tx_iq=True)
        # create a raw LoRa socket
        self.lora_sock = socket.socket(socket.AF_LORA, socket.SOCK_RAW)
        self.lora_sock.setblocking(False)
        self.lora_tx_done = False

        self.lora.callback(trigger=(LoRa.RX_PACKET_EVENT
                                    | LoRa.TX_PACKET_EVENT),
                           handler=self._lora_cb)

    def stop(self):
        # send the LoRa radio to sleep, stop the NTP sync, cancel the alarms and kill the UDP thread
        self.lora.callback(trigger=None, handler=None)
        self.lora.power_mode(LoRa.SLEEP)
        self.rtc.ntp_sync(None)
        self.stat_alarm.cancel()
        self.pull_alarm.cancel()
        self.udp_stop = True
        while self.udp_stop:
            time.sleep_ms(50)
        # disable WLAN
        self.wlan.disconnect()
        self.wlan.deinit()

    def _connect_to_wifi(self):
        self.wlan.connect(self.ssid, auth=(None, self.password))
        while not self.wlan.isconnected():
            time.sleep_ms(50)
        print("WiFi connected")

    def _dr_to_sf(self, dr):
        sf = dr[2:4]
        if sf[1] not in '0123456789':
            sf = sf[:1]
        return int(sf)

    def _sf_to_dr(self, sf):
        return self.datarate

    def _make_stat_packet(self):
        now = self.rtc.now()
        STAT_PK["stat"]["time"] = "%d-%02d-%02d %02d:%02d:%02d GMT" % (
            now[0], now[1], now[2], now[3], now[4], now[5])
        STAT_PK["stat"]["rxnb"] = self.rxnb
        STAT_PK["stat"]["rxok"] = self.rxok
        STAT_PK["stat"]["rxfw"] = self.rxfw
        STAT_PK["stat"]["dwnb"] = self.dwnb
        STAT_PK["stat"]["txnb"] = self.txnb
        return json.dumps(STAT_PK)

    def _make_node_packet(self, rx_data, rx_time, tmst, sf, rssi, snr):
        RX_PK["rxpk"][0]["time"] = "%d-%02d-%02dT%02d:%02d:%02d.%dZ" % (
            rx_time[0], rx_time[1], rx_time[2], rx_time[3], rx_time[4],
            rx_time[5], rx_time[6])
        RX_PK["rxpk"][0]["tmst"] = tmst
        RX_PK["rxpk"][0]["datr"] = self._sf_to_dr(sf)
        RX_PK["rxpk"][0]["rssi"] = rssi
        RX_PK["rxpk"][0]["lsnr"] = float(snr)
        RX_PK["rxpk"][0]["data"] = binascii.b2a_base64(rx_data)[:-1]
        RX_PK["rxpk"][0]["size"] = len(rx_data)
        return json.dumps(RX_PK)

    def _push_data(self, data):
        token = os.urandom(2)
        packet = bytes([PROTOCOL_VERSION]) + token + bytes(
            [PUSH_DATA]) + binascii.unhexlify(self.id) + data
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except Exception:
                print("PUSH exception")

    def _pull_data(self):
        token = os.urandom(2)
        packet = bytes([PROTOCOL_VERSION]) + token + bytes(
            [PULL_DATA]) + binascii.unhexlify(self.id)
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except Exception:
                print("PULL exception")

    def _ack_pull_rsp(self, token, error):
        TX_ACK_PK["txpk_ack"]["error"] = error
        resp = json.dumps(TX_ACK_PK)
        packet = bytes([PROTOCOL_VERSION]) + token + bytes(
            [PULL_ACK]) + binascii.unhexlify(self.id) + resp
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except Exception:
                print("PULL RSP ACK exception")

    def _lora_cb(self, lora):
        events = lora.events()
        if events & LoRa.RX_PACKET_EVENT:
            self.rxnb += 1
            self.rxok += 1
            rx_data = self.lora_sock.recv(256)
            stats = lora.stats()
            self._push_data(
                self._make_node_packet(rx_data, self.rtc.now(),
                                       stats.rx_timestamp, stats.sfrx,
                                       stats.rssi, stats.snr))
            self.rxfw += 1
        if events & LoRa.TX_PACKET_EVENT:
            self.txnb += 1
            lora.init(mode=LoRa.LORA,
                      frequency=self.frequency,
                      bandwidth=LoRa.BW_125KHZ,
                      sf=self.sf,
                      preamble=8,
                      coding_rate=LoRa.CODING_4_5,
                      tx_iq=True)

    def _send_down_link(self, data, tmst, datarate, frequency):
        self.lora.init(mode=LoRa.LORA,
                       frequency=frequency,
                       bandwidth=LoRa.BW_125KHZ,
                       sf=self._dr_to_sf(datarate),
                       preamble=8,
                       coding_rate=LoRa.CODING_4_5,
                       tx_iq=True)
        while time.ticks_us() < tmst:
            pass
        self.lora_sock.send(data)

    def _udp_thread(self):
        while not self.udp_stop:
            try:
                data, src = self.sock.recvfrom(1024)
                _token = data[1:3]
                _type = data[3]
                if _type == PUSH_ACK:
                    print("Push ack")
                elif _type == PULL_ACK:
                    print("Pull ack")
                elif _type == PULL_RESP:
                    self.dwnb += 1
                    ack_error = TX_ERR_NONE
                    tx_pk = json.loads(data[4:])
                    tmst = tx_pk["txpk"]["tmst"]
                    t_us = tmst - time.ticks_us() - 12500
                    if t_us < 0:
                        t_us += 0xFFFFFFFF
                    if t_us < 20000000:
                        self.uplink_alarm = Timer.Alarm(
                            handler=lambda x: self._send_down_link(
                                binascii.a2b_base64(tx_pk["txpk"]["data"]),
                                tx_pk["txpk"]["tmst"] - 50, tx_pk["txpk"][
                                    "datr"],
                                int(tx_pk["txpk"]["freq"] * 1000000)),
                            us=t_us)
                    else:
                        ack_error = TX_ERR_TOO_LATE
                        print("Downlink timestamp error!, t_us:", t_us)
                    self._ack_pull_rsp(_token, ack_error)
                    print("Pull rsp")
            except socket.timeout:
                pass
            except OSError as e:
                if e.errno == errno.EAGAIN:
                    pass
                else:
                    print("UDP recv OSError Exception")
            except Exception:
                print("UDP recv Exception")
            # wait before trying to receive again
            time.sleep(0.025)

        # we are to close the socket
        self.sock.close()
        self.udp_stop = False