@@ -1338,8 +1338,6 @@ def collect_bandsteering_values(self):
13381338 "channel" : channel_map .get (sta , "NA" ),
13391339 "bssid" : bssid
13401340 })
1341-
1342- logger .info ("[BANDSTEERING] Rows collected this tick: %d" , len (rows ))
13431341 logger .info ("[DEBUG] CX BATCH: %s" , self .bandsteering_cx_list )
13441342 return rows
13451343
@@ -1635,7 +1633,7 @@ def run_test(self, available_resources):
16351633 matched , abort = self .robo_obj .move_to_coordinate (coord = coordinate )
16361634 if not matched :
16371635 logging .warning (f"Failed to move to coordinate { coordinate } " )
1638- continue
1636+ continue
16391637 self .current_cord = coordinate
16401638 if self .rotations_enabled :
16411639 for angle in self .angles_list :
@@ -2373,6 +2371,15 @@ def create_report(self, iot_summary=None):
23732371 test_setup_info = with_iot_params_in_table (test_setup_info , iot_summary )
23742372 report .test_setup_table (
23752373 test_setup_data = test_setup_info , value = 'Test Parameters' )
2374+ # Ensure real_time_data.csv is included before graph generation
2375+ if os .path .isfile ("real_time_data.csv" ) and \
2376+ "real_time_data.csv" not in self .csv_file_names :
2377+ self .csv_file_names .insert (0 , "real_time_data.csv" )
2378+
2379+ # Keep only last iteration for normal full run
2380+ if not self .webui_stop_clicked and not self .do_bandsteering :
2381+ if len (self .csv_file_names ) > 1 :
2382+ self .csv_file_names = [self .csv_file_names [- 1 ]]
23762383
23772384 for i in range (0 , len (self .csv_file_names )):
23782385
@@ -2460,12 +2467,52 @@ def create_report(self, iot_summary=None):
24602467 total_err_data = data ['total_err' ].tolist ()
24612468 else :
24622469 raise ValueError ("The 'total_err' column was not found in the CSV file." )
2470+ # bandsteering bssid section
2471+ if self .do_bandsteering :
2472+ self .add_bandsteering_bssid_section (report )
2473+ # robot charging timestamps section
2474+ if self .do_bandsteering :
2475+ charging_ts = getattr (self .robo_obj , "charging_timestamps" , [])
2476+
2477+ if charging_ts :
2478+ report .set_obj_html (
2479+ _obj_title = "Robot Charging Timestamps" ,
2480+ _obj = ""
2481+ )
2482+ report .build_objective ()
2483+
2484+ df = pd .DataFrame (
2485+ charging_ts ,
2486+ columns = [
2487+ "charge_dock_arrival_timestamp" ,
2488+ "charging_completion_timestamp"
2489+ ]
2490+ )
2491+
2492+ # Add serial number column
2493+ df .insert (0 , "S.No" , range (1 , len (df ) + 1 ))
2494+
2495+ report .set_table_dataframe (df )
2496+ report .build_table ()
2497+ else :
2498+ report .set_obj_html (
2499+ _obj_title = "Robot Charging Timestamps" ,
2500+ _obj = "Robot did not go to charge during this test"
2501+ )
2502+ report .build_objective ()
24632503
24642504 report .set_table_title ("Final Test Results" )
24652505 report .build_table_title ()
24662506 if self .selected_groups and self .selected_profiles :
24672507 if self .expected_passfail_value or self .device_csv_name :
2468- pass_fail_list , test_input_list = self .generate_pass_fail_list (device_type_data , device_names , total_urls )
2508+
2509+ if self .webui_stop_clicked :
2510+ logging .info ("[REPORT] WebUI stop detected. Skipping PASS/FAIL evaluation." )
2511+ pass_fail_list = ["NA" ] * len (device_names )
2512+ test_input_list = ["NA" ] * len (device_names )
2513+ else :
2514+ pass_fail_list , test_input_list = self .generate_pass_fail_list (
2515+ device_type_data , device_names , total_urls )
24692516
24702517 final_test_results = {
24712518
@@ -2509,13 +2556,28 @@ def create_report(self, iot_summary=None):
25092556 continue
25102557 report .set_table_title (f"{ group } Test Results" )
25112558 report .build_table_title ()
2559+ # Ensure all columns have equal length
2560+ max_len = max (len (v ) for v in final_test_results .values ())
2561+
2562+ for key in final_test_results :
2563+ if len (final_test_results [key ]) < max_len :
2564+ diff = max_len - len (final_test_results [key ])
2565+ final_test_results [key ].extend (["NA" ] * diff )
2566+
25122567 test_results_df = pd .DataFrame (group_specific_test_results )
25132568 report .set_table_dataframe (test_results_df )
25142569 report .build_table ()
25152570
25162571 else :
25172572 if self .expected_passfail_value or self .device_csv_name :
2518- pass_fail_list , test_input_list = self .generate_pass_fail_list (device_type_data , device_names , total_urls )
2573+ if self .webui_stop_clicked :
2574+ logging .info ("[REPORT] WebUI stop detected. Skipping PASS/FAIL evaluation." )
2575+ pass_fail_list = ["NA" ] * len (device_names )
2576+ test_input_list = ["NA" ] * len (device_names )
2577+ else :
2578+ pass_fail_list , test_input_list = self .generate_pass_fail_list (
2579+ device_type_data , device_names , total_urls )
2580+
25192581 final_test_results = {
25202582
25212583 "Device Type" : device_type_data ,
@@ -2552,6 +2614,14 @@ def create_report(self, iot_summary=None):
25522614 "Link Speed" : tx_rate_data ,
25532615
25542616 }
2617+ # Ensure all columns have equal length
2618+ max_len = max (len (v ) for v in final_test_results .values ())
2619+
2620+ for key in final_test_results :
2621+ if len (final_test_results [key ]) < max_len :
2622+ diff = max_len - len (final_test_results [key ])
2623+ final_test_results [key ].extend (["NA" ] * diff )
2624+
25552625 test_results_df = pd .DataFrame (final_test_results )
25562626 report .set_table_dataframe (test_results_df )
25572627 report .build_table ()
@@ -2568,18 +2638,32 @@ def create_report(self, iot_summary=None):
25682638 except Exception as e :
25692639 logging .error (f"Error in create_report function { e } " , exc_info = True )
25702640 finally :
2571- if not self .dowebgui :
2572- source_dir = "."
2573- destination_dir = self .report_path_date_time
2574- self .csv_file_names .append ('real_time_data.csv' )
2641+ source_dir = os .getcwd ()
2642+ destination_dir = self .report_path_date_time
2643+
2644+ # Only move if report folder exists
2645+ if os .path .isdir (destination_dir ):
2646+
2647+ if 'real_time_data.csv' not in self .csv_file_names :
2648+ self .csv_file_names .append ('real_time_data.csv' )
2649+
25752650 for filename in self .csv_file_names :
25762651 source_path = os .path .join (source_dir , filename )
25772652 destination_path = os .path .join (destination_dir , filename )
2653+
25782654 if os .path .isfile (source_path ):
2579- shutil .move (source_path , destination_path )
2580- logging .info (f"Moved { filename } to { destination_dir } " )
2581- else :
2582- logging .info (f"{ filename } not found in the current directory" )
2655+ try :
2656+ shutil .move (source_path , destination_path )
2657+ logging .info (f"Moved { filename } to { destination_dir } " )
2658+ except Exception as e :
2659+ logging .warning (f"Could not move { filename } : { e } " )
2660+
2661+ if self .do_bandsteering and hasattr (self , "bandsteering_dir" ):
2662+ for fname in os .listdir (self .bandsteering_dir ):
2663+ src = os .path .join (self .bandsteering_dir , fname )
2664+ dst = os .path .join (destination_dir , fname )
2665+ if os .path .isfile (src ):
2666+ shutil .move (src , dst )
25832667
25842668 def extract_device_data (self , file_path ):
25852669 # Load the CSV file
@@ -2920,16 +3004,30 @@ def create_robo_report(self):
29203004 test_setup_info = self .generate_test_setup_info ()
29213005 self .report .test_setup_table (
29223006 test_setup_data = test_setup_info , value = 'Test Parameters' )
3007+ # Ensure real_time_data.csv is included before graph generation
3008+ if os .path .isfile ("real_time_data.csv" ) and \
3009+ "real_time_data.csv" not in self .csv_file_names :
3010+ self .csv_file_names .insert (0 , "real_time_data.csv" )
29233011
29243012 for coordinate in self .coordinates_list :
29253013 if self .rotations_enabled :
29263014 for angle in self .angles_list :
29273015 csv_file = f"{ coordinate } _{ angle } _webBrowser.csv"
3016+ if not os .path .isfile (csv_file ):
3017+ logging .warning (f"CSV file { csv_file } does not exist." )
3018+ continue
29283019 self .create_robo_graphs_test_results (csv_file , coordinate , angle )
29293020
29303021 else :
29313022 csv_file = f"{ coordinate } _webBrowser.csv"
3023+ if not os .path .isfile (csv_file ):
3024+ logging .warning (f"CSV file { csv_file } does not exist." )
3025+ continue
29323026 self .create_robo_graphs_test_results (csv_file , coordinate )
3027+ # bandsteering bssid section
3028+ if self .do_bandsteering :
3029+ self .add_bandsteering_bssid_section (self .report )
3030+
29333031 self .add_live_view_images_to_report ()
29343032
29353033 if self .dowebgui :
@@ -3058,7 +3156,13 @@ def create_robo_graphs_test_results(self, csv_file, coordinate, angle=None):
30583156 self .report .set_table_title (f"Final Test Results at coordinate { coordinate } :" )
30593157 self .report .build_table_title ()
30603158 if self .expected_passfail_value or self .device_csv_name :
3061- pass_fail_list , test_input_list = self .generate_pass_fail_list (device_type_data , device_names , total_urls )
3159+ if self .webui_stop_clicked :
3160+ logging .info ("[REPORT] WebUI stop detected. Skipping PASS/FAIL evaluation." )
3161+ pass_fail_list = ["NA" ] * len (device_names )
3162+ test_input_list = ["NA" ] * len (device_names )
3163+ else :
3164+ pass_fail_list , test_input_list = self .generate_pass_fail_list (
3165+ device_type_data , device_names , total_urls )
30623166
30633167 final_test_results = {
30643168
@@ -3102,6 +3206,104 @@ def create_robo_graphs_test_results(self, csv_file, coordinate, angle=None):
31023206 except Exception as e :
31033207 logging .error (f"Error in create_robo_graphs_test_results { e } " , exc_info = True )
31043208
3209+ def add_bandsteering_bssid_section (self , report ):
3210+ # for bssids things in pdf
3211+ report .set_table_title ("Band Steering – BSSID Transition Analysis" )
3212+ report .build_table_title ()
3213+
3214+ if not self .bssids :
3215+ return
3216+
3217+ # normalize CLI BSSIDs
3218+ cli_bssids = [b .strip ().upper () for b in self .bssids ]
3219+
3220+ for csv_file in self .band_csv_files .values ():
3221+
3222+ if not os .path .exists (csv_file ):
3223+ continue
3224+
3225+ df = pd .read_csv (csv_file )
3226+
3227+ if df .empty or "bssid" not in df .columns :
3228+ continue
3229+
3230+ device_name = df ["device_name" ].iloc [0 ]
3231+
3232+ # Normalize CSV
3233+ df ["bssid" ] = df ["bssid" ].astype (str ).str .upper ().str .strip ()
3234+
3235+ # Detect transitions from FULL CSV
3236+ df ["prev_bssid" ] = df ["bssid" ].shift ()
3237+
3238+ transition_mask = (
3239+ (df ["bssid" ] != df ["prev_bssid" ]) &
3240+ (df ["bssid" ] != "NA" )
3241+ )
3242+
3243+ transition_rows = df [transition_mask ]
3244+
3245+ # Count only CLI BSSIDs
3246+ bssid_counts = {b : 0 for b in cli_bssids }
3247+ transitions = []
3248+
3249+ for _ , row in transition_rows .iterrows ():
3250+ curr_bssid = row ["bssid" ]
3251+
3252+ if curr_bssid in cli_bssids :
3253+ bssid_counts [curr_bssid ] += 1
3254+
3255+ transitions .append ({
3256+ "BSSID" : curr_bssid ,
3257+ "Timestamp" : row .get ("timestamp" , "NA" ),
3258+ "From Coordinate" : row .get ("from_coordinate" , "NA" ),
3259+ "To Coordinate" : row .get ("to_coordinate" , "NA" ),
3260+ "Channel" : row .get ("channel" , "NA" )
3261+ })
3262+
3263+ bssid_list = list (bssid_counts .keys ())
3264+ count_list = list (bssid_counts .values ())
3265+
3266+ report .set_graph_title (f"BSSID Change Count – { device_name } " )
3267+ report .build_graph_title ()
3268+
3269+ graph = lf_bar_graph_horizontal (
3270+ _data_set = [count_list ],
3271+ _xaxis_name = "Transition Count" ,
3272+ _yaxis_name = "BSSID" ,
3273+ _yaxis_label = bssid_list ,
3274+ _yaxis_categories = bssid_list ,
3275+ _bar_height = 0.25 ,
3276+ _show_bar_value = True ,
3277+ _figsize = (18 , max (4 , len (bssid_list ))),
3278+ _graph_title = "BSSID Transitions" ,
3279+ _graph_image_name = f"{ device_name } _bssid_transitions" ,
3280+ _label = ["Transitions" ]
3281+ )
3282+
3283+ graph_image = graph .build_bar_graph_horizontal ()
3284+ report .set_graph_image (graph_image )
3285+ report .move_graph_image ()
3286+ report .build_graph ()
3287+
3288+ report .set_table_title (f"Band Steering Results for { device_name } " )
3289+ report .build_table_title ()
3290+
3291+ if not transitions :
3292+ first_row = df .iloc [0 ]
3293+ last_row = df .iloc [- 1 ]
3294+
3295+ transitions .append ({
3296+ "BSSID" : first_row .get ("bssid" , "NA" ),
3297+ "Timestamp" : last_row .get ("timestamp" , "NA" ),
3298+ "From Coordinate" : first_row .get ("from_coordinate" , "NA" ),
3299+ "To Coordinate" : last_row .get ("to_coordinate" , "NA" ),
3300+ "Channel" : first_row .get ("channel" , "NA" )
3301+ })
3302+
3303+ transition_df = pd .DataFrame (transitions )
3304+ report .set_table_dataframe (transition_df )
3305+ report .build_table ()
3306+
31053307 def clear_http_cx_data (self ):
31063308 """Clears endpoint counters for all created HTTP connections."""
31073309 for cx in self .http_profile .created_cx :
@@ -3266,6 +3468,7 @@ def main():
32663468 optional .add_argument ('--iot_increment' , type = str , default = '' , help = 'Comma-separated list of device counts to incrementally test (e.g., "1,3,5")' )
32673469
32683470 # ROBO ARGS
3471+ robo .add_argument ('--duration_to_skip' , help = 'Robot wait duration in seconds at obstacle' , default = "1" )
32693472 robo .add_argument ('--robo_ip' , type = str , help = 'Specify the robo ip' )
32703473 robo .add_argument (
32713474 '--coordinates' ,
@@ -3280,6 +3483,24 @@ def main():
32803483 '--do_robo' ,
32813484 help = "Specify this flag to perform the test with robo" , action = 'store_true'
32823485 )
3486+ robo .add_argument (
3487+ "--do_bandsteering" ,
3488+ action = "store_true" ,
3489+ help = "Enable continuous robo band-steering test"
3490+ )
3491+ robo .add_argument (
3492+ "--cycles" ,
3493+ type = int ,
3494+ default = 1 ,
3495+ help = "Number of cycles to repeat coordinates for band-steering"
3496+ )
3497+ robo .add_argument (
3498+ '--bssids' ,
3499+ type = str ,
3500+ default = '' ,
3501+ help = 'bssid values'
3502+ )
3503+
32833504 args = parser .parse_args ()
32843505 if args .help_summary :
32853506 print (help_summary )
@@ -3348,7 +3569,11 @@ def main():
33483569 coordinates_list = args .coordinates ,
33493570 angles_list = args .rotations ,
33503571 do_robo = args .do_robo ,
3572+ do_bandsteering = args .do_bandsteering ,
3573+ cycles = args .cycles ,
3574+ bssids = args .bssids ,
33513575 rotations_enabled = rotations_enabled ,
3576+ duration_to_skip = args .duration_to_skip
33523577 )
33533578 obj .change_port_to_ip ()
33543579 obj .validate_and_process_args ()
@@ -3414,7 +3639,11 @@ def main():
34143639 logger .error ("An exception occurred:\n %s" , tb_str )
34153640 finally :
34163641 if '--help' not in sys .argv and '-h' not in sys .argv :
3417- if args .do_robo :
3642+ if args .do_robo and args .do_bandsteering :
3643+ if args .dowebgui :
3644+ obj .stop_webui_test ()
3645+ obj .create_report (iot_summary = iot_summary )
3646+ elif args .do_robo :
34183647 if args .dowebgui :
34193648 obj .stop_webui_test ()
34203649 obj .create_robo_report ()
0 commit comments