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()
class Direction(builtins.str, enum.Enum):
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.

FORWARD = <Direction.FORWARD: 'Forward'>
BACKWARD = <Direction.BACKWARD: 'Backward'>
class Drive(builtins.str, enum.Enum):
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.

WAVE = <Drive.WAVE: 'Wave'>
FULL_STEP = <Drive.FULL_STEP: 'FullStep'>
HALF_STEP = <Drive.HALF_STEP: 'HalfStep'>
step_count: int
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)

How many steps make up one electrical cycle of this pattern.

class Pwm(typing.NamedTuple):
 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.

Pwm(on: int, off: int)

Create new instance of Pwm(on, off)

on: int

The count at which the output goes high.

off: int

The count at which it goes low, or the full-off flag.

class Stepper:
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)
Stepper(drive: Drive)
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.
def step(self, direction: Direction) -> int:
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.
coils: int
87    @property
88    def coils(self) -> int:
89        """The coil pattern currently held, without advancing."""
90        return self._native.coils

The coil pattern currently held, without advancing.

steps: int
92    @property
93    def steps(self) -> int:
94        """How many steps have been taken, signed by direction."""
95        return self._native.steps

How many steps have been taken, signed by direction.

pca9685 = <pamoja.actuators._Pca9685 object>
pwm = <pamoja.actuators._Pwm object>
def steps_for_degrees(degrees: float, steps_per_revolution: int) -> int:
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.