HandsON-BuildHat-API 1.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,324 @@
1
+ # Encoding : UTF-8
2
+ # Date : 2025-04-30
3
+ # Production : HandsON Technology Co., Ltd
4
+ # Author : HaNeul Jung (caffeine.reload@gmail.com)
5
+
6
+ # All rights to the code, excluding third-party libraries used herein, are reserved by HandsON Technology Co., Ltd.
7
+
8
+ from serial import Serial
9
+ import time
10
+
11
+ class _ConstModuleID:
12
+ '''
13
+ 모듈 ID 모음
14
+ '''
15
+ COLOR = 0x3D
16
+ DISTANCE = 0x3E
17
+ FORCE = 0x3F
18
+
19
+ SMALL_MOTOR = 0x41
20
+ MEDIUM_MOTOR = 0x30
21
+ LARGE_MOTOR = 0x31
22
+
23
+ class _ConstValue:
24
+ '''
25
+ 기타 상수 모음
26
+ '''
27
+ COMMAND = 0b1000 << 4
28
+ GET_DATA = 0b1001 << 4
29
+ ALL_PORT = 0b1111
30
+
31
+ MOTOR_PWM_MODE = 0
32
+ MOTOR_ABS_MODE = 1
33
+ MOTOR_REL_MODE = 2
34
+
35
+ class _PORT:
36
+ PORT = {
37
+ 'A':0, 'B':1, 'C':2, 'D':3, 'E':4, 'F':5
38
+ }
39
+
40
+ class _COLOR:
41
+ COLOR = {
42
+ 0:'black', 1:'violet', 3:'blue', 4:'cyan',
43
+ 5:'green', 7:'yellow', 9:'red', 10:'white',
44
+ 255:'None'
45
+ }
46
+
47
+ class _Availability:
48
+ '''
49
+ 내부용 시리얼 통신의 체크섬을 확인 및 체크섬을 만드는 함수를 제공합니다.
50
+ '''
51
+ @staticmethod
52
+ def make_checksum(byte:bytes) -> bytes:
53
+ '''
54
+ 입력 받은 바이트를 기반으로 체크섬을 만들고 반환합니다.
55
+ '''
56
+ sum = 0
57
+ for i in byte: sum ^= i
58
+ return bytes([sum])
59
+
60
+ @staticmethod
61
+ def checksum_check(byte:bytes) -> bool:
62
+ '''
63
+ 바이트열의 체크섬을 확인하고 불 값을 반환합니다.
64
+ '''
65
+ sum = 0
66
+ for i in byte[:-1]: sum ^= i
67
+ try:
68
+ if sum == byte[-1]: return True
69
+ else: return False
70
+ except IndexError:
71
+ return False
72
+
73
+ class _SerialManager:
74
+ '''
75
+ 내부용 시리얼 버스 객체, **외부 접근 절대 금지**.
76
+ '''
77
+ _serial_port = (
78
+ '/dev/ttyTHS1', # Jetson Orin Nano
79
+ '/dev/ttyAMA0', # Raspberry PI 5
80
+ '/dev/ttyS0' # Raspberry PI 4
81
+ )
82
+ serial = None
83
+
84
+ for i in _serial_port:
85
+ try: serial = Serial(port=i, baudrate=115200, timeout=0.1)
86
+ except: pass
87
+ else: break
88
+
89
+ if serial == None:
90
+ raise Exception('사용 가능한 시리얼 포트를 찾을 수 없습니다.')
91
+
92
+ class _ModuleParent:
93
+ def _get_data(self, byte:bytes) -> bytes:
94
+ while True:
95
+ self._send_data(byte)
96
+ data = _SerialManager.serial.read(16)
97
+
98
+ if data == b'': continue
99
+
100
+ if _Availability.checksum_check(data): return data
101
+
102
+ def _send_data(self, byte:bytes) -> None:
103
+ com = byte
104
+ for i in range(0, 7-len(com)): com += b'\x00'
105
+ com += _Availability.make_checksum(com)
106
+ time.sleep(0.005)
107
+ _SerialManager.serial.write(com)
108
+
109
+ def _module_check(self, port:int, ID:tuple) -> None:
110
+ command = [_ConstValue.GET_DATA + _ConstValue.ALL_PORT]
111
+ if not self._get_data(byte=bytes(command))[port + 1] in ID:
112
+ raise Exception('해당 포트에 연결된 모듈이 다르거나 없습니다.')
113
+
114
+ class ColorSensor(_ModuleParent):
115
+ def __init__(self, port:str):
116
+ self._PORT = _PORT.PORT[port]
117
+ self._module_check(self._PORT, (_ConstModuleID.COLOR, ))
118
+
119
+ def get_color(self) -> str:
120
+ command = [_ConstValue.GET_DATA + self._PORT]
121
+ data = self._get_data(bytes(command))
122
+ return _COLOR.COLOR[data[1]]
123
+
124
+ def get_reflected_light(self) -> int:
125
+ command = [_ConstValue.GET_DATA + self._PORT]
126
+ data = self._get_data(bytes(command))
127
+ return data[2]
128
+
129
+ def get_rgb_intensity(self) -> tuple:
130
+ command = [_ConstValue.GET_DATA + self._PORT]
131
+ data = self._get_data(bytes(command))
132
+ return (data[3] + (data[4] << 8), data[5] + (data[6] << 8), data[7] + (data[8] << 8))
133
+
134
+ def get_red(self) -> int:
135
+ command = [_ConstValue.GET_DATA + self._PORT]
136
+ data = self._get_data(bytes(command))
137
+ return data[3] + (data[4] << 8)
138
+
139
+ def get_green(self) -> int:
140
+ command = [_ConstValue.GET_DATA + self._PORT]
141
+ data = self._get_data(bytes(command))
142
+ return data[5] + (data[6] << 8)
143
+
144
+ def get_blue(self) -> int:
145
+ command = [_ConstValue.GET_DATA + self._PORT]
146
+ data = self._get_data(bytes(command))
147
+ return data[7] + (data[8] << 8)
148
+
149
+ class DistanceSensor(_ModuleParent):
150
+ def __init__(self, port:str):
151
+ self._PORT = _PORT.PORT[port]
152
+ self._module_check(self._PORT, (_ConstModuleID.DISTANCE, ))
153
+
154
+ def get_distance_cm(self) -> int:
155
+ command = [_ConstValue.GET_DATA + self._PORT]
156
+ data = self._get_data(bytes(command))
157
+ return (data[1] + (data[2] << 8)) // 10
158
+
159
+ class ForceSensor(_ModuleParent):
160
+ def __init__(self, port:str):
161
+ self._PORT = _PORT.PORT[port]
162
+ self._module_check(self._PORT, (_ConstModuleID.FORCE, ))
163
+
164
+ def is_pressed(self) -> bool:
165
+ command = [_ConstValue.GET_DATA + self._PORT]
166
+ data = self._get_data(bytes(command))
167
+ if data[2] == 1: return True
168
+ else: return False
169
+
170
+ def get_force_percentage(self) -> int:
171
+ command = [_ConstValue.GET_DATA + self._PORT]
172
+ data = self._get_data(bytes(command))
173
+ return data[1]
174
+
175
+ class Motor(_ModuleParent):
176
+ def __init__(self, port:str):
177
+ self._PORT = _PORT.PORT[port]
178
+ self._module_check(self._PORT, (_ConstModuleID.SMALL_MOTOR, _ConstModuleID.MEDIUM_MOTOR, _ConstModuleID.LARGE_MOTOR))
179
+
180
+ def stop(self) -> None:
181
+ com = bytes((_ConstValue.COMMAND + self._PORT, _ConstValue.MOTOR_PWM_MODE, 0, 0))
182
+ self._send_data(com)
183
+
184
+ def start(self, speed:int) -> None:
185
+ if not speed in range(-100, 101):
186
+ raise Exception('모터의 속도는 ~100 ~ 100 사이의 정수만 입력 가능합니다.')
187
+
188
+ if speed > 0: sw = 1
189
+ else: sw = 0
190
+
191
+ com = bytes((_ConstValue.COMMAND + self._PORT, _ConstValue.MOTOR_PWM_MODE, sw, abs(speed)))
192
+ self._send_data(com)
193
+
194
+ def run_to_position(self, degrees, speed) -> None:
195
+ if not speed in range(0, 101):
196
+ raise Exception('모터의 속도는 0 ~ 100 사이의 정수만 입력 가능합니다.')
197
+ if not degrees in range(-180, 181):
198
+ raise Exception('모터의 절대 각도는 -180 ~ 180 사이의 정수만 입력 가능합니다.')
199
+
200
+ if degrees >= 0: target = degrees
201
+ else: target = degrees & 0xFFFF
202
+
203
+ com = bytes((_ConstValue.COMMAND + self._PORT, _ConstValue.MOTOR_ABS_MODE, speed, (target & 0xFF), (target >> 8)))
204
+ self._send_data(com)
205
+
206
+ def run_for_degrees(self, degrees, speed) -> None:
207
+ if not speed in range(0, 101):
208
+ raise Exception('모터의 속도는 0 ~ 100 사이의 정수만 입력 가능합니다.')
209
+
210
+ if not degrees in range(-2147483647, 2147483647):
211
+ raise Exception('모터의 동작 각도는 -2147483647 ~ 2147483647 사이의 값만 입력 가능합니다.')
212
+
213
+ if degrees >= 0: target = degrees
214
+ else: target = degrees & 0xFFFFFFFF
215
+
216
+ com = bytes((_ConstValue.COMMAND + self._PORT, _ConstValue.MOTOR_REL_MODE, speed, (target & 0xFF), (target >> 8) & 0xFF, (target >> 16) & 0xFF, (target >> 24) & 0xFF))
217
+ self._send_data(com)
218
+
219
+ def get_speed(self) -> int:
220
+ command = [_ConstValue.GET_DATA + self._PORT]
221
+ data = self._get_data(bytes(command))
222
+ if data[1] >> 7 == 0: return data[1]
223
+ else: return -(0xFF - data[1])
224
+
225
+ def get_position(self) -> int:
226
+ command = [_ConstValue.GET_DATA + self._PORT]
227
+ data = self._get_data(bytes(command))
228
+ if data[7] >> 7 == 0: return data[6] + (data[7] << 8)
229
+ else: return -(0xFFFF - (data[6] + (data[7] << 8) - 1))
230
+
231
+ def get_degrees_counted(self) -> int:
232
+ command = [_ConstValue.GET_DATA + self._PORT]
233
+ data = self._get_data(bytes(command))
234
+ if data[5] >> 7 == 0: return data[2] + (data[3] << 8) + (data[4] << 16) + (data[5] << 24)
235
+ else: return -(0xFFFFFFFF - (data[2] + (data[3] << 8) + (data[4] << 16) + (data[5] << 24) - 1))
236
+
237
+ class MotorPair(_ModuleParent):
238
+ def __init__(self, left_motor, right_motor):
239
+ self._PORTs = (_PORT.PORT[left_motor], _PORT.PORT[right_motor])
240
+ for i in self._PORTs: self._module_check(i, (_ConstModuleID.LARGE_MOTOR, _ConstModuleID.MEDIUM_MOTOR, _ConstModuleID.SMALL_MOTOR))
241
+
242
+ def stop(self) -> None:
243
+ for i in self._PORTs:
244
+ com = bytes((_ConstValue.COMMAND + i, _ConstValue.MOTOR_PWM_MODE, 0, 0))
245
+ self._send_data(com)
246
+
247
+ def start(self, steering, speed) -> None:
248
+ if not steering in range(-100, 101):
249
+ raise Exception('조향 값은 -100 ~ 100 사이의 값만 입력 가능합니다.')
250
+ if not speed in range(-100, 101):
251
+ raise Exception('속도 값은 -100 ~ 100 사이의 값만 입력 가능합니다.')
252
+
253
+ out = (
254
+ int((speed + (speed * (steering / 100))) * 1),
255
+ int((speed - (speed * (steering / 100))) * -1)
256
+ )
257
+
258
+ for motor, speed in zip(self._PORTs, out):
259
+ if speed > 0: sw = 1
260
+ else: sw = 0
261
+
262
+ com = bytes((_ConstValue.COMMAND + motor, _ConstValue.MOTOR_PWM_MODE, sw, abs(speed)))
263
+ self._send_data(com)
264
+
265
+ def move(self, degrees, steering, speed) -> None:
266
+ if not steering in range(-100, 101):
267
+ raise Exception('조향 값은 -100 ~ 100 사이의 값만 입력 가능합니다.')
268
+ if not speed in range(0, 101):
269
+ raise Exception('속도 값은 0 ~ 100 사이의 값만 입력 가능합니다.')
270
+ if not degrees in range(-2147483647, 2147483647):
271
+ raise Exception('모터의 동작 각도는 -2147483647 ~ 2147483647 사이의 값만 입력 가능합니다.')
272
+
273
+ out = (
274
+ int(speed + (speed * (steering / 100))),
275
+ int(speed - (speed * (steering / 100)))
276
+ )
277
+
278
+ for motor, speed, count in zip(self._PORTs, out, (0, 1)):
279
+
280
+ if degrees >= 0: target = degrees
281
+ else: target = degrees & 0xFFFFFFFF
282
+
283
+ if count == 1: target = ~target
284
+
285
+ com = bytes((_ConstValue.COMMAND + motor, _ConstValue.MOTOR_REL_MODE, speed, (target & 0xFF), (target >> 8) & 0xFF, (target >> 16) & 0xFF, (target >> 24) & 0xFF))
286
+ self._send_data(com)
287
+
288
+
289
+ def move_tank(self, degrees, left_speed, right_speed) -> None:
290
+ if not left_speed in range(-100, 101) or not right_speed in range(-100, 101):
291
+ raise Exception('조향 값은 -100 ~ 100 사이의 값만 입력 가능합니다.')
292
+ if not degrees in range(-2147483647, 2147483647):
293
+ raise Exception('모터의 동작 각도는 -2147483647 ~ 2147483647 사이의 값만 입력 가능합니다.')
294
+
295
+ out = (
296
+ int(left_speed * 1),
297
+ int(right_speed * -1)
298
+ )
299
+
300
+ for motor, speed, count in zip(self._PORTs, out, (0, 1)):
301
+
302
+ if degrees >= 0: target = degrees
303
+ else: target = degrees & 0xFFFFFFFF
304
+
305
+ if count == 1: target = ~target
306
+
307
+ com = bytes((_ConstValue.COMMAND + motor, _ConstValue.MOTOR_REL_MODE, speed, (target & 0xFF), (target >> 8) & 0xFF, (target >> 16) & 0xFF, (target >> 24) & 0xFF))
308
+ self._send_data(com)
309
+
310
+ def start_tank(self, left_speed, right_speed) -> None:
311
+ if not left_speed in range(-100, 101) or not right_speed in range(-100, 101):
312
+ raise Exception('조향 값은 -100 ~ 100 사이의 값만 입력 가능합니다.')
313
+
314
+ out = (
315
+ int(left_speed * 1),
316
+ int(right_speed * -1)
317
+ )
318
+
319
+ for motor, speed in zip(self._PORTs, out):
320
+ if speed > 0: sw = 1
321
+ else: sw = 0
322
+
323
+ com = bytes((_ConstValue.COMMAND + motor, _ConstValue.MOTOR_PWM_MODE, sw, abs(speed)))
324
+ self._send_data(com)
@@ -0,0 +1,4 @@
1
+
2
+ from .SpikePI import Motor, MotorPair, ColorSensor, DistanceSensor, ForceSensor
3
+
4
+ __all__ = ['Motor', 'MotorPair', 'ColorSensor', 'DistanceSensor', 'ForceSensor']
@@ -0,0 +1,49 @@
1
+ Metadata-Version: 2.4
2
+ Name: HandsON-BuildHat-API
3
+ Version: 1.0
4
+ Summary: A third-party API for controlling LEGO SPIKE devices
5
+ Author-email: HandsON Technology <caffeine.reload@gmail.com>
6
+ License: Proprietary - Personal/Educational Use Only
7
+ Project-URL: Homepage, https://github.com/yourname/HandsON-BuildHat-API
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: Other/Proprietary License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.7
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: pyserial>=3.5
15
+ Dynamic: license-file
16
+
17
+ HandsON Build Hat API
18
+ =======
19
+ **Production** : HandsON Technology co., ltd.
20
+ **Author** : HaNeul Jung (caffeine.reload@gmail.com)
21
+ **Last Update** : 2025-06-07
22
+
23
+ ## 개요
24
+ 제작된 빌드햇의 제어에 필요한 API 라이브러리
25
+
26
+ ## 구동 환경
27
+ ### 컨트롤러
28
+ |이름|설명|
29
+ |:--|:--|
30
+ |Raspberry PI 4, 5|정상 동작 확인|
31
+ |Jetson Orin Nano, Nano|정상 동작 확인 (하드웨어 구조가 라즈베리파이 버전과 다름)|
32
+
33
+ ### 종속성
34
+ - python3-serial
35
+
36
+ ## 예제
37
+ 모든 API는 기존 Spike Legacy와 문법적으로 동일
38
+
39
+ ```python
40
+ from SpikePI import ColorSensor, Motor
41
+
42
+ motor = Motor('A')
43
+ colorsensor = ColorSensor('B')
44
+
45
+ reflected = colorsensor.get_reflected_light()
46
+ motor.start(100)
47
+ ```
48
+
49
+ 자세한 문법은 Lego Spike Legacy 앱에서 확인
@@ -0,0 +1,7 @@
1
+ HandsON_BuildHat_API/SpikePI.py,sha256=L4zmkYZxM1uZjmCVoax0lZ8E9XsBipPLWlbdjQsCsIc,11884
2
+ HandsON_BuildHat_API/__init__.py,sha256=nAmCRGKB6DwSg84Qv3N27Ay34fySDrYj6p3RUMFRImk,162
3
+ handson_buildhat_api-1.0.dist-info/licenses/LICENSE,sha256=xkedyvUIiuPGSXgx2D3uwqfpQ1LLkovfT0zPSAdK6Jg,442
4
+ handson_buildhat_api-1.0.dist-info/METADATA,sha256=k04oav7-OD3I0q4Zcer7oxyXljoGFxnxCKzqeQdbTzA,1367
5
+ handson_buildhat_api-1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
+ handson_buildhat_api-1.0.dist-info/top_level.txt,sha256=lZyPncSVT9fytRRmtXmZ5bIeg_Xlfmt8-_50B5ib5jU,21
7
+ handson_buildhat_api-1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,9 @@
1
+ Copyright (c) 2025 HandsON Technology co., ltd.
2
+
3
+ All rights reserved.
4
+
5
+ This software is provided for personal and educational use only.
6
+ You are granted permission to use this software *as-is* without modification.
7
+ You may not copy, modify, distribute, sublicense, or use this software in any commercial context without explicit written permission from the copyright holder.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
@@ -0,0 +1 @@
1
+ HandsON_BuildHat_API