pamoja.actuators
Idiomatic actuator-driver facade.
These are the command-encode half of two parts that move something: a PCA9685 driving up to sixteen servos, LEDs, or valves, and a stepper motor walked one coil pattern at a time. Applying the bytes is the caller's job; working out which bytes is this layer's.
1"""Idiomatic actuator-driver facade. 2 3These are the command-encode half of two parts that move something: a PCA9685 4driving up to sixteen servos, LEDs, or valves, and a stepper motor walked one coil 5pattern at a time. Applying the bytes is the caller's job; working out which bytes 6is this layer's. 7""" 8 9from __future__ import annotations 10 11import enum 12from typing import NamedTuple 13 14from pamoja._native import Stepper as _NativeStepper 15from pamoja._native import pca9685_channel_register as _channel_register 16from pamoja._native import pca9685_frequency_for_prescale as _frequency_for_prescale 17from pamoja._native import pca9685_limits as _limits 18from pamoja._native import pca9685_prescale_for_frequency as _prescale_for_frequency 19from pamoja._native import pwm_counts as _pwm_counts 20from pamoja._native import pwm_duty as _pwm_duty 21from pamoja._native import pwm_from_counts as _pwm_from_counts 22from pamoja._native import pwm_full_off as _pwm_full_off 23from pamoja._native import pwm_full_on as _pwm_full_on 24from pamoja._native import pwm_servo as _pwm_servo 25from pamoja._native import stepper_step_count as _step_count 26from pamoja._native import stepper_steps_for_degrees as _steps_for_degrees 27 28__all__ = ["Direction", "Drive", "Pwm", "Stepper", "pca9685", "pwm", "steps_for_degrees"] 29 30_INTERNAL_OSC_HZ, _CHANNELS, _COUNTS = _limits() 31 32 33class Drive(str, enum.Enum): 34 """A stepper drive pattern, trading torque, smoothness, and resolution.""" 35 36 #: One coil energised at a time: four steps, least torque and least power. 37 WAVE = "Wave" 38 #: Two adjacent coils at a time: four steps, most torque. 39 FULL_STEP = "FullStep" 40 #: Alternating one and two coils: eight steps, double resolution. 41 HALF_STEP = "HalfStep" 42 43 @property 44 def step_count(self) -> int: 45 """How many steps make up one electrical cycle of this pattern.""" 46 return _step_count(self.value) 47 48 49class Direction(str, enum.Enum): 50 """Which way to step a motor.""" 51 52 #: Advance the sequence, turning the shaft one way. 53 FORWARD = "Forward" 54 #: Reverse the sequence, turning the shaft the other way. 55 BACKWARD = "Backward" 56 57 58class Stepper: 59 """A stepper motor's place in its drive sequence, and how far it has turned. 60 61 Example:: 62 63 motor = Stepper(Drive.HALF_STEP) 64 coils = motor.step(Direction.FORWARD) 65 """ 66 67 __slots__ = ("_native",) 68 69 def __init__(self, drive: Drive) -> None: 70 """Create a stepper at the start of a pattern, with its position at zero. 71 72 :param drive: The coil pattern to walk. 73 """ 74 self._native = _NativeStepper(Drive(drive).value) 75 76 def step(self, direction: Direction) -> int: 77 """Advance one step and return the four-bit coil pattern to apply. 78 79 The most significant of the four bits is the first coil. 80 81 :param direction: Which way to turn. 82 :returns: The coil pattern. 83 """ 84 return self._native.step(Direction(direction).value) 85 86 @property 87 def coils(self) -> int: 88 """The coil pattern currently held, without advancing.""" 89 return self._native.coils 90 91 @property 92 def steps(self) -> int: 93 """How many steps have been taken, signed by direction.""" 94 return self._native.steps 95 96 97class Pwm(NamedTuple): 98 """When in a period a PWM output goes high and low, in counts.""" 99 100 on: int 101 """The count at which the output goes high.""" 102 103 off: int 104 """The count at which it goes low, or the full-off flag.""" 105 106 107class _Pca9685: 108 """An NXP PCA9685 16-channel PWM controller, for servos, LEDs, and valves.""" 109 110 __slots__ = () 111 112 #: The part's internal oscillator frequency, in hertz. 113 INTERNAL_OSC_HZ = _INTERNAL_OSC_HZ 114 #: How many channels it drives. 115 CHANNELS = _CHANNELS 116 #: How many counts each period is divided into. 117 COUNTS = _COUNTS 118 119 def channel_register(self, channel: int) -> int: 120 """Return the first of a channel's four consecutive registers. 121 122 :param channel: The channel, 0 to 15. 123 :returns: The register address. 124 :raises ValueError: If the channel is beyond the part. 125 """ 126 return _channel_register(channel) 127 128 def prescale_for_frequency( 129 self, update_rate_hz: int, osc_hz: int = _INTERNAL_OSC_HZ 130 ) -> int: 131 """Return the prescale value that sets an update rate. 132 133 :param update_rate_hz: The PWM frequency wanted. 134 :param osc_hz: The oscillator frequency, usually the internal one. 135 :returns: The prescale register value. 136 """ 137 return _prescale_for_frequency(update_rate_hz, osc_hz) 138 139 def frequency_for_prescale( 140 self, prescale: int, osc_hz: int = _INTERNAL_OSC_HZ 141 ) -> float: 142 """Return the update rate a prescale value produces. 143 144 :param prescale: The prescale register value. 145 :param osc_hz: The oscillator frequency, usually the internal one. 146 :returns: The frequency in hertz. 147 """ 148 return _frequency_for_prescale(prescale, osc_hz) 149 150 151class _Pwm: 152 """The four register bytes for one PCA9685 channel. 153 154 Each call returns them in the channel's own register order, so they can be 155 written in a single bus transaction. 156 """ 157 158 __slots__ = () 159 160 def from_counts(self, on: int, off: int) -> bytes: 161 """Build a setting from explicit on and off counts. 162 163 :param on: The count at which the output goes high. 164 :param off: The count at which it goes low. 165 :returns: The four register bytes; counts are masked to 12 bits. 166 """ 167 return _pwm_from_counts(on, off) 168 169 def duty(self, off: int) -> bytes: 170 """Build a setting with no phase delay: on at count 0, off at ``off``. 171 172 :param off: The count at which the output goes low, which sets the duty. 173 :returns: The four register bytes. 174 """ 175 return _pwm_duty(off) 176 177 def servo(self, pulse_micros: int, update_rate_hz: int = 50) -> bytes: 178 """Build the setting that drives a hobby servo to a pulse width. 179 180 :param pulse_micros: The high-pulse width in microseconds. Typical travel 181 is about 1000 to 2000 microseconds. 182 :param update_rate_hz: The PWM frequency the controller is set to. 183 :returns: The four register bytes. 184 """ 185 return _pwm_servo(pulse_micros, update_rate_hz) 186 187 def counts(self, data: bytes) -> Pwm: 188 """Read a setting back from the four register bytes a channel holds. 189 190 The inverse of the builders above, so a caller can read a channel off the bus 191 and see what it is set to rather than decoding the registers by hand. 192 193 :param data: The four channel registers. 194 :returns: The counts at which the output goes high and low, named. 195 :raises ValueError: If ``data`` is not four bytes. 196 """ 197 on, off = _pwm_counts(bytes(data)) 198 return Pwm(on, off) 199 200 def full_on(self) -> bytes: 201 """Return the setting that holds a channel continuously high. 202 203 :returns: The four register bytes. 204 """ 205 return _pwm_full_on() 206 207 def full_off(self) -> bytes: 208 """Return the setting that holds a channel continuously low. 209 210 This is the power-on state, and is not the same as a zero duty, which 211 still glitches high for one count. 212 213 :returns: The four register bytes. 214 """ 215 return _pwm_full_off() 216 217 218def steps_for_degrees(degrees: float, steps_per_revolution: int) -> int: 219 """Return how many steps a rotation of ``degrees`` takes on a given motor. 220 221 :param degrees: The angle to turn through. 222 :param steps_per_revolution: The motor's steps per full revolution. 223 :returns: The step count, negative for a negative angle. 224 """ 225 return _steps_for_degrees(degrees, steps_per_revolution) 226 227 228#: An NXP PCA9685 16-channel PWM controller. 229pca9685 = _Pca9685() 230 231#: The four register bytes for one PCA9685 channel. 232pwm = _Pwm()
50class Direction(str, enum.Enum): 51 """Which way to step a motor.""" 52 53 #: Advance the sequence, turning the shaft one way. 54 FORWARD = "Forward" 55 #: Reverse the sequence, turning the shaft the other way. 56 BACKWARD = "Backward"
Which way to step a motor.
34class Drive(str, enum.Enum): 35 """A stepper drive pattern, trading torque, smoothness, and resolution.""" 36 37 #: One coil energised at a time: four steps, least torque and least power. 38 WAVE = "Wave" 39 #: Two adjacent coils at a time: four steps, most torque. 40 FULL_STEP = "FullStep" 41 #: Alternating one and two coils: eight steps, double resolution. 42 HALF_STEP = "HalfStep" 43 44 @property 45 def step_count(self) -> int: 46 """How many steps make up one electrical cycle of this pattern.""" 47 return _step_count(self.value)
A stepper drive pattern, trading torque, smoothness, and resolution.
98class Pwm(NamedTuple): 99 """When in a period a PWM output goes high and low, in counts.""" 100 101 on: int 102 """The count at which the output goes high.""" 103 104 off: int 105 """The count at which it goes low, or the full-off flag."""
When in a period a PWM output goes high and low, in counts.
59class Stepper: 60 """A stepper motor's place in its drive sequence, and how far it has turned. 61 62 Example:: 63 64 motor = Stepper(Drive.HALF_STEP) 65 coils = motor.step(Direction.FORWARD) 66 """ 67 68 __slots__ = ("_native",) 69 70 def __init__(self, drive: Drive) -> None: 71 """Create a stepper at the start of a pattern, with its position at zero. 72 73 :param drive: The coil pattern to walk. 74 """ 75 self._native = _NativeStepper(Drive(drive).value) 76 77 def step(self, direction: Direction) -> int: 78 """Advance one step and return the four-bit coil pattern to apply. 79 80 The most significant of the four bits is the first coil. 81 82 :param direction: Which way to turn. 83 :returns: The coil pattern. 84 """ 85 return self._native.step(Direction(direction).value) 86 87 @property 88 def coils(self) -> int: 89 """The coil pattern currently held, without advancing.""" 90 return self._native.coils 91 92 @property 93 def steps(self) -> int: 94 """How many steps have been taken, signed by direction.""" 95 return self._native.steps
A stepper motor's place in its drive sequence, and how far it has turned.
Example::
motor = Stepper(Drive.HALF_STEP)
coils = motor.step(Direction.FORWARD)
70 def __init__(self, drive: Drive) -> None: 71 """Create a stepper at the start of a pattern, with its position at zero. 72 73 :param drive: The coil pattern to walk. 74 """ 75 self._native = _NativeStepper(Drive(drive).value)
Create a stepper at the start of a pattern, with its position at zero.
Parameters
- drive: The coil pattern to walk.
77 def step(self, direction: Direction) -> int: 78 """Advance one step and return the four-bit coil pattern to apply. 79 80 The most significant of the four bits is the first coil. 81 82 :param direction: Which way to turn. 83 :returns: The coil pattern. 84 """ 85 return self._native.step(Direction(direction).value)
Advance one step and return the four-bit coil pattern to apply.
The most significant of the four bits is the first coil.
Parameters
- direction: Which way to turn. :returns: The coil pattern.
219def steps_for_degrees(degrees: float, steps_per_revolution: int) -> int: 220 """Return how many steps a rotation of ``degrees`` takes on a given motor. 221 222 :param degrees: The angle to turn through. 223 :param steps_per_revolution: The motor's steps per full revolution. 224 :returns: The step count, negative for a negative angle. 225 """ 226 return _steps_for_degrees(degrees, steps_per_revolution)
Return how many steps a rotation of degrees takes on a given motor.
Parameters
- degrees: The angle to turn through.
- steps_per_revolution: The motor's steps per full revolution. :returns: The step count, negative for a negative angle.