pamoja.actuators

Idiomatic actuator-driver facade.

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. Pca9685 drives the part over an ~pamoja.hal.I2cBus, and pca9685.sim.part stands one up on a simulated bus that keeps the datasheet's rules. The pwm builders make the four register bytes of one channel's setting. FourWire and StepDir drive a stepper through its pins, over any ~pamoja.gpio.OutputLine.

  1"""Idiomatic actuator-driver facade.
  2
  3Two parts that move something: a PCA9685 driving up to sixteen servos, LEDs, or valves,
  4and a stepper motor walked one coil pattern at a time. :class:`Pca9685` drives the part
  5over an :class:`~pamoja.hal.I2cBus`, and ``pca9685.sim.part`` stands one up on a simulated
  6bus that keeps the datasheet's rules. The ``pwm`` builders make the four register bytes of
  7one channel's setting. :class:`FourWire` and :class:`StepDir` drive a stepper through its
  8pins, over any :class:`~pamoja.gpio.OutputLine`.
  9"""
 10
 11from __future__ import annotations
 12
 13import enum
 14from typing import Generic, NamedTuple, Optional, Tuple, TypeVar
 15
 16from pamoja._native import Pca9685 as _NativePca9685
 17from pamoja._native import Stepper as _NativeStepper
 18from pamoja._native import pca9685_sim_part as _pca9685_sim_part
 19from pamoja._native import pca9685_channel_register as _channel_register
 20from pamoja._native import pca9685_frequency_for_prescale as _frequency_for_prescale
 21from pamoja._native import pca9685_limits as _limits
 22from pamoja._native import pca9685_prescale_for_frequency as _prescale_for_frequency
 23from pamoja._native import pwm_counts as _pwm_counts
 24from pamoja._native import pwm_duty as _pwm_duty
 25from pamoja._native import pwm_from_counts as _pwm_from_counts
 26from pamoja._native import pwm_full_off as _pwm_full_off
 27from pamoja._native import pwm_full_on as _pwm_full_on
 28from pamoja._native import pwm_servo as _pwm_servo
 29from pamoja._native import stepper_step_count as _step_count
 30from pamoja._native import stepper_steps_for_degrees as _steps_for_degrees
 31from pamoja._native import stepper_timing as _stepper_timing
 32from pamoja.gpio import Level, OutputLine
 33from pamoja.hal import Delay, I2cBus, I2cPart, SleepDelay
 34
 35__all__ = [
 36    "Direction",
 37    "Drive",
 38    "FourWire",
 39    "Pca9685",
 40    "Pwm",
 41    "StepDir",
 42    "Stepper",
 43    "pca9685",
 44    "pwm",
 45    "stepper",
 46    "steps_for_degrees",
 47]
 48
 49_INTERNAL_OSC_HZ, _CHANNELS, _COUNTS = _limits()
 50_DEFAULT_STEP_MICROS, _DEFAULT_PULSE_MICROS = _stepper_timing()
 51
 52_Out = TypeVar("_Out", bound=OutputLine)
 53_Step = TypeVar("_Step", bound=OutputLine)
 54_Dir = TypeVar("_Dir", bound=OutputLine)
 55
 56
 57class Drive(str, enum.Enum):
 58    """A stepper drive pattern, trading torque, smoothness, and resolution."""
 59
 60    #: One coil energized at a time: four steps, least torque and least power.
 61    WAVE = "Wave"
 62    #: Two adjacent coils at a time: four steps, most torque.
 63    FULL_STEP = "FullStep"
 64    #: Alternating one and two coils: eight steps, double resolution.
 65    HALF_STEP = "HalfStep"
 66
 67    @property
 68    def step_count(self) -> int:
 69        """How many steps make up one electrical cycle of this pattern."""
 70        return _step_count(self.value)
 71
 72
 73class Direction(str, enum.Enum):
 74    """Which way to step a motor."""
 75
 76    #: Advance the sequence, turning the shaft one way.
 77    FORWARD = "Forward"
 78    #: Reverse the sequence, turning the shaft the other way.
 79    BACKWARD = "Backward"
 80
 81
 82class Stepper:
 83    """A stepper motor's place in its drive sequence, and how far it has turned.
 84
 85    Example::
 86
 87        motor = Stepper(Drive.HALF_STEP)
 88        coils = motor.step(Direction.FORWARD)
 89    """
 90
 91    __slots__ = ("_native",)
 92
 93    def __init__(self, drive: Drive) -> None:
 94        """Create a stepper at the start of a pattern, with its position at zero.
 95
 96        :param drive: The coil pattern to walk.
 97        """
 98        self._native = _NativeStepper(Drive(drive).value)
 99
100    def step(self, direction: Direction) -> int:
101        """Advance one step and return the four-bit coil pattern to apply.
102
103        The most significant of the four bits is the first coil.
104
105        :param direction: Which way to turn.
106        :returns: The coil pattern.
107        """
108        return self._native.step(Direction(direction).value)
109
110    @property
111    def coils(self) -> int:
112        """The coil pattern currently held, without advancing."""
113        return self._native.coils
114
115    @property
116    def steps(self) -> int:
117        """How many steps have been taken, signed by direction."""
118        return self._native.steps
119
120
121def _direction_of(count: int) -> Direction:
122    return Direction.BACKWARD if count < 0 else Direction.FORWARD
123
124
125class FourWire(Generic[_Out]):
126    """A four-wire stepper driven coil by coil, through a transistor array such as a
127    ULN2003 or an H-bridge, as ``pamoja_actuators::stepper::FourWire`` drives one in Rust.
128
129    Each step energizes the coils the drive pattern names, coil A for the pattern's high
130    bit down to coil D for its low bit, then waits the step interval. The position counts
131    steps from where the motor was when the driver was built, forward positive. A coil
132    line is any :class:`~pamoja.gpio.OutputLine`: a :class:`~pamoja.gpio.GpioLine` on a
133    board, or a :class:`~pamoja.gpio.PinScript` that records every level.
134
135    >>> from pamoja.gpio import PinScript
136    >>> from pamoja.hal import DelayLog
137    >>> coils = (PinScript(), PinScript(), PinScript(), PinScript())
138    >>> motor = FourWire(coils, Drive.FULL_STEP, delay=DelayLog())
139    >>> motor.steps(2)
140    >>> motor.position
141    2
142    """
143
144    __slots__ = ("_coils", "_delay", "_drive", "_position", "_sequence", "_step_micros")
145
146    def __init__(
147        self,
148        coils: Tuple[_Out, _Out, _Out, _Out],
149        drive: Drive,
150        *,
151        step_micros: int = _DEFAULT_STEP_MICROS,
152        delay: Optional[Delay] = None,
153    ) -> None:
154        """Wrap the four coil lines, without driving them.
155
156        :param coils: The lines for coils A, B, C, and D, in the motor's phase order.
157        :param drive: The coil pattern to step through.
158        :param step_micros: The pause after each step, which sets the speed.
159        :param delay: What waits: a :class:`~pamoja.hal.SleepDelay` unless given, or a
160            :class:`~pamoja.hal.DelayLog` to run with nothing plugged in.
161        :raises ValueError: If there are not four coil lines.
162        """
163        lines = tuple(coils)
164        if len(lines) != 4:
165            raise ValueError(f"a four-wire stepper has four coil lines, not {len(lines)}")
166        a, b, c, d = lines
167        self._coils = (a, b, c, d)
168        self._drive = Drive(drive)
169        self._sequence = _NativeStepper(self._drive.value)
170        self._position = 0
171        self._step_micros = step_micros
172        self._delay: Delay = SleepDelay() if delay is None else delay
173
174    @property
175    def position(self) -> int:
176        """The step count since the driver was built, forward positive."""
177        return self._position
178
179    @property
180    def drive(self) -> Drive:
181        """The coil pattern in use."""
182        return self._drive
183
184    @property
185    def step_micros(self) -> int:
186        """The pause after each step, in microseconds."""
187        return self._step_micros
188
189    def step(self, direction: Direction) -> None:
190        """Take one step and wait the step interval.
191
192        :param direction: Which way to step.
193        :raises Exception: Whatever a coil line raises when it cannot be driven.
194        """
195        direction = Direction(direction)
196        self._energize(self._sequence.step(direction.value))
197        self._position += 1 if direction is Direction.FORWARD else -1
198        self._delay.delay_micros(self._step_micros)
199
200    def steps(self, count: int) -> None:
201        """Take ``count`` steps, backward when negative.
202
203        :param count: The signed number of steps.
204        :raises Exception: Whatever a coil line raises; the steps already taken stay
205            counted.
206        """
207        direction = _direction_of(count)
208        for _ in range(abs(count)):
209            self.step(direction)
210
211    def idle(self) -> None:
212        """Drop every coil, so the motor holds nothing and draws nothing.
213
214        :raises Exception: Whatever a coil line raises when it cannot be driven.
215        """
216        self._energize(0)
217
218    def release(self) -> Tuple[_Out, _Out, _Out, _Out]:
219        """Hand the coil lines back, for a test to read what was driven or a program to
220        reuse them.
221
222        :returns: The lines for coils A, B, C, and D.
223        """
224        return self._coils
225
226    def _energize(self, coils: int) -> None:
227        for index, line in enumerate(self._coils):
228            line.drive(Level.HIGH if coils & (0b1000 >> index) else Level.LOW)
229
230
231class StepDir(Generic[_Step, _Dir]):
232    """A stepper behind a step and direction driver chip such as an A4988 or a DRV8825,
233    as ``pamoja_actuators::stepper::StepDir`` drives one in Rust.
234
235    Each step sets the direction line, high for forward, and holds it for the pulse
236    width, since the chip reads the direction on the step line's rising edge and needs it
237    settled first. Then it pulses the step line high for the pulse width, brings it low,
238    and waits the step interval. Microstepping, current limiting, and enable are the
239    chip's own pins and settings, outside this driver.
240
241    >>> from pamoja.gpio import PinScript
242    >>> from pamoja.hal import DelayLog
243    >>> motor = StepDir(PinScript(), PinScript(), delay=DelayLog())
244    >>> motor.steps(-1)
245    >>> motor.position
246    -1
247    """
248
249    __slots__ = (
250        "_delay",
251        "_direction",
252        "_position",
253        "_pulse_micros",
254        "_step",
255        "_step_micros",
256    )
257
258    def __init__(
259        self,
260        step: _Step,
261        direction: _Dir,
262        *,
263        pulse_micros: int = _DEFAULT_PULSE_MICROS,
264        step_micros: int = _DEFAULT_STEP_MICROS,
265        delay: Optional[Delay] = None,
266    ) -> None:
267        """Wrap the step and direction lines, without driving them.
268
269        :param step: The line the chip counts rising edges on.
270        :param direction: The line the chip reads the direction from; high is forward.
271        :param pulse_micros: How long the direction line is held before the step line
272            rises, and the step line is then held high.
273        :param step_micros: The pause after each step, which sets the speed.
274        :param delay: What waits: a :class:`~pamoja.hal.SleepDelay` unless given, or a
275            :class:`~pamoja.hal.DelayLog` to run with nothing plugged in.
276        """
277        self._step = step
278        self._direction = direction
279        self._position = 0
280        self._pulse_micros = pulse_micros
281        self._step_micros = step_micros
282        self._delay: Delay = SleepDelay() if delay is None else delay
283
284    @property
285    def position(self) -> int:
286        """The step count since the driver was built, forward positive."""
287        return self._position
288
289    @property
290    def pulse_micros(self) -> int:
291        """How long the direction is held before each step pulse, and the pulse itself, in
292        microseconds."""
293        return self._pulse_micros
294
295    @property
296    def step_micros(self) -> int:
297        """The pause after each step, in microseconds."""
298        return self._step_micros
299
300    def step(self, direction: Direction) -> None:
301        """Take one step and wait the step interval.
302
303        :param direction: Which way to step.
304        :raises Exception: Whatever a line raises when it cannot be driven.
305        """
306        forward = Direction(direction) is Direction.FORWARD
307        self._direction.drive(Level.HIGH if forward else Level.LOW)
308        self._delay.delay_micros(self._pulse_micros)
309        self._step.drive(Level.HIGH)
310        self._delay.delay_micros(self._pulse_micros)
311        self._step.drive(Level.LOW)
312        self._position += 1 if forward else -1
313        self._delay.delay_micros(self._step_micros)
314
315    def steps(self, count: int) -> None:
316        """Take ``count`` steps, backward when negative.
317
318        :param count: The signed number of steps.
319        :raises Exception: Whatever a line raises; the steps already taken stay counted.
320        """
321        direction = _direction_of(count)
322        for _ in range(abs(count)):
323            self.step(direction)
324
325    def release(self) -> Tuple[_Step, _Dir]:
326        """Hand the lines back.
327
328        :returns: The step line, then the direction line.
329        """
330        return self._step, self._direction
331
332
333class Pwm(NamedTuple):
334    """When in a period a PWM output goes high and low, in counts."""
335
336    on: int
337    """The count at which the output goes high."""
338
339    off: int
340    """The count at which it goes low, or the full-off flag."""
341
342
343class Pca9685:
344    """An NXP PCA9685 on an I2C bus, driving sixteen PWM outputs.
345
346    The driver programs the prescale for its frequency with the oscillator asleep, because
347    the part only takes a prescale then, wakes it, waits the 500 us it needs, and loads a
348    channel's four registers in one transfer. Every call releases the interpreter while the
349    bus is busy.
350
351    Example::
352
353        board = Pca9685(bus, pca9685.DEFAULT_ADDRESS, frequency_hz=50)
354        board.set_channel(0, pwm.servo(1500))
355    """
356
357    __slots__ = ("_native",)
358
359    def __init__(
360        self,
361        bus: I2cBus,
362        address: int = 0x40,
363        *,
364        frequency_hz: int = 200,
365        oscillator_hz: int = _INTERNAL_OSC_HZ,
366        totem_pole: bool = True,
367        inverted: bool = False,
368        change_on_ack: bool = False,
369    ) -> None:
370        """Make a driver for the part at ``address`` on ``bus``. Nothing is sent until
371        :meth:`init` or the first channel is loaded.
372
373        :param bus: The bus the part is on.
374        :param address: The address its A5 to A0 pins select, ``0x40`` with all six low.
375        :param frequency_hz: The PWM frequency every channel shares; 50 for hobby servos.
376        :param oscillator_hz: The clock the prescaler divides, for a board driving EXTCLK.
377        :param totem_pole: Totem-pole outputs when true, open-drain when false.
378        :param inverted: Invert the output logic, for a board with no external driver.
379        :param change_on_ack: Change the outputs on each write's acknowledge, not the stop.
380        """
381        self._native = _NativePca9685(
382            bus._native,
383            address,
384            frequency_hz,
385            oscillator_hz,
386            totem_pole,
387            inverted,
388            change_on_ack,
389        )
390
391    def init(self) -> None:
392        """Program the prescale and the output wiring, wake the oscillator, and restart.
393
394        :raises PamojaError: If the bus fails.
395        """
396        self._native.init()
397
398    def set_channel(self, channel: int, setting: bytes) -> None:
399        """Load one channel, initializing the part first if :meth:`init` has not run.
400
401        :param channel: The output, 0 to 15.
402        :param setting: The four register bytes the ``pwm`` builders make.
403        :raises ValueError: For a channel past 15, or a setting that is not four bytes.
404        :raises PamojaError: If the bus fails.
405        """
406        self._native.set_channel(channel, bytes(setting))
407
408    def channel(self, channel: int) -> bytes:
409        """Read one channel's four register bytes back from the part.
410
411        The registers are read one at a time, so the read works whether or not MODE1's
412        auto-increment bit is set, and it changes nothing on the part: a program that
413        restarts can ask a running part what it holds. ``pwm.counts`` names the counts.
414
415        :param channel: The output, 0 to 15.
416        :returns: The four register bytes the channel holds.
417        :raises ValueError: For a channel past 15.
418        :raises PamojaError: If the bus fails.
419        """
420        return bytes(self._native.channel(channel))
421
422    def set_all(self, setting: bytes) -> None:
423        """Load every channel with the same four bytes in one transfer.
424
425        :param setting: The four register bytes the ``pwm`` builders make.
426        :raises PamojaError: If the bus fails.
427        """
428        self._native.set_all(bytes(setting))
429
430    def sleep(self) -> None:
431        """Stop the oscillator; the channels keep their settings.
432
433        :raises PamojaError: If the bus fails.
434        """
435        self._native.sleep()
436
437    def wake(self) -> None:
438        """Wake the oscillator, wait the 500 us it needs, and restart the channels.
439
440        :raises PamojaError: If the bus fails.
441        """
442        self._native.wake()
443
444    def software_reset(self) -> None:
445        """Send the general-call software reset to every PCA9685 on the bus.
446
447        It goes to address ``0x00``, which nothing on a simulated bus answers.
448
449        :raises PamojaError: If the bus fails, or nothing acknowledges the general call.
450        """
451        self._native.software_reset()
452
453    @property
454    def prescale(self) -> int:
455        """The prescale value the driver writes for its frequency."""
456        return self._native.prescale
457
458    @property
459    def frequency(self) -> float:
460        """The frequency the part runs at once the prescaler has rounded, in hertz."""
461        return self._native.frequency
462
463
464class _Pca9685Sim:
465    """A PCA9685 that is not there, keeping its datasheet's rules: PRE_SCALE takes a write
466    only while the part sleeps, the register pointer moves on only with auto-increment set,
467    and one write to the ALL_LED registers loads every channel."""
468
469    __slots__ = ()
470
471    def part(self, address: int = 0x40) -> I2cPart:
472        """A simulated PCA9685 as it powers up: asleep at 200 Hz with every output off.
473
474        :param address: The address it answers to.
475        :returns: A part to put on a simulated bus.
476        """
477        return I2cPart._wrap(_pca9685_sim_part(address))
478
479
480class _Pca9685:
481    """An NXP PCA9685 16-channel PWM controller, for servos, LEDs, and valves."""
482
483    __slots__ = ()
484
485    #: The address it answers at with its six address pins low.
486    DEFAULT_ADDRESS = 0x40
487    #: The part's internal oscillator frequency, in hertz.
488    INTERNAL_OSC_HZ = _INTERNAL_OSC_HZ
489    #: How many channels it drives.
490    CHANNELS = _CHANNELS
491    #: How many counts each period is divided into.
492    COUNTS = _COUNTS
493    #: How long the oscillator takes to run once woken, in microseconds.
494    OSCILLATOR_STARTUP_MICROS = 500
495    #: Mode register 1: restart, clock, auto-increment, sleep, and the addresses answered.
496    REGISTER_MODE1 = 0x00
497    #: Mode register 2: how the outputs are wired and when they change.
498    REGISTER_MODE2 = 0x01
499    #: The first of channel 0's four registers; :meth:`channel_register` gives the rest.
500    REGISTER_LED0_ON_L = 0x06
501    #: The first of the four registers that load every channel at once.
502    REGISTER_ALL_LED_ON_L = 0xFA
503    #: The prescaler, writable only while the part sleeps.
504    REGISTER_PRE_SCALE = 0xFE
505    #: MODE1's RESTART bit: set when the part slept with a channel running.
506    MODE1_RESTART = 0x80
507    #: MODE1's EXTCLK bit: the prescaler divides the EXTCLK pin.
508    MODE1_EXTCLK = 0x40
509    #: MODE1's auto-increment bit: the register pointer moves on after each byte.
510    MODE1_AUTO_INCREMENT = 0x20
511    #: MODE1's SLEEP bit: the oscillator is off and PRE_SCALE takes a write.
512    MODE1_SLEEP = 0x10
513    #: MODE1 at power-up: asleep, answering the All Call address.
514    MODE1_RESET = 0x11
515    #: MODE2 at power-up: totem-pole outputs.
516    MODE2_RESET = 0x04
517    #: PRE_SCALE at power-up: 200 Hz on the internal oscillator.
518    PRE_SCALE_RESET = 0x1E
519    #: The smallest value the part loads into PRE_SCALE.
520    PRE_SCALE_MIN = 3
521    #: A PCA9685 that is not there.
522    sim = _Pca9685Sim()
523
524    def channel_register(self, channel: int) -> int:
525        """Return the first of a channel's four consecutive registers.
526
527        :param channel: The channel, 0 to 15.
528        :returns: The register address.
529        :raises ValueError: If the channel is beyond the part.
530        """
531        return _channel_register(channel)
532
533    def prescale_for_frequency(
534        self, update_rate_hz: int, osc_hz: int = _INTERNAL_OSC_HZ
535    ) -> int:
536        """Return the prescale value that sets an update rate.
537
538        :param update_rate_hz: The PWM frequency wanted.
539        :param osc_hz: The oscillator frequency, usually the internal one.
540        :returns: The prescale register value.
541        """
542        return _prescale_for_frequency(update_rate_hz, osc_hz)
543
544    def frequency_for_prescale(
545        self, prescale: int, osc_hz: int = _INTERNAL_OSC_HZ
546    ) -> float:
547        """Return the update rate a prescale value produces.
548
549        :param prescale: The prescale register value.
550        :param osc_hz: The oscillator frequency, usually the internal one.
551        :returns: The frequency in hertz.
552        """
553        return _frequency_for_prescale(prescale, osc_hz)
554
555
556class _Pwm:
557    """The four register bytes for one PCA9685 channel.
558
559    Each call returns them in the channel's own register order, so they can be
560    written in a single bus transaction.
561    """
562
563    __slots__ = ()
564
565    def from_counts(self, on: int, off: int) -> bytes:
566        """Build a setting from explicit on and off counts.
567
568        :param on: The count at which the output goes high.
569        :param off: The count at which it goes low.
570        :returns: The four register bytes; counts are masked to 12 bits.
571        """
572        return _pwm_from_counts(on, off)
573
574    def duty(self, off: int) -> bytes:
575        """Build a setting with no phase delay: on at count 0, off at ``off``.
576
577        The datasheet rules out the same count in on and off, so 0 is the full-off
578        setting and 4096 or more the full-on one.
579
580        :param off: The count at which the output goes low, which sets the duty.
581        :returns: The four register bytes.
582        """
583        return _pwm_duty(off)
584
585    def servo(self, pulse_micros: int, update_rate_hz: int = 50) -> bytes:
586        """Build the setting that drives a hobby servo to a pulse width.
587
588        :param pulse_micros: The high-pulse width in microseconds. Typical travel
589            is about 1000 to 2000 microseconds.
590        :param update_rate_hz: The PWM frequency the controller is set to.
591        :returns: The four register bytes.
592        """
593        return _pwm_servo(pulse_micros, update_rate_hz)
594
595    def counts(self, data: bytes) -> Pwm:
596        """Read a setting back from the four register bytes a channel holds.
597
598        The inverse of the builders above, so a caller can read a channel off the bus
599        and see what it is set to rather than decoding the registers by hand.
600
601        :param data: The four channel registers.
602        :returns: The counts at which the output goes high and low, named.
603        :raises ValueError: If ``data`` is not four bytes.
604        """
605        on, off = _pwm_counts(bytes(data))
606        return Pwm(on, off)
607
608    def full_on(self) -> bytes:
609        """Return the setting that holds a channel continuously high.
610
611        :returns: The four register bytes.
612        """
613        return _pwm_full_on()
614
615    def full_off(self) -> bytes:
616        """Return the setting that holds a channel continuously low.
617
618        This is the power-on state, and its flag takes precedence over the full-on flag
619        when both are set.
620
621        :returns: The four register bytes.
622        """
623        return _pwm_full_off()
624
625
626def steps_for_degrees(degrees: float, steps_per_revolution: int) -> int:
627    """Return how many steps a rotation of ``degrees`` takes on a given motor.
628
629    :param degrees: The angle to turn through.
630    :param steps_per_revolution: The motor's steps per full revolution.
631    :returns: The step count, negative for a negative angle.
632    """
633    return _steps_for_degrees(degrees, steps_per_revolution)
634
635
636class _StepperTiming:
637    """Stepper motor timing, as ``pamoja_actuators::stepper`` starts a driver with."""
638
639    __slots__ = ()
640
641    #: The pause a driver takes after each step unless given another, in microseconds.
642    DEFAULT_STEP_MICROS = _DEFAULT_STEP_MICROS
643    #: How long a step and direction driver holds the direction before a step pulse, and
644    #: the pulse itself, in microseconds.
645    DEFAULT_PULSE_MICROS = _DEFAULT_PULSE_MICROS
646
647
648#: Stepper motor timing.
649stepper = _StepperTiming()
650
651#: An NXP PCA9685 16-channel PWM controller.
652pca9685 = _Pca9685()
653
654#: The four register bytes for one PCA9685 channel.
655pwm = _Pwm()
class Direction(builtins.str, enum.Enum):
74class Direction(str, enum.Enum):
75    """Which way to step a motor."""
76
77    #: Advance the sequence, turning the shaft one way.
78    FORWARD = "Forward"
79    #: Reverse the sequence, turning the shaft the other way.
80    BACKWARD = "Backward"

Which way to step a motor.

FORWARD = <Direction.FORWARD: 'Forward'>
BACKWARD = <Direction.BACKWARD: 'Backward'>
class Drive(builtins.str, enum.Enum):
58class Drive(str, enum.Enum):
59    """A stepper drive pattern, trading torque, smoothness, and resolution."""
60
61    #: One coil energized at a time: four steps, least torque and least power.
62    WAVE = "Wave"
63    #: Two adjacent coils at a time: four steps, most torque.
64    FULL_STEP = "FullStep"
65    #: Alternating one and two coils: eight steps, double resolution.
66    HALF_STEP = "HalfStep"
67
68    @property
69    def step_count(self) -> int:
70        """How many steps make up one electrical cycle of this pattern."""
71        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
68    @property
69    def step_count(self) -> int:
70        """How many steps make up one electrical cycle of this pattern."""
71        return _step_count(self.value)

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

class FourWire(typing.Generic[~_Out]):
126class FourWire(Generic[_Out]):
127    """A four-wire stepper driven coil by coil, through a transistor array such as a
128    ULN2003 or an H-bridge, as ``pamoja_actuators::stepper::FourWire`` drives one in Rust.
129
130    Each step energizes the coils the drive pattern names, coil A for the pattern's high
131    bit down to coil D for its low bit, then waits the step interval. The position counts
132    steps from where the motor was when the driver was built, forward positive. A coil
133    line is any :class:`~pamoja.gpio.OutputLine`: a :class:`~pamoja.gpio.GpioLine` on a
134    board, or a :class:`~pamoja.gpio.PinScript` that records every level.
135
136    >>> from pamoja.gpio import PinScript
137    >>> from pamoja.hal import DelayLog
138    >>> coils = (PinScript(), PinScript(), PinScript(), PinScript())
139    >>> motor = FourWire(coils, Drive.FULL_STEP, delay=DelayLog())
140    >>> motor.steps(2)
141    >>> motor.position
142    2
143    """
144
145    __slots__ = ("_coils", "_delay", "_drive", "_position", "_sequence", "_step_micros")
146
147    def __init__(
148        self,
149        coils: Tuple[_Out, _Out, _Out, _Out],
150        drive: Drive,
151        *,
152        step_micros: int = _DEFAULT_STEP_MICROS,
153        delay: Optional[Delay] = None,
154    ) -> None:
155        """Wrap the four coil lines, without driving them.
156
157        :param coils: The lines for coils A, B, C, and D, in the motor's phase order.
158        :param drive: The coil pattern to step through.
159        :param step_micros: The pause after each step, which sets the speed.
160        :param delay: What waits: a :class:`~pamoja.hal.SleepDelay` unless given, or a
161            :class:`~pamoja.hal.DelayLog` to run with nothing plugged in.
162        :raises ValueError: If there are not four coil lines.
163        """
164        lines = tuple(coils)
165        if len(lines) != 4:
166            raise ValueError(f"a four-wire stepper has four coil lines, not {len(lines)}")
167        a, b, c, d = lines
168        self._coils = (a, b, c, d)
169        self._drive = Drive(drive)
170        self._sequence = _NativeStepper(self._drive.value)
171        self._position = 0
172        self._step_micros = step_micros
173        self._delay: Delay = SleepDelay() if delay is None else delay
174
175    @property
176    def position(self) -> int:
177        """The step count since the driver was built, forward positive."""
178        return self._position
179
180    @property
181    def drive(self) -> Drive:
182        """The coil pattern in use."""
183        return self._drive
184
185    @property
186    def step_micros(self) -> int:
187        """The pause after each step, in microseconds."""
188        return self._step_micros
189
190    def step(self, direction: Direction) -> None:
191        """Take one step and wait the step interval.
192
193        :param direction: Which way to step.
194        :raises Exception: Whatever a coil line raises when it cannot be driven.
195        """
196        direction = Direction(direction)
197        self._energize(self._sequence.step(direction.value))
198        self._position += 1 if direction is Direction.FORWARD else -1
199        self._delay.delay_micros(self._step_micros)
200
201    def steps(self, count: int) -> None:
202        """Take ``count`` steps, backward when negative.
203
204        :param count: The signed number of steps.
205        :raises Exception: Whatever a coil line raises; the steps already taken stay
206            counted.
207        """
208        direction = _direction_of(count)
209        for _ in range(abs(count)):
210            self.step(direction)
211
212    def idle(self) -> None:
213        """Drop every coil, so the motor holds nothing and draws nothing.
214
215        :raises Exception: Whatever a coil line raises when it cannot be driven.
216        """
217        self._energize(0)
218
219    def release(self) -> Tuple[_Out, _Out, _Out, _Out]:
220        """Hand the coil lines back, for a test to read what was driven or a program to
221        reuse them.
222
223        :returns: The lines for coils A, B, C, and D.
224        """
225        return self._coils
226
227    def _energize(self, coils: int) -> None:
228        for index, line in enumerate(self._coils):
229            line.drive(Level.HIGH if coils & (0b1000 >> index) else Level.LOW)

A four-wire stepper driven coil by coil, through a transistor array such as a ULN2003 or an H-bridge, as pamoja_actuators::stepper::FourWire drives one in Rust.

Each step energizes the coils the drive pattern names, coil A for the pattern's high bit down to coil D for its low bit, then waits the step interval. The position counts steps from where the motor was when the driver was built, forward positive. A coil line is any ~pamoja.gpio.OutputLine: a ~pamoja.gpio.GpioLine on a board, or a ~pamoja.gpio.PinScript that records every level.

>>> from pamoja.gpio import PinScript
>>> from pamoja.hal import DelayLog
>>> coils = (PinScript(), PinScript(), PinScript(), PinScript())
>>> motor = FourWire(coils, Drive.FULL_STEP, delay=DelayLog())
>>> motor.steps(2)
>>> motor.position
2
FourWire( coils: Tuple[~_Out, ~_Out, ~_Out, ~_Out], drive: Drive, *, step_micros: int = 2000, delay: Optional[pamoja.hal.Delay] = None)
147    def __init__(
148        self,
149        coils: Tuple[_Out, _Out, _Out, _Out],
150        drive: Drive,
151        *,
152        step_micros: int = _DEFAULT_STEP_MICROS,
153        delay: Optional[Delay] = None,
154    ) -> None:
155        """Wrap the four coil lines, without driving them.
156
157        :param coils: The lines for coils A, B, C, and D, in the motor's phase order.
158        :param drive: The coil pattern to step through.
159        :param step_micros: The pause after each step, which sets the speed.
160        :param delay: What waits: a :class:`~pamoja.hal.SleepDelay` unless given, or a
161            :class:`~pamoja.hal.DelayLog` to run with nothing plugged in.
162        :raises ValueError: If there are not four coil lines.
163        """
164        lines = tuple(coils)
165        if len(lines) != 4:
166            raise ValueError(f"a four-wire stepper has four coil lines, not {len(lines)}")
167        a, b, c, d = lines
168        self._coils = (a, b, c, d)
169        self._drive = Drive(drive)
170        self._sequence = _NativeStepper(self._drive.value)
171        self._position = 0
172        self._step_micros = step_micros
173        self._delay: Delay = SleepDelay() if delay is None else delay

Wrap the four coil lines, without driving them.

Parameters
  • coils: The lines for coils A, B, C, and D, in the motor's phase order.
  • drive: The coil pattern to step through.
  • step_micros: The pause after each step, which sets the speed.
  • delay: What waits: a ~pamoja.hal.SleepDelay unless given, or a ~pamoja.hal.DelayLog to run with nothing plugged in.
Raises
  • ValueError: If there are not four coil lines.
position: int
175    @property
176    def position(self) -> int:
177        """The step count since the driver was built, forward positive."""
178        return self._position

The step count since the driver was built, forward positive.

drive: Drive
180    @property
181    def drive(self) -> Drive:
182        """The coil pattern in use."""
183        return self._drive

The coil pattern in use.

step_micros: int
185    @property
186    def step_micros(self) -> int:
187        """The pause after each step, in microseconds."""
188        return self._step_micros

The pause after each step, in microseconds.

def step(self, direction: Direction) -> None:
190    def step(self, direction: Direction) -> None:
191        """Take one step and wait the step interval.
192
193        :param direction: Which way to step.
194        :raises Exception: Whatever a coil line raises when it cannot be driven.
195        """
196        direction = Direction(direction)
197        self._energize(self._sequence.step(direction.value))
198        self._position += 1 if direction is Direction.FORWARD else -1
199        self._delay.delay_micros(self._step_micros)

Take one step and wait the step interval.

Parameters
  • direction: Which way to step.
Raises
  • Exception: Whatever a coil line raises when it cannot be driven.
def steps(self, count: int) -> None:
201    def steps(self, count: int) -> None:
202        """Take ``count`` steps, backward when negative.
203
204        :param count: The signed number of steps.
205        :raises Exception: Whatever a coil line raises; the steps already taken stay
206            counted.
207        """
208        direction = _direction_of(count)
209        for _ in range(abs(count)):
210            self.step(direction)

Take count steps, backward when negative.

Parameters
  • count: The signed number of steps.
Raises
  • Exception: Whatever a coil line raises; the steps already taken stay counted.
def idle(self) -> None:
212    def idle(self) -> None:
213        """Drop every coil, so the motor holds nothing and draws nothing.
214
215        :raises Exception: Whatever a coil line raises when it cannot be driven.
216        """
217        self._energize(0)

Drop every coil, so the motor holds nothing and draws nothing.

Raises
  • Exception: Whatever a coil line raises when it cannot be driven.
def release(self) -> Tuple[~_Out, ~_Out, ~_Out, ~_Out]:
219    def release(self) -> Tuple[_Out, _Out, _Out, _Out]:
220        """Hand the coil lines back, for a test to read what was driven or a program to
221        reuse them.
222
223        :returns: The lines for coils A, B, C, and D.
224        """
225        return self._coils

Hand the coil lines back, for a test to read what was driven or a program to reuse them.

:returns: The lines for coils A, B, C, and D.

class Pca9685:
344class Pca9685:
345    """An NXP PCA9685 on an I2C bus, driving sixteen PWM outputs.
346
347    The driver programs the prescale for its frequency with the oscillator asleep, because
348    the part only takes a prescale then, wakes it, waits the 500 us it needs, and loads a
349    channel's four registers in one transfer. Every call releases the interpreter while the
350    bus is busy.
351
352    Example::
353
354        board = Pca9685(bus, pca9685.DEFAULT_ADDRESS, frequency_hz=50)
355        board.set_channel(0, pwm.servo(1500))
356    """
357
358    __slots__ = ("_native",)
359
360    def __init__(
361        self,
362        bus: I2cBus,
363        address: int = 0x40,
364        *,
365        frequency_hz: int = 200,
366        oscillator_hz: int = _INTERNAL_OSC_HZ,
367        totem_pole: bool = True,
368        inverted: bool = False,
369        change_on_ack: bool = False,
370    ) -> None:
371        """Make a driver for the part at ``address`` on ``bus``. Nothing is sent until
372        :meth:`init` or the first channel is loaded.
373
374        :param bus: The bus the part is on.
375        :param address: The address its A5 to A0 pins select, ``0x40`` with all six low.
376        :param frequency_hz: The PWM frequency every channel shares; 50 for hobby servos.
377        :param oscillator_hz: The clock the prescaler divides, for a board driving EXTCLK.
378        :param totem_pole: Totem-pole outputs when true, open-drain when false.
379        :param inverted: Invert the output logic, for a board with no external driver.
380        :param change_on_ack: Change the outputs on each write's acknowledge, not the stop.
381        """
382        self._native = _NativePca9685(
383            bus._native,
384            address,
385            frequency_hz,
386            oscillator_hz,
387            totem_pole,
388            inverted,
389            change_on_ack,
390        )
391
392    def init(self) -> None:
393        """Program the prescale and the output wiring, wake the oscillator, and restart.
394
395        :raises PamojaError: If the bus fails.
396        """
397        self._native.init()
398
399    def set_channel(self, channel: int, setting: bytes) -> None:
400        """Load one channel, initializing the part first if :meth:`init` has not run.
401
402        :param channel: The output, 0 to 15.
403        :param setting: The four register bytes the ``pwm`` builders make.
404        :raises ValueError: For a channel past 15, or a setting that is not four bytes.
405        :raises PamojaError: If the bus fails.
406        """
407        self._native.set_channel(channel, bytes(setting))
408
409    def channel(self, channel: int) -> bytes:
410        """Read one channel's four register bytes back from the part.
411
412        The registers are read one at a time, so the read works whether or not MODE1's
413        auto-increment bit is set, and it changes nothing on the part: a program that
414        restarts can ask a running part what it holds. ``pwm.counts`` names the counts.
415
416        :param channel: The output, 0 to 15.
417        :returns: The four register bytes the channel holds.
418        :raises ValueError: For a channel past 15.
419        :raises PamojaError: If the bus fails.
420        """
421        return bytes(self._native.channel(channel))
422
423    def set_all(self, setting: bytes) -> None:
424        """Load every channel with the same four bytes in one transfer.
425
426        :param setting: The four register bytes the ``pwm`` builders make.
427        :raises PamojaError: If the bus fails.
428        """
429        self._native.set_all(bytes(setting))
430
431    def sleep(self) -> None:
432        """Stop the oscillator; the channels keep their settings.
433
434        :raises PamojaError: If the bus fails.
435        """
436        self._native.sleep()
437
438    def wake(self) -> None:
439        """Wake the oscillator, wait the 500 us it needs, and restart the channels.
440
441        :raises PamojaError: If the bus fails.
442        """
443        self._native.wake()
444
445    def software_reset(self) -> None:
446        """Send the general-call software reset to every PCA9685 on the bus.
447
448        It goes to address ``0x00``, which nothing on a simulated bus answers.
449
450        :raises PamojaError: If the bus fails, or nothing acknowledges the general call.
451        """
452        self._native.software_reset()
453
454    @property
455    def prescale(self) -> int:
456        """The prescale value the driver writes for its frequency."""
457        return self._native.prescale
458
459    @property
460    def frequency(self) -> float:
461        """The frequency the part runs at once the prescaler has rounded, in hertz."""
462        return self._native.frequency

An NXP PCA9685 on an I2C bus, driving sixteen PWM outputs.

The driver programs the prescale for its frequency with the oscillator asleep, because the part only takes a prescale then, wakes it, waits the 500 us it needs, and loads a channel's four registers in one transfer. Every call releases the interpreter while the bus is busy.

Example::

board = Pca9685(bus, pca9685.DEFAULT_ADDRESS, frequency_hz=50)
board.set_channel(0, pwm.servo(1500))
Pca9685( bus: pamoja.hal.I2cBus, address: int = 64, *, frequency_hz: int = 200, oscillator_hz: int = 25000000, totem_pole: bool = True, inverted: bool = False, change_on_ack: bool = False)
360    def __init__(
361        self,
362        bus: I2cBus,
363        address: int = 0x40,
364        *,
365        frequency_hz: int = 200,
366        oscillator_hz: int = _INTERNAL_OSC_HZ,
367        totem_pole: bool = True,
368        inverted: bool = False,
369        change_on_ack: bool = False,
370    ) -> None:
371        """Make a driver for the part at ``address`` on ``bus``. Nothing is sent until
372        :meth:`init` or the first channel is loaded.
373
374        :param bus: The bus the part is on.
375        :param address: The address its A5 to A0 pins select, ``0x40`` with all six low.
376        :param frequency_hz: The PWM frequency every channel shares; 50 for hobby servos.
377        :param oscillator_hz: The clock the prescaler divides, for a board driving EXTCLK.
378        :param totem_pole: Totem-pole outputs when true, open-drain when false.
379        :param inverted: Invert the output logic, for a board with no external driver.
380        :param change_on_ack: Change the outputs on each write's acknowledge, not the stop.
381        """
382        self._native = _NativePca9685(
383            bus._native,
384            address,
385            frequency_hz,
386            oscillator_hz,
387            totem_pole,
388            inverted,
389            change_on_ack,
390        )

Make a driver for the part at address on bus. Nothing is sent until init() or the first channel is loaded.

Parameters
  • bus: The bus the part is on.
  • address: The address its A5 to A0 pins select, 0x40 with all six low.
  • frequency_hz: The PWM frequency every channel shares; 50 for hobby servos.
  • oscillator_hz: The clock the prescaler divides, for a board driving EXTCLK.
  • totem_pole: Totem-pole outputs when true, open-drain when false.
  • inverted: Invert the output logic, for a board with no external driver.
  • change_on_ack: Change the outputs on each write's acknowledge, not the stop.
def init(self) -> None:
392    def init(self) -> None:
393        """Program the prescale and the output wiring, wake the oscillator, and restart.
394
395        :raises PamojaError: If the bus fails.
396        """
397        self._native.init()

Program the prescale and the output wiring, wake the oscillator, and restart.

Raises
  • PamojaError: If the bus fails.
def set_channel(self, channel: int, setting: bytes) -> None:
399    def set_channel(self, channel: int, setting: bytes) -> None:
400        """Load one channel, initializing the part first if :meth:`init` has not run.
401
402        :param channel: The output, 0 to 15.
403        :param setting: The four register bytes the ``pwm`` builders make.
404        :raises ValueError: For a channel past 15, or a setting that is not four bytes.
405        :raises PamojaError: If the bus fails.
406        """
407        self._native.set_channel(channel, bytes(setting))

Load one channel, initializing the part first if init() has not run.

Parameters
  • channel: The output, 0 to 15.
  • setting: The four register bytes the pwm builders make.
Raises
  • ValueError: For a channel past 15, or a setting that is not four bytes.
  • PamojaError: If the bus fails.
def channel(self, channel: int) -> bytes:
409    def channel(self, channel: int) -> bytes:
410        """Read one channel's four register bytes back from the part.
411
412        The registers are read one at a time, so the read works whether or not MODE1's
413        auto-increment bit is set, and it changes nothing on the part: a program that
414        restarts can ask a running part what it holds. ``pwm.counts`` names the counts.
415
416        :param channel: The output, 0 to 15.
417        :returns: The four register bytes the channel holds.
418        :raises ValueError: For a channel past 15.
419        :raises PamojaError: If the bus fails.
420        """
421        return bytes(self._native.channel(channel))

Read one channel's four register bytes back from the part.

The registers are read one at a time, so the read works whether or not MODE1's auto-increment bit is set, and it changes nothing on the part: a program that restarts can ask a running part what it holds. pwm.counts names the counts.

Parameters
  • channel: The output, 0 to 15. :returns: The four register bytes the channel holds.
Raises
  • ValueError: For a channel past 15.
  • PamojaError: If the bus fails.
def set_all(self, setting: bytes) -> None:
423    def set_all(self, setting: bytes) -> None:
424        """Load every channel with the same four bytes in one transfer.
425
426        :param setting: The four register bytes the ``pwm`` builders make.
427        :raises PamojaError: If the bus fails.
428        """
429        self._native.set_all(bytes(setting))

Load every channel with the same four bytes in one transfer.

Parameters
  • setting: The four register bytes the pwm builders make.
Raises
  • PamojaError: If the bus fails.
def sleep(self) -> None:
431    def sleep(self) -> None:
432        """Stop the oscillator; the channels keep their settings.
433
434        :raises PamojaError: If the bus fails.
435        """
436        self._native.sleep()

Stop the oscillator; the channels keep their settings.

Raises
  • PamojaError: If the bus fails.
def wake(self) -> None:
438    def wake(self) -> None:
439        """Wake the oscillator, wait the 500 us it needs, and restart the channels.
440
441        :raises PamojaError: If the bus fails.
442        """
443        self._native.wake()

Wake the oscillator, wait the 500 us it needs, and restart the channels.

Raises
  • PamojaError: If the bus fails.
def software_reset(self) -> None:
445    def software_reset(self) -> None:
446        """Send the general-call software reset to every PCA9685 on the bus.
447
448        It goes to address ``0x00``, which nothing on a simulated bus answers.
449
450        :raises PamojaError: If the bus fails, or nothing acknowledges the general call.
451        """
452        self._native.software_reset()

Send the general-call software reset to every PCA9685 on the bus.

It goes to address 0x00, which nothing on a simulated bus answers.

Raises
  • PamojaError: If the bus fails, or nothing acknowledges the general call.
prescale: int
454    @property
455    def prescale(self) -> int:
456        """The prescale value the driver writes for its frequency."""
457        return self._native.prescale

The prescale value the driver writes for its frequency.

frequency: float
459    @property
460    def frequency(self) -> float:
461        """The frequency the part runs at once the prescaler has rounded, in hertz."""
462        return self._native.frequency

The frequency the part runs at once the prescaler has rounded, in hertz.

class Pwm(typing.NamedTuple):
334class Pwm(NamedTuple):
335    """When in a period a PWM output goes high and low, in counts."""
336
337    on: int
338    """The count at which the output goes high."""
339
340    off: int
341    """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 StepDir(typing.Generic[~_Step, ~_Dir]):
232class StepDir(Generic[_Step, _Dir]):
233    """A stepper behind a step and direction driver chip such as an A4988 or a DRV8825,
234    as ``pamoja_actuators::stepper::StepDir`` drives one in Rust.
235
236    Each step sets the direction line, high for forward, and holds it for the pulse
237    width, since the chip reads the direction on the step line's rising edge and needs it
238    settled first. Then it pulses the step line high for the pulse width, brings it low,
239    and waits the step interval. Microstepping, current limiting, and enable are the
240    chip's own pins and settings, outside this driver.
241
242    >>> from pamoja.gpio import PinScript
243    >>> from pamoja.hal import DelayLog
244    >>> motor = StepDir(PinScript(), PinScript(), delay=DelayLog())
245    >>> motor.steps(-1)
246    >>> motor.position
247    -1
248    """
249
250    __slots__ = (
251        "_delay",
252        "_direction",
253        "_position",
254        "_pulse_micros",
255        "_step",
256        "_step_micros",
257    )
258
259    def __init__(
260        self,
261        step: _Step,
262        direction: _Dir,
263        *,
264        pulse_micros: int = _DEFAULT_PULSE_MICROS,
265        step_micros: int = _DEFAULT_STEP_MICROS,
266        delay: Optional[Delay] = None,
267    ) -> None:
268        """Wrap the step and direction lines, without driving them.
269
270        :param step: The line the chip counts rising edges on.
271        :param direction: The line the chip reads the direction from; high is forward.
272        :param pulse_micros: How long the direction line is held before the step line
273            rises, and the step line is then held high.
274        :param step_micros: The pause after each step, which sets the speed.
275        :param delay: What waits: a :class:`~pamoja.hal.SleepDelay` unless given, or a
276            :class:`~pamoja.hal.DelayLog` to run with nothing plugged in.
277        """
278        self._step = step
279        self._direction = direction
280        self._position = 0
281        self._pulse_micros = pulse_micros
282        self._step_micros = step_micros
283        self._delay: Delay = SleepDelay() if delay is None else delay
284
285    @property
286    def position(self) -> int:
287        """The step count since the driver was built, forward positive."""
288        return self._position
289
290    @property
291    def pulse_micros(self) -> int:
292        """How long the direction is held before each step pulse, and the pulse itself, in
293        microseconds."""
294        return self._pulse_micros
295
296    @property
297    def step_micros(self) -> int:
298        """The pause after each step, in microseconds."""
299        return self._step_micros
300
301    def step(self, direction: Direction) -> None:
302        """Take one step and wait the step interval.
303
304        :param direction: Which way to step.
305        :raises Exception: Whatever a line raises when it cannot be driven.
306        """
307        forward = Direction(direction) is Direction.FORWARD
308        self._direction.drive(Level.HIGH if forward else Level.LOW)
309        self._delay.delay_micros(self._pulse_micros)
310        self._step.drive(Level.HIGH)
311        self._delay.delay_micros(self._pulse_micros)
312        self._step.drive(Level.LOW)
313        self._position += 1 if forward else -1
314        self._delay.delay_micros(self._step_micros)
315
316    def steps(self, count: int) -> None:
317        """Take ``count`` steps, backward when negative.
318
319        :param count: The signed number of steps.
320        :raises Exception: Whatever a line raises; the steps already taken stay counted.
321        """
322        direction = _direction_of(count)
323        for _ in range(abs(count)):
324            self.step(direction)
325
326    def release(self) -> Tuple[_Step, _Dir]:
327        """Hand the lines back.
328
329        :returns: The step line, then the direction line.
330        """
331        return self._step, self._direction

A stepper behind a step and direction driver chip such as an A4988 or a DRV8825, as pamoja_actuators::stepper::StepDir drives one in Rust.

Each step sets the direction line, high for forward, and holds it for the pulse width, since the chip reads the direction on the step line's rising edge and needs it settled first. Then it pulses the step line high for the pulse width, brings it low, and waits the step interval. Microstepping, current limiting, and enable are the chip's own pins and settings, outside this driver.

>>> from pamoja.gpio import PinScript
>>> from pamoja.hal import DelayLog
>>> motor = StepDir(PinScript(), PinScript(), delay=DelayLog())
>>> motor.steps(-1)
>>> motor.position
-1
StepDir( step: ~_Step, direction: ~_Dir, *, pulse_micros: int = 10, step_micros: int = 2000, delay: Optional[pamoja.hal.Delay] = None)
259    def __init__(
260        self,
261        step: _Step,
262        direction: _Dir,
263        *,
264        pulse_micros: int = _DEFAULT_PULSE_MICROS,
265        step_micros: int = _DEFAULT_STEP_MICROS,
266        delay: Optional[Delay] = None,
267    ) -> None:
268        """Wrap the step and direction lines, without driving them.
269
270        :param step: The line the chip counts rising edges on.
271        :param direction: The line the chip reads the direction from; high is forward.
272        :param pulse_micros: How long the direction line is held before the step line
273            rises, and the step line is then held high.
274        :param step_micros: The pause after each step, which sets the speed.
275        :param delay: What waits: a :class:`~pamoja.hal.SleepDelay` unless given, or a
276            :class:`~pamoja.hal.DelayLog` to run with nothing plugged in.
277        """
278        self._step = step
279        self._direction = direction
280        self._position = 0
281        self._pulse_micros = pulse_micros
282        self._step_micros = step_micros
283        self._delay: Delay = SleepDelay() if delay is None else delay

Wrap the step and direction lines, without driving them.

Parameters
  • step: The line the chip counts rising edges on.
  • direction: The line the chip reads the direction from; high is forward.
  • pulse_micros: How long the direction line is held before the step line rises, and the step line is then held high.
  • step_micros: The pause after each step, which sets the speed.
  • delay: What waits: a ~pamoja.hal.SleepDelay unless given, or a ~pamoja.hal.DelayLog to run with nothing plugged in.
position: int
285    @property
286    def position(self) -> int:
287        """The step count since the driver was built, forward positive."""
288        return self._position

The step count since the driver was built, forward positive.

pulse_micros: int
290    @property
291    def pulse_micros(self) -> int:
292        """How long the direction is held before each step pulse, and the pulse itself, in
293        microseconds."""
294        return self._pulse_micros

How long the direction is held before each step pulse, and the pulse itself, in microseconds.

step_micros: int
296    @property
297    def step_micros(self) -> int:
298        """The pause after each step, in microseconds."""
299        return self._step_micros

The pause after each step, in microseconds.

def step(self, direction: Direction) -> None:
301    def step(self, direction: Direction) -> None:
302        """Take one step and wait the step interval.
303
304        :param direction: Which way to step.
305        :raises Exception: Whatever a line raises when it cannot be driven.
306        """
307        forward = Direction(direction) is Direction.FORWARD
308        self._direction.drive(Level.HIGH if forward else Level.LOW)
309        self._delay.delay_micros(self._pulse_micros)
310        self._step.drive(Level.HIGH)
311        self._delay.delay_micros(self._pulse_micros)
312        self._step.drive(Level.LOW)
313        self._position += 1 if forward else -1
314        self._delay.delay_micros(self._step_micros)

Take one step and wait the step interval.

Parameters
  • direction: Which way to step.
Raises
  • Exception: Whatever a line raises when it cannot be driven.
def steps(self, count: int) -> None:
316    def steps(self, count: int) -> None:
317        """Take ``count`` steps, backward when negative.
318
319        :param count: The signed number of steps.
320        :raises Exception: Whatever a line raises; the steps already taken stay counted.
321        """
322        direction = _direction_of(count)
323        for _ in range(abs(count)):
324            self.step(direction)

Take count steps, backward when negative.

Parameters
  • count: The signed number of steps.
Raises
  • Exception: Whatever a line raises; the steps already taken stay counted.
def release(self) -> Tuple[~_Step, ~_Dir]:
326    def release(self) -> Tuple[_Step, _Dir]:
327        """Hand the lines back.
328
329        :returns: The step line, then the direction line.
330        """
331        return self._step, self._direction

Hand the lines back.

:returns: The step line, then the direction line.

class Stepper:
 83class Stepper:
 84    """A stepper motor's place in its drive sequence, and how far it has turned.
 85
 86    Example::
 87
 88        motor = Stepper(Drive.HALF_STEP)
 89        coils = motor.step(Direction.FORWARD)
 90    """
 91
 92    __slots__ = ("_native",)
 93
 94    def __init__(self, drive: Drive) -> None:
 95        """Create a stepper at the start of a pattern, with its position at zero.
 96
 97        :param drive: The coil pattern to walk.
 98        """
 99        self._native = _NativeStepper(Drive(drive).value)
100
101    def step(self, direction: Direction) -> int:
102        """Advance one step and return the four-bit coil pattern to apply.
103
104        The most significant of the four bits is the first coil.
105
106        :param direction: Which way to turn.
107        :returns: The coil pattern.
108        """
109        return self._native.step(Direction(direction).value)
110
111    @property
112    def coils(self) -> int:
113        """The coil pattern currently held, without advancing."""
114        return self._native.coils
115
116    @property
117    def steps(self) -> int:
118        """How many steps have been taken, signed by direction."""
119        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)
94    def __init__(self, drive: Drive) -> None:
95        """Create a stepper at the start of a pattern, with its position at zero.
96
97        :param drive: The coil pattern to walk.
98        """
99        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:
101    def step(self, direction: Direction) -> int:
102        """Advance one step and return the four-bit coil pattern to apply.
103
104        The most significant of the four bits is the first coil.
105
106        :param direction: Which way to turn.
107        :returns: The coil pattern.
108        """
109        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
111    @property
112    def coils(self) -> int:
113        """The coil pattern currently held, without advancing."""
114        return self._native.coils

The coil pattern currently held, without advancing.

steps: int
116    @property
117    def steps(self) -> int:
118        """How many steps have been taken, signed by direction."""
119        return self._native.steps

How many steps have been taken, signed by direction.

pca9685 = <pamoja.actuators._Pca9685 object>
pwm = <pamoja.actuators._Pwm object>
stepper = <pamoja.actuators._StepperTiming object>
def steps_for_degrees(degrees: float, steps_per_revolution: int) -> int:
627def steps_for_degrees(degrees: float, steps_per_revolution: int) -> int:
628    """Return how many steps a rotation of ``degrees`` takes on a given motor.
629
630    :param degrees: The angle to turn through.
631    :param steps_per_revolution: The motor's steps per full revolution.
632    :returns: The step count, negative for a negative angle.
633    """
634    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.