@tigerpython/robotics-libraries 1.4.0
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.
- package/CHANGELOG +68 -0
- package/LICENSE +373 -0
- package/README.md +81 -0
- package/calliope/README.md +15 -0
- package/calliope/callibot.py +176 -0
- package/calliope/callibotmot.py +46 -0
- package/calliope/callimk.py +143 -0
- package/calliope/cbalarm.py +14 -0
- package/calliope/cpglow.py +76 -0
- package/calliope/cpmike.py +16 -0
- package/calliope/cprover.py +33 -0
- package/calliope/cputils.py +23 -0
- package/calliope/libraries.json +10 -0
- package/calliope/libraries.raw.json +10 -0
- package/calliope/min/callibot.py +97 -0
- package/calliope/min/callibotmot.py +23 -0
- package/calliope/min/callimk.py +47 -0
- package/calliope/min/cbalarm.py +6 -0
- package/calliope/min/cpglow.py +29 -0
- package/calliope/min/cpmike.py +8 -0
- package/calliope/min/cprover.py +9 -0
- package/calliope/min/cputils.py +7 -0
- package/dist/index.d.mts +124 -0
- package/dist/index.d.ts +124 -0
- package/dist/index.js +2896 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2862 -0
- package/dist/index.mjs.map +1 -0
- package/microbit/README.md +48 -0
- package/microbit/controller.py +212 -0
- package/microbit/huskylens.py +479 -0
- package/microbit/libraries.json +19 -0
- package/microbit/libraries.raw.json +19 -0
- package/microbit/mbalarm.py +15 -0
- package/microbit/mbbitbot.py +127 -0
- package/microbit/mbglow.py +77 -0
- package/microbit/mbled.py +56 -0
- package/microbit/mbmarsrover.py +216 -0
- package/microbit/mbminibit.py +139 -0
- package/microbit/mbrobot.py +401 -0
- package/microbit/mbrobot_legacy.py +90 -0
- package/microbit/mbrobot_plus.py +179 -0
- package/microbit/mbrobot_plusV2.py +490 -0
- package/microbit/mbrobot_plusV3.py +427 -0
- package/microbit/mbrobotmot.py +49 -0
- package/microbit/mbthetabot.py +167 -0
- package/microbit/mbwait.py +50 -0
- package/microbit/mbxgo.py +173 -0
- package/microbit/min/controller.py +42 -0
- package/microbit/min/huskylens.py +145 -0
- package/microbit/min/mbalarm.py +6 -0
- package/microbit/min/mbbitbot.py +40 -0
- package/microbit/min/mbglow.py +29 -0
- package/microbit/min/mbled.py +18 -0
- package/microbit/min/mbmarsrover.py +62 -0
- package/microbit/min/mbminibit.py +50 -0
- package/microbit/min/mbrobot.py +82 -0
- package/microbit/min/mbrobot_legacy.py +45 -0
- package/microbit/min/mbrobot_plus.py +75 -0
- package/microbit/min/mbrobot_plusV2.py +102 -0
- package/microbit/min/mbrobot_plusV3.py +194 -0
- package/microbit/min/mbrobotmot.py +25 -0
- package/microbit/min/mbthetabot.py +47 -0
- package/microbit/min/mbwait.py +23 -0
- package/microbit/min/mbxgo.py +37 -0
- package/package.json +54 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# Controller v1.0, Date 21.06.24
|
|
2
|
+
# Enables easy use of the DFRobot Micro:bit GamePad by simulating buttons the same way as the microbit internal ones.
|
|
3
|
+
from microbit import run_every, pin13, pin14, pin15, pin16, pin8, pin1, pin2, pin12, button_a, button_b, sleep
|
|
4
|
+
|
|
5
|
+
class _Controller_Button:
|
|
6
|
+
#""" Simulated Button for regular pins with same functionality as the micro:bit buttons a and b."""
|
|
7
|
+
|
|
8
|
+
def __init__(self, pin):
|
|
9
|
+
# """create a new simulated button from a digital pin
|
|
10
|
+
|
|
11
|
+
# Parameter:
|
|
12
|
+
# pin (MicroBitDigitalPin): pin to use as button
|
|
13
|
+
# """
|
|
14
|
+
self._pin = pin
|
|
15
|
+
self.previous_state = 0
|
|
16
|
+
self._press_count = 0
|
|
17
|
+
self._pressed_before = False
|
|
18
|
+
pin.set_pull(pin.PULL_UP)
|
|
19
|
+
|
|
20
|
+
def _update_state(self):
|
|
21
|
+
# """ internally update the buttons state."""
|
|
22
|
+
current_state = 1 - self._pin.read_digital()
|
|
23
|
+
|
|
24
|
+
if current_state == 1 and self.previous_state == 0: # Low -> High
|
|
25
|
+
self._press_count += 1
|
|
26
|
+
self._pressed_before = True
|
|
27
|
+
|
|
28
|
+
self.previous_state = current_state
|
|
29
|
+
|
|
30
|
+
def is_pressed(self):
|
|
31
|
+
# """ check if the button is currently pressed.
|
|
32
|
+
|
|
33
|
+
# Returns:
|
|
34
|
+
# bool: if button is currently pressed.
|
|
35
|
+
# """
|
|
36
|
+
return False if self._pin.read_digital() else True
|
|
37
|
+
|
|
38
|
+
def was_pressed(self):
|
|
39
|
+
# """ check if button was pressed down before. Resets upon call.
|
|
40
|
+
|
|
41
|
+
# Returns:
|
|
42
|
+
# bool: True if button was pressed down after last call of this function.
|
|
43
|
+
# """
|
|
44
|
+
state = self._pressed_before
|
|
45
|
+
self._pressed_before = False
|
|
46
|
+
return state
|
|
47
|
+
|
|
48
|
+
def get_presses(self):
|
|
49
|
+
# """ get the amount of button presses since last call.
|
|
50
|
+
|
|
51
|
+
# Returns:
|
|
52
|
+
# int: number of button (down) presses since last call of this function.
|
|
53
|
+
# """
|
|
54
|
+
count = self._press_count
|
|
55
|
+
self._press_count = 0
|
|
56
|
+
return count
|
|
57
|
+
|
|
58
|
+
class _Controller_Analog_Stick:
|
|
59
|
+
# """ class that encapsulates the right analog stick of the controller.
|
|
60
|
+
# This includes the turning in an xy-plane and the pressing (z-button)."""
|
|
61
|
+
|
|
62
|
+
def __init__(self, pinX, pinY, pinZ):
|
|
63
|
+
# """ creates a new analog-stick given its input pins.
|
|
64
|
+
|
|
65
|
+
# Parameters:
|
|
66
|
+
# pinX (MicroBitAnalogDigitalPin): The pin to use for the x-axis (left-right).
|
|
67
|
+
# pinY (MicroBitAnalogDigitalPin): The pin to use for the y-axis (up-down).
|
|
68
|
+
# pinZ (MicroBitDigitalPin): The pin to use for the z-button.
|
|
69
|
+
# """
|
|
70
|
+
self.pin_x = pinX
|
|
71
|
+
self.pin_y = pinY
|
|
72
|
+
self.button_z = _Controller_Button(pinZ)
|
|
73
|
+
self.dead_zone = 0.01
|
|
74
|
+
self.center_x = 0
|
|
75
|
+
self.center_y = 0
|
|
76
|
+
self.min_x = -1
|
|
77
|
+
self.max_x = 1
|
|
78
|
+
self.min_y = -1
|
|
79
|
+
self.max_y = 1
|
|
80
|
+
|
|
81
|
+
def calibrate(self, dead_zone,
|
|
82
|
+
center_x=0.0, center_y=0.0,
|
|
83
|
+
x_min=-1.0, x_max=1.0,
|
|
84
|
+
y_min=-1.0, y_max=1.0):
|
|
85
|
+
# """ calibrate the analog-stick to remove drift (nonzero values even in resting position)
|
|
86
|
+
# and rescale to full [-1,1] range for the xy-inputs.
|
|
87
|
+
|
|
88
|
+
# After calibration, the analog-stick
|
|
89
|
+
# - does not react (returns 0) until it exceeds a value above the "dead_zone" range.
|
|
90
|
+
# - returns values in the range [-1, 1] when pushed to the extremes and 0 at rest.
|
|
91
|
+
|
|
92
|
+
# Parameters:
|
|
93
|
+
# dead_zone (int): range of xy-values (negative too) that should be mapped to 0.
|
|
94
|
+
# Default: 0.01
|
|
95
|
+
# center_x (int): returned value for get_x, when the stick is not moved (before calibration).
|
|
96
|
+
# Default: 0.0
|
|
97
|
+
# center_y (int): returned value for get_y, when the stick is not moved (before calibration).
|
|
98
|
+
# Default: 0.0
|
|
99
|
+
# x_min (int): minimal possible value for x (before calibration). Default: -1.0
|
|
100
|
+
# x_max (int): maximal possible value for x (before calibration). Default: 1.0
|
|
101
|
+
# y_min (int): minimal possible value for y (before calibration). Default: -1.0
|
|
102
|
+
# y_max (int): maximal possible value for y (before calibration). Default: 1.0
|
|
103
|
+
# """
|
|
104
|
+
self.dead_zone = min(0.2, max(0.01,dead_zone))
|
|
105
|
+
self.center_x = min(0.4, max(-0.4,center_x))
|
|
106
|
+
self.center_y = min(0.5, max(-0.5,center_y))
|
|
107
|
+
self.min_x = min(-0.5, max(-1.0,x_min))
|
|
108
|
+
self.max_x = min(1.0, max(0.5,x_max))
|
|
109
|
+
self.min_y = min(-0.5, max(-1.0,y_min))
|
|
110
|
+
self.max_y = min(1.0, max(0.5,y_max))
|
|
111
|
+
|
|
112
|
+
def _remap(self, value, center, v_min, v_max):
|
|
113
|
+
# """ internal function that applies remapping of the values according to the calibration.
|
|
114
|
+
# The returned value will live in a possible space from [-1,1] with center at 0.
|
|
115
|
+
|
|
116
|
+
# Parameters:
|
|
117
|
+
# value (float): the input value to remap to the desired range.
|
|
118
|
+
# center (float): the resting value before calibration.
|
|
119
|
+
# v_min (float): the minimally attainable value before calibration.
|
|
120
|
+
# v_max (float): the maximally attainable value before calibration.
|
|
121
|
+
|
|
122
|
+
# Returns:
|
|
123
|
+
# v (float): remapped parameter value according to calibration.
|
|
124
|
+
# """
|
|
125
|
+
v = value - 1.0 - center
|
|
126
|
+
if abs(v) - self.dead_zone/2.0 > 0.0:
|
|
127
|
+
if v < 0.0:
|
|
128
|
+
v = v / abs(v_min - center)
|
|
129
|
+
else:
|
|
130
|
+
v = v / abs(v_max - center)
|
|
131
|
+
v = min(1.0, max(-1.0, v))
|
|
132
|
+
v = 0.0 if abs(v) <= self.dead_zone else v
|
|
133
|
+
return v
|
|
134
|
+
|
|
135
|
+
def get_x(self):
|
|
136
|
+
# """ get the current x-axis value of the analog stick.
|
|
137
|
+
|
|
138
|
+
# Returns:
|
|
139
|
+
# (float): position of x-axis in range [-1,1] (left to right). 0 is resting position. (after calibration)
|
|
140
|
+
# """
|
|
141
|
+
v_raw = (self.pin_x.read_analog() / 512.0)
|
|
142
|
+
v = self._remap(v_raw, self.center_x, self.min_x, self.max_x)
|
|
143
|
+
return v
|
|
144
|
+
|
|
145
|
+
def get_y(self):
|
|
146
|
+
# """ get the current y-axis value of the analog stick.
|
|
147
|
+
|
|
148
|
+
# Returns:
|
|
149
|
+
# (float): position of y-axis in range [-1,1] (down to up). 0 is resting position. (after calibration)
|
|
150
|
+
# """
|
|
151
|
+
v_raw = (self.pin_y.read_analog() / 512.0)
|
|
152
|
+
v = self._remap(v_raw, self.center_y, self.min_y, self.max_y)
|
|
153
|
+
return v
|
|
154
|
+
|
|
155
|
+
def get_z(self):
|
|
156
|
+
# """ get the current z-button state as an int. 1 = pressed, 0 = not pressed. """
|
|
157
|
+
return 1 if self.button_z.is_pressed() else 0
|
|
158
|
+
|
|
159
|
+
def is_pressed(self):
|
|
160
|
+
# """ returns if the z-button is currently pressed as a bool."""
|
|
161
|
+
return self.button_z.is_pressed()
|
|
162
|
+
|
|
163
|
+
def was_pressed(self):
|
|
164
|
+
# """ returns if the z-button was pressed (down) before the last call to this function."""
|
|
165
|
+
return self.button_z.was_pressed()
|
|
166
|
+
|
|
167
|
+
def get_presses(self):
|
|
168
|
+
# """ returns how often the z-button was pressed (down) before the last call to this function."""
|
|
169
|
+
return self.button_z.get_presses()
|
|
170
|
+
|
|
171
|
+
# Controller LED and vibration motor
|
|
172
|
+
def vibrate(state):
|
|
173
|
+
# """ Set the state of the controllers vibration motor. 0=Off, 1=On.
|
|
174
|
+
# Also affects the controllers blue LED next to the motor. """
|
|
175
|
+
pin12.write_digital(state)
|
|
176
|
+
|
|
177
|
+
# Left side of controller (black joystick)
|
|
178
|
+
joystick = _Controller_Analog_Stick(pin1, pin2, pin8)
|
|
179
|
+
# also make the joystick button accessible independently.
|
|
180
|
+
button_z = joystick.button_z
|
|
181
|
+
|
|
182
|
+
# Right side of controller (4 colored buttons)
|
|
183
|
+
button_c = _Controller_Button(pin13)
|
|
184
|
+
button_green = button_c
|
|
185
|
+
button_d = _Controller_Button(pin14)
|
|
186
|
+
button_yellow = button_d
|
|
187
|
+
button_e = _Controller_Button(pin15)
|
|
188
|
+
button_red = button_e
|
|
189
|
+
button_f = _Controller_Button(pin16)
|
|
190
|
+
button_blue = button_f
|
|
191
|
+
|
|
192
|
+
# Back side of controller (2 white buttons, same as default buttons)
|
|
193
|
+
trigger_left = button_a
|
|
194
|
+
trigger_right = button_b
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _update_controller_buttons():
|
|
198
|
+
# """ function to be called repeatedly that updates the state of all the controllers buttons.
|
|
199
|
+
# It is necessary to run this function regularly to simulate the standard microbit button behaviour. """
|
|
200
|
+
global button_c
|
|
201
|
+
global button_d
|
|
202
|
+
global button_e
|
|
203
|
+
global button_f
|
|
204
|
+
global joystick
|
|
205
|
+
|
|
206
|
+
button_c._update_state()
|
|
207
|
+
button_d._update_state()
|
|
208
|
+
button_e._update_state()
|
|
209
|
+
button_f._update_state()
|
|
210
|
+
joystick.button_z._update_state()
|
|
211
|
+
|
|
212
|
+
run_every(_update_controller_buttons, days=0, h=0, min=0, s=0, ms=33) # ~30 Updates per Second
|
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
# For Huskylens (PRO inclusive) with Firmware version 0.5.1Norm or 0.5.3Alpha1.
|
|
2
|
+
from microbit import i2c, sleep, running_time
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
_algorithm_names = ["FaceRecognition",
|
|
6
|
+
"ObjectTracking",
|
|
7
|
+
"ObjectRecognition",
|
|
8
|
+
"LineTracking",
|
|
9
|
+
"ColorRecognition",
|
|
10
|
+
"TagRecognition",
|
|
11
|
+
"ObjectClassification",
|
|
12
|
+
"QRRecognition",
|
|
13
|
+
"BarcodeRecognition"]
|
|
14
|
+
|
|
15
|
+
class Request_Command:
|
|
16
|
+
# """Command Codes that can be sent to the Huskylens.""""
|
|
17
|
+
KNOCK = 0x2C
|
|
18
|
+
ALGORITHM = 0x2D
|
|
19
|
+
ALL = 0x20
|
|
20
|
+
BLOCKS = 0x21
|
|
21
|
+
BLOCKS_LEARNED = 0x24
|
|
22
|
+
BLOCKS_OF_ID = 0x27
|
|
23
|
+
ARROWS = 0x22
|
|
24
|
+
ARROWS_LEARNED = 0x25
|
|
25
|
+
ARROWS_OF_ID = 0x28
|
|
26
|
+
LEARNED = 0x23
|
|
27
|
+
ALL_OF_ID = 0x26
|
|
28
|
+
LEARN = 0x36
|
|
29
|
+
FORGET = 0x37
|
|
30
|
+
CUSTOM_LABEL = 0x2F
|
|
31
|
+
CUSTOM_TEXT = 0x34
|
|
32
|
+
CLEAR_TEXT = 0x35
|
|
33
|
+
SAVE_MODEL = 0x32
|
|
34
|
+
LOAD_MODEL = 0x33
|
|
35
|
+
SAVE_PHOTO = 0x30
|
|
36
|
+
SAVE_SCREENSHOT = 0x39
|
|
37
|
+
IS_PRO = 0x3B
|
|
38
|
+
VERSION = 0x3C
|
|
39
|
+
|
|
40
|
+
class Return_Code:
|
|
41
|
+
# """Return codes that identify answer types, received from the Huskylens."""
|
|
42
|
+
ANY = 0x01 # custom command for "don't care", never returned by Huskylens
|
|
43
|
+
OK = 0x2E
|
|
44
|
+
BUSY = 0x3D
|
|
45
|
+
INFO = 0x29
|
|
46
|
+
BLOCK = 0x2A
|
|
47
|
+
ARROW = 0x2B
|
|
48
|
+
IS_PRO = 0x3B
|
|
49
|
+
NEED_PRO = 0x3E
|
|
50
|
+
|
|
51
|
+
class Algorithm:
|
|
52
|
+
FACE_RECOGNITION = 0
|
|
53
|
+
OBJECT_TRACKING = 1
|
|
54
|
+
OBJECT_RECOGNITION = 2
|
|
55
|
+
LINE_TRACKING = 3
|
|
56
|
+
COLOR_RECOGNITION = 4
|
|
57
|
+
TAG_RECOGNITION = 5
|
|
58
|
+
OBJECT_CLASSIFICATION = 6
|
|
59
|
+
QR_RECOGNITION = 7
|
|
60
|
+
BARCODE_RECOGNITION = 8
|
|
61
|
+
|
|
62
|
+
class Block:
|
|
63
|
+
# """Create a new Block with (x,y) as its center and (width,height) as its extent. Id is learned id on Huskylens.
|
|
64
|
+
# Blocks are returned by all Algorithms except for Line_Recognition. """
|
|
65
|
+
def __init__(self, x, y, width, height, id):
|
|
66
|
+
self.x = x
|
|
67
|
+
self.y = y
|
|
68
|
+
self.width = width
|
|
69
|
+
self.height = height
|
|
70
|
+
self.id = id
|
|
71
|
+
|
|
72
|
+
def __str__(self):
|
|
73
|
+
return "Block: ID_" + str(self.id) + " Pos: (" + str(self.x) + " " + str(self.y) + ") Size: (" + str(self.width) + " " + str(self.height) + ")"
|
|
74
|
+
|
|
75
|
+
class Arrow:
|
|
76
|
+
# """Create a new Arrow from (x,y)_tail to (x,y)_head. Id is learned id on Huskylens.
|
|
77
|
+
# Arrows are returned only for the Line Recognition Algorithm. """
|
|
78
|
+
def __init__(self, x_tail, y_tail, x_head, y_head, id):
|
|
79
|
+
self.x_tail = x_tail
|
|
80
|
+
self.y_tail = y_tail
|
|
81
|
+
self.x_head = x_head
|
|
82
|
+
self.y_head = y_head
|
|
83
|
+
self.id = id
|
|
84
|
+
|
|
85
|
+
def get_direction(self):
|
|
86
|
+
dx = self.x_head - self.x_tail
|
|
87
|
+
dy = self.y_head - self.y_tail
|
|
88
|
+
deg = 90 - math.degrees(math.atan2(dy, dx))
|
|
89
|
+
if deg < 0: deg = deg + 360
|
|
90
|
+
return int(deg)
|
|
91
|
+
|
|
92
|
+
def __str__(self):
|
|
93
|
+
return "Arrow: ID_" + str(self.id) + " (" + str(self.x_tail) + " " + str(self.y_tail) + ")->(" + str(self.x_head) + " " + str(self.y_head) + ")"
|
|
94
|
+
|
|
95
|
+
def byte_checksum(byte_list):
|
|
96
|
+
# """Computes the checksum and returns the low byte of the sum."""
|
|
97
|
+
return sum(byte_list) & 0xFF
|
|
98
|
+
|
|
99
|
+
def hexify(byte_array):
|
|
100
|
+
# """Takes arraylike of bytes and converts it to hex string for pretty-printing."""
|
|
101
|
+
if len(byte_array) == 0: return ""
|
|
102
|
+
return "0x" + "".join("{:02x}".format(i) for i in byte_array)
|
|
103
|
+
|
|
104
|
+
class Huskylens:
|
|
105
|
+
|
|
106
|
+
I2C_ADDR = 0x32
|
|
107
|
+
|
|
108
|
+
def __init__(self):
|
|
109
|
+
# """Create a new Huskylens instance.
|
|
110
|
+
#
|
|
111
|
+
# A Huskylens instance is necessary for further communication.
|
|
112
|
+
#
|
|
113
|
+
# The Huskylens has its own internal state that can't be known by this instance if
|
|
114
|
+
# a Huskylens is operated manually or has data stored in advance (names, learned id's).
|
|
115
|
+
# """
|
|
116
|
+
self.learned_slot_count = 0
|
|
117
|
+
self.id_slots = {} # id to learning-slots.
|
|
118
|
+
self.id_names = {} # id to string
|
|
119
|
+
self.algorithm = Algorithm.OBJECT_TRACKING
|
|
120
|
+
self.clear_texts()
|
|
121
|
+
self.pro_enabled = self.is_pro()
|
|
122
|
+
|
|
123
|
+
def initialize(self):
|
|
124
|
+
# """Establishes Connection to Huskylens. Then clears custom Texts and activates Algorithm Object Tracking.
|
|
125
|
+
|
|
126
|
+
# First, knocks at most 5 times to setup connection.
|
|
127
|
+
# If successful, clears custom text and changes Algorithm to OBJECT_TRACKING.
|
|
128
|
+
# Learned ids and labels are kept from previous uses if available.
|
|
129
|
+
#
|
|
130
|
+
# Returns:
|
|
131
|
+
# bool: Wheter initialization was successfull.
|
|
132
|
+
# """
|
|
133
|
+
success = False
|
|
134
|
+
for i in range(5):
|
|
135
|
+
self.knock()
|
|
136
|
+
success, _ = self.get_response(Return_Code.OK)
|
|
137
|
+
if success:
|
|
138
|
+
break
|
|
139
|
+
|
|
140
|
+
if success > 0:
|
|
141
|
+
s = self.clear_texts()
|
|
142
|
+
s2 = self.set_algorithm(Algorithm.OBJECT_TRACKING)
|
|
143
|
+
if s and s2:
|
|
144
|
+
print("Initialization successful!")
|
|
145
|
+
return True
|
|
146
|
+
else:
|
|
147
|
+
print("Initialization Failed. Couldn't change Algorithm")
|
|
148
|
+
return False
|
|
149
|
+
else:
|
|
150
|
+
print("Initialization Failed. Please check connection to Huskylens.")
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
# Low Level communication commands (deal with byte-data yourself)
|
|
154
|
+
|
|
155
|
+
def send_request(self, command, data=None):
|
|
156
|
+
# """Request an action from the Huskylens through a command code and optional Data Bytes.
|
|
157
|
+
#
|
|
158
|
+
# Parameters:
|
|
159
|
+
# command (byte): a Request_Command byte to be sent.
|
|
160
|
+
# data (list of bytes | None): Optional data to attach to the request or None.
|
|
161
|
+
# """
|
|
162
|
+
buffer = bytearray(b'\x55\xAA\x11\x00\x00')
|
|
163
|
+
buffer[3] = 0 if data is None else len(data)
|
|
164
|
+
buffer[4] = command
|
|
165
|
+
if data:
|
|
166
|
+
for b in data:
|
|
167
|
+
buffer.append(b)
|
|
168
|
+
buffer.append(byte_checksum(buffer))
|
|
169
|
+
# print("request:", hexify(buffer))
|
|
170
|
+
i2c.write(Huskylens.I2C_ADDR, buffer)
|
|
171
|
+
sleep(50)
|
|
172
|
+
|
|
173
|
+
def get_response(self, return_code=Return_Code.ANY, timeout=500):
|
|
174
|
+
# """Read a response from I2C. Returns after a timeout when no data available.
|
|
175
|
+
# If a specific return code is expected and given as a parameter, all other results will set the returned code to 0.
|
|
176
|
+
# If a timeout occured, the return code is set to -1. Other errors return -2.
|
|
177
|
+
# This way, the return code is positive upon success, and negative or 0 otherwise.
|
|
178
|
+
#
|
|
179
|
+
# Parameters:
|
|
180
|
+
# return_code (byte): Optional expected return code.
|
|
181
|
+
# timeout (int): milliseconds to wait for response before giving up
|
|
182
|
+
#
|
|
183
|
+
# Returns:
|
|
184
|
+
# (code | error, data list): A tuple containing the return code or the error code as a signed byte in the first part
|
|
185
|
+
# The data as a list of bytes in the second argument or an empty list if nothing was sent along.
|
|
186
|
+
# """
|
|
187
|
+
response_header = bytearray(b"\x55\0\0\0\0")
|
|
188
|
+
start_time = running_time()
|
|
189
|
+
|
|
190
|
+
# bytewise polling, as there seem to be unpredictable 0 bytes between messages!
|
|
191
|
+
#response_header = i2c.read(Huskylens.I2C_ADDR, 5, True)
|
|
192
|
+
while running_time() - start_time < timeout:
|
|
193
|
+
byte = i2c.read(Huskylens.I2C_ADDR, 1)[0]
|
|
194
|
+
if byte == 0x55:
|
|
195
|
+
break
|
|
196
|
+
if byte != 0x55:
|
|
197
|
+
return -1, [] # Timeout error
|
|
198
|
+
|
|
199
|
+
# reading rest of header data
|
|
200
|
+
for i in range(4):
|
|
201
|
+
response_header[i+1] = i2c.read(Huskylens.I2C_ADDR, 1)[0]
|
|
202
|
+
|
|
203
|
+
if response_header[0:3] != b'\x55\xAA\x11':
|
|
204
|
+
return -2, [] # Wrong header structure error
|
|
205
|
+
|
|
206
|
+
data_length = response_header[3]
|
|
207
|
+
response_type = response_header[4]
|
|
208
|
+
|
|
209
|
+
data = []
|
|
210
|
+
if data_length > 0:
|
|
211
|
+
response_body = i2c.read(Huskylens.I2C_ADDR, data_length+1)
|
|
212
|
+
data = response_body[0:-1]
|
|
213
|
+
response_checksum = response_body[-1]
|
|
214
|
+
else:
|
|
215
|
+
response_checksum = ord(i2c.read(Huskylens.I2C_ADDR, 1))
|
|
216
|
+
if response_checksum != byte_checksum(list(response_header) + data):
|
|
217
|
+
return -3, [] # Checksum Error
|
|
218
|
+
|
|
219
|
+
# print("response: " + hexify([response_type]) + " " + hexify(data))
|
|
220
|
+
if return_code == Return_Code.ANY or response_type == return_code:
|
|
221
|
+
return response_type, data
|
|
222
|
+
else:
|
|
223
|
+
return 0, data # not expected answer Hint
|
|
224
|
+
|
|
225
|
+
def knock(self):
|
|
226
|
+
self.send_request(Request_Command.KNOCK)
|
|
227
|
+
|
|
228
|
+
# High level commands for students
|
|
229
|
+
|
|
230
|
+
def set_algorithm(self, algorithm):
|
|
231
|
+
# """ Change the active algorithm on the Huskylens. Returns True on success."""
|
|
232
|
+
if (algorithm == Algorithm.QR_RECOGNITION or algorithm == Algorithm.BARCODE_RECOGNITION) and (not self.pro_enabled):
|
|
233
|
+
raise RuntimeError("Error: Huskylens PRO version is required for algorithm ", _algorithm_names[algorithm])
|
|
234
|
+
return False
|
|
235
|
+
data = [algorithm, 0x00]
|
|
236
|
+
self.send_request(Request_Command.ALGORITHM, data)
|
|
237
|
+
success, _ = self.get_response(Return_Code.OK)
|
|
238
|
+
if success > 0:
|
|
239
|
+
print("Current Algorithm:", _algorithm_names[algorithm])
|
|
240
|
+
self.algorithm = algorithm
|
|
241
|
+
return True if success > 0 else False
|
|
242
|
+
|
|
243
|
+
def get_all(self):
|
|
244
|
+
# """Get All detected objects, Blocks or Arrows as a list."""
|
|
245
|
+
return self._get_results(Request_Command.ALL)
|
|
246
|
+
|
|
247
|
+
def get_all_learned(self):
|
|
248
|
+
# """Get all detected objects that are learned as a list."""
|
|
249
|
+
return self._get_results(Request_Command.LEARNED)
|
|
250
|
+
|
|
251
|
+
def get_all_with_id(self, id):
|
|
252
|
+
# """Get all detected objects with specific id as a list."""
|
|
253
|
+
if id <= 0 or id > 255:
|
|
254
|
+
raise RuntimeError("Error: ID must be in range from 1 to 255.")
|
|
255
|
+
return self._get_results(Request_Command.ALL_OF_ID, id)
|
|
256
|
+
|
|
257
|
+
def get_one(self):
|
|
258
|
+
# """Get one instance of centermost detected object (Block or Arrow) else None."""
|
|
259
|
+
results = self._get_results(Request_Command.ALL)
|
|
260
|
+
return self._get_centermost(results)
|
|
261
|
+
|
|
262
|
+
def get_one_learned(self):
|
|
263
|
+
# """Get one instance of centermost detected object that has an id > 0. else None."""
|
|
264
|
+
results = self._get_results(Request_Command.LEARNED)
|
|
265
|
+
return self._get_centermost(results)
|
|
266
|
+
|
|
267
|
+
def get_one_with_id(self, id):
|
|
268
|
+
# """Get one instance of centermost detected object that has given id. else None."""
|
|
269
|
+
if id <= 0 or id > 255:
|
|
270
|
+
raise RuntimeError("Error: ID must be in range from 1 to 255.")
|
|
271
|
+
results = self._get_results(Request_Command.ALL_OF_ID, id)
|
|
272
|
+
return self._get_centermost(results)
|
|
273
|
+
|
|
274
|
+
def attach_label(self, id, name):
|
|
275
|
+
# """Attach the label "name" to a learned id of current Algorithm. Returns True on success."""
|
|
276
|
+
if self.algorithm == Algorithm.OBJECT_TRACKING or \
|
|
277
|
+
self.algorithm == Algorithm.LINE_TRACKING: # single learn algorithms
|
|
278
|
+
success = self._set_name(1, name)
|
|
279
|
+
elif self.algorithm == Algorithm.FACE_RECOGNITION or \
|
|
280
|
+
self.algorithm == Algorithm.TAG_RECOGNITION or \
|
|
281
|
+
self.algorithm == Algorithm.OBJECT_CLASSIFICATION or \
|
|
282
|
+
self.algorithm == Algorithm.OBJECT_RECOGNITION: # multi learn algorithms
|
|
283
|
+
# avoid naming unlearned id's to avert bugs.
|
|
284
|
+
slots = self.id_slots.get(id)
|
|
285
|
+
if slots == None:
|
|
286
|
+
raise RuntimeError("Can't attach a name to an unlearned ID number")
|
|
287
|
+
|
|
288
|
+
self.id_names[id] = name
|
|
289
|
+
|
|
290
|
+
success = True
|
|
291
|
+
for slot in slots:
|
|
292
|
+
s = self._set_name(slot, name)
|
|
293
|
+
success = success and s > 0
|
|
294
|
+
else: # For color recognition (where it works properly!)
|
|
295
|
+
self.id_names[id] = name
|
|
296
|
+
success = self._set_name(id, name)
|
|
297
|
+
return True if success > 0 else False
|
|
298
|
+
|
|
299
|
+
def clear_labels(self):
|
|
300
|
+
# """Deletes all learned label names on Huskylens for the current algorithm."""
|
|
301
|
+
self.id_names.clear()
|
|
302
|
+
for i in range(10):
|
|
303
|
+
self._set_name(i, "")
|
|
304
|
+
|
|
305
|
+
def add_text(self, text, position_x, position_y):
|
|
306
|
+
# """Add a custom text to the Huskylens screen at a certain screen pixel-position (top left starting point).
|
|
307
|
+
# Text must be less than 20 bytes long and within pixel-borders.
|
|
308
|
+
# Multiple texts at the same location get overwritten. Returns True on success."""
|
|
309
|
+
text_bytes = bytes(text, "utf-8")
|
|
310
|
+
if len(text_bytes) > 19:
|
|
311
|
+
raise RuntimeError("Custom Text must be less than 20 bytes long.")
|
|
312
|
+
if position_x > 300 or position_x < 0 or position_y < 35 or position_y > 240:
|
|
313
|
+
raise RuntimeError("Custom Text can't be placed outside of screen pixel size.")
|
|
314
|
+
data = [len(text_bytes)]
|
|
315
|
+
data.append(0xFF if position_x > 255 else 0x00)
|
|
316
|
+
data.append(position_x % 255)
|
|
317
|
+
data.append(240 - position_y) # reverse flipped y axis
|
|
318
|
+
data.extend(list(text_bytes))
|
|
319
|
+
self.send_request(Request_Command.CUSTOM_TEXT, data)
|
|
320
|
+
success, _ = self.get_response(Return_Code.OK)
|
|
321
|
+
return True if success > 0 else False
|
|
322
|
+
|
|
323
|
+
def clear_texts(self):
|
|
324
|
+
# """Deletes all text on the Huskylens screen."""
|
|
325
|
+
self.send_request(Request_Command.CLEAR_TEXT)
|
|
326
|
+
success, _ = self.get_response(Return_Code.OK)
|
|
327
|
+
return True if success > 0 else False
|
|
328
|
+
|
|
329
|
+
def learn(self, id, name=None):
|
|
330
|
+
# """Learn and assign an id to the object currently centered on the huskylens camera.
|
|
331
|
+
# Optionally attach label to this learned id. Returns True on success."""
|
|
332
|
+
if id <= 0 or id > 255:
|
|
333
|
+
raise RuntimeError("Parameter ID for learned item must be in range [0,255]")
|
|
334
|
+
if self.algorithm == Algorithm.OBJECT_TRACKING or self.algorithm == Algorithm.LINE_TRACKING:
|
|
335
|
+
id = 1
|
|
336
|
+
timeout = 500
|
|
337
|
+
if self.algorithm == Algorithm.OBJECT_CLASSIFICATION:
|
|
338
|
+
timeout = 1000
|
|
339
|
+
self.send_request(Request_Command.LEARN, [id, 0x00])
|
|
340
|
+
success, _ = self.get_response(Return_Code.OK, timeout)
|
|
341
|
+
if success > 0:
|
|
342
|
+
# 1. remember slot for id
|
|
343
|
+
self.learned_slot_count += 1
|
|
344
|
+
if (not (id in self.id_slots)):
|
|
345
|
+
self.id_slots[id] = [self.learned_slot_count]
|
|
346
|
+
else:
|
|
347
|
+
self.id_slots[id].append(self.learned_slot_count)
|
|
348
|
+
# 2. attach, name if available.
|
|
349
|
+
known_by = self.id_names.get(id)
|
|
350
|
+
if known_by != None:
|
|
351
|
+
success = self.attach_label(self.learned_slot_count, known_by)
|
|
352
|
+
elif name != None:
|
|
353
|
+
success = self.attach_label(id, name)
|
|
354
|
+
return True if success > 0 else False
|
|
355
|
+
|
|
356
|
+
def forget(self):
|
|
357
|
+
# """Forget all learned objects (ids) of the current algorithm.
|
|
358
|
+
# Labels are unaffected. Returns True on success."""
|
|
359
|
+
self.send_request(Request_Command.FORGET)
|
|
360
|
+
success, _ = self.get_response(Return_Code.OK)
|
|
361
|
+
if success:
|
|
362
|
+
self.learned_slot_count = 0
|
|
363
|
+
self.id_slots.clear()
|
|
364
|
+
return True if success else False
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def save_photo(self):
|
|
368
|
+
# """Save a photo to the SD-Card. No Feedback, fails on Huskylens screen if no SD-Card is available."""
|
|
369
|
+
self.send_request(Request_Command.SAVE_PHOTO)
|
|
370
|
+
success, _ = self.get_response(Return_Code.OK, 1000)
|
|
371
|
+
return True if success > 0 else False
|
|
372
|
+
|
|
373
|
+
def save_screenshot(self):
|
|
374
|
+
# """Save a screenshot (including texts) to the SD-Card. No Feedback, fails on Huskylens screen if no SD-Card is available."""
|
|
375
|
+
self.send_request(Request_Command.SAVE_SCREENSHOT)
|
|
376
|
+
success, _ = self.get_response(Return_Code.OK, 1000)
|
|
377
|
+
return True if success > 0 else False
|
|
378
|
+
|
|
379
|
+
def save_model(self, model_id):
|
|
380
|
+
# """Save the learned ids and labels to the SD-Card. No Feedback, see Huskylens screen for Result.
|
|
381
|
+
# There can be at most 5 models per Algorithm, indexed by model_id."""
|
|
382
|
+
if model_id < 0 or model_id > 4:
|
|
383
|
+
raise RuntimeError("Invalid model_id. Must be number in range [0,4]")
|
|
384
|
+
self.send_request(Request_Command.SAVE_MODEL, [model_id, 0x00])
|
|
385
|
+
success, _ = self.get_response(Return_Code.OK, 1000)
|
|
386
|
+
print("Model saving: Check Huskylens screen for Result!\n\tModel name:", _algorithm_names[self.algorithm] + "_Backup_" + str(model_id) + ".conf")
|
|
387
|
+
return True if success > 0 else False
|
|
388
|
+
|
|
389
|
+
def load_model(self, model_id):
|
|
390
|
+
# """Load a previously saved model for the active Algorithm from the SD-Card. No Feedback, see Huskylens screen for Result."""
|
|
391
|
+
if model_id < 0 or model_id > 4:
|
|
392
|
+
raise RuntimeError("Invalid model_id. Must be number in range [0,4]")
|
|
393
|
+
self.send_request(Request_Command.LOAD_MODEL, [model_id, 0x00])
|
|
394
|
+
success, _ = self.get_response(Return_Code.OK, 1000)
|
|
395
|
+
print("Model Loading: Check Huskylens screen for Result!")
|
|
396
|
+
return True if success > 0 else False
|
|
397
|
+
|
|
398
|
+
def is_pro(self):
|
|
399
|
+
# """Checks wheter the Huskylens is the PRO version, which supports QR_RECOGNITION and BARCODE_RECOGNITION.
|
|
400
|
+
# Returns True/False on success, else 0."""
|
|
401
|
+
self.send_request(Request_Command.IS_PRO)
|
|
402
|
+
success, data = self.get_response(Return_Code.IS_PRO)
|
|
403
|
+
return bool(data[0]) if success > 0 else False
|
|
404
|
+
|
|
405
|
+
# hidden Utility functions
|
|
406
|
+
|
|
407
|
+
def _set_name(self, id, name):
|
|
408
|
+
# """Set the name of block "id" to the given "name". Name must be less than 20 characters long."""
|
|
409
|
+
name_bytes = bytes(name, "utf-8")
|
|
410
|
+
if len(name_bytes) > 19: raise RuntimeError("Custom Name must be less than 20 bytes long.")
|
|
411
|
+
data = [id, len(name_bytes)+1]
|
|
412
|
+
data.extend(list(name_bytes))
|
|
413
|
+
data.append(0x00)
|
|
414
|
+
self.send_request(Request_Command.CUSTOM_LABEL, data)
|
|
415
|
+
success, _ = self.get_response(Return_Code.OK)
|
|
416
|
+
return True if success > 0 else False
|
|
417
|
+
|
|
418
|
+
def _get_results(self, request_command, id=-1):
|
|
419
|
+
# """Get all detected Blocks or Arrows from the Huskylens, specified by the request and id.
|
|
420
|
+
# Possible Requests: BLOCKS, BLOCKS_LEARNED, BLOCKS_OF_ID, ARROWS, ...
|
|
421
|
+
# Returns a list of all the detected Block or Arrow instances.
|
|
422
|
+
# """
|
|
423
|
+
request_data = None if id < 0 else [id, 0]
|
|
424
|
+
self.send_request(request_command, request_data)
|
|
425
|
+
|
|
426
|
+
# 1. get info header
|
|
427
|
+
success, info = self.get_response(Return_Code.INFO)
|
|
428
|
+
if not success:
|
|
429
|
+
raise RuntimeError("Failed to request results. Got answer:" + str(success))
|
|
430
|
+
|
|
431
|
+
# Ignoring number of detected id's and current frame number.
|
|
432
|
+
n_elements = info[0] + info[1]*255
|
|
433
|
+
#n_ids = info[2] + info[3]*255
|
|
434
|
+
# frame = info[4] + info[5]*255
|
|
435
|
+
#print("result info:", n_elements, n_ids, frame)
|
|
436
|
+
|
|
437
|
+
# 2. receive data
|
|
438
|
+
el = 0
|
|
439
|
+
objects = []
|
|
440
|
+
|
|
441
|
+
while el < n_elements:
|
|
442
|
+
response_type, el_data = self.get_response(Return_Code.ANY)
|
|
443
|
+
if response_type == Return_Code.BLOCK and self.algorithm != 3:
|
|
444
|
+
x = el_data[0] + el_data[1]*255
|
|
445
|
+
y = 240 - el_data[2] + el_data[3]*255 # flipped y axis
|
|
446
|
+
width = el_data[4] + el_data[5]*255
|
|
447
|
+
height = el_data[6] + el_data[7]*255
|
|
448
|
+
el_id = el_data[8]
|
|
449
|
+
block = Block(x, y, width, height, el_id)
|
|
450
|
+
objects.append(block)
|
|
451
|
+
el += 1
|
|
452
|
+
|
|
453
|
+
elif response_type == Return_Code.ARROW:
|
|
454
|
+
xtail = el_data[0] + el_data[1]*255
|
|
455
|
+
ytail = 240 - el_data[2] + el_data[3]*255 # flipped y axis
|
|
456
|
+
xhead = el_data[4] + el_data[5]*255
|
|
457
|
+
yhead = 240 - el_data[6] + el_data[7]*255 # flipped y axis
|
|
458
|
+
el_id = el_data[8]
|
|
459
|
+
arrow = Arrow(xtail,ytail,xhead,yhead,el_id)
|
|
460
|
+
objects.append(arrow)
|
|
461
|
+
el += 1
|
|
462
|
+
elif response_type == 0:
|
|
463
|
+
return [] # Error (couldn't read all detected elements) fail silently.
|
|
464
|
+
return objects
|
|
465
|
+
|
|
466
|
+
def _get_centermost(self, results):
|
|
467
|
+
# """Select and Return the most centered (L1-distance) instance of all detected Objects in "results". """
|
|
468
|
+
centermost = None
|
|
469
|
+
max_offset = 320+120
|
|
470
|
+
for obj in results:
|
|
471
|
+
center_offset = 0
|
|
472
|
+
if self.algorithm == Algorithm.LINE_TRACKING:
|
|
473
|
+
center_offset = abs((obj.x_tail + (obj.x_tail - obj.x_head) // 2) - 160) \
|
|
474
|
+
+ abs((obj.y_tail + (obj.y_tail - obj.y_head) // 2) - 120)
|
|
475
|
+
else:
|
|
476
|
+
center_offset = abs(obj.x - 160) + abs(obj.y - 120)
|
|
477
|
+
if center_offset < max_offset:
|
|
478
|
+
centermost, max_offset = obj, center_offset
|
|
479
|
+
return centermost
|