@@ -284,6 +284,10 @@ def log_results(all_data: Mapping[str, str | float | int | None]) -> None:
284284 "WiFi_RSSI" : all_data .get ("wifi_rssi" , "N/A" ),
285285 "WiFi_Noise" : all_data .get ("wifi_noise" , "N/A" ),
286286 "WiFi_TxRate_Mbps" : all_data .get ("wifi_tx_rate" , "N/A" ),
287+ # LAN bufferbloat metrics
288+ "LAN_Idle_RTT_ms" : all_data .get ("lan_idle_rtt_ms" ),
289+ "LAN_Under_Load_RTT_ms" : all_data .get ("lan_under_load_rtt_ms" ),
290+ "LAN_Bufferbloat_ms" : all_data .get ("lan_bufferbloat_ms" ),
287291 }
288292
289293 # --- CSV Logging ---
@@ -454,6 +458,25 @@ def format_value(
454458 print (f" Noise Level: { data_points ['WiFi_Noise' ]} " )
455459 print (f" Channel/Band: { data_points ['WiFi_Channel' ]} " )
456460 print (f" Transmit Rate: { data_points ['WiFi_TxRate_Mbps' ]} Mbps" )
461+
462+ # --- LAN Bufferbloat Test ---
463+ print ("\n --- LAN Bufferbloat Test ---" )
464+ lan_idle = format_value (
465+ data_points ["LAN_Idle_RTT_ms" ],
466+ "ms" ,
467+ None ,
468+ default_color = Colors .CYAN ,
469+ precision = 3 ,
470+ )
471+ print (f" Idle LAN RTT: { lan_idle } " )
472+
473+ lan_bloat = format_value (
474+ data_points ["LAN_Bufferbloat_ms" ],
475+ "ms" ,
476+ config .LAN_BUFFERBLOAT_DELTA_THRESHOLD ,
477+ precision = 2 ,
478+ )
479+ print (f" LAN Bufferbloat Delta: { lan_bloat } " )
457480 print ("------------------------------------" )
458481 full_path = os .path .abspath (config .LOG_FILE )
459482 print (f"Results appended to: { full_path } " )
@@ -675,6 +698,78 @@ def run_local_speed_test_task() -> Optional[SpeedResults]:
675698 return None
676699
677700
701+ # --- LAN Bufferbloat Test ---
702+ def run_lan_bufferbloat_task () -> dict [str , float | None ]:
703+ """
704+ Measures LAN-specific bufferbloat by pinging a local server
705+ with and without a concurrent iperf3 load test.
706+ """
707+ if not config .LAN_TEST_TARGET_IP :
708+ print ("Warning: LAN_TEST_TARGET_IP not set. Skipping LAN bufferbloat test." )
709+ return {}
710+
711+ target_ip = config .LAN_TEST_TARGET_IP
712+ duration = config .LAN_BUFFERBLOAT_TEST_DURATION
713+ results : dict [str , float | None ] = {
714+ "lan_idle_rtt_ms" : None ,
715+ "lan_under_load_rtt_ms" : None ,
716+ "lan_bufferbloat_ms" : None ,
717+ }
718+
719+ print (f"--- Starting LAN Bufferbloat Test against { target_ip } ---" )
720+
721+ try :
722+ # 1. Measure Idle Latency
723+ print ("Measuring idle LAN latency..." )
724+ idle_ping_results = run_local_ping_task (target_ip )
725+ results ["lan_idle_rtt_ms" ] = idle_ping_results .get ("rtt_avg_ms" )
726+ if results ["lan_idle_rtt_ms" ] is None :
727+ print ("Error: Could not measure idle LAN latency. Aborting test." )
728+ return {}
729+
730+ # 2. Start iperf3 load in the background
731+ print (f"Starting iperf3 load test for { duration } seconds..." )
732+ iperf_command = ["iperf3" , "-c" , target_ip , "-t" , str (duration )]
733+ iperf_process = subprocess .Popen (
734+ iperf_command ,
735+ stdout = subprocess .DEVNULL ,
736+ stderr = subprocess .DEVNULL ,
737+ )
738+
739+ # Give iperf a moment to start before pinging
740+ time .sleep (1 )
741+
742+ # 3. Measure Latency Under Load (for the remaining duration)
743+ print ("Measuring LAN latency under load..." )
744+ ping_duration = max (1 , duration - 1 )
745+ # We use a different ping command here to control duration
746+ ping_command = ["ping" , "-c" , str (ping_duration ), "-i" , "1" , target_ip ]
747+ under_load_ping_process = subprocess .run (
748+ ping_command , capture_output = True , text = True , timeout = duration + 5
749+ )
750+ under_load_ping_results = parse_local_ping_results (under_load_ping_process .stdout )
751+ results ["lan_under_load_rtt_ms" ] = under_load_ping_results .get ("rtt_avg_ms" )
752+
753+ # 4. Wait for iperf3 to finish
754+ iperf_process .wait (timeout = 5 )
755+ print ("LAN load test finished." )
756+
757+ # 5. Calculate LAN Bufferbloat
758+ if results ["lan_under_load_rtt_ms" ] is not None :
759+ results ["lan_bufferbloat_ms" ] = (
760+ results ["lan_under_load_rtt_ms" ] - results ["lan_idle_rtt_ms" ]
761+ )
762+
763+ return results
764+
765+ except FileNotFoundError :
766+ print ("Error: 'iperf3' command not found. Please run 'brew install iperf3'." )
767+ return {}
768+ except Exception as e :
769+ print (f"An error occurred during the LAN bufferbloat test: { e } " )
770+ return {}
771+
772+
678773def run_wifi_diagnostics_task () -> WifiDiagnostics :
679774 """
680775 Uses a hybrid approach: wdutil for live Wi-Fi stats (signal, etc.) and
@@ -802,6 +897,12 @@ def perform_checks() -> None:
802897 debug_log .log ("run_local_speed_test_task: END" )
803898 if local_speed_results :
804899 master_results .update (local_speed_results )
900+ if getattr (config , "RUN_LAN_BUFFERBLOAT_TEST" , False ):
901+ debug_log .log ("run_lan_bufferbloat_task: START" )
902+ lan_bloat_results = run_lan_bufferbloat_task ()
903+ debug_log .log ("run_lan_bufferbloat_task: END" )
904+ if lan_bloat_results :
905+ master_results .update (lan_bloat_results )
805906
806907 # --- Run Gateway Tests (Selenium Required) in a single session ---
807908 should_run_gateway_speed_test = (
@@ -872,17 +973,27 @@ def perform_checks() -> None:
872973
873974
874975# --- Scheduler ---
976+ def main () -> None :
977+ """Sets up the schedule and runs the main application loop."""
978+ print ("--- Simple Gateway Logger Starting ---" )
979+
980+ # 1. Schedule the job to run every X minutes. This sets the timeline.
981+ schedule .every (config .RUN_INTERVAL_MINUTES ).minutes .do (perform_checks )
982+
983+ # 2. Manually run the job once immediately.
984+ # The next scheduled run will still be based on the timeline set above.
985+ perform_checks ()
986+
987+ # 3. Start the main loop to handle all subsequent scheduled runs.
988+ while True :
989+ schedule .run_pending ()
990+ time .sleep (1 )
991+
992+
875993if __name__ == "__main__" :
876994 if getattr (config , "ENABLE_DEBUG_LOGGING" , False ):
877995 logging .basicConfig ()
878996 schedule_logger = logging .getLogger ("schedule" )
879997 schedule_logger .setLevel (level = logging .DEBUG )
880- perform_checks ()
881- schedule .every (config .RUN_INTERVAL_MINUTES ).minutes .do (perform_checks )
882- if schedule .jobs :
883- next_run_datetime = schedule .jobs [0 ].next_run
884- print (f"Next test is scheduled for: { next_run_datetime .strftime ('%Y-%m-%d %H:%M:%S' )} " )
885- print ("Press Ctrl+C to exit." )
886- while True :
887- schedule .run_pending ()
888- time .sleep (1 )
998+
999+ main ()
0 commit comments