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.
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
@@ -0,0 +1,2 @@
1
+ .DS_Store
2
+ /dist
@@ -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,3 @@
1
+ from .pca9685 import MotoPi
2
+
3
+ name = "MotoPi"
@@ -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"