def test_receive(self): profile = SigfoxProfile("UPLINK", "ACK ON ERROR", 1) ack = "0000111111111111100000000000000000000000000000000000000000000000" ack_index_dtag = profile.RULE_ID_SIZE ack_index_w = ack_index_dtag + profile.T ack_index_c = ack_index_w + profile.M ack_index_bitmap = ack_index_c + 1 ack_index_padding = ack_index_bitmap + profile.BITMAP_SIZE received_ack = ACK(profile, rule_id=ack[:ack_index_dtag], dtag=ack[ack_index_dtag:ack_index_w], w=ack[ack_index_w:ack_index_c], c=ack[ack_index_c], bitmap=ack[ack_index_bitmap:ack_index_padding], padding=ack[ack_index_padding:]) self.assertTrue(received_ack.is_receiver_abort())
def test_from_hex(self): ack = ACK.parse_from_hex(SigfoxProfile("UPLINK", "ACK ON ERROR", 1), "07ff800000000000") self.assertEqual( ack.to_string(), "0000011111111111100000000000000000000000000000000000000000000000") self.assertEqual(ack.HEADER.RULE_ID, "00") self.assertEqual(ack.HEADER.DTAG, "0") self.assertEqual(ack.HEADER.W, "00") self.assertEqual(ack.HEADER.C, "1") self.assertTrue(ack.is_receiver_abort())
def test_from_hex(self): ack = ACK.parse_from_hex(Sigfox("UPLINK", "ACK ON ERROR", 1), "07ff800000000000") self.assertEqual( ack.to_string(), "0000011111111111100000000000000000000000000000000000000000000000") self.assertEqual(ack.rule_id, "00") self.assertEqual(ack.dtag, "0") self.assertEqual(ack.w, "00") self.assertEqual(ack.c, "1") self.assertTrue(ack.is_receiver_abort())
def test_from_hex(self): profile = SigfoxProfile("UPLINK", "ACK ON ERROR", 1) h = "0f08000000000000" ack = ACK.parse_from_hex(profile, h) self.assertEqual( ack.to_string(), "0000111100001000000000000000000000000000000000000000000000000000") self.assertEqual(ack.HEADER.RULE_ID, "00") self.assertEqual(ack.HEADER.DTAG, "0") self.assertEqual(ack.HEADER.W, "01") self.assertEqual(ack.HEADER.C, "1") self.assertEqual(ack.BITMAP, "1100001") self.assertTrue(is_monochar(ack.PADDING) and ack.PADDING[0] == '0')
def test_from_hex(self): profile = Sigfox("UPLINK", "ACK ON ERROR", 1) h = "0f08000000000000" ack = ACK.parse_from_hex(profile, h) self.assertEqual( ack.to_string(), "0000111100001000000000000000000000000000000000000000000000000000") self.assertEqual(ack.rule_id, "00") self.assertEqual(ack.dtag, "0") self.assertEqual(ack.w, "01") self.assertEqual(ack.c, "1") self.assertEqual(ack.bitmap, "1100001") self.assertTrue(is_monochar(ack.padding) and ack.padding[0] == '0')
def recv_mask(self): received = self.recv(self.PROFILE.DOWNLINK_MTU // 8) ack = ACK.parse_from_hex(self.PROFILE, received) attempts = 1 if self.ATTEMPTS == 0 else self.ATTEMPTS window_mask = self.LOSS_MASK["ack"][str(ack.HEADER.WINDOW_NUMBER)] if window_mask[attempts - 1] != '0': print("[RECV] Fragment lost") self.LOSS_MASK["ack"][str(ack.HEADER.WINDOW_NUMBER)] = replace_bit(window_mask, attempts - 1, str(int(window_mask[attempts - 1]) - 1)) raise SCHCTimeoutError else: return received
def hello_get(request): """HTTP Cloud Function. Args: request (flask.Request): The request object. <http://flask.pocoo.org/docs/1.0/api/#flask.Request> Returns: The response text, or any set of values that can be turned into a Response object using `make_response` <http://flask.pocoo.org/docs/1.0/api/#flask.Flask.make_response>. """ # Wait for an HTTP POST request. if request.method == 'POST': # Get request JSON. print("POST RECEIVED") request_dict = request.get_json() print('Received Sigfox message: {}'.format(request_dict)) # Get data and Sigfox Sequence Number. fragment = request_dict["data"] sigfox_sequence_number = request_dict["seqNumber"] # Initialize Cloud Storage variables. BUCKET_NAME = config.BUCKET_NAME # Parse fragment into "fragment = [header, payload] header_bytes = None header_first_hex = fragment[:1] if (header_first_hex) == '0' or (header_first_hex) =='1': header = bytes.fromhex(fragment[:2]) payload = bytearray.fromhex(fragment[2:]) header_bytes = 1 elif (header_first_hex) == '2': header = bytearray.fromhex(fragment[:4]) payload = bytearray.fromhex(fragment[4:]) header_bytes = 2 else: print("Wrong header in fragment") return 'wrong header', 204 if bytearray.fromhex(fragment[2:]).decode() == "CLEAN": try: _ = requests.post( url=config.CLEAN_URL, json={"header_bytes": header_bytes, "from_lopy": "True"}, timeout=0.1) except requests.exceptions.ReadTimeout: pass return '', 204 elif bytearray.fromhex(fragment[2:]).decode() == "CLEAN_ALL": try: _ = requests.post( url=config.CLEAN_URL, json={"header_bytes": header_bytes, "from_lopy": "True"}, timeout=0.1) except requests.exceptions.ReadTimeout: pass return '', 204 data = [header, payload] # Initialize SCHC variables. profile_uplink = Sigfox("UPLINK", "ACK ON ERROR", header_bytes) profile_downlink = Sigfox("DOWNLINK", "NO ACK", header_bytes) buffer_size = profile_uplink.MTU n = profile_uplink.N m = profile_uplink.M # If fragment size is greater than buffer size, ignore it and end function. if len(fragment) / 2 * 8 > buffer_size: # Fragment is hex, 1 hex = 1/2 byte return json.dumps({"message": "Fragment size is greater than buffer size D:"}), 200 # If the folder named "all windows" does not exist, create it along with all subdirectories. if not exists_blob("all_windows/"): print("INITIALIZING... (be patient)") create_folder("all_windows/") # For each window in the SCHC Profile, create its blob. for i in range(2 ** m): create_folder("all_windows/window_%d/" % i) # For each fragment in the SCHC Profile, create its blob. for j in range(2 ** n - 1): upload_blob("", "all_windows/window_%d/fragment_%d_%d" % (i, i, j)) # Create the blob for each bitmap. if not exists_blob("all_windows/window_%d/bitmap_%d" % (i, i) or size_blob("all_windows/window_%d/bitmap_%d" % (i, i)) == 0): bitmap = "" for b in range(profile_uplink.BITMAP_SIZE): bitmap += "0" upload_blob(bitmap, "all_windows/window_%d/bitmap_%d" % (i, i)) print("BLOBs created") # Find current experiment number current_experiment = 0 for blob in blob_list(): if blob.startswith("DL_LOSSES_"): current_experiment += 1 print(f"This is the {current_experiment}th experiment.") # Initialize empty window window = [] for i in range(2 ** n - 1): window.append([b"", b""]) # Compute the fragment compressed number (FCN) from the Profile fcn_dict = {} for j in range(2 ** n - 1): fcn_dict[zfill(bin((2 ** n - 2) - (j % (2 ** n - 1)))[2:], n)] = j # Convert to a Fragment class for easier manipulation. fragment_message = Fragment(profile_uplink, data) if 'enable_losses' in request_dict: if request_dict['enable_losses'] == "True": loss_rate = request_dict["loss_rate"] # loss_rate = 10 coin = random.random() print('loss rate: {}, random toss:{}'.format(loss_rate, coin * 100)) if coin * 100 < loss_rate: print("[LOSS] The fragment was lost.") if fragment_message.is_all_1(): last_sequence_number = read_blob("SSN") print("SSN is {} and last SSN is {}".format(sigfox_sequence_number, last_sequence_number)) if int(sigfox_sequence_number) - int(last_sequence_number) == 1: # We do that to save the last SSN value for future use (when the next All-1 Arrives) # In a Real Loss Scenario we will not know the SSN... upload_blob(sigfox_sequence_number, "SSN") return 'fragment lost', 204 # Get current window for this fragment. current_window = int(fragment_message.header.W, 2) # Get the current bitmap. bitmap = read_blob("all_windows/window_%d/bitmap_%d" % (current_window, current_window)) # Try getting the fragment number from the FCN dictionary. try: fragment_number = fcn_dict[fragment_message.header.FCN] upload_blob(fragment_number, "fragment_number") time_received = int(request_dict["time"]) if exists_blob("timestamp"): # Check time validation. last_time_received = int(read_blob("timestamp")) # If this is not the very first fragment and the inactivity timer has been reached, ignore the message. # TODO: Send SCHC abort message. if str(fragment_number) != "0" and str( current_window) != "0" and time_received - last_time_received > profile_uplink.INACTIVITY_TIMER_VALUE: return json.dumps({"message": "Inactivity timer reached. Message ignored."}), 200 # Upload current timestamp. upload_blob(time_received, "timestamp") # Print some data for the user. print("[RECV] This corresponds to the " + str(fragment_number) + "th fragment of the " + str( current_window) + "th window.") print("[RECV] Sigfox sequence number: " + str(sigfox_sequence_number)) #Controlled Errors check # losses_mask = read_blob(BUCKET_NAME, "all_windows/window_%d/losses_mask_%d" % (current_window, current_window)) # if (losses_mask[fragment_number]) != '0': # losses_mask = replace_bit(losses_mask, fragment_number, str(int(losses_mask[fragment_number])-1)) # upload_blob(BUCKET_NAME, losses_mask, "all_windows/window_%d/losses_mask_%d" % (current_window, current_window)) # print("[LOSS] The fragment was lost.") # return 'fragment lost', 204 # Update bitmap and upload it. bitmap = replace_bit(bitmap, fragment_number, '1') upload_blob(bitmap, "all_windows/window_%d/bitmap_%d" % (current_window, current_window)) # Upload the fragment data. upload_blob(data[0].decode("ISO-8859-1") + data[1].decode("utf-8"), "all_windows/window_%d/fragment_%d_%d" % (current_window, current_window, fragment_number)) # If the FCN could not been found, it almost certainly is the final fragment. except KeyError: print("[RECV] This seems to be the final fragment.") # Upload current timestamp. time_received = int(request_dict["time"]) upload_blob(time_received, "timestamp") print("is All-1:{}, is All-0:{}".format(fragment_message.is_all_1(), fragment_message.is_all_0())) # print("RULE_ID: {}, W:{}, FCN:{}".format(fragment.header.RULE_ID, fragment.header.W, fragment.header.FCN)) # Update bitmap and upload it. bitmap = replace_bit(bitmap, len(bitmap) - 1, '1') upload_blob(bitmap, "all_windows/window_%d/bitmap_%d" % (current_window, current_window)) # Get some SCHC values from the fragment. rule_id = fragment_message.header.RULE_ID dtag = fragment_message.header.DTAG w = fragment_message.header.W # Get last and current Sigfox sequence number (SSN) last_sequence_number = 0 if exists_blob("SSN"): last_sequence_number = read_blob("SSN") upload_blob(sigfox_sequence_number, "SSN") # If the fragment is at the end of a window (ALL-0 or ALL-1) if fragment_message.is_all_0() or fragment_message.is_all_1(): # Prepare the ACK bitmap. Find the first bitmap with a 0 in it. for i in range(current_window + 1): bitmap_ack = read_blob("all_windows/window_%d/bitmap_%d" % (i, i)) print(bitmap_ack) window_ack = i if '0' in bitmap_ack: break # If the ACK bitmap has a 0 at the end of a non-final window, a fragment has been lost. if fragment_message.is_all_0() and '0' in bitmap_ack: if 'enable_dl_losses' in request_dict: if request_dict['enable_dl_losses'] == "True": coin = random.random() print('loss rate: {}, random toss:{}'.format(loss_rate, coin * 100)) if coin * 100 < loss_rate: print("[LOSS-ALL0] The Downlink NACK was lost.") upload_blob(read_blob(f"DL_LOSSES_{current_experiment}") + "\n Lost DL message in window {}".format(current_window), f"DL_LOSSES_{current_experiment}") return 'Downlink lost', 204 print("[ALL0] Sending ACK for lost fragments...") print("bitmap with errors -> {}".format(bitmap_ack)) # Create an ACK message and send it. ack = ACK(profile_downlink, rule_id, dtag, zfill(format(window_ack, 'b'), m), bitmap_ack, '0') response_json = send_ack(request_dict, ack) print("Response content -> {}".format(response_json)) return response_json, 200 # If the ACK bitmap is complete and the fragment is an ALL-0, send an ACK # This is to be modified, as ACK-on-Error does not need an ACK for every window. if fragment_message.is_all_0() and bitmap[0] == '1' and all(bitmap): print("[ALL0] All Fragments of current window received") print("[ALL0] No need to send an ACK") # print("[ALLX] Sending ACK after window...") # Create an ACK message and send it. # ack = ACK(profile_downlink, rule_id, dtag, w, bitmap, '0') # response_json = send_ack(request_dict, ack) # print("200, Response content -> {}".format(response_json)) # Response to continue, no ACK is sent Back. return '', 204 # return response_json, 200 # If the fragment is an ALL-1 if fragment_message.is_all_1(): # response = {request_dict['device']: {'downlinkData': '080fffffffffffff'}} # print("response -> {}".format(json.dumps(response))) # return json.dumps(response), 200 # The bitmap in the last window follows the following regular expression: "1*0*1*" # Since the ALL-1, if received, changes the least significant bit of the bitmap. # For a "complete" bitmap in the last window, there shouldn't be non-consecutive zeroes: # 1110001 is a valid bitmap, 1101001 is not. pattern2 = re.compile("0*1") if pattern2.fullmatch(bitmap_ack): if 'enable_dl_losses' in request_dict: if request_dict['enable_dl_losses'] == "True": coin = random.random() print('loss rate: {}, random toss:{}'.format(loss_rate, coin * 100)) if coin * 100 < loss_rate: print("[LOSS-ALL1] The Downlink ACK was lost.") upload_blob(read_blob(f"DL_LOSSES_{current_experiment}") + "\n Lost DL message in window {}".format(current_window), f"DL_LOSSES_{current_experiment}") return 'Downlink lost', 204 print("SSN is {} and last SSN is {}".format(sigfox_sequence_number, last_sequence_number)) # Downlink Controlled Errors dl_errors = int(read_blob("dl_errors")) if dl_errors == 0: last_index = 0 upload_blob(data[0].decode("ISO-8859-1") + data[1].decode("utf-8"), "all_windows/window_%d/fragment_%d_%d" % ( current_window, current_window, last_index)) print("Info for reassemble: last_index:{}, current_window:{}".format(last_index, current_window)) try: print('Activating reassembly process...') _ = requests.post( url=config.REASSEMBLE_URL, json={"last_index": last_index, "current_window": current_window, "header_bytes": header_bytes}, timeout=0.1) except requests.exceptions.ReadTimeout: pass # Send last ACK to end communication. print("[ALL1] Reassembled: Sending last ACK") bitmap = '' for k in range(profile_uplink.BITMAP_SIZE): bitmap += '0' last_ack = ACK(profile_downlink, rule_id, dtag, w, bitmap, '1') response_json = send_ack(request_dict, last_ack) # return response_json, 200 # response_json = send_ack(request_dict, last_ack) print("200, Response content -> {}".format(response_json)) return response_json, 200 else: dl_errors -= 1 upload_blob(dl_errors, "dl_errors") print("[DL-ERROR] We simulate a downlink error. We don't send an ACK") return '', 204 pattern = re.compile("1*0*1") # If the bitmap matches the regex, check if the last two received fragments are consecutive. if pattern.fullmatch(bitmap_ack): print("SSN is {} and last SSN is {}".format(sigfox_sequence_number,last_sequence_number)) # If the last two received fragments are consecutive, accept the ALL-1 and start reassembling if 'enable_dl_losses' in request_dict: if request_dict['enable_dl_losses'] == "True": coin = random.random() print('loss rate: {}, random toss:{}'.format(loss_rate, coin * 100)) if coin * 100 < loss_rate: print("[LOSS-ALL1] The Downlink ACK was lost.") upload_blob(read_blob(f"DL_LOSSES_{current_experiment}") + "\n Lost DL message in window {}".format(current_window), f"DL_LOSSES_{current_experiment}") return 'Downlink lost', 204 if int(sigfox_sequence_number) - int(last_sequence_number) == 1: # Downlink Controlled Errors dl_errors = int(read_blob("dl_errors")) if dl_errors == 0: last_index = int(read_blob("fragment_number")) + 1 upload_blob(data[0].decode("ISO-8859-1") + data[1].decode("utf-8"), "all_windows/window_%d/fragment_%d_%d" % ( current_window, current_window, last_index)) print("Info for reassemble: last_index:{}, current_window:{}".format(last_index,current_window)) try: print('Activating reassembly process...') _ = requests.post(url=config.REASSEMBLE_URL, json={"last_index": last_index, "current_window": current_window, "header_bytes": header_bytes}, timeout=0.1) except requests.exceptions.ReadTimeout: pass # Send last ACK to end communication. print("[ALL1] Reassembled: Sending last ACK") bitmap = '' for k in range(profile_uplink.BITMAP_SIZE): bitmap += '0' last_ack = ACK(profile_downlink, rule_id, dtag, w, bitmap, '1') response_json = send_ack(request_dict, last_ack) # return response_json, 200 # response_json = send_ack(request_dict, last_ack) print("200, Response content -> {}".format(response_json)) return response_json, 200 else: dl_errors -= 1 upload_blob(dl_errors, "dl_errors") print("[DL-ERROR] We simulate a downlink error. We don't send an ACK") return '', 204 else: # Send NACK at the end of the window. if 'enable_dl_losses' in request_dict: if request_dict['enable_dl_losses'] == "True": coin = random.random() print('loss rate: {}, random toss:{}'.format(loss_rate, coin * 100)) if coin * 100 < loss_rate: print("[LOSS-ALL1] The Downlink NACK was lost.") upload_blob(read_blob(f"DL_LOSSES_{current_experiment}") + "\n Lost DL message in window {}".format(current_window), f"DL_LOSSES_{current_experiment}") return 'Downlink lost', 204 print("[ALLX] Sending NACK for lost fragments because of SSN...") ack = ACK(profile_downlink, rule_id, dtag, zfill(format(window_ack, 'b'), m), bitmap_ack, '0') response_json = send_ack(request_dict, ack) return response_json, 200 # If they are not, there is a gap between two fragments: a fragment has been lost. # The same happens if the bitmap doesn't match the regex. else: # Send NACK at the end of the window. if 'enable_dl_losses' in request_dict: if request_dict['enable_dl_losses'] == "True": coin = random.random() print('loss rate: {}, random toss:{}'.format(loss_rate, coin * 100)) if coin * 100 < loss_rate: print("[LOSS-ALL1] The Downlink NACK was lost.") upload_blob(read_blob(f"DL_LOSSES_{current_experiment}") + "\n Lost DL message in window {}".format(current_window), f"DL_LOSSES_{current_experiment}") return 'Downlink lost', 204 print("[ALLX] Sending NACK for lost fragments...") ack = ACK(profile_downlink, rule_id, dtag, zfill(format(window_ack, 'b'), m), bitmap_ack, '0') response_json = send_ack(request_dict, ack) return response_json, 200 return '', 204 else: print('Invalid HTTP Method to invoke Cloud Function. Only POST supported') return abort(405)
# If the fragment is at the end of a window (All-0 or All-1). if fragment_message.is_all_0() or fragment_message.is_all_1(): ack_has_been_sent = False print(bitmap) # Check for lost fragments in the bitmap. if '0' in bitmap: number_of_lost_fragments = bitmap.count('0') indices = find(bitmap, '0') print(indices) # Create an ACK object and send it as bytes to the sender. print("[ALLX] Sending NACK for lost fragments...") ack = ACK(profile_downlink, rule_id, dtag, w, bitmap, '0') print(ack.to_string()) the_socket.sendto(ack.to_bytes(), address) ack_has_been_sent = True # For every lost fragment: for j in range(number_of_lost_fragments): index = indices[j] # Try recovering the index-th fragment try: print("[ALLX] Recovering " + str(index) + "th fragment...") fragment_recovered, address = the_socket.recvfrom(buffer_size) data_recovered = [bytes([fragment_recovered[0]]), bytearray(fragment_recovered[1:])] fragment_message_recovered = Fragment(profile_uplink, data_recovered) fragment_number_recovered = fcn_dict[fragment_message_recovered.header.FCN]
def schc_post(): """HTTP Cloud Function. Args: request (flask.Request): The request object. <http://flask.pocoo.org/docs/1.0/api/#flask.Request> Returns: The response text, or any set of values that can be turned into a Response object using `make_response` <http://flask.pocoo.org/docs/1.0/api/#flask.Flask.make_response>. """ REASSEMBLER_URL = "http://localhost:5000/reassembler" CLEANUP_URL = "http://localhost:5000/cleanup" # File where we will store authentication credentials after acquiring them. os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = config.CLIENT_SECRETS_FILE # Wait for an HTTP POST request. if request.method == 'POST': # Get request JSON. print("POST RECEIVED") request_dict = request.get_json() print('Received Sigfox message: {}'.format(request_dict)) # Get data and Sigfox Sequence Number. raw_data = request_dict["data"] sigfox_sequence_number = request_dict["seqNumber"] ack_req = request_dict["ack"] # Initialize Cloud Storage variables. BUCKET_NAME = config.BUCKET_NAME header_first_hex = raw_data[:1] if header_first_hex == '0' or header_first_hex == '1': header = bytes.fromhex(raw_data[:2]) payload = bytearray.fromhex(raw_data[2:]) header_bytes = 1 elif header_first_hex == '2': header = bytearray.fromhex(raw_data[:4]) payload = bytearray.fromhex(raw_data[4:]) header_bytes = 2 else: print("Wrong header in raw_data") return 'wrong header', 204 # Initialize SCHC variables. profile = Sigfox("UPLINK", "ACK ON ERROR", header_bytes) n = profile.N m = profile.M # If fragment size is greater than buffer size, ignore it and end function. if len( raw_data ) / 2 * 8 > profile.UPLINK_MTU: # Fragment is hex, 1 hex = 1/2 byte return json.dumps( {"message": "Fragment size is greater than buffer size"}), 200 # If the folder named "all windows" does not exist, create it along with all subdirectories. initialize_blobs(BUCKET_NAME, profile) # Compute the fragment compressed number (FCN) from the Profile fcn_dict = {} for j in range(2**n - 1): fcn_dict[zfill(bin((2**n - 2) - (j % (2**n - 1)))[2:], 3)] = j # Parse raw_data into "data = [header, payload] # Convert to a Fragment class for easier manipulation. header = bytes.fromhex(raw_data[:2]) payload = bytearray.fromhex(raw_data[2:]) data = [header, payload] fragment_message = Fragment(profile, data) if fragment_message.is_sender_abort(): print("Sender-Abort received") try: print("Cleaning") _ = requests.post(url=CLEANUP_URL, json={"header_bytes": header_bytes}, timeout=0.1) except requests.exceptions.ReadTimeout: pass return 'Sender-Abort received', 204 # Get data from this fragment. fcn = fragment_message.header.FCN rule_id = fragment_message.header.RULE_ID dtag = fragment_message.header.DTAG current_window = int(fragment_message.header.W, 2) # Get the current bitmap. bitmap = read_blob( BUCKET_NAME, f"all_windows/window_{current_window}/bitmap_{current_window}") # Controlling deterministic losses. This loads the file "loss_mask.txt" which states when should a fragment be # lost, separated by windows. fd = None try: fd = open(config.LOSS_MASK_MODIFIED, "r") except FileNotFoundError: fd = open(config.LOSS_MASK, "r") finally: loss_mask = [] for line in fd: if not line.startswith("#"): for char in line: try: loss_mask.append(int(char)) except ValueError: pass fd.close() print(f"Loss mask: {loss_mask}") # Controlling random losses. if 'enable_losses' in request_dict and not ( fragment_message.is_all_0() or fragment_message.is_all_1()): if request_dict['enable_losses']: loss_rate = request_dict["loss_rate"] # loss_rate = 10 coin = random.random() print(f'loss rate: {loss_rate}, random toss:{coin * 100}') if coin * 100 < loss_rate: print("[LOSS] The fragment was lost.") return 'fragment lost', 204 # Check if the fragment is an All-1 if is_monochar(fcn) and fcn[0] == '1': print("[RECV] This is an All-1.") # Check if fragment is to be lost (All-1 is the very last fragment) if loss_mask[-1] != 0: loss_mask[-1] -= 1 with open("loss_mask_modified.txt", "w") as fd: for i in loss_mask: fd.write(str(i)) print(f"[RECV] Fragment lost.") return 'fragment lost', 204 # Inactivity timer validation time_received = int(request_dict["time"]) if exists_blob(BUCKET_NAME, "timestamp"): # Check time validation. last_time_received = int(read_blob(BUCKET_NAME, "timestamp")) print(f"[RECV] Previous timestamp: {last_time_received}") print(f"[RECV] This timestamp: {time_received}") # If the inactivity timer has been reached, abort communication. if time_received - last_time_received > profile.INACTIVITY_TIMER_VALUE: print("[RECV] Inactivity timer reached. Ending session.") receiver_abort = ReceiverAbort(profile, fragment_message.header) print("Sending Receiver Abort") response_json = send_ack(request_dict, receiver_abort) print(f"Response content -> {response_json}") try: print("Cleaning Inactivity timer reached") _ = requests.post(url=CLEANUP_URL, json={"header_bytes": header_bytes}, timeout=0.1) except requests.exceptions.ReadTimeout: pass return response_json, 200 # Update timestamp upload_blob(BUCKET_NAME, time_received, "timestamp") # Update bitmap and upload it. bitmap = replace_bit(bitmap, len(bitmap) - 1, '1') print(f"Bitmap is now {bitmap}") upload_blob( BUCKET_NAME, bitmap, f"all_windows/window_{current_window}/bitmap_{current_window}") # Upload the fragment data. upload_blob( BUCKET_NAME, data[0].decode("utf-8") + data[1].decode("utf-8"), f"all_windows/window_{current_window}/fragment_{current_window}_{profile.WINDOW_SIZE - 1}" ) # Else, it is a normal fragment. else: fragment_number = fcn_dict[fragment_message.header.FCN] # Check if fragment is to be lost position = current_window * profile.WINDOW_SIZE + fragment_number if loss_mask[position] != 0: loss_mask[position] -= 1 with open(config.LOSS_MASK_MODIFIED, "w") as fd: for i in loss_mask: fd.write(str(i)) print(f"[RECV] Fragment lost.") return 'fragment lost', 204 # Inactivity timer validation time_received = int(request_dict["time"]) if exists_blob(BUCKET_NAME, "timestamp"): # Check time validation. last_time_received = int(read_blob(BUCKET_NAME, "timestamp")) print(f"[RECV] Previous timestamp: {last_time_received}") print(f"[RECV] This timestamp: {time_received}") # If the inactivity timer has been reached, abort communication. if time_received - last_time_received > profile.INACTIVITY_TIMER_VALUE: print("[RECV] Inactivity timer reached. Ending session.") receiver_abort = ReceiverAbort(profile, fragment_message.header) print("Sending Receiver Abort") response_json = send_ack(request_dict, receiver_abort) print(f"Response content -> {response_json}") try: _ = requests.post(url=CLEANUP_URL, json={"header_bytes": header_bytes}, timeout=0.1) except requests.exceptions.ReadTimeout: pass return response_json, 200 # Update timestamp upload_blob(BUCKET_NAME, time_received, "timestamp") # Update Sigfox sequence number JSON sequence_numbers = json.loads(read_blob(BUCKET_NAME, "SSN")) sequence_numbers[position] = request_dict["seqNumber"] print(sequence_numbers) upload_blob(BUCKET_NAME, json.dumps(sequence_numbers), "SSN") upload_blob(BUCKET_NAME, fragment_number, "fragment_number") # Print some data for the user. print( f"[RECV] This corresponds to the {str(fragment_number)}th fragment " f"of the {str(current_window)}th window.") print( f"[RECV] Sigfox sequence number: {str(sigfox_sequence_number)}" ) # Update bitmap and upload it. bitmap = replace_bit(bitmap, fragment_number, '1') print(f"Bitmap is now {bitmap}") upload_blob( BUCKET_NAME, bitmap, f"all_windows/window_{current_window}/bitmap_{current_window}") # Upload the fragment data. upload_blob( BUCKET_NAME, data[0].decode("utf-8") + data[1].decode("utf-8"), f"all_windows/window_{current_window}/fragment_{current_window}_{fragment_number}" ) # If the fragment requests an ACK... if ack_req: # Prepare the ACK bitmap. Find the first bitmap with a 0 in it. # This bitmap corresponds to the lowest-numered window with losses. bitmap_ack = None window_ack = None for i in range(current_window + 1): bitmap_ack = read_blob(BUCKET_NAME, f"all_windows/window_{i}/bitmap_{i}") print(bitmap_ack) window_ack = i if '0' in bitmap_ack: break # The final window is only accessible through All-1. # If All-0, check non-final windows if fragment_message.is_all_0(): # If the ACK bitmap has a 0 at a non-final window, a fragment has been lost. if '0' in bitmap_ack: print( "[ALL0] Lost fragments have been detected. Preparing ACK." ) print(f"[ALL0] Bitmap with errors -> {bitmap_ack}") ack = ACK(profile=profile, rule_id=rule_id, dtag=dtag, w=zfill(format(window_ack, 'b'), m), c='0', bitmap=bitmap_ack) response_json = send_ack(request_dict, ack) print(f"Response content -> {response_json}") print("[ALL0] ACK sent.") return response_json, 200 # If all bitmaps are complete up to this point, no losses are detected. else: print("[ALL0] No losses have been detected.") print("Response content -> ''") return '', 204 # If the fragment is All-1, the last window should be considered. if fragment_message.is_all_1(): # First check for 0s in the bitmap. If the bitmap is of a non-final window, send corresponding ACK. if current_window != window_ack and '0' in bitmap_ack: print( "[ALL1] Lost fragments have been detected. Preparing ACK." ) print(f"[ALL1] Bitmap with errors -> {bitmap_ack}") ack = ACK(profile=profile, rule_id=rule_id, dtag=dtag, w=zfill(format(window_ack, 'b'), m), c='0', bitmap=bitmap_ack) response_json = send_ack(request_dict, ack) print(f"Response content -> {response_json}") print("[ALL1] ACK sent.") return response_json, 200 # If the bitmap is of the final window, check the following regex. else: # The bitmap in the last window follows the following regular expression: "1*0*1*" # Since the ALL-1, if received, changes the least significant bit of the bitmap. # For a "complete" bitmap in the last window, there shouldn't be non-consecutive zeroes: # 1110001 is a valid bitmap, 1101001 is not. # The bitmap may or may not contain the 0s. pattern = re.compile("1*0*1") # If the bitmap matches the regex, check if there are still lost fragments. if pattern.fullmatch(bitmap_ack) is not None: # The idea is the following: # Assume a fragment has been lost, but the regex has been matched. # For example, we want a bitmap 1111111 but due to a loss we have 1111101. # This incomplete bitmap matches the regex. # We should note that here the SSN of the All-1 and the penultimate fragment received # are not consecutive. # Retransmitting the lost fragment and resending the All-1 solves that problem. # There is another problematic case: we want a bitmap 1111111 but due to losses we have 1111001. # If the second of those two lost fragments is retransmitted, the new bitmap, 1111011, does not # match the regex. If, instead, the first of those fragments is retransmitted, the new bitmap # 1111101 does match the regex. As the sender should retransmit these messages sequentially, # the SSN of the resent All-1 and the penultimate fragment are still not consecutive. # The only way for these two SSNs to be consecutive in these cases # is that the penultimate fragment fills the bitmap in the 6th bit, # and the last fragment is the All-1. # This reasoning is still valid when the last window does not contain WINDOW_SIZE fragments. # These particular cases validate the use for this regex matching. # Remember that 1111011 is NOT a valid bitmap. # In conclusion, AFTER the regex matching, # we should check if the SSNs of the two last received fragments are consecutive. # The second to last fragment has the highest SSN registered in the JSON. # TODO: What happens when the All-0 prior to the last window is lost and is retransmitted with the All-1? # We should consider only the SSNs of the last window. If there is a retransmission in a window # prior to the last, the reasoning fails since the All-1 is always consecutive to a # retransmitted fragment of a non-final window. # If the All-1 is the only fragment of the last window (bitmap 0000001), and bitmap check of # prior windows has passed, check the consecutiveness of the last All-0 and the All-1. sequence_numbers = json.loads( read_blob(BUCKET_NAME, "SSN")) # This array has the SSNs of the last window. # last_window_ssn = list(sequence_numbers.values())[current_window * profile.WINDOW_SIZE + 1:] # If this array is empty, no messages have been received in the last window. Check if the # last All-0 and the All-1 are consecutive. If they are not, there are lost fragments. If they # are, the All-0 may have been retransmitted. # print(last_window_ssn) # The last sequence number should be the highest of these values. last_sequence_number = max( list(map(int, list(sequence_numbers.values())))) # TODO: If the All-0 has the highest of these values, it may have been retransmitted using the All-1 print( f"All-1 sequence number {sigfox_sequence_number}") print(f"Last sequence number {last_sequence_number}") if int(sigfox_sequence_number) - int( last_sequence_number) == 1: print( "[ALL1] Integrity checking complete, launching reassembler." ) # All-1 does not define a fragment number, so its fragment number must be the next # of the higest registered fragment number. last_index = max( list(map(int, list( sequence_numbers.keys())))) + 1 upload_blob_using_threads( BUCKET_NAME, data[0].decode("ISO-8859-1") + data[1].decode("utf-8"), f"all_windows/window_{current_window}/" f"fragment_{current_window}_{last_index}") try: _ = requests.post(url=REASSEMBLER_URL, json={ "last_index": last_index, "current_window": current_window, "header_bytes": header_bytes }, timeout=0.1) except requests.exceptions.ReadTimeout: pass # Send last ACK to end communication (on receiving an All-1, if no fragments are lost, # if it has received at least one tile, return an ACK for the highest numbered window we # currently have tiles for). print("[ALL1] Preparing last ACK") bitmap = '' for k in range(profile.BITMAP_SIZE): bitmap += '0' last_ack = ACK(profile=profile, rule_id=rule_id, dtag=dtag, w=zfill(format(window_ack, 'b'), m), c='1', bitmap=bitmap_ack) response_json = send_ack(request_dict, last_ack) print(f"200, Response content -> {response_json}") print("[ALL1] Last ACK has been sent.") return response_json, 200 # If the last two fragments are not consecutive, or the bitmap didn't match the regex, # send an ACK reporting losses. else: # Send NACK at the end of the window. print("[ALLX] Sending NACK for lost fragments...") ack = ACK(profile=profile, rule_id=rule_id, dtag=dtag, w=zfill(format(window_ack, 'b'), m), c='0', bitmap=bitmap_ack) response_json = send_ack(request_dict, ack) return response_json, 200 return '', 204 else: print( 'Invalid HTTP Method to invoke Cloud Function. Only POST supported' ) return abort(405)
def schc_send(self, fragment_sent, retransmit=False): ack = None current_fragment = {} logging = self.LOGGER is not None and self.LOGGER.JSON_FILE is not None self.TIMER.wait(timeout=self.DELAY, raise_exception=False) if logging: self.LOGGER.LOGGING_TIME += self.DELAY current_fragment = {'RULE_ID': fragment_sent.HEADER.RULE_ID, 'W': fragment_sent.HEADER.W, 'FCN': fragment_sent.HEADER.FCN, 'data': fragment_sent.to_bytes(), 'fragment_size': len(fragment_sent.to_bytes()), 'abort': False, 'sending_start': 0, 'sending_end': 0, 'send_time': 0, 'downlink_enable': False, 'timeout': 0, 'ack_received': False, 'ack': "", 'rssi': 0, 'receiver_abort_received': False, 'receiver_abort_message': ""} if fragment_sent.is_all_0() and not retransmit: self.LOGGER.debug("[POST] This is an All-0. Using All-0 SIGFOX_DL_TIMEOUT.") self.set_timeout(self.PROFILE.SIGFOX_DL_TIMEOUT) if logging: current_fragment["timeout"] = self.PROFILE.SIGFOX_DL_TIMEOUT elif fragment_sent.is_all_1(): self.LOGGER.debug("[POST] This is an All-1. Using RETRANSMISSION_TIMER_VALUE. Increasing ACK attempts.") self.ATTEMPTS += 1 self.set_timeout(self.PROFILE.RETRANSMISSION_TIMER_VALUE) if logging: current_fragment["timeout"] = self.PROFILE.RETRANSMISSION_TIMER_VALUE else: self.set_timeout(60) if logging: current_fragment["timeout"] = 60 # LoPy should use to_bytes() data = fragment_sent.to_hex().decode() self.LOGGER.info("[POST] Posting fragment {} ({})".format(fragment_sent.HEADER.to_string(), fragment_sent.to_hex())) if fragment_sent.expects_ack() and not retransmit: current_fragment['downlink_enable'] = True else: current_fragment['downlink_enable'] = False try: if logging: current_fragment['sending_start'] = self.LOGGER.CHRONO.read() self.send(data, loss=True) # self.send_mask(fragment_sent) if fragment_sent.expects_ack() and not retransmit: if logging: current_fragment['ack_received'] = False ack = self.recv(self.PROFILE.DOWNLINK_MTU // 8, loss=True) # ack = self.recv_mask() if logging: current_fragment['sending_end'] = self.LOGGER.CHRONO.read() current_fragment['send_time'] = current_fragment['sending_end'] - current_fragment['sending_start'] if ack is not None: current_fragment['rssi'] = self.PROTOCOL.rssi() self.LOGGER.debug("Response received at: {}: ".format(self.LOGGER.CHRONO.read())) self.LOGGER.debug('ack -> {}'.format(ack)) self.LOGGER.debug('message RSSI: {}'.format(self.PROTOCOL.rssi())) if fragment_sent.is_sender_abort(): self.LOGGER.debug("--- senderAbort:{}".format(fragment_sent.to_string())) self.LOGGER.debug("--- senderAbort:{}".format(fragment_sent.to_bytes())) if logging: current_fragment['abort'] = True self.LOGGER.FRAGMENTS_INFO_ARRAY.append(current_fragment) self.LOGGER.error("Sent Sender-Abort. Goodbye") self.LOGGER.SENDER_ABORTED = True raise SenderAbortError if not fragment_sent.expects_ack(): if not retransmit: self.FRAGMENT_INDEX += 1 if logging: self.LOGGER.FRAGMENTS_INFO_ARRAY.append(current_fragment) return if ack is not None: if logging: current_fragment['ack'] = ack current_fragment['ack_received'] = True self.LOGGER.info("[ACK] Bytes: {}. Ressetting attempts counter to 0.".format(ack)) self.ATTEMPTS = 0 # Parse ACK # ack_object = ACK.parse_from_bytes(self.PROFILE, ack) ack_object = ACK.parse_from_hex(self.PROFILE, ack) if ack_object.is_receiver_abort(): if logging: current_fragment['receiver_abort_message'] = ack current_fragment['receiver_abort_received'] = True self.LOGGER.FRAGMENTS_INFO_ARRAY.append(current_fragment) self.LOGGER.error("ERROR: Receiver Abort received. Aborting communication.") self.LOGGER.RECEIVER_ABORTED = True raise ReceiverAbortError if not fragment_sent.expects_ack(): self.LOGGER.error("ERROR: ACK received but not requested ({}).".format(ack)) self.LOGGER.FRAGMENTS_INFO_ARRAY.append(current_fragment) raise UnrequestedACKError # Extract data from ACK ack_window = ack_object.HEADER.W ack_window_number = ack_object.HEADER.WINDOW_NUMBER c = ack_object.HEADER.C bitmap = ack_object.BITMAP self.LOGGER.debug("ACK: {}".format(ack)) self.LOGGER.debug("ACK window: {}".format(ack_window)) self.LOGGER.debug("ACK bitmap: {}".format(bitmap)) self.LOGGER.debug("ACK C bit: {}".format(c)) self.LOGGER.debug("last window: {}".format(self.LAST_WINDOW)) # Save ACKREQ data self.LOGGER.FRAGMENTS_INFO_ARRAY.append(current_fragment) # If the W field in the SCHC ACK corresponds to the last window of the SCHC Packet: if ack_window_number == self.LAST_WINDOW: # If the C bit is set, the sender MAY exit successfully. if c == '1': self.LOGGER.info("Last ACK received, fragments reassembled successfully. End of transmission.") self.LOGGER.FINISHED = True self.FRAGMENT_INDEX += 1 return # Otherwise, else: # If the Profile mandates that the last tile be sent in an All-1 SCHC Fragment # (we are in the last window), .is_all_1() should be true: if fragment_sent.is_all_1(): # This is the last bitmap, it contains the data before the All-1 fragment. # The leftmost bit of this bitmap should always be 1, as the All-1 gets to the network # to request the ACK. last_bitmap = bitmap[:(len(self.FRAGMENTS) - 1) % self.PROFILE.WINDOW_SIZE] self.LOGGER.debug("last bitmap {}".format(last_bitmap)) # If the SCHC ACK shows no missing tile at the receiver, abort. # (C = 0 but transmission complete) if last_bitmap == '' or (last_bitmap[0] == '1' and is_monochar(last_bitmap)): self.LOGGER.error("ERROR: SCHC ACK shows no missing tile at the receiver.") self.schc_send(SenderAbort(fragment_sent.PROFILE, fragment_sent.HEADER)) # Otherwise (fragments are lost), else: # Check for lost fragments. for j in range(len(last_bitmap)): # If the j-th bit of the bitmap is 0, then the j-th fragment was lost. if last_bitmap[j] == '0': self.LOGGER.info("The {} ({} / {}) fragment was lost! " "Sending again...".format(ordinal(j), self.PROFILE.WINDOW_SIZE * ack_window_number + j, len(self.FRAGMENTS))) # Try sending again the lost fragment. fragment_to_be_resent = Fragment(self.PROFILE, self.FRAGMENTS[ self.PROFILE.WINDOW_SIZE * ack_window_number + j]) self.LOGGER.debug("Lost fragment: {}".format(fragment_to_be_resent.to_string())) self.schc_send(fragment_to_be_resent, retransmit=True) # Send All-1 again to end communication or check again for lost data. self.schc_send(fragment_sent) else: self.LOGGER.error("ERROR: While being at the last window, the ACK-REQ was not an All-1. " "This is outside of the Sigfox scope.") raise BadProfileError # Otherwise, there are lost fragments in a non-final window. else: # Check for lost fragments. for j in range(len(bitmap)): # If the j-th bit of the bitmap is 0, then the j-th fragment was lost. if bitmap[j] == '0': self.LOGGER.info("The {} ({} / {}) fragment was lost! " "Sending again...".format(ordinal(j), self.PROFILE.WINDOW_SIZE * ack_window_number + j, len(self.FRAGMENTS))) # Try sending again the lost fragment. fragment_to_be_resent = Fragment(self.PROFILE, self.FRAGMENTS[ self.PROFILE.WINDOW_SIZE * ack_window_number + j]) self.LOGGER.debug("Lost fragment: {}".format(fragment_to_be_resent.to_string())) self.schc_send(fragment_to_be_resent, retransmit=True) if fragment_sent.is_all_1(): # Send All-1 again to end communication. self.schc_send(fragment_sent) elif fragment_sent.is_all_0(): # Continue with next window self.FRAGMENT_INDEX += 1 self.CURRENT_WINDOW += 1 except SCHCTimeoutError as e: if logging: current_fragment['sending_end'] = self.LOGGER.CHRONO.read() current_fragment['send_time'] = current_fragment['sending_end'] - current_fragment['sending_start'] current_fragment['rssi'] = self.PROTOCOL.rssi() current_fragment['ack'] = "" current_fragment['ack_received'] = False self.LOGGER.info("OSError at: {}: ".format(self.LOGGER.CHRONO.read())) self.LOGGER.info('OSError number {}, {}'.format(e.args[0], e)) # Save ACKREQ data if logging: self.LOGGER.FRAGMENTS_INFO_ARRAY.append(current_fragment) # If an ACK was expected if fragment_sent.is_all_1(): # If the attempts counter is strictly less than MAX_ACK_REQUESTS, try again self.LOGGER.debug("attempts:{}".format(self.ATTEMPTS)) if self.ATTEMPTS < self.PROFILE.MAX_ACK_REQUESTS: self.LOGGER.info("SCHC Timeout reached while waiting for an ACK. Sending the ACK Request again...") self.schc_send(fragment_sent, retransmit=False) # Else, exit with an error. else: self.LOGGER.error("ERROR: MAX_ACK_REQUESTS reached. Sending Sender-Abort.") abort = SenderAbort(self.PROFILE, fragment_sent.HEADER) self.schc_send(abort) # If the ACK can be not sent (Sigfox only) if fragment_sent.is_all_0(): self.LOGGER.info("All-0 timeout reached. Proceeding to next window.") self.FRAGMENT_INDEX += 1 self.CURRENT_WINDOW += 1 # Else, Sigfox communication failed. else: self.LOGGER.error("ERROR: Timeout reached.") raise NetworkDownError
def post_message(): global counter_w0 global counter_w1 if request.method == 'POST': print("POST RECEIVED") # BUCKET_NAME = config.BUCKET_NAME request_dict = request.get_json() print('Received Sigfox message: {}'.format(request_dict)) # Get data and Sigfox Sequence Number. fragment = request_dict["data"] sigfox_sequence_number = request_dict["seqNumber"] device = request_dict['device'] print('Data received from device id:{}, data:{}'.format( device, request_dict['data'])) # Parse fragment into "fragment = [header, payload] header = bytes.fromhex(fragment[:2]) payload = bytearray.fromhex(fragment[2:]) data = [header, payload] # Initialize SCHC variables. profile_uplink = Sigfox("UPLINK", "ACK ON ERROR") profile_downlink = Sigfox("DOWNLINK", "NO ACK") buffer_size = profile_uplink.MTU n = profile_uplink.N m = profile_uplink.M # Convert to a Fragment class for easier manipulation. fragment_message = Fragment(profile_uplink, data) # Get some SCHC values from the fragment. rule_id = fragment_message.header.RULE_ID dtag = fragment_message.header.DTAG w = fragment_message.header.W print('RULE_ID: {} W: {}, FCN: {}'.format( fragment_message.header.RULE_ID, fragment_message.header.W, fragment_message.header.FCN)) if 'ack' in request_dict: if request_dict['ack'] == 'true': print('w:{}'.format(w)) if w == '00': # print('ACK already send for this window, move along') # counter_w0 = 0 # return '', 204 if counter_w0 == 1: # print('ACK already send for this window, move along') print("This time send an ACK for window 1") counter_w0 = 0 bitmap = '0000001' ack = ACK(profile_downlink, rule_id, dtag, "01", bitmap, '0') response_json = send_ack(request_dict, ack) print("200, Response content -> {}".format( response_json)) return 'fragment lost', 204 counter_w0 += 1 print('lets say we lost the All-0, so move along') return 'fragment lost', 204 # return str(counter) # Create an ACK message and send it. bitmap = '1011111' bitmap = '1000000' bitmap = '0100001' ack = ACK(profile_downlink, rule_id, dtag, w, bitmap, '1') response_json = send_ack(request_dict, ack) print("200, Response content -> {}".format(response_json)) # response = {request_dict['device']: {'downlinkData': '07f7ffffffffffff'}} # print("response -> {}".format(response)) return response_json, 200 elif w == '01': if counter_w1 == 1: print("This time send an ACK for window 1") # counter_w0 = 0 counter_w1 += 1 bitmap = '0000001' ack = ACK(profile_downlink, rule_id, dtag, "01", bitmap, '0') response_json = send_ack(request_dict, ack) print("200, Response content -> {}".format( response_json)) return '', 204 elif counter_w1 == 2: print('Resend an ACK for window 1') counter_w1 += 1 bitmap = '0000001' ack = ACK(profile_downlink, rule_id, dtag, w, bitmap, '0') response_json = send_ack(request_dict, ack) print("200, Response content -> {}".format( response_json)) # response = {request_dict['device']: {'downlinkData': '07f7ffffffffffff'}} # print("response -> {}".format(response)) return response_json, 200 elif counter_w1 == 3: print( 'ACK already send for this window, send last ACK') counter_w1 = 0 bitmap = '0100001' ack = ACK(profile_downlink, rule_id, dtag, w, bitmap, '1') response_json = send_ack(request_dict, ack) print("200, Response content -> {}".format( response_json)) # response = {request_dict['device']: {'downlinkData': '07f7ffffffffffff'}} # print("response -> {}".format(response)) return response_json, 200 bitmap = '0100001' ack = ACK(profile_downlink, rule_id, dtag, w, bitmap, '1') response_json = send_ack(request_dict, ack) print("200, Response content -> {}".format( response_json)) counter_w1 += 1 # Create an ACK message and send it. bitmap = '0000001' ack = ACK(profile_downlink, rule_id, dtag, w, bitmap, '0') # Test for loss of All-0 in window 0 bitmap = '1010110' ack = ACK(profile_downlink, rule_id, dtag, '00', bitmap, '0') # ack = ACK(profile_downlink, rule_id, dtag, w, bitmap, '1') response_json = send_ack(request_dict, ack) print("200, Response content -> {}".format(response_json)) # response = {request_dict['device']: {'downlinkData': '07f7ffffffffffff'}} # print("response -> {}".format(response)) return response_json, 200 else: return '', 204 return '', 204 else: return f'Not a correct format message', 404
def post(fragment_sent, retransmit=False): global seqNumber, attempts, current_window, last_window, i, sent, received, retransmitted headers = {'content-type': 'application/json'} profile = fragment_sent.profile if fragment_sent.is_all_0() and not retransmit: print("[POST] This is an All-0. Using All-0 SIGFOX_DL_TIMEOUT.") request_timeout = profile.SIGFOX_DL_TIMEOUT elif fragment_sent.is_all_1(): print( "[POST] This is an All-1. Using RETRANSMISSION_TIMER_VALUE. Increasing ACK attempts." ) attempts += 1 request_timeout = profile.RETRANSMISSION_TIMER_VALUE else: request_timeout = 45 payload_dict = { "deviceType": "WYSCHC", "device": device, "time": str(int(time.time())), "data": fragment_sent.hex, "seqNumber": str(seqNumber), "ack": fragment_sent.expects_ack() and not retransmit } print( f"[POST] Posting fragment {fragment_sent.header.string} ({fragment_sent.hex}) to {SCHC_POST_URL}" ) try: response = requests.post(SCHC_POST_URL, data=json.dumps(payload_dict), headers=headers, timeout=request_timeout) if fragment_sent.is_sender_abort(): print("Sent Sender-Abort. Goodbye") exit(1) seqNumber += 1 sent += 1 if retransmit: retransmitted += 1 print(f"[POST] Response: {response}") http_code = response.status_code # If 500, exit with an error if http_code == 500: print("Response: 500 Internal Server Error") exit(1) # If 204, the fragment was posted successfully elif http_code == 204: print("Response: 204 No Content") if fragment_sent.is_all_0() and not retransmit: print("Faking timeout") time.sleep(profile.SIGFOX_DL_TIMEOUT) raise Timeout if not retransmit: i += 1 return # If 200, the fragment was posted and an ACK has been received. elif http_code == 200: print( f"Response: 200 OK, Text: {response.text}. Ressetting attempts counter to 0." ) received += 1 attempts = 0 ack = response.json()[device]["downlinkData"] # Parse ACK ack_object = ACK.parse_from_hex(profile_uplink, ack) if ack_object.is_receiver_abort(): print( "ERROR: Receiver Abort received. Aborting communication.") exit(1) if not fragment_sent.expects_ack(): print(f"ERROR: ACK received but not requested ({ack}).") exit(1) # Extract data from ACK ack_window = ack_object.w ack_window_number = ack_object.window_number c = ack_object.c bitmap = ack_object.bitmap print(f"ACK: {ack}") print(f"ACK window: {str(ack_window)}") print(f"ACK bitmap: {bitmap}") print(f"ACK C bit: {c}") print(f"last window: {last_window}") # If the W field in the SCHC ACK corresponds to the last window of the SCHC Packet: if ack_window_number == last_window: # If the C bit is set, the sender MAY exit successfully. if c == '1': print( "Last ACK received, fragments reassembled successfully. End of transmission." ) print( f"TOTAL UPLINK: {sent} ({retransmitted} retransmisiones)" ) print(f"TOTAL DOWNLINK: {received}") exit(0) # Otherwise, else: # If the Profile mandates that the last tile be sent in an All-1 SCHC Fragment # (we are in the last window), .is_all_1() should be true: if fragment_sent.is_all_1(): # This is the last bitmap, it contains the data up to the All-1 fragment. last_bitmap = bitmap[:len(fragment_list) % window_size] print(f"last bitmap {last_bitmap}") # If the SCHC ACK shows no missing tile at the receiver, abort. # (C = 0 but transmission complete) if last_bitmap[0] == '1' and all(last_bitmap): print( "ERROR: SCHC ACK shows no missing tile at the receiver." ) post( SenderAbort(fragment_sent.profile, fragment_sent.header)) # Otherwise (fragments are lost), else: # Check for lost fragments. for j in range(len(last_bitmap)): # If the j-th bit of the bitmap is 0, then the j-th fragment was lost. if last_bitmap[j] == '0': print( f"The {j}th ({window_size * ack_window_number + j} / {len(fragment_list)}) fragment was lost! Sending again..." ) # Try sending again the lost fragment. fragment_to_be_resent = Fragment( profile_uplink, fragment_list[window_size * ack_window + j]) print( f"Lost fragment: {fragment_to_be_resent.string}" ) post(fragment_to_be_resent, retransmit=True) # Send All-1 again to end communication. post(fragment_sent) else: print( "ERROR: While being at the last window, the ACK-REQ was not an All-1." "This is outside of the Sigfox scope.") exit(1) # Otherwise, there are lost fragments in a non-final window. else: # Check for lost fragments. for j in range(len(bitmap)): # If the j-th bit of the bitmap is 0, then the j-th fragment was lost. if bitmap[j] == '0': print( f"The {j}th ({window_size * ack_window_number + j} / {len(fragment_list)}) fragment was lost! Sending again..." ) # Try sending again the lost fragment. fragment_to_be_resent = Fragment( profile_uplink, fragment_list[window_size * ack_window_number + j]) print(f"Lost fragment: {fragment_to_be_resent.string}") post(fragment_to_be_resent, retransmit=True) if fragment_sent.is_all_1(): # Send All-1 again to end communication. post(fragment_sent) elif fragment_sent.is_all_0(): i += 1 current_window += 1 # If the timer expires except Timeout: # If an ACK was expected if fragment_sent.is_all_1(): # If the attempts counter is strictly less than MAX_ACK_REQUESTS, try again if attempts < profile_uplink.MAX_ACK_REQUESTS: print( "SCHC Timeout reached while waiting for an ACK. Sending the ACK Request again..." ) post(fragment_sent) # Else, exit with an error. else: print("ERROR: MAX_ACK_REQUESTS reached. Sending Sender-Abort.") header = fragment_sent.header abort = SenderAbort(profile, header) post(abort) # If the ACK can be not sent (Sigfox only) if fragment_sent.is_all_0(): print("All-0 timeout reached. Proceeding to next window.") if not retransmit: i += 1 current_window += 1 # Else, HTTP communication failed. else: print("ERROR: HTTP Timeout reached.") exit(1)
def receiver(): """HTTP Cloud Function. Args: request (flask.Request): The request object. <http://flask.pocoo.org/docs/1.0/api/#flask.Request> Returns: The response text, or any set of values that can be turned into a Response object using `make_response` <http://flask.pocoo.org/docs/1.0/api/#flask.Flask.make_response>. """ # File where we will store authentication credentials after acquiring them. os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = config.CLIENT_SECRETS_FILE # Wait for an HTTP POST request. if request.method == 'POST': # Get request JSON. print("POST RECEIVED") request_dict = request.get_json() print('Received Sigfox message: {}'.format(request_dict)) # Get data and Sigfox Sequence Number. fragment = request_dict["data"] sigfox_sequence_number = request_dict["seqNumber"] header_first_hex = fragment[0] if header_first_hex == '0' or header_first_hex == '1': header = bytes.fromhex(fragment[:2]) payload = bytearray.fromhex(fragment[2:]) header_bytes = 1 elif header_first_hex == 'f' or header_first_hex == '2': header = bytearray.fromhex(fragment[:4]) payload = bytearray.fromhex(fragment[4:]) header_bytes = 2 else: print("Wrong header in fragment") return 'wrong header', 204 data = [header, payload] # Initialize SCHC variables. profile = SigfoxProfile("UPLINK", "ACK ON ERROR", header_bytes) buffer_size = profile.UPLINK_MTU n = profile.N m = profile.M # If fragment size is greater than buffer size, ignore it and end function. if len(fragment ) / 2 * 8 > buffer_size: # Fragment is hex, 1 hex = 1/2 byte return json.dumps( {"message": "Fragment size is greater than buffer size D:"}), 200 # Initialize empty window window = [] for i in range(2**n - 1): window.append([b"", b""]) # Compute the fragment compressed number (FCN) from the Profile fcn_dict = {} for j in range(2**n - 1): fcn_dict[zfill(bin((2**n - 2) - (j % (2**n - 1)))[2:], n)] = j # Convert to a Fragment class for easier manipulation. fragment_message = Fragment(profile, data) # Get current window for this fragment. current_window = int(fragment_message.HEADER.W, 2) # Get the current bitmap. bitmap = read_blob("all_windows/window_%d/bitmap_%d" % (current_window, current_window)) if fragment_message.is_sender_abort(): print("Sender-Abort received") return 'Sender-Abort received', 204 try: fragment_number = fcn_dict[fragment_message.HEADER.FCN] upload_blob(fragment_number, "fragment_number") time_received = int(request_dict["time"]) if exists_blob("timestamp"): # Check time validation. last_time_received = int(read_blob("timestamp")) # If this is not the very first fragment and the inactivity timer has been reached, ignore the message. if str(fragment_number) != "0" and str( current_window ) != "0" and time_received - last_time_received > profile.INACTIVITY_TIMER_VALUE: print("[RECV] Inactivity timer reached. Ending session.") receiver_abort = ReceiverAbort(profile, fragment_message.HEADER) print("Sending Receiver Abort") response_json = send_ack(request_dict, receiver_abort) print(f"Response content -> {response_json}") return response_json, 200 # Upload current timestamp. upload_blob(time_received, "timestamp") # Print some data for the user. print( f"[RECV] This corresponds to the {ordinal(fragment_number)} fragment " f"of the {ordinal(current_window)} window.") print(f"[RECV] Sigfox sequence number: {sigfox_sequence_number}") # Update bitmap and upload it. bitmap = replace_bit(bitmap, fragment_number, '1') # If the FCN could not been found, it almost certainly is the final fragment. except KeyError: print("[RECV] This seems to be the final fragment.") fragment_number = profile.WINDOW_SIZE - 1 # Upload current timestamp. time_received = int(request_dict["time"]) upload_blob(time_received, "timestamp") print( f"is All-1:{fragment_message.is_all_1()}, is All-0:{fragment_message.is_all_0()}" ) # Update bitmap and upload it. bitmap = replace_bit(bitmap, len(bitmap) - 1, '1') # Upload the fragment data. upload_blob( bitmap, f"all_windows/window_{current_window}/bitmap_{current_window}") upload_blob( data[0].decode("ISO-8859-1") + data[1].decode("utf-8"), f"all_windows/window_{current_window}/fragment_{current_window}_{fragment_number}" ) # Get some SCHC values from the fragment. rule_id = fragment_message.HEADER.RULE_ID dtag = fragment_message.HEADER.DTAG # Get last and current Sigfox sequence number (SSN) last_sequence_number = 0 if exists_blob("SSN"): last_sequence_number = read_blob("SSN") upload_blob(sigfox_sequence_number, "SSN") # # Controlling deterministic losses. This loads the file "loss_mask.txt" which states when should a fragment be # # lost, separated by windows. # fd = None # try: # fd = open(config.LOSS_MASK_MODIFIED, "r") # except FileNotFoundError: # fd = open(config.LOSS_MASK, "r") # finally: # loss_mask = [] # for line in fd: # if not line.startswith("#"): # for char in line: # try: # loss_mask.append(int(char)) # except ValueError: # pass # fd.close() # # print(f"Loss mask: {loss_mask}") # If the fragment is at the end of a window (ALL-0 or ALL-1) if fragment_message.is_all_0() or fragment_message.is_all_1(): # Prepare the ACK bitmap. Find the first bitmap with a 0 in it. for i in range(current_window + 1): bitmap_ack = read_blob("all_windows/window_%d/bitmap_%d" % (i, i)) print(bitmap_ack) window_ack = i if '0' in bitmap_ack: break # If the ACK bitmap has a 0 at the end of a non-final window, a fragment has been lost. if fragment_message.is_all_0() and '0' in bitmap_ack: print("[ALL0] Sending ACK for lost fragments...") print("bitmap with errors -> {}".format(bitmap_ack)) # Create an ACK message and send it. ack = ACK(profile=profile, rule_id=rule_id, dtag=dtag, w=zfill(format(window_ack, 'b'), m), c='0', bitmap=bitmap_ack) response_json = send_ack(request_dict, ack) print("Response content -> {}".format(response_json)) return response_json, 200 # If the ACK bitmap is complete and the fragment is an ALL-0, don't send an ACK if fragment_message.is_all_0() and bitmap[0] == '1' and all( bitmap): print("[ALL0] All Fragments of current window received") print("[ALL0] No need to send an ACK") return '', 204 # If the fragment is an ALL-1 if fragment_message.is_all_1(): # The bitmap in the last window follows the following regular expression: "1*0*1*" # Since the ALL-1, if received, changes the least significant bit of the bitmap. # For a "complete" bitmap in the last window, there shouldn't be non-consecutive zeroes: # 1110001 is a valid bitmap, 1101001 is not. pattern = re.compile("1*0*1") # If the bitmap matches the regex, check if the last two received fragments are consecutive. if pattern.fullmatch(bitmap_ack): print("SSN is {} and last SSN is {}".format( sigfox_sequence_number, last_sequence_number)) # If the last two received fragments are consecutive, accept the ALL-1 and start reassembling if int(sigfox_sequence_number) - int( last_sequence_number) == 1: last_index = profile.WINDOW_SIZE - 1 print( "Info for reassemble: last_index:{}, current_window:{}" .format(last_index, current_window)) print('Activating reassembly process...') start_request(url=config.LOCAL_REASSEMBLE_URL, body={ "current_window": current_window, "header_bytes": header_bytes }) # Send last ACK to end communication. print("[ALL1] Reassembled: Sending last ACK") bitmap = '' for k in range(profile.BITMAP_SIZE): bitmap += '0' last_ack = ACK(profile=profile, rule_id=rule_id, dtag=dtag, w=zfill(format(window_ack, 'b'), m), c='1', bitmap=bitmap) response_json = send_ack(request_dict, last_ack) # return response_json, 200 # response_json = send_ack(request_dict, last_ack) print("200, Response content -> {}".format( response_json)) return response_json, 200 else: # Send NACK at the end of the window. print( "[ALLX] Sending NACK for lost fragments because of SSN..." ) ack = ACK(profile=profile, rule_id=rule_id, dtag=dtag, w=zfill(format(window_ack, 'b'), m), c='0', bitmap=bitmap_ack) response_json = send_ack(request_dict, ack) return response_json, 200 # If they are not, there is a gap between two fragments: a fragment has been lost. # The same happens if the bitmap doesn't match the regex. else: # Send NACK at the end of the window. print("[ALLX] Sending NACK for lost fragments...") ack = ACK(profile=profile, rule_id=rule_id, dtag=dtag, w=zfill(format(window_ack, 'b'), m), c='0', bitmap=bitmap_ack) response_json = send_ack(request_dict, ack) return response_json, 200 return '', 204 else: print( 'Invalid HTTP Method to invoke Cloud Function. Only POST supported' ) return abort(405)