JoyIT-MotoPi 1.0.0__tar.gz
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.
- joyit_motopi-1.0.0/.gitattributes +2 -0
- joyit_motopi-1.0.0/.gitignore +2 -0
- joyit_motopi-1.0.0/LICENSE +21 -0
- joyit_motopi-1.0.0/MotoPi/__init__.py +3 -0
- joyit_motopi-1.0.0/MotoPi/_pwmregister.py +59 -0
- joyit_motopi-1.0.0/MotoPi/pca9685.py +368 -0
- joyit_motopi-1.0.0/MotoPi/pcachannel.py +68 -0
- joyit_motopi-1.0.0/MotoPi/pwmchannel.py +100 -0
- joyit_motopi-1.0.0/PKG-INFO +68 -0
- joyit_motopi-1.0.0/README.md +52 -0
- joyit_motopi-1.0.0/example/example.py +18 -0
- joyit_motopi-1.0.0/pyproject.toml +26 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kendra-eck
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import MotoPi
|
|
2
|
+
from .pwmchannel import PWMChannel
|
|
3
|
+
|
|
4
|
+
class _PWMRegister:
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
Provide low-level access to the 16 PCA9685 PWM register pairs.
|
|
8
|
+
Each entry is a tuple of 12-bit on/off counters with the optional
|
|
9
|
+
0x1000 full-on/full-off flag bit. This class is intended for internal use.
|
|
10
|
+
|
|
11
|
+
:param MotoPi pca: MotoPi object to controll pwm registers
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
_LED0 = 0x06
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def __checkValue(value, min, max):
|
|
18
|
+
"""
|
|
19
|
+
Validate an integer against a half-open interval [min, max).
|
|
20
|
+
:param int value: parameter to check if it is in range
|
|
21
|
+
:param int min: minimum of value range
|
|
22
|
+
:param int max: maximum of value range
|
|
23
|
+
"""
|
|
24
|
+
if not min <= value < max:
|
|
25
|
+
raise ValueError(f"Value is not in range of {min} and {max}")
|
|
26
|
+
return value
|
|
27
|
+
|
|
28
|
+
def __init__(self, pca: "MotoPi"):
|
|
29
|
+
self._pca = pca
|
|
30
|
+
|
|
31
|
+
def __len__(self):
|
|
32
|
+
"""
|
|
33
|
+
Return the number of register groups (16).
|
|
34
|
+
"""
|
|
35
|
+
return 16
|
|
36
|
+
|
|
37
|
+
def __getitem__(self, index):
|
|
38
|
+
"""
|
|
39
|
+
Read the on/off counter values for a channel index from 0 to 15.
|
|
40
|
+
:param int index: index of channel to read on/off values from
|
|
41
|
+
"""
|
|
42
|
+
index = self.__checkValue(index, 0, len(self))
|
|
43
|
+
self._pca._enable_auto_increment()
|
|
44
|
+
data = self._pca._read_block(self._LED0 + index * 4, 4)
|
|
45
|
+
return (data[0] | (data[1] << 8), data[2] | (data[3] << 8))#
|
|
46
|
+
|
|
47
|
+
def __setitem__(self, index, value):
|
|
48
|
+
"""
|
|
49
|
+
Write the on/off counter values for a channel index from 0 to 15.
|
|
50
|
+
The values must each be in the inclusive range 0x0000 through 0x1000.
|
|
51
|
+
:param int index: index of channel to write value on
|
|
52
|
+
:param int value: value to write onto channel
|
|
53
|
+
"""
|
|
54
|
+
index = self.__checkValue(index, 0, len(self))
|
|
55
|
+
on, off = value
|
|
56
|
+
on = self.__checkValue(value[0], 0x00, 0x1001)
|
|
57
|
+
off = self.__checkValue(value[1], 0x00, 0x1001)
|
|
58
|
+
self._pca._enable_auto_increment()
|
|
59
|
+
self._pca._write_block(self._LED0 + index * 4, [on & 0xFF, on >> 8, off & 0xFF, off >> 8])
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
# This is a edited library from Adafruit
|
|
2
|
+
# Original can be found here: https://github.com/adafruit/Adafruit_CircuitPython_PCA9685
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
The MIT License (MIT)
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2016 Radomir Dopieralski, written for Adafruit Industries
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
26
|
+
"""
|
|
27
|
+
from ._pwmregister import _PWMRegister
|
|
28
|
+
from .pcachannel import PCAChannel
|
|
29
|
+
import math
|
|
30
|
+
import time
|
|
31
|
+
|
|
32
|
+
class MotoPi:
|
|
33
|
+
"""
|
|
34
|
+
Control the Joy-IT RB-Moto3 PCA9685 PWM controller over I2C.
|
|
35
|
+
|
|
36
|
+
The controller exposes 16 PWM outputs that share one frequency. By default
|
|
37
|
+
it uses I2C bus 1, address 0x41, an external 25 MHz clock, and BCM GPIO 7
|
|
38
|
+
as the board-enable output. Pass ``enable_pin=None`` to skip GPIO control.
|
|
39
|
+
|
|
40
|
+
Use the controller as a context manager to release hardware resources on exit.
|
|
41
|
+
``external_clock_hz`` specifies the clock rate for PWM calculations; it does
|
|
42
|
+
not measure or generate the external clock.
|
|
43
|
+
|
|
44
|
+
:param i2c_bus: I2C bus number (default: 1) or an SMBus-compatible bus object
|
|
45
|
+
:param int address: I2C device address (default: 0x41)
|
|
46
|
+
:param int external_clock_hz: Clock frequency used to calculate PWM timing in Hz (default: 25_000_000)
|
|
47
|
+
:param bool external_clock: if board is using external clock during reset (default: True)
|
|
48
|
+
:param int enable_pin: BCM GPIO number used to enable board or None to omit it (default: 7)
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
_MODE1 = 0x00
|
|
52
|
+
_MODE2 = 0x01
|
|
53
|
+
_PRESCALE = 0xFE
|
|
54
|
+
_RESTART = 0x80
|
|
55
|
+
_EXTCLK = 0x40
|
|
56
|
+
_AI = 0x20
|
|
57
|
+
_SLEEP = 0x10
|
|
58
|
+
_LED0 = 0x06
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
i2c_bus=1,
|
|
63
|
+
*,
|
|
64
|
+
address: int = 0x41,
|
|
65
|
+
external_clock_hz: int = 25_000_000,
|
|
66
|
+
external_clock=True,
|
|
67
|
+
enable_pin = 7,
|
|
68
|
+
) -> None:
|
|
69
|
+
|
|
70
|
+
self._address = address
|
|
71
|
+
if self._address == 0x70:
|
|
72
|
+
raise ValueError("0x70 is the all-call-address of the PCA9685")
|
|
73
|
+
self.reference_clock_speed = external_clock_hz
|
|
74
|
+
self._external_clock = bool(external_clock)
|
|
75
|
+
self._closed = False
|
|
76
|
+
self._owns_bus = isinstance(i2c_bus, int)
|
|
77
|
+
self._bus = None
|
|
78
|
+
self._enable = None
|
|
79
|
+
try:
|
|
80
|
+
if enable_pin is not None:
|
|
81
|
+
from gpiozero import OutputDevice
|
|
82
|
+
self._enable = OutputDevice(enable_pin, active_high = True, initial_value = True)
|
|
83
|
+
if self._owns_bus:
|
|
84
|
+
from smbus2 import SMBus
|
|
85
|
+
self._bus = SMBus(i2c_bus)
|
|
86
|
+
else:
|
|
87
|
+
self._bus = i2c_bus
|
|
88
|
+
self.channels = PCAChannel(self)
|
|
89
|
+
self.pwm_regs = _PWMRegister(self)
|
|
90
|
+
self.reset()
|
|
91
|
+
except Exception:
|
|
92
|
+
try:
|
|
93
|
+
self._release_resources()
|
|
94
|
+
finally:
|
|
95
|
+
self._closed = True
|
|
96
|
+
raise
|
|
97
|
+
|
|
98
|
+
@staticmethod
|
|
99
|
+
def _positiveValue(value):
|
|
100
|
+
"""
|
|
101
|
+
Convert a value to a finite, strictly positive float.
|
|
102
|
+
Raises ValueError: If the converted value is zero, negative, or non-finite.
|
|
103
|
+
|
|
104
|
+
:param float value: value to check if finite and positive
|
|
105
|
+
"""
|
|
106
|
+
value = float(value)
|
|
107
|
+
if not math.isfinite(value) or value <= 0:
|
|
108
|
+
raise ValueError("Value is not above 0")
|
|
109
|
+
return value
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def reference_clock_speed(self):
|
|
113
|
+
"""
|
|
114
|
+
Return the configured reference-clock frequency in hertz.
|
|
115
|
+
"""
|
|
116
|
+
return self._clock_hz
|
|
117
|
+
|
|
118
|
+
@reference_clock_speed.setter
|
|
119
|
+
def reference_clock_speed(self, value):
|
|
120
|
+
"""
|
|
121
|
+
Set the clock rate used for PWM calculations (greater than 0, at most 50 MHz).
|
|
122
|
+
|
|
123
|
+
:param int value: new value to set clock rate to
|
|
124
|
+
"""
|
|
125
|
+
value = self._positiveValue(value)
|
|
126
|
+
if value > 50_000_000:
|
|
127
|
+
raise ValueError("The PCA9685 only supports a maximum of 50 MHz.")
|
|
128
|
+
self._clock_hz = value
|
|
129
|
+
|
|
130
|
+
def _check_open(self):
|
|
131
|
+
"""
|
|
132
|
+
Raises RuntimeError: this controller has already been closed.
|
|
133
|
+
"""
|
|
134
|
+
if self._closed:
|
|
135
|
+
raise RuntimeError("PCA9685 was already closed")
|
|
136
|
+
|
|
137
|
+
def _read8(self, register):
|
|
138
|
+
"""
|
|
139
|
+
Read one byte from an I2C register of the configured controller.
|
|
140
|
+
|
|
141
|
+
:param int register: register to read byte from
|
|
142
|
+
"""
|
|
143
|
+
self._check_open()
|
|
144
|
+
return self._bus.read_byte_data(self._address, register)
|
|
145
|
+
|
|
146
|
+
def _write8(self, register, value):
|
|
147
|
+
"""
|
|
148
|
+
Write one byte to an I2C register of the configured controller.
|
|
149
|
+
|
|
150
|
+
:param int register: register to write byte onto
|
|
151
|
+
:param int value: byte to write onto register
|
|
152
|
+
"""
|
|
153
|
+
self._check_open()
|
|
154
|
+
self._bus.write_byte_data(self._address, register, value)
|
|
155
|
+
|
|
156
|
+
def _read_block(self, register, length):
|
|
157
|
+
"""
|
|
158
|
+
Read exactly ``length`` consecutive bytes from an I2C register.
|
|
159
|
+
Raises OSError: If the bus returns fewer bytes than requested.
|
|
160
|
+
|
|
161
|
+
:param int register: register to read from
|
|
162
|
+
:param int length: anmount of bytes to read from register
|
|
163
|
+
"""
|
|
164
|
+
self._check_open()
|
|
165
|
+
data = self._bus.read_i2c_block_data(self._address, register, length)
|
|
166
|
+
if len(data) != length:
|
|
167
|
+
raise OSError("Incomplete I2C answer")
|
|
168
|
+
return data
|
|
169
|
+
|
|
170
|
+
def _write_block(self, register, data):
|
|
171
|
+
"""
|
|
172
|
+
Write a sequence of bytes beginning at the given I2C register.
|
|
173
|
+
|
|
174
|
+
:param int register: register to write onto
|
|
175
|
+
:param int[] data: bytes to write onto register
|
|
176
|
+
"""
|
|
177
|
+
self._check_open()
|
|
178
|
+
self._bus.write_i2c_block_data(self._address, register, data)
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def mode1_reg(self):
|
|
182
|
+
"""
|
|
183
|
+
Read the PCA9685 MODE1 configuration register.
|
|
184
|
+
"""
|
|
185
|
+
return self._read8(self._MODE1)
|
|
186
|
+
|
|
187
|
+
@mode1_reg.setter
|
|
188
|
+
def mode1_reg(self, value):
|
|
189
|
+
"""
|
|
190
|
+
Write the PCA9685 MODE1 configuration register.
|
|
191
|
+
|
|
192
|
+
:param int value: value to write onto MODE1 configuration register
|
|
193
|
+
"""
|
|
194
|
+
self._write8(self._MODE1, value)
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def mode2_reg(self):
|
|
198
|
+
"""
|
|
199
|
+
Read the PCA9685 MODE2 output configuration register.
|
|
200
|
+
"""
|
|
201
|
+
return self._read8(self._MODE2)
|
|
202
|
+
|
|
203
|
+
@mode2_reg.setter
|
|
204
|
+
def mode2_reg(self, value):
|
|
205
|
+
"""
|
|
206
|
+
Write the PCA9685 MODE2 output configuration register.
|
|
207
|
+
|
|
208
|
+
:param int value: value to write onto MODE2 configuration register
|
|
209
|
+
"""
|
|
210
|
+
self._write8(self._MODE2, value)
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def prescale_reg(self):
|
|
214
|
+
"""
|
|
215
|
+
Read the PWM frequency prescaler register.
|
|
216
|
+
"""
|
|
217
|
+
return self._read8(self._PRESCALE)
|
|
218
|
+
|
|
219
|
+
@prescale_reg.setter
|
|
220
|
+
def prescale_reg(self, value):
|
|
221
|
+
"""
|
|
222
|
+
Write the PWM frequency prescaler register.
|
|
223
|
+
|
|
224
|
+
:param int value: value to write onto frequency prescaler register
|
|
225
|
+
"""
|
|
226
|
+
self._write8(self._PRESCALE, value)
|
|
227
|
+
|
|
228
|
+
def _enable_auto_increment(self):
|
|
229
|
+
"""
|
|
230
|
+
Enable automatic I2C register-address increment for block transfers.
|
|
231
|
+
"""
|
|
232
|
+
mode = self.mode1_reg
|
|
233
|
+
if not mode & self._AI:
|
|
234
|
+
self.mode1_reg = (mode & ~self._RESTART) | self._AI
|
|
235
|
+
|
|
236
|
+
def reset(self) -> None:
|
|
237
|
+
"""
|
|
238
|
+
Reset PWM output configuration and initialize the selected clock source.
|
|
239
|
+
All 16 PWM channels are switched off. The PCA9685 is configured with
|
|
240
|
+
auto-increment enabled and MODE2 set to 0x04.
|
|
241
|
+
|
|
242
|
+
Raises RuntimeError: If an active external clock conflicts with the requested internal-clock configuration. Power-cycle the device if necessary.
|
|
243
|
+
"""
|
|
244
|
+
mode = self.mode1_reg
|
|
245
|
+
if mode & self._EXTCLK and not self._external_clock:
|
|
246
|
+
raise RuntimeError("EXTCLK is already active. External tact has to be configured or switch board on and off again to use internal tact.")
|
|
247
|
+
base = self._AI | (mode & self._EXTCLK)
|
|
248
|
+
self.mode1_reg = base
|
|
249
|
+
self.all_off()
|
|
250
|
+
self.mode1_reg = base | self._SLEEP
|
|
251
|
+
if self._external_clock:
|
|
252
|
+
base |= self._EXTCLK
|
|
253
|
+
self.mode1_reg = base | self._SLEEP
|
|
254
|
+
self.mode2_reg = 0x04
|
|
255
|
+
self.mode1_reg = base
|
|
256
|
+
time.sleep(0.005)
|
|
257
|
+
|
|
258
|
+
@property
|
|
259
|
+
def frequency(self) -> float:
|
|
260
|
+
"""
|
|
261
|
+
Get the common PWM frequency in hertz, calculated from the prescaler.
|
|
262
|
+
"""
|
|
263
|
+
prescale = self.prescale_reg
|
|
264
|
+
if prescale < 3:
|
|
265
|
+
raise ValueError("The device pre_scale register (0xFE) was not read or returned a value < 3")
|
|
266
|
+
return self.reference_clock_speed / (4096 * (prescale + 1))
|
|
267
|
+
|
|
268
|
+
@frequency.setter
|
|
269
|
+
def frequency(self, freq: float) -> None:
|
|
270
|
+
"""
|
|
271
|
+
Set the PWM frequency shared by all 16 channels, in hertz.
|
|
272
|
+
The frequency is quantized to the PCA9685 prescaler resolution.
|
|
273
|
+
|
|
274
|
+
Raises ValueError: If the requested frequency cannot be represented.
|
|
275
|
+
|
|
276
|
+
:param float freq: new PWM frequency shared by all 16 channels in Hz
|
|
277
|
+
"""
|
|
278
|
+
freq = self._positiveValue(freq)
|
|
279
|
+
prescale = int(self.reference_clock_speed / (4096.0 * freq) + 0.5) - 1
|
|
280
|
+
if not 3 <= prescale <= 0xFF:
|
|
281
|
+
raise ValueError("PCA9685 cannot output at the given frequency")
|
|
282
|
+
mode = self.mode1_reg # Mode 1
|
|
283
|
+
base = (mode & ~(self._RESTART | self._SLEEP)) | self._AI
|
|
284
|
+
self.mode1_reg = base | self._SLEEP
|
|
285
|
+
self.prescale_reg = prescale # Prescale
|
|
286
|
+
self.mode1_reg = base # Mode 1
|
|
287
|
+
time.sleep(0.005)
|
|
288
|
+
# Mode 1, autoincrement on, fix to stop pca9685 from accepting commands at all addresses
|
|
289
|
+
self.mode1_reg = base | self._RESTART
|
|
290
|
+
|
|
291
|
+
def set_pwm(self, channel, on, off):
|
|
292
|
+
"""
|
|
293
|
+
Set raw 12-bit PWM on/off counter positions for a channel.
|
|
294
|
+
|
|
295
|
+
:param int channel: PWM output index from 0 to 15
|
|
296
|
+
:param int on: Counter value for the rising edge, or 0x1000 for full-on
|
|
297
|
+
:param int off: Counter value for the falling edge, or 0x1000 for full-off
|
|
298
|
+
"""
|
|
299
|
+
self.pwm_regs[channel] = (on, off)
|
|
300
|
+
|
|
301
|
+
def write_microseconds(self, channel, microseconds):
|
|
302
|
+
"""
|
|
303
|
+
Set a channel's high pulse width in microseconds.
|
|
304
|
+
The width is converted to 12-bit ticks using the current shared PWM
|
|
305
|
+
frequency. A width of zero switches the channel off. Set ``frequency``
|
|
306
|
+
before using this method.
|
|
307
|
+
|
|
308
|
+
Raises ValueError: If the width is invalid or cannot fit into one PWM period.
|
|
309
|
+
|
|
310
|
+
:param int channel: PWM output index from 0 to 15
|
|
311
|
+
:param int microseconds: Non-negative pulse duration in microseconds
|
|
312
|
+
"""
|
|
313
|
+
microseconds = float(microseconds)
|
|
314
|
+
if not math.isfinite(microseconds) or microseconds < 0:
|
|
315
|
+
raise ValueError("Pulse length must be finite and >= 0")
|
|
316
|
+
if microseconds == 0:
|
|
317
|
+
self.channels[channel].duty_cycle = 0
|
|
318
|
+
return
|
|
319
|
+
ticks = int(microseconds * self.frequency * 4096 / 1_000_000 + 0.5)
|
|
320
|
+
if not 1 <= ticks <= 4095:
|
|
321
|
+
raise ValueError("Pulse length is not within PWM period")
|
|
322
|
+
self.set_pwm(channel, 0, ticks)
|
|
323
|
+
|
|
324
|
+
def all_off(self):
|
|
325
|
+
"""
|
|
326
|
+
Switch all 16 PWM outputs fully off using block I2C writes.
|
|
327
|
+
"""
|
|
328
|
+
self._enable_auto_increment()
|
|
329
|
+
for first in range(0, 16, 4):
|
|
330
|
+
self._write_block(self._LED0 + first * 4, [0, 0, 0, 0x10] * 4)
|
|
331
|
+
|
|
332
|
+
def __enter__(self) -> "MotoPi":
|
|
333
|
+
"""
|
|
334
|
+
Return the open controller for use in a ``with`` statement.
|
|
335
|
+
"""
|
|
336
|
+
self._check_open()
|
|
337
|
+
return self
|
|
338
|
+
|
|
339
|
+
def __exit__(self, exception_type, exception_value, traceback) -> None:
|
|
340
|
+
"""
|
|
341
|
+
Release the controller when leaving a ``with`` statement.
|
|
342
|
+
"""
|
|
343
|
+
self.deinit()
|
|
344
|
+
|
|
345
|
+
def deinit(self) -> None:
|
|
346
|
+
"""
|
|
347
|
+
Turn off all channels and release the owned I2C bus and GPIO device.
|
|
348
|
+
This operation is idempotent. ``close`` is an alias of this method.
|
|
349
|
+
"""
|
|
350
|
+
if self._closed:
|
|
351
|
+
return
|
|
352
|
+
try:
|
|
353
|
+
self.all_off()
|
|
354
|
+
finally:
|
|
355
|
+
self._closed = True
|
|
356
|
+
self._release_resources()
|
|
357
|
+
|
|
358
|
+
close = deinit
|
|
359
|
+
|
|
360
|
+
def _release_resources(self):
|
|
361
|
+
"""
|
|
362
|
+
Close the I2C bus if owned and deactivate/close the GPIO output.
|
|
363
|
+
"""
|
|
364
|
+
if self._owns_bus:
|
|
365
|
+
self._bus.close()
|
|
366
|
+
if self._enable is not None:
|
|
367
|
+
self._enable.off()
|
|
368
|
+
self._enable.close()
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# This is a edited library from Adafruit
|
|
2
|
+
# Original can be found here: https://github.com/adafruit/Adafruit_CircuitPython_PCA9685
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
The MIT License (MIT)
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2016 Radomir Dopieralski, written for Adafruit Industries
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
26
|
+
"""
|
|
27
|
+
import MotoPi
|
|
28
|
+
from .pwmchannel import PWMChannel
|
|
29
|
+
import operator
|
|
30
|
+
|
|
31
|
+
class PCAChannel:
|
|
32
|
+
"""
|
|
33
|
+
Provide indexed, lazily created access to the 16 PWM channels.
|
|
34
|
+
Channel objects are cached so repeated lookup returns the same object.
|
|
35
|
+
Integer indices and Python slices are supported.
|
|
36
|
+
|
|
37
|
+
:param MotoPi pca: object of MotoPi
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, pca: "MotoPi") -> None:
|
|
41
|
+
self._pca = pca
|
|
42
|
+
self._channels = [None] * len(self)
|
|
43
|
+
|
|
44
|
+
def __len__(self) -> int:
|
|
45
|
+
"""
|
|
46
|
+
Return the number of PWM output channels (16).
|
|
47
|
+
"""
|
|
48
|
+
return 16
|
|
49
|
+
|
|
50
|
+
def __getitem__(self, index: int) -> PWMChannel:
|
|
51
|
+
"""
|
|
52
|
+
Return a PWMChannel or a list of channels for a slice.
|
|
53
|
+
Negative integer indices are supported as in a Python sequence.
|
|
54
|
+
|
|
55
|
+
Raises IndexError: If an integer index is outside the available channels.
|
|
56
|
+
|
|
57
|
+
:param int index: index of pwm channel
|
|
58
|
+
"""
|
|
59
|
+
if isinstance(index, slice):
|
|
60
|
+
return [self[i] for i in range(*index.indices(len(self)))]
|
|
61
|
+
index = operator.index(index)
|
|
62
|
+
if index < 0:
|
|
63
|
+
index += len(self)
|
|
64
|
+
if not 0 <= index < 16:
|
|
65
|
+
raise IndexError("Index is not in range")
|
|
66
|
+
if self._channels[index] is None:
|
|
67
|
+
self._channels[index] = PWMChannel(self._pca, index)
|
|
68
|
+
return self._channels[index]
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# This is a edited library from Adafruit
|
|
2
|
+
# Original can be found here: https://github.com/adafruit/Adafruit_CircuitPython_PCA9685
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
The MIT License (MIT)
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2016 Radomir Dopieralski, written for Adafruit Industries
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
26
|
+
"""
|
|
27
|
+
import MotoPi
|
|
28
|
+
|
|
29
|
+
class PWMChannel:
|
|
30
|
+
"""
|
|
31
|
+
Represent one of the 16 PCA9685 PWM output channels.
|
|
32
|
+
The public ``duty_cycle`` property uses the 16-bit PWMOut-style scale, while the underlying PCA9685 chip has 12-bit timing resolution.
|
|
33
|
+
|
|
34
|
+
:param MotoPi pca: The MotoPi object
|
|
35
|
+
:param int index: The index of the channel
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def __checkIndex(index: int, min=0, max=15):
|
|
40
|
+
"""
|
|
41
|
+
Validate that a channel index falls within the inclusive bounds."""
|
|
42
|
+
if not min <= index <= max:
|
|
43
|
+
raise ValueError(f"Index is not in range of {min} and {max}")
|
|
44
|
+
return index
|
|
45
|
+
|
|
46
|
+
def __init__(self, pca: "MotoPi", index: int):
|
|
47
|
+
self._pca = pca
|
|
48
|
+
self._index = self.__checkIndex(index)
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def frequency(self) -> float:
|
|
52
|
+
"""
|
|
53
|
+
Return the shared PCA9685 PWM frequency in Hertz.
|
|
54
|
+
"""
|
|
55
|
+
return self._pca.frequency
|
|
56
|
+
|
|
57
|
+
@frequency.setter
|
|
58
|
+
def frequency(self, value):
|
|
59
|
+
"""
|
|
60
|
+
The overall PWM frequency in Hertz (read-only).
|
|
61
|
+
A PWMChannel's frequency cannot be set individually.
|
|
62
|
+
All channels share a common frequency, set by PCA9685.frequency.
|
|
63
|
+
"""
|
|
64
|
+
raise NotImplementedError("frequency cannot be set on individual channels")
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def duty_cycle(self) -> int:
|
|
68
|
+
"""
|
|
69
|
+
Read duty cycle as a 16-bit value between 0 and 65535.
|
|
70
|
+
|
|
71
|
+
Zero means fully off, while 65535 means fully on.
|
|
72
|
+
Because the chip uses 12-bit PWM timing, intermediate values have limited resolution.
|
|
73
|
+
"""
|
|
74
|
+
pwm = self._pca.pwm_regs[self._index]
|
|
75
|
+
if pwm[1] == 0x1000:
|
|
76
|
+
return 0x0000
|
|
77
|
+
if pwm[0] == 0x1000:
|
|
78
|
+
return 0xFFFF
|
|
79
|
+
return ((pwm[1] - pwm[0]) & 0x0FFF) << 4
|
|
80
|
+
|
|
81
|
+
@duty_cycle.setter
|
|
82
|
+
def duty_cycle(self, value: int) -> None:
|
|
83
|
+
"""
|
|
84
|
+
Set the channel duty cycle using a 16-bit value (0 to 65535).
|
|
85
|
+
|
|
86
|
+
Zero selects full-off and 65535 selects full-on.
|
|
87
|
+
Other values are quantized to the controller's 12-bit PWM resolution.
|
|
88
|
+
|
|
89
|
+
Raises ValueError: If the value is outside the supported range.
|
|
90
|
+
"""
|
|
91
|
+
if not 0 <= value <= 0xFFFF:
|
|
92
|
+
raise ValueError(f"Out of range: value {value} not 0 <= value <= 65,535")
|
|
93
|
+
if value == 0xFFFF: # Special case for "fully on":
|
|
94
|
+
self._pca.pwm_regs[self._index] = (0x1000, 0)
|
|
95
|
+
elif value < 0x0010: # Special case for "fully off":
|
|
96
|
+
self._pca.pwm_regs[self._index] = (0, 0x1000)
|
|
97
|
+
else: # Shift our value by four because the PCA9685 is only 12 bits but our value is 16
|
|
98
|
+
# value should never be zero here because of the test for the "fully off" case
|
|
99
|
+
# (the LEDn_ON and LEDn_OFF registers should never be set with the same values)
|
|
100
|
+
self._pca.pwm_regs[self._index] = (0, value >> 4)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: JoyIT_MotoPi
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: This library enables usage of RB-Moto3 with the Raspberry Pi.
|
|
5
|
+
Project-URL: Homepage, https://github.com/joy-it/JoyIT_MotoPi
|
|
6
|
+
Project-URL: Issues, https://github.com/joy-it/JoyIT_MotoPi/issues
|
|
7
|
+
Author-email: Joy-IT <service@joy-it.net>
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Requires-Dist: adafruit-circuitpython-motor
|
|
13
|
+
Requires-Dist: gpiozero
|
|
14
|
+
Requires-Dist: smbus2
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# JoyIT_MotoPi
|
|
18
|
+
|
|
19
|
+
Python library for controlling the **Joy-IT RB-Moto3** motor/servo expansion board on a Raspberry Pi. The board uses a **PCA9685 16-channel, 12-bit PWM controller** connected via I²C. The library provides access to the PWM channels, a shared frequency setting, raw PWM counter values and microsecond pulse generation.
|
|
20
|
+
|
|
21
|
+
This implementation communicates directly with the controller through `smbus2` and uses `gpiozero` for the optional board-enable GPIO. **It does not require Adafruit CircuitPython PCA9685.** The included servo example uses `adafruit-circuitpython-motor`, which is a separate package.
|
|
22
|
+
|
|
23
|
+
## Requirements
|
|
24
|
+
|
|
25
|
+
- Raspberry Pi with Python **3.9 or later** and the I²C interface enabled.
|
|
26
|
+
- Joy-IT RB-Moto3 connected to the appropriate Raspberry Pi GPIO/I²C interface.
|
|
27
|
+
- Python packages: `smbus2`, `gpiozero` and `adafruit-circuitpython-motor` (declared in `pyproject.toml`).
|
|
28
|
+
- The Linux user must have access to the I²C device and the configured GPIO pin.
|
|
29
|
+
|
|
30
|
+
Enable I²C using `sudo raspi-config` (Interface Options → I2C), then reboot if prompted.
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
You can install this library from PyPI. To install it for the current user on your Raspberry Pi, use the following command:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip3 install JoyIT_MotoPi
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Install library into virtual environment:
|
|
41
|
+
```bash
|
|
42
|
+
python -m venv --system-site-packages env
|
|
43
|
+
source env/bin/activate
|
|
44
|
+
pip3 install JoyIT_MotoPi
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## API reference
|
|
48
|
+
|
|
49
|
+
| API | Description |
|
|
50
|
+
| --- | --- |
|
|
51
|
+
| `MotoPi(...)` | Open the PWM controller and initialize its channels. |
|
|
52
|
+
| `motopi.frequency` | Read or set the frequency shared by all 16 PWM outputs, in Hz. |
|
|
53
|
+
| `motopi.channels[index]` | Get a channel (`0` through `15`); slices are also supported. |
|
|
54
|
+
| `channel.duty_cycle` | Read/set a 16-bit duty-cycle value, from `0` (off) to `65535` (fully on). |
|
|
55
|
+
| `channel.frequency` | Read-only shared frequency; cannot be changed per channel. |
|
|
56
|
+
| `motopi.set_pwm(channel, on, off)` | Write raw PWM on/off counter values (0–4095; `0x1000` is a special full-on/full-off flag). |
|
|
57
|
+
| `motopi.write_microseconds(channel, duration)` | Set the high pulse duration in µs at the current PWM frequency; `0` turns the channel off. |
|
|
58
|
+
| `motopi.all_off()` | Turn off all outputs. |
|
|
59
|
+
| `motopi.reset()` | Reinitialize the controller and clock selection; turns all channels off. |
|
|
60
|
+
| `motopi.deinit()` / `motopi.close()` | Turn outputs off and release resources. |
|
|
61
|
+
|
|
62
|
+
>[!Note]
|
|
63
|
+
> Prefer the `with MotoPi(...) as motopi:` form: it calls `deinit()` when the block ends, including on exceptions. Without a context manager, call `motopi.close()` explicitly. When the library opens the I²C bus itself, it closes that bus; when you pass an existing bus object, it leaves ownership of that bus with the caller.
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
## License and attribution
|
|
67
|
+
|
|
68
|
+
See [LICENSE](LICENSE). Portions of the implementation are adapted from the [Adafruit CircuitPython PCA9685 library](https://github.com/adafruit/Adafruit_CircuitPython_PCA9685); the corresponding copyright and MIT license notices are retained in the source files.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# JoyIT_MotoPi
|
|
2
|
+
|
|
3
|
+
Python library for controlling the **Joy-IT RB-Moto3** motor/servo expansion board on a Raspberry Pi. The board uses a **PCA9685 16-channel, 12-bit PWM controller** connected via I²C. The library provides access to the PWM channels, a shared frequency setting, raw PWM counter values and microsecond pulse generation.
|
|
4
|
+
|
|
5
|
+
This implementation communicates directly with the controller through `smbus2` and uses `gpiozero` for the optional board-enable GPIO. **It does not require Adafruit CircuitPython PCA9685.** The included servo example uses `adafruit-circuitpython-motor`, which is a separate package.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Raspberry Pi with Python **3.9 or later** and the I²C interface enabled.
|
|
10
|
+
- Joy-IT RB-Moto3 connected to the appropriate Raspberry Pi GPIO/I²C interface.
|
|
11
|
+
- Python packages: `smbus2`, `gpiozero` and `adafruit-circuitpython-motor` (declared in `pyproject.toml`).
|
|
12
|
+
- The Linux user must have access to the I²C device and the configured GPIO pin.
|
|
13
|
+
|
|
14
|
+
Enable I²C using `sudo raspi-config` (Interface Options → I2C), then reboot if prompted.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
You can install this library from PyPI. To install it for the current user on your Raspberry Pi, use the following command:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip3 install JoyIT_MotoPi
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Install library into virtual environment:
|
|
25
|
+
```bash
|
|
26
|
+
python -m venv --system-site-packages env
|
|
27
|
+
source env/bin/activate
|
|
28
|
+
pip3 install JoyIT_MotoPi
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## API reference
|
|
32
|
+
|
|
33
|
+
| API | Description |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| `MotoPi(...)` | Open the PWM controller and initialize its channels. |
|
|
36
|
+
| `motopi.frequency` | Read or set the frequency shared by all 16 PWM outputs, in Hz. |
|
|
37
|
+
| `motopi.channels[index]` | Get a channel (`0` through `15`); slices are also supported. |
|
|
38
|
+
| `channel.duty_cycle` | Read/set a 16-bit duty-cycle value, from `0` (off) to `65535` (fully on). |
|
|
39
|
+
| `channel.frequency` | Read-only shared frequency; cannot be changed per channel. |
|
|
40
|
+
| `motopi.set_pwm(channel, on, off)` | Write raw PWM on/off counter values (0–4095; `0x1000` is a special full-on/full-off flag). |
|
|
41
|
+
| `motopi.write_microseconds(channel, duration)` | Set the high pulse duration in µs at the current PWM frequency; `0` turns the channel off. |
|
|
42
|
+
| `motopi.all_off()` | Turn off all outputs. |
|
|
43
|
+
| `motopi.reset()` | Reinitialize the controller and clock selection; turns all channels off. |
|
|
44
|
+
| `motopi.deinit()` / `motopi.close()` | Turn outputs off and release resources. |
|
|
45
|
+
|
|
46
|
+
>[!Note]
|
|
47
|
+
> Prefer the `with MotoPi(...) as motopi:` form: it calls `deinit()` when the block ends, including on exceptions. Without a context manager, call `motopi.close()` explicitly. When the library opens the I²C bus itself, it closes that bus; when you pass an existing bus object, it leaves ownership of that bus with the caller.
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
## License and attribution
|
|
51
|
+
|
|
52
|
+
See [LICENSE](LICENSE). Portions of the implementation are adapted from the [Adafruit CircuitPython PCA9685 library](https://github.com/adafruit/Adafruit_CircuitPython_PCA9685); the corresponding copyright and MIT license notices are retained in the source files.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from MotoPi import MotoPi
|
|
3
|
+
from adafruit_motor import servo
|
|
4
|
+
|
|
5
|
+
# open MotoPi as motopi
|
|
6
|
+
with MotoPi(address=0x41, enable_pin=7) as motopi:
|
|
7
|
+
# set frequency of MotoPi
|
|
8
|
+
motopi.frequency = 50
|
|
9
|
+
# declare servo channel 0 of MotoPi
|
|
10
|
+
servo0 = servo.Servo(motopi.channels[0])
|
|
11
|
+
# move servo from position 0 to 180
|
|
12
|
+
for i in range(180):
|
|
13
|
+
servo0.angle = i
|
|
14
|
+
time.sleep(0.03)
|
|
15
|
+
# move servo from position 0 to 180
|
|
16
|
+
for i in range(180):
|
|
17
|
+
servo0.angle = 180 - i
|
|
18
|
+
time.sleep(0.03)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling >= 1.26"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[tool.hatch.build.targets.wheel]
|
|
6
|
+
packages = ["MotoPi"]
|
|
7
|
+
|
|
8
|
+
[project]
|
|
9
|
+
name = "JoyIT_MotoPi"
|
|
10
|
+
version = "1.0.0"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name="Joy-IT", email="service@joy-it.net" },
|
|
13
|
+
]
|
|
14
|
+
description = "This library enables usage of RB-Moto3 with the Raspberry Pi."
|
|
15
|
+
readme = "README.md"
|
|
16
|
+
requires-python = ">=3.9"
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
]
|
|
21
|
+
license-files = ["LICENSE"]
|
|
22
|
+
dependencies = ["smbus2", "gpiozero", "adafruit-circuitpython-motor"]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://github.com/joy-it/JoyIT_MotoPi"
|
|
26
|
+
Issues = "https://github.com/joy-it/JoyIT_MotoPi/issues"
|