Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 68 additions & 17 deletions gpiozero/output_devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@
from itertools import repeat, cycle, chain
from colorzero import Color
from collections import OrderedDict
from enum import Enum, auto
from math import log2

from .exc import (
OutputDeviceBadValue,
OutputDeviceError,
GPIOPinMissing,
PWMSoftwareFallback,
DeviceClosed,
Expand Down Expand Up @@ -1185,24 +1187,64 @@ class Motor(SourceMixin, CompositeDevice):
:class:`DigitalOutputDevice` instances, allowing only direction
control.

:param bool enable_is_pwm:
If :data:`True`, construct a :class:`PWMOutputDevice` instance for
the enable pin, allowing variable speed control using the enable pin
and not the forward and backward pins. Additionally, if this is
specified, *enable* must not be :data:`None` and *pwm* must be
:data:`False`. The default is :data:`False`.

:type pin_factory: Factory or None
:param pin_factory:
See :doc:`api_pins` for more information (this is an advanced feature
which most users can ignore).
"""

class ControlType(Enum):
DIGITAL = auto()
PWM = auto()
DIGITAL_WITH_DIGITAL_ENABLE = auto()
PWM_WITH_DIGITAL_ENABLE = auto()
DIGITAL_WITH_PWM_ENABLE = auto()

@property
def has_pwm(self):
"""
Returns :data:`True` if the control type involves PWM.
"""
return "PWM" in self.name

@property
def has_enable(self):
"""
Returns :data:`True` if the control type involves an enable pin.
"""
return "ENABLE" in self.name

def __init__(self, forward, backward, *, enable=None, pwm=True,
pin_factory=None):
enable_is_pwm=False, pin_factory=None):
if pwm and enable_is_pwm:
raise OutputDeviceError("Can't specify both PWM direction and enable pins")
if enable_is_pwm and enable is None:
raise OutputDeviceError("PWM enable requires a valid enable pin")
PinClass = PWMOutputDevice if pwm else DigitalOutputDevice
devices = OrderedDict((
('forward_device', PinClass(forward, pin_factory=pin_factory)),
('backward_device', PinClass(backward, pin_factory=pin_factory)),
))
if enable is not None:
devices['enable_device'] = DigitalOutputDevice(
enable,
initial_value=True,
pin_factory=pin_factory
)
if not enable_is_pwm:
devices['enable_device'] = DigitalOutputDevice(
enable,
initial_value=True,
pin_factory=pin_factory
)
self.control_type = self.ControlType.PWM_WITH_DIGITAL_ENABLE if pwm else self.ControlType.DIGITAL_WITH_DIGITAL_ENABLE
else:
devices['enable_device'] = PWMOutputDevice(enable, pin_factory=pin_factory)
self.control_type = self.ControlType.DIGITAL_WITH_PWM_ENABLE
else:
self.control_type = self.ControlType.PWM if pwm else self.ControlType.DIGITAL
super().__init__(_order=devices.keys(), pin_factory=pin_factory, **devices)

@property
Expand All @@ -1212,6 +1254,8 @@ def value(self):
(full speed backward) and 1 (full speed forward), with 0 representing
stopped.
"""
if self.control_type.has_enable:
return (self.forward_device.value - self.backward_device.value) * self.enable_device.value
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hah, clever 😉

return self.forward_device.value - self.backward_device.value

@value.setter
Expand Down Expand Up @@ -1245,37 +1289,43 @@ def forward(self, speed=1):

:param float speed:
The speed at which the motor should turn. Can be any value between
0 (stopped) and the default 1 (maximum speed) if *pwm* was
:data:`True` when the class was constructed (and only 0 or 1 if
not).
0 (stopped) and the default 1 (maximum speed) if *pwm* or *enable_is_pwm*
was :data:`True` when the class was constructed (and only 0 or 1 if not).
"""
if not 0 <= speed <= 1:
raise ValueError('forward speed must be between 0 and 1')
if isinstance(self.forward_device, DigitalOutputDevice):
if not self.control_type.has_pwm:
if speed not in (0, 1):
raise ValueError(
'forward speed must be 0 or 1 with non-PWM Motors')
self.backward_device.off()
self.forward_device.value = speed
if self.control_type == self.ControlType.DIGITAL_WITH_PWM_ENABLE:
self.enable_device.value = speed
self.forward_device.on()
else:
self.forward_device.value = speed

def backward(self, speed=1):
"""
Drive the motor backwards.

:param float speed:
The speed at which the motor should turn. Can be any value between
0 (stopped) and the default 1 (maximum speed) if *pwm* was
:data:`True` when the class was constructed (and only 0 or 1 if
not).
0 (stopped) and the default 1 (maximum speed) if *pwm* or *enable_is_pwm*
was :data:`True` when the class was constructed (and only 0 or 1 if not).
"""
if not 0 <= speed <= 1:
raise ValueError('backward speed must be between 0 and 1')
if isinstance(self.backward_device, DigitalOutputDevice):
if not self.has_pwm:
if speed not in (0, 1):
raise ValueError(
'backward speed must be 0 or 1 with non-PWM Motors')
self.forward_device.off()
self.backward_device.value = speed
if self.control_type == self.ControlType.DIGITAL_WITH_PWM_ENABLE:
self.enable_device.value = speed
self.backward_device.on()
else:
self.backward_device.value = speed

def reverse(self):
"""
Expand All @@ -1291,7 +1341,8 @@ def stop(self):
"""
self.forward_device.off()
self.backward_device.off()

if self.control_type == self.ControlType.DIGITAL_WITH_PWM_ENABLE:
self.enable_device.off()

class PhaseEnableMotor(SourceMixin, CompositeDevice):
"""
Expand Down
90 changes: 90 additions & 0 deletions tests/test_outputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -975,6 +975,19 @@ def test_motor_pins_nonpwm(mock_factory):
assert motor.backward_device.pin is b
assert isinstance(motor.backward_device, DigitalOutputDevice)

def test_motor_pins_enable_pwm(mock_factory, pwm):
s = mock_factory.pin(1)
f = mock_factory.pin(2)
b = mock_factory.pin(3)
with Motor(2, 3, enable=1, pwm=False, enable_is_pwm=True) as motor:
assert repr(motor).startswith('<gpiozero.Motor object')
assert motor.enable_device.pin is s
assert isinstance(motor.enable_device, PWMOutputDevice)
assert motor.forward_device.pin is f
assert isinstance(motor.forward_device, DigitalOutputDevice)
assert motor.backward_device.pin is b
assert isinstance(motor.backward_device, DigitalOutputDevice)

def test_motor_close(mock_factory, pwm):
f = mock_factory.pin(1)
b = mock_factory.pin(2)
Expand All @@ -995,6 +1008,19 @@ def test_motor_close_nonpwm(mock_factory):
assert motor.forward_device.pin is None
assert motor.backward_device.pin is None

def test_motor_close_enable_pwm(mock_factory, pwm):
s = mock_factory.pin(1)
f = mock_factory.pin(2)
b = mock_factory.pin(3)
with Motor(2, 3, enable=1, pwm=False, enable_is_pwm=True) as motor:
motor.close()
assert motor.closed
assert motor.enable_device.pin is None
assert motor.forward_device.pin is None
assert motor.backward_device.pin is None
motor.close()
assert motor.closed

def test_motor_value(mock_factory, pwm):
f = mock_factory.pin(1)
b = mock_factory.pin(2)
Expand Down Expand Up @@ -1037,6 +1063,32 @@ def test_motor_value_nonpwm(mock_factory):
assert not motor.value
assert b.state == 0 and f.state == 0

def test_motor_value_enable_pwm(mock_factory, pwm):
s = mock_factory.pin(1)
f = mock_factory.pin(2)
b = mock_factory.pin(3)
with Motor(2, 3, enable=1, pwm=False, enable_is_pwm=True) as motor:
motor.value = -1
assert motor.is_active
assert motor.value == -1
assert b.state == 1 and f.state == 0 and s.state == 1
motor.value = 1
assert motor.is_active
assert motor.value == 1
assert b.state == 0 and f.state == 1 and s.state == 1
motor.value = 0.5
assert motor.is_active
assert motor.value == 0.5
assert b.state == 0 and f.state == 1 and s.state == 0.5
motor.value = -0.5
assert motor.is_active
assert motor.value == -0.5
assert b.state == 1 and f.state == 0 and s.state == 0.5
motor.value = 0
assert not motor.is_active
assert not motor.value
assert b.state == 0 and f.state == 0 and s.state == 0

def test_motor_bad_value(mock_factory, pwm):
f = mock_factory.pin(1)
b = mock_factory.pin(2)
Expand Down Expand Up @@ -1071,6 +1123,20 @@ def test_motor_bad_value_nonpwm(mock_factory):
with pytest.raises(ValueError):
motor.backward(0.5)

def test_motor_bad_value_enable_pwm(mock_factory, pwm):
s = mock_factory.pin(1)
f = mock_factory.pin(2)
b = mock_factory.pin(3)
with Motor(2, 3, enable=1, pwm=False, enable_is_pwm=True) as motor:
with pytest.raises(ValueError):
motor.value = -2
with pytest.raises(ValueError):
motor.value = 2
with pytest.raises(ValueError):
motor.forward(2)
with pytest.raises(ValueError):
motor.backward(2)

def test_motor_reverse(mock_factory, pwm):
f = mock_factory.pin(1)
b = mock_factory.pin(2)
Expand Down Expand Up @@ -1099,6 +1165,24 @@ def test_motor_reverse_nonpwm(mock_factory):
assert motor.value == -1
assert b.state == 1 and f.state == 0

def test_motor_reverse_enable_pwm(mock_factory, pwm):
s = mock_factory.pin(1)
f = mock_factory.pin(2)
b = mock_factory.pin(3)
with Motor(2, 3, enable=1, pwm=False, enable_is_pwm=True) as motor:
motor.forward()
assert motor.value == 1
assert b.state == 0 and f.state == 1 and s.state == 1
motor.reverse()
assert motor.value == -1
assert b.state == 1 and f.state == 0 and s.state == 1
motor.backward(0.5)
assert motor.value == -0.5
assert b.state == 1 and f.state == 0 and s.state == 0.5
motor.reverse()
assert motor.value == 0.5
assert b.state == 0 and f.state == 1 and s.state == 0.5

def test_motor_enable_pin_init(mock_factory, pwm):
f = mock_factory.pin(1)
b = mock_factory.pin(2)
Expand Down Expand Up @@ -1141,6 +1225,12 @@ def test_motor_enable_pin(mock_factory, pwm):
motor.stop()
assert motor.value == 0

def test_motor_bad_init(mock_factory):
with pytest.raises(OutputDeviceError):
motor = Motor(1, 2, enable_is_pwm=True)
with pytest.raises(OutputDeviceError):
motor = Motor(1, 2, pwm=False, enable_is_pwm=True)

def test_phaseenable_motor_pins(mock_factory, pwm):
p = mock_factory.pin(1)
e = mock_factory.pin(2)
Expand Down