Skip to content

Commit 6e7ecc9

Browse files
Renusree-ctgoyalsaurabh06
authored andcommitted
lf_base_robo.py : Add functionality for improved navigation, bandsteering monitoring, and charging insights
- Added stuck detection of robot using movement timeout to treat unreachable coordinates as obstacles - Integrated monitor function during movement for real-time bandsteering client stats collection - Captured charging timestamps and navigation transitions for enhanced CSV reporting and analysis Signed-off-by: Renusree-ct <renusree.rayavarapu@candelatech.com>
1 parent 473b680 commit 6e7ecc9

File tree

1 file changed

+185
-6
lines changed

1 file changed

+185
-6
lines changed

py-scripts/lf_base_robo.py

Lines changed: 185 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import json
55
import math
66
import logging
7+
from datetime import datetime
78

89

910
class 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

Comments
 (0)