#!/usr/bin/python3 from time import sleep import argparse import threading import ctypes import os import struct import datetime import time import math import traceback from importlib import util import queue import serial from LoRaMonUIApp import LoRaMonUIApp from LoRaMonHelperClasses import RNS from LoRaMonHelperClasses import RNSSerial from LoRaMonHelperClasses import KISS from LoRaMonHelperClasses import ROM #returned number of packets that were captured #the return value in Linux is only 8 bits. we need to cap it #make this a global for use, even if rnode isn't initialized number_of_packets_received_exit_code = 0 class RNode(): def __init__(self, serial_instance): self.rns_serial = serial_instance self.timeout = 100 #These are parameters recevied from the radio self.r_frequency = None self.r_bandwidth = None self.r_txpower = None self.r_sf = None self.r_state = None self.r_lock = None self.r_stat_rssi = 0 self.r_stat_snr = 0 self.r_battery = None self.r_cr = None self.r_promiscuous = None self.r_implicit_length = 0 self.rssi_offset = 157 # command line arguments self.sf = None self.cr = None self.txpower = None self.frequency = None self.bandwidth = None self.implicit_length = 0 self.promiscuous = False self.detected = None self.eeprom = None self.fw_major_version = None self.fw_minor_version = None self.fw_version = None self.provisioned = None self.product = None self.model = None self.hw_rev = None self.made = None self.serialno = None self.checksum = None self.signature = None self.signature_valid = False self.vendor = None #channel parameters self.ats = None #Air time, short term self.atl = None #air time, long term self.cls = None #Channel utilization, short term self.cll = None #Channel utilization, long term self.crs = None #Current RSSI self.nfl = None #Current noise floor self.ntf = None #Interference #phy parameters self.lst = None #Symbol time self.lsr = None #Symbol rate self.prs = None #Preamble length self.prt = None #PRT self.cst = None #CSMA Slot MS self.dft = None #DIFS MS self.print_hex = None #duration of time, in seconds, to capture self.duration_to_capture_for = 0 #total number of packets captured self.number_of_packets_received = 0 self.capture_start_time = time.time() #flag for printing raw bytes from RNode self.print_raw_data_enabled = False #flag for stopping the thread self.thread_continue = None #link back to the ui app, used for updating self.loramon_ui_app = None #queue for receiving updates from the UI self.queue_from_ui = None def setCapturDuration(self, seconds): #set the start time, first self.capture_start_time = time.time() #set seconds after setting the time, to eliminate race issues with the thread self.duration_to_capture_for = seconds def device_probe(self): deviced_detected = False iterations_to_wait = 10 sleep(2.5) self.detectRequest() while True: if self.detected == True: RNS.log("RNode connected") RNS.log("Firmware version: " + str(self.fw_version)) device_detected = True break else: sleep(0.1) iterations_to_wait -= 1 if iterations_to_wait == 0: device_detected = False break return device_detected def updateIUApp(self, type, value): if (self.loramon_ui_app != None): msg = { "type": type, "value": value } self.loramon_ui_app.queue_from_radio.put(msg) def readFromUIApp(self): if self.queue_from_ui: if not self.queue_from_ui.empty(): msg = self.queue_from_ui.get() match msg['type']: case "frequency": #print(f"UI is requesting frequency to change to {msg['value']}") self.frequency = msg['value'] self.setFrequency() case "bandwidth": #print(f"UI is requesting bandwidth to change to {msg['value']}") self.bandwidth = msg['value'] self.setBandwidth() case "spread_factor": #print(f"UI is requesting spread_factor to change to {msg['value']}") self.sf = msg['value'] self.setSpreadingFactor() case "coding_rate": #print(f"UI is requesting coding_rate to change to {msg['value']}") self.cr = msg['value'] self.setCodingRate() case "print_raw_data": self.setPrintRawDataBytes(msg['value']) case _: None def packetReadLoop(self): global number_of_packets_received_exit_code try: in_frame = False escape = False command = KISS.CMD_UNKNOWN data_buffer = b"" command_buffer = b"" last_read_ms = int(time.time()*1000) packet_string = "" while self.rns_serial.IsOpen() and self.thread_continue == True: # process messages from the UI self.readFromUIApp() if (self.duration_to_capture_for > 0): #handle case where a capture duration is specified seconds_elapsed = int(time.time() - self.capture_start_time) if(seconds_elapsed > self.duration_to_capture_for): return (self.number_of_packets_received) if self.rns_serial.InWaiting(): byte = ord(self.rns_serial.Read(1)) last_read_ms = int(time.time()*1000) if (in_frame == True): if (self.print_raw_data_enabled == True): #logic to print raw frame data when in frame if (byte == KISS.FEND): # we have detected end of a frame packet_string += str(f"{byte:#0{4}x}") packet_string += "<--" RNS.log(packet_string) packet_string = "" else: # we are still capturing bytes while a frame was detected packet_string += f"{byte:#0{4}x} " if (byte == KISS.FEND): #received FEND, which signals end of a frame #first make sure we actually received a command if (len(command_buffer) == 0): #double FEND #detected a FEND FEND situation #stay in frame None else: #we have a command, handle it match command_buffer[0]: case KISS.CMD_DATA: self.processIncoming(data_buffer) self.number_of_packets_received += 1 if (self.number_of_packets_received > 255): number_of_packets_received_exit_code = 255 else: number_of_packets_received_exit_code = self.number_of_packets_received self.updateIUApp("r_captured_packets", self.number_of_packets_received) case KISS.CMD_ROM_READ: self.eeprom = data_buffer case KISS.CMD_FREQUENCY: if (len(data_buffer) == 4): self.r_frequency = data_buffer[0] << 24 | data_buffer[1] << 16 | data_buffer[2] << 8 | data_buffer[3] RNS.log("Radio reporting frequency is "+str(self.r_frequency/1000000.0)+" MHz") self.updateIUApp("r_frequency", self.r_frequency) self.updateBitrate() else: RNS.log(f"Wrong number of bytes {len(data_buffer)} for frequency") case KISS.CMD_BANDWIDTH: if (len(data_buffer) == 4): self.r_bandwidth = data_buffer[0] << 24 | data_buffer[1] << 16 | data_buffer[2] << 8 | data_buffer[3] RNS.log("Radio reporting bandwidth is "+str(self.r_bandwidth/1000.0)+" KHz") self.updateIUApp("r_bandwidth", self.r_bandwidth) self.updateBitrate() else: RNS.log(f"Wrong number of bytes {len(data_buffer)} for bandwith") case KISS.CMD_FW_VERSION: if (len(data_buffer) == 2): self.fw_major_version = data_buffer[0] self.fw_minor_version = data_buffer[1] self.updateVersion() self.updateIUApp("fw_version", self.fw_version) else: RNS.log(f"Wrong number of bytes {len(data_buffer)} for version") case KISS.CMD_TXPOWER: if (len(data_buffer) == 1): self.r_txpower = data_buffer[0] RNS.log("Radio reporting TX power is "+str(self.r_txpower)+" dBm") else: RNS.log(f"Wrong number of bytes {len(data_buffer)} for tx power") case KISS.CMD_SF: if (len(data_buffer) == 1): self.r_sf = data_buffer[0] RNS.log("Radio reporting spreading factor is "+str(self.r_sf)) self.updateIUApp("r_spread_factor", self.r_sf) self.updateBitrate() else: RNS.log(f"Wrong number of bytes {len(data_buffer)} for spreading factor") case KISS.CMD_CR: if (len(data_buffer) == 1): self.r_cr = data_buffer[0] RNS.log("Radio reporting coding rate is "+str(self.r_cr)) self.updateIUApp("r_coding_rate", self.r_cr) self.updateBitrate() else: RNS.log(f"Wrong number of bytes {len(data_buffer)} for coding rate") case KISS.CMD_IMPLICIT: if (len(data_buffer) == 1): self.r_implicit_length = data_buffer[0] if self.r_implicit_length != 0: RNS.log("Radio in implicit header mode, listening for packets with a length of "+str(self.r_implicit_length)+" bytes") else: RNS.log(f"Wrong number of bytes {len(data_buffer)} for implicit header") case KISS.CMD_RADIO_STATE: if (len(data_buffer) == 1): self.r_state = data_buffer[0] self.updateIUApp("r_state", self.r_state) RNS.log("Radio reporting radio state is "+str(self.r_state)) else: RNS.log(f"Wrong number of bytes {len(data_buffer)} for radio state") case KISS.CMD_RADIO_LOCK: if (len(data_buffer) == 1): self.r_lock = data_buffer[0] self.updateIUApp("r_lock", self.r_lock) RNS.log("Radio reporting radio lock is "+str(self.r_lock)) else: RNS.log(f"Wrong number of bytes {len(data_buffer)} for radio lock") case KISS.CMD_ERROR: if (len(data_buffer) == 1): if (data_buffer[0] == KISS.ERROR_INITRADIO): RNS.log(str(self)+" hardware initialisation error (code "+RNS.hexrep(data_buffer[0])+")") elif (data_buffer[0] == KISS.ERROR_INITRADIO): RNS.log(str(self)+" hardware TX error (code "+RNS.hexrep(data_buffer[0])+")") else: RNS.log(str(self)+" hardware error (code "+RNS.hexrep(data_buffer[0])+")") else: RNS.log(f"Wrong number of bytes {len(data_buffer)} for error") case KISS.CMD_DETECT: if (len(data_buffer) == 1): if data_buffer[0] == KISS.DETECT_RESP: self.detected = True else: self.detected = False self.updateIUApp("r_detected", self.detected) else: RNS.log(f"Wrong number of bytes {len(data_buffer)} for detect") case KISS.CMD_STAT_RSSI: if (len(data_buffer) == 1): self.r_stat_rssi = data_buffer[0] - self.rssi_offset RNS.log("Radio reporting rssi is "+str(self.r_stat_rssi)) self.updateIUApp("r_stat_rssi", self.r_stat_rssi) else: RNS.log(f"Wrong number of bytes {len(data_buffer)} for rssi") case KISS.CMD_STAT_SNR: if (len(data_buffer) == 1): self.r_stat_snr = int.from_bytes(bytes([data_buffer[0]]), byteorder="big", signed=True) * 0.25 RNS.log("Radio reporting snr is "+str(self.r_stat_snr)) self.updateIUApp("r_stat_snr", self.r_stat_snr) else: RNS.log(f"Wrong number of bytes {len(data_buffer)} for snr") case KISS.CMD_STAT_BAT: if (len(data_buffer) == 2): RNS.log(f"Radio reporting battery state is {data_buffer[0]}, % {data_buffer[1]}") self.r_battery = f"S:{data_buffer[0]}, % {data_buffer[1]}" self.updateIUApp("r_battery", self.r_battery) else: RNS.log(f"Wrong number of bytes {len(data_buffer)} for battery") case KISS.CMD_STAT_CHTM: if (len(data_buffer) == 11): self.ats = data_buffer[0] << 8 | data_buffer[1] self.atl = data_buffer[2] << 8 | data_buffer[3] self.cls = data_buffer[4] << 8 | data_buffer[5] self.cll = data_buffer[6] << 8 | data_buffer[7] self.crs = data_buffer[8] self.nfl = data_buffer[9] self.ntf = data_buffer[10] RNS.log(f"Radio reporting CHTM: " + f"ats:{self.ats}, " + f"atl:{self.atl}, " + f"cls:{self.cls}, " + f"cll:{self.cll}, " + f"crs:{self.crs}, " + f"nfl:{self.nfl}, " + f"ntf:{self.ntf}") else: RNS.log(f"Wrong number of bytes {len(data_buffer)} for chtm") case KISS.CMD_STAT_PHYPRM: if (len(data_buffer) == 12): self.lst = data_buffer[0] << 8 | data_buffer[1] self.lsr = data_buffer[2] << 8 | data_buffer[3] self.prs = data_buffer[4] << 8 | data_buffer[5] self.prt = data_buffer[6] << 8 | data_buffer[7] self.cst = data_buffer[8] << 8 | data_buffer[9] self.dft = data_buffer[10] << 8 | data_buffer[11] RNS.log(f"Radio reporting PHYPRM: " + f"lst:{self.lst}, " + f"lsr:{self.lsr}, " + f"prs:{self.prs}, " + f"prt:{self.prt}, " + f"cst:{self.cst}, " + f"dft:{self.dft}") else: RNS.log(f"Wrong number of bytes {len(data_buffer)} for phyprm") case KISS.CMD_PROMISC: if (len(data_buffer) == 1): RNS.log("Radio reporting promiscuous mode is " + str(data_buffer[0])) if str(data_buffer[0]) == 0: self.r_promiscuous = False else: self.r_promiscuous = True self.updateIUApp("r_promiscuous", self.r_promiscuous) else: RNS.log(f"Wrong number of bytes {len(data_buffer)} for promisc") case _: #any command not handled here RNS.log(f"Received unhandled command {command_buffer[0]}") # set all buffer to empty in_frame = False data_buffer = b"" command_buffer = b"" else: #still in frame, accumulate received bytes in data_buffer if (len(command_buffer) == 0): #first byte after FEND is the command command_buffer = bytes([byte]) #add the received byte to data_buffer but account for escape characters else: #receive bytes after the command if (byte == KISS.FESC): escape = True else: if (escape): if (byte == KISS.TFEND): byte = KISS.FEND elif (byte == KISS.TFESC): byte = KISS.FESC else: RNS.log(f"Recieved bad escape code {byte}") escape = False data_buffer = data_buffer+bytes([byte]) else: #in_frame != True if (byte == KISS.FEND): #not in frame and received FEND. start a new frame in_frame = True command = KISS.CMD_UNKNOWN data_buffer = b"" command_buffer = b"" if (self.print_raw_data_enabled == True): packet_string += "-->" packet_string += f"{byte:#0{4}x} " else: #we are out of frame and received a non FEND byte!!! #shouldn't happen packet_string += f"{byte:#0{4}x}" None else: time_since_last = int(time.time()*1000) - last_read_ms if in_frame == True and time_since_last > self.timeout: RNS.log(str(self)+" serial read timeout while receiving frame") data_buffer = b"" in_frame = False command = KISS.CMD_UNKNOWN escape = False sleep(0.08) except Exception as e: RNS.log("Error while reading from serial port") traceback.print_exc() return(0) def processIncoming(self, data): self.callback(data, self) def updateBitrate(self): try: self.bitrate = self.sf * ( (4.0/self.cr) / (math.pow(2,self.sf)/(self.bandwidth/1000)) ) * 1000 self.bitrate_kbps = round(self.bitrate/1000.0, 2) except: self.bitrate = 0 def updateVersion(self): minstr = str(self.fw_minor_version) if len(minstr) == 1: minstr = "0"+minstr self.fw_version = str(self.fw_major_version)+"."+minstr def detectRequest(self): kiss_command = bytes([KISS.FEND, KISS.CMD_DETECT, KISS.DETECT_REQ, KISS.FEND, KISS.CMD_FW_VERSION, 0x00, KISS.FEND]) written = self.rns_serial.Write(kiss_command) if written != len(kiss_command): raise IOError("An IO error occurred while configuring spreading factor for "+self(str)) def initRadio(self): self.setFrequency() self.setBandwidth() self.setTXPower() self.setSpreadingFactor() self.setCodingRate() self.setImplicitLength() self.setRadioState(KISS.RADIO_STATE_ON) self.setPromiscuousMode() sleep(0.5) def setFrequency(self): c1 = self.frequency >> 24 c2 = self.frequency >> 16 & 0xFF c3 = self.frequency >> 8 & 0xFF c4 = self.frequency & 0xFF data = KISS.escape(bytes([c1])+bytes([c2])+bytes([c3])+bytes([c4])) kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_FREQUENCY])+data+bytes([KISS.FEND]) written = self.rns_serial.Write(kiss_command) if written != len(kiss_command): raise IOError("An IO error occurred while configuring frequency for "+self(str)) def setBandwidth(self): c1 = self.bandwidth >> 24 c2 = self.bandwidth >> 16 & 0xFF c3 = self.bandwidth >> 8 & 0xFF c4 = self.bandwidth & 0xFF data = KISS.escape(bytes([c1])+bytes([c2])+bytes([c3])+bytes([c4])) kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_BANDWIDTH])+data+bytes([KISS.FEND]) written = self.rns_serial.Write(kiss_command) if written != len(kiss_command): raise IOError("An IO error occurred while configuring bandwidth for "+self(str)) def setTXPower(self): txp = bytes([self.txpower]) kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_TXPOWER])+txp+bytes([KISS.FEND]) written = self.rns_serial.Write(kiss_command) if written != len(kiss_command): raise IOError("An IO error occurred while configuring TX power for "+self(str)) def setSpreadingFactor(self): sf = bytes([self.sf]) kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_SF])+sf+bytes([KISS.FEND]) written = self.rns_serial.Write(kiss_command) if written != len(kiss_command): raise IOError("An IO error occurred while configuring spreading factor for "+self(str)) def setCodingRate(self): cr = bytes([self.cr]) kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_CR])+cr+bytes([KISS.FEND]) written = self.rns_serial.Write(kiss_command) if written != len(kiss_command): raise IOError("An IO error occurred while configuring coding rate for "+self(str)) def setImplicitLength(self): if self.implicit_length != 0: length = KISS.escape(bytes([self.implicit_length])) kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_IMPLICIT])+length+bytes([KISS.FEND]) written = self.rns_serial.Write(kiss_command) if written != len(kiss_command): raise IOError("An IO error occurred while configuring implicit header mode for "+self(str)) def setRadioState(self, state): kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_RADIO_STATE])+bytes([state])+bytes([KISS.FEND]) written = self.rns_serial.Write(kiss_command) if written != len(kiss_command): raise IOError("An IO error occurred while configuring radio state for "+self(str)) def setPromiscuousMode(self): if self.promiscuous == True: kiss_command = bytes([KISS.FEND, KISS.CMD_PROMISC, 0x01, KISS.FEND]) else: kiss_command = bytes([KISS.FEND, KISS.CMD_PROMISC, 0x00, KISS.FEND]) written = self.rns_serial.Write(kiss_command) if written != len(kiss_command): raise IOError("An IO error occurred while configuring promiscuous mode for "+self(str)) def setPrintRawDataBytes(self, state): if state == True: self.print_raw_data_enabled = True # for debugging # RNS.log("setPrintRawDataBytes Received True") else: self.print_raw_data_enabled = False # for debugging # RNS.log("setPrintRawDataBytes Received False") def packet_captured(data, rnode_instance): if rnode_instance.console_output: if rnode_instance.print_hex: if len(data) == 1: data = [data] datastring = "\n"+RNS.hexrep(data)+"\n" else: datastring = str(data) RNS.log("["+str(rnode_instance.r_stat_rssi)+" dBm] [SNR "+str(rnode_instance.r_stat_snr)+" dB] ["+str(len(data))+" bytes]\t"+datastring); if rnode_instance.write_to_disk: try: filename = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S.%f")+".pkt" file = open(rnode_instance.write_dir+"/"+filename, "wb") file.write(data) file.close() except Exception as e: RNS.log("Error while writing packet to disk") os._exit(number_of_packets_received_exit_code) def main(): try: if not util.find_spec("serial"): raise ImportError("Serial module could not be found") except ImportError: print("") print("RNode Config Utility needs pyserial to work.") print("You can install it with: pip3 install pyserial") print("") exit(number_of_packets_received_exit_code) try: parser = argparse.ArgumentParser(description="LoRa packet sniffer for RNode hardware.") parser.add_argument("-C", "--console", action="store_true", help="Print captured packets to the console") parser.add_argument("-H", "--hex", action="store_true", help="Print out packets as hexadecimal") parser.add_argument("-W", action="store", metavar="directory", type=str, default=None, help="Write captured packets to a directory") parser.add_argument("--freq", action="store", metavar="Hz", type=int, default=None, help="Frequency in Hz") parser.add_argument("--bw", action="store", metavar="Hz", type=int, default=None, help="Bandwidth in Hz") parser.add_argument("--txp", action="store", metavar="dBm", type=int, default=None, help="TX power in dBm") parser.add_argument("--sf", action="store", metavar="factor", type=int, default=None, help="Spreading factor") parser.add_argument("--cr", action="store", metavar="rate", type=int, default=None, help="Coding rate") parser.add_argument("--implicit", action="store", metavar="length", type=int, default=None, help="Packet length in implicit header mode") parser.add_argument("--duration", action="store", metavar="seconds", type=int, default=0,help="Duration of time to capture packets") parser.add_argument("-Q", action="store_true", help="Quite mode, no logging") parser.add_argument("-R", action="store_true", help="Raw frame mode") parser.add_argument("-P", action="store_true", help="Set promiscuous mode") parser.add_argument("-U", action="store_true", help="Use a URWID based UI") parser.add_argument("port", nargs="?", default=None, help="Serial port where RNode is attached", type=str) args = parser.parse_args() console_output = False write_to_disk = False write_dir = None if args.duration and args.U: print("UI mode and timed capture are not compatible") exit(number_of_packets_received_exit_code) if args.Q and args.U: print("Quite mode and timed capture are not compatible") exit(number_of_packets_received_exit_code) if args.port: if not args.Q: RNS.log("Opening serial port "+args.port+"...") rnode = None serial_device = None rnode_baudrate = 115200 try: serial_device = serial.Serial( port = args.port, baudrate = rnode_baudrate, bytesize = 8, parity = serial.PARITY_NONE, stopbits = 1, xonxoff = False, rtscts = False, timeout = 0, inter_byte_timeout = None, write_timeout = None, dsrdtr = False ) except Exception as e: RNS.log("Could not open the specified serial port. The contained exception was:") RNS.log(str(e)) exit(number_of_packets_received_exit_code) else: print("") parser.print_help() print("") exit(number_of_packets_received_exit_code) # create an RNS Serial device that uses locking for read / write rns_serial = RNSSerial(serial_device) # create the RNode object and give it the lockable serial device rnode = RNode(rns_serial) if args.console: console_output = True if args.W: if not os.path.isdir(args.W): try: os.mkdir(args.W) write_to_disk = True write_dir = args.W except Exception as e: RNS.log("Could not open or create specified directory") else: write_to_disk = True write_dir = args.W if args.freq: rnode.frequency = args.freq else: RNS.log("Freq is a required parameter.") exit(number_of_packets_received_exit_code) if args.bw: rnode.bandwidth = args.bw else: RNS.log("BW is a required parameter.") exit(number_of_packets_received_exit_code) if args.txp and (args.txp >= 0 and args.txp <= 17): rnode.txpower = args.txp else: rnode.txpower = 0 if args.sf: rnode.sf = args.sf else: RNS.log("SF is a required parameter.") exit(number_of_packets_received_exit_code) if args.cr: rnode.cr = args.cr else: RNS.log("CR is a required parameter.") exit(number_of_packets_received_exit_code) if args.implicit: rnode.implicit_length = args.implicit else: rnode.implicit_length = 0 if args.hex: rnode.print_hex = True else: rnode.print_hex = False if args.P: rnode.promiscuous = True else: rnode.promiscuous = False if (args.R): rnode.setPrintRawDataBytes(True) if not args.W and not args.console: RNS.log("Warning! No output destination specified! You won't see any captured packets.") #turn off all logging until we probe the RNode device #we don't want leftover packets, data, etc. to spew out RNS.log_enabled = False #Get the background thread going that will read from radio and help detect it rnode.callback = packet_captured rnode.console_output = console_output rnode.write_to_disk = write_to_disk rnode.write_dir = write_dir #set the flag that keeps the thread running rnode.thread_continue = True thread = threading.Thread(target=rnode.packetReadLoop) thread.daemon = True thread.start() #kick off a probe if rnode.device_probe() == False: print("Serial port opened, but RNode did not respond.") rnode.thread_continue = False else: #decide whether we are running on console, or URWID based UI if (args.U): #enable logging again, now that radio is detected #don't look at args.Q, it's not compatable with UI mode RNS.log_enabled = True #UI Mode RNS.queue_to_ui = queue.Queue() rnode.queue_from_ui = queue.Queue() loramon_ui_app = LoRaMonUIApp(RNS.queue_to_ui, rnode.queue_from_ui) rnode.loramon_ui_app = loramon_ui_app #initialize the radio rnode.initRadio() #set the setting for print raw data rnode.updateIUApp("print_raw_data", args.R) loramon_ui_app.run() rnode.thread_continue = False None else: #Console mode if args.Q: RNS.log_enabled = False else: RNS.log_enabled = True #initialize the radio rnode.initRadio() # set the duration here, after radio has been initialized # duration mode is only available in console mode rnode.setCapturDuration(args.duration) RNS.log(f"Capture Duration {rnode.duration_to_capture_for}") #program is ending, by the it gets here #wait for the thread to finish thread.join() exit(number_of_packets_received_exit_code) except KeyboardInterrupt: print("") exit(number_of_packets_received_exit_code) if __name__ == "__main__": main()