OTAA Node to Nano Gateway AU915
-
Hi,
I just recently got a couple of Lopy 4's to act as nodes, but while I wait for my gateway, I was trying to set the pair up in a LoraWan nano gateway and OTAA node test.I’m using the GitHub code for a base https://github.com/pycom/pycom-libraries/tree/master/examples/lorawan-nano-gateway and trying to modify it for use in Australia AU915.
I think I got the gateway working but it never shows connected in the TNN console.
#!/usr/bin/env python # # Copyright (c) 2019, Pycom Limited. # # This software is licensed under the GNU GPL version 3 or any # later version, with permitted additional terms. For more information # see the Pycom Licence v1.0 document supplied with this file, or # available at https://www.pycom.io/opensource/licensing # """ LoPy LoRaWAN Nano Gateway configuration options """ import machine import ubinascii WIFI_MAC = ubinascii.hexlify(machine.unique_id()).upper() # Set the Gateway ID to be the first 3 bytes of MAC address + 'FFFE' + last 3 bytes of MAC address GATEWAY_ID = WIFI_MAC[:6] + "FFFE" + WIFI_MAC[6:12] SERVER = 'router.thethings.meshed.com.au' PORT = 1700 NTP = "pool.ntp.org" NTP_PERIOD_S = 3600 WIFI_SSID = 'ssid' WIFI_PASS = 'password' WIFI_ADDRESS = '192.168.1.50' WIFI_MASK = '255.255.255.0' WIFI_GATEWAY = '192.168.1.1' # for EU868 # LORA_FREQUENCY = 868100000 # LORA_GW_DR = "SF7BW125" # DR_5 # LORA_NODE_DR = 5 # for US915 # LORA_FREQUENCY = 903900000 # LORA_GW_DR = "SF7BW125" # DR_3 # LORA_NODE_DR = 3 # for AU915 LORA_FREQUENCY = 916800000 LORA_GW_DR = "SF7BW125" LORA_NODE_DR = 4
#!/usr/bin/env python # # Copyright (c) 2019, Pycom Limited. # # This software is licensed under the GNU GPL version 3 or any # later version, with permitted additional terms. For more information # see the Pycom Licence v1.0 document supplied with this file, or # available at https://www.pycom.io/opensource/licensing # """ LoPy LoRaWAN Nano Gateway. Can be used for both EU868 and US915. """ import errno import machine import ubinascii import ujson import uos import usocket import utime import _thread from micropython import const from network import LoRa from network import WLAN from machine import Timer 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(20) 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': '' } } 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, address, mask, gateway, 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.address = address self.mask = mask self.gateway = gateway 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.wlan.ifconfig(config=(self.address, self.mask, self.gateway, self.gateway)) 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.LORAWAN, 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) packet = self.frequency_rounding_fix(packet, self.frequency) 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 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, 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 _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, tx_iq=True, device_class=LoRa.CLASS_C ) self.lora_sock.send(data) self._log( 'Sent downlink packet scheduled on {:.3f}, at {:.3f} Mhz using {}: {}', utime.time(), 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:]) if "tmst" in data: 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) else: self.uplink_alarm = Timer.Alarm( handler=lambda x: self._send_down_link_class_c( ubinascii.a2b_base64(tx_pk["txpk"]["data"]), tx_pk["txpk"]["datr"], int(tx_pk["txpk"]["freq"] * 1000) * 1000 ), us=50 ) 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) ))
#!/usr/bin/env python # # Copyright (c) 2019, Pycom Limited. # # This software is licensed under the GNU GPL version 3 or any # later version, with permitted additional terms. For more information # see the Pycom Licence v1.0 document supplied with this file, or # available at https://www.pycom.io/opensource/licensing # """ LoPy LoRaWAN Nano Gateway example usage """ import config from nanogateway import NanoGateway if __name__ == '__main__': nanogw = NanoGateway( id=config.GATEWAY_ID, frequency=config.LORA_FREQUENCY, datarate=config.LORA_GW_DR, ssid=config.WIFI_SSID, password=config.WIFI_PASS, address=config.WIFI_ADDRESS, mask=config.WIFI_MASK, gateway=config.WIFI_GATEWAY, server=config.SERVER, port=config.PORT, ntp_server=config.NTP, ntp_period=config.NTP_PERIOD_S ) nanogw.start() nanogw._log('You may now press ENTER to enter the REPL') input()
Start up log
rst:0x7 (TG0WDT_SYS_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT) configsip: 0, SPIWP:0xee clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00 mode:DIO, clock div:1 load:0x3fff8028,len:8 load:0x3fff8030,len:1688 load:0x4009fa00,len:0 load:0x4009fa00,len:14592 entry 0x400a059c WMAC: 3C71BF8750F0 Firmware: 1.18.2.r6 Pybytes: 0.9.12 Pybytes configuration read from /flash/pybytes_config.json Initialized watchdog for WiFi and LTE connection with timeout 660000 ms WiFi connection established WARNING: consider enabling certificate validation Using MQTT over TLS Connected to MQTT mqtt.pybytes.pycom.io Pybytes connected successfully (using the built-in pybytes library) Now starting user code in main.py [ 31.212] Starting LoRaWAN nano gateway with id: b'3C71BFFFFE8750F0' [ 31.238] WiFi connected to: ssid [ 31.245] Syncing time with pool.ntp.org ... [ 35.502] RTC NTP sync complete [ 42.098] Opening UDP socket to router.thethings.meshed.com.au (224.94.149.63) port 1700... [ 42.116] Setting up the LoRa radio at 916.7999 Mhz using SF7BW125 [ 42.160] LoRaWAN nano gateway online [ 42.167] You may now press ENTER to enter the REPL
As for the OTAA client I can not get it to connect
config.py#!/usr/bin/env python # # Copyright (c) 2019, Pycom Limited. # # This software is licensed under the GNU GPL version 3 or any # later version, with permitted additional terms. For more information # see the Pycom Licence v1.0 document supplied with this file, or # available at https://www.pycom.io/opensource/licensing # """ LoPy Node configuration options """ import machine import ubinascii WIFI_MAC = ubinascii.hexlify(machine.unique_id()).upper() # Set the Node ID to be the first 3 bytes of MAC address + 'FFFE' + last 3 bytes of MAC address NODE_ID = WIFI_MAC[:6] + "FFFE" + WIFI_MAC[6:12] # for EU868 # LORA_FREQUENCY = 868100000 # LORA_GW_DR = "SF7BW125" # DR_5 # LORA_NODE_DR = 5 # for US915 # LORA_FREQUENCY = 903900000 # LORA_GW_DR = "SF7BW125" # DR_3 # LORA_NODE_DR = 3 # for AU915 LORA_FREQUENCY = 916800000 LORA_GW_DR = "SF7BW125" LORA_NODE_DR = 4
#!/usr/bin/env python # # Copyright (c) 2019, Pycom Limited. # # This software is licensed under the GNU GPL version 3 or any # later version, with permitted additional terms. For more information # see the Pycom Licence v1.0 document supplied with this file, or # available at https://www.pycom.io/opensource/licensing # """ OTAA Node example compatible with the LoPy Nano Gateway """ from network import LoRa import socket import binascii import struct import time import config # initialize LoRa in LORAWAN mode. # Please pick the region that matches where you are using the device: # Asia = LoRa.AS923 # Australia = LoRa.AU915 # Europe = LoRa.EU868 # United States = LoRa.US915 lora = LoRa(mode=LoRa.LORAWAN, region=LoRa.AU915) # create an OTA authentication params dev_eui = config.NODE_ID app_eui = binascii.unhexlify('3C71BFFFFE8750F0') app_key = binascii.unhexlify('36AB7625FE770B6881683B495300FFD6') for channel in range(0, 16): lora.remove_channel(channel) # set the 3 default channels to the same frequency (must be before sending the OTAA join request) lora.add_channel(0, frequency=config.LORA_FREQUENCY, dr_min=0, dr_max=config.LORA_NODE_DR) lora.add_channel(1, frequency=config.LORA_FREQUENCY, dr_min=0, dr_max=config.LORA_NODE_DR) lora.add_channel(2, frequency=config.LORA_FREQUENCY, dr_min=0, dr_max=config.LORA_NODE_DR) # join a network using OTAA lora.join(activation=LoRa.OTAA, auth=(dev_eui, app_eui, app_key), timeout=0) #, dr=config.LORA_NODE_DR) # wait until the module has joined the network while not lora.has_joined(): time.sleep(2.5) print('Not joined yet...') # remove all the non-default channels for i in range(3, 16): lora.remove_channel(i) # create a LoRa socket s = socket.socket(socket.AF_LORA, socket.SOCK_RAW) # set the LoRaWAN data rate s.setsockopt(socket.SOL_LORA, socket.SO_DR, config.LORA_NODE_DR) # make the socket non-blocking s.setblocking(False) time.sleep(5.0) for i in range (200): pkt = b'PKT #' + bytes([i]) print('Sending:', pkt) s.send(pkt) time.sleep(4) rx, port = s.recvfrom(256) if rx: print('Received: {}, on port: {}'.format(rx, port)) time.sleep(6)
Startup Log
rst:0x7 (TG0WDT_SYS_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT) configsip: 0, SPIWP:0xee clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00 mode:DIO, clock div:1 load:0x3fff8028,len:8 load:0x3fff8030,len:1688 load:0x4009fa00,len:0 load:0x4009fa00,len:14592 entry 0x400a059c WMAC: 807D3A934E9C Firmware: 1.18.2.r6 Pybytes: 0.9.12 Pybytes configuration read from /flash/pybytes_config.json Initialized watchdog for WiFi and LTE connection with timeout 660000 ms WiFi connection established WARNING: consider enabling certificate validation Using MQTT over TLS Connected to MQTT mqtt.pybytes.pycom.io Pybytes connected successfully (using the built-in pybytes library) Now starting user code in main.py Not joined yet... Not joined yet... Not joined yet... Not joined yet... Not joined yet... Not joined yet... Not joined yet... Not joined yet...
Most likely I’ve don’t something wrong been new to Lora and the Lopy’s
Any help would be appreciated, thanks.
-
Thanks @jcaron ,
Got it working using ABP and a few change to the TNN config, but would still like to get it working with OTAA this was all done with the Pybytes firmware, I have now removed it on your suggestion and I'll see how it goes.Current configuration
Gateway config.py#!/usr/bin/env python # # Copyright (c) 2019, Pycom Limited. # # This software is licensed under the GNU GPL version 3 or any # later version, with permitted additional terms. For more information # see the Pycom Licence v1.0 document supplied with this file, or # available at https://www.pycom.io/opensource/licensing # """ LoPy LoRaWAN Nano Gateway configuration options """ import machine import ubinascii WIFI_MAC = ubinascii.hexlify(machine.unique_id()).upper() # Set the Gateway ID to be the first 3 bytes of MAC address + 'FFFE' + last 3 bytes of MAC address GATEWAY_ID = WIFI_MAC[:6] + "FFFE" + WIFI_MAC[6:12] SERVER = 'router.au.thethings.network' PORT = 1700 NTP = "au.pool.ntp.org" NTP_PERIOD_S = 3600 WIFI_SSID = 'ssid' WIFI_PASS = 'password' WIFI_ADDRESS = '192.168.1.50' WIFI_MASK = '255.255.255.0' WIFI_GATEWAY = '192.168.1.1' # for EU868 # LORA_FREQUENCY = 868100000 # LORA_GW_DR = "SF7BW125" # DR_5 # LORA_NODE_DR = 5 # for US915 # LORA_FREQUENCY = 903900000 # LORA_GW_DR = "SF7BW125" # DR_3 # LORA_NODE_DR = 3 # for AU915 LORA_FREQUENCY = 916800000 LORA_GW_DR = "SF7BW125" LORA_NODE_DR = 5
#!/usr/bin/env python # # Copyright (c) 2019, Pycom Limited. # # This software is licensed under the GNU GPL version 3 or any # later version, with permitted additional terms. For more information # see the Pycom Licence v1.0 document supplied with this file, or # available at https://www.pycom.io/opensource/licensing # """ LoPy LoRaWAN Nano Gateway. Can be used for both EU868 and US915. """ import errno import machine import ubinascii import ujson import uos import usocket import utime import _thread from micropython import const from network import LoRa from network import WLAN from machine import Timer 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(20) 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': '' } } 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, address, mask, gateway, server, port, ntp_server='au.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.address = address self.mask = mask self.gateway = gateway 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.wlan.ifconfig(config=(self.address, self.mask, self.gateway, self.gateway)) 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) packet = self.frequency_rounding_fix(packet, self.frequency) 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 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, 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 _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, tx_iq=True, device_class=LoRa.CLASS_C ) self.lora_sock.send(data) self._log( 'Sent downlink packet scheduled on {:.3f}, at {:.3f} Mhz using {}: {}', utime.time(), 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:]) if "tmst" in data: 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) else: self.uplink_alarm = Timer.Alarm( handler=lambda x: self._send_down_link_class_c( ubinascii.a2b_base64(tx_pk["txpk"]["data"]), tx_pk["txpk"]["datr"], int(tx_pk["txpk"]["freq"] * 1000) * 1000 ), us=50 ) 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) ))
#!/usr/bin/env python # # Copyright (c) 2019, Pycom Limited. # # This software is licensed under the GNU GPL version 3 or any # later version, with permitted additional terms. For more information # see the Pycom Licence v1.0 document supplied with this file, or # available at https://www.pycom.io/opensource/licensing # """ LoPy LoRaWAN Nano Gateway example usage """ import config from nanogateway import NanoGateway if __name__ == '__main__': nanogw = NanoGateway( id=config.GATEWAY_ID, frequency=config.LORA_FREQUENCY, datarate=config.LORA_GW_DR, ssid=config.WIFI_SSID, password=config.WIFI_PASS, address=config.WIFI_ADDRESS, mask=config.WIFI_MASK, gateway=config.WIFI_GATEWAY, server=config.SERVER, port=config.PORT, ntp_server=config.NTP, ntp_period=config.NTP_PERIOD_S ) nanogw.start() nanogw._log('You may now press ENTER to enter the REPL') input()
Connecting to COM11... [ 2.595] Starting LoRaWAN nano gateway with id: b'3C71BFFFFE8750F0' [ 6.118] WiFi connected to: ssid [ 6.123] Syncing time with au.pool.ntp.org ... [ 6.179] RTC NTP sync complete [ 6.189] Opening UDP socket to router.au.thethings.network (52.62.83.250) port 1700... [ 6.204] Setting up the LoRa radio at 916.7999 Mhz using SF7BW125 [ 6.213] LoRaWAN nano gateway online [ 6.218] You may now press ENTER to enter the REPL Pycom MicroPython 1.18.2.r7 [v1.8.6-849-df9f237] on 2019-05-14; LoPy4 with ESP32 Type "help()" for more information. >>> [ 6.249] Push ack [ 21.068] Received packet: {"rxpk": [{"data": "QLIXAiYAAAACQ9+WpQPqPkmVOg==", "time": "2019-05-31T19:49:52.173381Z", "chan": 0, "tmst": 21050429, "stat": 1, "modu": "LORA", "lsnr": 6.0, "rssi": 0, "rfch": 0, "codr": "4/5", "freq": 916.8, "datr": "SF7BW125", "size": 19}]} [ 21.129] Push ack [ 31.066] Received packet: {"rxpk": [{"data": "QLIXAiYAAQAC1T4VWxMKlrC8Aw==", "time": "2019-05-31T19:50:02.172292Z", "chan": 0, "tmst": 31049439, "stat": 1, "modu": "LORA", "lsnr": 6.0, "rssi": -1, "rfch": 0, "codr": "4/5", "freq": 916.8, "datr": "SF7BW125", "size": 19}]} [ 31.208] Push ack [ 31.252] Pull ack
ABP Node config.py
#!/usr/bin/env python # # Copyright (c) 2019, Pycom Limited. # # This software is licensed under the GNU GPL version 3 or any # later version, with permitted additional terms. For more information # see the Pycom Licence v1.0 document supplied with this file, or # available at https://www.pycom.io/opensource/licensing # """ LoPy Node configuration options """ import machine import binascii import struct WIFI_MAC = binascii.hexlify(machine.unique_id()).upper() # Set the Node ID to be the first 3 bytes of MAC address + 'FFFE' + last 3 bytes of MAC address NODE_ID = WIFI_MAC[:6] + "FFFE" + WIFI_MAC[6:12] # create an OTA authentication params DEV_EUI = binascii.unhexlify('0x') APP_EUI = binascii.unhexlify('0x') APP_KEY = binascii.unhexlify('0x') DEV_ADDR = struct.unpack(">l", binascii.unhexlify('0x'))[0] NWK_SWKEY = binascii.unhexlify('0x') APP_SWKEY = binascii.unhexlify('0x') # for EU868 # LORA_FREQUENCY = 868100000 # LORA_GW_DR = "SF7BW125" # DR_5 # LORA_NODE_DR = 5 # for US915 # LORA_FREQUENCY = 903900000 # LORA_GW_DR = "SF7BW125" # DR_3 # LORA_NODE_DR = 3 # for AU915 LORA_FREQUENCY = 916800000 LORA_GATEWAY_DR = "SF7BW125" LORA_NODE_DR = 5
#!/usr/bin/env python # # Copyright (c) 2019, Pycom Limited. # # This software is licensed under the GNU GPL version 3 or any # later version, with permitted additional terms. For more information # see the Pycom Licence v1.0 document supplied with this file, or # available at https://www.pycom.io/opensource/licensing # """ ABP Node example compatible with the LoPy Nano Gateway """ from network import LoRa import socket import binascii import struct import time import config # initialize LoRa in LORAWAN mode. # Please pick the region that matches where you are using the device: # Asia = LoRa.AS923 # Australia = LoRa.AU915 # Europe = LoRa.EU868 # United States = LoRa.US915 lora = LoRa(mode=LoRa.LORAWAN, region=LoRa.AU915) # remove all the channels for channel in range(0, 72): lora.remove_channel(channel) # # set all channels to the same frequency (must be before sending the OTAA join request) # for channel in range(0, 72): # lora.add_channel(channel, frequency=config.LORA_FREQUENCY, dr_min=0, dr_max=config.LORA_NODE_DR) # set the 3 default channels to the same frequency lora.add_channel(0, frequency=config.LORA_FREQUENCY, dr_min=0, dr_max=config.LORA_NODE_DR) lora.add_channel(1, frequency=config.LORA_FREQUENCY, dr_min=0, dr_max=config.LORA_NODE_DR) lora.add_channel(2, frequency=config.LORA_FREQUENCY, dr_min=0, dr_max=config.LORA_NODE_DR) # join a network using ABP lora.join(activation=LoRa.ABP, auth=(config.DEV_ADDR, config.NWK_SWKEY, config.APP_SWKEY)) # create a LoRa socket s = socket.socket(socket.AF_LORA, socket.SOCK_RAW) # set the LoRaWAN data rate s.setsockopt(socket.SOL_LORA, socket.SO_DR, config.LORA_NODE_DR) # make the socket non-blocking s.setblocking(False) for i in range (200): pkt = b'PKT #' + bytes([i]) print('Sending:', pkt) s.send(pkt) time.sleep(4) rx, port = s.recvfrom(256) if rx: print('Received: {}, on port: {}'.format(rx, port)) time.sleep(6)
Connecting to COM11... Sending: b'PKT #\x00' Sending: b'PKT #\x01' Sending: b'PKT #\x02'
-
@Yendor I think you have the Pybytes firmware loaded, you probably want the version without Pybytes in this case.