diff --git a/communication.py b/communication.py index a842487..baec57e 100644 --- a/communication.py +++ b/communication.py @@ -162,6 +162,7 @@ def collect_iperf( server_ip="10.45.0.1", duration=10, is_client=True, + stations="", ): if is_client: commands = [ @@ -233,6 +234,7 @@ def collect_iperf( "start_distance": start_distance, "location": location, "type": test_type, + "stations": stations, } print(f"\n{server_ip} {test_type} IPERF complete") @@ -240,6 +242,7 @@ def collect_iperf( print(f"IPERF receiver bitrate: {receiver_bitrate}") save_data_to_json(data=data, filename=filename) + time.sleep(0.25) except Exception as e: print(f"iPerf Error: {e}") @@ -269,7 +272,7 @@ def collect_data(filename, address): # Send to server print(f"\nDistance: {data.get('distance')}") print(f"Service: {data.get('network information')}") - print(f"Ping Time: {data.get('ping_time')}") + print(f"Ping Time: {data.get(f'{address}_ping_time')}") print( f"RSRP: {data.get('RSRP PRX')} {data.get('RSRP DRX')} {data.get('RSRP RX2')} {data.get('RSRP RX3')}" ) @@ -306,12 +309,12 @@ def collect_sierra_data(filename): def main(): global running address = "10.46.0.1" - foldername = "data/boat_relay_sept_18" + foldername = "data/office_test_sept_19" os.makedirs(foldername, exist_ok=True) filename = foldername + "/test_" + str(int(time.time())) + ".json" - print("Setting configs...") - set_configs() + # print("Setting configs...") + # set_configs() print( "Type 'l' to set basestation location, 'b' to begin, " diff --git a/end_to_relay_client.py b/end_to_relay_client.py new file mode 100644 index 0000000..c89db4a --- /dev/null +++ b/end_to_relay_client.py @@ -0,0 +1,95 @@ +import re +import socket + +from helper_functions import normalize, save_data_to_json + +RELAY_IP = "10.45.0.1" # gNodeB IP of relay +PORT = 5005 +BUFFER_SIZE = 4096 + + +def parse_iperf_output(output, test_type): + matches = re.findall( + r"\[\s*\d+\]\s+\d+\.\d+\-\d+\.\d+\s+sec\s+[\d.]+\s+\w+Bytes\s+([\d.]+)\s+(Kbits/sec|Mbits/sec)", + output, + ) + + if len(matches) >= 1: + # Normalize the last entries + last_value, last_unit = matches[-1] + sender_bitrate = normalize(float(last_value), last_unit) + + # Try to differentiate sender vs receiver + receiver_match = re.search(r"receiver", output, re.IGNORECASE) + sender_match = re.search(r"sender", output, re.IGNORECASE) + + if receiver_match and sender_match and len(matches) >= 2: + recv_value, recv_unit = matches[-1] + send_value, send_unit = matches[-2] + receiver_bitrate = normalize(float(recv_value), recv_unit) + sender_bitrate = normalize(float(send_value), send_unit) + else: + receiver_bitrate = sender_bitrate + + else: + bitrates = re.findall(r"(\d+\.\d+) Mbits/sec", output) + sender_bitrate = float(bitrates[-2]) + receiver_bitrate = float(bitrates[-1]) + + print(f"\nrelay -> ground {test_type} IPERF complete") + print(f"IPERF sender bitrate: {sender_bitrate}") + print(f"IPERF receiver bitrate: {receiver_bitrate}") + + data = { + "iperf_full": output, + "sender_bitrate": sender_bitrate, + "receiver_bitrate": receiver_bitrate, + "type": test_type, + "stations": "rg", + } + return data + + +def collect_iperf_remote(filename: str = ""): + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.settimeout(20) # timeout for receiving results + + # Send "start" command + sock.sendto(b"start", (RELAY_IP, PORT)) + print(f"[End] Sent start command to relay {RELAY_IP}:{PORT}") + + # Collect results + results = [] + try: + while True: + data, _ = sock.recvfrom(BUFFER_SIZE) + if not data: + break + results.append(data.decode()) + except socket.timeout: + pass + + full_output = "".join(results) + + # Split output into normal vs reverse + normal_section = "" + reverse_section = "" + if "=== Reverse Test" in full_output: + parts = full_output.split("=== Reverse Test (Ground → Relay) ===") + normal_section = ( + parts[0].replace("=== Normal Test (Relay → Ground) ===", "").strip() + ) + reverse_section = parts[1].strip() if len(parts) > 1 else "" + else: + normal_section = full_output.strip() + + uplink_data = parse_iperf_output(output=normal_section, test_type="uplink") + downlink_data = parse_iperf_output(output=reverse_section, test_type="downlink") + + if filename: + save_data_to_json(data=uplink_data, filename=filename) + save_data_to_json(data=downlink_data, filename=filename) + + +if __name__ == "__main__": + collect_iperf_remote() diff --git a/relay.py b/relay.py new file mode 100644 index 0000000..ae48d30 --- /dev/null +++ b/relay.py @@ -0,0 +1,213 @@ +import os +import re +import subprocess +import threading +import time + +from at_commands import ( + get_modem_cops, + get_modem_cpol, + get_modem_creg, + get_modem_csq, + get_modem_qnwcfg, + get_modem_qnwinfo, + get_modem_qspn, + get_modem_rsrp, + get_modem_rsrq, + get_modem_sinr, + set_configs, +) +from communication import ( + collect_iperf, + get_current_location, + ping_basestation, + set_base_location, +) +from end_to_relay_client import collect_iperf_remote +from helper_functions import save_data_to_json + +# Globals +running = False # To control start/stop + + +# Ping base station +def ping_basestation(address="10.45.0.1", name="10.45.0.1", dictionary={}) -> dict: + try: + result = subprocess.run( + ["ping", "-c", "1", address], capture_output=True, text=True + ) + match = re.search(r"time=([\d.]+)", result.stdout) + + if match: + time_value = match.group(1) + dictionary[f"{name}_ping_time"] = time_value + else: + dictionary[f"{name}_ping_time"] = "Not found" + + dictionary[f"{name}_ping_stats"] = result.stdout + + except Exception as e: + dictionary[f"{name}_ping error"] = f"{e}" + + return dictionary + + +def calculate_ping( + dictionary: dict, base_address: str, relay_address: str, name: str +) -> dict: + try: + base_ping = float(dictionary[f"{base_address}_ping_time"]) + relay_ping = float(dictionary[f"{relay_address}_ping_time"]) + calculated_ping = base_ping - relay_ping + dictionary[f"{name}_c_ping_time"] = calculated_ping + except Exception as e: + dictionary[f"{name}_c_ping error"] = f"{e}" + + return dictionary + + +def relay_iperf( + filename: str, base_address: str, relay_address: str, duration: str +) -> None: + collect_iperf( + filename=filename, server_ip=base_address, duration=duration, stations="eg" + ) + collect_iperf( + filename=filename, server_ip=relay_address, duration=duration, stations="er" + ) + try: + collect_iperf_remote(filename=filename) + except Exception as e: + print(f"Error collecting relay -> ground iperf: {e}") + + +# Collect and send data continuously +def collect_data( + filename: str, base_address: str, relay_address: str, interval: int +) -> None: + global running + + while running: + data = {} + data = get_current_location(dictionary=data) + data = get_modem_cops(dictionary=data) + data = get_modem_creg(dictionary=data) + data = get_modem_csq(dictionary=data) + data = get_modem_rsrp(dictionary=data) + data = get_modem_rsrq(dictionary=data) + data = get_modem_sinr(dictionary=data) + data = get_modem_cpol(dictionary=data) + data = get_modem_qnwcfg(dictionary=data) + data = get_modem_qnwinfo(dictionary=data) + data = get_modem_qspn(dictionary=data) + data = ping_basestation( + dictionary=data, name="eg", address=base_address + ) # edge to ground + data = ping_basestation( + dictionary=data, name="er", address=relay_address + ) # edge to relay + data = calculate_ping( + dictionary=data, + base_address=base_address, + relay_address=relay_address, + name="rg", + ) # relay to ground + data["timestamp"] = time.time() + + # Send to server + print(f"\nPing Time: {data.get('ping_time')}") + print( + f"RSRP: {data.get('RSRP PRX')} {data.get('RSRP DRX')} {data.get('RSRP RX2')} {data.get('RSRP RX3')}" + ) + print(f"Service: {data.get('network information')}") + + save_data_to_json(data=data, filename=filename) + + time.sleep(interval) + + +def set_parameters(): + set_modem = ( + input( + "Do you want limit the modem to NR5G without roaming? (Not recommended if PLMN is not 001) (y/n )" + ) + .strip() + .lower() + ) + while set_modem not in ("y", "yes", "n", "no"): + set_modem = input("Your response must be y or n ").strip().lower() + if set_modem in ("y", "yes"): + print("Setting configs...") + set_configs() + + base_address = ( + input("Enter the base station (ground) ip address (Default is 10.46.0.1): ") + .strip() + .lower() + or "10.46.0.1" + ) + relay_address = ( + input("Enter the relay station ip address (Default is 10.45.0.1): ") + .strip() + .lower() + or "10.45.0.1" + ) + folder_name = ( + input("Enter the log folder name (Default is boat_relay_sept): ") + .strip() + .lower() + or "boat_relay_sept" + ) + interval = int( + input( + "Enter the time interval (s) in between pings/modem data collection (Default is 5): " + ) + .strip() + .lower() + or 5 + ) + iperf_duration = int( + input("Enter the iperf test duration (s) (Default is 5): ").strip().lower() or 5 + ) + + return base_address, relay_address, folder_name, interval, iperf_duration + + +if __name__ == "__main__": + base_address, relay_address, folder_name, interval, iperf_duration = ( + set_parameters() + ) + + foldername = f"data/{folder_name}" + filename = foldername + "/test_" + str(int(time.time())) + ".json" + os.makedirs(foldername, exist_ok=True) + + print( + "Type 'l' to set basestation location, 'b' to begin, " + "'s' to stop, 'i' to run an iperf test, or 'x' to exit:" + ) + while True: + command = input("> ").strip().lower() + if command == "b" and not running: + print("Starting data collection...") + running = True + threading.Thread( + target=collect_data, + args=(filename, base_address, relay_address, interval), + ).start() + elif command == "l": + base_location_data = set_base_location() + save_data_to_json(data=base_location_data, filename=filename) + elif command == "i": + threading.Thread( + target=relay_iperf, + args=(filename, base_address, relay_address, iperf_duration), + ).start() + elif command == "s" and running: + print("Stopping data collection...") + running = False + elif command == "x": + running = False + break + elif command not in ["l", "b", "s", "i", "x"]: + print("Invalid command. Type 'l', 'b', 's', 'i', or 'x'.") diff --git a/relay_to_ground_iperf.py b/relay_to_ground_iperf.py new file mode 100644 index 0000000..c1c8a72 --- /dev/null +++ b/relay_to_ground_iperf.py @@ -0,0 +1,55 @@ +import socket +import subprocess +import time + +GROUND_IP = "10.46.0.1" # gNodeB IP of ground station +PORT = 5005 # port to listen on for commands +BUFFER_SIZE = 4096 + + +def run_iperf(normal=True): + """Run iperf3 to ground. normal=True -> normal test, False -> reverse test.""" + cmd = ["iperf3", "-c", GROUND_IP, "-t", "5"] + if not normal: + cmd.append("-R") # reverse mode + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + return result.stdout + result.stderr + except subprocess.TimeoutExpired: + return "[Relay] iperf test timed out\n" + + +def main(): + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.bind(("0.0.0.0", PORT)) + print(f"[Relay] Listening for commands on port {PORT}") + + while True: + data, addr = sock.recvfrom(BUFFER_SIZE) + command = data.decode().strip() + print(f"[Relay] Received command '{command}' from {addr}") + + if command.lower() == "start": + # Run normal test + output_normal = run_iperf(normal=True) + time.sleep(0.5) + + # Run reverse test + output_reverse = run_iperf(normal=False) + + # Combine results + combined = ( + "\n=== Normal Test (Relay → Ground) ===\n" + + output_normal + + "\n=== Reverse Test (Ground → Relay) ===\n" + + output_reverse + ) + + # Send results back in chunks + for i in range(0, len(combined), BUFFER_SIZE): + sock.sendto(combined[i : i + BUFFER_SIZE].encode(), addr) + print(f"[Relay] Sent results back to {addr}") + + +if __name__ == "__main__": + main()