Skip to content

Check fraction range; add post-constructor access to actuation_range and pulse widths. #10

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jul 3, 2018
Merged
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
20 changes: 7 additions & 13 deletions adafruit_motor/motor.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,27 +51,21 @@ class DCMotor:
def __init__(self, positive_pwm, negative_pwm):
self._positive = positive_pwm
self._negative = negative_pwm
self._throttle = None

@property
def throttle(self):
"""How much power is being delivered to the motor. Values range from ``-1.0`` (full
throttle reverse) to ``1.0`` (full throttle forwards.) ``0`` will stop the motor from
spinning and ``None`` will let the motor spin freely."""
if self._positive.duty_cycle == 0 and self._negative.duty_cycle == 0:
return None
if self._positive.duty_cycle == 0xffff and self._negative.duty_cycle == 0xffff:
return float(0)
if self._positive.duty_cycle > 0 and self._negative.duty_cycle > 0:
raise RuntimeError("PWMs in invalid state")
value = max(self._positive.duty_cycle, self._negative.duty_cycle) / 0xffff
if self._negative.duty_cycle > 0:
return -1 * value
return value
"""Motor speed, ranging from -1.0 (full speed reverse) to 1.0 (full speed forward),
or ``None``.
If ``None``, both PWMs are turned full off. If ``0.0``, both PWMs are turned full on.
"""
return self._throttle

@throttle.setter
def throttle(self, value):
if value is not None and (value > 1.0 or value < -1.0):
raise ValueError("Throttle must be None or between -1.0 and 1.0")
self._throttle = value
if value is None:
self._positive.duty_cycle = 0
self._negative.duty_cycle = 0
Expand Down
30 changes: 22 additions & 8 deletions adafruit_motor/servo.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,14 @@ class _BaseServo: # pylint: disable-msg=too-few-public-methods
:param int min_pulse: The minimum pulse length of the servo in microseconds.
:param int max_pulse: The maximum pulse length of the servo in microseconds."""
def __init__(self, pwm_out, *, min_pulse=750, max_pulse=2250):
self._min_duty = int((min_pulse * pwm_out.frequency) / 1000000 * 0xffff)
max_duty = (max_pulse * pwm_out.frequency) / 1000000 * 0xffff
self._duty_range = int(max_duty - self._min_duty)
self._pwm_out = pwm_out
self.set_pulse_width_range(min_pulse, max_pulse)

def set_pulse_width_range(self, min_pulse=750, max_pulse=2250):
"""Change min and max pulse widths."""
self._min_duty = int((min_pulse * self._pwm_out.frequency) / 1000000 * 0xffff)
max_duty = (max_pulse * self._pwm_out.frequency) / 1000000 * 0xffff
self._duty_range = int(max_duty - self._min_duty)

@property
def fraction(self):
Expand All @@ -56,6 +60,8 @@ def fraction(self):

@fraction.setter
def fraction(self, value):
if not 0.0 <= value <= 1.0:
raise ValueError("Must be 0.0 to 1.0")
duty_cycle = self._min_duty + int(value * self._duty_range)
self._pwm_out.duty_cycle = duty_cycle

Expand All @@ -68,6 +74,13 @@ class Servo(_BaseServo):
:param int min_pulse: The minimum pulse width of the servo in microseconds.
:param int max_pulse: The maximum pulse width of the servo in microseconds.

``actuation_range`` is an exposed property and can be changed at any time:

.. code-block:: python

servo = Servo(pwm)
servo.actuation_range = 135

The specified pulse width range of a servo has historically been 1000-2000us,
for a 90 degree range of motion. But nearly all modern servos have a 170-180
degree range, and the pulse widths can go well out of the range to achieve this
Expand All @@ -80,22 +93,23 @@ class Servo(_BaseServo):
get a wider range of movement. But if you go too low or too high,
the servo mechanism may hit the end stops, buzz, and draw extra current as it stalls.
Test carefully to find the safe minimum and maximum.
"""
"""
def __init__(self, pwm_out, *, actuation_range=180, min_pulse=750, max_pulse=2250):
super().__init__(pwm_out, min_pulse=min_pulse, max_pulse=max_pulse)
self._actuation_range = actuation_range
self.actuation_range = actuation_range
Copy link
Member

Choose a reason for hiding this comment

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

Add a triple quoted doc string here so it is included in RTD.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since the Servo class has a docstring, it's already in RTD. See https://circuitpython.readthedocs.io/projects/motor/en/latest/api.html#adafruit_motor.servo.Servo.

Alphabetically ContinuousServo came first -- maybe that was confusing?

Copy link
Member

Choose a reason for hiding this comment

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

It does for the argument but not the object attribute (like angle).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

Right. actuation_angle should look like angle too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It was impressively difficult to do this. I can add a docstring after the self.actuation_range = actuation_range, but then it shows up in the RTD as "actuation_range = None". After some Googling, found a hack to add to conf.py to fix it.

The other alternative is to document actuation_range as an :ivar, but then it shows up with the parameters. The current new way looks better.

"""The physical range of motion of the servo in degrees."""
self._pwm = pwm_out

@property
def angle(self):
"""The servo angle in degrees. Must be in the range ``0`` to ``actuation_range``."""
return self._actuation_range * self.fraction
return self.actuation_range * self.fraction

@angle.setter
def angle(self, new_angle):
if new_angle < 0 or new_angle > self._actuation_range:
if new_angle < 0 or new_angle > self.actuation_range:
raise ValueError("Angle out of range")
self.fraction = new_angle / self._actuation_range
self.fraction = new_angle / self.actuation_range

class ContinuousServo(_BaseServo):
"""Control a continuous rotation servo.
Expand Down
10 changes: 10 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,16 @@
# If this is True, todo emits a warning for each TODO entries. The default is False.
todo_emit_warnings = True

# Avoid an `= None` on instance attributes with their own doc strings.
# Workaround from here: https://github.com/sphinx-doc/sphinx/issues/2044#issuecomment-285888160
#
from sphinx.ext.autodoc import (
ClassLevelDocumenter, InstanceAttributeDocumenter)

def iad_add_directive_header(self, sig):
ClassLevelDocumenter.add_directive_header(self, sig)

InstanceAttributeDocumenter.add_directive_header = iad_add_directive_header

# -- Options for HTML output ----------------------------------------------

Expand Down