44import json
55import math
66import logging
7+ from datetime import datetime
78
89
910class RobotClass :
@@ -32,10 +33,20 @@ def __init__(self, robo_ip=None, angle_list=None):
3233 self .runtime_dir = None
3334 self .ip = None
3435 self .testname = None
36+ self .do_bandsteering = False
37+ self .from_coordinate = ""
38+ self .to_coordinate = ""
39+ self .charging_timestamps = []
40+ # max time to reach a point in seconds
41+ self .time_to_reach = 60
42+ self .total_cycles = 1
43+ self .coordinate_list = []
44+ self .total_cycles = 1
3545
3646 # Create waypoint list on initialization
3747 if self .robo_ip is not None :
3848 self .create_waypointlist ()
49+ open ("bandsteering.csv" , "w" ).write ("Timestamp,MAC,Channel,BSSID,Signal,Robot x,Robot y\n " )
3950
4051 def create_waypointlist (self ):
4152 """
@@ -83,13 +94,14 @@ def check_test_status(self):
8394
8495 return False
8596
86- def wait_for_battery (self , stop = None ):
97+ def wait_for_battery (self , stop = None , monitor_function = None ):
8798 """Monitor robot battery status and pause execution if battery is low.
8899
89100 Sends robot to charging station and resumes once fully charged.
90101
91102 Args:
92103 stop (callable, optional): Callback to pause external execution.
104+ monitor_function (function, optional): Function to call during movement.
93105
94106 Returns:
95107 tuple: (pause (bool), stopped (bool))
@@ -100,6 +112,10 @@ def wait_for_battery(self, stop=None):
100112 battery_url = f"http://{ self .robo_ip } /reeman/base_encode"
101113 status_url = f"http://{ self .robo_ip } /reeman/nav_status"
102114 move_url = f"http://{ self .robo_ip } /cmd/nav_name"
115+ # Timestamps for tracking charging cycle
116+ # charge_dock_move_start_timestamp = ""
117+ charge_dock_arrival_timestamp = ""
118+ charging_completion_timestamp = ""
103119 while True :
104120 try :
105121 response = requests .get (battery_url , timeout = 5 )
@@ -112,12 +128,22 @@ def wait_for_battery(self, stop=None):
112128 if stop is not None :
113129 stop ()
114130 logging .info ("Battery low ({}%). Pausing test until fully charged..." .format (battery ))
131+ # charge_dock_move_start_timestamp = datetime.now()
115132 requests .post (move_url , json = {"point" : self .charge_point_name })
133+ # Update navigation state of robot during bandsteering
134+ if self .to_coordinate != self .charge_point_name :
135+ if self .to_coordinate != "" :
136+ self .from_coordinate = self .to_coordinate
137+ self .to_coordinate = self .charge_point_name
116138 while True :
117139 try :
118140 response = requests .get (status_url , timeout = 5 )
119141 response .raise_for_status ()
120142 nav_status = response .json ()
143+ # Continue monitoring if function provided
144+ if monitor_function :
145+ all_dataframes = monitor_function ()
146+
121147 except (requests .RequestException , ValueError ) as e :
122148 logging .info ("[ERROR] Failed to get robot status: {}" .format (e ))
123149 time .sleep (5 )
@@ -130,14 +156,20 @@ def wait_for_battery(self, stop=None):
130156 state = nav_status .get ("res" , "" )
131157 distance = nav_status .get ("dist" , "" )
132158 if goal == self .charge_point_name and state == 3 and distance < 0.5 :
159+ self .from_coordinate = self .charge_point_name
160+ charge_dock_arrival_timestamp = datetime .now ()
133161 break
134162
135163 while True :
136164 if self .runtime_dir is not None and self .check_test_status ():
137165 stopped = True
166+ if monitor_function :
167+ return pause , stopped , all_dataframes
138168 return pause , stopped
139169
140170 current_time = time .time ()
171+ if monitor_function :
172+ all_dataframes = monitor_function ()
141173 if current_time - last_battery_check >= 300 :
142174 try :
143175 resp = requests .get (battery_url , timeout = 5 )
@@ -147,46 +179,73 @@ def wait_for_battery(self, stop=None):
147179 logging .info ("Current battery: {}%" .format (new_battery ))
148180 if new_battery > 99 :
149181 logging .info ("Battery full. Resuming test..." )
182+ charging_completion_timestamp = datetime .now ()
183+ self .charging_timestamps .append ([charge_dock_arrival_timestamp , charging_completion_timestamp ])
184+ if monitor_function :
185+ return pause , stopped , all_dataframes
150186 return pause , stopped
151187 except Exception as e :
152188 logging .info ("[ERROR] Checking charge: {}" .format (e ))
153189
154190 last_battery_check = time .time ()
155- time .sleep (10 )
191+ time .sleep (1 )
156192 else :
157193 logging .info ("[OK] Battery at {}%. Continuing test." .format (battery ))
194+ if monitor_function :
195+ return pause , stopped , {}
158196 return pause , stopped
159197
160198 except Exception as e :
161199 logging .info ("[ERROR] Failed to check battery: {}" .format (e ))
162200 stopped = True
201+ if monitor_function :
202+ return pause , stopped , {}
163203 return pause , stopped
164204
165- def move_to_coordinate (self , coord ):
205+ def move_to_coordinate (self , coord , monitor_function = None ):
166206 """
167207 Move the robot to a specified position.
168208
169209 Args:
170210 coord (str): position name.
171-
211+ monitor_function (function, optional): Function to call during movement.
172212 Returns:
173213 tuple: (matched (bool), abort (bool))
174214 """
175215 abort = False
176216 moverobo_url = 'http://' + self .robo_ip + '/cmd/nav_name'
177217 status_url = 'http://' + self .robo_ip + '/reeman/nav_status'
178- requests .post (moverobo_url , json = {"point" : coord })
218+ try :
219+ response = requests .post (moverobo_url , json = {"point" : coord })
220+ response .raise_for_status ()
221+ logging .info ("Move command sent successfully" )
222+ except requests .exceptions .RequestException as e :
223+ logging .info ("Error occurred:" , e )
179224 for wp in self .waypoint_list :
180225 if coord in wp :
181226 self .target_x = wp [coord ]["x" ]
182227 self .target_y = wp [coord ]["y" ]
183228
184229 retries = 0
185230 logging .info ("Moving to point {}" .format (coord ))
231+ time .sleep (5 )
232+ # If the robot is unable to reach the coordinate within the time given by the user
233+ # we consider that coordinate to be skipped
234+ prev_x , prev_y = None , None
235+ last_movement_time = time .time ()
236+ movement_timeout = self .time_to_reach
237+ movement_threshold = 0.8
238+ second_check = False
186239 while True :
187240 matched = False
188241 try :
189242 response = requests .get (status_url , timeout = 5 )
243+ x_coord , y_coord , from_coord , to_coord = self .get_robot_pose ()
244+ if monitor_function :
245+ self .to_coordinate = coord
246+ all_dataframes = monitor_function ()
247+ time .sleep (1 )
248+
190249 response .raise_for_status ()
191250 nav_status = response .json ()
192251 except (requests .RequestException , ValueError ) as e :
@@ -206,8 +265,38 @@ def move_to_coordinate(self, coord):
206265 distance = nav_status .get ("dist" , "" )
207266 if goal == coord and state == 3 and distance < 0.5 :
208267 matched = True
268+ self .from_coordinate = coord
209269 break
210270
271+ current_time = time .time ()
272+
273+ if prev_x is not None and prev_y is not None :
274+ movement = math .sqrt ((x_coord - prev_x ) ** 2 + (y_coord - prev_y ) ** 2 )
275+ if movement > movement_threshold :
276+ last_movement_time = current_time
277+
278+ prev_x , prev_y = x_coord , y_coord
279+ # ---- HANDLE STUCK CONDITION ----
280+ if current_time - last_movement_time > movement_timeout :
281+ # Robot is confirmed stuck after retry
282+ if second_check :
283+ logging .info ("Robot appears stuck. No movement detected." )
284+ matched = False
285+ if self .do_bandsteering :
286+ return matched , abort , all_dataframes
287+ else :
288+ return matched , abort
289+ else :
290+ try :
291+ response = requests .post (moverobo_url , json = {"point" : coord })
292+ response .raise_for_status ()
293+ logging .info ("Move command sent successfully and waiting for 10sec to observe movement" )
294+ time .sleep (10 )
295+ except requests .exceptions .RequestException as e :
296+ logging .info ("Error occurred:" , e )
297+ second_check = True
298+ continue
299+
211300 # Store the coordinate in navdatajson only from webui
212301 if self .nav_data_path :
213302 if not os .path .exists (self .nav_data_path ):
@@ -219,13 +308,16 @@ def move_to_coordinate(self, coord):
219308 navdata ['status' ] = "Stopped"
220309 navdata ['Canbee_location' ] = ''
221310 navdata ['Canbee_angle' ] = ''
311+ navdata ['Test_status' ] = 'Running'
222312 else :
223313 navdata ['status' ] = "Running"
224314 navdata ['Canbee_location' ] = coord
225315 navdata ['Canbee_angle' ] = ''
316+ navdata ['Test_status' ] = 'Running'
226317 with open (self .nav_data_path , 'w' ) as x :
227318 json .dump (navdata , x , indent = 4 )
228-
319+ if self .do_bandsteering :
320+ return matched , abort , all_dataframes
229321 return matched , abort
230322
231323 def rotate_angle (self , angle_degree ):
@@ -302,3 +394,90 @@ def angles_to_radians(self, angles):
302394 angle += 360
303395 result .append (round (math .radians (angle ), 2 ))
304396 return result
397+
398+ def get_robot_pose (self ):
399+ """
400+ Retrieve the robot's current position and navigation state.
401+
402+ Sends a request to the robot's pose API and extracts the current
403+ x and y coordinates. Also returns the robot's last known source
404+ and destination coordinates stored internally.
405+
406+ Returns:
407+ tuple:
408+ x (float): Current x-coordinate of the robot (default 0 if unavailable).
409+ y (float): Current y-coordinate of the robot (default 0 if unavailable).
410+ from_coordinate (str): Last known starting point of the robot.
411+ to_coordinate (str): Current target destination of the robot.
412+ """
413+ pose_url = f"http://{ self .robo_ip } /reeman/pose"
414+
415+ try :
416+ response = requests .get (pose_url , timeout = 5 )
417+ response .raise_for_status ()
418+ data_pose = response .json ()
419+
420+ x = data_pose .get ("x" , 0 )
421+ y = data_pose .get ("y" , 0 )
422+
423+ return x , y , self .from_coordinate , self .to_coordinate
424+
425+ except Exception as e :
426+ logging .error ("Failed to get robot pose: %s" , e )
427+ return 0 , 0 , self .from_coordinate , self .to_coordinate
428+
429+ def get_coordinates_list (self ):
430+ """
431+ Generate a filtered and ordered list of coordinates for navigation cycles.
432+
433+ This function attempts to move the robot through the initial coordinate list
434+ to find the first reachable (matched) coordinate. Any coordinates that fail
435+ before reaching a valid one are considered "skipped" and excluded from the
436+ final navigation plan.
437+
438+ Once a valid starting point is found, the function constructs a full navigation
439+ path based on the configured number of cycles. It then removes skipped points
440+ and the initially matched coordinate to produce the final execution list.
441+
442+ Returns:
443+ list:
444+ A list of coordinates representing the final navigation sequence.
445+ Returns an empty list if:
446+ - The robot aborts during movement
447+ - No coordinate is reachable
448+
449+ """
450+ skipped_list = []
451+ matched_index = None
452+
453+ # Find the matched coordinate
454+ for idx , coordinate in enumerate (self .coordinate_list ):
455+ matched , abort = self .move_to_coordinate (coordinate )
456+
457+ if matched :
458+ matched_index = idx
459+ break
460+
461+ skipped_list .append (coordinate )
462+
463+ if abort :
464+ return []
465+
466+ if matched_index is None :
467+ logging .info ("It couldn't reach any point, so ending the test." )
468+ return []
469+
470+ cycles = int (self .total_cycles )
471+
472+ # Build full cycle path
473+ full_path = self .coordinate_list * cycles
474+ full_path .append (self .coordinate_list [0 ]) # close the cycle
475+
476+ skip_count = len (skipped_list )
477+
478+ # Remove skipped points + matched point
479+ final_coordinate_list = full_path [skip_count + 1 :]
480+
481+ logging .info ("Final coordinate list: {}" .format (final_coordinate_list ))
482+
483+ return final_coordinate_list
0 commit comments