5
5
import rclpy
6
6
from gui_interfaces .general .measuring_threading_gui import MeasuringThreadingGUI
7
7
from console_interfaces .general .console import start_console
8
+
9
+ from hal_interfaces .general .camera import CameraNode
10
+ from cv_bridge import CvBridge
8
11
from hal_interfaces .general .odometry import OdometryNode
9
12
from lap import Lap
13
+ from sensor_msgs .msg import Image
14
+ from rclpy .node import Node
10
15
11
16
import sys
12
17
13
18
sys .path .insert (0 , "/RoboticsApplicationManager" )
14
19
15
20
from manager .ram_logging .log_manager import LogManager
16
21
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 )
18
29
19
30
20
31
class WebGUI (MeasuringThreadingGUI ):
@@ -25,22 +36,67 @@ def __init__(self, host="ws://127.0.0.1:2303"):
25
36
self .image_to_be_shown_updated = False
26
37
self .image_show_lock = threading .Lock ()
27
38
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
+
29
47
self .payload = {"image" : "" , "lap" : "" , "map" : "" }
30
- # TODO: maybe move this to HAL and have it be hybrid
48
+ self .bridge = CvBridge ()
49
+
31
50
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
+
36
60
self .lap = Lap (self .pose3d_object )
37
61
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
+
38
68
self .start ()
39
69
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
42
85
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 ):
44
100
if "ack" in message :
45
101
with self .ack_lock :
46
102
self .ack = True
@@ -49,44 +105,35 @@ def gui_in_thread(self, ws, message):
49
105
self .lap .unpause ()
50
106
elif "pause" in message :
51
107
self .lap .pause ()
52
- else :
53
- LogManager .logger .error ("Unsupported msg" )
54
108
55
- # Prepares and sends a map to the websocket server
56
109
def update_gui (self ):
57
-
58
110
payload = self .payloadImage ()
59
111
self .payload ["image" ] = json .dumps (payload )
60
112
61
- # Payload Lap Message
62
113
lapped = self .lap .check_threshold ()
63
114
self .payload ["lap" ] = ""
64
- if lapped != None :
115
+ if lapped is not None :
65
116
self .payload ["lap" ] = str (lapped )
66
117
67
- # Payload Map Message
68
118
pose = self .pose3d_object .getPose3d ()
69
119
pos_message = str ((pose .x , pose .y ))
70
120
self .payload ["map" ] = pos_message
71
121
72
122
message = json .dumps (self .payload )
73
123
self .send_to_client (message )
74
124
75
- # Function to prepare image payload
76
- # Encodes the image as a JSON string and sends through the WS
77
125
def payloadImage (self ):
78
126
with self .image_show_lock :
79
127
image_to_be_shown_updated = self .image_to_be_shown_updated
80
128
image_to_be_shown = self .image_to_be_shown
81
129
82
- image = image_to_be_shown
83
130
payload = {"image" : "" , "shape" : "" }
84
131
85
- if not image_to_be_shown_updated :
132
+ if not image_to_be_shown_updated or image_to_be_shown is None :
86
133
return payload
87
134
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 ]
90
137
encoded_image = base64 .b64encode (frame )
91
138
92
139
payload ["image" ] = encoded_image .decode ("utf-8" )
@@ -97,20 +144,42 @@ def payloadImage(self):
97
144
98
145
return payload
99
146
100
- # Function for student to call
101
147
def showImage (self , image ):
148
+ """Single point of entry for all images"""
102
149
with self .image_show_lock :
103
150
self .image_to_be_shown = image
104
151
self .image_to_be_shown_updated = True
105
152
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
+
106
160
161
+ # Create GUI instance directly
107
162
host = "ws://127.0.0.1:2303"
108
163
gui = WebGUI (host )
109
-
110
- # Redirect the console
111
164
start_console ()
112
165
113
166
114
- # Expose to the user
115
167
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