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.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_lock = _thread.allocate_lock() self.lora = None self.lora_sock = None def start(self): # Change WiFi to STA mode and connect self.wlan = WLAN(mode=WLAN.STA) self._connect_to_wifi() # Get a time Sync self.rtc = machine.RTC() self.rtc.ntp_sync(self.ntp, update_period=self.ntp_period) # 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 _thread.start_new_thread(self._udp_thread, ()) # Initialize LoRa 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): # TODO: Check how to stop the NTP sync # TODO: Create a cancel method for the alarm # TODO: kill the UDP thread self.sock.close() def _connect_to_wifi(self): self.wlan.connect(self.ssid, auth=(None, self.password)) while not self.wlan.isconnected(): time.sleep(0.5) 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)) # Fix the "not joined yet" issue: https://forum.pycom.io/topic/1330/lopy-lorawan-gateway-with-an-st-lorawan-device/2 self._push_data(self._make_node_packet(rx_data, self.rtc.now(), time.ticks_us(), 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 True: 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() - 5000 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"] - 10, 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)
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)
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) ))
class LoraSense: """ Class object that receives optional pin arguments for the SDA-pin (Serial Data), the SCL-pin (Serial Clock) and the pin for the photometry module. The defaults are SDA = Pin3, SCL = Pin4, Phot = Pin20 on the Pycom Extension Board v3.1. """ def __init__(self, sda="P3", scl="P4", als="P20", frequency=1, mode=0, debug=0): if not (mode == 0 or mode == 1): self.__exitError("Please initialize this module in either mode 0 or mode 1.") self.mode = mode self.rtc = RTC() self.debug = debug self.wlan = WLAN(mode=WLAN.STA) self.UDPactive = False self.loraActive = False if (mode == 0): adc = ADC() self.bme280 = bme280.BME280(i2c=I2C(pins=(sda, scl))) self.l_pin = adc.channel(pin=als) self.frequency = frequency """ This procedure sets up the LoRa-connection. """ def setupLoRa(self, mode=LoRa.LORA, region=LoRa.EU868, tx_power=14, sf=7): self.lora = LoRa(mode=mode, region=region) self.lora.init(mode=mode, tx_power=tx_power, sf=sf) self.lorasocket = socket.socket(socket.AF_LORA, socket.SOCK_RAW) self.lorasocket.setblocking(False) self.loraActive = True def setupWLAN(self, ssid, pw, timeout=60): self.wlan.connect(ssid=ssid, auth=(WLAN.WPA2, pw)) counter = 0 print("Connecting to WLAN", end = '') while not self.wlan.isconnected(): counter = counter + 1 if (counter == timeout): print("Unable to connect (timed out).") return time.sleep(1) print(".", end = '') if self.wlan.isconnected(): print(" Connected!") counter = 0 self.rtc.ntp_sync("0.ch.pool.ntp.org") print("Connecting to NTP server ", end = '') while not self.rtc.synced(): counter = counter + 1 if (counter == timeout): print("Unable to connect (timed out).") return print(".", end = '') time.sleep(1) print(" Completed!") def setupUDP(self, IP, port): self.__debug("DEBUG: STARTING UDP -> IP: {} Port: {}$".format(IP, port)) if not self.wlan.isconnected(): self.__exitError("Please establish a WLAN connection first.") if (self.mode == 0): print("UDP can only be set up in mode 1.") elif (self.mode == 1): self.UDPaddress = (IP, port) print("Establishing a UDP connection.. to {}".format(self.UDPaddress[0])) self.UDPsocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.UDPsocket.connect(self.UDPaddress) self.UDPsocket.sendto("Establish".encode("utf-8"), self.UDPaddress) self.UDPactive = True else: print("LoRa mode must be either 0 or 1.s") def setSendFreq(self, sec): self.frequency = sec def startCommunication(self): if self.mode == 0: self.__commInMode0() elif self.mode == 1: self.__commInMode1() def __createSocketList(self): if (self.loraActive and self.UDPactive): return [self.lorasocket, self.UDPsocket] elif (self.loraActive and not self.UDPactive): return [self.lorasocket] elif (not self.loraActive and self.UDPactive): return [self.UDPsocket] else: self.__exitError("Please establish a LoRa or a UDP connection before starting communication.") def __commInMode0(self): self.sockets = self.__createSocketList() self.__debug("Communication in mode 0 initiated. Sockets = {}".format(self.sockets)) while True: readIn, writeOut, excep = select.select(self.sockets, self.sockets, []) for insocket in readIn: if insocket == self.lorasocket: info = insocket.recv(52) print(info) self.__processInfo(info) elif insocket == self.UDPsocket: info = insocket.recv(52) print(info) insocket.send(info) for outsocket in writeOut: if outsocket == self.lorasocket: print(self.__getValues()) self.lorasocket.send(self.__getValues()) time.sleep(self.frequency) def __commInMode1(self): self.sockets = self.__createSocketList() self.__debug("Communication in mode 1 initiated. Sockets = {}".format(self.sockets)) while True: readIn, writeOut, excep = select.select(self.sockets, self.sockets, []) for insocket in readIn: if insocket == self.lorasocket: info = insocket.recv(52) insocket.send(info) self.__sendUDP(info) elif insocket == self.UDPsocket: info = insocket.recv(52) self.lorasocket.send(info) def __getTimeStamp(self, offset_sec=0, offset_min=0, offset_hour=0, offset_day=0, offset_month=0, offset_year=0): self.rtc.ntp_sync("0.ch.pool.ntp.org") time = self.rtc.now() seconds = self.__zfill(str(time[5] + offset_sec),2) minutes = self.__zfill(str(time[4] + offset_min),2) hour = self.__zfill(time[3] + offset_hour,2) day = time[2] + offset_day month = time[1] + offset_month year = time[0] - 2000 + offset_year return "{}/{}/{}|{}:{}:{}|".format(day, month, year, hour, minutes, seconds) def __zfill(self, s, width): return '{:0>{w}}'.format(s, w=width) def __getValues(self): t, p, h = self.bme280.values li = self.l_pin() msg = '{:.02f}|{:.02f}|{:.02f}|{:.02f}'.format(t, p, h, li / 4095 * 100) if self.wlan.isconnected(): if (self.debug == 1): print(self.__getTimeStamp(offset_hour=2) + msg) return self.__getTimeStamp(offset_hour=2) + msg else: if (self.debug == 1): print(msg) return msg def __sendLoRa(self): self.__getTimeStamp(offset_hour=2) self.lorasocket.send(self.__getValues()) def __sendInfo(self): while True: self.__sendLoRa() time.sleep(self.frequency) def __getInfo(self): try: while True: info = self.lorasocket.recv(52) if self.UDPactive: self.__sendUDP(info) if self.debug == 1: print(info) time.sleep(1) except UnicodeError: pass def __processInfo(self, info): try: info = info.decode("utf-8").split("|") if (info[0] == "freq"): self.frequency = int(info[1]) print("New frequency set: {}".format(int(info[1]))) except UnicodeError: pass def showIP(self): if self.wlan.isconnected(): print("IP: {}".format(self.wlan.ifconfig()[0])) else: print("You need to establish an internet connection first.") def __sendUDP(self, msg): self.UDPsocket.sendto(msg, self.UDPaddress) def __getUDP(self): while True: data, ip = self.UDPsocket.recvfrom(1024) if data: print("Data: " + data.decode()) def __exitError(self, str): print('\033[91m' + "Error: " + str + '\x1b[0m') sys.exit() def __debug(self, str): if self.debug == 1: print('\033[93m' + "Debug: " + str + '\x1b[0m')
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)))
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.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_lock = _thread.allocate_lock() self.lora = None self.lora_sock = None def start(self): # Change WiFi to STA mode and connect self.wlan = WLAN(mode=WLAN.STA) self._connect_to_wifi() # Get a time Sync self.rtc = machine.RTC() self.rtc.ntp_sync(self.ntp, update_period=self.ntp_period) # 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 _thread.start_new_thread(self._udp_thread, ()) # Initialize LoRa 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): # TODO: Check how to stop the NTP sync # TODO: Create a cancel method for the alarm # TODO: kill the UDP thread self.sock.close() def _connect_to_wifi(self): self.wlan.connect(self.ssid, auth=(None, self.password)) while not self.wlan.isconnected(): time.sleep(0.5) 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 True: 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() - 5000 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"] - 10, 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)
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) ))
s = socket.socket(socket.AF_LORA, socket.SOCK_RAW) s.setsockopt(socket.SOL_LORA, socket.SO_DR, 5) s.setblocking(False) s.bind(2) # try all the datarates from 0 to 6 and toggle confirmed and non confirmed messages for i in range(7): print(s.send("Sending pk #%d" % i)) time.sleep(0.5) # the packet should not have been received yet as it's non-blocking print(lora.events() == LoRa.TX_PACKET_EVENT) print(lora.events() == 0) time.sleep(2) lora.init(mode=LoRa.LORAWAN, public=True, adr=False) try: s.send('123') except Exception as e: if e.errno == errno.ENETDOWN: print('Exception') otaa_join(lora) def lora_cb_handler(lora): global s print(type(lora)) try: events = lora.events() if events & LoRa.TX_PACKET_EVENT:
] rand_index = random_integer(0, 3) return list_of_coding_rate_values[rand_index] # LoRa setup lora = LoRa(mode=LoRa.LORA, rx_iq=True) #lora.init(mode=LoRa.LORA, tx_power=14, bandwidth=LoRa.BW_125KHZ, sf=12, coding_rate=LoRa.CODING_4_5) lora_sock = socket.socket(socket.AF_LORA, socket.SOCK_RAW) lora_sock.setblocking(False) while True: lora.init(mode=LoRa.LORA, tx_power=14, bandwidth=LoRa.BW_125KHZ, sf=12, coding_rate=LoRa.CODING_4_7) tx_p = randomize_tx_power() bw = randomize_bandwith() s_f = randomize_sf() c_r = randomize_coding_rate() # time.sleep(1) """ Broadcast new parameters to nodes """ for i in range(0, 5): lora_params = str(5-i) + "|" + str(tx_p) + "|" + str(bw) + "|" + \ str(s_f) + "|" + str(c_r) # print("NEW PARAMETERS: ", lora_params)
def receive_data(): global index global guard global slot global packet_size lora = LoRa(mode=LoRa.LORA, rx_iq=True, frequency=freqs[my_sf - 5], region=LoRa.EU868, power_mode=LoRa.ALWAYS_ON, bandwidth=my_bw, sf=my_sf) lora_sock = socket.socket(socket.AF_LORA, socket.SOCK_RAW) lora_sock.setblocking(False) guard = 1000 * guard (overall_received, overall_sent) = (0, 0) airt = int(airtime_calc(my_sf, 1, packet_size + 2, my_bw_plain) * 1000) duty_cycle_limit_slots = math.ceil(100 * airt / (airt + 2 * guard)) print("duty cycle slots:", duty_cycle_limit_slots) print("packet airtime (ms):", airt / 1000) print("guard time (ms):", guard / 1000) chrono = Timer.Chrono() chrono.start() i = 1 while (True): print(i, "----------------------------------------------------") print("Net size is:", index + 1) chrono.reset() round_start = chrono.read_us() received = 0 acks = [] if (int(index) > duty_cycle_limit_slots): round_length = math.ceil(int(index) * (airt + 2 * guard)) else: round_length = math.ceil(duty_cycle_limit_slots * (airt + 2 * guard)) lora.init(mode=LoRa.LORA, rx_iq=True, region=LoRa.EU868, frequency=freqs[my_sf - 5], power_mode=LoRa.ALWAYS_ON, bandwidth=my_bw, sf=my_sf) rec_start = chrono.read_us() pycom.rgbled(green) while ((chrono.read_us() - round_start) < round_length - 66000): # the following line may take up to 66ms recv_pkg = lora_sock.recv(256) if (len(recv_pkg) > 2): recv_pkg_len = recv_pkg[1] recv_pkg_id = recv_pkg[0] if (int(recv_pkg_id) <= 35) and (int(recv_pkg_len) == int(packet_size)): dev_id, leng, msg = struct.unpack( _LORA_RCV_PKG_FORMAT % recv_pkg_len, recv_pkg) if (len(msg) == packet_size): # format check received += 1 # print('Received from: %d' % dev_id) # print(lora.stats()) acks.append(str(int(dev_id))) pycom.rgbled(off) print(received, "packets received") rec_lasted = chrono.read_us() - rec_start if (rec_lasted < round_length): print("I'll sleep a bit to align with the round length") time.sleep_us(int(round_length - rec_lasted)) print("Receiving lasted (ms):", (chrono.read_us() - rec_start) / 1000) print("...should last (ms):", round_length / 1000) proc_t = chrono.read_us() ack_msg = "" for n in range(int(index) + 1): if n in slot: id = str(slot[n]) if id in acks: ack_msg = ack_msg + "1" else: ack_msg = ack_msg + "0" if (ack_msg != ""): ack_msg = str(hex(int(ack_msg, 2)))[2:] print("proc time (ms):", (chrono.read_us() - proc_t) / 1000) proc_t = chrono.read_us() - proc_t if (i % sync_rate == 0): # SACK sync_start = chrono.read_us() pycom.rgbled(white) time.sleep_us(int(guard * 3 / 2)) # let's make it long so all the nodes are up lora.init(mode=LoRa.LORA, tx_iq=True, frequency=freqs[my_sf - 5], region=LoRa.EU868, power_mode=LoRa.ALWAYS_ON, bandwidth=my_bw, sf=my_sf, tx_power=14) data = str(index + 1) + ":" + str(int( proc_t / 1000)) + ":" + ack_msg pkg = struct.pack(_LORA_PKG_FORMAT % len(data), MY_ID, len(data), data) pycom.rgbled(red) lora_sock.send(pkg) print("Sent sync: " + data) pycom.rgbled(off) time.sleep_ms(13) # node time after sack print("sync lasted (ms):", (chrono.read_us() - sync_start) / 1000) print("round lasted (ms):", (chrono.read_us() - round_start) / 1000) i += 1