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.
@@ -0,0 +1,206 @@
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.01
10
+ #
11
+ # This script is an example of how to use the base teleop behavior.
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 import Hackerbot
19
+ import time
20
+ import os
21
+
22
+ import sys, termios, atexit
23
+ from select import select
24
+
25
+ class KBHit:
26
+
27
+ def __init__(self):
28
+ '''Creates a KBHit object that you can call to do various keyboard things.
29
+ '''
30
+ # Save the terminal settings
31
+ self.fd = sys.stdin.fileno()
32
+ self.new_term = termios.tcgetattr(self.fd)
33
+ self.old_term = termios.tcgetattr(self.fd)
34
+
35
+ # New terminal setting unbuffered
36
+ self.new_term[3] = (self.new_term[3] & ~termios.ICANON & ~termios.ECHO)
37
+ termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.new_term)
38
+
39
+ # Support normal-terminal reset at exit
40
+ atexit.register(self.set_normal_term)
41
+
42
+
43
+ def set_normal_term(self):
44
+ ''' Resets to normal terminal. On Windows this is a no-op.
45
+ '''
46
+ termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.old_term)
47
+
48
+
49
+ def getch(self):
50
+ ''' Returns a keyboard character after kbhit() has been called.
51
+ '''
52
+ ch1 = sys.stdin.read(1)
53
+ if ch1 == '\x1b':
54
+ # special key pressed
55
+ ch2 = sys.stdin.read(1)
56
+ ch3 = sys.stdin.read(1)
57
+ ch = ch1 + ch2 + ch3
58
+ else:
59
+ # not a special key
60
+ ch = ch1
61
+ while sys.stdin in select([sys.stdin], [], [], 0)[0]:
62
+ sys.stdin.read(1)
63
+ return ch
64
+
65
+
66
+ def kbhit(self):
67
+ ''' Returns True if keyboard character was hit, False otherwise.
68
+ '''
69
+ dr,dw,de = select([sys.stdin], [], [], 0)
70
+ while sys.stdin in select([sys.stdin], [], [], 0)[0]:
71
+ sys.stdin.read(1)
72
+ return dr != []
73
+
74
+ class BaseTeleop:
75
+ def __init__(self):
76
+ self.kb = KBHit()
77
+
78
+ self.robot = Hackerbot()
79
+ self.robot.base.initialize()
80
+
81
+ # Modify movement parameters
82
+ self.step_size = 0.2 # mm
83
+ self.max_l_step_size = 300.0 # mm/s
84
+ self.max_r_step_size = 90.0 # degree/s
85
+
86
+ self.stop = False
87
+ self.last_key = None # Track last keypress
88
+
89
+ # Print initial instructions to terminal
90
+ self.print_terminal_instructions()
91
+
92
+ def print_terminal_instructions(self):
93
+ """Print instructions to the terminal"""
94
+ os.system('clear' if os.name == 'posix' else 'cls')
95
+ print("\n" + "="*10 + " Robot Teleop Controls " + "="*10 + "\r")
96
+ print("\nMoving controls:\r")
97
+ print(" ↑ / ↓ : forward/backward | ← / → : rotate left/right | space : stop\r")
98
+ print("o/p : increase/decrease step size by 10%\r")
99
+ print("\nCTRL-C or '0' to quit\r")
100
+ print("=" * 43 + "\r")
101
+
102
+ def update_display(self):
103
+ """Update step size in place without adding new lines"""
104
+ sys.stdout.write(f"\rCurrent step size: {self.step_size:.2f}m\r")
105
+ sys.stdout.flush() # Ensure immediate update
106
+
107
+ def get_command(self):
108
+ key = None
109
+ # Read keyboard input
110
+ if self.kb.kbhit() is not None:
111
+ key = self.kb.getch()
112
+ while sys.stdin in select([sys.stdin], [], [], 0)[0]:
113
+ sys.stdin.read(1)
114
+
115
+ if key == self.last_key:
116
+ self.last_key = None
117
+ return None, None
118
+
119
+ self.last_key = key # Update last key
120
+
121
+ # Check for quit conditions
122
+ if key == '0': # '0' key to quit
123
+ self.stop = True
124
+ return None, None
125
+
126
+ if key == 'o':
127
+ self.step_size += 0.1
128
+ elif key == 'p':
129
+ self.step_size -= 0.1
130
+ if key in ['\x1b[A', '\x1b[B', '\x1b[D', '\x1b[C', ' ']:
131
+ l_vel, r_vel = self.get_base_command_from_key(key)
132
+ else:
133
+ l_vel = None
134
+ r_vel = None
135
+
136
+ return l_vel, r_vel
137
+ else:
138
+ self.last_key = None
139
+ return 0.0, 0.0
140
+
141
+ def get_base_command_from_key(self, key):
142
+ if key == '\x1b[A': # Up arrow - Forward
143
+ l_vel = self.max_l_step_size * self.step_size
144
+ r_vel = 0.0
145
+ elif key == '\x1b[B': # Down arrow - Backward
146
+ l_vel = -self.max_l_step_size * self.step_size
147
+ r_vel = 0.0
148
+ elif key == '\x1b[D': # Left arrow - Rotate left
149
+ l_vel = 0.0
150
+ r_vel = self.max_r_step_size * self.step_size
151
+ elif key == '\x1b[C': # Right arrow - Rotate right
152
+ l_vel = 0.0
153
+ r_vel = -self.max_r_step_size * self.step_size
154
+ elif key == ' ': # Space - Stop
155
+ l_vel = 0.0
156
+ r_vel = 0.0
157
+ else:
158
+ l_vel = None
159
+ r_vel = None
160
+ return l_vel, r_vel
161
+
162
+ def run(self):
163
+ while not self.stop:
164
+ l_vel, r_vel = self.get_command()
165
+ if l_vel is not None and r_vel is not None:
166
+ respone = self.robot.base.drive(l_vel, r_vel, block=False)
167
+ if respone == False:
168
+ break
169
+ l_vel = None
170
+ r_vel = None
171
+ # time.sleep(0.01)
172
+ self.update_display()
173
+ def cleanup(self):
174
+ """Cleanup method to properly shut down the robot and restore terminal settings"""
175
+ try:
176
+ # Restore terminal settings
177
+ self.kb.set_normal_term()
178
+ # Destroy the robot connection
179
+ self.robot.base.destroy(auto_dock=True)
180
+
181
+ except Exception as e:
182
+ print(f"\nError during cleanup: {e}")
183
+ # Try to restore terminal settings even if there's an error
184
+ try:
185
+ self.kb.set_normal_term()
186
+ except:
187
+ pass
188
+
189
+ def __del__(self):
190
+ """Destructor to ensure cleanup is called"""
191
+ self.cleanup()
192
+
193
+
194
+ # Main entry point
195
+ if __name__ == '__main__':
196
+ teleop = None
197
+ try:
198
+ teleop = BaseTeleop()
199
+ teleop.run()
200
+ except KeyboardInterrupt:
201
+ print("\nShutting down...")
202
+ except Exception as e:
203
+ print(f"\nError: {e}")
204
+ finally:
205
+ if teleop:
206
+ teleop.cleanup()
@@ -0,0 +1,170 @@
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.01
10
+ #
11
+ # This script is an example of how to use the head teleop behavior.
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 import Hackerbot
19
+ import time
20
+ import os
21
+
22
+ import sys
23
+ from select import select
24
+ from base_teleop import KBHit
25
+
26
+ class HeadTeleop:
27
+ def __init__(self):
28
+ self.kb = KBHit()
29
+
30
+ self.robot = Hackerbot()
31
+ self.robot.head.set_idle_mode(False)
32
+
33
+ # Modify movement parameters
34
+ self.joint_speed = 50
35
+ self.step_size = 0.2 # mm
36
+
37
+ self.yaw = 180
38
+ self.pitch = 180
39
+
40
+ self.stop = False
41
+ self.last_key = None # Track last keypress
42
+
43
+ # Print initial instructions to terminal
44
+ self.print_terminal_instructions()
45
+
46
+ def print_terminal_instructions(self):
47
+ """Print static instructions to the terminal"""
48
+ os.system('clear' if os.name == 'posix' else 'cls')
49
+ print("\n" + "="*10 + " Robot Head Teleop Controls " + "="*10 + "\r")
50
+ print(" u/i : Yaw rotate L/R")
51
+ print(" j/k : Pitch rotate B/F")
52
+
53
+ print("\nOther Controls:")
54
+ print(" p/o : increase/decrease step size")
55
+ print(" -/+ : decrease/increase speed")
56
+ print("\nCTRL-C or 0 to quit")
57
+
58
+ # Reserve space for dynamic updates
59
+ print("\n" + "=" * 43 + "\r")
60
+
61
+ def update_display(self):
62
+ """Update step size and speed in place without adding new lines"""
63
+ sys.stdout.write(f"\rCurrent step size: {self.step_size:.1f} | Current speed: {self.joint_speed}% ")
64
+ sys.stdout.flush() # Ensure immediate update
65
+
66
+
67
+ def get_head_command(self):
68
+ key = None
69
+ # Read keyboard input
70
+ if self.kb.kbhit() is not None:
71
+ key = self.kb.getch()
72
+
73
+ while sys.stdin in select([sys.stdin], [], [], 0)[0]:
74
+ sys.stdin.read(1)
75
+
76
+ if key == self.last_key:
77
+ self.last_key = None
78
+ return None, None
79
+
80
+ self.last_key = key
81
+
82
+ # Check for quit conditions
83
+ if key == '0': # ESC or Ctrl-C
84
+ self.stop = True
85
+ return None, None
86
+
87
+ if key == 'o':
88
+ self.step_size += 0.1
89
+ elif key == 'p':
90
+ self.step_size -= 0.1
91
+ if key == '-':
92
+ self.joint_speed -= 10
93
+ if self.joint_speed < 0:
94
+ self.joint_speed = 0
95
+ elif key == '+':
96
+ self.joint_speed += 10
97
+ if self.joint_speed > 100:
98
+ self.joint_speed = 100
99
+
100
+ return self.get_head_value_from_key(key)
101
+ else:
102
+ self.last_key = None
103
+ return None, None
104
+
105
+ def get_head_value_from_key(self, key):
106
+ # Joint controls
107
+ if key == 'u': # Joint 1 left
108
+ self.yaw += self.step_size*100
109
+ self.yaw = max(100, min(260, self.yaw))
110
+ elif key == 'i': # Joint 1 right
111
+ self.yaw -= self.step_size*100
112
+ self.yaw = max(100, min(260, self.yaw))
113
+ elif key == 'j': # Joint 2 left
114
+ self.pitch += self.step_size*100
115
+ self.pitch = max(150, min(250, self.pitch))
116
+ elif key == 'k': # Joint 2 right
117
+ self.pitch -= self.step_size*100
118
+ self.pitch = max(150, min(250, self.pitch))
119
+ else:
120
+ return None, None
121
+
122
+ return self.yaw, self.pitch
123
+
124
+ def run(self):
125
+ while not self.stop:
126
+ y, p = self.get_head_command()
127
+ if y is not None and p is not None:
128
+ response = self.robot.head.look(y, p, self.joint_speed)
129
+
130
+ time.sleep(0.2)
131
+ if response == False:
132
+ break
133
+ self.update_display()
134
+
135
+ def cleanup(self):
136
+ """Cleanup method to properly shut down the robot and restore terminal settings"""
137
+ try:
138
+ # Restore terminal settings
139
+ self.kb.set_normal_term()
140
+ # Dock the robot
141
+ self.robot.head.look(180,180,50)
142
+ # Destroy the robot connection
143
+ self.robot.destroy()
144
+
145
+ except Exception as e:
146
+ print(f"\nError during cleanup: {e}")
147
+ # Try to restore terminal settings even if there's an error
148
+ try:
149
+ self.kb.set_normal_term()
150
+ except:
151
+ pass
152
+
153
+ def __del__(self):
154
+ """Destructor to ensure cleanup is called"""
155
+ self.cleanup()
156
+
157
+
158
+ # Main entry point
159
+ if __name__ == '__main__':
160
+ teleop = None
161
+ try:
162
+ teleop = HeadTeleop()
163
+ teleop.run()
164
+ except KeyboardInterrupt:
165
+ print("\nShutting down...")
166
+ except Exception as e:
167
+ print(f"\nError: {e}")
168
+ finally:
169
+ if teleop:
170
+ teleop.cleanup()
@@ -0,0 +1,61 @@
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 Head 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 .eyes import Eyes
20
+
21
+ class Head():
22
+ def __init__(self, controller: HackerbotHelper):
23
+ self._controller = controller
24
+ self.idle_mode = True
25
+
26
+ self.setup()
27
+ self.eyes = Eyes(self._controller)
28
+
29
+ def setup(self):
30
+ if not self._controller._dynamixel_controller_attached:
31
+ self._controller.log_warning("Dynamixel controller not attached, can't control head.")
32
+ if not self._controller._audio_mouth_eyes_attached:
33
+ self._controller.log_warning("Audio mouth and eyes not attached, can't control eyes.")
34
+
35
+ self.set_idle_mode(True)
36
+
37
+ # float: yaw - Unit is in degrees (eg. 180 degrees). Valid values are in the range of 100.0 to 260.0
38
+ # float: pitch - Unit is in degrees (eg. 180 degrees). Valid values are in the range of 150.0 to 250.0
39
+ # int: speed - Unitless. Valid values are integers in the range of 6 (slow) to 70 (fast)
40
+ def look(self, yaw, pitch, speed):
41
+ try:
42
+ self.set_idle_mode(False)
43
+ self._controller.send_raw_command(f"H_LOOK, {yaw}, {pitch}, {speed}")
44
+ # Not fetching json response since machine mode not implemented
45
+ return True
46
+ except Exception as e:
47
+ self._controller.log_error(f"Error in head:look: {e}")
48
+ return False
49
+
50
+ def set_idle_mode(self, mode):
51
+ try:
52
+ if mode:
53
+ self._controller.send_raw_command("H_IDLE, 1")
54
+ else:
55
+ self._controller.send_raw_command("H_IDLE, 0")
56
+ # Not fetching json response since machine mode not implemented
57
+ self.idle_mode = mode
58
+ return True
59
+ except Exception as e:
60
+ self._controller.log_error(f"Error in head:set_idle_mode: {e}")
61
+ return False
hackerbot/head/eyes.py ADDED
@@ -0,0 +1,41 @@
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 Eyes 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 Eyes():
21
+ def __init__(self, controller: HackerbotHelper):
22
+ self._controller = controller
23
+
24
+ def gaze(self, x, y):
25
+ """
26
+ Move the eyes to the specified position in the view.
27
+
28
+ Args:
29
+ x (float): x position between -1.0 and 1.0
30
+ y (float): y position between -1.0 and 1.0
31
+
32
+ Returns:
33
+ bool: Whether the command was successful
34
+ """
35
+ try:
36
+ self._controller.send_raw_command(f"H_GAZE,{x},{y}")
37
+ # Not fetching json response since machine mode not implemented
38
+ return True
39
+ except Exception as e:
40
+ self._controller.log_error(f"Error in eyes:gaze: {e}")
41
+ return False
@@ -0,0 +1,142 @@
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 HackerbotHelper class, which is a subclass of SerialHelper.
12
+ # It contains the fields that will be share among higher level classes.
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 .serial_helper import SerialHelper
20
+ import time
21
+ import logging
22
+
23
+ class HackerbotHelper(SerialHelper):
24
+ def __init__(self, port=None, board=None, verbose_mode=False):
25
+ self._error_msg = ""
26
+ self._warning_msg = ""
27
+ self._v_mode = verbose_mode
28
+ self._main_controller_init = False # Ensure this is set before exception handling
29
+
30
+ self._json_mode = False
31
+
32
+ self._main_controller_attached = False
33
+ self._temperature_sensor_attached = False
34
+
35
+ self._left_tof_attached = False
36
+ self._right_tof_attached = False
37
+
38
+ self._tofs_enabled = False
39
+
40
+ self._base_init = False
41
+ self._driver_mode = False
42
+
43
+ self._audio_mouth_eyes_attached = False
44
+ self._dynamixel_controller_attached = False
45
+
46
+ self._arm_attached = False
47
+
48
+ self._port = port
49
+ self._board = board
50
+
51
+ self.setup()
52
+
53
+
54
+ def setup(self):
55
+ """
56
+ Initialize the main controller.
57
+
58
+ If self._port and self._board are None, this method will find the first available serial port and use it to initialize the main controller.
59
+ Otherwise, it will use the provided port and board to initialize.
60
+ After initialization, it will set the main controller to JSON mode and enable the TOFs.
61
+ This setup ensures json mode are set and controller is initialized properly.
62
+ If an exception occurs during initialization, it will raise an exception with the error message.
63
+ """
64
+ try:
65
+ if self._port is None or self._board is None:
66
+ super().__init__()
67
+ self._board, self._port = super().get_board_and_port()
68
+ else:
69
+ super().__init__(self._port, self._board)
70
+
71
+ self._main_controller_init = True
72
+ self.set_json_mode(True)
73
+ # self.set_TOFs(True)
74
+ except Exception as e:
75
+ raise Exception(f"Error in setting up hackerbot helper: {e}")
76
+
77
+ # Activate JSON mode
78
+ def set_json_mode(self, mode):
79
+ try:
80
+ if mode == True:
81
+ super().send_raw_command("JSON, 1")
82
+ time.sleep(0.1) # Short sleep to process json response
83
+ response = super().get_json_from_command("json")
84
+ if response is None:
85
+ raise Exception("Failed to set json mode to: ", mode)
86
+ else:
87
+ super().send_raw_command("JSON, 0")
88
+ self._json_mode = mode
89
+ except Exception as e:
90
+ raise Exception(f"Error in set_json_mode: {e}")
91
+
92
+ #Set TOFs
93
+ def set_TOFs(self, mode):
94
+ try:
95
+ if not self._left_tof_attached or not self._right_tof_attached:
96
+ raise Exception("TOFs not attached")
97
+ if mode == True:
98
+ super().send_raw_command("TOFS, 1")
99
+ else:
100
+ super().send_raw_command("TOFS, 0")
101
+ time.sleep(0.1) # Short sleep to process json response
102
+ response = super().get_json_from_command("tofs")
103
+ if response is None:
104
+ raise Exception("TOFs activation failed")
105
+ self._tofs_enabled = mode
106
+ except Exception as e:
107
+ raise Exception(f"Error in set_TOFs: {e}")
108
+
109
+ def get_current_action(self):
110
+ return super().get_state()
111
+
112
+ def get_error(self):
113
+ # Serial error should be priority
114
+ if super().get_ser_error() is not None:
115
+ return super().get_ser_error()
116
+ else:
117
+ return self._error_msg
118
+
119
+ def log_error(self, error):
120
+ if self._v_mode:
121
+ logging.error(error)
122
+ self._error_msg = error
123
+
124
+ def log_warning(self, warning):
125
+ if self._v_mode:
126
+ logging.warning(warning)
127
+ self._warning_msg = warning
128
+
129
+ def check_controller_init(self):
130
+ if not self._main_controller_init:
131
+ raise Exception("Main controller not initialized.")
132
+ if not self._json_mode:
133
+ raise Exception("JSON mode not enabled.")
134
+
135
+ def destroy(self):
136
+ try:
137
+ super().disconnect_serial()
138
+ self._main_controller_init = False
139
+ return True
140
+ except Exception as e:
141
+ self.log_error(f"Error in destroy: {e}")
142
+ return False