Skip to content

Commit e3ceb82

Browse files
authored
Merge pull request #3209 from JdeRobot/onnxruntime-gpu
update end_to_end visual control
2 parents 1f63ba2 + bc80de4 commit e3ceb82

File tree

7 files changed

+313
-107
lines changed

7 files changed

+313
-107
lines changed

compose_cfg/dev_humble_nvidia.yaml

Lines changed: 54 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,56 @@
11
services:
2-
my-postgres:
3-
image: jderobot/robotics-database:latest
4-
container_name: universe_db
5-
environment:
6-
POSTGRES_DB: academy_db
7-
POSTGRES_USER: user-dev
8-
POSTGRES_PASSWORD: robotics-academy-dev
9-
ports:
10-
- "5432:5432"
11-
volumes:
12-
- ./RoboticsInfrastructure/database/universes.sql:/docker-entrypoint-initdb.d/1.sql
13-
- ./database/exercises/db.sql:/docker-entrypoint-initdb.d/2.sql
14-
- ./database/django_auth.sql:/docker-entrypoint-initdb.d/3.sql
15-
- ./scripts:/scripts
16-
healthcheck:
17-
test: pg_isready -U user-dev -d academy_db
18-
interval: 3s
19-
timeout: 1s
20-
retries: 20
2+
my-postgres:
3+
image: jderobot/robotics-database:latest
4+
container_name: universe_db
5+
environment:
6+
POSTGRES_DB: academy_db
7+
POSTGRES_USER: user-dev
8+
POSTGRES_PASSWORD: robotics-academy-dev
9+
ports:
10+
- "5432:5432"
11+
volumes:
12+
- ./RoboticsInfrastructure/database/universes.sql:/docker-entrypoint-initdb.d/1.sql
13+
- ./database/exercises/db.sql:/docker-entrypoint-initdb.d/2.sql
14+
- ./database/django_auth.sql:/docker-entrypoint-initdb.d/3.sql
15+
- ./scripts:/scripts
16+
healthcheck:
17+
test: pg_isready -U user-dev -d academy_db
18+
interval: 3s
19+
timeout: 1s
20+
retries: 20
2121

22-
robotics-academy:
23-
image: jderobot/robotics-academy:latest
24-
container_name: developer-container
25-
command: "-s"
26-
deploy:
27-
resources:
28-
reservations:
29-
devices:
30-
- driver: nvidia
31-
count: all
32-
capabilities: [gpu]
33-
runtime: nvidia
34-
environment:
35-
- NVIDIA_VISIBLE_DEVICES=all
36-
- NVIDIA_DRIVER_CAPABILITIES=all
37-
ports:
38-
- "7164:7164"
39-
- "7163:7163"
40-
- "6080:6080"
41-
- "1108:1108"
42-
volumes:
43-
- type: bind
44-
source: ./
45-
target: /RoboticsAcademy
46-
- type: bind
47-
source: ./src
48-
target: /RoboticsApplicationManager
49-
tty: true
50-
stdin_open: true
51-
depends_on:
52-
my-postgres:
53-
condition: service_healthy
22+
robotics-academy:
23+
image: jderobot/robotics-academy:latest
24+
container_name: developer-container
25+
command: "-s"
26+
deploy:
27+
resources:
28+
reservations:
29+
devices:
30+
- driver: nvidia
31+
count: all
32+
capabilities: [gpu]
33+
environment:
34+
- NVIDIA_VISIBLE_DEVICES=all
35+
- NVIDIA_DRIVER_CAPABILITIES=all
36+
devices:
37+
- "/dev/dri:/dev/dri"
38+
ports:
39+
- "7164:7164"
40+
- "7163:7163"
41+
- "6080:6080"
42+
- "1108:1108"
43+
volumes:
44+
- type: bind
45+
source: ./
46+
target: /RoboticsAcademy
47+
- type: bind
48+
source: ./src
49+
target: /RoboticsApplicationManager
50+
tty: true
51+
stdin_open: true
52+
depends_on:
53+
my-postgres:
54+
condition: service_healthy
55+
links:
56+
- my-postgres
1.66 MB
Loading
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
model_dir_path = "/workspace/code/model.onnx"
2+
3+
4+
# This file is part of the Human Detection ROS2 package.
5+
def model_path_func() -> str:
6+
return model_dir_path
7+
8+
9+
model_path = model_path_func()

exercises/static/exercises/follow_line/python_template/ros2_humble/WebGUI.py

Lines changed: 97 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,27 @@
55
import rclpy
66
from gui_interfaces.general.measuring_threading_gui import MeasuringThreadingGUI
77
from console_interfaces.general.console import start_console
8+
9+
from hal_interfaces.general.camera import CameraNode
10+
from cv_bridge import CvBridge
811
from hal_interfaces.general.odometry import OdometryNode
912
from lap import Lap
13+
from sensor_msgs.msg import Image
14+
from rclpy.node import Node
1015

1116
import sys
1217

1318
sys.path.insert(0, "/RoboticsApplicationManager")
1419

1520
from manager.ram_logging.log_manager import LogManager
1621

17-
# Graphical User Interface Class
22+
23+
class WebGUIImagePublisher(Node):
24+
"""Internal publisher to create /webgui_image topic"""
25+
26+
def __init__(self):
27+
super().__init__("webgui_image_publisher_internal")
28+
self.publisher = self.create_publisher(Image, "/webgui_image", 10)
1829

1930

2031
class WebGUI(MeasuringThreadingGUI):
@@ -25,22 +36,67 @@ def __init__(self, host="ws://127.0.0.1:2303"):
2536
self.image_to_be_shown_updated = False
2637
self.image_show_lock = threading.Lock()
2738

28-
# Payload vars
39+
if not rclpy.ok():
40+
rclpy.init()
41+
42+
self.webgui_publisher = WebGUIImagePublisher()
43+
self.camera_node = None
44+
self.auto_image_mode = False
45+
self._setup_auto_mode()
46+
2947
self.payload = {"image": "", "lap": "", "map": ""}
30-
# TODO: maybe move this to HAL and have it be hybrid
48+
self.bridge = CvBridge()
49+
3150
self.pose3d_object = OdometryNode("/odom")
32-
executor = rclpy.executors.MultiThreadedExecutor()
33-
executor.add_node(self.pose3d_object)
34-
executor_thread = threading.Thread(target=executor.spin, daemon=True)
35-
executor_thread.start()
51+
52+
self.executor = rclpy.executors.MultiThreadedExecutor()
53+
self.executor.add_node(self.webgui_publisher)
54+
if self.camera_node:
55+
self.executor.add_node(self.camera_node)
56+
self.executor.add_node(self.pose3d_object)
57+
self.executor_thread = threading.Thread(target=self.executor.spin, daemon=True)
58+
self.executor_thread.start()
59+
3660
self.lap = Lap(self.pose3d_object)
3761

62+
if self.auto_image_mode:
63+
self.auto_image_thread = threading.Thread(
64+
target=self._unified_image_loop, daemon=True
65+
)
66+
self.auto_image_thread.start()
67+
3868
self.start()
3969

40-
# Process incoming messages to the GUI
41-
def gui_in_thread(self, ws, message):
70+
def _setup_auto_mode(self):
71+
"""Set up automatic image subscription"""
72+
try:
73+
temp_node = rclpy.create_node("topic_checker_temp")
74+
topic_names_and_types = temp_node.get_topic_names_and_types()
75+
topic_names = [topic_name for topic_name, _ in topic_names_and_types]
76+
77+
if "/webgui_image" in topic_names:
78+
self.camera_node = CameraNode("/webgui_image")
79+
self.auto_image_mode = True
80+
81+
temp_node.destroy_node()
82+
83+
except Exception:
84+
pass
4285

43-
# In this case, incoming msgs can only be acks
86+
def _unified_image_loop(self):
87+
"""Unified image handling loop"""
88+
while True:
89+
try:
90+
if self.camera_node:
91+
image = self.camera_node.getImage()
92+
if image is not None:
93+
self.showImage(image.data)
94+
95+
threading.Event().wait(0.033) # ~30 FPS
96+
except Exception:
97+
threading.Event().wait(1.0)
98+
99+
def gui_in_thread(self, ws, message):
44100
if "ack" in message:
45101
with self.ack_lock:
46102
self.ack = True
@@ -49,44 +105,35 @@ def gui_in_thread(self, ws, message):
49105
self.lap.unpause()
50106
elif "pause" in message:
51107
self.lap.pause()
52-
else:
53-
LogManager.logger.error("Unsupported msg")
54108

55-
# Prepares and sends a map to the websocket server
56109
def update_gui(self):
57-
58110
payload = self.payloadImage()
59111
self.payload["image"] = json.dumps(payload)
60112

61-
# Payload Lap Message
62113
lapped = self.lap.check_threshold()
63114
self.payload["lap"] = ""
64-
if lapped != None:
115+
if lapped is not None:
65116
self.payload["lap"] = str(lapped)
66117

67-
# Payload Map Message
68118
pose = self.pose3d_object.getPose3d()
69119
pos_message = str((pose.x, pose.y))
70120
self.payload["map"] = pos_message
71121

72122
message = json.dumps(self.payload)
73123
self.send_to_client(message)
74124

75-
# Function to prepare image payload
76-
# Encodes the image as a JSON string and sends through the WS
77125
def payloadImage(self):
78126
with self.image_show_lock:
79127
image_to_be_shown_updated = self.image_to_be_shown_updated
80128
image_to_be_shown = self.image_to_be_shown
81129

82-
image = image_to_be_shown
83130
payload = {"image": "", "shape": ""}
84131

85-
if not image_to_be_shown_updated:
132+
if not image_to_be_shown_updated or image_to_be_shown is None:
86133
return payload
87134

88-
shape = image.shape
89-
frame = cv2.imencode(".JPEG", image)[1]
135+
shape = image_to_be_shown.shape
136+
frame = cv2.imencode(".JPEG", image_to_be_shown)[1]
90137
encoded_image = base64.b64encode(frame)
91138

92139
payload["image"] = encoded_image.decode("utf-8")
@@ -97,20 +144,42 @@ def payloadImage(self):
97144

98145
return payload
99146

100-
# Function for student to call
101147
def showImage(self, image):
148+
"""Single point of entry for all images"""
102149
with self.image_show_lock:
103150
self.image_to_be_shown = image
104151
self.image_to_be_shown_updated = True
105152

153+
def get_image_mode(self):
154+
return {
155+
"auto_mode": self.auto_image_mode,
156+
"topic_subscribed": "/webgui_image" if self.auto_image_mode else None,
157+
"manual_mode_available": True,
158+
}
159+
106160

161+
# Create GUI instance directly
107162
host = "ws://127.0.0.1:2303"
108163
gui = WebGUI(host)
109-
110-
# Redirect the console
111164
start_console()
112165

113166

114-
# Expose to the user
115167
def showImage(image):
116-
gui.showImage(image)
168+
"""Display an image in the GUI"""
169+
if gui is not None:
170+
gui.showImage(image)
171+
172+
173+
def get_image_mode():
174+
if gui is not None:
175+
return gui.get_image_mode()
176+
return {"auto_mode": False, "topic_subscribed": None, "manual_mode_available": True}
177+
178+
179+
_gui = gui
180+
_gui_lock = threading.Lock()
181+
182+
183+
def get_gui():
184+
"""Backward compatibility function"""
185+
return gui

0 commit comments

Comments
 (0)