hackerbot 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
hackerbot/__init__.py ADDED
@@ -0,0 +1,33 @@
1
+ ################################################################################
2
+ # Copyright (c) 2025 Hackerbot Industries LLC
3
+ #
4
+ # This source code is licensed under the MIT license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+ #
7
+ # Created By: Allen Chien
8
+ # Created: April 2025
9
+ # Updated: 2025.04.08
10
+ #
11
+ # This is the file for the hackerbot package. It imports
12
+ # and initialized the sub components
13
+ #
14
+ # Special thanks to the following for their code contributions to this codebase:
15
+ # Allen Chien - https://github.com/AllenChienXXX
16
+ ################################################################################
17
+
18
+
19
+ from .core import Core
20
+ from .base import Base
21
+ from .head import Head
22
+ from .arm import Arm
23
+ from .utils.hackerbot_helper import HackerbotHelper
24
+
25
+ class Hackerbot(HackerbotHelper):
26
+ def __init__(self, port=None, board=None, model=None,verbose_mode=False):
27
+ super().__init__(port, board, verbose_mode)
28
+ # Share self (which is a HackerbotHelper) with subsystems
29
+ self.core = Core(controller=self)
30
+ self.base = Base(controller=self)
31
+ self.head = Head(controller=self)
32
+ self.arm = Arm(controller=self)
33
+ # TODO based on model decide which subsystems to initialize
@@ -0,0 +1,77 @@
1
+ ################################################################################
2
+ # Copyright (c) 2025 Hackerbot Industries LLC
3
+ #
4
+ # This source code is licensed under the MIT license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+ #
7
+ # Created By: Allen Chien
8
+ # Created: April 2025
9
+ # Updated: 2025.04.07
10
+ #
11
+ # This module contains the Arm component of the hackerbot
12
+ #
13
+ # Special thanks to the following for their code contributions to this codebase:
14
+ # Allen Chien - https://github.com/AllenChienXXX
15
+ ################################################################################
16
+
17
+
18
+ from hackerbot.utils.hackerbot_helper import HackerbotHelper
19
+ from .gripper import Gripper
20
+
21
+ class Arm():
22
+ def __init__(self, controller: HackerbotHelper):
23
+ self._controller = controller
24
+ self.idle_mode = True
25
+
26
+ self.setup()
27
+ self.gripper = Gripper(self._controller)
28
+
29
+ def setup(self):
30
+ if not self._controller._arm_attached:
31
+ self._controller.log_warning("Arm not attached, can't control arm.")
32
+
33
+ def move_joint(self, joint_id, angle, speed):
34
+ """
35
+ Moves a single joint of the robotic arm to a specified angle at a given speed.
36
+
37
+ Args:
38
+ joint_id (int): Joint number from 1 to 6. Joint 1 is the base and is in order moving up the arm.
39
+ angle (float): Angle for the specified joint. Valid range is -165.0 to 165.0 degrees for joints 1 to 5 and -175.0 to 175.0 for joint 6.
40
+ speed (int): Speed at which the arm moves to the new position. Valid range is 0 to 100.
41
+
42
+ Returns:
43
+ bool: True if the movement command was successfully sent, False if an error occurred.
44
+ """
45
+ try:
46
+ self._controller.send_raw_command(f"A_ANGLE,{joint_id},{angle},{speed}")
47
+ # Not fetching json response since machine mode not implemented
48
+ return True
49
+ except Exception as e:
50
+ self._controller.log_error(f"Error in arm:move_joint: {e}")
51
+ return False
52
+
53
+ def move_joints(self, j_agl_1, j_agl_2, j_agl_3, j_agl_4, j_agl_5, j_agl_6, speed):
54
+ """
55
+ Moves all six joints of the robotic arm to specified angles at a given speed.
56
+
57
+ Args:
58
+ j_agl_1 (float): Angle for joint 1, base joint. Valid range is -165.0 to 165.0 degrees.
59
+ j_agl_2 (float): Angle for joint 2. Valid range is -165.0 to 165.0 degrees.
60
+ j_agl_3 (float): Angle for joint 3. Valid range is -165.0 to 165.0 degrees.
61
+ j_agl_4 (float): Angle for joint 4. Valid range is -165.0 to 165.0 degrees.
62
+ j_agl_5 (float): Angle for joint 5. Valid range is -165.0 to 165.0 degrees.
63
+ j_agl_6 (float): Angle for joint 6. Valid range is -175.0 to 175.0 degrees.
64
+ speed (int): Speed at which the arm moves to the new positions. Valid range is 0 to 100.
65
+
66
+ Returns:
67
+ bool: True if the movement command was successfully sent, False if an error occurred.
68
+ """
69
+ try:
70
+ self._controller.send_raw_command(f"A_ANGLES,{j_agl_1},{j_agl_2},{j_agl_3},{j_agl_4},{j_agl_5},{j_agl_6},{speed}")
71
+ # Not fetching json response since machine mode not implemented
72
+ return True
73
+ except Exception as e:
74
+ self._controller.log_error(f"Error in arm:move_joints: {e}")
75
+ return False
76
+
77
+
@@ -0,0 +1,55 @@
1
+ ################################################################################
2
+ # Copyright (c) 2025 Hackerbot Industries LLC
3
+ #
4
+ # This source code is licensed under the MIT license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+ #
7
+ # Created By: Allen Chien
8
+ # Created: April 2025
9
+ # Updated: 2025.04.07
10
+ #
11
+ # This module contains the Gripper component of the hackerbot
12
+ #
13
+ # Special thanks to the following for their code contributions to this codebase:
14
+ # Allen Chien - https://github.com/AllenChienXXX
15
+ ################################################################################
16
+
17
+
18
+ from hackerbot.utils.hackerbot_helper import HackerbotHelper
19
+
20
+ class Gripper(HackerbotHelper):
21
+ def __init__(self, controller: HackerbotHelper):
22
+ self._controller = controller
23
+
24
+ def calibrate(self):
25
+ """
26
+ Calibrates the gripper by sending a raw calibration command.
27
+
28
+ Returns:
29
+ bool: True if the calibration command was successfully sent, False if an error occurred.
30
+ """
31
+ try:
32
+ self._controller.send_raw_command("A_CAL")
33
+ # Not fetching json response since machine mode not implemented
34
+ return True
35
+ except Exception as e:
36
+ self._controller.log_error(f"Error in gripper:calibrate: {e}")
37
+ return False
38
+
39
+ def open(self):
40
+ try:
41
+ self._controller.send_raw_command("A_OPEN")
42
+ # Not fetching json response since machine mode not implemented
43
+ return True
44
+ except Exception as e:
45
+ self._controller.log_error(f"Error in gripper:open: {e}")
46
+ return False
47
+
48
+ def close(self):
49
+ try:
50
+ self._controller.send_raw_command("A_CLOSE")
51
+ # Not fetching json response since machine mode not implemented
52
+ return True
53
+ except Exception as e:
54
+ self._controller.log_error(f"Error in gripper:close: {e}")
55
+ return False
@@ -0,0 +1,223 @@
1
+ ################################################################################
2
+ # Copyright (c) 2025 Hackerbot Industries LLC
3
+ #
4
+ # This source code is licensed under the MIT license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+ #
7
+ # Created By: Allen Chien
8
+ # Created: April 2025
9
+ # Updated: 2025.04.08
10
+ #
11
+ # This module contains the Base component of the hackerbot
12
+ #
13
+ # Special thanks to the following for their code contributions to this codebase:
14
+ # Allen Chien - https://github.com/AllenChienXXX
15
+ ################################################################################
16
+
17
+
18
+ from hackerbot.utils.hackerbot_helper import HackerbotHelper
19
+ from .maps import Maps
20
+ import time
21
+
22
+ class Base():
23
+ def __init__(self, controller: HackerbotHelper):
24
+ """
25
+ Initialize Core component with HackerbotHelper object
26
+
27
+ :param controller: HackerbotHelper object
28
+ """
29
+ self._controller = controller
30
+ self.initialize() # Call before any action is done on the base
31
+
32
+ self.maps = Maps(controller)
33
+
34
+ self._future_completed = False
35
+ self._docked = True # Default to true, assume always start from charger
36
+
37
+
38
+ def initialize(self):
39
+ try:
40
+ self._controller.send_raw_command("B_INIT")
41
+ self._controller._base_init = True
42
+ # Not fetching json response since machine mode not implemented
43
+ return True
44
+ except Exception as e:
45
+ self._controller.log_error(f"Error in base:initialize: {e}")
46
+ raise Exception(f"Error in initialize: {e}")
47
+
48
+ def set_mode(self, mode):
49
+ try:
50
+ self._controller.send_raw_command(f"B_MODE,{mode}")
51
+ # Not fetching json response since machine mode not implemented
52
+ return True
53
+ except Exception as e:
54
+ self._controller.log_error(f"Error in base:set_mode: {e}")
55
+ return False
56
+
57
+ def status(self):
58
+ try:
59
+ self._controller.send_raw_command("B_STATUS")
60
+ time.sleep(0.1)
61
+ response = self._controller.get_json_from_command("status")
62
+ if response is None:
63
+ raise Exception("Status command failed")
64
+
65
+ if response.get("left_set_speed") == 0 and response.get("right_set_speed") == 0:
66
+ self._future_completed = True
67
+ else:
68
+ self._future_completed = False
69
+
70
+ # Parse and return relevant fields
71
+ parsed_data = {
72
+ "timestamp": response.get("timestamp"),
73
+ "left_encoder": response.get("left_encoder"),
74
+ "right_encoder": response.get("right_encoder"),
75
+ "left_speed": response.get("left_speed"),
76
+ "right_speed": response.get("right_speed"),
77
+ "left_set_speed": response.get("left_set_speed"),
78
+ "right_set_speed": response.get("right_set_speed"),
79
+ "wall_tof": response.get("wall_tof"),
80
+ }
81
+ return parsed_data
82
+ except Exception as e:
83
+ self._controller.log_error(f"Error in base:status: {e}")
84
+ return None
85
+
86
+ def start(self, block=True):
87
+ try:
88
+ self._controller.send_raw_command("B_START")
89
+ # Not fetching json response since machine mode not implemented
90
+ self._controller._driver_mode = True
91
+ if self._docked:
92
+ time.sleep(2)
93
+ self._docked = False
94
+ self._wait_until_completed(block=block)
95
+
96
+ return True
97
+ except Exception as e:
98
+ self._controller.log_error(f"Error in base:start: {e}")
99
+ return False
100
+
101
+ def quickmap(self, block=True):
102
+ """
103
+ Start the quick mapping process.
104
+
105
+ This function sends a command to the base to initiate the quick mapping process.
106
+ It first checks the system status to ensure all components are ready. If
107
+ the quick mapping command is successfully sent, the function returns True.
108
+ In case of any errors, it logs the error message and returns False.
109
+
110
+ :return: True if the quick mapping command is successful, False otherwise.
111
+ """
112
+ try:
113
+ self._controller.send_raw_command("B_QUICKMAP")
114
+ time.sleep(0.1)
115
+ # Not fetching json response since machine mode not implemented
116
+ self._wait_until_completed(block=block)
117
+ return True
118
+ except Exception as e:
119
+ self._controller.log_error(f"Error in base:quickmap: {e}")
120
+ return False
121
+
122
+ def dock(self, block=True):
123
+ """
124
+ Dock the base to the docking station.
125
+
126
+ This function sends a command to the base to initiate the docking process.
127
+ It first checks the system status to ensure all components are ready. If
128
+ the docking command is successfully sent, the function returns True.
129
+ In case of any errors, it logs the error message and returns False.
130
+
131
+ :return: True if the docking command is successful, False otherwise.
132
+ """
133
+ try:
134
+ self._controller.send_raw_command("B_DOCK")
135
+ time.sleep(3)
136
+ # Not fetching json response since machine mode not implemented
137
+ self._wait_until_completed(block=block)
138
+ self._docked = True
139
+ self._controller._driver_mode = False
140
+ return True
141
+ except Exception as e:
142
+ self._controller.log_error(f"Error in base:dock: {e}")
143
+ return False
144
+
145
+
146
+ def kill(self):
147
+ """
148
+ Kill the base's movement. This is a blocking call and will not return until the base is stopped.
149
+ After calling this method, the base will not be able to move until start() is called again.
150
+ :return: True if successful, False otherwise.
151
+ """
152
+ try:
153
+ self._controller.send_raw_command("B_KILL")
154
+ self._controller._base_init = False
155
+ # Not fetching json response since machine mode not implemented
156
+ return True
157
+ except Exception as e:
158
+ self._controller.log_error(f"Error in base:kill: {e}")
159
+ return False
160
+
161
+ def trigger_bump(self, left, right):
162
+ """
163
+ Trigger the bump sensors on the base.
164
+
165
+ :param left: 0 or 1 to disable or enable the left bump sensor.
166
+ :param right: 0 or 1 to disable or enable the right bump sensor.
167
+ :return: True if the command is successful, False if it fails.
168
+ """
169
+ left = 1 if True else 0
170
+ right = 1 if True else 0
171
+ try:
172
+ self._controller.send_raw_command("B_BUMP, {0}, {1}".format(left, right))
173
+ # Not fetching json response since machine mode not implemented
174
+ return True
175
+ except Exception as e:
176
+ self._controller.log_error(f"Error in base:trigger_bump: {e}")
177
+ return False
178
+
179
+ def drive(self, l_vel, a_vel, block=True):
180
+ """
181
+ Set the base velocity.
182
+
183
+ :param l_vel: Linear velocity in mm/s. Positive is forward, negative is backward.
184
+ :param a_vel: Angular velocity in degrees/s. Positive is counterclockwise, negative is clockwise.
185
+ :return: True if the command is successful, False if it fails.
186
+ """
187
+ try:
188
+ if not self._controller._driver_mode:
189
+ self.start()
190
+ self._controller.send_raw_command(f"B_DRIVE,{l_vel},{a_vel}")
191
+ time.sleep(0.1)
192
+ response = self._controller.get_json_from_command("drive")
193
+ if response is None:
194
+ raise Exception("Drive command failed")
195
+ self._wait_until_completed(block=block)
196
+ return True
197
+ except Exception as e:
198
+ self._controller.log_error(f"Error in base:drive: {e}")
199
+ return False
200
+
201
+ def _wait_until_completed(self, block=True):
202
+ if not block:
203
+ return
204
+ while not self._future_completed:
205
+ self.status()
206
+ # print(self.status())
207
+ self._future_completed = False
208
+
209
+ def destroy(self, auto_dock=False):
210
+ """
211
+ Clean up and shut down the base.
212
+
213
+ This method kills the base's movement and optionally docks it before
214
+ destroying the controller. If `auto_dock` is set to True, the base will
215
+ dock before the destruction process.
216
+
217
+ :param auto_dock: If True, the base will dock before being destroyed. Defaults to False.
218
+ """
219
+ self.kill()
220
+ if auto_dock:
221
+ time.sleep(3.0)
222
+ self.dock(block=False)
223
+ self._controller.destroy()
hackerbot/base/maps.py ADDED
@@ -0,0 +1,147 @@
1
+ ################################################################################
2
+ # Copyright (c) 2025 Hackerbot Industries LLC
3
+ #
4
+ # This source code is licensed under the MIT license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+ #
7
+ # Created By: Allen Chien
8
+ # Created: April 2025
9
+ # Updated: 2025.04.07
10
+ #
11
+ # This module contains the Maps component of the hackerbot
12
+ #
13
+ # Special thanks to the following for their code contributions to this codebase:
14
+ # Allen Chien - https://github.com/AllenChienXXX
15
+ ################################################################################
16
+
17
+
18
+ from hackerbot.utils.hackerbot_helper import HackerbotHelper
19
+ import time
20
+
21
+ class Maps():
22
+ def __init__(self, controller: HackerbotHelper):
23
+ self._controller = controller
24
+ self._goto_completed = False
25
+
26
+ self.map_id = None
27
+ self._x = None
28
+ self._y = None
29
+ self._angle = None
30
+
31
+ self._goal_x = None
32
+ self._goal_y = None
33
+ self._goal_angle = None
34
+
35
+ self._docked = True
36
+
37
+ # Returns a string of map data
38
+ def fetch(self, map_id):
39
+ """
40
+ Fetch the map data for a given map id.
41
+
42
+ This function sends a command to the base to generate the map data
43
+ for a given map id. It first checks the system status to ensure all
44
+ components are ready. If the command is successfully sent, the
45
+ function returns the compressed map data as a string. In case of any
46
+ errors, it logs the error message and returns None.
47
+
48
+ :param map_id: The id of the map to fetch
49
+ :return: The compressed map data as a string if successful, None otherwise.
50
+ """
51
+ try:
52
+ # Check if controller and driver are initialized and in machine mode
53
+ command = f"B_MAPDATA,{map_id}"
54
+ self._controller.send_raw_command(command)
55
+ time.sleep(5) # Wait for map to be generated
56
+ map_data_json = self._controller.get_json_from_command("mapdata")
57
+ if map_data_json is None:
58
+ raise Exception("No map {map_id} found")
59
+ return map_data_json.get("compressedmapdata")
60
+ except Exception as e:
61
+ self._controller.log_error(f"Error in maps:fetch: {e}")
62
+ return None
63
+
64
+ # Returns a list of map ids
65
+ def list(self):
66
+ """
67
+ Get a list of available maps.
68
+
69
+ This function sends a command to the base to generate a list of available maps.
70
+ It first checks the system status to ensure all components are ready. If
71
+ the command is successfully sent, the function returns a list of map ids.
72
+ In case of any errors, it logs the error message and returns None.
73
+
74
+ :return: A list of map ids if successful, None otherwise.
75
+ """
76
+ try:
77
+ # Check if controller and driver are initialized and in machine mode
78
+ self._controller.send_raw_command("B_MAPLIST")
79
+ time.sleep(2) # Wait for map list to be generated
80
+ map_list_json = self._controller.get_json_from_command("maplist")
81
+ if map_list_json is None:
82
+ raise Exception("No maps found")
83
+ return map_list_json.get("map_ids")
84
+ except Exception as e:
85
+ self._controller.log_error(f"Error in maps:list: {e}")
86
+ return None
87
+
88
+ def goto(self, x, y, angle, speed, block=True):
89
+ """
90
+ Move the robot to the specified location on the map.
91
+
92
+ Args:
93
+ x (float): The x coordinate of the location to move to, in meters.
94
+ y (float): The y coordinate of the location to move to, in meters.
95
+ angle (float): The angle of the location to move to, in degrees.
96
+ speed (float): The speed at which to move to the location, in meters per second.
97
+
98
+ Returns:
99
+ bool: True if the command was successfully sent, False if an error occurred.
100
+ """
101
+ try:
102
+ command = f"B_GOTO,{x},{y},{angle},{speed}"
103
+ self._controller.send_raw_command(command)
104
+ self._goal_x = x
105
+ self._goal_y = y
106
+ self._goal_angle = angle
107
+ # Not fetching json response since machine mode not implemented
108
+ if self._docked == True:
109
+ time.sleep(2) # Some time to leave the base
110
+ self._docked = False
111
+ if block:
112
+ self._wait_until_reach_pose()
113
+ return True
114
+ except Exception as e:
115
+ self._controller.log_error(f"Error in maps:goto: {e}")
116
+ return False
117
+
118
+ def position(self):
119
+ try:
120
+ self._controller.send_raw_command("B_POSE")
121
+ time.sleep(0.1)
122
+ pose = self._controller.get_json_from_command("pose")
123
+ if pose is None:
124
+ raise Exception("No position found")
125
+ self.map_id = pose.get("map_id")
126
+ self._x = pose.get("pose_x")
127
+ self._y = pose.get("pose_y")
128
+ self._angle = pose.get("pose_angle")
129
+ # Not fetching json response since machine mode not implemented
130
+ return {"x": self._x, "y": self._y, "angle": self._angle}
131
+ except Exception as e:
132
+ self._controller.log_error(f"Error in base:position: {e}")
133
+ return False
134
+
135
+ def _wait_until_reach_pose(self):
136
+ while not self._goto_completed:
137
+ self.position()
138
+ self._calculate_position_offset()
139
+ self._goto_completed = False
140
+
141
+ def _calculate_position_offset(self):
142
+ x_offset = self._goal_x - self._x
143
+ y_offset = self._goal_y - self._y
144
+ angle_offset = (self._goal_angle - self._angle) // 180
145
+ # print("x_offset: {0}, y_offset: {1}, angle_offset: {2}".format(x_offset, y_offset, angle_offset))
146
+ if max(abs(x_offset), abs(y_offset), abs(angle_offset)) < 0.1:
147
+ self._goto_completed = True
hackerbot/core.py ADDED
@@ -0,0 +1,120 @@
1
+ ################################################################################
2
+ # Copyright (c) 2025 Hackerbot Industries LLC
3
+ #
4
+ # This source code is licensed under the MIT license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+ #
7
+ # Created By: Allen Chien
8
+ # Created: April 2025
9
+ # Updated: 2025.04.07
10
+ #
11
+ # This module contains the Core component of the hackerbot
12
+ #
13
+ # Special thanks to the following for their code contributions to this codebase:
14
+ # Allen Chien - https://github.com/AllenChienXXX
15
+ ################################################################################
16
+
17
+
18
+ from hackerbot.utils.hackerbot_helper import HackerbotHelper
19
+ import time
20
+ import json
21
+
22
+ class Core():
23
+ def __init__(self, controller: HackerbotHelper):
24
+ """
25
+ Initialize Core component with HackerbotHelper object
26
+
27
+ :param controller: HackerbotHelper object
28
+ """
29
+ self.tofs_enabled = controller._tofs_enabled
30
+ self.json_response = controller._json_mode
31
+
32
+ self._controller = controller
33
+
34
+ self.ping() # Ping to check attached components
35
+
36
+ def ping(self):
37
+ """
38
+ Pings the main controller to check component statuses and returns a JSON-style string
39
+ indicating the status of the components.
40
+ This is called during set up.
41
+
42
+ :return: JSON-style string of component statuses or None if there is an error
43
+ """
44
+ try:
45
+ self._controller.check_controller_init()
46
+ self._controller.send_raw_command("PING")
47
+ time.sleep(0.1)
48
+ response = self._controller.get_json_from_command("ping")
49
+ if response is None:
50
+ raise Exception("No response from main controller")
51
+
52
+ # Build JSON-style dict for robot state (excluding warnings)
53
+ robots_state = {
54
+ "main_controller_attached": False,
55
+ "temperature_sensor_attached": False,
56
+ "audio_mouth_eyes_attached": False,
57
+ "dynamixel_controller_attached": False,
58
+ "arm_control_attached": False
59
+ }
60
+
61
+ # Check component statuses
62
+ self._controller._main_controller_attached = response.get("main_controller") == "attached"
63
+ self._controller._temperature_sensor_attached = response.get("temperature_sensor") == "attached"
64
+ self._controller._left_tof_attached = response.get("left_tof") == "attached"
65
+ self._controller._right_tof_attached = response.get("right_tof") == "attached"
66
+ self._controller._audio_mouth_eyes_attached = response.get("audio_mouth_eyes") == "attached"
67
+ self._controller._dynamixel_controller_attached = response.get("dynamixel_controller") == "attached"
68
+ self._controller._arm_attached = response.get("arm_controller") == "attached"
69
+
70
+ if not self._controller._main_controller_attached:
71
+ self._controller.log_warning("Main controller not attached")
72
+ if not self._controller._temperature_sensor_attached:
73
+ self._controller.log_warning("Temperature sensor not attached")
74
+ if not self._controller._left_tof_attached:
75
+ self._controller.log_warning("Left TOF not attached")
76
+ if not self._controller._right_tof_attached:
77
+ self._controller.log_warning("Right TOF not attached")
78
+
79
+ # Update status
80
+ robots_state["main_controller_attached"] = self._controller._main_controller_attached
81
+ robots_state["temperature_sensor_attached"] = self._controller._temperature_sensor_attached
82
+ robots_state["left_tof_attached"] = self._controller._left_tof_attached
83
+ robots_state["right_tof_attached"] = self._controller._right_tof_attached
84
+ robots_state["audio_mouth_eyes_attached"] = self._controller._audio_mouth_eyes_attached
85
+ robots_state["dynamixel_controller_attached"] = self._controller._dynamixel_controller_attached
86
+ robots_state["arm_control_attached"] = self._controller._arm_attached
87
+ # Convert to JSON string (excluding warnings) before returning
88
+ return json.dumps(robots_state, indent=2)
89
+
90
+ except Exception as e:
91
+ self._controller.log_error(f"Error in core:ping: {e}")
92
+ return None
93
+
94
+ def version(self):
95
+ """
96
+ Get the version numbers of the main controller, audio mouth eyes, dynamixel controller, and arm controller.
97
+
98
+ :return: A JSON string containing the version numbers of the components.
99
+ """
100
+ try:
101
+ self._controller.check_controller_init()
102
+ self._controller.send_raw_command("VERSION")
103
+ time.sleep(0.1)
104
+ response = self._controller.get_json_from_command("version")
105
+ if response is None:
106
+ raise Exception("No response from main controller")
107
+
108
+ # Build response dictionary with all relevant version info
109
+ version_info = {
110
+ "main_controller_version": response.get("main_controller"),
111
+ "audio_mouth_eyes_version": response.get("audio_mouth_eyes"),
112
+ "dynamixel_controller_version": response.get("dynamixel_controller"),
113
+ "arm_controller_version": response.get("arm_controller")
114
+ }
115
+
116
+ return json.dumps(version_info, indent=2)
117
+
118
+ except Exception as e:
119
+ self._controller.log_error(f"Error in core:versions: {e}")
120
+ return None