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 +33 -0
- hackerbot/arm/__init__.py +77 -0
- hackerbot/arm/gripper.py +55 -0
- hackerbot/base/__init__.py +223 -0
- hackerbot/base/maps.py +147 -0
- hackerbot/core.py +120 -0
- hackerbot/examples/keyboard_teleop_examples/AI_ELITE_teleop.py +219 -0
- hackerbot/examples/keyboard_teleop_examples/AI_PRO_teleop.py +180 -0
- hackerbot/examples/keyboard_teleop_examples/arm_teleop.py +230 -0
- hackerbot/examples/keyboard_teleop_examples/base_teleop.py +206 -0
- hackerbot/examples/keyboard_teleop_examples/head_teleop.py +170 -0
- hackerbot/head/__init__.py +61 -0
- hackerbot/head/eyes.py +41 -0
- hackerbot/utils/hackerbot_helper.py +142 -0
- hackerbot/utils/serial_helper.py +156 -0
- hackerbot-0.2.0.dist-info/METADATA +67 -0
- hackerbot-0.2.0.dist-info/RECORD +20 -0
- hackerbot-0.2.0.dist-info/WHEEL +5 -0
- hackerbot-0.2.0.dist-info/licenses/LICENSE +21 -0
- hackerbot-0.2.0.dist-info/top_level.txt +1 -0
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
|
+
|
hackerbot/arm/gripper.py
ADDED
@@ -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
|