def start(): print('Started') lora = LoRa(mode=LoRa.LORAWAN, region=LoRa.AS923, device_class=LoRa.CLASS_C, adr=False) lora_otaa_join(lora) # create a LoRa socket lora_socket = socket.socket(socket.AF_LORA, socket.SOCK_RAW) # set the LoRaWAN data rate # self.lora_socket.setsockopt(socket.SOL_LORA, socket.SO_DR, self.config["lora"]["data_rate"]) #lora_socket.setsockopt(socket.SOL_LORA, socket.SO_DR, 2) # msg are confirmed at the FMS level lora_socket.setsockopt(socket.SOL_LORA, socket.SO_CONFIRMED, 0) # make the socket non blocking y default lora_socket.setblocking(False) #prepare_channels(lora, 2, 2) #How do I use this?? lora.callback(trigger=(LoRa.RX_PACKET_EVENT | LoRa.TX_PACKET_EVENT | LoRa.TX_FAILED_EVENT), handler=lora_cb)
def create_lora_abp(tx_power = 14, coding_rate = 1): assert 2 <= tx_power <= 14 assert 1 <= coding_rate <= 4 lora = LoRa(mode=LoRa.LORAWAN, region=LoRa.EU868, adr = False) lora.tx_power(tx_power) # from 2 to 14 lora.coding_rate(coding_rate) # 1 = 4/5, 2 = 4/6, 3 = 4/7, 4 = 4/8 lora.callback(trigger=LoRa.RX_PACKET_EVENT, handler=update_tx_params_cb) lora.join(activation=LoRa.ABP, auth=(dev_addr, nwk_swkey, app_swkey)) return lora
def create_lora_adr(handler, app_eui, app_key): lora = LoRa(mode=LoRa.LORAWAN, region=LoRa.EU868, adr = False) lora.callback(trigger=LoRa.RX_PACKET_EVENT, handler=handler) lora.join(activation=LoRa.OTAA, auth=(app_eui, app_key), timeout=0, dr=0) while not lora.has_joined(): time.sleep(2.5) print("Waiting for lora to join") return lora
def create_lora(handler, app_eui, app_key, tx_power = 14, coding_rate = 1): assert 2 <= tx_power <= 14 assert 1 <= coding_rate <= 4 lora = LoRa(mode=LoRa.LORAWAN, region=LoRa.EU868, adr = False) lora.tx_power(tx_power) # from 2 to 14 lora.coding_rate(coding_rate) # 1 = 4/5, 2 = 4/6, 3 = 4/7, 4 = 4/8 lora.callback(trigger=LoRa.RX_PACKET_EVENT, handler=handler) lora.join(activation=LoRa.OTAA, auth=(app_eui, app_key), timeout=0, dr=0) while not lora.has_joined(): time.sleep(2.5) print("Waiting for lora to join") return lora
def start(): print('Started') lora = LoRa(mode=LoRa.LORAWAN, region=LoRa.AS923, device_class=LoRa.CLASS_C, adr=False) lora_otaa_join(lora) # create a LoRa socket lora_socket = socket.socket(socket.AF_LORA, socket.SOCK_RAW) # set the LoRaWAN data rate # self.lora_socket.setsockopt(socket.SOL_LORA, socket.SO_DR, self.config["lora"]["data_rate"]) #lora_socket.setsockopt(socket.SOL_LORA, socket.SO_DR, 2) # msg are confirmed at the FMS level lora_socket.setsockopt(socket.SOL_LORA, socket.SO_CONFIRMED, 0) # make the socket non blocking y default lora_socket.setblocking(False) #prepare_channels(lora, 2, 2) #for channel in range(0, 15): # lora.remove_channel(channel) lora.add_channel(0, frequency=923200000, dr_min=0, dr_max=6) lora.add_channel(1, frequency=923400000, dr_min=0, dr_max=6) #lora.add_channel(2, frequency=922200000, dr_min=0, dr_max=6) #lora.add_channel(3, frequency=922400000, dr_min=0, dr_max=6) #lora.add_channel(4, frequency=922600000, dr_min=0, dr_max=6) #lora.add_channel(5, frequency=922800000, dr_min=0, dr_max=6) #lora.add_channel(6, frequency=922000000, dr_min=0, dr_max=6) lora.callback(trigger=(LoRa.RX_PACKET_EVENT | LoRa.TX_PACKET_EVENT | LoRa.TX_FAILED_EVENT), handler=lora_cb) lora_socket.send(bytes([0x01, 0x02, 0x03])) while (True): time.sleep(5) print(".")
print(' event: ', event) if event & LoRa.RX_PACKET_EVENT: print('lora packed received') recv_ack = lora_sock.recv(256) if len(recv_ack) > 0: device_id, pkg_len, ack = struct.unpack(_LORA_PKG_ACK_FORMAT, recv_ack) if device_id == DEVICE_ID: if ack == 200: print("ACK") else: print("Message Failed", ack) elif event & LoRa.TX_PACKET_EVENT: print('lora packet sent - we should never see this message') lora.callback(trigger=LoRa.RX_PACKET_EVENT, handler=handle_ack) print('receive handler connected') while True: # Package send containing a simple string _input = input('color?') values = _input.split(' ') msg = values[0] if len(values) > 1: try: DEVICE_ID = int(values[1]) print('changing DEVICE_ID to', DEVICE_ID) except: pass print('couleur: (%d) %r' % (len(msg), msg))
received_object = cbor.loads(received) led_color_value = received_object.get('led-color', None) led_value = received_object.get('led', None) if led_color_value: pycom.rgbled(int(led_color_value)) if led_value: if led_value == False: pycom.rgbled(0x000000) elif events & LoRa.TX_PACKET_EVENT: print('LoRa packet sent') lora.callback(LoRa.RX_PACKET_EVENT | LoRa.TX_PACKET_EVENT, lora_callback) print('creating the i2c component') pwm = PWM(0, frequency=5000) # use PWM timer 0, with a frequency of 5KHz # create pwm channel on pin P12 with a duty cycle of 50% pwm_c = pwm.channel(0, pin='P22', duty_cycle=1.0) i2c = I2C(0, I2C.MASTER) # create and init as a master sensor0 = drivers.adafruit_sht31d.SHT31D( i2c, address=drivers.adafruit_sht31d._SHT31_ADDRESSES[0]) #sensor1 = drivers.adafruit_sht31d.SHT31D(i2c, address=drivers.adafruit_sht31d._SHT31_ADDRESSES[1]) print('i2c component created') while True: print('i2c component reading') t0 = sensor0.temperature
# Init LoRa and socket lora = LoRa(mode=LoRa.LORA, tx_power = 2, sf = 7, bandwidth=LoRa.BW_500KHZ, coding_rate=LoRa.CODING_4_8, preamble=8, frequency=868000000) s = socket.socket(socket.AF_LORA, socket.SOCK_RAW) s.setblocking(False) # Mount SD card and open a file on it time.sleep(2) sd = SD() os.mount(sd, '/sd') f = open('/sd/gps-record.txt', 'w') f.write('LoRa test with: tx_power = 2, sf = 7, bandwidth=LoRa.BW_500KHZ, coding_rate=LoRa.CODING_4_8, preamble=8, frequency=868000000\n') # Send first packet to init comm on the other side s.send('Ping') print('Send Ping') # Init RGB Led and global variables pycom.heartbeat(False) pycom.rgbled(0x7f0000) toggle = True fileOpen = True # Init button for closing file purpose p_in = Pin('P10', mode=Pin.IN, pull = Pin.PULL_UP) # Call lora_cb function when a packet is received lora.callback(trigger = LoRa.RX_PACKET_EVENT, handler = lora_cb) # Call pin_handler function when button is pressed p_in.callback(Pin.IRQ_FALLING, pin_handler)
def main(): pycom.heartbeat(False) # Sensor initializations with default params # bus = 1 # sda = P10 # scl = P11 # baud = 20000 # interval = 10 pm_sensor = sps30() if using_pm else None co2_sensor = scd30() if using_co2 else None # Start sensors in a separate thread pm_thread = _thread.start_new_thread(pm_sensor.start, ()) if using_pm else None co2_thread = _thread.start_new_thread(co2_sensor.start, ()) if using_co2 else None # Prepare LoRa channels lora = LoRa(mode=LoRa.LORAWAN, region=LoRa.US915, device_class=LoRa.CLASS_C) prepare_channels(lora, LORA_FREQUENCY) #lora = LoRa(mode=LoRa.LORA, region=LoRa.US915, frequency=904600000, bandwidth=LoRa.BW_500KHZ, sf=8) # Join LoRa network with OTAA lora.join(activation=LoRa.OTAA, auth=(dev_eui, app_key, nwk_key), timeout=0, dr=0) # wait until the module has joined the network print('Over the air network activation ... ', end='') while not lora.has_joined(): time.sleep(2.5) print('.', end='') print('') # Socket initializations lora_socket = socket.socket(socket.AF_LORA, socket.SOCK_RAW) lora_socket.setsockopt(socket.SOL_LORA, socket.SO_DR, LORA_DR) # msg are confirmed at the FMS level lora_socket.setsockopt(socket.SOL_LORA, socket.SO_CONFIRMED, False) # make the socket non blocking by default lora_socket.setblocking(False) lora.callback(trigger=(LoRa.RX_PACKET_EVENT | LoRa.TX_PACKET_EVENT | LoRa.TX_FAILED_EVENT), handler=lora_cb) time.sleep(4) # this timer is important and caused me some trouble ... # Send pm (payload=40bytes) and co2 (payload=12bytes) data every 5 minutes while True: # Poll data if using_pm: pm_data = pm_sensor.get_packed_msg() if using_co2: co2_data = co2_sensor.get_packed_msg() if using_pysense_sensor: py = Pysense() mp = MPL3115A2( py, mode=ALTITUDE ) # Returns height in meters. Mode may also be set to PRESSURE, returning a value in Pascals si = SI7006A20(py) lt = LTR329ALS01(py) li = LIS2HH12(py) # Send data if using_pm: send_pkt(lora_socket, pm_data, 8) if using_co2: send_pkt(lora_socket, co2_data, 9) if using_pysense_sensor: temp = si.temperature() rh = si.humidity() rlux = lt.light()[0] blux = lt.light()[1] pysense_pkt = struct.pack('<ffii', temp, rh, rlux, blux) send_pkt(lora_socket, pysense_pkt, 10) time.sleep(15 - using_pm * 5 - using_co2 * 5 - using_pysense_sensor * 5) # Stop polling and end threads if using_pm: pm_sensor.stop() if using_co2: co2_sensor.stop()
app_key = binascii.unhexlify('') # removed before uploading it to github g_tx_power = 14 g_data_rate = 0 # sf = 12 g_coding_rate = 1 g_lambda_ = 1 g_pkt_length = 10 paq_bytes = None g_counter = -1 g_keep_alive_counter = 0 lora = LoRa(mode=LoRa.LORAWAN, region=LoRa.EU868, adr=False) lora.nvram_restore() if lora.has_joined(): print('Joining not required') lora.callback(trigger=LoRa.RX_PACKET_EVENT, handler=packet_received_hd) lora.nvram_save() else: print('Joining for the first time') lora = create_lora_adr(handler=packet_received_hd, app_eui=app_eui, app_key=app_key) lora.nvram_save() s = create_socket_adr() while True: set_tx_power(lora, tx_power=14) s.send(bytes([0x01, 0x02, 0x03, 0x04, 0x05])) time.sleep(3)
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) ))
if (events & LoRa.RX_PACKET_EVENT): messageReceived = True socketLora.setblocking(True) Log.i("LoRa.RX_PACKET_EVENT") Led.blink_blue() data = socketLora.recv(256) socketLora.setblocking(True) _callback(data) # if(events & LoRa.TX_PACKET_EVENT): # Log.i("LoRa.TX_PACKET_EVENT") if (events & LoRa.TX_FAILED_EVENT): Log.i("LoRa.TX_FAILED_EVENT") _join() lora.callback(trigger=(LoRa.RX_PACKET_EVENT | LoRa.TX_FAILED_EVENT), handler=_lora_callback) def _join(): global lopy_connected if not lora.has_joined(): Log.i("Connecting Lora...") lopy_connected = False Led.blink_yellow() lora.join(activation=LoRa.OTAA, auth=(app_eui, app_key), timeout=0) while not lora.has_joined(): time.sleep(2.5) lopy_connected = True Led.blink_green() Log.i("Connected")
class Lora_Link_Layer: def __init__(self, receive_msg_cb): print("Initializing Link Layer...") self.receive_msg_cb = receive_msg_cb self.msg_buffer_list = [] self.incoming_cts_key = -1 self.wait = False #Use other name, e.g. wait_for_channel self.wait_time = 0 #use int instead of boolean. If multiple cts/rts arrive sum. self.lora = LoRa(mode=LoRa.LORA, region=LoRa.EU868) self.s = socket.socket(socket.AF_LORA, socket.SOCK_RAW) self.s.setblocking(False) self.lora.callback(trigger=(LoRa.RX_PACKET_EVENT | LoRa.TX_PACKET_EVENT), handler=self.lora_cb) self.lora_mac_address_b = binascii.hexlify(self.lora.mac()) self.lora_mac_address = str(self.lora_mac_address_b, "utf-8") #_thread.start_new_thread(self.process_msg_pipeline, ()) def get_lora_mac(self): return self.lora_mac_address def lora_cb(self, lora): events = lora.events() if events & LoRa.RX_PACKET_EVENT: msg = self.s.recv(250) #print(msg) #unpack message and pass meta information (like mac address or protocol) as parameter #meta, data = self.unpack_frame(msg.decode("utf-8")) #msg_utf8 = data #msg_1 = msg.decode("utf-8") #print('Link Layer: Lora packet received. ' + str(msg_utf8, "utf-8")) #if (msg_1.startswith("cts")): # self.handle_incoming_cts(msg_1) #elif (msg_1.startswith("rts")): # self.handle_incoming_rts(msg_1) #send cts. what if cts gets lost? Ignore? CSMA sending messages #only one rts at a time. identify rts if it is repeated. #else: print('Link Layer: Passing received data to Network Layer') self.receive_msg_cb(msg) #if events & LoRa.TX_PACKET_EVENT: #print('Lora packet sent') def append_msg_to_pipeline(self, msg, use_ca): #self.msg_buffer_list.append([msg, use_ca]) self.lora_send_csma_ca(msg, use_ca) #Buffer Länge definieren und Nachrichten verwerfen oder auf Teilpackete aufteilen. #Time on air -> vielfaches (zufällig) warten, falls Kanal nicht frei. def process_msg_pipeline(self): while True: if (len(self.msg_buffer_list) > 0): msg_and_ca = self.msg_buffer_list.pop(0) msg = msg_and_ca[0] use_ca = msg_and_ca[1] self.lora_send_csma_ca(msg, use_ca) time.sleep(1) def lora_send_csma_ca(self, msg, use_ca): if (use_ca): print("Link Layer: using CA") #do not send rts if wait = true rts_random_key_b = binascii.hexlify(os.urandom(2)) rts_random_key = str(rts_random_key_b, "utf-8") rts = "rts." + rts_random_key while not (rts_random_key == self.incoming_cts_key): #and not wait. Probably? #maximum repetition if not self.wait: self.lora_send_csma(rts) print("Link Layer: Waiting for cts. expected: " + str(rts_random_key) + " received: " + str(self.incoming_cts_key)) else: print("Link Layer: Waiting..." + str(self.wait_time)) #change delay randomly/exponential #Wie lange warten? cts soll nicht mit nächstem rts versuch überlagert werden? time.sleep(2) else: print("Link Layer: NOT using CA") #blocking function self.lora_send_csma(msg) def lora_send_csma(self, msg): #Semtech LoRa: High sensitivity -111 to -148dBm (Datasheet: https://semtech.my.salesforce.com/sfc/p/#E0000000JelG/a/2R0000001OKs/Bs97dmPXeatnbdoJNVMIDaKDlQz8q1N_gxDcgqi7g2o) while not self.lora.ischannel_free(-100,100): #max rep. print("Link Layer: channel not free") print("Link Layer: channel free (CSMA). Sending data...") self.lora_send(msg) def lora_send(self, msg): #frame = self.pack_frame("c", msg) frame = msg print("Link Layer | Sending data: " + str(frame)) self.s.send(frame) def handle_incoming_cts(self, incoming_cts): print("Link Layer: CTS received. Key=" + incoming_cts) self.incoming_cts_key = str(incoming_cts.split(".")[1], "utf-8") #Important: Wait also if CTS is for other lora. Use MAC adress as identifier #Combine with incoming RTS def handle_incoming_rts(self, incoming_rts): incoming_rts_key = str(incoming_rts.split(".")[1], "utf-8") print("Link Layer: RTS received. Key=" + incoming_rts_key) #send cts. what if cts gets lost? Ignore? CSMA sending messages #only one rts at a time. identify rts if it is repeated. #check if rts is new. Problem: other lora did not receive cts. Important: Waiting time if (not self.wait): _thread.start_new_thread(self.wait_timer, (5,)) print("Link Layer: CTS other lora. Waiting for other lora...") #save mac address of other lora and wait until packet from this lora arrived or max cts = "cts." + incoming_rts_key #self.lora_send_csma(cts.encode("utf-8")) self.lora_send_csma(cts) def wait_timer(self, wait_time): self.wait_time = wait_time self.wait = True print("Wait timer") while self.wait_time > 0: time.sleep(1) self.wait_time = self.wait_time - 1 print(str(self.wait_time)) self.wait = False #This field states whether the frame is a data frame or it is used for control functions like error and flow control or link management etc. def pack_frame(self, type, data): #Use single bits for control and 8 bytes for MAC without delimiters. character delimiters can cause problems and need much space. frame = type + "::" + self.get_lora_mac() + "::" + data print("Link Layer:" + frame) return frame def unpack_frame(self, frame): meta = [frame.split("::")[0], frame.split("::")[1]] data = frame.split("::")[2] return meta, data
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: """ 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. """ PROTOCOL_VERSION = const(2) PUSH_DATA = const(0) PUSH_ACK = const(1) PULL_DATA = const(2) PULL_ACK = const(4) PULL_RESP = const(3) TX_ERR_NONE = 'NONE' TX_ERR_TOO_LATE = 'TOO_LATE' TX_ERR_TOO_EARLY = 'TOO_EARLY' TX_ERR_COLLISION_PACKET = 'COLLISION_PACKET' TX_ERR_COLLISION_BEACON = 'COLLISION_BEACON' TX_ERR_TX_FREQ = 'TX_FREQ' TX_ERR_TX_POWER = 'TX_POWER' TX_ERR_GPS_UNLOCKED = 'GPS_UNLOCKED' UDP_THREAD_CYCLE_MS = const(10) STAT_PK = { 'stat': { 'time': '', 'lati': 0, 'long': 0, 'alti': 0, 'rxnb': 0, 'rxok': 0, 'rxfw': 0, 'ackr': 100.0, 'dwnb': 0, 'txnb': 0 } } RX_PK = { 'rxpk': [{ 'time': '', 'tmst': 0, 'chan': 0, 'rfch': 0, 'freq': 0, 'stat': 1, 'modu': 'LORA', 'datr': '', 'codr': '4/5', 'rssi': 0, 'lsnr': 0, 'size': 0, 'data': '' }] } TX_ACK_PK = {'txpk_ack': {'error': ''}} def __init__(self, wifi_ssid, wifi_password, gateway_id=None, server='router.eu.thethings.network', port=1700, frequency=868100000, datarate='SF7BW125', ntp_server='pool.ntp.org', ntp_period=3600): # If unset, set the Gateway ID to be the first 3 bytes # of MAC address + 'FFFE' + last 3 bytes of MAC address if gateway_id is None: gateway_id = ubinascii.hexlify(machine.unique_id()).upper() gateway_id = gateway_id[:6] + 'FFFE' + gateway_id[6:12] self.gateway_id = gateway_id self.server = server self.port = port self.frequency = frequency self.datarate = datarate self.wifi_ssid = wifi_ssid self.wifi_password = wifi_password self.ntp_server = ntp_server self.ntp_period = ntp_period self.server_ip = None self.rxnb = 0 self.rxok = 0 self.rxfw = 0 self.dwnb = 0 self.txnb = 0 self.sf = self._dr_to_sf(self.datarate) self.bw = self._dr_to_bw(self.datarate) self.stat_alarm = None self.pull_alarm = None self.uplink_alarm = None self.wlan = None self.sock = None self.udp_stop = False self.udp_lock = _thread.allocate_lock() self.lora = None self.lora_sock = None self.rtc = machine.RTC() def start(self): """ Starts the nano gateway. """ self.log('Starting nano gateway with id {}', self.gateway_id) # Change WiFi to STA mode and connect self.wlan = WLAN(mode=WLAN.STA) self._connect_to_wifi() # Get a time sync self.log('Syncing time with {} ...', self.ntp_server) self.rtc.ntp_sync(self.ntp_server, update_period=self.ntp_period) while not self.rtc.synced(): utime.sleep_ms(50) self.log('RTC NTP sync complete') # Get the server IP and create an UDP socket self.server_ip = usocket.getaddrinfo(self.server, self.port)[0][-1] self.log('Opening UDP socket to {} ({}) port {}...', self.server, self.server_ip[0], self.server_ip[1]) self.sock = usocket.socket(usocket.AF_INET, usocket.SOCK_DGRAM, usocket.IPPROTO_UDP) self.sock.setsockopt(usocket.SOL_SOCKET, usocket.SO_REUSEADDR, 1) self.sock.setblocking(False) # Push the first time stat immediately self._push_data(self._make_stat_packet()) # Create the alarms self.stat_alarm = machine.Timer.Alarm( handler=lambda t: self._push_data(self._make_stat_packet()), s=60, periodic=True) self.pull_alarm = machine.Timer.Alarm( handler=lambda u: self._pull_data(), s=25, periodic=True) # Start the UDP receive thread self.udp_stop = False _thread.start_new_thread(self._udp_thread, ()) # Initialize the LoRa radio in LORA mode self.log('Setting up LoRa socket on {:.1f} Mhz using {}', self._freq_to_float(self.frequency), self.datarate) self.lora = LoRa(mode=LoRa.LORA, frequency=self.frequency, bandwidth=self.bw, sf=self.sf, preamble=8, coding_rate=LoRa.CODING_4_5, tx_iq=True) # Create a raw LoRa socket self.lora_sock = usocket.socket(usocket.AF_LORA, usocket.SOCK_RAW) self.lora_sock.setblocking(False) self.lora.callback(trigger=(LoRa.RX_PACKET_EVENT | LoRa.TX_PACKET_EVENT), handler=self._lora_cb) self.log('Nano gateway online') def stop(self): """ Stops the nano gateway. """ self.log('Stopping...') # Send the LoRa radio to sleep self.lora.callback(trigger=None, handler=None) self.lora.power_mode(LoRa.SLEEP) # Stop the NTP sync self.rtc.ntp_sync(None) # Cancel all the alarms self.stat_alarm.cancel() self.pull_alarm.cancel() # Signal the UDP thread to stop self.udp_stop = True while self.udp_stop: utime.sleep_ms(50) # Disable WLAN self.wlan.disconnect() self.wlan.deinit() def _connect_to_wifi(self): self.wlan.connect(self.wifi_ssid, auth=(None, self.wifi_password)) while not self.wlan.isconnected(): utime.sleep_ms(50) self.log('WiFi connected: {}', self.wifi_ssid) def _dr_to_sf(self, dr): sf = dr[2:4] if sf[1] not in '0123456789': sf = sf[:1] return int(sf) def _dr_to_bw(self, dr): bw = dr[-5:] if bw == 'BW125': return LoRa.BW_125KHZ elif bw == 'BW250': return LoRa.BW_250KHZ else: return LoRa.BW_500KHZ def _sf_bw_to_dr(self, sf, bw): dr = 'SF' + str(sf) if bw == LoRa.BW_125KHZ: return dr + 'BW125' elif bw == LoRa.BW_250KHZ: return dr + 'BW250' else: return dr + 'BW500' def _lora_cb(self, lora): """ Event listener for LoRa radio events. """ events = lora.events() if events & LoRa.RX_PACKET_EVENT: self.rxnb += 1 self.rxok += 1 rx_data = self.lora_sock.recv(256) stats = lora.stats() packet = self._make_node_packet(rx_data, self.rtc.now(), stats.rx_timestamp, stats.sfrx, self.bw, stats.rssi, stats.snr) self.log('Received packet: {}', packet) self._push_data(packet) self.rxfw += 1 if events & LoRa.TX_PACKET_EVENT: self.log('Re-initing LoRa radio after transmission') self.txnb += 1 lora.init(mode=LoRa.LORA, frequency=self.frequency, bandwidth=self.bw, sf=self.sf, preamble=8, coding_rate=LoRa.CODING_4_5, tx_iq=True) def _freq_to_float(self, frequency): """ MicroPython has some inprecision when doing large float division. To counter this, this method first does integer division until we reach the decimal breaking point. This doesn't completely elimate the issue in all cases, but it does help for a number of commonly used frequencies. """ divider = 6 while divider > 0 and frequency % 10 == 0: frequency = frequency // 10 divider -= 1 if divider > 0: frequency = frequency / (10**divider) return frequency def _make_stat_packet(self): now = self.rtc.now() self.STAT_PK['stat']['time'] = '%d-%02d-%02d %02d:%02d:%02d GMT' % ( now[0], now[1], now[2], now[3], now[4], now[5]) self.STAT_PK['stat']['rxnb'] = self.rxnb self.STAT_PK['stat']['rxok'] = self.rxok self.STAT_PK['stat']['rxfw'] = self.rxfw self.STAT_PK['stat']['dwnb'] = self.dwnb self.STAT_PK['stat']['txnb'] = self.txnb return ujson.dumps(self.STAT_PK) def _make_node_packet(self, rx_data, rx_time, tmst, sf, bw, rssi, snr): self.RX_PK['rxpk'][0]['time'] = '%d-%02d-%02dT%02d:%02d:%02d.%dZ' % ( rx_time[0], rx_time[1], rx_time[2], rx_time[3], rx_time[4], rx_time[5], rx_time[6]) self.RX_PK['rxpk'][0]['tmst'] = tmst self.RX_PK['rxpk'][0]['freq'] = self._freq_to_float(self.frequency) self.RX_PK['rxpk'][0]['datr'] = self._sf_bw_to_dr(sf, bw) self.RX_PK['rxpk'][0]['rssi'] = rssi self.RX_PK['rxpk'][0]['lsnr'] = float(snr) self.RX_PK['rxpk'][0]['data'] = ubinascii.b2a_base64(rx_data)[:-1] self.RX_PK['rxpk'][0]['size'] = len(rx_data) return ujson.dumps(self.RX_PK) def _push_data(self, data): token = uos.urandom(2) packet = bytes([self.PROTOCOL_VERSION]) + token + bytes( [self.PUSH_DATA]) + ubinascii.unhexlify(self.gateway_id) + data with self.udp_lock: try: self.sock.sendto(packet, self.server_ip) except BaseException as ex: self.log('Failed to push uplink packet to server: {}', ex) def _pull_data(self): token = uos.urandom(2) packet = bytes([self.PROTOCOL_VERSION]) + token + bytes( [self.PULL_DATA]) + ubinascii.unhexlify(self.gateway_id) with self.udp_lock: try: self.sock.sendto(packet, self.server_ip) except BaseException as ex: self.log('Failed to pull downlink packets from server: {}', ex) def _ack_pull_rsp(self, token, error): self.TX_ACK_PK['txpk_ack']['error'] = error resp = ujson.dumps(self.TX_ACK_PK) packet = bytes([self.PROTOCOL_VERSION]) + token + bytes( [self.PULL_ACK]) + ubinascii.unhexlify(self.gateway_id) + resp with self.udp_lock: try: self.sock.sendto(packet, self.server_ip) except BaseException as ex: self.log('PULL RSP ACK exception: {}', ex) def _send_down_link(self, data, tmst, datarate, frequency): """ Transmits a downlink message over LoRa. """ self.lora.init(mode=LoRa.LORA, frequency=frequency, bandwidth=self._dr_to_bw(datarate), sf=self._dr_to_sf(datarate), preamble=8, coding_rate=LoRa.CODING_4_5, tx_iq=True) while utime.ticks_us() < tmst: pass self.lora_sock.send(data) self.log( 'Sent downlink packet scheduled for {:.3f}, at {:.1f} Mhz using {}: {}', tmst / 1000000, self._freq_to_float(frequency), datarate, data) def _udp_thread(self): """ UDP thread, reads data from the server and handles it. """ while not self.udp_stop: try: data, src = self.sock.recvfrom(1024) _token = data[1:3] _type = data[3] if _type == self.PUSH_ACK: self.log('Push ack') elif _type == self.PULL_ACK: self.log('Pull ack') elif _type == self.PULL_RESP: self.dwnb += 1 ack_error = self.TX_ERR_NONE tx_pk = ujson.loads(data[4:]) tmst = tx_pk['txpk']['tmst'] t_us = tmst - utime.ticks_us() - 12500 if t_us < 0: t_us += 0xFFFFFFFF if t_us < 20000000: self.uplink_alarm = machine.Timer.Alarm( handler=lambda x: self._send_down_link( ubinascii.a2b_base64(tx_pk['txpk']['data']), tx_pk['txpk']['tmst'] - 50, tx_pk['txpk'][ 'datr'], int(tx_pk['txpk']['freq'] * 1000000)), us=t_us) else: ack_error = self.TX_ERR_TOO_LATE self.log('Downlink timestamp error!, t_us: {}', t_us) self._ack_pull_rsp(_token, ack_error) self.log('Pull rsp') else: self.log('Unknown message type from server: {}', _type) except usocket.timeout: pass except OSError as ex: if ex.errno != errno.EAGAIN: self.log('UDP recv OSError Exception: {}', ex) except BaseException as ex: self.log('UDP recv Exception: {}', ex) # Wait before trying to receive again utime.sleep_ms(self.UDP_THREAD_CYCLE_MS) self.sock.close() self.udp_stop = False self.log('UDP thread stopped') def log(self, message, *args): """ Prints a log message to the stdout. """ print('[{:>10.3f}] {}'.format(utime.ticks_ms() / 1000, str(message).format(*args)))
class LoraCoverage: def __init__(self, host, port, ssid, wpa, log_path): self.host = host self.port = port self.ssid = ssid self.wpa = wpa self.path = log_path self.gps_buffer = None self.log_time = None self.log_file = None self.rxnb = 0 self.loramac = binascii.hexlify(network.LoRa().mac()) # to be refactored self.end = False self.lock = _thread.allocate_lock() # Create proper directory for log file self._init_log() # Setup wypy/lopy as a station self.wlan = network.WLAN(mode=network.WLAN.STA) self.wlan.connect(self.ssid, auth=(network.WLAN.WPA2, self.wpa)) while not self.wlan.isconnected(): print('Connecting to Android WIFI HOTPOST...') time.sleep(1) print('Connected to Android WIFI HOTPOST') # Lora socket self.lora = None self.lora_sock = None # TCP socket self.tcp_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # Connect self.tcp_sock.connect((self.host, self.port)) def _init_log(self): # create directory log if not exist try: os.mkdir(self.path) except OSError: pass # Check if string is empty def _isNotBlank(self, myString): return bool(myString and myString.strip()) def _get_log_name(self): #print(self.log_time[:19]) #print(type(ts)) # ts = ts.replace('-', '') # ts = ts.replace('T', '') # ts = ts.replace(':', '') #ts = '1974' #base = self.path + '/acq/' #return base + ts + 'list.csv' return self.path + '/acq.list.csv' def _lora_cb(self, lora): events = lora.events() if (events & LoRa.RX_PACKET_EVENT) and (self.gps_buffer is not None): #if self.log_file is None: # self.log_file = open(self._get_log_name(), 'w') data_rx = self.lora_sock.recv(256) stats = self.lora.stats() # get lora stats (data is tuple) if self._isNotBlank(data_rx): with self.lock: print(self.log_time) print(self.gps_buffer) #print(self.gps_buffer['time']) # time_stamp = stats[0] # get timestamp value # rssi = stats[1] # snr = stats[2] # sf = stats[3] # # msgData = '' # msgCrc = '' # if len(data_rx) >= 5: # msg_data = data_rx[:-5] # remove the last 5 char data crc # msg_crc = data_rx[-4:] # get the last 4 char # # # Calculate CRC # crc8 = utils.crc(msg_data) # # # Check CRC # crc_ok = True if crc8 == msg_crc.decode('utf-8') else False # # crc_ok = False # # if crc8 == msg_crc.decode('utf-8'): # # crc_ok = True # # # fields_lorastats(LoraStats) # # # form csv row # msg = str(self.rxnb) # msg = msg + ',' + self.loramac.decode('utf8') # msg = msg + ',' + self.gps_buffer['time'] # msg = msg + ',' + str(self.gps_buffer.lat) # msg = msg + ',' + str(self.gps_buffer.lon) # msg = msg + ',' + str(self.gps_buffer.alt) # # msg = msg + ',' + msg_data.decode('utf-8') # msg = msg + ',' + msg_crc.decode('utf-8') # msg = msg + ',' + str(crc8) # msg = msg + ',' + str(crc_ok) # msg = msg + ',' + str(time_stamp) # msg = msg + ',' + str(rssi) # msg = msg + ',' + str(snr) # msg = msg + ',' + str(sf) # # # # calc crc8 row # #crc8row = calc(msg.encode('utf-8')) # crc8row = utils.crc(msg.encode('utf-8')) # # # # add crc8row as last item # msg = msg + ',' + str(crc8row) # # # write csv and terminal # self.log_file.write(msg) # self.log_file.write('\n') # self.log_file.flush() # # print(msg) # show in repl # self.rxnb += 1 def start(self): # Initialize a LoRa sockect self.lora = LoRa(mode=LoRa.LORA) self.lora_sock = socket.socket(socket.AF_LORA, socket.SOCK_RAW) self.lora_sock.setblocking(False) #self.tcp_alarm = Timer.Alarm(handler=lambda u: self._tcp_gps(), s=1, periodic=True) self.lora.callback(trigger=LoRa.RX_PACKET_EVENT, handler=self._lora_cb) # Start the TCP thread receiving GPS coordinates _thread.start_new_thread(self._tcp_thread, ()) def stop(self): self.wlan.mode(network.WLAN.AP) self.tcp_sock.close() self.lora.callback(trigger=LoRa.RX_PACKET_EVENT, handler=None) self.lora_sock.close() #self.log_file.close() # Set end flag to terminate TCP thread self.end = True def _tcp_thread(self): while True: if self.end: _thread.exit() try: # Send request self.tcp_sock.send(b'?SHGPS.LOCATION;\r\n') # Get response rx_data = self.tcp_sock.recv(4096) # Release the lock in case of previous TCP error #self.lock.release() # Save last gps received to buffer with self.lock: self.gps_buffer = json.loads(rx_data.decode('ascii')) if not self.log_time: self.log_time = self.gps_buffer['time'] except socket.timeout: self.lock.locked() except OSError as e: if e.errno == errno.EAGAIN: pass else: self.lock.locked() print( "Error: Android 'ShareGPS' connection status should be 'Listening'" ) print('Change mode and soft reboot Pycom device') except Exception: self.lock.locked() print("TCP recv Exception") time.sleep(0.5)
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 Zapette: def __init__(self, device_id, handler_rx=None, handler_tx=None): #.addHandler(ZapetteHandler("self"),2) l.debug("loraBlk init ...") self.device_id = bytes([device_id]) self._user_handler_rx = handler_rx self._user_handler_tx = handler_tx n = uos.urandom(2) self.frame_cnt = n[0] + n[1] * 256 self.lpp = zapetteLPP.LPP() # initialise LoRa in LORA mode self.lora = LoRa( mode=LoRa.LORA, region=LoRa.EU868, sf=12, tx_power=20, public=False, preamble=8, #default bandwidth=LoRa.BW_125KHZ, coding_rate=LoRa.CODING_4_8) # create a raw LoRa socket self.lora_sock = socket.socket(socket.AF_LORA, socket.SOCK_RAW) self.lora_sock.setsockopt(socket.SOL_LORA, socket.SO_CONFIRMED, True) # selecting confirmed type of messages self.lora.callback(trigger=(LoRa.RX_PACKET_EVENT | LoRa.TX_PACKET_EVENT), handler=self._lora_cb) self.frame_TX = [] self.frame_ACK = None self.frame_ACK_stats = None self.tx_timeout = 4000 self.tx_retries = 3 def _lora_cb(self, lora): """ LoRa radio events callback handler. """ global RX, TX, lpp events = lora.events() stats = lora.stats() l.debug("event: {}".format(events)) l.debug("stats: {}".format(stats)) if events & LoRa.RX_PACKET_EVENT: rx_frame = self.lora_sock.recv(256) l.debug("rx : {}".format(ubinascii.hexlify(rx_frame))) #header device_from = rx_frame[0:1] device_to = rx_frame[1:2] frame_type = rx_frame[2:3] frame_cnt = rx_frame[3:5] payload = rx_frame[5:-1] crc = rx_frame[-1] l.debug( "device_from:{} ,device_to:{} ,frame_type:{}, frame_cnt:{}, crc:{}, payload:{}" .format(device_from, device_to, frame_type, frame_cnt, crc, payload)) if device_to == self.device_id: if frame_type == b'\x00': #RX w/ ACK l.debug("FRAME RX w/ ACK") self._send_frame_ACK(device_from, frame_cnt, stats, b'\x01') if self._user_handler_rx is not None: lpp_rx = self.lpp.decode(payload) if lpp_rx != None: self._user_handler_rx(lpp_rx, { 'rssi': stats.rssi, 'snr': stats.snr }) else: l.debug("no Handler") elif frame_type == b'\x01': #RX w/o ACK l.debug("FRAME RX w/o ACK") if self._user_handler_rx is not None: lpp_rx = self.lpp.decode(payload) if lpp_rx != None: self._user_handler_rx(lpp_rx, { 'rssi': stats.rssi, 'snr': stats.snr }) else: l.debug("no Handler") elif frame_type == b'\x10': #ACK l.debug("FRAME ACK") #tmp = struct.unpack("bhf",payload) Bug status = payload[0] rssi = struct.unpack("h", payload[1:3])[0] snr = struct.unpack("f", payload[3:])[0] self.frame_ACK = frame_cnt self.frame_ACK_stats = {'rssi': rssi, 'snr': snr} if self._user_handler_rx is not None: self._user_handler_rx(None, { 'rssi': stats.rssi, 'snr': stats.snr }) else: l.debug("messages for: {}".format(device_to)) elif events & LoRa.TX_PACKET_EVENT: l.debug("FRAME send") else: l.debug("???") def _send_frame_ACK(self, device_to, frame_cnt, stats, status): device_from = self.device_id device_to = device_to frame_type = b'\x10' frame_cnt = frame_cnt rssi = stats.rssi snr = stats.snr payload = status + struct.pack("h", rssi) + struct.pack("f", snr) crc = b'\x00' tx_frame = self.device_id + device_to + frame_type + frame_cnt + payload + crc l.debug("tx : {}".format(ubinascii.hexlify(tx_frame))) self.lora_sock.send(tx_frame) def send_frame_TX(self, device_to, msg, ack=True): device_from = self.device_id device_to = bytes([device_to]) frame_type = b'\x00' if ack == True else b'\x01' frame_cnt = b'\x00\x01' payload = msg.get_buffer() crc = b'\x00' msg.reset() tx_frame = self.device_id + device_to + frame_type + frame_cnt + payload + crc l.debug("tx: {}, ack: {}".format(ubinascii.hexlify(tx_frame), ack)) if ack == True: self.frame_ACK = None for i in range(0, self.tx_retries): self.lora_sock.send(tx_frame) ticks_sign = utime.ticks_diff( 1, 2) # get the sign of ticks_diff, e.g. in init code. deadline = utime.ticks_add(utime.ticks_ms(), self.tx_timeout) if self._user_handler_tx is not None: self._user_handler_tx(i) while (utime.ticks_diff(deadline, utime.ticks_ms()) * ticks_sign) < 0: if self.frame_ACK == frame_cnt: l.debug("tx OK") return self.frame_ACK_stats l.debug("tx NOK {}/{}".format(i + 1, self.tx_retries)) else: self.lora_sock.send(tx_frame) return None
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 LoRaWAN: def __init__(self, logger): """ Connects to LoRaWAN using OTAA and sets up a ring buffer for storing messages. :param logger: status logger :type logger: LoggerFactory object """ self.logger = logger self.message_limit = int( float(config.get_config("fair_access")) / (float(config.get_config("air_time")) / 1000)) self.transmission_date = config.get_config( "transmission_date") # last date when lora was transmitting today = time.gmtime() date = str(today[0]) + str(today[1]) + str(today[2]) if self.transmission_date == date: # if device was last transmitting today self.message_count = config.get_config( "message_count") # get number of messages sent today else: self.message_count = 0 # if device was last transmitting a day or more ago, reset message_count for the day self.transmission_date = date config.save_config({ "message_count": self.message_count, "transmission_date": date }) regions = { "Europe": LoRa.EU868, "Asia": LoRa.AS923, "Australia": LoRa.AU915, "United States": LoRa.US915 } region = regions[config.get_config("region")] self.lora = LoRa(mode=LoRa.LORAWAN, region=region, adr=True) # create an OTAA authentication parameters app_eui = ubinascii.unhexlify(config.get_config("application_eui")) app_key = ubinascii.unhexlify(config.get_config("app_key")) # join a network using OTAA (Over the Air Activation) self.lora.join(activation=LoRa.OTAA, auth=(app_eui, app_key), timeout=0) # create a LoRa socket self.lora_socket = socket.socket(socket.AF_LORA, socket.SOCK_RAW) # request acknowledgment of data sent # self.lora_socket.setsockopt(socket.SOL_LORA, socket.SO_CONFIRMED, True) # do not request acknowledgment of data sent self.lora_socket.setsockopt(socket.SOL_LORA, socket.SO_CONFIRMED, False) # sets timeout for sending data self.lora_socket.settimeout( int(config.get_config("lora_timeout")) * 1000) # set up callback for receiving downlink messages self.lora.callback(trigger=LoRa.RX_PACKET_EVENT, handler=self.lora_recv) # initialises circular lora stack to back up data up to about 22.5 days depending on the length of the month self.lora_buffer = RingBuffer(self.logger, s.processing_path, s.lora_file_name, 31 * self.message_limit, 100) try: # this fails if the buffer is empty self.check_date() # remove messages that are over a month old except Exception as e: pass def lora_recv(self, arg): """ Callback for receiving packets through LoRaWAN. Decodes messages to commands for updating over WiFi. Requires a dummy argument. """ payload = self.lora_socket.recv(600) # receive bytes message self.logger.info("Lora message received") msg = payload.decode() # convert to string try: if msg == "0": # reboot device self.logger.info("Reset triggered over LoRa") self.logger.info("Rebooting...") machine.reset() elif msg == "1": # start software update self.logger.info("Software update triggered over LoRa") config.save_config({"update": True}) machine.reset() else: split_msg = msg.split(":") if split_msg[0] == "2": # update wifi credentials self.logger.info("WiFi credentials updated over LoRa") config.save_config({ "SSID": split_msg[1], "wifi_password": split_msg[2] }) elif split_msg[ 0] == "3": # update wifi credentials and start software update self.logger.info("WiFi credentials updated over LoRa") config.save_config({ "SSID": split_msg[1], "wifi_password": split_msg[2] }) self.logger.info("Software update triggered over LoRa") config.save_config({"update": True}) machine.reset() else: self.logger.error("Unknown command received over LoRa") except Exception as e: self.logger.exception( "Failed to interpret message received over LoRa") def lora_send(self): """Lora send method to run as a thread. Checks if messages are up to date in the lora buffer, pops the one on top of the stack, encodes it to a message and sends it to the right port. Takes two dummy arguments required by the threading library""" if lora_lock.locked(): self.logger.debug("Waiting for other lora thread to finish") with lora_lock: self.logger.debug("LoRa thread started") try: self.check_date() # remove messages that are over a month old if self.lora.has_joined(): self.logger.debug("LoRa connected") else: raise Exception("LoRa is not connected") if s.lora_file_name not in os.listdir(s.root_path + s.processing): raise Exception('LoRa - File: {} does not exist'.format( s.lora_file_name)) else: port, payload = self.get_sending_details() self.lora_socket.bind( port) # bind to port to decode at backend self.lora_socket.send( payload) # send payload to the connected socket self.logger.debug("LoRa - sent payload") self.message_count += 1 # increment number of files sent over LoRa today config.save_config({"message_count": self.message_count }) # save number of messages today # remove message sent self.lora_buffer.remove_head() except Exception: self.logger.exception("Sending payload over LoRaWAN failed") blink_led((0x550000, 0.4, True)) def get_sending_details(self): """ Gets message sitting on top of the lora stack, and constructs payload according to its format :return: port, payload :rtype: int, bytes """ buffer_line = self.lora_buffer.read() buffer_lst = buffer_line.split( ',') # convert string to a list of strings # get structure and port from format fmt = buffer_lst[2] # format is third item in the list fmt_dict = { "TPP": s.TPP, "TP": s.TP, "PP": s.PP, "P": s.P, "T": s.T, "G": s.G } port_struct_dict = fmt_dict[ fmt] # get dictionary corresponding to the format port = port_struct_dict["port"] # get port corresponding to the format structure = port_struct_dict[ "structure"] # get structure corresponding to the format # cast message according to format to form valid payload lst_message = buffer_lst[3:] # chop year, month and format off cast_lst_message = [] for i in range( (len(structure) - 1)): # iterate for length of structure having '<' stripped if structure[i + 1] == 'f': # iterate through structure ignoring '<' cast_lst_message.append(float( lst_message[i])) # cast to float if structure is 'f' else: cast_lst_message.append(int( lst_message[i])) # cast to int otherwise # pack payload self.logger.debug("Sending over LoRa: " + str(cast_lst_message)) payload = struct.pack( structure, *cast_lst_message ) # define payload with given structure and list of averages return port, payload # removes messages from end of lora stack until they are all within a month def check_date(self): """ Checks recursively if the message on the bottom of the stack is within a month """ buffer_line = self.lora_buffer.read( read_tail=True) # read the tail of the lora_buffer buffer_lst = buffer_line.split( ',') # convert string to a list of strings time_now = time.gmtime() # get current date # get year, month and minutes of the month now year_now, month_now, minutes_now = time_now[0], time_now[ 1], minutes_of_the_month() # get year, month and minutes of the month of the last message in the lora_buffer year_then, month_then, minutes_then = int(buffer_lst[0]) + 2000, int( buffer_lst[1]), int(buffer_lst[4]) # logic to decide if message is older than a month if year_then < year_now or month_then < month_now: if (month_then + 1 == month_now) or (month_then == 12 and month_now == 1 and year_then + 1 == year_now): if minutes_then < minutes_now + 24 * 60: self.lora_buffer.remove_tail() # remove message self.check_date() # call recursively else: self.lora_buffer.remove_tail() # remove message self.check_date() # call recursively
lora.add_channel(4, frequency=867300000, dr_min=0, dr_max=5) lora.add_channel(5, frequency=867500000, dr_min=0, dr_max=5) lora.add_channel(6, frequency=867700000, dr_min=0, dr_max=5) lora.add_channel(7, frequency=867900000, dr_min=0, dr_max=5) # 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, LORA_DR) # settings for ACK after TX s.setsockopt(socket.SOL_LORA, socket.SO_CONFIRMED, LORA_ACK) # install callback handler for TX/RX events lora.callback(trigger=(LoRa.RX_PACKET_EVENT|LoRa.TX_PACKET_EVENT), handler=event_handler) # send data every 30 seconds counter = 0 while True and lora.has_joined(): counter += 1 RX_EVENT = False print("Sending uplink packet {} with SF{}{}...".format( counter, 12-LORA_DR, ' asking for ACK' if LORA_ACK else ''), end="") try: s.setblocking(True) s.settimeout(10) flash_led(LED_GREEN, 0.1, 0.3, 2) s.send(bytes([counter])) print("OK.") except socket.timeout:
def lora_cb_handler(lora): global s print(type(lora)) try: events = lora.events() if events & LoRa.TX_PACKET_EVENT: print("Packet sent") if events & LoRa.RX_PACKET_EVENT: print("Packet received") print(s.recv(64)) except Exception: print('Exception') cb = lora.callback(handler=lora_cb_handler, trigger=LoRa.TX_PACKET_EVENT | LoRa.RX_PACKET_EVENT) s.setblocking(True) for i in range(2): print(s.send("Sending pk #%d" % i)) time.sleep(0.5) lst = (lora, s) cb = lora.callback(handler=lora_cb_handler, trigger=LoRa.TX_PACKET_EVENT | LoRa.RX_PACKET_EVENT, arg=lst) s.setblocking(True) for i in range(2): print(s.send("Sending pk #%d" % i)) time.sleep(0.5)
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.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 "SF7BW125" 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.timestamp, stats.sf, 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 LoraNet: def __init__(self, frequency, dr, region, device_class=LoRa.CLASS_C, activation=LoRa.OTAA, auth=None): self.frequency = frequency self.dr = dr self.region = region self.device_class = device_class self.activation = activation self.auth = auth self.sock = None self._exit = False self.s_lock = _thread.allocate_lock() self.lora = LoRa(mode=LoRa.LORAWAN, region=self.region, device_class=self.device_class) self._msg_queue = [] self.q_lock = _thread.allocate_lock() self._process_ota_msg = None def stop(self): self._exit = True def init(self, process_msg_callback): self._process_ota_msg = process_msg_callback def receive_callback(self, lora): events = lora.events() if events & LoRa.RX_PACKET_EVENT: rx, port = self.sock.recvfrom(256) if rx: if '$OTA' in rx: print("OTA msg received: {}".format(rx)) self._process_ota_msg(rx.decode()) else: self.q_lock.acquire() self._msg_queue.append(rx) self.q_lock.release() def connect(self): if self.activation != LoRa.OTAA and self.activation != LoRa.ABP: raise ValueError("Invalid Lora activation method") if len(self.auth) < 3: raise ValueError("Invalid authentication parameters") self.lora.callback(trigger=LoRa.RX_PACKET_EVENT, handler=self.receive_callback) # set the 3 default channels to the same frequency self.lora.add_channel(0, frequency=self.frequency, dr_min=0, dr_max=5) self.lora.add_channel(1, frequency=self.frequency, dr_min=0, dr_max=5) self.lora.add_channel(2, frequency=self.frequency, dr_min=0, dr_max=5) # remove all the non-default channels for i in range(3, 16): self.lora.remove_channel(i) # authenticate with abp or ota if self.activation == LoRa.OTAA: self._authenticate_otaa(self.auth) else: self._authenticate_abp(self.auth) # create socket to server self._create_socket() def _authenticate_otaa(self, auth_params): # create an OTAA authentication params self.dev_eui = binascii.unhexlify(auth_params[0]) self.app_eui = binascii.unhexlify(auth_params[1]) self.app_key = binascii.unhexlify(auth_params[2]) self.lora.join(activation=LoRa.OTAA, auth=(self.dev_eui, self.app_eui, self.app_key), timeout=0, dr=self.dr) while not self.lora.has_joined(): time.sleep(2.5) print('Not joined yet...') def has_joined(self): return self.lora.has_joined() def _authenticate_abp(self, auth_params): # create an ABP authentication params self.dev_addr = struct.unpack(">l", binascii.unhexlify(auth_params[0]))[0] self.nwk_swkey = binascii.unhexlify(auth_params[1]) self.app_swkey = binascii.unhexlify(auth_params[2]) self.lora.join(activation=LoRa.ABP, auth=(self.dev_addr, self.nwk_swkey, self.app_swkey)) def _create_socket(self): # create a LoRa socket self.sock = socket.socket(socket.AF_LORA, socket.SOCK_RAW) # set the LoRaWAN data rate self.sock.setsockopt(socket.SOL_LORA, socket.SO_DR, self.dr) # make the socket non blocking self.sock.setblocking(False) time.sleep(2) def send(self, packet): with self.s_lock: self.sock.send(packet) def receive(self, bufsize): with self.q_lock: if len(self._msg_queue) > 0: return self._msg_queue.pop(0) return '' def get_dev_eui(self): return binascii.hexlify(self.lora.mac()).decode('ascii') def change_to_multicast_mode(self, mcAuth): print('Start listening for firmware updates ...........') if self.device_class != LoRa.CLASS_C: self.lora = LoRa(mode=LoRa.LORAWAN, region=self.region, device_class=LoRa.CLASS_C) self.connect() mcAddr = struct.unpack(">l", binascii.unhexlify(mcAuth[0]))[0] mcNwkKey = binascii.unhexlify(mcAuth[1]) mcAppKey = binascii.unhexlify(mcAuth[2]) self.lora.join_multicast_group(mcAddr, mcNwkKey, mcAppKey)
print("Joined LoRa Network!") # Print LoRa Stats lora.stats() # Call back for T/RX Messages def lora_cb(lora): events = lora.events() if events & LoRa.RX_PACKET_EVENT: print('Lora packet received') if events & LoRa.TX_PACKET_EVENT: print('Lora packet sent') lora.callback(trigger=(LoRa.RX_PACKET_EVENT | LoRa.TX_PACKET_EVENT), handler=lora_cb) # 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, 5) # selecting confirmed type of messages s.setsockopt(socket.SOL_LORA, socket.SO_CONFIRMED, True) # make the socket non-blocking s.setblocking(False) # send some data while True:
class LoraController: def __init__(self, options, logger, eventLog, ledController): self.options = options self.logger = logger self.eventLog = eventLog self.led = ledController self.lora = LoRa(mode=LoRa.LORA, power_mode=LoRa.SLEEP) self.tx_runner = None # thread which sends events over lora self.lastJoin = 0 # when did we join the lora network self.isJoinLogged = False # did we log the initial LORA join self.lastEventId = 0 # last sent event id self.sendLock = _thread.allocate_lock() self.socketLock = _thread.allocate_lock() self.isAckingCounter = 0 self.noDownlinkCounter = 0 self.lastUplinkTime = 0 self.isAcking = False # logging def log(self, *text): self.logger.log("LORA", *text) # start lora connectivity def start(self): # setup lorawan self.lora = LoRa(mode=LoRa.LORAWAN, region=LoRa.EU868, device_class=LoRa.CLASS_A, tx_retries=3, adr=True, sf=12) self.lora.nvram_restore() self.lora.callback(trigger=(LoRa.RX_PACKET_EVENT | LoRa.TX_PACKET_EVENT | LoRa.TX_FAILED_EVENT), handler=self.lora_callback) self.log('Lora DevEUI is', self.getDeviceEUI()) self.log('Lora AppEUI is', self.options['lora_app_eui']) if len(self.options['lora_app_eui']) != 16: self.log("ERROR", "Setting 'lora_app_eui' is invalid:", self.options['lora_app_eui']) return # issue join if self.options['lora_mode'] == "abp": self.join() elif self.lora.has_joined(): self.log("Lora network is already joined, re-joining anyway") else: self.join() def lora_callback(self, lora): events = lora.events() if events & LoRa.TX_FAILED_EVENT: self.log('Lora TX FAILED') # determines the LORA MAC address (string) def getDeviceEUI(self): return ubinascii.hexlify(self.lora.mac()).decode('ascii').upper() # joins the lora network via OTAA def joinOTAA(self): app_eui = ubinascii.unhexlify(self.options['lora_app_eui']) app_key = ubinascii.unhexlify(self.options['lora_app_key']) self.lora.join(activation=LoRa.OTAA, auth=(app_eui, app_key), timeout=0) self.log("Joining via OTAA") self.lastJoin = time.time() # joins the lora network via ABP def joinABP(self): net_key = ubinascii.unhexlify(self.options['lora_net_key']) app_key = ubinascii.unhexlify(self.options['lora_app_key']) # note: TTN seems to want the reverse bytes of this address device_address = ubinascii.unhexlify(self.options['lora_dev_adr']) self.lora.join(activation=LoRa.ABP, auth=(device_address, net_key, app_key)) self.log("Joining via ABP with device address", device_address) self.lastJoin = time.time() # joins the lora network via ABP or OTAA def join(self): if self.options['lora_mode'] == "abp": self.joinABP() else: self.joinOTAA() def hasJoined(self): return self.lora.has_joined() def stats(self): return self.lora.stats() def makePayload(self, event): payload = None command = event['Command'] idbytes = event['ID'].to_bytes(2, 'little') event_ts = event['Time'] try: if command == eventlog.CMD_TAG_DETECTED: # Tag with 4-Byte UID detected # <0x01> <Event ID 0..1> <Timestamp 0..3> <UID 0..3/6/9> timeBytes = event_ts.to_bytes(4, 'little') uid = event['Data'][0:10] # remove trailing 0x00 uid_size = 10 for i in range(uid_size - 1, 3, -1): if uid[i] != 0x00: break uid_size = uid_size - 1 uid = uid[:uid_size] payload = bytes([0x01]) + idbytes + timeBytes + uid uidText = ubinascii.hexlify(uid).decode() self.log("CMD 0x01 [NFC_DETECTED] SEQ#", event['ID'], ". uid =", uidText, ", ts =", event_ts) if command == eventlog.CMD_TIME_REQUEST2: # ask backend for current time (new) # <0x04> <ID 0..1> <Our Time 0..3> mytime = time.time().to_bytes(4, 'little') payload = bytes([command]) + idbytes + mytime self.log("CMD 0x04 [TIME_REQUEST] ID#", event['ID'], ". our_time =", time.time(), utime.gmtime(time.time())) if command == eventlog.CMD_TIME_CHANGED: # <0x05> <Event ID 0..1> <Our Time 0..3> <Old Time 0..3> mytime = event_ts.to_bytes(4, 'little') oldTime = event['Data'][0:4] payload = bytes([eventlog.CMD_TIME_CHANGED ]) + idbytes + mytime + oldTime self.log("CMD 0x05 [TIME_CHANGED] SEQ#", event['ID'], ". our_time =", event_ts, utime.gmtime(event_ts), ", old_time =", oldTime) except Exception as e: self.log("ERROR: Unable to prepare LORA payload:", e.args[0], e) return payload # attempts to send the given event def sendEvent(self, event): with self.sendLock: eventId = event['ID'] command = event['Command'] self.log("Preparing to send CMD =", command, ", SEQ_NO =", eventId) if self.lastEventId > 0 and eventId > self.lastEventId + 1: self.log("ERROR", "Event IDs are not in sequence - last:", self.lastEventId, ", current:", eventId) self.lastEventId = eventId # prepare lora payload for supported event log entries payload = self.makePayload(event) if payload == None: self.log( "WARN: Event payload is None and therefore ignored for lora transmission" ) return True # send payload return self.sendAndHandleResponse(payload) # sends the payload and handles the optional response def sendAndHandleResponse(self, payload): if not self.hasJoined(): self.log("ERROR", "Unable to send LORA payload because not joined") return False # send responseData = self.sendPayload(payload) if responseData == False: self.noDownlinkCounter = self.noDownlinkCounter + 1 return False # handle response if responseData != None and len(responseData) > 0: try: return True except Exception as e: self.log("ERROR: Unable to handle LORA payload: ", e.args[0], e) self.noDownlinkCounter = self.noDownlinkCounter + 1 else: self.noDownlinkCounter = self.noDownlinkCounter + 1 # the message has been sent return True def sendTimeRequest(self, clockSyncEvent, clockSyncRequests): clockSyncEvent['Command'] = eventlog.CMD_TIME_REQUEST2 payload = self.makePayload(clockSyncEvent) try: with self.sendLock: # send lora uplink responseData = self.sendPayload(payload, False) if responseData == False: return None except Exception as e: self.log("ERROR", "Unable to sync clock via LORA:", e.args[0], e) return None # send the specified payload def sendPayload(self, data, updateTime=True): try: with self.socketLock: self.log("> sending", len(data), "bytes:", binascii.hexlify(data)) responseData = None # create a LoRa socket s = socket.socket(socket.AF_LORA, socket.SOCK_RAW) s.setblocking(False) try: """free_memory = gc.mem_free() allocated_memory = gc.mem_alloc() print("Free Memory: " + str(free_memory) + " -- Allocated Memory : " + str(allocated_memory))""" s.send(data) time.sleep(5) responseData = s.recv(64) except Exception as e: self.log("ERROR", "LORA Socket Exception", e) s.close() if responseData != None: responseLen = len(responseData) if responseLen > 0: self.log("< received", responseLen, "bytes:", binascii.hexlify(responseData)) else: self.log("< no downlink") # log if updateTime == True: self.lastUplinkTime = time.time() self.log(self.stats()) time.sleep_ms(10) # save frame counters self.lora.nvram_save() time.sleep_ms(5) return responseData except Exception as e: self.log("ERROR", "Unable to send payload", e.args[0], e) return False
class LoraCoverage: def __init__(self, host, port, ssid, wpa, log_path): self.host = host self.port = port self.ssid = ssid self.wpa = wpa self.path = log_path self.gps_buffer = None self.log_time = None self.log_file = None self.rxnb = 0 self.loramac = binascii.hexlify(network.LoRa().mac()) # to be refactored self.end = False self.lock = _thread.allocate_lock() # Create proper directory for log file self._init_log() # Setup wypy/lopy as a station self.wlan = network.WLAN(mode=network.WLAN.STA) self.wlan.connect(self.ssid, auth=(network.WLAN.WPA2, self.wpa)) while not self.wlan.isconnected(): print('Connecting to Android WIFI HOTPOST...') time.sleep(1) print('Connected to Android WIFI HOTPOST') # Lora socket self.lora = None self.lora_sock = None # TCP socket self.tcp_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # Connect self.tcp_sock.connect((self.host, self.port)) def _init_log(self): # create directory log if not exist try: os.mkdir(self.path) except OSError: pass # Check if string is empty def _isNotBlank (self, myString): return bool(myString and myString.strip()) def _get_log_name(self): #print(self.log_time[:19]) #print(type(ts)) # ts = ts.replace('-', '') # ts = ts.replace('T', '') # ts = ts.replace(':', '') #ts = '1974' #base = self.path + '/acq/' #return base + ts + 'list.csv' return self.path + '/acq.list.csv' def _lora_cb(self, lora): events = lora.events() if (events & LoRa.RX_PACKET_EVENT) and (self.gps_buffer is not None): #if self.log_file is None: # self.log_file = open(self._get_log_name(), 'w') data_rx = self.lora_sock.recv(256) stats = self.lora.stats() # get lora stats (data is tuple) if self._isNotBlank(data_rx): with self.lock: print(self.log_time) print(self.gps_buffer) #print(self.gps_buffer['time']) # time_stamp = stats[0] # get timestamp value # rssi = stats[1] # snr = stats[2] # sf = stats[3] # # msgData = '' # msgCrc = '' # if len(data_rx) >= 5: # msg_data = data_rx[:-5] # remove the last 5 char data crc # msg_crc = data_rx[-4:] # get the last 4 char # # # Calculate CRC # crc8 = utils.crc(msg_data) # # # Check CRC # crc_ok = True if crc8 == msg_crc.decode('utf-8') else False # # crc_ok = False # # if crc8 == msg_crc.decode('utf-8'): # # crc_ok = True # # # fields_lorastats(LoraStats) # # # form csv row # msg = str(self.rxnb) # msg = msg + ',' + self.loramac.decode('utf8') # msg = msg + ',' + self.gps_buffer['time'] # msg = msg + ',' + str(self.gps_buffer.lat) # msg = msg + ',' + str(self.gps_buffer.lon) # msg = msg + ',' + str(self.gps_buffer.alt) # # msg = msg + ',' + msg_data.decode('utf-8') # msg = msg + ',' + msg_crc.decode('utf-8') # msg = msg + ',' + str(crc8) # msg = msg + ',' + str(crc_ok) # msg = msg + ',' + str(time_stamp) # msg = msg + ',' + str(rssi) # msg = msg + ',' + str(snr) # msg = msg + ',' + str(sf) # # # # calc crc8 row # #crc8row = calc(msg.encode('utf-8')) # crc8row = utils.crc(msg.encode('utf-8')) # # # # add crc8row as last item # msg = msg + ',' + str(crc8row) # # # write csv and terminal # self.log_file.write(msg) # self.log_file.write('\n') # self.log_file.flush() # # print(msg) # show in repl # self.rxnb += 1 def start(self): # Initialize a LoRa sockect self.lora = LoRa(mode=LoRa.LORA) self.lora_sock = socket.socket(socket.AF_LORA, socket.SOCK_RAW) self.lora_sock.setblocking(False) #self.tcp_alarm = Timer.Alarm(handler=lambda u: self._tcp_gps(), s=1, periodic=True) self.lora.callback(trigger=LoRa.RX_PACKET_EVENT, handler=self._lora_cb) # Start the TCP thread receiving GPS coordinates _thread.start_new_thread(self._tcp_thread, ()) def stop(self): self.wlan.mode(network.WLAN.AP) self.tcp_sock.close() self.lora.callback(trigger=LoRa.RX_PACKET_EVENT, handler=None) self.lora_sock.close() #self.log_file.close() # Set end flag to terminate TCP thread self.end = True def _tcp_thread(self): while True: if self.end: _thread.exit() try: # Send request self.tcp_sock.send(b'?SHGPS.LOCATION;\r\n') # Get response rx_data = self.tcp_sock.recv(4096) # Release the lock in case of previous TCP error #self.lock.release() # Save last gps received to buffer with self.lock: self.gps_buffer = json.loads(rx_data.decode('ascii')) if not self.log_time: self.log_time = self.gps_buffer['time'] except socket.timeout: self.lock.locked() except OSError as e: if e.errno == errno.EAGAIN: pass else: self.lock.locked() print("Error: Android 'ShareGPS' connection status should be 'Listening'") print('Change mode and soft reboot Pycom device') except Exception: self.lock.locked() print("TCP recv Exception") time.sleep(0.5)
class LoRaMQ: def __init__(self, config): self.lora = LoRa(mode=LoRa.LORAWAN) # set the 3 default channels to the same frequency (must be before sending the OTAA join request) self.lora.add_channel(0, frequency=config.LORA_FREQUENCY, dr_min=0, dr_max=5) self.lora.add_channel(1, frequency=config.LORA_FREQUENCY, dr_min=0, dr_max=5) self.lora.add_channel(2, frequency=config.LORA_FREQUENCY, dr_min=0, dr_max=5) # join a network using OTAA self.lora.join(activation=LoRa.OTAA, auth=(binascii.unhexlify(config.DEV_EUI), binascii.unhexlify(config.APP_EUI), binascii.unhexlify(config.APP_KEY)), timeout=0, dr=config.LORA_NODE_DR) # wait until the module has joined the network while not self.lora.has_joined(): time.sleep(2.5) print('Not joined yet...') # remove all the non-default channels for i in range(3, 16): self.lora.remove_channel(i) # create a LoRa socket self.s = socket.socket(socket.AF_LORA, socket.SOCK_RAW) # set the LoRaWAN data rate self.s.setsockopt(socket.SOL_LORA, socket.SO_DR, config.LORA_NODE_DR) # make the socket non-blocking self.s.setblocking(False) time.sleep(5.0) self.send_mutex = _thread.allocate_lock() self.send_queue = [] self.recv_mutex = _thread.allocate_lock() self.recv_queue = [] self.callback = None self.lora.callback(LoRa.RX_PACKET_EVENT, self._recv) def _recv(self, data): rx, port = s.recvfrom(256) self.recv_mutex.acquire() self.recv_queue.append((port, rx)) self.recv_mutex.release() if self.callback: _thread.start_new_thread(self.callback, tuple()) def send(self, data): self.send_mutex.acquire() self.send_queue.append(data) self.send_mutex.release() def receive(self): self.recv_mutex.acquire() if not len(self.recv_queue): return False data = self.recv_queue.pop(0) self.recv_mutex.release() return data def rq_length(self): self.recv_mutex.acquire() size = len(self.recv_queue) self.recv_mutex.release() return size def attach_callback(self, callback): self.callback = callback def _loop(self): while True: #acquire a lock on the send message queue self.send_mutex.acquire() #get the message from the front of the queue if len(self.send_queue): data = self.send_queue.pop(0) else: data = None self.send_mutex.release() if data: #acquire a lock on the channel self.recv_mutex.acquire() #send the data self.s.send(data) #release the channel lock self.recv_mutex.release() #LoRa protocol only allows sending and recieving every 5 seconds #so to avoid protocol errors from the transciever sleep the thread #for 5 seconds time.sleep(5) def start(self): _thread.start_new_thread(self._loop, tuple())