def test_init(self): hex_data = "053131313231333134313531" header = bytes.fromhex(hex_data[:2]) payload = bytearray.fromhex(hex_data[2:]) data = [header, payload] profile = SigfoxProfile("UPLINK", "ACK ON ERROR", 1) fragment = Fragment(profile, data) abort = SenderAbort(profile, fragment.HEADER) self.assertEqual(type(abort.PROFILE), SigfoxProfile) self.assertEqual(abort.HEADER.RULE_ID, fragment.HEADER.RULE_ID) self.assertEqual(abort.HEADER.DTAG, fragment.HEADER.DTAG) self.assertEqual(abort.HEADER.W, fragment.HEADER.W) self.assertTrue( abort.HEADER.FCN[0] == '1' and all(abort.HEADER.FCN), msg=f"{abort.HEADER.FCN[0] == '1'} and {all(abort.HEADER.FCN)}") self.assertTrue( abort.PAYLOAD.decode()[0] == '0' and all(abort.PAYLOAD.decode()), msg=f"{abort.PAYLOAD[0] == '0'} and {all(abort.PAYLOAD)}") self.assertFalse(abort.is_all_1()) self.assertTrue(abort.is_sender_abort()) hex_data = "1f353235" header = bytes.fromhex(hex_data[:2]) payload = bytearray.fromhex(hex_data[2:]) data = [header, payload] profile = SigfoxProfile("UPLINK", "ACK ON ERROR", 1) fragment = Fragment(profile, data) self.assertFalse(fragment.is_sender_abort()) hex_string = "1f3030303030303030303030" fragment_sent = Fragment.from_hex( SigfoxProfile("UPLINK", "ACK ON ERROR", 1), hex_string) abort = SenderAbort(fragment_sent.PROFILE, fragment_sent.HEADER) self.assertTrue(abort.is_sender_abort())
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_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 if bytearray.fromhex(fragment[2:]).decode() == "CLEAN": try: _ = requests.post(url=config.CLEAN_URL, json={ "header_bytes": header_bytes, "not_delete_dl_losses": "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, "not_delete_dl_losses": "False" }, timeout=0.1) except requests.exceptions.ReadTimeout: pass return '', 204 data = [header, payload] # Initialize SCHC variables. profile = Sigfox("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 # 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.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, data) 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.") 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)) if fragment_message.is_sender_abort(): print("Sender-Abort received") return 'Sender-Abort received', 204 # 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. 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("[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": loss_rate = request_dict["loss_rate"] 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=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") # 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": loss_rate = request_dict["loss_rate"] 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.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: 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": loss_rate = request_dict["loss_rate"] 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.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: 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": loss_rate = request_dict["loss_rate"] 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=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. if 'enable_dl_losses' in request_dict: if request_dict['enable_dl_losses'] == "True": loss_rate = request_dict["loss_rate"] 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=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_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 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)