pamoja-actuators
A PCA9685 driver for servos, LEDs, and valves, and stepper drivers for four coil lines or a step and direction chip, in every language. One capability of pamoja, one memory-safe Rust core with bindings for TypeScript, Python, and C#.
Install
pip install pamoja-actuators
from pamoja import actuators
This pulls in pamoja-native, the compiled engine, and pamoja-gpio and pamoja-hal. pip install pamoja is the whole framework in one package.
Example
The script the test suite runs, spliced here as it ran.
From bindings/python/guides/actuators.py:
from pamoja.actuators import Drive, FourWire, Pca9685, StepDir, pca9685, pwm, steps_for_degrees
from pamoja.gpio import Level, PinScript
from pamoja.hal import DelayLog, I2cBus
# Where the rig's parts connect: the PCA9685 answers at 0x40 with its six address pins low, the
# tilt servo is on its channel 0, and the status LED on channel 15.
ADDRESS = pca9685.DEFAULT_ADDRESS
TILT = 0
STATUS = 15
GLOW = pwm.duty(pca9685.COUNTS // 16)
# The rig with nothing plugged in: a PCA9685 that powers up and keeps its datasheet's rules the
# way the part does. On a Raspberry Pi the bus is I2cBus.open("/dev/i2c-1") and nothing after
# this statement changes.
bus = I2cBus.simulated([pca9685.sim.part(ADDRESS)])
# A hobby servo wants a pulse every 20 ms, 50 Hz. The driver works the prescale out from that and
# writes it with the oscillator asleep, since only then does the part take it, then wakes the
# oscillator and waits the 500 us it needs to settle.
controller = Pca9685(bus, ADDRESS, frequency_hz=50)
controller.init()
print(
f"controller prescale {controller.prescale} for {controller.frequency:.1f} Hz, "
f"awake after {bus.waited_micros} us"
)
# A servo turns to the width of the pulse it is sent, and this one points the camera a little
# below level at 1300 us. The LED glows at a sixteenth of full brightness while the rig waits.
# Reading the channels back shows what the part now holds.
controller.set_channel(TILT, pwm.servo(1_300, 50))
controller.set_channel(STATUS, GLOW)
tilt = pwm.counts(controller.channel(TILT))
status = pwm.counts(controller.channel(STATUS))
print(f"tilt 1300 us pulse, low at count {tilt.off} of {pca9685.COUNTS}")
print(f"status glowing, high for {status.off} of {pca9685.COUNTS} counts")
# The slider: a 1.8-degree motor, 200 steps a turn, behind an A4988 with MS1 to MS3 high, which
# splits each step into sixteen. A 20-tooth GT2 pulley pulls 40 mm of belt a turn, so 5 mm
# between frames is an eighth of a turn.
SLIDER_STEPS_PER_TURN = 200 * 16
BELT_MM_PER_TURN = 40.0
slide = steps_for_degrees(360.0 * 5.0 / BELT_MM_PER_TURN, SLIDER_STEPS_PER_TURN)
slider_delay = DelayLog()
slider = StepDir(PinScript(), PinScript(), step_micros=500, delay=slider_delay)
# The pan head: a 28BYJ-48 through a ULN2003, half-stepped, 4096 half-steps a turn through its
# gearbox. With a camera on it, it steps every 4 ms, half the 500 Hz its pack is rated to start
# at with no load.
PAN_STEPS_PER_TURN = 4096
pan_step = steps_for_degrees(2.0, PAN_STEPS_PER_TURN)
coils = (PinScript(), PinScript(), PinScript(), PinScript())
pan_delay = DelayLog()
pan = FourWire(coils, Drive.HALF_STEP, step_micros=4_000, delay=pan_delay)
# Four frames. The LED lights for each exposure, and between frames the rig slides and pans
# while it glows. The stepper lines record every level, and the delays count every wait without
# sleeping through it.
for frame in range(1, 5):
controller.set_channel(STATUS, pwm.full_on())
mm = slider.position * BELT_MM_PER_TURN / SLIDER_STEPS_PER_TURN
degrees = pan.position * 360.0 / PAN_STEPS_PER_TURN
print(f"frame {frame} slider {mm:.1f} mm, pan {degrees:.2f} degrees")
controller.set_channel(STATUS, GLOW)
if frame < 4:
slider.steps(slide)
pan.steps(pan_step)
# A four-wire motor draws current for as long as its coils hold, so the pan head drops them once
# the shoot is over.
pan.idle()
step_line, direction_line = slider.release()
a, b, c, d = pan.release()
pulses = step_line.driven.count(Level.HIGH)
print(f"slider {pulses} pulses on STEP, DIR {direction_line.level.value}")
levels = " ".join(line.level.value for line in (a, b, c, d))
print(f"pan head {pan.position} half-steps, coils {levels}")
print(f"moving slider {slider_delay.total_millis} ms, pan head {pan_delay.total_millis} ms")
# The part takes a new prescale only while its oscillator sleeps. Written while it runs, as a
# driver that skipped the sleep would write it, the value is dropped and the servos stay at
# 50 Hz.
fast = pca9685.prescale_for_frequency(1_000)
bus.write(ADDRESS, bytes([pca9685.REGISTER_PRE_SCALE, fast]))
held = bus.part(ADDRESS)
print(f"prescale written while awake, still {held.register(pca9685.REGISTER_PRE_SCALE)}")
# A channel the part does not have is refused before anything reaches the bus.
try:
controller.set_channel(16, pwm.full_on())
print("channel 16 accepted, which should never happen")
except ValueError as error:
print(f"channel 16 {error}")
# The shoot is over: every channel off in one transfer through the ALL_LED registers, then the
# oscillator asleep. The part keeps its registers while it sleeps.
controller.set_all(pwm.full_off())
controller.sleep()
parked = bus.part(ADDRESS)
all_off = all(controller.channel(channel) == pwm.full_off() for channel in (TILT, STATUS))
asleep = parked.register(pca9685.REGISTER_MODE1) & pca9685.MODE1_SLEEP != 0
print(
f"parked every channel {'off' if all_off else 'still on'}, "
f"oscillator {'asleep' if asleep else 'running'}"
)
The same capability in every language
| Language | Package | Reference |
|---|---|---|
| Rust | pamoja-actuators |
reference, docs.rs, install |
| TypeScript | @pamoja/actuators |
reference, install |
| Python | pamoja-actuators |
reference, install |
| C# | Pamoja.Actuators |
reference, install |
Documentation
pamoja.actuatorsreference, every class and function in this module.- The Actuator drivers guide, with the same example in Rust, TypeScript, and C#.
- Every capability, and the install page.
License
MIT
Release files for pamoja-actuators 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pamoja_actuators-0.2.0.tar.gz | 10.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pamoja_actuators-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 22.3 kB
Release files / pamoja_actuators-0.2.0.tar.gz
| Download URL | pamoja_actuators-0.2.0.tar.gz |
|---|---|
| Size | 10.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
063ce3fee143fa994218af8c70dc1c64e2d5684ee9a82565787bb6ea6a4a21f3
|
|
BLAKE2b-256 checksum How to use checksums |
b8adeb69e15c3de0f3901c59201207b7014f92f53d3180fbe98af2c530dca4f4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.15
|
Release files / pamoja_actuators-0.2.0-py3-none-any.whl
| Download URL | pamoja_actuators-0.2.0-py3-none-any.whl |
|---|---|
| Size | 11.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5f81b9e4919c48bff3116f1da019271907031e128e98e9cb4eef5661a32af8a7
|
|
BLAKE2b-256 checksum How to use checksums |
f2971ebe8a17e54432d7414f40c2322a9118a1bb4272741a0bbbb23ade6b1757
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.15
|